Compare commits

...
Author SHA1 Message Date
Maksim Eltyshev 5acf84c20b chore: Update version 2024-04-05 22:49:02 +02:00
Matthieu Bollot 6cd9da844f feat: Add ability to duplicate card (#668) 2024-04-05 22:40:35 +02:00
Brian Stinson d7847fe89b fix: Allow specifying external dburl in Helm (#672) 2024-04-05 10:38:46 +02:00
piero d033520927 fix: Update HorizontalPodAutoscaler version and spec in chart (#666)
Closes #667
2024-04-03 20:59:23 +02:00
Maksim Eltyshev d911030831 chore: Update version 2024-03-26 14:08:59 +01:00
Maksim Eltyshev 9d95ed6c41 fix: Fix OIDC authentication error when redirecting from another tab
Closes #650
2024-03-26 14:07:07 +01:00
Maksim Eltyshev 1a5a853fa3 fix: Fix scrollbar styles 2024-03-22 14:26:23 +01:00
Maksim Eltyshev bb358e01dd chore: Update version 2024-03-22 00:52:45 +01:00
Edouard 9f0fce098e feat: SMTP integration and email notifications (#631) 2024-03-22 00:14:09 +01:00
Maksim Eltyshev 60ab09771d fix: Add date-fns locale to Portuguese translation 2024-03-21 12:41:31 +01:00
Jeffrey cb3ef783ba feat: Add Dutch translation (#644) 2024-03-21 12:38:26 +01:00
Reuben Hu fe376b10c1 feat: Add Indonesian translation (#643) 2024-03-21 11:46:33 +01:00
IV16SL 786441cda0 fix: Fix Chinese translation (#641) 2024-03-20 19:13:21 +01:00
Chris 0d3cf0a32d feat: Add Portuguese translation (#640) 2024-03-20 14:55:17 +01:00
Maksim Eltyshev b4e01d7256 docs: Update features, add contributors 2024-03-13 11:02:52 +01:00
Maksim Eltyshev 73a641b935 chore: Update version 2024-03-12 20:44:27 +01:00
Maksim Eltyshev 6802a0dc69 fix: Fix case sensitivity of default admin environment variables 2024-03-12 20:40:46 +01:00
Daniel Hiller 81d6c43cd9 fix: Container build windows 2024-02-14 03:15:34 +01:00
Maksim Eltyshev ca6a1a14f5 fix: Fix nullable boolean inputs 2024-02-08 16:42:10 +01:00
Daniel Hiller c096444acb Update build-and-push-docker-image-dev.yml 2024-02-08 15:34:43 +01:00
Maksim Eltyshev 51d05f3cd3 fix: Include due date when importing from Trello
Closes #598
2024-02-08 13:30:07 +01:00
Daniel Hiller 221da3c455 ci: update workflow 2024-02-06 02:07:27 +01:00
Daniel Hiller 92098e136c feat: Add healthcheck for docker deployments
fix: Missing healthcheck.js

fix: missing healthcheck.js

fix: HEALTHCHECK command
2024-02-06 01:51:00 +01:00
Maksim Eltyshev fd5c33dd5d fix: Remove overflow of attachment dropzone 2024-02-05 13:24:50 +01:00
Maksim Eltyshev 0c1c7e2192 chore: Update version 2024-02-01 00:34:31 +01:00
Maksim Eltyshev b8d262f745 feat: Add ability to enforce SSO
Closes #543, closes #545
2024-02-01 00:31:15 +01:00
77 changed files with 1719 additions and 124 deletions
+8 -3
View File
@@ -6,7 +6,7 @@ RUN apk -U upgrade \
WORKDIR /app
COPY server/package.json server/package-lock.json .
COPY server/package.json server/package-lock.json ./
RUN npm install npm@latest --global \
&& npm install pnpm --global \
@@ -17,7 +17,7 @@ FROM node:lts AS client
WORKDIR /app
COPY client/package.json client/package-lock.json .
COPY client/package.json client/package-lock.json ./
RUN npm install npm@latest --global \
&& npm install pnpm --global \
@@ -38,6 +38,7 @@ WORKDIR /app
COPY --chown=node:node start.sh .
COPY --chown=node:node server .
COPY --chown=node:node healthcheck.js .
RUN mv .env.sample .env
@@ -52,4 +53,8 @@ VOLUME /app/private/attachments
EXPOSE 1337
CMD ["./start.sh"]
HEALTHCHECK --interval=10s --timeout=2s --start-period=15s \
CMD node ./healthcheck.js
CMD [ "bash", "start.sh" ]
+10 -5
View File
@@ -10,13 +10,14 @@
## Features
- Create projects, boards, lists, cards, labels and tasks
- Add card members, track time, set a due date, add attachments, write comments
- Markdown support in a card description and comment
- Add card members, track time, set due dates, add attachments, write comments
- Markdown support in card description and comments
- Filter by members and labels
- Customize project background
- Customize project backgrounds
- Real-time updates
- User notifications
- Internationalization
- Internal notifications
- Multiple interface languages
- Single sign-on via OpenID Connect
## How to deploy Planka
@@ -44,3 +45,7 @@ See the [development section](https://docs.planka.cloud/docs/Development).
## License
Planka is [AGPL-3.0 licensed](https://github.com/plankanban/planka/blob/master/LICENSE).
## Contributors
[![](https://contrib.rocks/image?repo=plankanban/planka)](https://github.com/plankanban/planka/graphs/contributors)
+2 -2
View File
@@ -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.1.16
version: 0.1.23
# 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.15.5"
appVersion: "1.16.2"
dependencies:
- alias: postgresql
+3 -2
View File
@@ -46,7 +46,7 @@ helm install planka . --set secretkey=$SECRETKEY \
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo"
--set admin_username="demo" \
--set ingress.enabled=true \
--set ingress.hosts[0].host=planka.example.dev \
@@ -55,7 +55,7 @@ helm install planka . --set secretkey=$SECRETKEY \
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo"
--set admin_username="demo" \
--set ingress.enabled=true \
--set ingress.hosts[0].host=planka.example.dev \
--set ingress.tls[0].secretName=planka-tls \
@@ -76,6 +76,7 @@ admin_name: "Demo Demo"
admin_username: "demo"
# Admin user
# Ingress
ingress:
enabled: true
hosts:
+5
View File
@@ -58,11 +58,16 @@ spec:
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- if not .Values.postgresql.enabled }}
- name: DATABASE_URL
value: {{ required "If the included postgresql deployment is disabled you need to define a Database URL in 'dburl'" .Values.dburl }}
{{- else }}
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: planka-postgresql-svcbind-custom-user
key: uri
{{- end }}
- name: BASE_URL
{{- if .Values.baseUrl }}
value: {{ .Values.baseUrl }}
+7 -3
View File
@@ -1,5 +1,5 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "planka.fullname" . }}
@@ -17,12 +17,16 @@ spec:
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}
+3 -1
View File
@@ -105,12 +105,14 @@ postgresql:
serviceBindings:
enabled: true
## Set this if you disable the built-in postgresql deployment
dburl:
## PVC-based data storage configuration
persistence:
enabled: false
# existingClaim: netbox-data
# storageClass: "-"
accessMode: ReadWriteOnce
size: 10Gi
+1 -1
View File
@@ -1 +1 @@
REACT_APP_VERSION=1.15.5
REACT_APP_VERSION=1.16.2
+34 -1
View File
@@ -23,10 +23,14 @@ createCard.failure = (localId, error) => ({
},
});
const handleCardCreate = (card) => ({
const handleCardCreate = (card, cardMemberships, cardLabels, tasks, attachments) => ({
type: ActionTypes.CARD_CREATE_HANDLE,
payload: {
card,
cardMemberships,
cardLabels,
tasks,
attachments,
},
});
@@ -60,6 +64,34 @@ const handleCardUpdate = (card) => ({
},
});
const duplicateCard = (id, card, taskIds) => ({
type: ActionTypes.CARD_DUPLICATE,
payload: {
id,
card,
taskIds,
},
});
duplicateCard.success = (localId, card, cardMemberships, cardLabels, tasks) => ({
type: ActionTypes.CARD_DUPLICATE__SUCCESS,
payload: {
localId,
card,
cardMemberships,
cardLabels,
tasks,
},
});
duplicateCard.failure = (id, error) => ({
type: ActionTypes.CARD_DUPLICATE__FAILURE,
payload: {
id,
error,
},
});
const deleteCard = (id) => ({
type: ActionTypes.CARD_DELETE,
payload: {
@@ -94,6 +126,7 @@ export default {
handleCardCreate,
updateCard,
handleCardUpdate,
duplicateCard,
deleteCard,
handleCardDelete,
};
+7
View File
@@ -57,6 +57,12 @@ const updateCard = (id, data, headers) =>
item: transformCard(body.item),
}));
const duplicateCard = (id, data, headers) =>
socket.post(`/cards/${id}/duplicate`, data, headers).then((body) => ({
...body,
item: transformCard(body.item),
}));
const deleteCard = (id, headers) =>
socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({
...body,
@@ -81,6 +87,7 @@ export default {
getCard,
updateCard,
deleteCard,
duplicateCard,
makeHandleCardCreate,
makeHandleCardUpdate,
makeHandleCardDelete,
@@ -4,7 +4,6 @@
max-height: 60vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
width: 100%;
&::-webkit-scrollbar {
@@ -85,7 +85,6 @@
height: 56px;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
&:hover {
height: 38px;
@@ -36,6 +36,7 @@ const ActionsStep = React.memo(
onUpdate,
onMove,
onTransfer,
onDuplicate,
onDelete,
onUserAdd,
onUserRemove,
@@ -76,6 +77,11 @@ const ActionsStep = React.memo(
openStep(StepTypes.MOVE);
}, [openStep]);
const handleDuplicateClick = useCallback(() => {
onDuplicate();
onClose();
}, [onDuplicate, onClose]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
@@ -207,6 +213,11 @@ const ActionsStep = React.memo(
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDuplicateClick}>
{t('action.duplicateCard', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteCard', {
context: 'title',
@@ -232,6 +243,7 @@ ActionsStep.propTypes = {
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired,
onDuplicate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
+3
View File
@@ -41,6 +41,7 @@ const Card = React.memo(
onUpdate,
onMove,
onTransfer,
onDuplicate,
onDelete,
onUserAdd,
onUserRemove,
@@ -185,6 +186,7 @@ const Card = React.memo(
onUpdate={onUpdate}
onMove={onMove}
onTransfer={onTransfer}
onDuplicate={onDuplicate}
onDelete={onDelete}
onUserAdd={onUserAdd}
onUserRemove={onUserRemove}
@@ -238,6 +240,7 @@ Card.propTypes = {
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired,
onDuplicate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
@@ -94,7 +94,7 @@ const AttachmentAddZone = React.memo(({ children, onCreate }) => {
return (
<>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div {...getRootProps()} className={styles.wrapper}>
<div {...getRootProps()}>
{isDragActive && <div className={styles.dropzone}>{t('common.dropFileToUpload')}</div>}
{children}
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
@@ -12,8 +12,4 @@
width: 100%;
z-index: 2001;
}
.wrapper {
overflow: hidden;
}
}
@@ -55,6 +55,7 @@ const CardModal = React.memo(
onUpdate,
onMove,
onTransfer,
onDuplicate,
onDelete,
onUserAdd,
onUserRemove,
@@ -140,6 +141,11 @@ const CardModal = React.memo(
});
}, [isSubscribed, onUpdate]);
const handleDuplicateClick = useCallback(() => {
onDuplicate();
onClose();
}, [onDuplicate, onClose]);
const handleGalleryOpen = useCallback(() => {
isGalleryOpened.current = true;
}, []);
@@ -496,6 +502,10 @@ const CardModal = React.memo(
{t('action.move')}
</Button>
</CardMovePopup>
<Button fluid className={styles.actionButton} onClick={handleDuplicateClick}>
<Icon name="copy outline" className={styles.actionIcon} />
{t('action.duplicate')}
</Button>
<DeletePopup
title="common.deleteCard"
content="common.areYouSureYouWantToDeleteThisCard"
@@ -555,6 +565,7 @@ CardModal.propTypes = {
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired,
onDuplicate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
@@ -49,7 +49,6 @@
max-height: 60vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 5px;
@@ -25,7 +25,6 @@
max-height: 60vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 5px;
@@ -43,7 +43,6 @@
max-height: calc(100vh - 268px);
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
width: 290px;
&:hover {
+14 -4
View File
@@ -68,6 +68,7 @@ const Login = React.memo(
isSubmittingUsingOidc,
error,
withOidc,
isOidcEnforced,
onAuthenticate,
onAuthenticateUsingOidc,
onMessageDismiss,
@@ -107,8 +108,10 @@ const Login = React.memo(
}, [onAuthenticate, data]);
useEffect(() => {
if (!isOidcEnforced) {
emailOrUsernameField.current.focus();
}, []);
}
}, [isOidcEnforced]);
useEffect(() => {
if (wasSubmitting && !isSubmitting && error) {
@@ -159,6 +162,7 @@ const Login = React.memo(
onDismiss={onMessageDismiss}
/>
)}
{!isOidcEnforced && (
<Form size="large" onSubmit={handleSubmit}>
<div className={styles.inputWrapper}>
<div className={styles.inputLabel}>{t('common.emailOrUsername')}</div>
@@ -195,15 +199,20 @@ const Login = React.memo(
disabled={isSubmitting || isSubmittingUsingOidc}
/>
</Form>
)}
{withOidc && (
<Button
type="button"
fluid={isOidcEnforced}
primary={isOidcEnforced}
size={isOidcEnforced ? 'large' : undefined}
icon={isOidcEnforced ? 'right arrow' : undefined}
labelPosition={isOidcEnforced ? 'right' : undefined}
content={t('action.logInWithSSO')}
loading={isSubmittingUsingOidc}
disabled={isSubmitting || isSubmittingUsingOidc}
onClick={onAuthenticateUsingOidc}
>
{t('action.logInWithSSO')}
</Button>
/>
)}
</div>
</div>
@@ -242,6 +251,7 @@ Login.propTypes = {
isSubmittingUsingOidc: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
withOidc: PropTypes.bool.isRequired,
isOidcEnforced: PropTypes.bool.isRequired,
onAuthenticate: PropTypes.func.isRequired,
onAuthenticateUsingOidc: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
@@ -4,7 +4,6 @@
max-height: 60vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 5px;
@@ -10,6 +10,7 @@ import Item from './Item';
const UsersModal = React.memo(
({
items,
canAdd,
onUpdate,
onUsernameUpdate,
onUsernameUpdateMessageDismiss,
@@ -130,11 +131,13 @@ const UsersModal = React.memo(
</Table.Body>
</Table>
</Modal.Content>
{canAdd && (
<Modal.Actions>
<UserAddPopupContainer>
<Button positive content={t('action.addUser')} />
</UserAddPopupContainer>
</Modal.Actions>
)}
</Modal>
);
},
@@ -142,6 +145,7 @@ const UsersModal = React.memo(
UsersModal.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canAdd: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onUsernameUpdate: PropTypes.func.isRequired,
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
+3
View File
@@ -194,6 +194,9 @@ export default {
CARD_TRANSFER: 'CARD_TRANSFER',
CARD_TRANSFER__SUCCESS: 'CARD_TRANSFER__SUCCESS',
CARD_TRANSFER__FAILURE: 'CARD_TRANSFER__FAILURE',
CARD_DUPLICATE: 'CARD_DUPLICATE',
CARD_DUPLICATE__SUCCESS: 'CARD_DUPLICATE__SUCCESS',
CARD_DUPLICATE__FAILURE: 'CARD_DUPLICATE__FAILURE',
CARD_DELETE: 'CARD_DELETE',
CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS',
CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE',
+2
View File
@@ -132,6 +132,8 @@ export default {
CURRENT_CARD_MOVE: `${PREFIX}/CURRENT_CARD_MOVE`,
CARD_TRANSFER: `${PREFIX}/CARD_TRANSFER`,
CURRENT_CARD_TRANSFER: `${PREFIX}/CURRENT_CARD_TRANSFER`,
CARD_DUPLICATE: `${PREFIX}/CARD_DUPLICATE`,
CURRENT_CARD_DUPLICATE: `${PREFIX}/CURRENT_CARD_DUPLICATE`,
CARD_DELETE: `${PREFIX}/CARD_DELETE`,
CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`,
CARD_DELETE_HANDLE: `${PREFIX}/CARD_DELETE_HANDLE`,
+1
View File
@@ -62,6 +62,7 @@ const mapDispatchToProps = (dispatch, { id }) =>
onUpdate: (data) => entryActions.updateCard(id, data),
onMove: (listId, index) => entryActions.moveCard(id, listId, index),
onTransfer: (boardId, listId) => entryActions.transferCard(id, boardId, listId),
onDuplicate: () => entryActions.duplicateCard(id),
onDelete: () => entryActions.deleteCard(id),
onUserAdd: (userId) => entryActions.addUserToCard(userId, id),
onUserRemove: (userId) => entryActions.removeUserFromCard(userId, id),
@@ -78,6 +78,7 @@ const mapDispatchToProps = (dispatch) =>
onUpdate: entryActions.updateCurrentCard,
onMove: entryActions.moveCurrentCard,
onTransfer: entryActions.transferCurrentCard,
onDuplicate: entryActions.duplicateCurrentCard,
onDelete: entryActions.deleteCurrentCard,
onUserAdd: entryActions.addUserToCurrentCard,
onUserRemove: entryActions.removeUserFromCurrentCard,
+1
View File
@@ -20,6 +20,7 @@ const mapStateToProps = (state) => {
isSubmittingUsingOidc,
error,
withOidc: !!oidcConfig,
isOidcEnforced: oidcConfig && oidcConfig.isEnforced,
};
};
@@ -6,10 +6,12 @@ import entryActions from '../entry-actions';
import UsersModal from '../components/UsersModal';
const mapStateToProps = (state) => {
const oidcConfig = selectors.selectOidcConfig(state);
const users = selectors.selectUsersExceptCurrent(state);
return {
items: users,
canAdd: !oidcConfig || !oidcConfig.isEnforced,
};
};
+14
View File
@@ -74,6 +74,18 @@ const transferCurrentCard = (boardId, listId, index = 0) => ({
},
});
const duplicateCard = (id) => ({
type: EntryActionTypes.CARD_DUPLICATE,
payload: {
id,
},
});
const duplicateCurrentCard = () => ({
type: EntryActionTypes.CURRENT_CARD_DUPLICATE,
payload: {},
});
const deleteCard = (id) => ({
type: EntryActionTypes.CARD_DELETE,
payload: {
@@ -103,6 +115,8 @@ export default {
moveCurrentCard,
transferCard,
transferCurrentCard,
duplicateCard,
duplicateCurrentCard,
deleteCard,
deleteCurrentCard,
handleCardDelete,
+3
View File
@@ -51,6 +51,7 @@ export default {
cardNotFound_title: 'Card Not Found',
cardOrActionAreDeleted: 'Card or action are deleted.',
color: 'Color',
copy_inline: 'copy',
createBoard_title: 'Create Board',
createLabel_title: 'Create Label',
createNewOneOrSelectExistingOne: 'Create a new one or select<br />an existing one.',
@@ -196,6 +197,8 @@ export default {
deleteTask: 'Delete task',
deleteTask_title: 'Delete Task',
deleteUser: 'Delete user',
duplicate: 'Duplicate',
duplicateCard_title: 'Duplicate Card',
edit: 'Edit',
editDueDate_title: 'Edit Due Date',
editDescription_title: 'Edit Description',
+1
View File
@@ -169,6 +169,7 @@ export default {
deleteTask: 'Supprimer la tâche',
deleteTask_title: 'Supprimer la tâche',
deleteUser: "Supprimer l'utilisateur",
duplicate: 'Dupliquer',
edit: 'Modifier',
editDueDate_title: "Modifier la date d'échéance",
editDescription_title: 'Éditer la description',
+241
View File
@@ -0,0 +1,241 @@
import dateFns from 'date-fns/locale/id';
export default {
dateFns,
format: {
date: 'dd MMM yyyy',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMM',
longDateTime: "d MMMM 'pada' p",
fullDate: 'd MMM, y',
fullDateTime: "d MMM, y 'pada' p",
},
translation: {
common: {
aboutPlanka: 'Tentang Planka',
account: 'Akun',
actions: 'Tindakan',
addAttachment_title: 'Tambah Lampiran',
addComment: 'Tambahkan komentar',
addManager_title: 'Tambahkan Manager',
addMember_title: 'Tambahkan Anggota',
addUser_title: 'Tambahkan Pengguna',
administrator: 'Administrator',
all: 'Semua',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Semua perubahan akan disimpan<br />setelah koneksi pulih.',
areYouSureYouWantToDeleteThisAttachment: 'Apakah anda ingin menghapus lampiran ini?',
areYouSureYouWantToDeleteThisBoard: 'Apakah anda ingin menghapus papan ini?',
areYouSureYouWantToDeleteThisCard: 'Apakah anda ingin menghapus kartu ini?',
areYouSureYouWantToDeleteThisComment: 'Apakah anda ingin menghapus komentar ini?',
areYouSureYouWantToDeleteThisLabel: 'Apakah anda ingin menghapus label ini?',
areYouSureYouWantToDeleteThisList: 'Apakah anda ingin menghapus daftar ini?',
areYouSureYouWantToDeleteThisProject: 'Apakah anda ingin menghapus proyek ini?',
areYouSureYouWantToDeleteThisTask: 'Apakah anda ingin menghapus tugas ini?',
areYouSureYouWantToDeleteThisUser: 'Apakah anda ingin menghapus pengguna ini?',
areYouSureYouWantToLeaveBoard: 'Apakah anda ingin keluar dari papan ini?',
areYouSureYouWantToLeaveProject: 'Apakah anda ingin keluar dari proyek ini?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Apakah anda ingin menghapus manajer ini dari papan ini?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Apakah anda ingin menghapus anggota ini dari papan ini?',
attachment: 'Lampiran',
attachments: 'Lampiran-lampiran',
authentication: 'Autentikasi',
background: 'Latar belakang',
board: 'Papan',
boardNotFound_title: 'Papan Tidak Ditemukan',
canComment: 'Bisa berkomentar',
canEditContentOfBoard: 'Bisa mengubah isi papan.',
canOnlyViewBoard: 'Hanya dapat menglihat isi papan.',
cardActions_title: 'Aksi Kartu',
cardNotFound_title: 'Kartu Tidak Ditemukan',
cardOrActionAreDeleted: 'Kartu atau aksi telah dihapus.',
color: 'Warna',
createBoard_title: 'Buat Papan',
createLabel_title: 'Buat Label',
createNewOneOrSelectExistingOne: 'Create a new one or select<br />an existing one.',
createProject_title: 'Buat Proyek',
createTextFile_title: 'Buat Berkas Teks',
currentPassword: 'Kata sandi sekarang',
dangerZone_title: 'Zona Berbahaya',
date: 'Tanggal',
dueDate_title: 'Tenggat Waktu',
deleteAttachment_title: 'Hapus Lampiran',
deleteBoard_title: 'Hapus Papan',
deleteCard_title: 'Hapus Kartu',
deleteComment_title: 'Hapus Komentar',
deleteLabel_title: 'Hapus Label',
deleteList_title: 'Hapus Daftar',
deleteProject_title: 'Hapus Proyek',
deleteTask_title: 'Hapus Tugas',
deleteUser_title: 'Hapus Pengguna',
description: 'Deskripsi',
detectAutomatically: 'Deteksi otomatis',
dropFileToUpload: 'Tarik berkas untuk menggungah',
editor: 'Pengubah',
editAttachment_title: 'Ubah Lampiran',
editAvatar_title: 'Ubah Avatar',
editBoard_title: 'Ubah Papan',
editDueDate_title: 'Ubah Tenggat Waktu',
editEmail_title: 'Ubah E-mail',
editInformation_title: 'Ubah Informasi',
editLabel_title: 'Ubah Label',
editPassword_title: 'Ubah Kata Sandi',
editPermissions_title: 'Ubah Izin',
editStopwatch_title: 'Ubah Stopwatch',
editUsername_title: 'Ubah Username',
email: 'E-mail',
emailAlreadyInUse: 'E-mail telah digunakan',
enterCardTitle: 'Masukkan judul kartu... [Ctrl+Enter] untuk membuka otomatis.',
enterDescription: 'Masukkan deskripsi...',
enterFilename: 'Masukkan nama berkas...',
enterListTitle: 'Masukkan judul daftar...',
enterProjectTitle: 'Masukkan judul proyek',
enterTaskDescription: 'Masukkan deskripsi tugas...',
filterByLabels_title: 'Saring berdasarkan Label',
filterByMembers_title: 'Saring berdasarkan Anggota',
fromComputer_title: 'Dari Komputer',
fromTrello: 'Dari Trello',
general: 'Umum',
hours: 'Jam',
importBoard_title: 'Impor Papan',
invalidCurrentPassword: 'Kata sandi saat ini tidak valid',
labels: 'Label',
language: 'Bahasa',
leaveBoard_title: 'Keluar dari Papan',
leaveProject_title: 'Keluar dari Proyek',
list: 'Daftar',
listActions_title: 'Aksi Daftar',
managers: 'Manager',
members: 'Anggota',
minutes: 'Menit',
moveCard_title: 'Pindahkan Kartu',
name: 'Nama',
newEmail: 'E-mail baru',
newPassword: 'Kata sandi baru',
newUsername: 'Username baru',
noConnectionToServer: 'Tidak ada koneksi ke server',
noBoards: 'Tidak ada papan',
noLists: 'Tidak ada daftar',
noProjects: 'Tidak ada projek',
notifications: 'Notifikasi',
noUnreadNotifications: 'Tiada notifikasi yang belum dibaca.',
openBoard_title: 'Buka Papan',
optional_inline: 'opsional',
organization: 'Organisasi',
phone: 'Ponsel',
preferences: 'Preferensi',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: tekan Ctrl-V (Cmd-V di Mac) untuk menambahkan lampiran dari papan klip.',
project: 'Proyek',
projectNotFound_title: 'Proyek Tidak Ditemukan',
removeManager_title: 'Hapus Manager',
removeMember_title: 'Hapus Anggota',
searchLabels: 'Cari label...',
searchMembers: 'Cari anggota...',
searchUsers: 'Cari pengguna...',
seconds: 'Detik',
selectBoard: 'Pilih papan',
selectList: 'Pilih daftar',
selectPermissions_title: 'Pilih Izin',
selectProject: 'Pilih proyek',
settings: 'Setelan',
stopwatch: 'Stopwatch',
subscribeToMyOwnCardsByDefault: 'Berlangganan kartu saya sendiri secara default',
taskActions_title: 'Aksi Tugas',
tasks: 'Tugas',
thereIsNoPreviewAvailableForThisAttachment:
'Tidak ada pratinjau yang tersedia untuk lampiran ini.',
time: 'Waktu',
title: 'Judul',
userActions_title: 'Aksi Pengguna',
userAddedThisCardToList: '<0>{{user}}</0><1> menambahkan kartu ini ke {{list}}</1>',
userLeftNewCommentToCard: '{{user}} mengomentari «{{comment}}» di <2>{{card}}</2>',
userMovedCardFromListToList:
'{{user}} memindahkan <2>{{card}}</2> dari {{fromList}} ke {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> memindahkan kartu ini dari {{fromList}} ke {{toList}}</1>',
username: 'Username',
usernameAlreadyInUse: 'Username telah digunakan',
users: 'Pengguna',
version: 'Versi',
viewer: 'Penglihat',
writeComment: 'Tuliskan komentar...',
},
action: {
addAnotherCard: 'Tambahkan kartu lain',
addAnotherList: 'Tambahkan daftar lain',
addAnotherTask: 'Tambahkan aksi lain',
addCard: 'Tambah kartu',
addCard_title: 'Tambah Kartu',
addComment: 'Tambah komentar',
addList: 'Tambah daftar',
addMember: 'Tambah anggota',
addMoreDetailedDescription: 'Tambahkan deskripsi yang lebih detail',
addTask: 'Tambah tugas',
addToCard: 'Tambahkan ke kartu',
addUser: 'Tambah pengguna',
createBoard: 'Tambah papan',
createFile: 'Tambah berkas',
createLabel: 'Tambah label',
createNewLabel: 'Tambah label baru',
createProject: 'Tambah proyek',
delete: 'Hapus',
deleteAttachment: 'Hapus lampiran',
deleteAvatar: 'Hapus avatar',
deleteBoard: 'Hapus papan',
deleteCard: 'Hapus kartu',
deleteCard_title: 'Hapus Kartu',
deleteComment: 'Hapus komentar',
deleteImage: 'Hapus gambar',
deleteLabel: 'Hapus labek',
deleteList: 'Hapus daftar',
deleteList_title: 'Hapus Daftar',
deleteProject: 'Hapus proyek',
deleteProject_title: 'Hapus Proyek',
deleteTask: 'Hapus tugas',
deleteTask_title: 'Hapus Tugas',
deleteUser: 'Hapus pengguna',
edit: 'Ubah',
editDueDate_title: 'Ubah Tenggat Waktu',
editDescription_title: 'Ubdah Deskripsi',
editEmail_title: 'Ubah E-mail',
editInformation_title: 'Ubah Informasi',
editPassword_title: 'Ubah Kata Sandi',
editPermissions: 'Ubah izin',
editStopwatch_title: 'Ubah Stopwatch',
editTitle_title: 'Ubah Judul',
editUsername_title: 'Ubah Username',
hideDetails: 'Sembunyikan detail',
import: 'Impor',
leaveBoard: 'Keluar dari papan',
leaveProject: 'Keluar dari proyek',
logOut_title: 'Keluar',
makeCover_title: 'Buat Cover',
move: 'Pindah',
moveCard_title: 'Pindahkan Kartu',
remove: 'Hapus',
removeBackground: 'Hapus latar belakang',
removeCover_title: 'Hapus Cover',
removeFromBoard: 'Hapus dari papan',
removeFromProject: 'Hapus dari proyek',
removeManager: 'Hapus manager',
removeMember: 'Hapus papan',
save: 'Simpan',
showAllAttachments: 'Tampilkan semua lampiran ({{hidden}} tersembunyi)',
showDetails: 'Tampilkan detail',
showFewerAttachments: 'Tampilkan lampiran lebih sedikit',
start: 'Mulai',
stop: 'Berhenti',
subscribe: 'Berlanggan',
unsubscribe: 'Berhenti berlangganan',
uploadNewAvatar: 'Unggah avatar baru',
uploadNewImage: 'Unggah gambar baru',
},
},
};
+8
View File
@@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'id',
country: 'id',
name: 'Bahasa Indonesia',
embeddedLocale: login,
};
+22
View File
@@ -0,0 +1,22 @@
export default {
translation: {
common: {
emailOrUsername: 'E-mail atau username',
invalidEmailOrUsername: 'E-mail atau username salah',
invalidPassword: 'Kata sandi salah',
logInToPlanka: 'Masuk ke Planka',
noInternetConnection: 'Tidak ada koneksi internet',
pageNotFound_title: 'Halaman Tidak Ditemukan',
password: 'Kata sandi',
projectManagement: 'Manajemen projek',
serverConnectionFailed: 'Koneksi server gagal',
unknownError: 'Kesalahan tidak diketahui, coba lagi nanti.',
useSingleSignOn: 'Gunakan single sign-on',
},
action: {
logIn: 'Masuk',
logInWithSSO: 'Masuk dengan SSO',
},
},
};
+26 -1
View File
@@ -4,10 +4,13 @@ import de from './de';
import en from './en';
import es from './es';
import fr from './fr';
import id from './id';
import it from './it';
import ja from './ja';
import ko from './ko';
import nl from './nl';
import pl from './pl';
import pt from './pt';
import ro from './ro';
import ru from './ru';
import sk from './sk';
@@ -17,7 +20,29 @@ import ua from './ua';
import uz from './uz';
import zh from './zh';
const locales = [cs, da, de, en, es, fr, it, ja, ko, pl, ro, ru, sk, sv, tr, ua, uz, zh];
const locales = [
cs,
da,
de,
en,
es,
fr,
id,
it,
ja,
ko,
nl,
pl,
pt,
ro,
ru,
sk,
sv,
tr,
ua,
uz,
zh,
];
export default locales;
+242
View File
@@ -0,0 +1,242 @@
import dateFns from 'date-fns/locale/nl';
export default {
dateFns,
format: {
date: 'd-M-yyyy',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMM',
longDateTime: "d MMMM 'om' p",
fullDate: 'd MMM y',
fullDateTime: "d MMMM y 'om' p",
},
translation: {
common: {
aboutPlanka: 'Over Planka',
account: 'Account',
actions: 'Acties',
addAttachment_title: 'Bijlage toevoegen',
addComment: 'Opmerking toevoegen',
addManager_title: 'Manager toevoegen',
addMember_title: 'Lid toevoegen',
addUser_title: 'Gebruiker toevoegen',
administrator: 'Beheerder',
all: 'Alle',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Alle wijzigingen worden automatisch opgeslagen<br />nadat de verbinding is hersteld.',
areYouSureYouWantToDeleteThisAttachment: 'Weet u zeker dat u deze bijlage wilt verwijderen?',
areYouSureYouWantToDeleteThisBoard: 'Weet u zeker dat u dit bord wilt verwijderen?',
areYouSureYouWantToDeleteThisCard: 'Weet u zeker dat u deze kaart wilt verwijderen?',
areYouSureYouWantToDeleteThisComment: 'Weet u zeker dat u deze opmerking wilt verwijderen?',
areYouSureYouWantToDeleteThisLabel: 'Weet u zeker dat u dit label wilt verwijderen?',
areYouSureYouWantToDeleteThisList: 'Weet u zeker dat u deze lijst wilt verwijderen?',
areYouSureYouWantToDeleteThisProject: 'Weet u zeker dat u dit project wilt verwijderen?',
areYouSureYouWantToDeleteThisTask: 'Weet u zeker dat u deze taak wilt verwijderen?',
areYouSureYouWantToDeleteThisUser: 'Weet u zeker dat u deze gebruiker wilt verwijderen?',
areYouSureYouWantToLeaveBoard: 'Weet u zeker dat u het bord wilt verlaten?',
areYouSureYouWantToLeaveProject: 'Weet u zeker dat u het project wilt verlaten?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Weet u zeker dat u deze manager uit het project wilt verwijderen?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Weet u zeker dat u dit lid uit het bord wilt verwijderen?',
attachment: 'Bijlage',
attachments: 'Bijlagen',
authentication: 'Authenticatie',
background: 'Achtergrond',
board: 'Bord',
boardNotFound_title: 'Bord niet gevonden',
canComment: 'Kan opmerking plaatsen',
canEditContentOfBoard: 'Kan de inhoud van het bord bewerken.',
canOnlyViewBoard: 'Kan alleen het bord bekijken.',
cardActions_title: 'Kaartacties',
cardNotFound_title: 'Kaart niet gevonden',
cardOrActionAreDeleted: 'Kaart of actie zijn verwijderd.',
color: 'Kleur',
createBoard_title: 'Bord aanmaken',
createLabel_title: 'Label aanmaken',
createNewOneOrSelectExistingOne: 'Maak een nieuwe of selecteer een bestaande.',
createProject_title: 'Project aanmaken',
createTextFile_title: 'Tekstbestand aanmaken',
currentPassword: 'Huidig wachtwoord',
dangerZone_title: 'Gevaarlijke zone',
date: 'Datum',
dueDate_title: 'Vervaldatum',
deleteAttachment_title: 'Bijlage verwijderen',
deleteBoard_title: 'Bord verwijderen',
deleteCard_title: 'Kaart verwijderen',
deleteComment_title: 'Opmerking verwijderen',
deleteLabel_title: 'Label verwijderen',
deleteList_title: 'Lijst verwijderen',
deleteProject_title: 'Project verwijderen',
deleteTask_title: 'Taak verwijderen',
deleteUser_title: 'Gebruiker verwijderen',
description: 'Beschrijving',
detectAutomatically: 'Automatisch detecteren',
dropFileToUpload: 'Sleep bestand om te uploaden',
editor: 'Editor',
editAttachment_title: 'Bijlage bewerken',
editAvatar_title: 'Avatar bewerken',
editBoard_title: 'Bord bewerken',
editDueDate_title: 'Vervaldatum bewerken',
editEmail_title: 'E-mail bewerken',
editInformation_title: 'Informatie bewerken',
editLabel_title: 'Label bewerken',
editPassword_title: 'Wachtwoord bewerken',
editPermissions_title: 'Machtigingen bewerken',
editStopwatch_title: 'Stopwatch bewerken',
editUsername_title: 'Gebruikersnaam bewerken',
email: 'E-mail',
emailAlreadyInUse: 'E-mail is al in gebruik',
enterCardTitle: 'Voer kaarttitel in... [Ctrl+Enter] om automatisch te openen.',
enterDescription: 'Beschrijving invoeren...',
enterFilename: 'Bestandsnaam invoeren',
enterListTitle: 'Voer lijsttitel in...',
enterProjectTitle: 'Voer projecttitel in',
enterTaskDescription: 'Taakbeschrijving invoeren...',
filterByLabels_title: 'Filteren op labels',
filterByMembers_title: 'Filteren op leden',
fromComputer_title: 'Van computer',
fromTrello: 'Van Trello',
general: 'Algemeen',
hours: 'Uren',
importBoard_title: 'Bord importeren',
invalidCurrentPassword: 'Ongeldig huidig wachtwoord',
labels: 'Labels',
language: 'Taal',
leaveBoard_title: 'Bord verlaten',
leaveProject_title: 'Project verlaten',
list: 'Lijst',
listActions_title: 'Lijstacties',
managers: 'Managers',
members: 'Leden',
minutes: 'Minuten',
moveCard_title: 'Kaart verplaatsen',
name: 'Naam',
newEmail: 'Nieuwe e-mail',
newPassword: 'Nieuw wachtwoord',
newUsername: 'Nieuwe gebruikersnaam',
noConnectionToServer: 'Geen verbinding met server',
noBoards: 'Geen borden',
noLists: 'Geen lijsten',
noProjects: 'Geen projecten',
notifications: 'Meldingen',
noUnreadNotifications: 'Geen ongelezen meldingen.',
openBoard_title: 'Bord openen',
optional_inline: 'optioneel',
organization: 'Organisatie',
phone: 'Telefoon',
preferences: 'Voorkeuren',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: druk op Ctrl-V (Cmd-V op Mac) om een bijlage van het klembord toe te voegen.',
project: 'Project',
projectNotFound_title: 'Project niet gevonden',
removeManager_title: 'Manager verwijderen',
removeMember_title: 'Lid verwijderen',
searchLabels: 'Labels zoeken...',
searchMembers: 'Leden zoeken...',
searchUsers: 'Gebruikers zoeken...',
seconds: 'Seconden',
selectBoard: 'Bord selecteren',
selectList: 'Lijst selecteren',
selectPermissions_title: 'Machtigingen selecteren',
selectProject: 'Project selecteren',
settings: 'Instellingen',
stopwatch: 'Stopwatch',
subscribeToMyOwnCardsByDefault: 'Standaard abonneren op mijn eigen kaarten',
taskActions_title: 'Takenacties',
tasks: 'Taken',
thereIsNoPreviewAvailableForThisAttachment:
'Er is geen voorbeeld beschikbaar voor deze bijlage.',
time: 'Tijd',
title: 'Titel',
userActions_title: 'Gebruikersacties',
userAddedThisCardToList: '<0>{{user}}</0><1> heeft deze kaart toegevoegd aan {{list}}</1>',
userLeftNewCommentToCard:
'{{user}} heeft een nieuwe opmerking achtergelaten «{{comment}}» bij <2>{{card}}</2>',
userMovedCardFromListToList:
'{{user}} heeft <2>{{card}}</2> verplaatst van {{fromList}} naar {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> heeft deze kaart verplaatst van {{fromList}} naar {{toList}}</1>',
username: 'Gebruikersnaam',
usernameAlreadyInUse: 'Gebruikersnaam is al in gebruik',
users: 'Gebruikers',
version: 'Versie',
viewer: 'Kijker',
writeComment: 'Schrijf een opmerking...',
},
action: {
addAnotherCard: 'Voeg nog een kaart toe',
addAnotherList: 'Voeg nog een lijst toe',
addAnotherTask: 'Voeg nog een taak toe',
addCard: 'Kaart toevoegen',
addCard_title: 'Kaart toevoegen',
addComment: 'Opmerking toevoegen',
addList: 'Lijst toevoegen',
addMember: 'Lid toevoegen',
addMoreDetailedDescription: 'Meer gedetailleerde beschrijving toevoegen',
addTask: 'Taak toevoegen',
addToCard: 'Toevoegen aan kaart',
addUser: 'Gebruiker toevoegen',
createBoard: 'Bord aanmaken',
createFile: 'Bestand aanmaken',
createLabel: 'Label aanmaken',
createNewLabel: 'Nieuw label aanmaken',
createProject: 'Project aanmaken',
delete: 'Verwijderen',
deleteAttachment: 'Bijlage verwijderen',
deleteAvatar: 'Avatar verwijderen',
deleteBoard: 'Bord verwijderen',
deleteCard: 'Kaart verwijderen',
deleteCard_title: 'Kaart verwijderen',
deleteComment: 'Opmerking verwijderen',
deleteImage: 'Afbeelding verwijderen',
deleteLabel: 'Label verwijderen',
deleteList: 'Lijst verwijderen',
deleteList_title: 'Lijst verwijderen',
deleteProject: 'Project verwijderen',
deleteProject_title: 'Project verwijderen',
deleteTask: 'Taak verwijderen',
deleteTask_title: 'Taak verwijderen',
deleteUser: 'Gebruiker verwijderen',
edit: 'Bewerken',
editDueDate_title: 'Vervaldatum bewerken',
editDescription_title: 'Beschrijving bewerken',
editEmail_title: 'E-mail bewerken',
editInformation_title: 'Informatie bewerken',
editPassword_title: 'Wachtwoord bewerken',
editPermissions: 'Machtigingen bewerken',
editStopwatch_title: 'Stopwatch bewerken',
editTitle_title: 'Titel bewerken',
editUsername_title: 'Gebruikersnaam bewerken',
hideDetails: 'Details verbergen',
import: 'Importeren',
leaveBoard: 'Bord verlaten',
leaveProject: 'Project verlaten',
logOut_title: 'Uitloggen',
makeCover_title: 'Omslag maken',
move: 'Verplaatsen',
moveCard_title: 'Kaart verplaatsen',
remove: 'Verwijderen',
removeBackground: 'Achtergrond verwijderen',
removeCover_title: 'Omslag verwijderen',
removeFromBoard: 'Verwijderen van bord',
removeFromProject: 'Verwijderen van project',
removeManager: 'Manager verwijderen',
removeMember: 'Lid verwijderen',
save: 'Opslaan',
showAllAttachments: 'Alle bijlagen weergeven ({{hidden}} verbergen)',
showDetails: 'Details weergeven',
showFewerAttachments: 'Minder bijlagen weergeven',
start: 'Start',
stop: 'Stop',
subscribe: 'Abonneren',
unsubscribe: 'Afmelden',
uploadNewAvatar: 'Nieuwe avatar uploaden',
uploadNewImage: 'Nieuwe afbeelding uploaden',
},
},
};
+8
View File
@@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'nl',
country: 'nl',
name: 'Nederlands',
embeddedLocale: login,
};
+22
View File
@@ -0,0 +1,22 @@
export default {
translation: {
common: {
emailOrUsername: 'E-mail of gebruikersnaam',
invalidEmailOrUsername: 'Ongeldig e-mailadres of gebruikersnaam',
invalidPassword: 'Ongeldig wachtwoord',
logInToPlanka: 'Inloggen bij Planka',
noInternetConnection: 'Geen internetverbinding',
pageNotFound_title: 'Pagina niet gevonden',
password: 'Wachtwoord',
projectManagement: 'Projectbeheer',
serverConnectionFailed: 'Verbinding met de server mislukt',
unknownError: 'Onbekende fout, probeer het later opnieuw',
useSingleSignOn: 'Gebruik single sign-on',
},
action: {
logIn: 'Inloggen',
logInWithSSO: 'Inloggen met SSO',
},
},
};
+241
View File
@@ -0,0 +1,241 @@
import dateFns from 'date-fns/locale/pt-BR';
export default {
dateFns,
format: {
date: 'dd/MM/yyyy',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMM',
longDateTime: "d 'de' MMMM 'às' p",
fullDate: 'd MMM, y',
fullDateTime: "d 'de' MMMM, y 'às' p",
},
translation: {
common: {
aboutPlanka: 'Sobre o Planka',
account: 'Conta',
actions: 'Ações',
addAttachment_title: 'Adicionar Anexo',
addComment: 'Adicionar comentário',
addManager_title: 'Adicionar Gerente',
addMember_title: 'Adicionar Membro',
addUser_title: 'Adicionar Usuário',
administrator: 'Administrador',
all: 'Todos',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Todas as alterações serão salvas automaticamente<br />após a conexão ser restaurada.',
areYouSureYouWantToDeleteThisAttachment: 'Tem certeza de que deseja excluir este anexo?',
areYouSureYouWantToDeleteThisBoard: 'Tem certeza de que deseja excluir este quadro?',
areYouSureYouWantToDeleteThisCard: 'Tem certeza de que deseja excluir este cartão?',
areYouSureYouWantToDeleteThisComment: 'Tem certeza de que deseja excluir este comentário?',
areYouSureYouWantToDeleteThisLabel: 'Tem certeza de que deseja excluir este rótulo?',
areYouSureYouWantToDeleteThisList: 'Tem certeza de que deseja excluir esta lista?',
areYouSureYouWantToDeleteThisProject: 'Tem certeza de que deseja excluir este projeto?',
areYouSureYouWantToDeleteThisTask: 'Tem certeza de que deseja excluir esta tarefa?',
areYouSureYouWantToDeleteThisUser: 'Tem certeza de que deseja excluir este usuário?',
areYouSureYouWantToLeaveBoard: 'Tem certeza de que deseja sair do quadro?',
areYouSureYouWantToLeaveProject: 'Tem certeza de que deseja sair do projeto?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Tem certeza de que deseja remover este gerente do projeto?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Tem certeza de que deseja remover este membro do quadro?',
attachment: 'Anexo',
attachments: 'Anexos',
authentication: 'Autenticação',
background: 'Fundo',
board: 'Quadro',
boardNotFound_title: 'Quadro não encontrado',
canComment: 'Pode comentar',
canEditContentOfBoard: 'Pode editar o conteúdo do quadro.',
canOnlyViewBoard: 'Só pode visualizar o quadro.',
cardActions_title: 'Ações do Cartão',
cardNotFound_title: 'Cartão não encontrado',
cardOrActionAreDeleted: 'Cartão ou ação foram excluídos.',
color: 'Cor',
createBoard_title: 'Criar Quadro',
createLabel_title: 'Criar Rótulo',
createNewOneOrSelectExistingOne: 'Criar um novo ou selecionar<br />um existente.',
createProject_title: 'Criar Projeto',
createTextFile_title: 'Criar Arquivo de Texto',
currentPassword: 'Senha atual',
dangerZone_title: 'Zona de Perigo',
date: 'Data',
dueDate_title: 'Data de Vencimento',
deleteAttachment_title: 'Excluir Anexo',
deleteBoard_title: 'Excluir Quadro',
deleteCard_title: 'Excluir Cartão',
deleteComment_title: 'Excluir Comentário',
deleteLabel_title: 'Excluir Rótulo',
deleteList_title: 'Excluir Lista',
deleteProject_title: 'Excluir Projeto',
deleteTask_title: 'Excluir Tarefa',
deleteUser_title: 'Excluir Usuário',
description: 'Descrição',
detectAutomatically: 'Detectar automaticamente',
dropFileToUpload: 'Solte o arquivo para enviar',
editor: 'Editor',
editAttachment_title: 'Editar Anexo',
editAvatar_title: 'Editar Avatar',
editBoard_title: 'Editar Quadro',
editDueDate_title: 'Editar Data de Vencimento',
editEmail_title: 'Editar E-mail',
editInformation_title: 'Editar Informações',
editLabel_title: 'Editar Rótulo',
editPassword_title: 'Editar Senha',
editPermissions_title: 'Editar Permissões',
editStopwatch_title: 'Editar Cronômetro',
editUsername_title: 'Editar Nome de Usuário',
email: 'E-mail',
emailAlreadyInUse: 'E-mail já está em uso',
enterCardTitle: 'Digite o título do cartão... [Ctrl+Enter] para abrir automaticamente.',
enterDescription: 'Digite a descrição...',
enterFilename: 'Digite o nome do arquivo',
enterListTitle: 'Digite o título da lista...',
enterProjectTitle: 'Digite o título do projeto',
enterTaskDescription: 'Digite a descrição da tarefa...',
filterByLabels_title: 'Filtrar por Rótulos',
filterByMembers_title: 'Filtrar por Membros',
fromComputer_title: 'Do Computador',
fromTrello: 'Do Trello',
general: 'Geral',
hours: 'Horas',
importBoard_title: 'Importar Quadro',
invalidCurrentPassword: 'Senha atual inválida',
labels: 'Rótulos',
language: 'Idioma',
leaveBoard_title: 'Sair do Quadro',
leaveProject_title: 'Sair do Projeto',
list: 'Lista',
listActions_title: 'Ações da Lista',
managers: 'Gerentes',
members: 'Membros',
minutes: 'Minutos',
moveCard_title: 'Mover Cartão',
name: 'Nome',
newEmail: 'Novo e-mail',
newPassword: 'Nova senha',
newUsername: 'Novo nome de usuário',
noConnectionToServer: 'Sem conexão com o servidor',
noBoards: 'Sem quadros',
noLists: 'Sem listas',
noProjects: 'Sem projetos',
notifications: 'Notificações',
noUnreadNotifications: 'Nenhuma notificação não lida.',
openBoard_title: 'Abrir Quadro',
optional_inline: 'opcional',
organization: 'Organização',
phone: 'Telefone',
preferences: 'Preferências',
pressPasteShortcutToAddAttachmentFromClipboard:
'Dica: pressione Ctrl-V (Cmd-V no Mac) para adicionar um anexo da área de transferência.',
project: 'Projeto',
projectNotFound_title: 'Projeto não encontrado',
removeManager_title: 'Remover Gerente',
removeMember_title: 'Remover Membro',
searchLabels: 'Pesquisar rótulos...',
searchMembers: 'Pesquisar membros...',
searchUsers: 'Pesquisar usuários...',
seconds: 'Segundos',
selectBoard: 'Selecionar quadro',
selectList: 'Selecionar lista',
selectPermissions_title: 'Selecionar Permissões',
selectProject: 'Selecionar projeto',
settings: 'Configurações',
stopwatch: 'Cronômetro',
subscribeToMyOwnCardsByDefault: 'Inscrever-se automaticamente em meus próprios cartões',
taskActions_title: 'Ações da Tarefa',
tasks: 'Tarefas',
thereIsNoPreviewAvailableForThisAttachment:
'Não há pré-visualização disponível para este anexo.',
time: 'Tempo',
title: 'Título',
userActions_title: 'Ações do Usuário',
userAddedThisCardToList: '<0>{{user}}</0><1> adicionou este cartão a {{list}}</1>',
userLeftNewCommentToCard:
'{{user}} deixou um novo comentário «{{comment}}» em <2>{{card}}</2>',
userMovedCardFromListToList: '{{user}} moveu <2>{{card}}</2> de {{fromList}} para {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> moveu este cartão de {{fromList}} para {{toList}}</1>',
username: 'Nome de usuário',
usernameAlreadyInUse: 'Nome de usuário já está em uso',
users: 'Usuários',
version: 'Versão',
viewer: 'Visualizador',
writeComment: 'Escreva um comentário...',
},
action: {
addAnotherCard: 'Adicionar outro cartão',
addAnotherList: 'Adicionar outra lista',
addAnotherTask: 'Adicionar outra tarefa',
addCard: 'Adicionar cartão',
addCard_title: 'Adicionar Cartão',
addComment: 'Adicionar comentário',
addList: 'Adicionar lista',
addMember: 'Adicionar membro',
addMoreDetailedDescription: 'Adicionar descrição mais detalhada',
addTask: 'Adicionar tarefa',
addToCard: 'Adicionar ao cartão',
addUser: 'Adicionar usuário',
createBoard: 'Criar quadro',
createFile: 'Criar arquivo',
createLabel: 'Criar rótulo',
createNewLabel: 'Criar novo rótulo',
createProject: 'Criar projeto',
delete: 'Excluir',
deleteAttachment: 'Excluir anexo',
deleteAvatar: 'Excluir avatar',
deleteBoard: 'Excluir quadro',
deleteCard: 'Excluir cartão',
deleteCard_title: 'Excluir Cartão',
deleteComment: 'Excluir comentário',
deleteImage: 'Excluir imagem',
deleteLabel: 'Excluir rótulo',
deleteList: 'Excluir lista',
deleteList_title: 'Excluir Lista',
deleteProject: 'Excluir projeto',
deleteProject_title: 'Excluir Projeto',
deleteTask: 'Excluir tarefa',
deleteTask_title: 'Excluir Tarefa',
deleteUser: 'Excluir usuário',
edit: 'Editar',
editDueDate_title: 'Editar Data de Vencimento',
editDescription_title: 'Editar Descrição',
editEmail_title: 'Editar E-mail',
editInformation_title: 'Editar Informações',
editPassword_title: 'Editar Senha',
editPermissions: 'Editar permissões',
editStopwatch_title: 'Editar Cronômetro',
editTitle_title: 'Editar Título',
editUsername_title: 'Editar Nome de Usuário',
hideDetails: 'Ocultar detalhes',
import: 'Importar',
leaveBoard: 'Sair do quadro',
leaveProject: 'Sair do projeto',
logOut_title: 'Sair',
makeCover_title: 'Tornar Capa',
move: 'Mover',
moveCard_title: 'Mover Cartão',
remove: 'Remover',
removeBackground: 'Remover fundo',
removeCover_title: 'Remover Capa',
removeFromBoard: 'Remover do quadro',
removeFromProject: 'Remover do projeto',
removeManager: 'Remover gerente',
removeMember: 'Remover membro',
save: 'Salvar',
showAllAttachments: 'Mostrar todos os anexos ({{hidden}} ocultos)',
showDetails: 'Mostrar detalhes',
showFewerAttachments: 'Mostrar menos anexos',
start: 'Iniciar',
stop: 'Parar',
subscribe: 'Inscrever-se',
unsubscribe: 'Cancelar inscrição',
uploadNewAvatar: 'Enviar novo avatar',
uploadNewImage: 'Enviar nova imagem',
},
},
};
+8
View File
@@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'pt',
country: 'br',
name: 'Português',
embeddedLocale: login,
};
+22
View File
@@ -0,0 +1,22 @@
export default {
translation: {
common: {
emailOrUsername: 'E-mail ou nome de usuário',
invalidEmailOrUsername: 'E-mail ou nome de usuário inválido',
invalidPassword: 'Senha inválida',
logInToPlanka: 'Entrar no Planka',
noInternetConnection: 'Sem conexão com a internet',
pageNotFound_title: 'Página não encontrada',
password: 'Senha',
projectManagement: 'Gerenciamento de projetos',
serverConnectionFailed: 'Falha na conexão com o servidor',
unknownError: 'Erro desconhecido, tente novamente mais tarde',
useSingleSignOn: 'Usar login único',
},
action: {
logIn: 'Entrar',
logInWithSSO: 'Entrar com SSO',
},
},
};
+1 -1
View File
@@ -67,7 +67,7 @@ export default {
deleteTask_title: '删除任务',
deleteUser_title: '删除用户',
description: '描述',
detectAutomatically: '自动删除',
detectAutomatically: '自动检测',
dropFileToUpload: '拖放文件以上传',
editor: '编辑器',
editAttachment_title: '编辑附件',
+54 -1
View File
@@ -1,3 +1,4 @@
import pick from 'lodash/pick';
import { attr, fk, many, oneToOne } from 'redux-orm';
import BaseModel from './BaseModel';
@@ -165,7 +166,6 @@ export default class extends BaseModel {
break;
case ActionTypes.CARD_CREATE:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_UPDATE__SUCCESS:
case ActionTypes.CARD_UPDATE_HANDLE:
Card.upsert(payload.card);
@@ -176,10 +176,63 @@ export default class extends BaseModel {
Card.upsert(payload.card);
break;
case ActionTypes.CARD_CREATE_HANDLE: {
const cardModel = Card.upsert(payload.card);
payload.cardMemberships.forEach(({ userId }) => {
cardModel.users.add(userId);
});
payload.cardLabels.forEach(({ labelId }) => {
cardModel.labels.add(labelId);
});
break;
}
case ActionTypes.CARD_UPDATE:
Card.withId(payload.id).update(payload.data);
break;
case ActionTypes.CARD_DUPLICATE: {
const cardModel = Card.withId(payload.id);
const nextCardModel = Card.upsert({
...pick(cardModel.ref, [
'boardId',
'listId',
'position',
'name',
'description',
'dueDate',
'stopwatch',
]),
...payload.card,
});
cardModel.users.toRefArray().forEach(({ id }) => {
nextCardModel.users.add(id);
});
cardModel.labels.toRefArray().forEach(({ id }) => {
nextCardModel.labels.add(id);
});
break;
}
case ActionTypes.CARD_DUPLICATE__SUCCESS: {
Card.withId(payload.localId).deleteWithRelated();
const cardModel = Card.upsert(payload.card);
payload.cardMemberships.forEach(({ userId }) => {
cardModel.users.add(userId);
});
payload.cardLabels.forEach(({ labelId }) => {
cardModel.labels.add(labelId);
});
break;
}
case ActionTypes.CARD_DELETE:
Card.withId(payload.id).deleteWithRelated();
+15
View File
@@ -1,5 +1,6 @@
import { attr, fk } from 'redux-orm';
import { createLocalId } from '../utils/local-id';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
@@ -44,10 +45,24 @@ export default class extends BaseModel {
break;
case ActionTypes.BOARD_FETCH__SUCCESS:
case ActionTypes.CARD_CREATE_HANDLE:
case ActionTypes.CARD_DUPLICATE__SUCCESS:
payload.tasks.forEach((task) => {
Task.upsert(task);
});
break;
case ActionTypes.CARD_DUPLICATE:
payload.taskIds.forEach((taskId, index) => {
const taskModel = Task.withId(taskId);
Task.upsert({
...taskModel.ref,
id: `${createLocalId()}-${index}`, // TODO: hack?
cardId: payload.card.id,
});
});
break;
case ActionTypes.TASK_CREATE:
case ActionTypes.TASK_CREATE_HANDLE:
+70 -3
View File
@@ -5,6 +5,7 @@ import request from '../request';
import selectors from '../../../selectors';
import actions from '../../../actions';
import api from '../../../api';
import i18n from '../../../i18n';
import { createLocalId } from '../../../utils/local-id';
export function* createCard(listId, data, autoOpen) {
@@ -41,8 +42,23 @@ export function* createCard(listId, data, autoOpen) {
}
}
export function* handleCardCreate(card) {
yield put(actions.handleCardCreate(card));
export function* handleCardCreate({ id }) {
let card;
let cardMemberships;
let cardLabels;
let tasks;
let attachments;
try {
({
item: card,
included: { cardMemberships, cardLabels, tasks, attachments },
} = yield call(request, api.getCard, id));
} catch (error) {
return;
}
yield put(actions.handleCardCreate(card, cardMemberships, cardLabels, tasks, attachments));
}
export function* updateCard(id, data) {
@@ -106,6 +122,55 @@ export function* transferCurrentCard(boardId, listId, index) {
yield call(transferCard, cardId, boardId, listId, index);
}
export function* duplicateCard(id) {
const { listId, name } = yield select(selectors.selectCardById, id);
const index = yield select(selectors.selectCardIndexById, id);
const nextData = {
position: yield select(selectors.selectNextCardPosition, listId, index + 1),
name: `${name} (${i18n.t('common.copy', {
context: 'inline',
})})`,
};
const localId = yield call(createLocalId);
const taskIds = yield select(selectors.selectTaskIdsByCardId, id);
yield put(
actions.duplicateCard(
id,
{
...nextData,
id: localId,
},
taskIds,
),
);
let card;
let cardMemberships;
let cardLabels;
let tasks;
try {
({
item: card,
included: { cardMemberships, cardLabels, tasks },
} = yield call(request, api.duplicateCard, id, nextData));
} catch (error) {
yield put(actions.duplicateCard.failure(localId, error));
return;
}
yield put(actions.duplicateCard.success(localId, card, cardMemberships, cardLabels, tasks));
}
export function* duplicateCurrentCard() {
const { cardId } = yield select(selectors.selectPath);
yield call(duplicateCard, cardId);
}
export function* deleteCard(id) {
const { cardId, boardId } = yield select(selectors.selectPath);
@@ -147,11 +212,13 @@ export default {
handleCardCreate,
updateCard,
updateCurrentCard,
handleCardUpdate,
moveCard,
moveCurrentCard,
transferCard,
transferCurrentCard,
handleCardUpdate,
duplicateCard,
duplicateCurrentCard,
deleteCard,
deleteCurrentCard,
handleCardDelete,
+2
View File
@@ -32,6 +32,8 @@ export default function* cardsWatchers() {
takeEvery(EntryActionTypes.CURRENT_CARD_TRANSFER, ({ payload: { boardId, listId, index } }) =>
services.transferCurrentCard(boardId, listId, index),
),
takeEvery(EntryActionTypes.CARD_DUPLICATE, ({ payload: { id } }) => services.duplicateCard(id)),
takeEvery(EntryActionTypes.CURRENT_CARD_DUPLICATE, () => services.duplicateCurrentCard()),
takeEvery(EntryActionTypes.CARD_DELETE, ({ payload: { id } }) => services.deleteCard(id)),
takeEvery(EntryActionTypes.CURRENT_CARD_DELETE, () => services.deleteCurrentCard()),
takeEvery(EntryActionTypes.CARD_DELETE_HANDLE, ({ payload: { card } }) =>
+6 -6
View File
@@ -33,10 +33,10 @@ export function* authenticateUsingOidc() {
const oidcConfig = yield select(selectors.selectOidcConfig);
const state = nanoid();
window.sessionStorage.setItem('oidc-state', state);
window.localStorage.setItem('oidc-state', state);
const nonce = nanoid();
window.sessionStorage.setItem('oidc-nonce', nonce);
window.localStorage.setItem('oidc-nonce', nonce);
let redirectUrl = `${oidcConfig.authorizationUrl}`;
redirectUrl += `&state=${encodeURIComponent(state)}`;
@@ -49,11 +49,11 @@ export function* authenticateUsingOidcCallback() {
// https://github.com/plankanban/planka/issues/511#issuecomment-1771385639
const params = new URLSearchParams(window.location.hash.substring(1) || window.location.search);
const state = window.sessionStorage.getItem('oidc-state');
window.sessionStorage.removeItem('oidc-state');
const state = window.localStorage.getItem('oidc-state');
window.localStorage.removeItem('oidc-state');
const nonce = window.sessionStorage.getItem('oidc-nonce');
window.sessionStorage.removeItem('oidc-nonce');
const nonce = window.localStorage.getItem('oidc-nonce');
window.localStorage.removeItem('oidc-nonce');
yield put(replace(Paths.LOGIN));
+42
View File
@@ -26,6 +26,24 @@ export const makeSelectCardById = () =>
export const selectCardById = makeSelectCardById();
export const makeSelectCardIndexById = () =>
createSelector(
orm,
(_, id) => id,
({ Card }, id) => {
const cardModel = Card.withId(id);
if (!cardModel) {
return cardModel;
}
const cardModels = cardModel.list.getFilteredOrderedCardsModelArray();
return cardModels.findIndex((cardModelItem) => cardModelItem.id === cardModel.id);
},
);
export const selectCardIndexById = makeSelectCardIndexById();
export const makeSelectUsersByCardId = () =>
createSelector(
orm,
@@ -60,6 +78,26 @@ export const makeSelectLabelsByCardId = () =>
export const selectLabelsByCardId = makeSelectLabelsByCardId();
export const makeSelectTaskIdsByCardId = () =>
createSelector(
orm,
(_, id) => id,
({ Card }, id) => {
const cardModel = Card.withId(id);
if (!cardModel) {
return cardModel;
}
return cardModel
.getOrderedTasksQuerySet()
.toRefArray()
.map((task) => task.id);
},
);
export const selectTaskIdsByCardId = makeSelectTaskIdsByCardId();
export const makeSelectTasksByCardId = () =>
createSelector(
orm,
@@ -286,10 +324,14 @@ export const selectNotificationIdsForCurrentCard = createSelector(
export default {
makeSelectCardById,
selectCardById,
makeSelectCardIndexById,
selectCardIndexById,
makeSelectUsersByCardId,
selectUsersByCardId,
makeSelectLabelsByCardId,
selectLabelsByCardId,
makeSelectTaskIdsByCardId,
selectTaskIdsByCardId,
makeSelectTasksByCardId,
selectTasksByCardId,
makeSelectLastActivityIdByCardId,
+22 -11
View File
@@ -3,15 +3,7 @@ version: '3'
services:
planka:
image: ghcr.io/plankanban/planka:master
command: >
bash -c
"for i in `seq 1 30`; do
./start.sh &&
s=$$? && break || s=$$?;
echo \"Tried $$i times. Waiting 5 seconds...\";
sleep 5;
done; (exit $$s)"
restart: unless-stopped
restart: on-failure
volumes:
- user-avatars:/app/public/user-avatars
- project-background-images:/app/public/project-background-images
@@ -39,24 +31,43 @@ services:
# - DEFAULT_ADMIN_NAME=Demo Demo
# - DEFAULT_ADMIN_USERNAME=demo
# Email Notifications (https://nodemailer.com/smtp/)
# - SMTP_HOST=
# - SMTP_PORT=587
# - SMTP_SECURE=true
# - SMTP_USER=
# - SMTP_PASSWORD=
# - SMTP_FROM="Demo Demo" <demo@demo.demo>
# - OIDC_ISSUER=
# - OIDC_CLIENT_ID=
# - OIDC_CLIENT_SECRET=
# - OIDC_SCOPES=openid email profile
# - OIDC_ADMIN_ROLES=admin
# - OIDC_EMAIL_ATTRIBUTE=email
# - OIDC_NAME_ATTRIBUTE=name
# - OIDC_USERNAME_ATTRIBUTE=preferred_username
# - OIDC_ROLES_ATTRIBUTE=groups
# - OIDC_IGNORE_USERNAME=true
# - OIDC_IGNORE_ROLES=true
# - OIDC_ENFORCED=true
depends_on:
- postgres
postgres:
condition: service_healthy
postgres:
image: postgres:14-alpine
restart: unless-stopped
restart: on-failure
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=planka
- POSTGRES_HOST_AUTH_METHOD=trust
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d planka"]
interval: 10s
timeout: 5s
retries: 5
volumes:
user-avatars:
+18 -11
View File
@@ -3,15 +3,7 @@ version: '3'
services:
planka:
image: ghcr.io/plankanban/planka:latest
command: >
bash -c
"for i in `seq 1 30`; do
./start.sh &&
s=$$? && break || s=$$?;
echo \"Tried $$i times. Waiting 5 seconds...\";
sleep 5;
done; (exit $$s)"
restart: unless-stopped
restart: on-failure
volumes:
- user-avatars:/app/public/user-avatars
- project-background-images:/app/public/project-background-images
@@ -39,6 +31,14 @@ services:
# - DEFAULT_ADMIN_NAME=Demo Demo
# - DEFAULT_ADMIN_USERNAME=demo
# Email Notifications (https://nodemailer.com/smtp/)
# - SMTP_HOST=
# - SMTP_PORT=587
# - SMTP_SECURE=true
# - SMTP_USER=
# - SMTP_PASSWORD=
# - SMTP_FROM="Demo Demo" <demo@demo.demo>
# - OIDC_ISSUER=
# - OIDC_CLIENT_ID=
# - OIDC_CLIENT_SECRET=
@@ -50,17 +50,24 @@ services:
# - OIDC_ROLES_ATTRIBUTE=groups
# - OIDC_IGNORE_USERNAME=true
# - OIDC_IGNORE_ROLES=true
# - OIDC_ENFORCED=true
depends_on:
- postgres
postgres:
condition: service_healthy
postgres:
image: postgres:14-alpine
restart: unless-stopped
restart: on-failure
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=planka
- POSTGRES_HOST_AUTH_METHOD=trust
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d planka"]
interval: 10s
timeout: 5s
retries: 5
volumes:
user-avatars:
+23
View File
@@ -0,0 +1,23 @@
const http = require('http');
const options = {
host: 'localhost',
port: 1337,
timeout: 2000
};
const healthCheck = http.request(options, (res) => {
console.log(`HEALTHCHECK STATUS: ${res.statusCode}`);
if (res.statusCode == 200) {
process.exit(0);
}
else {
process.exit(1);
}
});
healthCheck.on('error', function (err) {
console.error('ERROR');
process.exit(1);
});
healthCheck.end();
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "planka",
"version": "1.15.5",
"version": "1.16.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "planka",
"version": "1.15.5",
"version": "1.16.2",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "planka",
"version": "1.15.5",
"version": "1.16.2",
"private": true,
"homepage": "https://plankanban.github.io/planka",
"repository": {
+9
View File
@@ -22,6 +22,14 @@ SECRET_KEY=notsecretkey
# DEFAULT_ADMIN_NAME=Demo Demo
# DEFAULT_ADMIN_USERNAME=demo
# Email Notifications (https://nodemailer.com/smtp/)
# SMTP_HOST=
# SMTP_PORT=587
# SMTP_SECURE=true
# SMTP_USER=
# SMTP_PASSWORD=
# SMTP_FROM="Demo Demo" <demo@demo.demo>
# OIDC_ISSUER=
# OIDC_CLIENT_ID=
# OIDC_CLIENT_SECRET=
@@ -33,6 +41,7 @@ SECRET_KEY=notsecretkey
# OIDC_ROLES_ATTRIBUTE=groups
# OIDC_IGNORE_USERNAME=true
# OIDC_IGNORE_ROLES=true
# OIDC_ENFORCED=true
## Do not edit this
@@ -46,8 +46,11 @@ module.exports = {
},
async fn(inputs) {
const remoteAddress = getRemoteAddress(this.req);
if (sails.config.custom.oidcEnforced) {
throw Errors.USE_SINGLE_SIGN_ON;
}
const remoteAddress = getRemoteAddress(this.req);
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
if (!user) {
@@ -29,6 +29,7 @@ module.exports = {
},
canComment: {
type: 'boolean',
allowNull: true,
},
},
@@ -17,6 +17,7 @@ module.exports = {
},
canComment: {
type: 'boolean',
allowNull: true,
},
},
+3 -2
View File
@@ -78,12 +78,12 @@ module.exports = {
async fn(inputs) {
const { currentUser } = this.req;
const { list } = await sails.helpers.lists
const { board, list } = await sails.helpers.lists
.getProjectPath(inputs.listId)
.intercept('pathNotFound', () => Errors.LIST_NOT_FOUND);
const boardMembership = await BoardMembership.findOne({
boardId: list.boardId,
boardId: board.id,
userId: currentUser.id,
});
@@ -99,6 +99,7 @@ module.exports = {
const card = await sails.helpers.cards.createOne
.with({
board,
values: {
...values,
list,
+82
View File
@@ -0,0 +1,82 @@
const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: {
cardNotFound: 'Card not found',
},
};
module.exports = {
inputs: {
id: {
type: 'string',
regex: /^[0-9]+$/,
required: true,
},
position: {
type: 'number',
required: true,
},
name: {
type: 'string',
},
},
exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: {
responseType: 'notFound',
},
},
async fn(inputs) {
const { currentUser } = this.req;
const { card, list, board } = await sails.helpers.cards
.getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
const boardMembership = await BoardMembership.findOne({
boardId: card.boardId,
userId: currentUser.id,
});
if (!boardMembership) {
throw Errors.CARD_NOT_FOUND; // Forbidden
}
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['position', 'name']);
const {
card: nextCard,
cardMemberships,
cardLabels,
tasks,
} = await sails.helpers.cards.duplicateOne.with({
board,
list,
record: card,
values: {
...values,
creatorUser: currentUser,
},
request: this.req,
});
return {
item: nextCard,
included: {
cardMemberships,
cardLabels,
tasks,
},
};
},
};
@@ -32,12 +32,12 @@ module.exports = {
async fn(inputs) {
const { currentUser } = this.req;
const { card } = await sails.helpers.cards
const { board, card } = await sails.helpers.cards
.getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
const boardMembership = await BoardMembership.findOne({
boardId: card.boardId,
boardId: board.id,
userId: currentUser.id,
});
@@ -55,6 +55,7 @@ module.exports = {
};
const action = await sails.helpers.actions.createOne.with({
board,
values: {
...values,
card,
+1
View File
@@ -10,6 +10,7 @@ module.exports = {
response_mode: 'fragment',
}),
endSessionUrl: oidcClient.issuer.end_session_endpoint ? oidcClient.endSessionUrl({}) : null,
isEnforced: sails.config.custom.oidcEnforced,
};
}
+10
View File
@@ -1,6 +1,9 @@
const zxcvbn = require('zxcvbn');
const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
EMAIL_ALREADY_IN_USE: {
emailAlreadyInUse: 'Email already in use',
},
@@ -56,6 +59,9 @@ module.exports = {
},
exits: {
notEnoughRights: {
responseType: 'forbidden',
},
emailAlreadyInUse: {
responseType: 'conflict',
},
@@ -65,6 +71,10 @@ module.exports = {
},
async fn(inputs) {
if (sails.config.custom.oidcEnforced) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, [
'email',
'password',
+7
View File
@@ -21,6 +21,10 @@ module.exports = {
custom: valuesValidator,
required: true,
},
board: {
type: 'ref',
required: true,
},
request: {
type: 'ref',
},
@@ -56,6 +60,9 @@ module.exports = {
userId,
action,
},
user: values.user,
board: inputs.board,
card: values.card,
}),
),
);
@@ -1,3 +1,5 @@
const POSITION_GAP = 65535; // TODO: move to config
module.exports = {
inputs: {
user: {
@@ -107,6 +109,7 @@ module.exports = {
position: trelloCard.pos,
name: trelloCard.name,
description: trelloCard.desc || null,
dueDate: trelloCard.due,
}).fetch();
await importCardLabels(plankaCard, trelloCard);
@@ -123,7 +126,7 @@ module.exports = {
getUsedTrelloLabels().map(async (trelloLabel, index) => {
const plankaLabel = await Label.create({
boardId: inputs.board.id,
position: 65535 * (index + 1), // TODO: move to config
position: POSITION_GAP * (index + 1),
name: trelloLabel.name || null,
color: getPlankaLabelColor(trelloLabel.color),
}).fetch();
+5
View File
@@ -25,6 +25,10 @@ module.exports = {
custom: valuesValidator,
required: true,
},
board: {
type: 'ref',
required: true,
},
request: {
type: 'ref',
},
@@ -104,6 +108,7 @@ module.exports = {
},
user: values.creatorUser,
},
board: inputs.board,
});
return card;
+144
View File
@@ -0,0 +1,144 @@
const valuesValidator = (value) => {
if (!_.isPlainObject(value)) {
return false;
}
if (!_.isUndefined(value.position) && !_.isFinite(value.position)) {
return false;
}
if (!_.isPlainObject(value.creatorUser)) {
return false;
}
return true;
};
module.exports = {
inputs: {
record: {
type: 'ref',
required: true,
},
values: {
type: 'ref',
custom: valuesValidator,
required: true,
},
board: {
type: 'ref',
required: true,
},
list: {
type: 'ref',
required: true,
},
request: {
type: 'ref',
},
},
async fn(inputs) {
const { values } = inputs;
const cards = await sails.helpers.lists.getCards(inputs.record.listId);
const { position, repositions } = sails.helpers.utils.insertToPositionables(
values.position,
cards,
);
repositions.forEach(async ({ id, position: nextPosition }) => {
await Card.update({
id,
listId: inputs.record.listId,
}).set({
position: nextPosition,
});
sails.sockets.broadcast(`board:${inputs.record.boardId}`, 'cardUpdate', {
item: {
id,
position: nextPosition,
},
});
});
const card = await Card.create({
..._.pick(inputs.record, [
'boardId',
'listId',
'name',
'description',
'dueDate',
'stopwatch',
]),
...values,
position,
creatorUserId: values.creatorUser.id,
}).fetch();
const cardMemberships = await sails.helpers.cards.getCardMemberships(inputs.record.id);
const cardMembershipsValues = cardMemberships.map((cardMembership) => ({
..._.pick(cardMembership, ['userId']),
cardId: card.id,
}));
const nextCardMemberships = await CardMembership.createEach(cardMembershipsValues).fetch();
const cardLabels = await sails.helpers.cards.getCardLabels(inputs.record.id);
const cardLabelsValues = cardLabels.map((cardLabel) => ({
..._.pick(cardLabel, ['labelId']),
cardId: card.id,
}));
const nextCardLabels = await CardLabel.createEach(cardLabelsValues).fetch();
const tasks = await sails.helpers.cards.getTasks(inputs.record.id);
const tasksValues = tasks.map((task) => ({
..._.pick(task, ['position', 'name', 'isCompleted']),
cardId: card.id,
}));
const nextTasks = await Task.createEach(tasksValues).fetch();
sails.sockets.broadcast(
`board:${card.boardId}`,
'cardCreate',
{
item: card,
},
inputs.request,
);
if (values.creatorUser.subscribeToOwnCards) {
await CardSubscription.create({
cardId: card.id,
userId: card.creatorUserId,
}).tolerate('E_UNIQUE');
sails.sockets.broadcast(`user:${card.creatorUserId}`, 'cardUpdate', {
item: {
id: card.id,
isSubscribed: true,
},
});
}
await sails.helpers.actions.createOne.with({
values: {
card,
type: Action.Types.CREATE_CARD, // TODO: introduce separate type?
data: {
list: _.pick(inputs.list, ['id', 'name']),
},
user: values.creatorUser,
},
board: inputs.board,
});
return {
card,
cardMemberships: nextCardMemberships,
cardLabels: nextCardLabels,
tasks: nextTasks,
};
},
};
+1
View File
@@ -232,6 +232,7 @@ module.exports = {
toList: _.pick(values.list, ['id', 'name']),
},
},
board: inputs.board,
});
}
@@ -14,6 +14,40 @@ const valuesValidator = (value) => {
return true;
};
// TODO: use templates (views) to build html
const buildAndSendEmail = async (user, board, card, action, notifiableUser) => {
let emailData;
switch (action.type) {
case Action.Types.MOVE_CARD:
emailData = {
subject: `${user.name} moved ${card.name} from ${action.data.fromList.name} to ${action.data.toList.name} on ${board.name}`,
html:
`<p>${user.name} moved ` +
`<a href="${process.env.BASE_URL}/cards/${card.id}">${card.name}</a> ` +
`from ${action.data.fromList.name} to ${action.data.toList.name} ` +
`on <a href="${process.env.BASE_URL}/boards/${board.id}">${board.name}</a></p>`,
};
break;
case Action.Types.COMMENT_CARD:
emailData = {
subject: `${user.name} left a new comment to ${card.name} on ${board.name}`,
html:
`<p>${user.name} left a new comment to ` +
`<a href="${process.env.BASE_URL}/cards/${card.id}">${card.name}</a> ` +
`on <a href="${process.env.BASE_URL}/boards/${board.id}">${board.name}</a></p>` +
`<p>${action.data.text}</p>`,
};
break;
default:
return;
}
await sails.helpers.utils.sendEmail.with({
...emailData,
to: notifiableUser.email,
});
};
module.exports = {
inputs: {
values: {
@@ -21,6 +55,18 @@ module.exports = {
custom: valuesValidator,
required: true,
},
user: {
type: 'ref',
required: true,
},
board: {
type: 'ref',
required: true,
},
card: {
type: 'ref',
required: true,
},
},
async fn(inputs) {
@@ -40,6 +86,17 @@ module.exports = {
item: notification,
});
if (sails.hooks.smtp.isActive()) {
let notifiableUser;
if (values.user) {
notifiableUser = values.user;
} else {
notifiableUser = await sails.helpers.users.getOne(notification.userId);
}
buildAndSendEmail(inputs.user, inputs.board, inputs.card, values.action, notifiableUser);
}
return notification;
},
};
+31
View File
@@ -0,0 +1,31 @@
module.exports = {
inputs: {
to: {
type: 'string',
required: true,
},
subject: {
type: 'string',
required: true,
},
html: {
type: 'string',
required: true,
},
},
async fn(inputs) {
const transporter = sails.hooks.smtp.getTransporter(); // TODO: check if active?
try {
const info = await transporter.sendMail({
...inputs,
from: sails.config.custom.smtpFrom,
});
sails.log.info('Email sent: %s', info.messageId);
} catch (error) {
sails.log.error(error);
}
},
};
+35
View File
@@ -0,0 +1,35 @@
const nodemailer = require('nodemailer');
module.exports = function smtpServiceHook(sails) {
let transporter = null;
return {
/**
* Runs when this Sails app loads/lifts.
*/
async initialize() {
if (sails.config.custom.smtpHost) {
transporter = nodemailer.createTransport({
pool: true,
host: sails.config.custom.smtpHost,
port: sails.config.custom.smtpPort,
secure: sails.config.custom.smtpSecure,
auth: sails.config.custom.smtpUser && {
user: sails.config.custom.smtpUser,
pass: sails.config.custom.smtpPassword,
},
});
sails.log.info('SMTP hook has been loaded successfully');
}
},
getTransporter() {
return transporter;
},
isActive() {
return transporter !== null;
},
};
};
+10 -1
View File
@@ -31,7 +31,15 @@ module.exports.custom = {
attachmentsPath: path.join(sails.config.appPath, 'private', 'attachments'),
attachmentsUrl: `${process.env.BASE_URL}/attachments`,
defaultAdminEmail: process.env.DEFAULT_ADMIN_EMAIL,
defaultAdminEmail:
process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase(),
smtpHost: process.env.SMTP_HOST,
smtpPort: process.env.SMTP_PORT || 587,
smtpSecure: process.env.SMTP_SECURE === 'true',
smtpUser: process.env.SMTP_USER,
smtpPassword: process.env.SMTP_PASSWORD,
smtpFrom: process.env.SMTP_FROM,
oidcIssuer: process.env.OIDC_ISSUER,
oidcClientId: process.env.OIDC_CLIENT_ID,
@@ -44,6 +52,7 @@ module.exports.custom = {
oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups',
oidcIgnoreUsername: process.env.OIDC_IGNORE_USERNAME === 'true',
oidcIgnoreRoles: process.env.OIDC_IGNORE_ROLES === 'true',
oidcEnforced: process.env.OIDC_ENFORCED === 'true',
// TODO: move client base url to environment variable?
oidcRedirectUri: `${
+1
View File
@@ -55,6 +55,7 @@ module.exports.routes = {
'POST /api/lists/:listId/cards': 'cards/create',
'GET /api/cards/:id': 'cards/show',
'PATCH /api/cards/:id': 'cards/update',
'POST /api/cards/:id/duplicate': 'cards/duplicate',
'DELETE /api/cards/:id': 'cards/delete',
'POST /api/cards/:cardId/memberships': 'card-memberships/create',
'DELETE /api/cards/:cardId/memberships': 'card-memberships/delete',
+4 -3
View File
@@ -13,7 +13,7 @@ const buildData = () => {
data.name = process.env.DEFAULT_ADMIN_NAME;
}
if (process.env.DEFAULT_ADMIN_USERNAME) {
data.username = process.env.DEFAULT_ADMIN_USERNAME;
data.username = process.env.DEFAULT_ADMIN_USERNAME.toLowerCase();
}
return data;
@@ -24,16 +24,17 @@ exports.seed = async (knex) => {
return;
}
const email = process.env.DEFAULT_ADMIN_EMAIL.toLowerCase();
const data = buildData();
try {
await knex('user_account').insert({
...data,
email: process.env.DEFAULT_ADMIN_EMAIL,
email,
subscribeToOwnCards: false,
createdAt: new Date().toISOString(),
});
} catch (error) {
await knex('user_account').update(data).where('email', process.env.DEFAULT_ADMIN_EMAIL);
await knex('user_account').update(data).where('email', email);
}
};
+14
View File
@@ -15,6 +15,7 @@
"lodash": "^4.17.21",
"moment": "^2.29.4",
"move-file": "^2.1.0",
"nodemailer": "^6.9.12",
"openid-client": "^5.6.1",
"rimraf": "^5.0.5",
"sails": "^1.5.7",
@@ -5257,6 +5258,14 @@
}
}
},
"node_modules/nodemailer": {
"version": "6.9.12",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.12.tgz",
"integrity": "sha512-pnLo7g37Br3jXbF0bl5DekBJihm2q+3bB3l2o/B060sWmb5l+VqeScAQCBqaQ+5ezRZFzW5SciZNGdRDEbq89w==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nodemon": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz",
@@ -12852,6 +12861,11 @@
"whatwg-url": "^5.0.0"
}
},
"nodemailer": {
"version": "6.9.12",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.12.tgz",
"integrity": "sha512-pnLo7g37Br3jXbF0bl5DekBJihm2q+3bB3l2o/B060sWmb5l+VqeScAQCBqaQ+5ezRZFzW5SciZNGdRDEbq89w=="
},
"nodemon": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz",
+1
View File
@@ -36,6 +36,7 @@
"lodash": "^4.17.21",
"moment": "^2.29.4",
"move-file": "^2.1.0",
"nodemailer": "^6.9.12",
"openid-client": "^5.6.1",
"rimraf": "^5.0.5",
"sails": "^1.5.7",
+1 -3
View File
@@ -1,4 +1,2 @@
#!/bin/bash
set -e
node db/init.js
exec node app.js --prod $@
export NODE_ENV=production && set -e && node db/init.js && node app.js --prod