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',