mirror of
https://github.com/plankanban/planka.git
synced 2025-12-12 17:23:14 +03:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca3b0a75d3 | ||
|
|
9eee486356 | ||
|
|
6698437727 | ||
|
|
a3f101a139 | ||
|
|
843b44cea7 | ||
|
|
8d245e8722 | ||
|
|
617246ec40 | ||
|
|
e6ab870827 | ||
|
|
fee35918d1 | ||
|
|
38ec9bd5cc | ||
|
|
54a969ec31 | ||
|
|
d0a8c8b61a | ||
|
|
1d11d9e35c | ||
|
|
2632edb44c | ||
|
|
96956e1268 | ||
|
|
5a3c3bb39b | ||
|
|
1a70b2b7e6 | ||
|
|
71d0815891 | ||
|
|
b700c307c3 | ||
|
|
9794919fd2 | ||
|
|
917dcf31cf | ||
|
|
850f6df0ac | ||
|
|
242d415142 | ||
|
|
950a070589 |
@@ -15,13 +15,13 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.2.14
|
||||
version: 0.2.19
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.23.5"
|
||||
appVersion: "1.24.3"
|
||||
|
||||
dependencies:
|
||||
- alias: postgresql
|
||||
|
||||
@@ -67,6 +67,20 @@ spec:
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if .Values.extraEnv }}
|
||||
{{- range .Values.extraEnv }}
|
||||
- name: {{ .name }}
|
||||
{{- if .value }}
|
||||
value: {{ .value | quote}}
|
||||
{{- end }}
|
||||
{{- if .valueFrom }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .valueFrom.secretName }}
|
||||
key: {{ .valueFrom.key }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if not .Values.postgresql.enabled }}
|
||||
{{- if .Values.existingDburlSecret }}
|
||||
- name: DATABASE_URL
|
||||
@@ -82,7 +96,7 @@ spec:
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: planka-postgresql-svcbind-custom-user
|
||||
name: {{ include "planka.fullname" . }}-postgresql-svcbind-custom-user
|
||||
key: uri
|
||||
{{- end }}
|
||||
- name: BASE_URL
|
||||
|
||||
@@ -69,7 +69,7 @@ ingress:
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
# Used to set planka BASE_URL if no `baseurl` is provided.
|
||||
# Used to set planka BASE_URL if no `baseurl` is provided.
|
||||
- host: planka.local
|
||||
paths:
|
||||
- path: /
|
||||
@@ -197,3 +197,16 @@ oidc:
|
||||
##
|
||||
roles: []
|
||||
# - planka-admin
|
||||
|
||||
## Extra environment variables for planka deployment
|
||||
## Supports hard coded and getting values from a k8s secret
|
||||
## - name: test
|
||||
## value: valuetest
|
||||
## - name: another
|
||||
## value: another
|
||||
## - name: test-secret
|
||||
## valueFrom:
|
||||
## secretName: k8s-secret-name
|
||||
## key: key-inside-the-secret
|
||||
##
|
||||
extraEnv: []
|
||||
|
||||
4856
client/package-lock.json
generated
4856
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -66,15 +66,15 @@
|
||||
"dequal": "^2.0.3",
|
||||
"easymde": "^2.18.0",
|
||||
"history": "^5.3.0",
|
||||
"i18next": "^23.15.1",
|
||||
"i18next": "23.15.2",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"initials": "^3.1.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"linkify-react": "^4.1.3",
|
||||
"linkifyjs": "^4.1.3",
|
||||
"linkify-react": "^4.1.4",
|
||||
"linkifyjs": "^4.1.4",
|
||||
"lodash": "^4.17.21",
|
||||
"nanoid": "^5.0.7",
|
||||
"nanoid": "^5.0.8",
|
||||
"node-sass": "^9.0.0",
|
||||
"photoswipe": "^5.4.4",
|
||||
"prop-types": "^15.8.1",
|
||||
@@ -83,16 +83,16 @@
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-datepicker": "^4.25.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-i18next": "^15.0.2",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-i18next": "^15.1.1",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-photoswipe-gallery": "^2.2.7",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-simplemde-editor": "^5.2.0",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"react-textarea-autosize": "^8.5.5",
|
||||
"redux": "^4.2.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-orm": "^0.16.2",
|
||||
@@ -109,22 +109,22 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@cucumber/cucumber": "^7.3.1",
|
||||
"@cucumber/cucumber": "^7.3.2",
|
||||
"@cucumber/pretty-formatter": "^1.0.1",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"axios": "^1.6.2",
|
||||
"babel-preset-airbnb": "^5.0.0",
|
||||
"chai": "^4.5.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.0",
|
||||
"eslint-plugin-react": "^7.36.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"playwright": "^1.46.1",
|
||||
"playwright": "^1.49.0",
|
||||
"react-test-renderer": "18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
name="description"
|
||||
content="Planka is an open source project management software"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="logo192.png" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
|
||||
@@ -11,6 +11,7 @@ import ListAdd from './ListAdd';
|
||||
import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-icon.svg';
|
||||
|
||||
import styles from './Board.module.scss';
|
||||
import globalStyles from '../../styles.module.scss';
|
||||
|
||||
const parseDndId = (dndId) => dndId.split(':')[1];
|
||||
|
||||
@@ -31,11 +32,14 @@ const Board = React.memo(
|
||||
}, []);
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
document.body.classList.add(globalStyles.dragging);
|
||||
closePopup();
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
({ draggableId, type, source, destination }) => {
|
||||
document.body.classList.remove(globalStyles.dragging);
|
||||
|
||||
if (
|
||||
!destination ||
|
||||
(source.droppableId === destination.droppableId && source.index === destination.index)
|
||||
@@ -72,13 +76,16 @@ const Board = React.memo(
|
||||
}
|
||||
|
||||
prevPosition.current = event.clientX;
|
||||
|
||||
window.getSelection().removeAllRanges();
|
||||
document.body.classList.add(globalStyles.dragScrolling);
|
||||
},
|
||||
[wrapper],
|
||||
);
|
||||
|
||||
const handleWindowMouseMove = useCallback(
|
||||
(event) => {
|
||||
if (!prevPosition.current) {
|
||||
if (prevPosition.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,8 +100,13 @@ const Board = React.memo(
|
||||
[prevPosition],
|
||||
);
|
||||
|
||||
const handleWindowMouseUp = useCallback(() => {
|
||||
const handleWindowMouseRelease = useCallback(() => {
|
||||
if (prevPosition.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
prevPosition.current = null;
|
||||
document.body.classList.remove(globalStyles.dragScrolling);
|
||||
}, [prevPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -112,14 +124,20 @@ const Board = React.memo(
|
||||
}, [listIds, isListAddOpened]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mouseup', handleWindowMouseUp);
|
||||
window.addEventListener('mousemove', handleWindowMouseMove);
|
||||
|
||||
window.addEventListener('mouseup', handleWindowMouseRelease);
|
||||
window.addEventListener('blur', handleWindowMouseRelease);
|
||||
window.addEventListener('contextmenu', handleWindowMouseRelease);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mouseup', handleWindowMouseUp);
|
||||
window.removeEventListener('mousemove', handleWindowMouseMove);
|
||||
|
||||
window.removeEventListener('mouseup', handleWindowMouseRelease);
|
||||
window.removeEventListener('blur', handleWindowMouseRelease);
|
||||
window.removeEventListener('contextmenu', handleWindowMouseRelease);
|
||||
};
|
||||
}, [handleWindowMouseUp, handleWindowMouseMove]);
|
||||
}, [handleWindowMouseMove, handleWindowMouseRelease]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -13,6 +13,7 @@ import AddStep from './AddStep';
|
||||
import EditStep from './EditStep';
|
||||
|
||||
import styles from './Boards.module.scss';
|
||||
import globalStyles from '../../styles.module.scss';
|
||||
|
||||
const Boards = React.memo(({ items, currentId, canEdit, onCreate, onUpdate, onMove, onDelete }) => {
|
||||
const tabsWrapper = useRef(null);
|
||||
@@ -24,11 +25,14 @@ const Boards = React.memo(({ items, currentId, canEdit, onCreate, onUpdate, onMo
|
||||
}, []);
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
document.body.classList.add(globalStyles.dragging);
|
||||
closePopup();
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
({ draggableId, source, destination }) => {
|
||||
document.body.classList.remove(globalStyles.dragging);
|
||||
|
||||
if (!destination || source.index === destination.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
|
||||
);
|
||||
|
||||
const handleChildrenClick = useCallback(() => {
|
||||
if (!getSelection().toString()) {
|
||||
if (!window.getSelection().toString()) {
|
||||
open();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
@@ -10,16 +10,20 @@ import Item from './Item';
|
||||
import Add from './Add';
|
||||
|
||||
import styles from './Tasks.module.scss';
|
||||
import globalStyles from '../../../styles.module.scss';
|
||||
|
||||
const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onMove, onDelete }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
document.body.classList.add(globalStyles.dragging);
|
||||
closePopup();
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
({ draggableId, source, destination }) => {
|
||||
document.body.classList.remove(globalStyles.dragging);
|
||||
|
||||
if (!destination || source.index === destination.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,11 +67,18 @@ const DueDateEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose })
|
||||
return;
|
||||
}
|
||||
|
||||
const value = parseTime(data.time, nullableDate);
|
||||
let value = t('format:dateTime', {
|
||||
postProcess: 'parseDate',
|
||||
value: `${data.date} ${data.time}`,
|
||||
});
|
||||
|
||||
if (Number.isNaN(value.getTime())) {
|
||||
timeField.current.select();
|
||||
return;
|
||||
value = parseTime(data.time, nullableDate);
|
||||
|
||||
if (Number.isNaN(value.getTime())) {
|
||||
timeField.current.select();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defaultValue || value.getTime() !== defaultValue.getTime()) {
|
||||
@@ -79,7 +86,7 @@ const DueDateEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose })
|
||||
}
|
||||
|
||||
onClose();
|
||||
}, [defaultValue, onUpdate, onClose, data, nullableDate]);
|
||||
}, [defaultValue, onUpdate, onClose, data, nullableDate, t]);
|
||||
|
||||
const handleClearClick = useCallback(() => {
|
||||
if (defaultValue) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import EditStep from './EditStep';
|
||||
import Item from './Item';
|
||||
|
||||
import styles from './LabelsStep.module.scss';
|
||||
import globalStyles from '../../styles.module.scss';
|
||||
|
||||
const StepTypes = {
|
||||
ADD: 'ADD',
|
||||
@@ -77,8 +78,14 @@ const LabelsStep = React.memo(
|
||||
[onDeselect],
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
document.body.classList.add(globalStyles.dragging);
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
({ draggableId, source, destination }) => {
|
||||
document.body.classList.remove(globalStyles.dragging);
|
||||
|
||||
if (!destination || source.index === destination.index) {
|
||||
return;
|
||||
}
|
||||
@@ -159,7 +166,7 @@ const LabelsStep = React.memo(
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
{filteredItems.length > 0 && (
|
||||
<DragDropContext onDragEnd={handleDragEnd}>
|
||||
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<Droppable droppableId="labels" type={DroppableTypes.LABEL}>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<div
|
||||
|
||||
@@ -32,7 +32,7 @@ const List = React.memo(
|
||||
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
|
||||
|
||||
const nameEdit = useRef(null);
|
||||
const listWrapper = useRef(null);
|
||||
const cardsWrapper = useRef(null);
|
||||
|
||||
const handleHeaderClick = useCallback(() => {
|
||||
if (isPersisted && canEdit) {
|
||||
@@ -67,7 +67,7 @@ const List = React.memo(
|
||||
|
||||
useEffect(() => {
|
||||
if (isAddCardOpened) {
|
||||
listWrapper.current.scrollTop = listWrapper.current.scrollHeight;
|
||||
cardsWrapper.current.scrollTop = cardsWrapper.current.scrollHeight;
|
||||
}
|
||||
}, [cardIds, isAddCardOpened]);
|
||||
|
||||
@@ -133,13 +133,7 @@ const List = React.memo(
|
||||
</ActionsPopup>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
ref={listWrapper}
|
||||
className={classNames(
|
||||
styles.cardsInnerWrapper,
|
||||
(isAddCardOpened || !canEdit) && styles.cardsInnerWrapperFull,
|
||||
)}
|
||||
>
|
||||
<div ref={cardsWrapper} className={styles.cardsInnerWrapper}>
|
||||
<div className={styles.cardsOuterWrapper}>{cardsNode}</div>
|
||||
</div>
|
||||
{!isAddCardOpened && canEdit && (
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
}
|
||||
|
||||
.cardsInnerWrapper {
|
||||
max-height: calc(100vh - 268px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
width: 290px;
|
||||
@@ -62,10 +61,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cardsInnerWrapperFull {
|
||||
max-height: calc(100vh - 232px);
|
||||
}
|
||||
|
||||
.cardsOuterWrapper {
|
||||
padding: 0 8px;
|
||||
white-space: normal;
|
||||
@@ -140,6 +135,9 @@
|
||||
.outerWrapper {
|
||||
background: #dfe3e6;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 198px);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import ptBR from './pt-BR';
|
||||
import roRO from './ro-RO';
|
||||
import ruRU from './ru-RU';
|
||||
import skSK from './sk-SK';
|
||||
import srCyrlCS from './sr-Cyrl-CS';
|
||||
import srLatnCS from './sr-Latn-CS';
|
||||
import svSE from './sv-SE';
|
||||
import trTR from './tr-TR';
|
||||
import ukUA from './uk-UA';
|
||||
@@ -48,6 +50,8 @@ const locales = [
|
||||
roRO,
|
||||
ruRU,
|
||||
skSK,
|
||||
srCyrlCS,
|
||||
srLatnCS,
|
||||
svSE,
|
||||
trTR,
|
||||
ukUA,
|
||||
|
||||
@@ -3,6 +3,7 @@ export default {
|
||||
common: {
|
||||
emailOrUsername: 'E-mail или имя пользователя',
|
||||
invalidEmailOrUsername: 'Неверный e-mail или имя пользователя',
|
||||
invalidCredentials: 'Недействительные учетные данные',
|
||||
invalidPassword: 'Неверный пароль',
|
||||
logInToPlanka: 'Вход в Planka',
|
||||
noInternetConnection: 'Нет соединения',
|
||||
|
||||
253
client/src/locales/sr-Cyrl-CS/core.js
Normal file
253
client/src/locales/sr-Cyrl-CS/core.js
Normal file
@@ -0,0 +1,253 @@
|
||||
import dateFns from 'date-fns/locale/sr';
|
||||
|
||||
export default {
|
||||
dateFns,
|
||||
|
||||
format: {
|
||||
date: 'd.M.yyyy.',
|
||||
time: 'p',
|
||||
dateTime: '$t(format:date) $t(format:time)',
|
||||
longDate: 'd. MMM',
|
||||
longDateTime: "d. MMMM 'u' p",
|
||||
fullDate: 'd. MMM y',
|
||||
fullDateTime: "d. MMMM y 'u' p",
|
||||
},
|
||||
|
||||
translation: {
|
||||
common: {
|
||||
aboutPlanka: 'O Planka',
|
||||
account: 'Налог',
|
||||
actions: 'Радње',
|
||||
addAttachment_title: 'Додај прилог',
|
||||
addComment: 'Додај коментар',
|
||||
addManager_title: 'Додај руководиоца',
|
||||
addMember_title: 'Додај члана',
|
||||
addUser_title: 'Додај корисника',
|
||||
administrator: 'Администратор',
|
||||
all: 'Све',
|
||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||
'Све промене ће аутоматски бити сачуване<br />након успостављања конекције.',
|
||||
areYouSureYouWantToDeleteThisAttachment: 'Да ли заиста желите да обришете овај прилог?',
|
||||
areYouSureYouWantToDeleteThisBoard: 'Да ли заиста желите да обришете ову таблу?',
|
||||
areYouSureYouWantToDeleteThisCard: 'Да ли заиста желите да обришете ову картицу?',
|
||||
areYouSureYouWantToDeleteThisComment: 'Да ли заиста желите да обришете овај коментар?',
|
||||
areYouSureYouWantToDeleteThisLabel: 'Да ли заиста желите да обришете ову ознаку?',
|
||||
areYouSureYouWantToDeleteThisList: 'Да ли заиста желите да обришете овај списак?',
|
||||
areYouSureYouWantToDeleteThisProject: 'Да ли заиста желите да обришете овај пројекат?',
|
||||
areYouSureYouWantToDeleteThisTask: 'Да ли заиста желите да обришете овај задатак?',
|
||||
areYouSureYouWantToDeleteThisUser: 'Да ли заиста желите да обришете овог корисника?',
|
||||
areYouSureYouWantToLeaveBoard: 'Да ли заиста желите да напустите ову таблу?',
|
||||
areYouSureYouWantToLeaveProject: 'Да ли заиста желите да напустите овај пројекат?',
|
||||
areYouSureYouWantToRemoveThisManagerFromProject:
|
||||
'Да ли заиста желите да уклоните овог руководиоца из овог пројекта?',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'Да ли заиста желите да уклоните овог члана из ове табле?',
|
||||
attachment: 'Прилог',
|
||||
attachments: 'Прилози',
|
||||
authentication: 'Аутентификација',
|
||||
background: 'Позадина',
|
||||
board: 'Табла',
|
||||
boardNotFound_title: 'Табла није пронађена',
|
||||
canComment: 'Може да коментарише',
|
||||
canEditContentOfBoard: 'Може да уређује садржај табле.',
|
||||
canOnlyViewBoard: 'Може само да прегледа таблу.',
|
||||
cardActions_title: 'Радње над картицом',
|
||||
cardNotFound_title: 'Картица није пронађена',
|
||||
cardOrActionAreDeleted: 'Картица или радња су обрисане.',
|
||||
color: 'Боја',
|
||||
copy_inline: 'копија',
|
||||
createBoard_title: 'Направи таблу',
|
||||
createLabel_title: 'Направи ознаку',
|
||||
createNewOneOrSelectExistingOne: 'Направи нову или изабери<br />постојећу.',
|
||||
createProject_title: 'Направи пројекат',
|
||||
createTextFile_title: 'Направи текстуалну датотеку',
|
||||
currentPassword: 'Тренутна лозинка',
|
||||
dangerZone_title: 'Опасна зона',
|
||||
date: 'Датум',
|
||||
dueDate: 'Рок',
|
||||
dueDate_title: 'Рок',
|
||||
deleteAttachment_title: 'Обриши прилог',
|
||||
deleteBoard_title: 'Обриши таблу',
|
||||
deleteCard_title: 'Обриши картицу',
|
||||
deleteComment_title: 'Обриши коментар',
|
||||
deleteLabel_title: 'Обриши ознаку',
|
||||
deleteList_title: 'Обриши списак',
|
||||
deleteProject_title: 'Обриши пројекат',
|
||||
deleteTask_title: 'Обриши задатак',
|
||||
deleteUser_title: 'Обриши корисника',
|
||||
description: 'Опис',
|
||||
detectAutomatically: 'Детектуј аутоматски',
|
||||
dropFileToUpload: 'Превуци датотеку за слање',
|
||||
editor: 'Уређивач',
|
||||
editAttachment_title: 'Уреди прилог',
|
||||
editAvatar_title: 'Уреди аватара',
|
||||
editBoard_title: 'Уреди таблу',
|
||||
editDueDate_title: 'Уреди рок',
|
||||
editEmail_title: 'Уреди е-пошту',
|
||||
editInformation_title: 'Уреди информације',
|
||||
editLabel_title: 'Уреди ознаку',
|
||||
editPassword_title: 'Измени лозинку',
|
||||
editPermissions_title: 'Уреди овлашћења',
|
||||
editStopwatch_title: 'Уреди штоперицу',
|
||||
editUsername_title: 'Измени корисничко име',
|
||||
email: 'Е-пошта',
|
||||
emailAlreadyInUse: 'Е-пошта је већ у употреби',
|
||||
enterCardTitle: 'Унеси наслов картице... [Ctrl+Enter] да се аутоматски отвори.',
|
||||
enterDescription: 'Унеси опис...',
|
||||
enterFilename: 'Унеси назив датотеке',
|
||||
enterListTitle: 'Унеси наслов списка...',
|
||||
enterProjectTitle: 'Унеси наслов пројекта',
|
||||
enterTaskDescription: 'Унеси опис задатка...',
|
||||
filterByLabels_title: 'Филтрирај према ознакама',
|
||||
filterByMembers_title: 'Филтрирај према члановима',
|
||||
fromComputer_title: 'Са рачунара',
|
||||
fromTrello: 'Са Trello-а',
|
||||
general: 'Опште',
|
||||
hours: 'Сати',
|
||||
importBoard_title: 'Увези таблу',
|
||||
invalidCurrentPassword: 'Неисправна тренутна лозинка',
|
||||
labels: 'Ознаке',
|
||||
language: 'Језик',
|
||||
leaveBoard_title: 'Напусти таблу',
|
||||
leaveProject_title: 'Напусти пројекат',
|
||||
linkIsCopied: 'Веза је ископирана',
|
||||
list: 'Списак',
|
||||
listActions_title: 'Радње над списком',
|
||||
managers: 'Руководиоци',
|
||||
managerActions_title: 'Радње над руководиоцима',
|
||||
members: 'Чланови',
|
||||
memberActions_title: 'Радње над члановима',
|
||||
minutes: 'Минути',
|
||||
moveCard_title: 'Премести картицу',
|
||||
name: 'Име',
|
||||
newestFirst: 'Прво најновије',
|
||||
newEmail: 'Нова е-пошта',
|
||||
newPassword: 'Нова лозинка',
|
||||
newUsername: 'Ново корисничко име',
|
||||
noConnectionToServer: 'Нема конекције са сервером',
|
||||
noBoards: 'Нема табли',
|
||||
noLists: 'Нема спискова',
|
||||
noProjects: 'Нема пројеката',
|
||||
notifications: 'Обавештења',
|
||||
noUnreadNotifications: 'Нема непрочитаних обавештења.',
|
||||
oldestFirst: 'Прво најстарије',
|
||||
openBoard_title: 'Отвори таблу',
|
||||
optional_inline: 'опционо',
|
||||
organization: 'Организација',
|
||||
phone: 'Телефон',
|
||||
preferences: 'Својства',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Савет: притисни Ctrl-V (Cmd-V на Меку) да би додао прилог са бележнице.',
|
||||
project: 'Пројекат',
|
||||
projectNotFound_title: 'Пројекат није пронађен',
|
||||
removeManager_title: 'Уклони руководиоца',
|
||||
removeMember_title: 'Уклони члана',
|
||||
searchLabels: 'Претражи ознаке...',
|
||||
searchMembers: 'Претражи чланове...',
|
||||
searchUsers: 'Претражи кориснике...',
|
||||
searchCards: 'Претражи картице...',
|
||||
seconds: 'Секунде',
|
||||
selectBoard: 'Изабери таблу',
|
||||
selectList: 'Изабери списак',
|
||||
selectPermissions_title: 'Изабери одобрења',
|
||||
selectProject: 'Изабери пројекат',
|
||||
settings: 'Подешавања',
|
||||
sortList_title: 'Сложи списак',
|
||||
stopwatch: 'Штоперица',
|
||||
subscribeToMyOwnCardsByDefault: 'Подразумевано се претплати на сопствене картице',
|
||||
taskActions_title: 'Радње над задатком',
|
||||
tasks: 'Задаци',
|
||||
thereIsNoPreviewAvailableForThisAttachment: 'Нема прегледа доступног за овај прилог.',
|
||||
time: 'Време',
|
||||
title: 'Наслов',
|
||||
userActions_title: 'Корисничке радње',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> је додао ову картицу на {{list}}</1>',
|
||||
userLeftNewCommentToCard: '{{user}} је оставио нови коментар «{{comment}}» у <2>{{card}}</2>',
|
||||
userMovedCardFromListToList:
|
||||
'{{user}} је преместио <2>{{card}}</2> са {{fromList}} у {{toList}}',
|
||||
userMovedThisCardFromListToList:
|
||||
'<0>{{user}}</0><1> је преместио ову картицу са {{fromList}} на {{toList}}</1>',
|
||||
username: 'Корисничко име',
|
||||
usernameAlreadyInUse: 'Корисничко име је већ у употреби',
|
||||
users: 'Корисници',
|
||||
version: 'Верзија',
|
||||
viewer: 'Прегледач',
|
||||
writeComment: 'Напиши коментар...',
|
||||
},
|
||||
|
||||
action: {
|
||||
addAnotherCard: 'Додај још једну картицу',
|
||||
addAnotherList: 'Додај још један списак',
|
||||
addAnotherTask: 'Додај још један задатак',
|
||||
addCard: 'Додај картицу',
|
||||
addCard_title: 'Додај картицу',
|
||||
addComment: 'Додај коментар',
|
||||
addList: 'Додај списак',
|
||||
addMember: 'Додај члана',
|
||||
addMoreDetailedDescription: 'Додај детаљнији опис',
|
||||
addTask: 'Додај задатак',
|
||||
addToCard: 'Додај на картицу',
|
||||
addUser: 'Додај корисника',
|
||||
copyLink_title: 'Копирај везу',
|
||||
createBoard: 'Направи таблу',
|
||||
createFile: 'Направи датотеку',
|
||||
createLabel: 'Направи ознаку',
|
||||
createNewLabel: 'Направи нову ознаку',
|
||||
createProject: 'Направи пројекат',
|
||||
delete: 'Обриши',
|
||||
deleteAttachment: 'Обриши прилог',
|
||||
deleteAvatar: 'Обриши аватара',
|
||||
deleteBoard: 'Обриши таблу',
|
||||
deleteCard: 'Обриши картицу',
|
||||
deleteCard_title: 'Обриши картицу',
|
||||
deleteComment: 'Обриши коментар',
|
||||
deleteImage: 'Обриши слику',
|
||||
deleteLabel: 'Обриши ознаку',
|
||||
deleteList: 'Обриши списак',
|
||||
deleteList_title: 'Обриши списак',
|
||||
deleteProject: 'Обриши пројекат',
|
||||
deleteProject_title: 'Обриши пројекат',
|
||||
deleteTask: 'Обриши задатак',
|
||||
deleteTask_title: 'Обриши задатак',
|
||||
deleteUser: 'Обриши корисника',
|
||||
duplicate: 'Клонирај',
|
||||
duplicateCard_title: 'Клонирај картицу',
|
||||
edit: 'Измени',
|
||||
editDueDate_title: 'Измени рок',
|
||||
editDescription_title: 'Измени опис',
|
||||
editEmail_title: 'Измени е-пошту',
|
||||
editInformation_title: 'Измени информације',
|
||||
editPassword_title: 'Измени лозинку',
|
||||
editPermissions: 'Измени одобрења',
|
||||
editStopwatch_title: 'Измени штоперицу',
|
||||
editTitle_title: 'Измени наслов',
|
||||
editUsername_title: 'Измени корисничко име',
|
||||
hideDetails: 'Сакриј детаље',
|
||||
import: 'Увези',
|
||||
leaveBoard: 'Напусти таблу',
|
||||
leaveProject: 'Напусти пројекат',
|
||||
logOut_title: 'Одјава',
|
||||
makeCover_title: 'Направи омот',
|
||||
move: 'Премести',
|
||||
moveCard_title: 'Премести картицу',
|
||||
remove: 'Уклони',
|
||||
removeBackground: 'Уклони позадину',
|
||||
removeCover_title: 'Уклони омот',
|
||||
removeFromBoard: 'Уклони са табле',
|
||||
removeFromProject: 'Уклони из пројекта',
|
||||
removeManager: 'Уклони руководиоца',
|
||||
removeMember: 'Уклони члана',
|
||||
save: 'Сачувај',
|
||||
showAllAttachments: 'Прикажи све ({{hidden}} сакривене прилоге)',
|
||||
showDetails: 'Прикажи детаље',
|
||||
showFewerAttachments: 'Прикажи мање прилога',
|
||||
sortList_title: 'Сложи списак',
|
||||
start: 'Почни',
|
||||
stop: 'Заустави',
|
||||
subscribe: 'Претплати се',
|
||||
unsubscribe: 'Укини претплату',
|
||||
uploadNewAvatar: 'Постави нови аватар',
|
||||
uploadNewImage: 'Постави нову слику',
|
||||
},
|
||||
},
|
||||
};
|
||||
8
client/src/locales/sr-Cyrl-CS/index.js
Normal file
8
client/src/locales/sr-Cyrl-CS/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import login from './login';
|
||||
|
||||
export default {
|
||||
language: 'sr-Cyrl-CS',
|
||||
country: 'rs',
|
||||
name: 'Српски (ћирилица)',
|
||||
embeddedLocale: login,
|
||||
};
|
||||
23
client/src/locales/sr-Cyrl-CS/login.js
Normal file
23
client/src/locales/sr-Cyrl-CS/login.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
emailOrUsername: 'Е-пошта или корисничко име',
|
||||
invalidEmailOrUsername: 'Неисправна е-пошта или корисничко име',
|
||||
invalidCredentials: 'Неисправни акредитиви',
|
||||
invalidPassword: 'Неисправна лозинка',
|
||||
logInToPlanka: 'Пријавите се у Planka',
|
||||
noInternetConnection: 'Нема конекције са интернетом',
|
||||
pageNotFound_title: 'Страница није пронађена',
|
||||
password: 'Лозинка',
|
||||
projectManagement: 'Управљање пројектима',
|
||||
serverConnectionFailed: 'Неуспешна конекција са сервером',
|
||||
unknownError: 'Непозната грешка, покушајте поново касније',
|
||||
useSingleSignOn: 'Користи универзалну пријаву',
|
||||
},
|
||||
|
||||
action: {
|
||||
logIn: 'Пријава',
|
||||
logInWithSSO: 'Пријава са УП',
|
||||
},
|
||||
},
|
||||
};
|
||||
253
client/src/locales/sr-Latn-CS/core.js
Normal file
253
client/src/locales/sr-Latn-CS/core.js
Normal file
@@ -0,0 +1,253 @@
|
||||
import dateFns from 'date-fns/locale/sr-Latn';
|
||||
|
||||
export default {
|
||||
dateFns,
|
||||
|
||||
format: {
|
||||
date: 'd.M.yyyy.',
|
||||
time: 'p',
|
||||
dateTime: '$t(format:date) $t(format:time)',
|
||||
longDate: 'd. MMM',
|
||||
longDateTime: "d. MMMM 'u' p",
|
||||
fullDate: 'd. MMM y',
|
||||
fullDateTime: "d. MMMM y 'u' p",
|
||||
},
|
||||
|
||||
translation: {
|
||||
common: {
|
||||
aboutPlanka: 'O Planka',
|
||||
account: 'Nalog',
|
||||
actions: 'Radnje',
|
||||
addAttachment_title: 'Dodaj prilog',
|
||||
addComment: 'Dodaj komentar',
|
||||
addManager_title: 'Dodaj rukovodioca',
|
||||
addMember_title: 'Dodaj člana',
|
||||
addUser_title: 'Dodaj korisnika',
|
||||
administrator: 'Administrator',
|
||||
all: 'Sve',
|
||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||
'Sve promene će automatski biti sačuvane<br />nakon uspostavljanja konekcije.',
|
||||
areYouSureYouWantToDeleteThisAttachment: 'Da li zaista želite da obrišete ovaj prilog?',
|
||||
areYouSureYouWantToDeleteThisBoard: 'Da li zaista želite da obrišete ovu tablu?',
|
||||
areYouSureYouWantToDeleteThisCard: 'Da li zaista želite da obrišete ovu karticu?',
|
||||
areYouSureYouWantToDeleteThisComment: 'Da li zaista želite da obrišete ovaj komentar?',
|
||||
areYouSureYouWantToDeleteThisLabel: 'Da li zaista želite da obrišete ovu oznaku?',
|
||||
areYouSureYouWantToDeleteThisList: 'Da li zaista želite da obrišete ovaj spisak?',
|
||||
areYouSureYouWantToDeleteThisProject: 'Da li zaista želite da obrišete ovaj projekat?',
|
||||
areYouSureYouWantToDeleteThisTask: 'Da li zaista želite da obrišete ovaj zadatak?',
|
||||
areYouSureYouWantToDeleteThisUser: 'Da li zaista želite da obrišete ovog korisnika?',
|
||||
areYouSureYouWantToLeaveBoard: 'Da li zaista želite da napustite ovu tablu?',
|
||||
areYouSureYouWantToLeaveProject: 'Da li zaista želite da napustite ovaj projekat?',
|
||||
areYouSureYouWantToRemoveThisManagerFromProject:
|
||||
'Da li zaista želite da uklonite ovog rukovodioca iz ovog projekta?',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'Da li zaista želite da uklonite ovog člana iz ove table?',
|
||||
attachment: 'Prilog',
|
||||
attachments: 'Prilozi',
|
||||
authentication: 'Autentifikacija',
|
||||
background: 'Pozadina',
|
||||
board: 'Tabla',
|
||||
boardNotFound_title: 'Tabla nije pronađena',
|
||||
canComment: 'Može da komentariše',
|
||||
canEditContentOfBoard: 'Može da uređuje sadržaj table.',
|
||||
canOnlyViewBoard: 'Može samo da pregleda tablu.',
|
||||
cardActions_title: 'Radnje nad karticom',
|
||||
cardNotFound_title: 'Kartica nije pronađena',
|
||||
cardOrActionAreDeleted: 'Kartica ili radnja su obrisane.',
|
||||
color: 'Boja',
|
||||
copy_inline: 'kopija',
|
||||
createBoard_title: 'Napravi tablu',
|
||||
createLabel_title: 'Napravi oznaku',
|
||||
createNewOneOrSelectExistingOne: 'Napravi novu ili izaberi<br />postojeću.',
|
||||
createProject_title: 'Napravi projekat',
|
||||
createTextFile_title: 'Napravi tekstualnu datoteku',
|
||||
currentPassword: 'Trenutna lozinka',
|
||||
dangerZone_title: 'Opasna zona',
|
||||
date: 'Datum',
|
||||
dueDate: 'Rok',
|
||||
dueDate_title: 'Rok',
|
||||
deleteAttachment_title: 'Obriši prilog',
|
||||
deleteBoard_title: 'Obriši tablu',
|
||||
deleteCard_title: 'Obriši karticu',
|
||||
deleteComment_title: 'Obriši komentar',
|
||||
deleteLabel_title: 'Obriši oznaku',
|
||||
deleteList_title: 'Obriši spisak',
|
||||
deleteProject_title: 'Obriši projekat',
|
||||
deleteTask_title: 'Obriši zadatak',
|
||||
deleteUser_title: 'Obriši korisnika',
|
||||
description: 'Opis',
|
||||
detectAutomatically: 'Detektuj automatski',
|
||||
dropFileToUpload: 'Prevuci datoteku za slanje',
|
||||
editor: 'Uređivač',
|
||||
editAttachment_title: 'Uredi prilog',
|
||||
editAvatar_title: 'Uredi avatara',
|
||||
editBoard_title: 'Uredi tablu',
|
||||
editDueDate_title: 'Uredi rok',
|
||||
editEmail_title: 'Uredi e-poštu',
|
||||
editInformation_title: 'Uredi informacije',
|
||||
editLabel_title: 'Uredi oznaku',
|
||||
editPassword_title: 'Izmeni lozinku',
|
||||
editPermissions_title: 'Uredi ovlašćenja',
|
||||
editStopwatch_title: 'Uredi štopericu',
|
||||
editUsername_title: 'Izmeni korisničko ime',
|
||||
email: 'E-pošta',
|
||||
emailAlreadyInUse: 'E-pošta je već u upotrebi',
|
||||
enterCardTitle: 'Unesi naslov kartice... [Ctrl+Enter] da se automatski otvori.',
|
||||
enterDescription: 'Unesi opis...',
|
||||
enterFilename: 'Unesi naziv datoteke',
|
||||
enterListTitle: 'Unesi naslov spiska...',
|
||||
enterProjectTitle: 'Unesi naslov projekta',
|
||||
enterTaskDescription: 'Unesi opis zadatka...',
|
||||
filterByLabels_title: 'Filtriraj prema oznakama',
|
||||
filterByMembers_title: 'Filtriraj prema članovima',
|
||||
fromComputer_title: 'Sa računara',
|
||||
fromTrello: 'Sa Trello-a',
|
||||
general: 'Opšte',
|
||||
hours: 'Sati',
|
||||
importBoard_title: 'Uvezi tablu',
|
||||
invalidCurrentPassword: 'Neispravna trenutna lozinka',
|
||||
labels: 'Oznake',
|
||||
language: 'Jezik',
|
||||
leaveBoard_title: 'Napusti tablu',
|
||||
leaveProject_title: 'Napusti projekat',
|
||||
linkIsCopied: 'Veza je iskopirana',
|
||||
list: 'Spisak',
|
||||
listActions_title: 'Radnje nad spiskom',
|
||||
managers: 'Rukovodioci',
|
||||
managerActions_title: 'Radnje nad rukovodiocima',
|
||||
members: 'Članovi',
|
||||
memberActions_title: 'Radnje nad članovima',
|
||||
minutes: 'Minuti',
|
||||
moveCard_title: 'Premesti karticu',
|
||||
name: 'Ime',
|
||||
newestFirst: 'Prvo najnovije',
|
||||
newEmail: 'Nova e-pošta',
|
||||
newPassword: 'Nova lozinka',
|
||||
newUsername: 'Novo korisničko ime',
|
||||
noConnectionToServer: 'Nema konekcije sa serverom',
|
||||
noBoards: 'Nema tabli',
|
||||
noLists: 'Nema spiskova',
|
||||
noProjects: 'Nema projekata',
|
||||
notifications: 'Obaveštenja',
|
||||
noUnreadNotifications: 'Nema nepročitanih obaveštenja.',
|
||||
oldestFirst: 'Prvo najstarije',
|
||||
openBoard_title: 'Otvori tablu',
|
||||
optional_inline: 'opciono',
|
||||
organization: 'Organizacija',
|
||||
phone: 'Telefon',
|
||||
preferences: 'Svojstva',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Savet: pritisni Ctrl-V (Cmd-V na Meku) da bi dodao prilog sa beležnice.',
|
||||
project: 'Projekat',
|
||||
projectNotFound_title: 'Projekat nije pronađen',
|
||||
removeManager_title: 'Ukloni rukovodioca',
|
||||
removeMember_title: 'Ukloni člana',
|
||||
searchLabels: 'Pretraži oznake...',
|
||||
searchMembers: 'Pretraži članove...',
|
||||
searchUsers: 'Pretraži korisnike...',
|
||||
searchCards: 'Pretraži kartice...',
|
||||
seconds: 'Sekunde',
|
||||
selectBoard: 'Izaberi tablu',
|
||||
selectList: 'Izaberi spisak',
|
||||
selectPermissions_title: 'Izaberi odobrenja',
|
||||
selectProject: 'Izaberi projekat',
|
||||
settings: 'Podešavanja',
|
||||
sortList_title: 'Složi spisak',
|
||||
stopwatch: 'Štoperica',
|
||||
subscribeToMyOwnCardsByDefault: 'Podrazumevano se pretplati na sopstvene kartice',
|
||||
taskActions_title: 'Radnje nad zadatkom',
|
||||
tasks: 'Zadaci',
|
||||
thereIsNoPreviewAvailableForThisAttachment: 'Nema pregleda dostupnog za ovaj prilog.',
|
||||
time: 'Vreme',
|
||||
title: 'Naslov',
|
||||
userActions_title: 'Korisničke radnje',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> je dodao ovu karticu na {{list}}</1>',
|
||||
userLeftNewCommentToCard: '{{user}} je ostavio novi komentar «{{comment}}» u <2>{{card}}</2>',
|
||||
userMovedCardFromListToList:
|
||||
'{{user}} je premestio <2>{{card}}</2> sa {{fromList}} u {{toList}}',
|
||||
userMovedThisCardFromListToList:
|
||||
'<0>{{user}}</0><1> je premestio ovu karticu sa {{fromList}} na {{toList}}</1>',
|
||||
username: 'Korisničko ime',
|
||||
usernameAlreadyInUse: 'Korisničko ime je već u upotrebi',
|
||||
users: 'Korisnici',
|
||||
version: 'Verzija',
|
||||
viewer: 'Pregledač',
|
||||
writeComment: 'Napiši komentar...',
|
||||
},
|
||||
|
||||
action: {
|
||||
addAnotherCard: 'Dodaj još jednu karticu',
|
||||
addAnotherList: 'Dodaj još jedan spisak',
|
||||
addAnotherTask: 'Dodaj još jedan zadatak',
|
||||
addCard: 'Dodaj karticu',
|
||||
addCard_title: 'Dodaj karticu',
|
||||
addComment: 'Dodaj komentar',
|
||||
addList: 'Dodaj spisak',
|
||||
addMember: 'Dodaj člana',
|
||||
addMoreDetailedDescription: 'Dodaj detaljniji opis',
|
||||
addTask: 'Dodaj zadatak',
|
||||
addToCard: 'Dodaj na karticu',
|
||||
addUser: 'Dodaj korisnika',
|
||||
copyLink_title: 'Kopiraj vezu',
|
||||
createBoard: 'Napravi tablu',
|
||||
createFile: 'Napravi datoteku',
|
||||
createLabel: 'Napravi oznaku',
|
||||
createNewLabel: 'Napravi novu oznaku',
|
||||
createProject: 'Napravi projekat',
|
||||
delete: 'Obriši',
|
||||
deleteAttachment: 'Obriši prilog',
|
||||
deleteAvatar: 'Obriši avatara',
|
||||
deleteBoard: 'Obriši tablu',
|
||||
deleteCard: 'Obriši karticu',
|
||||
deleteCard_title: 'Obriši karticu',
|
||||
deleteComment: 'Obriši komentar',
|
||||
deleteImage: 'Obriši sliku',
|
||||
deleteLabel: 'Obriši oznaku',
|
||||
deleteList: 'Obriši spisak',
|
||||
deleteList_title: 'Obriši spisak',
|
||||
deleteProject: 'Obriši projekat',
|
||||
deleteProject_title: 'Obriši projekat',
|
||||
deleteTask: 'Obriši zadatak',
|
||||
deleteTask_title: 'Obriši zadatak',
|
||||
deleteUser: 'Obriši korisnika',
|
||||
duplicate: 'Kloniraj',
|
||||
duplicateCard_title: 'Kloniraj karticu',
|
||||
edit: 'Izmeni',
|
||||
editDueDate_title: 'Izmeni rok',
|
||||
editDescription_title: 'Izmeni opis',
|
||||
editEmail_title: 'Izmeni e-poštu',
|
||||
editInformation_title: 'Izmeni informacije',
|
||||
editPassword_title: 'Izmeni lozinku',
|
||||
editPermissions: 'Izmeni odobrenja',
|
||||
editStopwatch_title: 'Izmeni štopericu',
|
||||
editTitle_title: 'Izmeni naslov',
|
||||
editUsername_title: 'Izmeni korisničko ime',
|
||||
hideDetails: 'Sakrij detalje',
|
||||
import: 'Uvezi',
|
||||
leaveBoard: 'Napusti tablu',
|
||||
leaveProject: 'Napusti projekat',
|
||||
logOut_title: 'Odjava',
|
||||
makeCover_title: 'Napravi omot',
|
||||
move: 'Premesti',
|
||||
moveCard_title: 'Premesti karticu',
|
||||
remove: 'Ukloni',
|
||||
removeBackground: 'Ukloni pozadinu',
|
||||
removeCover_title: 'Ukloni omot',
|
||||
removeFromBoard: 'Ukloni sa table',
|
||||
removeFromProject: 'Ukloni iz projekta',
|
||||
removeManager: 'Ukloni rukovodioca',
|
||||
removeMember: 'Ukloni člana',
|
||||
save: 'Sačuvaj',
|
||||
showAllAttachments: 'Prikaži sve ({{hidden}} sakrivene priloge)',
|
||||
showDetails: 'Prikaži detalje',
|
||||
showFewerAttachments: 'Prikaži manje priloga',
|
||||
sortList_title: 'Složi spisak',
|
||||
start: 'Počni',
|
||||
stop: 'Zaustavi',
|
||||
subscribe: 'Pretplati se',
|
||||
unsubscribe: 'Ukini pretplatu',
|
||||
uploadNewAvatar: 'Postavi novi avatar',
|
||||
uploadNewImage: 'Postavi novu sliku',
|
||||
},
|
||||
},
|
||||
};
|
||||
8
client/src/locales/sr-Latn-CS/index.js
Normal file
8
client/src/locales/sr-Latn-CS/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import login from './login';
|
||||
|
||||
export default {
|
||||
language: 'sr-Latn-CS',
|
||||
country: 'rs',
|
||||
name: 'Srpski (latinica)',
|
||||
embeddedLocale: login,
|
||||
};
|
||||
23
client/src/locales/sr-Latn-CS/login.js
Normal file
23
client/src/locales/sr-Latn-CS/login.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
emailOrUsername: 'E-pošta ili korisničko ime',
|
||||
invalidEmailOrUsername: 'Neispravna e-pošta ili korisničko ime',
|
||||
invalidCredentials: 'Neispravni akreditivi',
|
||||
invalidPassword: 'Neispravna lozinka',
|
||||
logInToPlanka: 'Prijavite se u Planka',
|
||||
noInternetConnection: 'Nema konekcije sa internetom',
|
||||
pageNotFound_title: 'Stranica nije pronađena',
|
||||
password: 'Lozinka',
|
||||
projectManagement: 'Upravljanje projektima',
|
||||
serverConnectionFailed: 'Neuspešna konekcija sa serverom',
|
||||
unknownError: 'Nepoznata greška, pokušajte ponovo kasnije',
|
||||
useSingleSignOn: 'Koristi univerzalnu prijavu',
|
||||
},
|
||||
|
||||
action: {
|
||||
logIn: 'Prijava',
|
||||
logInWithSSO: 'Prijava sa UP',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -142,6 +142,15 @@
|
||||
}
|
||||
|
||||
:global(#app) {
|
||||
&.dragging>* {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.dragScrolling>* {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Backgrounds */
|
||||
|
||||
.backgroundBerryRed {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export default '1.23.5';
|
||||
export default '1.24.3';
|
||||
|
||||
@@ -9,36 +9,36 @@ PLANKA_DOCKER_CONTAINER_PLANKA="planka_planka_1"
|
||||
|
||||
# Create Temporary folder
|
||||
BACKUP_DATETIME=$(date --utc +%FT%H-%M-%SZ)
|
||||
mkdir -p $BACKUP_DATETIME-backup
|
||||
mkdir -p "$BACKUP_DATETIME-backup"
|
||||
|
||||
# Dump DB into SQL File
|
||||
echo -n "Exporting postgres database ... "
|
||||
docker exec -t $PLANKA_DOCKER_CONTAINER_POSTGRES pg_dumpall -c -U postgres > $BACKUP_DATETIME-backup/postgres.sql
|
||||
docker exec -t "$PLANKA_DOCKER_CONTAINER_POSTGRES" pg_dumpall -c -U postgres > "$BACKUP_DATETIME-backup/postgres.sql"
|
||||
echo "Success!"
|
||||
|
||||
# Export Docker Voumes
|
||||
echo -n "Exporting user-avatars ... "
|
||||
docker run --rm --volumes-from $PLANKA_DOCKER_CONTAINER_PLANKA -v $(pwd)/$BACKUP_DATETIME-backup:/backup ubuntu cp -r /app/public/user-avatars /backup/user-avatars
|
||||
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$BACKUP_DATETIME-backup:/backup" ubuntu cp -r /app/public/user-avatars /backup/user-avatars
|
||||
echo "Success!"
|
||||
echo -n "Exporting project-background-images ... "
|
||||
docker run --rm --volumes-from $PLANKA_DOCKER_CONTAINER_PLANKA -v $(pwd)/$BACKUP_DATETIME-backup:/backup ubuntu cp -r /app/public/project-background-images /backup/project-background-images
|
||||
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$BACKUP_DATETIME-backup:/backup" ubuntu cp -r /app/public/project-background-images /backup/project-background-images
|
||||
echo "Success!"
|
||||
echo -n "Exporting attachments ... "
|
||||
docker run --rm --volumes-from $PLANKA_DOCKER_CONTAINER_PLANKA -v $(pwd)/$BACKUP_DATETIME-backup:/backup ubuntu cp -r /app/private/attachments /backup/attachments
|
||||
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$BACKUP_DATETIME-backup:/backup" ubuntu cp -r /app/private/attachments /backup/attachments
|
||||
echo "Success!"
|
||||
|
||||
# Create tgz
|
||||
echo -n "Creating final tarball $BACKUP_DATETIME-backup.tgz ... "
|
||||
tar -czf $BACKUP_DATETIME-backup.tgz \
|
||||
$BACKUP_DATETIME-backup/postgres.sql \
|
||||
$BACKUP_DATETIME-backup/user-avatars \
|
||||
$BACKUP_DATETIME-backup/project-background-images \
|
||||
$BACKUP_DATETIME-backup/attachments
|
||||
tar -czf "$BACKUP_DATETIME-backup.tgz" \
|
||||
"$BACKUP_DATETIME-backup/postgres.sql" \
|
||||
"$BACKUP_DATETIME-backup/user-avatars" \
|
||||
"$BACKUP_DATETIME-backup/project-background-images" \
|
||||
"$BACKUP_DATETIME-backup/attachments"
|
||||
echo "Success!"
|
||||
|
||||
#Remove source files
|
||||
echo -n "Cleaning up temporary files and folders ... "
|
||||
rm -rf $BACKUP_DATETIME-backup
|
||||
rm -rf "$BACKUP_DATETIME-backup"
|
||||
echo "Success!"
|
||||
|
||||
echo "Backup Complete!"
|
||||
|
||||
@@ -25,9 +25,15 @@ services:
|
||||
# - KNEX_REJECT_UNAUTHORIZED_SSL_CERTIFICATE=false
|
||||
|
||||
# - SHOW_DETAILED_AUTH_ERRORS=false # Set to true to show more detailed authentication error messages. It should not be enabled without a rate limiter for security reasons.
|
||||
|
||||
# - ALLOW_ALL_TO_CREATE_PROJECTS=true
|
||||
|
||||
# - S3_ENDPOINT=
|
||||
# - S3_REGION=
|
||||
# - S3_ACCESS_KEY_ID=
|
||||
# - S3_SECRET_ACCESS_KEY=
|
||||
# - S3_BUCKET=
|
||||
# - S3_FORCE_PATH_STYLE=true
|
||||
|
||||
# - OIDC_ISSUER=
|
||||
# - OIDC_CLIENT_ID=
|
||||
# - OIDC_CLIENT_SECRET=
|
||||
|
||||
@@ -32,9 +32,15 @@ services:
|
||||
# - DEFAULT_ADMIN_USERNAME=demo
|
||||
|
||||
# - SHOW_DETAILED_AUTH_ERRORS=false # Set to true to show more detailed authentication error messages. It should not be enabled without a rate limiter for security reasons.
|
||||
|
||||
# - ALLOW_ALL_TO_CREATE_PROJECTS=true
|
||||
|
||||
# - S3_ENDPOINT=
|
||||
# - S3_REGION=
|
||||
# - S3_ACCESS_KEY_ID=
|
||||
# - S3_SECRET_ACCESS_KEY=
|
||||
# - S3_BUCKET=
|
||||
# - S3_FORCE_PATH_STYLE=true
|
||||
|
||||
# - OIDC_ISSUER=
|
||||
# - OIDC_CLIENT_ID=
|
||||
# - OIDC_CLIENT_SECRET=
|
||||
|
||||
@@ -9,29 +9,29 @@ PLANKA_DOCKER_CONTAINER_PLANKA="planka_planka_1"
|
||||
|
||||
# Extract tgz archive
|
||||
PLANKA_BACKUP_ARCHIVE_TGZ=$1
|
||||
PLANKA_BACKUP_ARCHIVE=$(basename $PLANKA_BACKUP_ARCHIVE_TGZ .tgz)
|
||||
PLANKA_BACKUP_ARCHIVE=$(basename "$PLANKA_BACKUP_ARCHIVE_TGZ" .tgz)
|
||||
echo -n "Extracting tarball $PLANKA_BACKUP_ARCHIVE_TGZ ... "
|
||||
tar -xzf $PLANKA_BACKUP_ARCHIVE_TGZ
|
||||
tar -xzf "$PLANKA_BACKUP_ARCHIVE_TGZ"
|
||||
echo "Success!"
|
||||
|
||||
# Import Database
|
||||
echo -n "Importing postgres database ... "
|
||||
cat $PLANKA_BACKUP_ARCHIVE/postgres.sql | docker exec -i $PLANKA_DOCKER_CONTAINER_POSTGRES psql -U postgres
|
||||
cat "$PLANKA_BACKUP_ARCHIVE/postgres.sql" | docker exec -i "$PLANKA_DOCKER_CONTAINER_POSTGRES" psql -U postgres
|
||||
echo "Success!"
|
||||
|
||||
# Restore Docker Volumes
|
||||
echo -n "Importing user-avatars ... "
|
||||
docker run --rm --volumes-from $PLANKA_DOCKER_CONTAINER_PLANKA -v $(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup ubuntu cp -rf /backup/user-avatars /app/public/
|
||||
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup" ubuntu cp -rf /backup/user-avatars /app/public/
|
||||
echo "Success!"
|
||||
echo -n "Importing project-background-images ... "
|
||||
docker run --rm --volumes-from $PLANKA_DOCKER_CONTAINER_PLANKA -v $(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup ubuntu cp -rf /backup/project-background-images /app/public/
|
||||
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup" ubuntu cp -rf /backup/project-background-images /app/public/
|
||||
echo "Success!"
|
||||
echo -n "Importing attachments ... "
|
||||
docker run --rm --volumes-from $PLANKA_DOCKER_CONTAINER_PLANKA -v $(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup ubuntu cp -rf /backup/attachments /app/private/
|
||||
docker run --rm --volumes-from "$PLANKA_DOCKER_CONTAINER_PLANKA" -v "$(pwd)/$PLANKA_BACKUP_ARCHIVE:/backup" ubuntu cp -rf /backup/attachments /app/private/
|
||||
echo "Success!"
|
||||
|
||||
echo -n "Cleaning up temporary files and folders ... "
|
||||
rm -r $PLANKA_BACKUP_ARCHIVE
|
||||
rm -r "$PLANKA_BACKUP_ARCHIVE"
|
||||
echo "Success!"
|
||||
|
||||
echo "Restore complete!"
|
||||
|
||||
104
package-lock.json
generated
104
package-lock.json
generated
@@ -1,31 +1,31 @@
|
||||
{
|
||||
"name": "planka",
|
||||
"version": "1.23.5",
|
||||
"version": "1.24.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "planka",
|
||||
"version": "1.23.5",
|
||||
"version": "1.24.3",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"concurrently": "^8.2.2",
|
||||
"genversion": "^3.2.0",
|
||||
"husky": "^9.1.6",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.2.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"prettier": "^3.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz",
|
||||
"integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==",
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
|
||||
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
@@ -34,24 +34,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
|
||||
"integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
|
||||
"integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/regexpp": {
|
||||
"version": "4.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz",
|
||||
"integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==",
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
|
||||
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
||||
@@ -81,22 +84,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
|
||||
"integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
|
||||
"version": "8.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
|
||||
"integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.14",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||
"integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
|
||||
"deprecated": "Use @eslint/config-array instead",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@humanwhocodes/object-schema": "^2.0.2",
|
||||
"@humanwhocodes/object-schema": "^2.0.3",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.0.5"
|
||||
},
|
||||
@@ -178,9 +181,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -466,9 +469,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -578,16 +581,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
|
||||
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
|
||||
"version": "8.57.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
|
||||
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
"@eslint/eslintrc": "^2.1.4",
|
||||
"@eslint/js": "8.57.0",
|
||||
"@humanwhocodes/config-array": "^0.11.14",
|
||||
"@eslint/js": "8.57.1",
|
||||
"@humanwhocodes/config-array": "^0.13.0",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"@ungap/structured-clone": "^1.2.0",
|
||||
@@ -910,9 +914,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
|
||||
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
|
||||
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
@@ -946,9 +950,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-east-asian-width": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz",
|
||||
"integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
|
||||
"integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -1038,9 +1042,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/husky": {
|
||||
"version": "9.1.6",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz",
|
||||
"integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==",
|
||||
"version": "9.1.7",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
||||
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
|
||||
"bin": {
|
||||
"husky": "bin.js"
|
||||
},
|
||||
@@ -1293,9 +1297,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/listr2": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz",
|
||||
"integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==",
|
||||
"version": "8.2.5",
|
||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz",
|
||||
"integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==",
|
||||
"dependencies": {
|
||||
"cli-truncate": "^4.0.0",
|
||||
"colorette": "^2.0.20",
|
||||
@@ -1994,9 +1998,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz",
|
||||
"integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==",
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
|
||||
"integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.1.0",
|
||||
@@ -2035,9 +2039,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planka",
|
||||
"version": "1.23.5",
|
||||
"version": "1.24.3",
|
||||
"private": true,
|
||||
"homepage": "https://plankanban.github.io/planka",
|
||||
"repository": {
|
||||
@@ -59,11 +59,11 @@
|
||||
"dependencies": {
|
||||
"concurrently": "^8.2.2",
|
||||
"genversion": "^3.2.0",
|
||||
"husky": "^9.1.6",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.2.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"prettier": "^3.3.3"
|
||||
|
||||
@@ -25,9 +25,15 @@ SECRET_KEY=notsecretkey
|
||||
# DEFAULT_ADMIN_USERNAME=demo
|
||||
|
||||
# SHOW_DETAILED_AUTH_ERRORS=false # Set to true to show more detailed authentication error messages. It should not be enabled without a rate limiter for security reasons.
|
||||
|
||||
# ALLOW_ALL_TO_CREATE_PROJECTS=true
|
||||
|
||||
# S3_ENDPOINT=
|
||||
# S3_REGION=
|
||||
# S3_ACCESS_KEY_ID=
|
||||
# S3_SECRET_ACCESS_KEY=
|
||||
# S3_BUCKET=
|
||||
# S3_FORCE_PATH_STYLE=true
|
||||
|
||||
# OIDC_ISSUER=
|
||||
# OIDC_CLIENT_ID=
|
||||
# OIDC_CLIENT_SECRET=
|
||||
|
||||
@@ -3,6 +3,9 @@ const { v4: uuid } = require('uuid');
|
||||
const { getRemoteAddress } = require('../../../utils/remoteAddress');
|
||||
|
||||
const Errors = {
|
||||
INVALID_OIDC_CONFIGURATION: {
|
||||
invalidOIDCConfiguration: 'Invalid OIDC configuration',
|
||||
},
|
||||
INVALID_CODE_OR_NONCE: {
|
||||
invalidCodeOrNonce: 'Invalid code or nonce',
|
||||
},
|
||||
@@ -37,6 +40,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
invalidOIDCConfiguration: {
|
||||
responseType: 'serverError',
|
||||
},
|
||||
invalidCodeOrNonce: {
|
||||
responseType: 'unauthorized',
|
||||
},
|
||||
@@ -63,6 +69,7 @@ module.exports = {
|
||||
sails.log.warn(`Invalid code or nonce! (IP: ${remoteAddress})`);
|
||||
return Errors.INVALID_CODE_OR_NONCE;
|
||||
})
|
||||
.intercept('invalidOIDCConfiguration', () => Errors.INVALID_OIDC_CONFIGURATION)
|
||||
.intercept('invalidUserinfoConfiguration', () => Errors.INVALID_USERINFO_CONFIGURATION)
|
||||
.intercept('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE)
|
||||
.intercept('usernameAlreadyInUse', () => Errors.USERNAME_ALREADY_IN_USE)
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const Errors = {
|
||||
ATTACHMENT_NOT_FOUND: {
|
||||
attachmentNotFound: 'Attachment not found',
|
||||
@@ -46,20 +43,20 @@ module.exports = {
|
||||
throw Errors.ATTACHMENT_NOT_FOUND;
|
||||
}
|
||||
|
||||
const filePath = path.join(
|
||||
sails.config.custom.attachmentsPath,
|
||||
attachment.dirname,
|
||||
'thumbnails',
|
||||
`cover-256.${attachment.image.thumbnailsExtension}`,
|
||||
);
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
let readStream;
|
||||
try {
|
||||
readStream = await fileManager.read(
|
||||
`${sails.config.custom.attachmentsPathSegment}/${attachment.dirname}/thumbnails/cover-256.${attachment.image.thumbnailsExtension}`,
|
||||
);
|
||||
} catch (error) {
|
||||
throw Errors.ATTACHMENT_NOT_FOUND;
|
||||
}
|
||||
|
||||
this.res.type('image/jpeg');
|
||||
this.res.set('Cache-Control', 'private, max-age=900'); // TODO: move to config
|
||||
|
||||
return exits.success(fs.createReadStream(filePath));
|
||||
return exits.success(readStream);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const Errors = {
|
||||
@@ -42,13 +41,14 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
|
||||
const filePath = path.join(
|
||||
sails.config.custom.attachmentsPath,
|
||||
attachment.dirname,
|
||||
attachment.filename,
|
||||
);
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
let readStream;
|
||||
try {
|
||||
readStream = await fileManager.read(
|
||||
`${sails.config.custom.attachmentsPathSegment}/${attachment.dirname}/${attachment.filename}`,
|
||||
);
|
||||
} catch (error) {
|
||||
throw Errors.ATTACHMENT_NOT_FOUND;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,6 @@ module.exports = {
|
||||
}
|
||||
this.res.set('Cache-Control', 'private, max-age=900'); // TODO: move to config
|
||||
|
||||
return exits.success(fs.createReadStream(filePath));
|
||||
return exits.success(readStream);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
const Errors = {
|
||||
INVALID_OIDC_CONFIGURATION: {
|
||||
invalidOidcConfiguration: 'Invalid OIDC configuration',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
fn() {
|
||||
exits: {
|
||||
invalidOidcConfiguration: {
|
||||
responseType: 'serverError',
|
||||
},
|
||||
},
|
||||
|
||||
async fn() {
|
||||
let oidc = null;
|
||||
if (sails.hooks.oidc.isActive()) {
|
||||
const oidcClient = sails.hooks.oidc.getClient();
|
||||
let oidcClient;
|
||||
try {
|
||||
oidcClient = await sails.hooks.oidc.getClient();
|
||||
} catch (error) {
|
||||
sails.log.warn(`Error while initializing OIDC client: ${error}`);
|
||||
throw Errors.INVALID_OIDC_CONFIGURATION;
|
||||
}
|
||||
|
||||
const authorizationUrlParams = {
|
||||
scope: sails.config.custom.oidcScopes,
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
const path = require('path');
|
||||
const rimraf = require('rimraf');
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
record: {
|
||||
@@ -50,8 +47,12 @@ module.exports = {
|
||||
const attachment = await Attachment.archiveOne(inputs.record.id);
|
||||
|
||||
if (attachment) {
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
try {
|
||||
rimraf.sync(path.join(sails.config.custom.attachmentsPath, attachment.dirname));
|
||||
await fileManager.deleteDir(
|
||||
`${sails.config.custom.attachmentsPathSegment}/${attachment.dirname}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(error.stack); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const rimraf = require('rimraf');
|
||||
const moveFile = require('move-file');
|
||||
const { rimraf } = require('rimraf');
|
||||
const { v4: uuid } = require('uuid');
|
||||
const sharp = require('sharp');
|
||||
|
||||
@@ -16,17 +13,19 @@ module.exports = {
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const dirname = uuid();
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
const dirname = uuid();
|
||||
const dirPathSegment = `${sails.config.custom.attachmentsPathSegment}/${dirname}`;
|
||||
const filename = filenamify(inputs.file.filename);
|
||||
|
||||
const rootPath = path.join(sails.config.custom.attachmentsPath, dirname);
|
||||
const filePath = path.join(rootPath, filename);
|
||||
const filePath = await fileManager.move(
|
||||
inputs.file.fd,
|
||||
`${dirPathSegment}/${filename}`,
|
||||
inputs.file.type,
|
||||
);
|
||||
|
||||
fs.mkdirSync(rootPath);
|
||||
await moveFile(inputs.file.fd, filePath);
|
||||
|
||||
let image = sharp(filePath, {
|
||||
let image = sharp(filePath || inputs.file.fd, {
|
||||
animated: true,
|
||||
});
|
||||
|
||||
@@ -43,9 +42,6 @@ module.exports = {
|
||||
};
|
||||
|
||||
if (metadata && !['svg', 'pdf'].includes(metadata.format)) {
|
||||
const thumbnailsPath = path.join(rootPath, 'thumbnails');
|
||||
fs.mkdirSync(thumbnailsPath);
|
||||
|
||||
let { width, pageHeight: height = metadata.height } = metadata;
|
||||
if (metadata.orientation && metadata.orientation > 4) {
|
||||
[image, width, height] = [image.rotate(), height, width];
|
||||
@@ -55,7 +51,7 @@ module.exports = {
|
||||
const thumbnailsExtension = metadata.format === 'jpeg' ? 'jpg' : metadata.format;
|
||||
|
||||
try {
|
||||
await image
|
||||
const resizeBuffer = await image
|
||||
.resize(
|
||||
256,
|
||||
isPortrait ? 320 : undefined,
|
||||
@@ -65,19 +61,29 @@ module.exports = {
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
.toFile(path.join(thumbnailsPath, `cover-256.${thumbnailsExtension}`));
|
||||
.toBuffer();
|
||||
|
||||
await fileManager.save(
|
||||
`${dirPathSegment}/thumbnails/cover-256.${thumbnailsExtension}`,
|
||||
resizeBuffer,
|
||||
inputs.file.type,
|
||||
);
|
||||
|
||||
fileData.image = {
|
||||
width,
|
||||
height,
|
||||
thumbnailsExtension,
|
||||
};
|
||||
} catch (error1) {
|
||||
try {
|
||||
rimraf.sync(thumbnailsPath);
|
||||
} catch (error2) {
|
||||
console.warn(error2.stack); // eslint-disable-line no-console
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(error.stack); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
try {
|
||||
await rimraf(inputs.file.fd);
|
||||
} catch (error) {
|
||||
console.warn(error.stack); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const fs = require('fs').promises;
|
||||
const rimraf = require('rimraf');
|
||||
const fs = require('fs');
|
||||
const { rimraf } = require('rimraf');
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
@@ -14,7 +14,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const content = await fs.readFile(inputs.file.fd);
|
||||
const content = await fs.promises.readFile(inputs.file.fd);
|
||||
const trelloBoard = JSON.parse(content);
|
||||
|
||||
if (
|
||||
@@ -28,7 +28,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
try {
|
||||
rimraf.sync(inputs.file.fd);
|
||||
await rimraf(inputs.file.fd);
|
||||
} catch (error) {
|
||||
console.warn(error.stack); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const rimraf = require('rimraf');
|
||||
const { rimraf } = require('rimraf');
|
||||
const { v4: uuid } = require('uuid');
|
||||
const sharp = require('sharp');
|
||||
|
||||
@@ -32,10 +30,10 @@ module.exports = {
|
||||
throw 'fileIsNotImage';
|
||||
}
|
||||
|
||||
const dirname = uuid();
|
||||
const rootPath = path.join(sails.config.custom.projectBackgroundImagesPath, dirname);
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
fs.mkdirSync(rootPath);
|
||||
const dirname = uuid();
|
||||
const dirPathSegment = `${sails.config.custom.projectBackgroundImagesPathSegment}/${dirname}`;
|
||||
|
||||
let { width, pageHeight: height = metadata.height } = metadata;
|
||||
if (metadata.orientation && metadata.orientation > 4) {
|
||||
@@ -45,9 +43,15 @@ module.exports = {
|
||||
const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format;
|
||||
|
||||
try {
|
||||
await image.toFile(path.join(rootPath, `original.${extension}`));
|
||||
const originalBuffer = await image.toBuffer();
|
||||
|
||||
await image
|
||||
await fileManager.save(
|
||||
`${dirPathSegment}/original.${extension}`,
|
||||
originalBuffer,
|
||||
inputs.file.type,
|
||||
);
|
||||
|
||||
const cover336Buffer = await image
|
||||
.resize(
|
||||
336,
|
||||
200,
|
||||
@@ -57,10 +61,18 @@ module.exports = {
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
.toFile(path.join(rootPath, `cover-336.${extension}`));
|
||||
.toBuffer();
|
||||
|
||||
await fileManager.save(
|
||||
`${dirPathSegment}/cover-336.${extension}`,
|
||||
cover336Buffer,
|
||||
inputs.file.type,
|
||||
);
|
||||
} catch (error1) {
|
||||
console.warn(error1.stack); // eslint-disable-line no-console
|
||||
|
||||
try {
|
||||
rimraf.sync(rootPath);
|
||||
fileManager.deleteDir(dirPathSegment);
|
||||
} catch (error2) {
|
||||
console.warn(error2.stack); // eslint-disable-line no-console
|
||||
}
|
||||
@@ -69,7 +81,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
try {
|
||||
rimraf.sync(inputs.file.fd);
|
||||
await rimraf(inputs.file.fd);
|
||||
} catch (error) {
|
||||
console.warn(error.stack); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
const path = require('path');
|
||||
const rimraf = require('rimraf');
|
||||
|
||||
const valuesValidator = (value) => {
|
||||
if (!_.isPlainObject(value)) {
|
||||
return false;
|
||||
@@ -86,12 +83,11 @@ module.exports = {
|
||||
(!project.backgroundImage ||
|
||||
project.backgroundImage.dirname !== inputs.record.backgroundImage.dirname)
|
||||
) {
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
try {
|
||||
rimraf.sync(
|
||||
path.join(
|
||||
sails.config.custom.projectBackgroundImagesPath,
|
||||
inputs.record.backgroundImage.dirname,
|
||||
),
|
||||
await fileManager.deleteDir(
|
||||
`${sails.config.custom.projectBackgroundImagesPathSegment}/${inputs.record.backgroundImage.dirname}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(error.stack); // eslint-disable-line no-console
|
||||
|
||||
@@ -11,6 +11,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
invalidOIDCConfiguration: {},
|
||||
invalidCodeOrNonce: {},
|
||||
invalidUserinfoConfiguration: {},
|
||||
missingValues: {},
|
||||
@@ -19,7 +20,13 @@ module.exports = {
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const client = sails.hooks.oidc.getClient();
|
||||
let client;
|
||||
try {
|
||||
client = await sails.hooks.oidc.getClient();
|
||||
} catch (error) {
|
||||
sails.log.warn(`Error while initializing OIDC client: ${error}`);
|
||||
throw 'invalidOIDCConfiguration';
|
||||
}
|
||||
|
||||
let tokenSet;
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const rimraf = require('rimraf');
|
||||
const { rimraf } = require('rimraf');
|
||||
const { v4: uuid } = require('uuid');
|
||||
const sharp = require('sharp');
|
||||
|
||||
@@ -32,10 +30,10 @@ module.exports = {
|
||||
throw 'fileIsNotImage';
|
||||
}
|
||||
|
||||
const dirname = uuid();
|
||||
const rootPath = path.join(sails.config.custom.userAvatarsPath, dirname);
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
fs.mkdirSync(rootPath);
|
||||
const dirname = uuid();
|
||||
const dirPathSegment = `${sails.config.custom.userAvatarsPathSegment}/${dirname}`;
|
||||
|
||||
let { width, pageHeight: height = metadata.height } = metadata;
|
||||
if (metadata.orientation && metadata.orientation > 4) {
|
||||
@@ -45,9 +43,15 @@ module.exports = {
|
||||
const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format;
|
||||
|
||||
try {
|
||||
await image.toFile(path.join(rootPath, `original.${extension}`));
|
||||
const originalBuffer = await image.toBuffer();
|
||||
|
||||
await image
|
||||
await fileManager.save(
|
||||
`${dirPathSegment}/original.${extension}`,
|
||||
originalBuffer,
|
||||
inputs.file.type,
|
||||
);
|
||||
|
||||
const square100Buffer = await image
|
||||
.resize(
|
||||
100,
|
||||
100,
|
||||
@@ -57,10 +61,18 @@ module.exports = {
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
.toFile(path.join(rootPath, `square-100.${extension}`));
|
||||
.toBuffer();
|
||||
|
||||
await fileManager.save(
|
||||
`${dirPathSegment}/square-100.${extension}`,
|
||||
square100Buffer,
|
||||
inputs.file.type,
|
||||
);
|
||||
} catch (error1) {
|
||||
console.warn(error1.stack); // eslint-disable-line no-console
|
||||
|
||||
try {
|
||||
rimraf.sync(rootPath);
|
||||
fileManager.deleteDir(dirPathSegment);
|
||||
} catch (error2) {
|
||||
console.warn(error2.stack); // eslint-disable-line no-console
|
||||
}
|
||||
@@ -69,7 +81,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
try {
|
||||
rimraf.sync(inputs.file.fd);
|
||||
await rimraf(inputs.file.fd);
|
||||
} catch (error) {
|
||||
console.warn(error.stack); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
const path = require('path');
|
||||
const bcrypt = require('bcrypt');
|
||||
const rimraf = require('rimraf');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
const valuesValidator = (value) => {
|
||||
@@ -101,8 +99,12 @@ module.exports = {
|
||||
inputs.record.avatar &&
|
||||
(!user.avatar || user.avatar.dirname !== inputs.record.avatar.dirname)
|
||||
) {
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
try {
|
||||
rimraf.sync(path.join(sails.config.custom.userAvatarsPath, inputs.record.avatar.dirname));
|
||||
await fileManager.deleteDir(
|
||||
`${sails.config.custom.userAvatarsPathSegment}/${inputs.record.avatar.dirname}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(error.stack); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const { v4: uuid } = require('uuid');
|
||||
async function doUpload(paramName, req, options) {
|
||||
const uploadOptions = {
|
||||
...options,
|
||||
dirname: options.dirname || sails.config.custom.fileUploadTmpDir,
|
||||
dirname: options.dirname || sails.config.custom.uploadsTempPath,
|
||||
};
|
||||
const upload = util.promisify((opts, callback) => {
|
||||
return req.file(paramName).upload(opts, (error, files) => callback(error, files));
|
||||
@@ -33,7 +33,7 @@ module.exports = {
|
||||
exits.success(
|
||||
await doUpload(inputs.paramName, inputs.req, {
|
||||
saveAs: uuid(),
|
||||
dirname: sails.config.custom.fileUploadTmpDir,
|
||||
dirname: sails.config.custom.uploadsTempPath,
|
||||
maxBytes: null,
|
||||
}),
|
||||
);
|
||||
|
||||
51
server/api/hooks/file-manager/LocalFileManager.js
Normal file
51
server/api/hooks/file-manager/LocalFileManager.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const fs = require('fs');
|
||||
const fse = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { rimraf } = require('rimraf');
|
||||
|
||||
const PATH_SEGMENT_TO_URL_REPLACE_REGEX = /(public|private)\//;
|
||||
|
||||
const buildPath = (pathSegment) => path.join(sails.config.custom.uploadsBasePath, pathSegment);
|
||||
|
||||
class LocalFileManager {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async move(sourceFilePath, filePathSegment) {
|
||||
const { dir, base } = path.parse(filePathSegment);
|
||||
|
||||
const dirPath = buildPath(dir);
|
||||
const filePath = path.join(dirPath, base);
|
||||
|
||||
await fs.promises.mkdir(dirPath);
|
||||
await fse.move(sourceFilePath, filePath);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async save(filePathSegment, buffer) {
|
||||
await fse.outputFile(buildPath(filePathSegment), buffer);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
read(filePathSegment) {
|
||||
const filePath = buildPath(filePathSegment);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error('File does not exist');
|
||||
}
|
||||
|
||||
return fs.createReadStream(filePath);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async deleteDir(dirPathSegment) {
|
||||
await rimraf(buildPath(dirPathSegment));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
buildUrl(filePathSegment) {
|
||||
return `${sails.config.custom.baseUrl}/${filePathSegment.replace(PATH_SEGMENT_TO_URL_REPLACE_REGEX, '')}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LocalFileManager;
|
||||
76
server/api/hooks/file-manager/S3FileManager.js
Normal file
76
server/api/hooks/file-manager/S3FileManager.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const fs = require('fs');
|
||||
const {
|
||||
DeleteObjectsCommand,
|
||||
GetObjectCommand,
|
||||
ListObjectsV2Command,
|
||||
PutObjectCommand,
|
||||
} = require('@aws-sdk/client-s3');
|
||||
|
||||
class S3FileManager {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async move(sourceFilePath, filePathSegment, contentType) {
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: sails.config.custom.s3Bucket,
|
||||
Key: filePathSegment,
|
||||
Body: fs.createReadStream(sourceFilePath),
|
||||
ContentType: contentType,
|
||||
});
|
||||
|
||||
await this.client.send(command);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async save(filePathSegment, buffer, contentType) {
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: sails.config.custom.s3Bucket,
|
||||
Key: filePathSegment,
|
||||
Body: buffer,
|
||||
ContentType: contentType,
|
||||
});
|
||||
|
||||
await this.client.send(command);
|
||||
}
|
||||
|
||||
async read(filePathSegment) {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: sails.config.custom.s3Bucket,
|
||||
Key: filePathSegment,
|
||||
});
|
||||
|
||||
const result = await this.client.send(command);
|
||||
return result.Body;
|
||||
}
|
||||
|
||||
async deleteDir(dirPathSegment) {
|
||||
const listObjectsCommand = new ListObjectsV2Command({
|
||||
Bucket: sails.config.custom.s3Bucket,
|
||||
Prefix: dirPathSegment,
|
||||
});
|
||||
|
||||
const result = await this.client.send(listObjectsCommand);
|
||||
|
||||
if (!result.Contents || result.Contents.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteObjectsCommand = new DeleteObjectsCommand({
|
||||
Bucket: sails.config.custom.s3Bucket,
|
||||
Delete: {
|
||||
Objects: result.Contents.map(({ Key }) => ({ Key })),
|
||||
},
|
||||
});
|
||||
|
||||
await this.client.send(deleteObjectsCommand);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
buildUrl(filePathSegment) {
|
||||
return `${sails.hooks.s3.getBaseUrl()}/${filePathSegment}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = S3FileManager;
|
||||
41
server/api/hooks/file-manager/index.js
Normal file
41
server/api/hooks/file-manager/index.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const LocalFileManager = require('./LocalFileManager');
|
||||
const S3FileManager = require('./S3FileManager');
|
||||
|
||||
/**
|
||||
* file-manager hook
|
||||
*
|
||||
* @description :: A hook definition. Extends Sails by adding shadow routes, implicit actions,
|
||||
* and/or initialization logic.
|
||||
* @docs :: https://sailsjs.com/docs/concepts/extending-sails/hooks
|
||||
*/
|
||||
|
||||
module.exports = function defineFileManagerHook(sails) {
|
||||
let instance = null;
|
||||
|
||||
const createInstance = () => {
|
||||
instance = sails.hooks.s3.isActive()
|
||||
? new S3FileManager(sails.hooks.s3.getClient())
|
||||
: new LocalFileManager();
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Runs when this Sails app loads/lifts.
|
||||
*/
|
||||
|
||||
async initialize() {
|
||||
sails.log.info('Initializing custom hook (`file-manager`)');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
sails.after('hook:s3:loaded', () => {
|
||||
createInstance();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getInstance() {
|
||||
return instance;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -15,37 +15,40 @@ module.exports = function defineOidcHook(sails) {
|
||||
/**
|
||||
* Runs when this Sails app loads/lifts.
|
||||
*/
|
||||
|
||||
async initialize() {
|
||||
if (!sails.config.custom.oidcIssuer) {
|
||||
if (!this.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
sails.log.info('Initializing custom hook (`oidc`)');
|
||||
|
||||
const issuer = await openidClient.Issuer.discover(sails.config.custom.oidcIssuer);
|
||||
|
||||
const metadata = {
|
||||
client_id: sails.config.custom.oidcClientId,
|
||||
client_secret: sails.config.custom.oidcClientSecret,
|
||||
redirect_uris: [sails.config.custom.oidcRedirectUri],
|
||||
response_types: ['code'],
|
||||
userinfo_signed_response_alg: sails.config.custom.oidcUserinfoSignedResponseAlg,
|
||||
};
|
||||
|
||||
if (sails.config.custom.oidcIdTokenSignedResponseAlg) {
|
||||
metadata.id_token_signed_response_alg = sails.config.custom.oidcIdTokenSignedResponseAlg;
|
||||
}
|
||||
|
||||
client = new issuer.Client(metadata);
|
||||
},
|
||||
|
||||
getClient() {
|
||||
async getClient() {
|
||||
if (client === null && this.isActive()) {
|
||||
sails.log.info('Initializing OIDC client');
|
||||
|
||||
const issuer = await openidClient.Issuer.discover(sails.config.custom.oidcIssuer);
|
||||
|
||||
const metadata = {
|
||||
client_id: sails.config.custom.oidcClientId,
|
||||
client_secret: sails.config.custom.oidcClientSecret,
|
||||
redirect_uris: [sails.config.custom.oidcRedirectUri],
|
||||
response_types: ['code'],
|
||||
userinfo_signed_response_alg: sails.config.custom.oidcUserinfoSignedResponseAlg,
|
||||
};
|
||||
|
||||
if (sails.config.custom.oidcIdTokenSignedResponseAlg) {
|
||||
metadata.id_token_signed_response_alg = sails.config.custom.oidcIdTokenSignedResponseAlg;
|
||||
}
|
||||
|
||||
client = new issuer.Client(metadata);
|
||||
}
|
||||
|
||||
return client;
|
||||
},
|
||||
|
||||
isActive() {
|
||||
return client !== null;
|
||||
return sails.config.custom.oidcIssuer !== undefined;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
64
server/api/hooks/s3/index.js
Normal file
64
server/api/hooks/s3/index.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const { URL } = require('url');
|
||||
const { S3Client } = require('@aws-sdk/client-s3');
|
||||
|
||||
/**
|
||||
* s3 hook
|
||||
*
|
||||
* @description :: A hook definition. Extends Sails by adding shadow routes, implicit actions,
|
||||
* and/or initialization logic.
|
||||
* @docs :: https://sailsjs.com/docs/concepts/extending-sails/hooks
|
||||
*/
|
||||
|
||||
module.exports = function defineS3Hook(sails) {
|
||||
let client = null;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Runs when this Sails app loads/lifts.
|
||||
*/
|
||||
|
||||
async initialize() {
|
||||
if (!sails.config.custom.s3Endpoint && !sails.config.custom.s3Region) {
|
||||
return;
|
||||
}
|
||||
|
||||
sails.log.info('Initializing custom hook (`s3`)');
|
||||
|
||||
client = new S3Client({
|
||||
endpoint: sails.config.custom.s3Endpoint,
|
||||
region: sails.config.custom.s3Region || '-',
|
||||
credentials: {
|
||||
accessKeyId: sails.config.custom.s3AccessKeyId,
|
||||
secretAccessKey: sails.config.custom.s3SecretAccessKey,
|
||||
},
|
||||
forcePathStyle: sails.config.custom.s3ForcePathStyle,
|
||||
});
|
||||
},
|
||||
|
||||
getClient() {
|
||||
return client;
|
||||
},
|
||||
|
||||
getBaseUrl() {
|
||||
if (sails.config.custom.s3Endpoint) {
|
||||
const { protocol, host } = new URL(sails.config.custom.s3Endpoint);
|
||||
|
||||
if (sails.config.custom.s3ForcePathStyle) {
|
||||
return `${protocol}//${host}/${sails.config.custom.s3Bucket}`;
|
||||
}
|
||||
|
||||
return `${protocol}//${sails.config.custom.s3Bucket}.${host}`;
|
||||
}
|
||||
|
||||
if (sails.config.custom.s3ForcePathStyle) {
|
||||
return `https://s3.${sails.config.custom.s3Region}.amazonaws.com/${sails.config.custom.s3Bucket}`;
|
||||
}
|
||||
|
||||
return `https://${sails.config.custom.s3Bucket}.s3.${sails.config.custom.s3Region}.amazonaws.com`;
|
||||
},
|
||||
|
||||
isActive() {
|
||||
return client !== null;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -50,9 +50,9 @@ module.exports = {
|
||||
customToJSON() {
|
||||
return {
|
||||
..._.omit(this, ['dirname', 'filename', 'image.thumbnailsExtension']),
|
||||
url: `${sails.config.custom.attachmentsUrl}/${this.id}/download/${this.filename}`,
|
||||
url: `${sails.config.custom.baseUrl}/attachments/${this.id}/download/${this.filename}`,
|
||||
coverUrl: this.image
|
||||
? `${sails.config.custom.attachmentsUrl}/${this.id}/download/thumbnails/cover-256.${this.image.thumbnailsExtension}`
|
||||
? `${sails.config.custom.baseUrl}/attachments/${this.id}/download/thumbnails/cover-256.${this.image.thumbnailsExtension}`
|
||||
: null,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -79,11 +79,13 @@ module.exports = {
|
||||
},
|
||||
|
||||
customToJSON() {
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
return {
|
||||
..._.omit(this, ['backgroundImage']),
|
||||
backgroundImage: this.backgroundImage && {
|
||||
url: `${sails.config.custom.projectBackgroundImagesUrl}/${this.backgroundImage.dirname}/original.${this.backgroundImage.extension}`,
|
||||
coverUrl: `${sails.config.custom.projectBackgroundImagesUrl}/${this.backgroundImage.dirname}/cover-336.${this.backgroundImage.extension}`,
|
||||
url: `${fileManager.buildUrl(`${sails.config.custom.projectBackgroundImagesPathSegment}/${this.backgroundImage.dirname}/original.${this.backgroundImage.extension}`)}`,
|
||||
coverUrl: `${fileManager.buildUrl(`${sails.config.custom.projectBackgroundImagesPathSegment}/${this.backgroundImage.dirname}/cover-336.${this.backgroundImage.extension}`)}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -27,6 +27,8 @@ const LANGUAGES = [
|
||||
'ro-RO',
|
||||
'ru-RU',
|
||||
'sk-SK',
|
||||
'sr-Cyrl-CS',
|
||||
'sr-Latn-CS',
|
||||
'sv-SE',
|
||||
'tr-TR',
|
||||
'uk-UA',
|
||||
@@ -147,6 +149,7 @@ module.exports = {
|
||||
tableName: 'user_account',
|
||||
|
||||
customToJSON() {
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
const isDefaultAdmin = this.email === sails.config.custom.defaultAdminEmail;
|
||||
|
||||
return {
|
||||
@@ -157,7 +160,7 @@ module.exports = {
|
||||
isDeletionLocked: isDefaultAdmin,
|
||||
avatarUrl:
|
||||
this.avatar &&
|
||||
`${sails.config.custom.userAvatarsUrl}/${this.avatar.dirname}/square-100.${this.avatar.extension}`,
|
||||
`${fileManager.buildUrl(`${sails.config.custom.userAvatarsPathSegment}/${this.avatar.dirname}/square-100.${this.avatar.extension}`)}`,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,11 +8,10 @@
|
||||
* https://sailsjs.com/config/custom
|
||||
*/
|
||||
|
||||
const url = require('url');
|
||||
const path = require('path');
|
||||
const { URL } = require('url');
|
||||
const sails = require('sails');
|
||||
|
||||
const parsedBasedUrl = new url.URL(process.env.BASE_URL);
|
||||
const parsedBasedUrl = new URL(process.env.BASE_URL);
|
||||
|
||||
module.exports.custom = {
|
||||
/**
|
||||
@@ -28,24 +27,26 @@ module.exports.custom = {
|
||||
tokenExpiresIn: parseInt(process.env.TOKEN_EXPIRES_IN, 10) || 365,
|
||||
|
||||
// Location to receive uploaded files in. Default (non-string value) is a Sails-specific location.
|
||||
fileUploadTmpDir: null,
|
||||
uploadsTempPath: null,
|
||||
uploadsBasePath: sails.config.appPath,
|
||||
|
||||
userAvatarsPath: path.join(sails.config.paths.public, 'user-avatars'),
|
||||
userAvatarsUrl: `${process.env.BASE_URL}/user-avatars`,
|
||||
|
||||
projectBackgroundImagesPath: path.join(sails.config.paths.public, 'project-background-images'),
|
||||
projectBackgroundImagesUrl: `${process.env.BASE_URL}/project-background-images`,
|
||||
|
||||
attachmentsPath: path.join(sails.config.appPath, 'private', 'attachments'),
|
||||
attachmentsUrl: `${process.env.BASE_URL}/attachments`,
|
||||
userAvatarsPathSegment: 'public/user-avatars',
|
||||
projectBackgroundImagesPathSegment: 'public/project-background-images',
|
||||
attachmentsPathSegment: 'private/attachments',
|
||||
|
||||
defaultAdminEmail:
|
||||
process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase(),
|
||||
|
||||
showDetailedAuthErrors: process.env.SHOW_DETAILED_AUTH_ERRORS === 'true',
|
||||
|
||||
allowAllToCreateProjects: process.env.ALLOW_ALL_TO_CREATE_PROJECTS === 'true',
|
||||
|
||||
s3Endpoint: process.env.S3_ENDPOINT,
|
||||
s3Region: process.env.S3_REGION,
|
||||
s3AccessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||||
s3SecretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||
s3Bucket: process.env.S3_BUCKET,
|
||||
s3ForcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
|
||||
|
||||
oidcIssuer: process.env.OIDC_ISSUER,
|
||||
oidcClientId: process.env.OIDC_CLIENT_ID,
|
||||
oidcClientSecret: process.env.OIDC_CLIENT_SECRET,
|
||||
|
||||
4
server/config/env/production.js
vendored
4
server/config/env/production.js
vendored
@@ -19,11 +19,11 @@
|
||||
* https://sailsjs.com/docs/concepts/deployment
|
||||
*/
|
||||
|
||||
const url = require('url');
|
||||
const { URL } = require('url');
|
||||
|
||||
const { customLogger } = require('../../utils/logger');
|
||||
|
||||
const parsedBasedUrl = new url.URL(process.env.BASE_URL);
|
||||
const parsedBasedUrl = new URL(process.env.BASE_URL);
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
|
||||
@@ -135,13 +135,21 @@ module.exports.routes = {
|
||||
'PATCH /api/notifications/:ids': 'notifications/update',
|
||||
|
||||
'GET /user-avatars/*': {
|
||||
fn: staticDirServer('/user-avatars', () => path.resolve(sails.config.custom.userAvatarsPath)),
|
||||
fn: staticDirServer('/user-avatars', () =>
|
||||
path.join(
|
||||
path.resolve(sails.config.custom.uploadsBasePath),
|
||||
sails.config.custom.userAvatarsPathSegment,
|
||||
),
|
||||
),
|
||||
skipAssets: false,
|
||||
},
|
||||
|
||||
'GET /project-background-images/*': {
|
||||
fn: staticDirServer('/project-background-images', () =>
|
||||
path.resolve(sails.config.custom.projectBackgroundImagesPath),
|
||||
path.join(
|
||||
path.resolve(sails.config.custom.uploadsBasePath),
|
||||
sails.config.custom.projectBackgroundImagesPathSegment,
|
||||
),
|
||||
),
|
||||
skipAssets: false,
|
||||
},
|
||||
|
||||
2496
server/package-lock.json
generated
2496
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,18 +27,19 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.698.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"dotenv-cli": "^7.4.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"knex": "^3.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"move-file": "^2.1.0",
|
||||
"nodemailer": "^6.9.15",
|
||||
"openid-client": "^5.7.0",
|
||||
"nodemailer": "^6.9.16",
|
||||
"openid-client": "^5.7.1",
|
||||
"rimraf": "^5.0.10",
|
||||
"sails": "^1.5.12",
|
||||
"sails": "^1.5.13",
|
||||
"sails-hook-orm": "^4.0.3",
|
||||
"sails-hook-sockets": "^3.0.1",
|
||||
"sails-postgresql": "^5.0.1",
|
||||
@@ -47,16 +48,16 @@
|
||||
"stream-to-array": "^2.3.0",
|
||||
"uuid": "^9.0.1",
|
||||
"validator": "^13.12.0",
|
||||
"winston": "^3.14.2",
|
||||
"winston": "^3.17.0",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.5.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"mocha": "^10.7.3",
|
||||
"nodemon": "^3.1.4"
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"mocha": "^10.8.2",
|
||||
"nodemon": "^3.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
||||
Reference in New Issue
Block a user