diff --git a/client/src/actions/cards.js b/client/src/actions/cards.js index 136a81d..2bbe194 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,61 +64,58 @@ const handleCardUpdate = (card) => ({ }, }); -const deleteCard = (id) => ({ - type: ActionTypes.CARD_DELETE, +const duplicateCard = (id, card, taskIds) => ({ + type: ActionTypes.CARD_DUPLICATE, payload: { id, + card, + taskIds, }, }); -deleteCard.success = (card) => ({ - type: ActionTypes.CARD_DELETE__SUCCESS, +duplicateCard.success = (localId, card, cardMemberships, cardLabels, tasks) => ({ + type: ActionTypes.CARD_DUPLICATE__SUCCESS, payload: { + localId, card, + cardMemberships, + cardLabels, + tasks, }, }); -deleteCard.failure = (id, error) => ({ - type: ActionTypes.CARD_DELETE__FAILURE, +duplicateCard.failure = (id, error) => ({ + type: ActionTypes.CARD_DUPLICATE__FAILURE, payload: { id, error, }, }); -const handleCardDelete = (card) => ({ - type: ActionTypes.CARD_DELETE_HANDLE, - payload: { - card, - }, -}); - -const duplicateCard = (id, localId) => ({ - type: ActionTypes.CARD_DUPLICATE, +const deleteCard = (id) => ({ + type: ActionTypes.CARD_DELETE, payload: { id, - localId, }, }); -duplicateCard.success = (localId, card) => ({ - type: ActionTypes.CARD_DUPLICATE__SUCCESS, +deleteCard.success = (card) => ({ + type: ActionTypes.CARD_DELETE__SUCCESS, payload: { - localId, card, }, }); -duplicateCard.failure = (id, error) => ({ - type: ActionTypes.CARD_DUPLICATE__FAILURE, +deleteCard.failure = (id, error) => ({ + type: ActionTypes.CARD_DELETE__FAILURE, payload: { id, error, }, }); -const handleCardDuplicate = (card) => ({ - type: ActionTypes.CARD_DUPLICATE_HANDLE, +const handleCardDelete = (card) => ({ + type: ActionTypes.CARD_DELETE_HANDLE, payload: { card, }, @@ -125,8 +126,7 @@ export default { handleCardCreate, updateCard, handleCardUpdate, + duplicateCard, deleteCard, handleCardDelete, - duplicateCard, - handleCardDuplicate, }; diff --git a/client/src/actions/tasks.js b/client/src/actions/tasks.js index 9279018..679ade0 100644 --- a/client/src/actions/tasks.js +++ b/client/src/actions/tasks.js @@ -7,15 +7,6 @@ const createTask = (task) => ({ }, }); -const createTasks = () => ({}); - -createTasks.success = (tasks) => ({ - type: ActionTypes.TASKS_CREATE__SUCCESS, - payload: { - tasks, - }, -}); - createTask.success = (localId, task) => ({ type: ActionTypes.TASK_CREATE__SUCCESS, payload: { @@ -99,7 +90,6 @@ const handleTaskDelete = (task) => ({ }); export default { - createTasks, createTask, handleTaskCreate, updateTask, diff --git a/client/src/api/cards.js b/client/src/api/cards.js index a9c59cf..568ca8e 100755 --- a/client/src/api/cards.js +++ b/client/src/api/cards.js @@ -57,17 +57,18 @@ const updateCard = (id, data, headers) => item: transformCard(body.item), })); -const deleteCard = (id, headers) => - socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({ +const duplicateCard = (id, data, headers) => + socket.post(`/cards/${id}/duplicate`, data, headers).then((body) => ({ ...body, item: transformCard(body.item), })); -const duplicateCard = (id, headers) => - socket.post(`/cards/${id}/duplicate`, undefined, headers).then((body) => ({ +const deleteCard = (id, headers) => + socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({ ...body, item: transformCard(body.item), })); + /* Event handlers */ const makeHandleCardCreate = (next) => (body) => { diff --git a/client/src/components/Card/ActionsStep.jsx b/client/src/components/Card/ActionsStep.jsx index 1165caa..bd9db9e 100644 --- a/client/src/components/Card/ActionsStep.jsx +++ b/client/src/components/Card/ActionsStep.jsx @@ -21,7 +21,6 @@ const StepTypes = { EDIT_DUE_DATE: 'EDIT_DUE_DATE', EDIT_STOPWATCH: 'EDIT_STOPWATCH', MOVE: 'MOVE', - DUPLICATE: 'DUPLICATE', DELETE: 'DELETE', }; @@ -37,8 +36,8 @@ const ActionsStep = React.memo( onUpdate, onMove, onTransfer, - onDelete, onDuplicate, + onDelete, onUserAdd, onUserRemove, onBoardFetch, @@ -215,7 +214,7 @@ const ActionsStep = React.memo( })} - {t('action.duplicate', { + {t('action.duplicateCard', { context: 'title', })} @@ -244,8 +243,8 @@ ActionsStep.propTypes = { onUpdate: PropTypes.func.isRequired, onMove: PropTypes.func.isRequired, onTransfer: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, onDuplicate: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, onUserAdd: PropTypes.func.isRequired, onUserRemove: PropTypes.func.isRequired, onBoardFetch: PropTypes.func.isRequired, diff --git a/client/src/components/Card/Card.jsx b/client/src/components/Card/Card.jsx index 7e69fcb..4adddbc 100755 --- a/client/src/components/Card/Card.jsx +++ b/client/src/components/Card/Card.jsx @@ -41,8 +41,8 @@ const Card = React.memo( onUpdate, onMove, onTransfer, - onDelete, onDuplicate, + onDelete, onUserAdd, onUserRemove, onBoardFetch, @@ -240,8 +240,8 @@ Card.propTypes = { onUpdate: PropTypes.func.isRequired, onMove: PropTypes.func.isRequired, onTransfer: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, onDuplicate: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, onUserAdd: PropTypes.func.isRequired, onUserRemove: PropTypes.func.isRequired, onBoardFetch: PropTypes.func.isRequired, diff --git a/client/src/components/CardModal/CardModal.jsx b/client/src/components/CardModal/CardModal.jsx index a7f2d63..ff77aa9 100755 --- a/client/src/components/CardModal/CardModal.jsx +++ b/client/src/components/CardModal/CardModal.jsx @@ -55,8 +55,8 @@ const CardModal = React.memo( onUpdate, onMove, onTransfer, - onDelete, onDuplicate, + onDelete, onUserAdd, onUserRemove, onBoardFetch, @@ -565,8 +565,8 @@ CardModal.propTypes = { onUpdate: PropTypes.func.isRequired, onMove: PropTypes.func.isRequired, onTransfer: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, onDuplicate: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, onUserAdd: PropTypes.func.isRequired, onUserRemove: PropTypes.func.isRequired, onBoardFetch: PropTypes.func.isRequired, diff --git a/client/src/constants/ActionTypes.js b/client/src/constants/ActionTypes.js index c68468d..d5724e1 100644 --- a/client/src/constants/ActionTypes.js +++ b/client/src/constants/ActionTypes.js @@ -194,14 +194,13 @@ 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', CARD_DELETE_HANDLE: 'CARD_DELETE_HANDLE', - CARD_DUPLICATE: 'CARD_DUPLICATE', - CARD_DUPLICATE__SUCCESS: 'CARD_DUPLICATE__SUCCESS', - CARD_DUPLICATE__FAILURE: 'CARD_DUPLICATE__FAILURE', - CARD_DUPLICATE_HANDLE: 'CARD_DUPLICATE_HANDLE', /* Tasks */ @@ -209,7 +208,6 @@ export default { TASK_CREATE__SUCCESS: 'TASK_CREATE__SUCCESS', TASK_CREATE__FAILURE: 'TASK_CREATE__FAILURE', TASK_CREATE_HANDLE: 'TASK_CREATE_HANDLE', - TASKS_CREATE__SUCCESS: 'TASKS_CREATE__SUCCESS', TASK_UPDATE: 'TASK_UPDATE', TASK_UPDATE__SUCCESS: 'TASK_UPDATE__SUCCESS', TASK_UPDATE__FAILURE: 'TASK_UPDATE__FAILURE', diff --git a/client/src/constants/EntryActionTypes.js b/client/src/constants/EntryActionTypes.js index 95b84de..9cd0694 100755 --- a/client/src/constants/EntryActionTypes.js +++ b/client/src/constants/EntryActionTypes.js @@ -132,12 +132,11 @@ 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`, - CARD_DUPLICATE: `${PREFIX}/CARD_DUPLICATE`, - CURRENT_CARD_DUPLICATE: `${PREFIX}/CURRENT_CARD_DUPLICATE`, - CARD_DUPLICATE_HANDLE: `${PREFIX}/CARD_DUPLICATE_HANDLE`, /* Tasks */ diff --git a/client/src/containers/CardContainer.js b/client/src/containers/CardContainer.js index c602a2b..e174564 100755 --- a/client/src/containers/CardContainer.js +++ b/client/src/containers/CardContainer.js @@ -62,8 +62,8 @@ 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), - onDelete: () => entryActions.deleteCard(id), onDuplicate: () => entryActions.duplicateCard(id), + onDelete: () => entryActions.deleteCard(id), onUserAdd: (userId) => entryActions.addUserToCard(userId, id), onUserRemove: (userId) => entryActions.removeUserFromCard(userId, id), onBoardFetch: entryActions.fetchBoard, diff --git a/client/src/containers/CardModalContainer.js b/client/src/containers/CardModalContainer.js index f5142cc..a8df6ea 100755 --- a/client/src/containers/CardModalContainer.js +++ b/client/src/containers/CardModalContainer.js @@ -78,8 +78,8 @@ const mapDispatchToProps = (dispatch) => onUpdate: entryActions.updateCurrentCard, onMove: entryActions.moveCurrentCard, onTransfer: entryActions.transferCurrentCard, - onDelete: entryActions.deleteCurrentCard, onDuplicate: entryActions.duplicateCurrentCard, + onDelete: entryActions.deleteCurrentCard, onUserAdd: entryActions.addUserToCurrentCard, onUserRemove: entryActions.removeUserFromCurrentCard, onBoardFetch: entryActions.fetchBoard, diff --git a/client/src/entry-actions/cards.js b/client/src/entry-actions/cards.js index 6f70671..346fc8d 100755 --- a/client/src/entry-actions/cards.js +++ b/client/src/entry-actions/cards.js @@ -74,39 +74,32 @@ const transferCurrentCard = (boardId, listId, index = 0) => ({ }, }); -const deleteCard = (id) => ({ - type: EntryActionTypes.CARD_DELETE, +const duplicateCard = (id) => ({ + type: EntryActionTypes.CARD_DUPLICATE, payload: { id, }, }); -const deleteCurrentCard = () => ({ - type: EntryActionTypes.CURRENT_CARD_DELETE, +const duplicateCurrentCard = () => ({ + type: EntryActionTypes.CURRENT_CARD_DUPLICATE, payload: {}, }); -const handleCardDelete = (card) => ({ - type: EntryActionTypes.CARD_DELETE_HANDLE, - payload: { - card, - }, -}); - -const duplicateCard = (id) => ({ - type: EntryActionTypes.CARD_DUPLICATE, +const deleteCard = (id) => ({ + type: EntryActionTypes.CARD_DELETE, payload: { id, }, }); -const duplicateCurrentCard = () => ({ - type: EntryActionTypes.CURRENT_CARD_DUPLICATE, +const deleteCurrentCard = () => ({ + type: EntryActionTypes.CURRENT_CARD_DELETE, payload: {}, }); -const handleCardDuplicate = (card) => ({ - type: EntryActionTypes.CARD_DUPLICATE_HANDLE, +const handleCardDelete = (card) => ({ + type: EntryActionTypes.CARD_DELETE_HANDLE, payload: { card, }, @@ -122,10 +115,9 @@ export default { moveCurrentCard, transferCard, transferCurrentCard, + duplicateCard, + duplicateCurrentCard, deleteCard, deleteCurrentCard, handleCardDelete, - duplicateCard, - duplicateCurrentCard, - handleCardDuplicate, }; diff --git a/client/src/locales/en/core.js b/client/src/locales/en/core.js index 160453c..41e4494 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.', @@ -197,6 +198,7 @@ export default { 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/models/Card.js b/client/src/models/Card.js index f1d5c76..3c45256 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(); @@ -194,21 +247,6 @@ export default class extends BaseModel { break; } - case ActionTypes.CARD_DUPLICATE: { - const cardModel = Card.withId(payload.id); - Card.upsert({ - ...cardModel.fields, - name: `${cardModel.name} (copy)`, - id: payload.localId, - }); - break; - } - case ActionTypes.CARD_DUPLICATE__SUCCESS: - case ActionTypes.CARD_DUPLICATE_HANDLE: { - Card.withId(payload.localId).delete(); - Card.upsert(payload.card); - break; - } case ActionTypes.ACTIVITIES_FETCH: Card.withId(payload.cardId).update({ isActivitiesFetching: true, diff --git a/client/src/models/Task.js b/client/src/models/Task.js index 08ba5c7..b990fe2 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,17 +45,25 @@ 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.TASKS_CREATE__SUCCESS: { - payload.tasks.forEach((task) => { - Task.upsert(task); + 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: case ActionTypes.TASK_UPDATE__SUCCESS: diff --git a/client/src/sagas/core/services/cards.js b/client/src/sagas/core/services/cards.js index 29c1e35..eb2d1da 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); @@ -142,48 +207,19 @@ export function* handleCardDelete(card) { yield put(actions.handleCardDelete(card)); } -export function* duplicateCard(id) { - const localId = yield call(createLocalId); - - yield put(actions.duplicateCard(id, localId)); - - let card; - let included; - try { - ({ item: card, included } = yield call(request, api.duplicateCard, id)); - } catch (error) { - yield put(actions.duplicateCard.failure(localId, error)); - return; - } - - yield put(actions.duplicateCard.success(localId, card)); - yield put(actions.createTasks.success(included.tasks)); -} - -export function* duplicateCurrentCard() { - const { cardId } = yield select(selectors.selectPath); - - yield call(duplicateCard, cardId); -} - -export function* handleCardDuplicate(card) { - yield put(actions.handleCardDuplicate(card)); -} - export default { createCard, handleCardCreate, updateCard, updateCurrentCard, + handleCardUpdate, moveCard, moveCurrentCard, transferCard, transferCurrentCard, - handleCardUpdate, + duplicateCard, + duplicateCurrentCard, deleteCard, deleteCurrentCard, handleCardDelete, - duplicateCard, - duplicateCurrentCard, - handleCardDuplicate, }; diff --git a/client/src/sagas/core/watchers/cards.js b/client/src/sagas/core/watchers/cards.js index 32dced4..3fcb599 100644 --- a/client/src/sagas/core/watchers/cards.js +++ b/client/src/sagas/core/watchers/cards.js @@ -32,15 +32,12 @@ 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 } }) => services.handleCardDelete(card), ), - takeEvery(EntryActionTypes.CARD_DUPLICATE, ({ payload: { id } }) => services.duplicateCard(id)), - takeEvery(EntryActionTypes.CURRENT_CARD_DUPLICATE, () => services.duplicateCurrentCard()), - takeEvery(EntryActionTypes.CARD_DUPLICATE_HANDLE, ({ payload: { card } }) => - services.handleCardDuplicate(card), - ), ]); } diff --git a/client/src/selectors/cards.js b/client/src/selectors/cards.js index 8f100d5..d1137dc 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 index 0060a4e..918a8ec 100755 --- a/server/api/controllers/cards/duplicate.js +++ b/server/api/controllers/cards/duplicate.js @@ -14,6 +14,13 @@ module.exports = { regex: /^[0-9]+$/, required: true, }, + position: { + type: 'number', + required: true, + }, + name: { + type: 'string', + }, }, exits: { @@ -28,7 +35,7 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { card } = await sails.helpers.cards + const { card, list, board } = await sails.helpers.cards .getProjectPath(inputs.id) .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); @@ -45,46 +52,30 @@ module.exports = { throw Errors.NOT_ENOUGH_RIGHTS; } - const { board, list } = await sails.helpers.lists - .getProjectPath(card.listId) - .intercept('pathNotFound', () => Errors.LIST_NOT_FOUND); - - const values = _.pick(card, ['position', 'name', 'description', 'dueDate', 'stopwatch']); + const values = _.pick(inputs, ['position', 'name']); - const newCard = await sails.helpers.cards.createOne - .with({ - board, - values: { - ...values, - name: `${card.name} (copy)`, - list, - creatorUser: currentUser, - }, - request: this.req, - }) - .intercept('positionMustBeInValues', () => Errors.POSITION_MUST_BE_PRESENT); - - const tasks = await sails.helpers.cards.getTasks(inputs.id); - - const newTasks = await Promise.all( - tasks.map(async (task) => { - const taskValues = _.pick(task, ['position', 'name', 'isCompleted']); - const newTask = await sails.helpers.tasks.createOne.with({ - values: { - ...taskValues, - card: newCard, - }, - request: this.req, - }); - return newTask; - }), - ); + const { + card: nextCard, + cardMemberships, + cardLabels, + tasks, + } = await sails.helpers.cards.duplicateOne.with({ + board, + list, + record: card, + values: { + ...values, + creatorUser: currentUser, + }, + request: this.req, + }); - // TODO: add labels return { - item: newCard, + item: nextCard, included: { - tasks: newTasks, + cardMemberships, + cardLabels, + tasks, }, }; }, diff --git a/server/api/helpers/boards/import-from-trello.js b/server/api/helpers/boards/import-from-trello.js index efc74dc..2ab6e0a 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 0000000..a63feaa --- /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 37e8f5d..8b88df5 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -54,8 +54,8 @@ module.exports.routes = { 'POST /api/lists/:listId/cards': 'cards/create', 'GET /api/cards/:id': 'cards/show', - 'POST /api/cards/:id/duplicate': 'cards/duplicate', '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',