From c61b23c713d231f759005fc8987569f0eda847a9 Mon Sep 17 00:00:00 2001 From: Matthieu Bollot Date: Fri, 5 Apr 2024 22:40:35 +0200 Subject: [PATCH] feat: Add ability to duplicate card (#668) --- client/src/actions/cards.js | 35 ++++- client/src/api/cards.js | 7 + client/src/components/Card/ActionsStep.jsx | 12 ++ client/src/components/Card/Card.jsx | 3 + client/src/components/CardModal/CardModal.jsx | 11 ++ client/src/constants/ActionTypes.js | 3 + client/src/constants/EntryActionTypes.js | 2 + client/src/containers/CardContainer.js | 1 + client/src/containers/CardModalContainer.js | 1 + client/src/entry-actions/cards.js | 14 ++ client/src/locales/en/core.js | 3 + client/src/locales/fr/core.js | 1 + client/src/models/Card.js | 55 ++++++- client/src/models/Task.js | 15 ++ client/src/sagas/core/services/cards.js | 73 ++++++++- client/src/sagas/core/watchers/cards.js | 2 + client/src/selectors/cards.js | 42 +++++ server/api/controllers/cards/duplicate.js | 82 ++++++++++ .../api/helpers/boards/import-from-trello.js | 4 +- server/api/helpers/cards/duplicate-one.js | 144 ++++++++++++++++++ server/config/routes.js | 1 + 21 files changed, 505 insertions(+), 6 deletions(-) create mode 100755 server/api/controllers/cards/duplicate.js create mode 100644 server/api/helpers/cards/duplicate-one.js diff --git a/client/src/actions/cards.js b/client/src/actions/cards.js index 09699108..2bbe194b 100644 --- a/client/src/actions/cards.js +++ b/client/src/actions/cards.js @@ -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, }; diff --git a/client/src/api/cards.js b/client/src/api/cards.js index 5240038f..568ca8e8 100755 --- a/client/src/api/cards.js +++ b/client/src/api/cards.js @@ -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, diff --git a/client/src/components/Card/ActionsStep.jsx b/client/src/components/Card/ActionsStep.jsx index dbd349e7..bd9db9eb 100644 --- a/client/src/components/Card/ActionsStep.jsx +++ b/client/src/components/Card/ActionsStep.jsx @@ -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', })} + + {t('action.duplicateCard', { + context: 'title', + })} + {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, diff --git a/client/src/components/Card/Card.jsx b/client/src/components/Card/Card.jsx index 37521a06..4adddbc5 100755 --- a/client/src/components/Card/Card.jsx +++ b/client/src/components/Card/Card.jsx @@ -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, diff --git a/client/src/components/CardModal/CardModal.jsx b/client/src/components/CardModal/CardModal.jsx index 76fa5aa5..ff77aa97 100755 --- a/client/src/components/CardModal/CardModal.jsx +++ b/client/src/components/CardModal/CardModal.jsx @@ -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')} + 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), diff --git a/client/src/containers/CardModalContainer.js b/client/src/containers/CardModalContainer.js index ceae9840..a8df6ea0 100755 --- a/client/src/containers/CardModalContainer.js +++ b/client/src/containers/CardModalContainer.js @@ -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, diff --git a/client/src/entry-actions/cards.js b/client/src/entry-actions/cards.js index d73b2476..346fc8d8 100755 --- a/client/src/entry-actions/cards.js +++ b/client/src/entry-actions/cards.js @@ -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, diff --git a/client/src/locales/en/core.js b/client/src/locales/en/core.js index 83b01168..41e4494d 100644 --- a/client/src/locales/en/core.js +++ b/client/src/locales/en/core.js @@ -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
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', diff --git a/client/src/locales/fr/core.js b/client/src/locales/fr/core.js index e20e2910..1b9296ff 100644 --- a/client/src/locales/fr/core.js +++ b/client/src/locales/fr/core.js @@ -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', diff --git a/client/src/models/Card.js b/client/src/models/Card.js index 78c682cc..3c452569 100755 --- a/client/src/models/Card.js +++ b/client/src/models/Card.js @@ -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(); diff --git a/client/src/models/Task.js b/client/src/models/Task.js index 1a472718..b990fe21 100755 --- a/client/src/models/Task.js +++ b/client/src/models/Task.js @@ -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: diff --git a/client/src/sagas/core/services/cards.js b/client/src/sagas/core/services/cards.js index 40c3db83..eb2d1da4 100644 --- a/client/src/sagas/core/services/cards.js +++ b/client/src/sagas/core/services/cards.js @@ -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, diff --git a/client/src/sagas/core/watchers/cards.js b/client/src/sagas/core/watchers/cards.js index 02905150..3fcb5992 100644 --- a/client/src/sagas/core/watchers/cards.js +++ b/client/src/sagas/core/watchers/cards.js @@ -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 } }) => diff --git a/client/src/selectors/cards.js b/client/src/selectors/cards.js index 8f100d57..d1137dc8 100644 --- a/client/src/selectors/cards.js +++ b/client/src/selectors/cards.js @@ -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, diff --git a/server/api/controllers/cards/duplicate.js b/server/api/controllers/cards/duplicate.js new file mode 100755 index 00000000..918a8ec1 --- /dev/null +++ b/server/api/controllers/cards/duplicate.js @@ -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, + }, + }; + }, +}; diff --git a/server/api/helpers/boards/import-from-trello.js b/server/api/helpers/boards/import-from-trello.js index efc74dc0..2ab6e0af 100644 --- a/server/api/helpers/boards/import-from-trello.js +++ b/server/api/helpers/boards/import-from-trello.js @@ -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(); diff --git a/server/api/helpers/cards/duplicate-one.js b/server/api/helpers/cards/duplicate-one.js new file mode 100644 index 00000000..a63feaac --- /dev/null +++ b/server/api/helpers/cards/duplicate-one.js @@ -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, + }; + }, +}; diff --git a/server/config/routes.js b/server/config/routes.js index 77f5c43b..8b88df5b 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -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',