Skip to content

Commit

Permalink
feat: Add ability to duplicate card (#668)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattboll committed Apr 5, 2024
1 parent d7847fe commit 6cd9da8
Show file tree
Hide file tree
Showing 21 changed files with 505 additions and 6 deletions.
35 changes: 34 additions & 1 deletion client/src/actions/cards.js
Expand Up @@ -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,
},
});

Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -94,6 +126,7 @@ export default {
handleCardCreate,
updateCard,
handleCardUpdate,
duplicateCard,
deleteCard,
handleCardDelete,
};
7 changes: 7 additions & 0 deletions client/src/api/cards.js
Expand Up @@ -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,
Expand All @@ -81,6 +87,7 @@ export default {
getCard,
updateCard,
deleteCard,
duplicateCard,
makeHandleCardCreate,
makeHandleCardUpdate,
makeHandleCardDelete,
Expand Down
12 changes: 12 additions & 0 deletions client/src/components/Card/ActionsStep.jsx
Expand Up @@ -36,6 +36,7 @@ const ActionsStep = React.memo(
onUpdate,
onMove,
onTransfer,
onDuplicate,
onDelete,
onUserAdd,
onUserRemove,
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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',
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions client/src/components/Card/Card.jsx
Expand Up @@ -41,6 +41,7 @@ const Card = React.memo(
onUpdate,
onMove,
onTransfer,
onDuplicate,
onDelete,
onUserAdd,
onUserRemove,
Expand Down Expand Up @@ -185,6 +186,7 @@ const Card = React.memo(
onUpdate={onUpdate}
onMove={onMove}
onTransfer={onTransfer}
onDuplicate={onDuplicate}
onDelete={onDelete}
onUserAdd={onUserAdd}
onUserRemove={onUserRemove}
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions client/src/components/CardModal/CardModal.jsx
Expand Up @@ -55,6 +55,7 @@ const CardModal = React.memo(
onUpdate,
onMove,
onTransfer,
onDuplicate,
onDelete,
onUserAdd,
onUserRemove,
Expand Down Expand Up @@ -140,6 +141,11 @@ const CardModal = React.memo(
});
}, [isSubscribed, onUpdate]);

const handleDuplicateClick = useCallback(() => {
onDuplicate();
onClose();
}, [onDuplicate, onClose]);

const handleGalleryOpen = useCallback(() => {
isGalleryOpened.current = true;
}, []);
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions client/src/constants/ActionTypes.js
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions client/src/constants/EntryActionTypes.js
Expand Up @@ -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`,
Expand Down
1 change: 1 addition & 0 deletions client/src/containers/CardContainer.js
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions client/src/containers/CardModalContainer.js
Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions client/src/entry-actions/cards.js
Expand Up @@ -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: {
Expand Down Expand Up @@ -103,6 +115,8 @@ export default {
moveCurrentCard,
transferCard,
transferCurrentCard,
duplicateCard,
duplicateCurrentCard,
deleteCard,
deleteCurrentCard,
handleCardDelete,
Expand Down
3 changes: 3 additions & 0 deletions client/src/locales/en/core.js
Expand Up @@ -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.',
Expand Down Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions client/src/locales/fr/core.js
Expand Up @@ -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',
Expand Down
55 changes: 54 additions & 1 deletion 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';
Expand Down Expand Up @@ -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);
Expand All @@ -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();

Expand Down
15 changes: 15 additions & 0 deletions 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';

Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 6cd9da8

Please sign in to comment.