Compare commits

...
56 Commits
Author SHA1 Message Date
Maksim Eltyshev 16499052f7 chore: Update version 2024-04-10 15:57:15 +02:00
HannesOberreiter cc32daa009 feat: Display clickable links in tasks (#694)
Closes #330
2024-04-10 15:53:05 +02:00
Stan M ea1b3b7f92 feat: Ability to run with read-only root filesystem in Helm (#695) 2024-04-10 12:56:21 +02:00
Maksim Eltyshev 4e2863faa7 feat: Automatic logout when session expires
Closes #693
2024-04-09 15:12:46 +02:00
Maksim Eltyshev b46fb43e6f chore: Cleanup 2024-04-08 01:47:24 +02:00
Maksim Eltyshev bcacfa8fe2 Merge branch 'GlitchSecure-slack-bot-integration' 2024-04-08 01:29:28 +02:00
Niccolò Pedrini 4df2becd6f fix: Update Italian translation (#688) 2024-04-08 01:27:19 +02:00
Matthieu Bollot 2990ea593a feat: Slack bot notifications (#676) 2024-04-08 01:27:10 +02:00
Blyamur b976673826 fix: Update Russian translation (#686) 2024-04-08 01:24:50 +02:00
leroyloren 16e034efcb fix: Update Czech translation (#679) 2024-04-08 01:24:50 +02:00
Maksim Eltyshev 6712625c3f chore: Update version 2024-04-08 01:24:50 +02:00
Matthieu Bollot b5bbf6a6a4 feat: Add ability to duplicate card (#668) 2024-04-08 01:24:50 +02:00
Brian Stinson 3d3e7761f5 fix: Allow specifying external dburl in Helm (#672) 2024-04-08 01:24:50 +02:00
piero fa56b9c01f fix: Update HorizontalPodAutoscaler version and spec in chart (#666)
Closes #667
2024-04-08 01:24:50 +02:00
Maksim Eltyshev 882213784f chore: Update version 2024-04-08 01:24:50 +02:00
Maksim Eltyshev 251bd793f3 fix: Fix OIDC authentication error when redirecting from another tab
Closes #650
2024-04-08 01:24:50 +02:00
Maksim Eltyshev 088b0e760b fix: Fix scrollbar styles 2024-04-08 01:24:50 +02:00
Maksim Eltyshev cc1fa1cc3b chore: Update version 2024-04-08 01:24:50 +02:00
Edouard bcd3ea86e8 feat: SMTP integration and email notifications (#631) 2024-04-08 01:24:50 +02:00
Maksim Eltyshev 0176650f67 fix: Add date-fns locale to Portuguese translation 2024-04-08 01:24:50 +02:00
Jeffrey f3963aded3 feat: Add Dutch translation (#644) 2024-04-08 01:24:50 +02:00
Reuben Hu bbe720481a feat: Add Indonesian translation (#643) 2024-04-08 01:24:50 +02:00
IV16SL 04f9030474 fix: Fix Chinese translation (#641) 2024-04-08 01:24:50 +02:00
Chris 6d135be859 feat: Add Portuguese translation (#640) 2024-04-08 01:24:50 +02:00
Maksim Eltyshev 63cbe7063e docs: Update features, add contributors 2024-04-08 01:24:50 +02:00
Maksim Eltyshev 7985bea2b8 chore: Update version 2024-04-08 01:24:50 +02:00
Maksim Eltyshev 733abccaa9 fix: Fix case sensitivity of default admin environment variables 2024-04-08 01:24:50 +02:00
Daniel Hiller 119cce82b4 fix: Container build windows 2024-04-08 01:24:50 +02:00
Maksim Eltyshev 988e70ec40 fix: Fix nullable boolean inputs 2024-04-08 01:24:50 +02:00
Daniel Hiller 5e3fca3771 Update build-and-push-docker-image-dev.yml 2024-04-08 01:24:50 +02:00
Maksim Eltyshev 6d9ce56f4a fix: Include due date when importing from Trello
Closes #598
2024-04-08 01:24:50 +02:00
Daniel Hiller 3c8c04fd37 ci: update workflow 2024-04-08 01:24:50 +02:00
Daniel Hiller 790a1d5cc9 feat: Add healthcheck for docker deployments
fix: Missing healthcheck.js

fix: missing healthcheck.js

fix: HEALTHCHECK command
2024-04-08 01:24:50 +02:00
Maksim Eltyshev 4b352fd878 fix: Remove overflow of attachment dropzone 2024-04-08 01:24:50 +02:00
Maksim Eltyshev a196f2ef11 chore: Update version 2024-04-08 01:24:50 +02:00
Maksim Eltyshev 6c65d135f7 feat: Add ability to enforce SSO
Closes #543, closes #545
2024-04-08 01:24:10 +02:00
Maksim Eltyshev a1a1e9a86a chore: Update version 2024-04-08 01:18:26 +02:00
Maksim Eltyshev 0d39a7567f feat: Add ability to map OIDC attributes and ignore username
Closes #554
2024-04-08 01:18:26 +02:00
Maksim Eltyshev 76a18d06d6 chore: Update version 2024-04-08 01:18:26 +02:00
Maksim Eltyshev ce52fc5af7 fix: Fix images becoming black and white when resizing
Closes #574, closes #585
2024-04-08 01:18:26 +02:00
Maksim Eltyshev 0a2863a79c chore: Bump sharp version 2024-04-08 01:18:26 +02:00
Niccolò Pedrini 39e320fe4b fix: Update Italian translation (#688) 2024-04-08 00:50:52 +02:00
Matthieu Bollot 2f3dfe775e feat: Slack bot notifications (#676) 2024-04-08 00:33:29 +02:00
Blyamur 22964cb375 fix: Update Russian translation (#686) 2024-04-07 22:05:22 +02:00
leroyloren fd89cf7f2b fix: Update Czech translation (#679) 2024-04-07 21:58:27 +02:00
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
GlitchWitch 273d994750 Remove unused code and comments 2023-12-28 12:01:38 -06:00
Brad Bahls 86d21d6abd updated to use currentUser.name for messages 2023-12-28 09:03:41 -07:00
GlitchWitch a9c200cc3b Merge branch 'master' into slack-bot-integration 2023-12-23 14:41:54 -06:00
GlitchWitch 3fedc14062 Rename custom.js -> slack.js 2023-12-23 10:52:15 -06:00
GlitchWitch c42e1fb5dd Add slack variables to docker-compose 2023-12-23 10:49:02 -06:00
GlitchWitch 9961340118 Replace plankaProdUrl with one set by environment. Remove hardcoded channel and unused variables. 2023-12-23 10:48:50 -06:00
Brad Bahls f54bd22757 removed test webhook url 2023-12-23 08:58:22 -07:00
Brad Bahls 1043dacd67 added new custom service with slack integration helper functions; added axios package; added notifications for card create, delete, and update (move); added notifications for comment create 2023-12-23 08:52:07 -07:00
58 changed files with 903 additions and 93 deletions
+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.21
version: 0.1.25
# 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.16.1"
appVersion: "1.16.3"
dependencies:
- alias: postgresql
+14
View File
@@ -55,14 +55,24 @@ spec:
- mountPath: /app/private/attachments
subPath: attachments
name: planka
{{- if .Values.securityContext.readOnlyRootFilesystem }}
- mountPath: /app/logs
subPath: app-logs
name: emptydir
{{- end }}
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 }}
@@ -134,3 +144,7 @@ spec:
{{- else }}
emptyDir: {}
{{- end }}
{{- if .Values.securityContext.readOnlyRootFilesystem }}
- name: emptydir
emptyDir: {}
{{- 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.16.1
REACT_APP_VERSION=1.16.3
+16
View File
@@ -17,6 +17,8 @@
"initials": "^3.1.2",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"linkify-react": "^4.1.3",
"linkifyjs": "^4.1.3",
"lodash": "^4.17.21",
"nanoid": "^5.0.3",
"node-sass": "^9.0.0",
@@ -11911,6 +11913,20 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/linkify-react": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz",
"integrity": "sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA==",
"peerDependencies": {
"linkifyjs": "^4.0.0",
"react": ">= 15.0.0"
}
},
"node_modules/linkifyjs": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz",
"integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg=="
},
"node_modules/loader-runner": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
+2
View File
@@ -70,6 +70,8 @@
"initials": "^3.1.2",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"linkify-react": "^4.1.3",
"linkifyjs": "^4.1.3",
"lodash": "^4.17.21",
"nanoid": "^5.0.3",
"node-sass": "^9.0.0",
+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,
};
+4 -2
View File
@@ -47,9 +47,11 @@ initializeCore.fetchConfig = (config) => ({
},
});
const logout = () => ({
const logout = (invalidateAccessToken) => ({
type: ActionTypes.LOGOUT,
payload: {},
payload: {
invalidateAccessToken,
},
});
logout.invalidateAccessToken = () => ({
+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,
@@ -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,
+3 -1
View File
@@ -4,6 +4,8 @@ import classNames from 'classnames';
import { Progress } from 'semantic-ui-react';
import { useToggle } from '../../lib/hooks';
import Linkify from '../Linkify';
import styles from './Tasks.module.scss';
const Tasks = React.memo(({ items }) => {
@@ -48,7 +50,7 @@ const Tasks = React.memo(({ items }) => {
key={item.id}
className={classNames(styles.task, item.isCompleted && styles.taskCompleted)}
>
{item.name}
<Linkify linkStopPropagation>{item.name}</Linkify>
</li>
))}
</ul>
@@ -55,8 +55,10 @@
display: block;
font-size: 12px;
line-height: 14px;
overflow: hidden;
padding-bottom: 6px;
padding-left: 14px;
text-overflow: ellipsis;
&:before {
content: "–";
@@ -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,
@@ -8,6 +8,7 @@ import { usePopup } from '../../../lib/popup';
import NameEdit from './NameEdit';
import ActionsStep from './ActionsStep';
import Linkify from '../../Linkify';
import styles from './Item.module.scss';
@@ -65,7 +66,7 @@ const Item = React.memo(
onClick={handleClick}
>
<span className={classNames(styles.task, isCompleted && styles.taskCompleted)}>
{name}
<Linkify linkStopPropagation>{name}</Linkify>
</span>
</span>
{isPersisted && canEdit && (
+68
View File
@@ -0,0 +1,68 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import LinkifyReact from 'linkify-react';
import history from '../history';
const Linkify = React.memo(({ children, linkStopPropagation, ...props }) => {
const handleLinkClick = useCallback(
(event) => {
if (linkStopPropagation) {
event.stopPropagation();
}
if (!event.target.getAttribute('target')) {
event.preventDefault();
history.push(event.target.href);
}
},
[linkStopPropagation],
);
const linkRenderer = useCallback(
({ attributes: { href, ...linkProps }, content }) => {
let url;
try {
url = new URL(href, window.location);
} catch (error) {} // eslint-disable-line no-empty
const isSameSite = !!url && url.origin === window.location.origin;
return (
<a
{...linkProps} // eslint-disable-line react/jsx-props-no-spreading
href={href}
target={isSameSite ? undefined : '_blank'}
rel={isSameSite ? undefined : 'noreferrer'}
onClick={handleLinkClick}
>
{isSameSite ? url.pathname : content}
</a>
);
},
[handleLinkClick],
);
return (
<LinkifyReact
{...props} // eslint-disable-line react/jsx-props-no-spreading
options={{
defaultProtocol: 'https',
render: linkRenderer,
}}
>
{children}
</LinkifyReact>
);
});
Linkify.propTypes = {
children: PropTypes.string.isRequired,
linkStopPropagation: PropTypes.bool,
};
Linkify.defaultProps = {
linkStopPropagation: false,
};
export default Linkify;
+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,
+18 -4
View File
@@ -38,7 +38,7 @@ const handleCardUpdate = (card) => ({
},
});
const moveCard = (id, listId, index = 0) => ({
const moveCard = (id, listId, index) => ({
type: EntryActionTypes.CARD_MOVE,
payload: {
id,
@@ -47,7 +47,7 @@ const moveCard = (id, listId, index = 0) => ({
},
});
const moveCurrentCard = (listId, index = 0) => ({
const moveCurrentCard = (listId, index) => ({
type: EntryActionTypes.CURRENT_CARD_MOVE,
payload: {
listId,
@@ -55,7 +55,7 @@ const moveCurrentCard = (listId, index = 0) => ({
},
});
const transferCard = (id, boardId, listId, index = 0) => ({
const transferCard = (id, boardId, listId, index) => ({
type: EntryActionTypes.CARD_TRANSFER,
payload: {
id,
@@ -65,7 +65,7 @@ const transferCard = (id, boardId, listId, index = 0) => ({
},
});
const transferCurrentCard = (boardId, listId, index = 0) => ({
const transferCurrentCard = (boardId, listId, index) => ({
type: EntryActionTypes.CURRENT_CARD_TRANSFER,
payload: {
boardId,
@@ -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,
+4 -2
View File
@@ -1,8 +1,10 @@
import EntryActionTypes from '../constants/EntryActionTypes';
const logout = () => ({
const logout = (invalidateAccessToken) => ({
type: EntryActionTypes.LOGOUT,
payload: {},
payload: {
invalidateAccessToken,
},
});
export default {
+8 -5
View File
@@ -39,9 +39,9 @@ export default {
areYouSureYouWantToLeaveBoard: 'Opravdu chcete opustit tuto tabuli?',
areYouSureYouWantToLeaveProject: 'Opravdu chcete opustit projekt?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Are you sure you want to remove this manager from the project?',
'Jste si jisti, že chcete tohoto správce z projektu odebrat?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Opravdu chcete odstranit tohoto člena z tabule?',
'Jste si jisti, že chcete tohoto člena odebrat z tabule?',
attachment: 'Příloha',
attachments: 'Přílohy',
authentication: 'Ověření',
@@ -55,6 +55,7 @@ export default {
cardNotFound_title: 'Karta nenalezena',
cardOrActionAreDeleted: 'Karta nebo akce je smazána.',
color: 'Barva',
copy_inline: 'copy',
createBoard_title: 'Vytvořit tabuli',
createLabel_title: 'Vytvořit štítek',
createNewOneOrSelectExistingOne: 'Vytvořit nový nebo vyberte<br />již existující.',
@@ -130,7 +131,7 @@ export default {
phone: 'Telefon',
preferences: 'Volby',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: dejte Ctrl-V (Cmd-V na Mac) pro vložení přílohy ze schránky.',
'Tip: stisknutím Ctrl-V (Cmd-V na Mac) přidáte přílohu ze schránky.',
project: 'Projekt',
projectNotFound_title: 'Projekt nenalezen',
removeManager_title: 'Odstranit vedoucího',
@@ -199,6 +200,8 @@ export default {
deleteTask: 'Smazat úkol',
deleteTask_title: 'Smazat úkol',
deleteUser: 'Smazat uživatele',
duplicate: 'Duplikovat',
duplicateCard_title: 'Duplikovat kartu',
edit: 'Upravit',
editDueDate_title: 'Upravit termín do',
editDescription_title: 'Upravit popis',
@@ -226,13 +229,13 @@ export default {
removeMember: 'Odstranit člena',
save: 'Uložit',
showAllAttachments: 'Zozbrazit všechny přílohy ({{hidden}} skryté)',
showFewerAttachments: 'Zobrazit méně příloh',
showDetails: 'Zobrazit detaily',
showFewerAttachments: 'Zobrazit méně příloh',
start: 'Start',
stop: 'Stop',
subscribe: 'Odebírat',
unsubscribe: 'Neodebírat',
uploadNewAvatar: 'Nahrát nového avatara',
uploadNewAvatar: 'Nahrát nový avatar',
uploadNewImage: 'Nahrát nový obrázek',
},
},
+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',
+13 -7
View File
@@ -3,10 +3,10 @@ export default {
date: 'd/M/yyyy',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'MMM d',
longDateTime: "MMMM d 'at' p",
fullDate: 'MMM d, y',
fullDateTime: "MMMM d, y 'at' p",
longDate: 'd MMM',
longDateTime: "d MMMM 'alle' p",
fullDate: 'd MMM, y',
fullDateTime: "d MMMM, y 'alle' p",
},
translation: {
@@ -21,7 +21,7 @@ export default {
administrator: 'Amministratore',
all: 'tutto',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'All changes will be automatically saved<br />after connection restored.',
'Tutte le modifiche verranno salvate<br />al ripristino della connessione.',
areYouSureYouWantToDeleteThisAttachment: 'Sei sicuro di voler eliminare questo allegato?',
areYouSureYouWantToDeleteThisBoard: 'Sei sicuro di voler eliminare questa bacheca?',
areYouSureYouWantToDeleteThisCard: 'Sei sicuro di voler eliminare questa card?',
@@ -50,6 +50,7 @@ export default {
cardNotFound_title: 'Card non trovata',
cardOrActionAreDeleted: "La card o l'azione vengono eliminate.",
color: 'Colore',
copy_inline: 'copia',
createBoard_title: 'Crea Bacheca',
createLabel_title: 'Crea Etichetta',
createNewOneOrSelectExistingOne: 'Crea nuovo o seleziona<br />esistente.',
@@ -94,8 +95,10 @@ export default {
filterByLabels_title: 'Filtra per Etichetta',
filterByMembers_title: 'Filtra per Membro',
fromComputer_title: 'Dal Computer',
fromTrello: 'Da Trello',
general: 'Generale',
hours: 'Ore',
importBoard_title: 'Importa Board',
invalidCurrentPassword: 'Password corrente non valida',
labels: 'Etichette',
language: 'Lingua',
@@ -123,7 +126,7 @@ export default {
phone: 'Telefono',
preferences: 'Preferenze',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: prmi Ctrl-V (Cmd-V on Mac) per aggiungere un allegato dalla clipboard.',
'Consiglio: premi Ctrl-V (Cmd-V on Mac) per aggiungere un allegato dalla clipboard.',
project: 'Progetto',
projectNotFound_title: 'Progetto non trovato',
removeManager_title: 'Rimuovi Manager',
@@ -193,6 +196,8 @@ export default {
deleteTask: 'Elimina task',
deleteTask_title: 'Elimina Task',
deleteUser: 'Elimina utente',
duplicate: 'Duplica',
duplicateCard_title: 'Duplica Card',
edit: 'Modifica',
editDueDate_title: 'Modifica data di scadenza',
editDescription_title: 'Modifica Descrizione',
@@ -204,9 +209,10 @@ export default {
editTitle_title: 'Modifica Titolo',
editUsername_title: 'Modifica Username',
hideDetails: 'Nascondi dettagli',
import: 'Importa',
leaveBoard: 'Lascia bacheca',
leaveProject: 'Lascia progetto',
logOut_title: 'Log Out',
logOut_title: 'Disconnettiti',
makeCover_title: 'Crea Cover',
move: 'Muovi',
moveCard_title: 'Muovi Card',
+2
View File
@@ -199,6 +199,8 @@ export default {
deleteProject: 'Удалить проект',
deleteTask: 'Удалить задачу',
deleteUser: 'Удалить пользователя',
duplicate: 'Дублировать',
duplicateCard_title: 'Дублировать карточку',
edit: 'Изменить',
editBackground: 'Изменить фон',
editDueDate: 'Изменить срок',
+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:
+2 -4
View File
@@ -1,8 +1,7 @@
import { call, fork, join, put, select, take } from 'redux-saga/effects';
import selectors from '../../selectors';
import actions from '../../actions';
import { removeAccessToken } from '../../utils/access-token-storage';
import entryActions from '../../entry-actions';
import ErrorCodes from '../../constants/ErrorCodes';
let lastRequestTask;
@@ -22,8 +21,7 @@ function* queueRequest(method, ...args) {
});
} catch (error) {
if (error.code === ErrorCodes.UNAUTHORIZED) {
yield call(removeAccessToken);
yield put(actions.logout()); // TODO: next url
yield put(entryActions.logout(false));
yield take();
}
+72 -5
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) {
@@ -70,7 +86,7 @@ export function* handleCardUpdate(card) {
yield put(actions.handleCardUpdate(card));
}
export function* moveCard(id, listId, index) {
export function* moveCard(id, listId, index = 0) {
const position = yield select(selectors.selectNextCardPosition, listId, index, id);
yield call(updateCard, id, {
@@ -85,7 +101,7 @@ export function* moveCurrentCard(listId, index) {
yield call(moveCard, cardId, listId, index);
}
export function* transferCard(id, boardId, listId, index) {
export function* transferCard(id, boardId, listId, index = 0) {
const { cardId: currentCardId, boardId: currentBoardId } = yield select(selectors.selectPath);
const position = yield select(selectors.selectNextCardPosition, listId, index, id);
@@ -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 -3
View File
@@ -1,4 +1,4 @@
import { call, put, select, take } from 'redux-saga/effects';
import { call, put, select } from 'redux-saga/effects';
import request from '../request';
import requests from '../requests';
@@ -84,8 +84,7 @@ export function* logout(invalidateAccessToken = true) {
} catch (error) {} // eslint-disable-line no-empty
}
yield put(actions.logout());
yield take();
yield put(actions.logout()); // TODO: next url
}
export default {
+9
View File
@@ -1,10 +1,12 @@
import { call, put, select, take } from 'redux-saga/effects';
import { push } from '../../../lib/redux-router';
import { logout } from './core';
import request from '../request';
import selectors from '../../../selectors';
import actions from '../../../actions';
import api from '../../../api';
import { getAccessToken } from '../../../utils/access-token-storage';
import ActionTypes from '../../../constants/ActionTypes';
import Paths from '../../../constants/Paths';
@@ -25,6 +27,13 @@ export function* goToCard(cardId) {
}
export function* handleLocationChange() {
const accessToken = yield call(getAccessToken);
if (!accessToken) {
yield call(logout, false);
return;
}
const pathsMatch = yield select(selectors.selectPathsMatch);
if (!pathsMatch) {
+1
View File
@@ -218,6 +218,7 @@ export function* handleUserDelete(user) {
if (user.id === currentUserId) {
yield call(logout, false);
return;
}
yield put(actions.handleUserDelete(user));
+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 } }) =>
+5 -1
View File
@@ -4,5 +4,9 @@ import services from '../services';
import EntryActionTypes from '../../../constants/EntryActionTypes';
export default function* coreWatchers() {
yield all([takeEvery(EntryActionTypes.LOGOUT, () => services.logout())]);
yield all([
takeEvery(EntryActionTypes.LOGOUT, ({ payload: { invalidateAccessToken } }) =>
services.logout(invalidateAccessToken),
),
]);
}
+8
View File
@@ -16,6 +16,10 @@ const createSocketEventsChannel = () =>
emit(entryActions.handleSocketReconnect());
};
const handleLogout = () => {
emit(entryActions.logout(false));
};
const handleUserCreate = api.makeHandleUserCreate(({ item }) => {
emit(entryActions.handleUserCreate(item));
});
@@ -171,6 +175,8 @@ const createSocketEventsChannel = () =>
socket.on('disconnect', handleDisconnect);
socket.on('reconnect', handleReconnect);
socket.on('logout', handleLogout);
socket.on('userCreate', handleUserCreate);
socket.on('userUpdate', handleUserUpdate);
socket.on('userDelete', handleUserDelete);
@@ -227,6 +233,8 @@ const createSocketEventsChannel = () =>
socket.off('disconnect', handleDisconnect);
socket.off('reconnect', handleReconnect);
socket.off('logout', handleLogout);
socket.off('userCreate', handleUserCreate);
socket.off('userUpdate', handleUserUpdate);
socket.off('userDelete', handleUserDelete);
+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,
+11 -8
View File
@@ -31,14 +31,6 @@ 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=
@@ -51,6 +43,17 @@ services:
# - OIDC_IGNORE_USERNAME=true
# - OIDC_IGNORE_ROLES=true
# - OIDC_ENFORCED=true
# 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>
# - SLACK_BOT_TOKEN=
# - SLACK_CHANNEL_ID=
depends_on:
postgres:
condition: service_healthy
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "planka",
"version": "1.16.1",
"version": "1.16.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "planka",
"version": "1.16.1",
"version": "1.16.3",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "planka",
"version": "1.16.1",
"version": "1.16.3",
"private": true,
"homepage": "https://plankanban.github.io/planka",
"repository": {
+11 -8
View File
@@ -22,14 +22,6 @@ 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=
@@ -43,6 +35,17 @@ SECRET_KEY=notsecretkey
# OIDC_IGNORE_ROLES=true
# OIDC_ENFORCED=true
# 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>
# SLACK_BOT_TOKEN=
# SLACK_CHANNEL_ID=
## Do not edit this
TZ=UTC
@@ -9,6 +9,10 @@ module.exports = {
deletedAt: new Date().toISOString(),
});
if (this.req.isSocket) {
sails.sockets.leaveAll(`@accessToken:${accessToken}`);
}
return {
item: accessToken,
};
+1
View File
@@ -47,6 +47,7 @@ module.exports = {
card = await sails.helpers.cards.deleteOne.with({
record: card,
user: currentUser,
request: this.req,
});
+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,
},
};
},
};
+28
View File
@@ -14,6 +14,30 @@ const valuesValidator = (value) => {
return true;
};
const buildAndSendSlackMessage = async (user, card, action) => {
const cardLink = `<${sails.config.custom.baseUrl}/cards/${card.id}|${card.name}>`;
let markdown;
switch (action.type) {
case Action.Types.CREATE_CARD:
markdown = `${cardLink} was created by ${user.name} in *${action.data.list.name}*`;
break;
case Action.Types.MOVE_CARD:
markdown = `${cardLink} was moved by ${user.name} to *${action.data.toList.name}*`;
break;
case Action.Types.COMMENT_CARD:
markdown = `*${user.name}* commented on ${cardLink}:\n>${action.data.text}`;
break;
default:
return;
}
await sails.helpers.utils.sendSlackMessage(markdown);
};
module.exports = {
inputs: {
values: {
@@ -67,6 +91,10 @@ module.exports = {
),
);
if (sails.config.custom.slackBotToken) {
buildAndSendSlackMessage(values.user, values.card, action);
}
return action;
},
};
@@ -1,3 +1,5 @@
const POSITION_GAP = 65535; // TODO: move to config
module.exports = {
inputs: {
user: {
@@ -124,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();
+12
View File
@@ -1,9 +1,17 @@
const buildAndSendSlackMessage = async (user, card) => {
await sails.helpers.utils.sendSlackMessage(`*${card.name}* was deleted by ${user.name}`);
};
module.exports = {
inputs: {
record: {
type: 'ref',
required: true,
},
user: {
type: 'ref',
required: true,
},
request: {
type: 'ref',
},
@@ -21,6 +29,10 @@ module.exports = {
},
inputs.request,
);
if (sails.config.custom.slackBotToken) {
buildAndSendSlackMessage(inputs.user, card);
}
}
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,
};
},
};
@@ -27,6 +27,7 @@ const buildAndSendEmail = async (user, board, card, action, notifiableUser) => {
`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 = {
@@ -37,6 +38,7 @@ const buildAndSendEmail = async (user, board, card, action, notifiableUser) => {
`on <a href="${process.env.BASE_URL}/boards/${board.id}">${board.name}</a></p>` +
`<p>${action.data.text}</p>`,
};
break;
default:
return;
+1 -1
View File
@@ -25,7 +25,7 @@ module.exports = {
sails.log.info('Email sent: %s', info.messageId);
} catch (error) {
sails.log.error(error);
sails.log.error(error); // TODO: provide description text?
}
},
};
@@ -0,0 +1,53 @@
const POST_MESSAGE_API_URL = 'https://slack.com/api/chat.postMessage';
module.exports = {
inputs: {
markdown: {
type: 'string',
required: true,
},
},
async fn(inputs) {
const headers = {
Authorization: `Bearer ${sails.config.custom.slackBotToken}`,
'Content-Type': 'application/json; charset=utf-8',
};
const body = {
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: inputs.markdown,
},
},
],
channel: sails.config.custom.slackChannelId,
};
let response;
try {
response = await fetch(POST_MESSAGE_API_URL, {
headers,
method: 'POST',
body: JSON.stringify(body),
});
} catch (error) {
sails.log.error(error); // TODO: provide description text?
return;
}
if (!response.ok) {
sails.log.error('Error sending to Slack: %s', response.error);
return;
}
const responseJson = await response.json();
if (!responseJson.ok) {
sails.log.error('Error sending to Slack: %s', responseJson.error);
}
},
};
+1
View File
@@ -61,6 +61,7 @@ module.exports = function defineCurrentUserHook(sails) {
});
if (req.isSocket) {
sails.sockets.join(req, `@accessToken:${accessToken}`);
sails.sockets.join(req, `@user:${currentUser.id}`);
}
}
+22 -11
View File
@@ -1,6 +1,14 @@
const openidClient = require('openid-client');
module.exports = function oidcServiceHook(sails) {
/**
* oidc 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 defineOidcHook(sails) {
let client = null;
return {
@@ -9,17 +17,20 @@ module.exports = function oidcServiceHook(sails) {
*/
async initialize() {
if (sails.config.custom.oidcIssuer) {
const issuer = await openidClient.Issuer.discover(sails.config.custom.oidcIssuer);
client = new issuer.Client({
client_id: sails.config.custom.oidcClientId,
client_secret: sails.config.custom.oidcClientSecret,
redirect_uris: [sails.config.custom.oidcRedirectUri],
response_types: ['code'],
});
sails.log.info('OIDC hook has been loaded successfully');
if (!sails.config.custom.oidcIssuer) {
return;
}
sails.log.info('Initializing custom hook (`oidc`)');
const issuer = await openidClient.Issuer.discover(sails.config.custom.oidcIssuer);
client = new issuer.Client({
client_id: sails.config.custom.oidcClientId,
client_secret: sails.config.custom.oidcClientSecret,
redirect_uris: [sails.config.custom.oidcRedirectUri],
response_types: ['code'],
});
},
getClient() {
+24 -13
View File
@@ -1,6 +1,14 @@
const nodemailer = require('nodemailer');
module.exports = function smtpServiceHook(sails) {
/**
* smtp 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 defineSmtpHook(sails) {
let transporter = null;
return {
@@ -9,19 +17,22 @@ module.exports = function smtpServiceHook(sails) {
*/
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');
if (!sails.config.custom.smtpHost) {
return;
}
sails.log.info('Initializing custom hook (`smtp`)');
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,
},
});
},
getTransporter() {
+38
View File
@@ -0,0 +1,38 @@
/**
* watcher 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 defineWatcherHook(sails) {
const checkSocketConnectionsToLogout = () => {
Object.keys(sails.io.sockets.adapter.rooms).forEach((room) => {
if (!room.startsWith('@accessToken:')) {
return;
}
const accessToken = room.split(':')[1];
try {
sails.helpers.utils.verifyToken(accessToken);
} catch (error) {
sails.sockets.broadcast(room, 'logout');
sails.sockets.leaveAll(room);
}
});
};
return {
/**
* Runs when this Sails app loads/lifts.
*/
async initialize() {
sails.log.info('Initializing custom hook (`watcher`)');
setInterval(checkSocketConnectionsToLogout, 60 * 1000);
},
};
};
+10 -7
View File
@@ -34,13 +34,6 @@ module.exports.custom = {
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,
oidcClientSecret: process.env.OIDC_CLIENT_SECRET,
@@ -58,4 +51,14 @@ module.exports.custom = {
oidcRedirectUri: `${
sails.config.environment === 'production' ? process.env.BASE_URL : 'http://localhost:3000'
}/oidc-callback`,
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,
slackBotToken: process.env.SLACK_BOT_TOKEN,
slackChannelId: process.env.SLACK_CHANNEL_ID,
};
+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',