From b1d187476d7c63b19d9d67976cb4ee6e66004cd6 Mon Sep 17 00:00:00 2001 From: Maksim Eltyshev Date: Tue, 5 May 2020 01:30:06 +0500 Subject: [PATCH] Move cards between boards and projects --- client/src/actions/entry/board.js | 7 + client/src/actions/entry/card.js | 29 +++- client/src/api/http.js | 2 +- client/src/components/Card/ActionsPopup.jsx | 32 ++++ client/src/components/Card/Card.jsx | 21 +++ client/src/components/CardModal/CardModal.jsx | 39 ++++- client/src/components/MoveCardPopup.jsx | 5 + .../components/MoveCardStep/MoveCardStep.jsx | 161 ++++++++++++++++++ .../MoveCardStep/MoveCardStep.module.css | 10 ++ client/src/components/MoveCardStep/index.js | 3 + client/src/constants/EntryActionTypes.js | 4 + client/src/containers/CardContainer.js | 19 ++- client/src/containers/CardModalContainer.js | 18 +- client/src/lib/popup/Popup.module.css | 2 - client/src/locales/en/app.js | 14 +- client/src/locales/ru/app.js | 14 +- client/src/sagas/app/services/board.js | 11 +- client/src/sagas/app/services/card.js | 23 +++ client/src/sagas/app/services/router.js | 2 +- client/src/sagas/app/watchers/board.js | 2 + client/src/sagas/app/watchers/card.js | 12 ++ client/src/selectors/current.js | 30 ++++ server/api/controllers/cards/update.js | 25 ++- server/api/helpers/update-card.js | 5 + 24 files changed, 474 insertions(+), 16 deletions(-) create mode 100644 client/src/components/MoveCardPopup.jsx create mode 100644 client/src/components/MoveCardStep/MoveCardStep.jsx create mode 100644 client/src/components/MoveCardStep/MoveCardStep.module.css create mode 100644 client/src/components/MoveCardStep/index.js diff --git a/client/src/actions/entry/board.js b/client/src/actions/entry/board.js index b34028c4..32e97848 100755 --- a/client/src/actions/entry/board.js +++ b/client/src/actions/entry/board.js @@ -7,6 +7,13 @@ export const createBoardInCurrentProject = (data) => ({ }, }); +export const fetchBoard = (id) => ({ + type: EntryActionTypes.BOARD_FETCH, + payload: { + id, + }, +}); + export const updateBoard = (id, data) => ({ type: EntryActionTypes.BOARD_UPDATE, payload: { diff --git a/client/src/actions/entry/card.js b/client/src/actions/entry/card.js index fc4e61ae..76a9129e 100755 --- a/client/src/actions/entry/card.js +++ b/client/src/actions/entry/card.js @@ -23,7 +23,7 @@ export const updateCurrentCard = (data) => ({ }, }); -export const moveCard = (id, listId, index) => ({ +export const moveCard = (id, listId, index = 0) => ({ type: EntryActionTypes.CARD_MOVE, payload: { id, @@ -32,6 +32,33 @@ export const moveCard = (id, listId, index) => ({ }, }); +export const moveCurrentCard = (listId, index = 0) => ({ + type: EntryActionTypes.CURRENT_CARD_MOVE, + payload: { + listId, + index, + }, +}); + +export const transferCard = (id, boardId, listId, index = 0) => ({ + type: EntryActionTypes.CARD_TRANSFER, + payload: { + id, + boardId, + listId, + index, + }, +}); + +export const transferCurrentCard = (boardId, listId, index = 0) => ({ + type: EntryActionTypes.CURRENT_CARD_TRANSFER, + payload: { + boardId, + listId, + index, + }, +}); + export const deleteCard = (id) => ({ type: EntryActionTypes.CARD_DELETE, payload: { diff --git a/client/src/api/http.js b/client/src/api/http.js index f6ce4cb3..646528da 100755 --- a/client/src/api/http.js +++ b/client/src/api/http.js @@ -4,7 +4,7 @@ import Config from '../constants/Config'; const http = {}; -// TODO: all methods +// TODO: add all methods ['POST'].forEach((method) => { http[method.toLowerCase()] = (url, data, headers) => { const formData = Object.keys(data).reduce((result, key) => { diff --git a/client/src/components/Card/ActionsPopup.jsx b/client/src/components/Card/ActionsPopup.jsx index e39d6f6a..48e636d5 100644 --- a/client/src/components/Card/ActionsPopup.jsx +++ b/client/src/components/Card/ActionsPopup.jsx @@ -1,3 +1,4 @@ +import pick from 'lodash/pick'; import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; @@ -10,6 +11,7 @@ import ProjectMembershipsStep from '../ProjectMembershipsStep'; import LabelsStep from '../LabelsStep'; import EditDueDateStep from '../EditDueDateStep'; import EditTimerStep from '../EditTimerStep'; +import MoveCardStep from '../MoveCardStep'; import DeleteStep from '../DeleteStep'; import styles from './ActionsPopup.module.css'; @@ -19,21 +21,26 @@ const StepTypes = { LABELS: 'LABELS', EDIT_DUE_DATE: 'EDIT_DUE_DATE', EDIT_TIMER: 'EDIT_TIMER', + MOVE: 'MOVE', DELETE: 'DELETE', }; const ActionsStep = React.memo( ({ card, + projectsToLists, projectMemberships, currentUserIds, labels, currentLabelIds, onNameEdit, onUpdate, + onMove, + onTransfer, onDelete, onUserAdd, onUserRemove, + onBoardFetch, onLabelAdd, onLabelRemove, onLabelCreate, @@ -65,6 +72,10 @@ const ActionsStep = React.memo( openStep(StepTypes.EDIT_TIMER); }, [openStep]); + const handleMoveClick = useCallback(() => { + openStep(StepTypes.MOVE); + }, [openStep]); + const handleDeleteClick = useCallback(() => { openStep(StepTypes.DELETE); }, [openStep]); @@ -130,6 +141,18 @@ const ActionsStep = React.memo( onClose={onClose} /> ); + case StepTypes.MOVE: + return ( + + ); case StepTypes.DELETE: return ( + + {t('action.moveCard', { + context: 'title', + })} + {t('action.deleteCard', { context: 'title', @@ -195,6 +223,7 @@ const ActionsStep = React.memo( ActionsStep.propTypes = { /* eslint-disable react/forbid-prop-types */ card: PropTypes.object.isRequired, + projectsToLists: PropTypes.array.isRequired, projectMemberships: PropTypes.array.isRequired, currentUserIds: PropTypes.array.isRequired, labels: PropTypes.array.isRequired, @@ -202,9 +231,12 @@ ActionsStep.propTypes = { /* eslint-enable react/forbid-prop-types */ onNameEdit: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired, + onMove: PropTypes.func.isRequired, + onTransfer: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, onUserAdd: PropTypes.func.isRequired, onUserRemove: PropTypes.func.isRequired, + onBoardFetch: PropTypes.func.isRequired, onLabelAdd: PropTypes.func.isRequired, onLabelRemove: PropTypes.func.isRequired, onLabelCreate: PropTypes.func.isRequired, diff --git a/client/src/components/Card/Card.jsx b/client/src/components/Card/Card.jsx index f4b8b961..f85796c0 100755 --- a/client/src/components/Card/Card.jsx +++ b/client/src/components/Card/Card.jsx @@ -24,17 +24,24 @@ const Card = React.memo( dueDate, timer, coverUrl, + listId, + boardId, + projectId, isPersisted, notificationsTotal, users, labels, tasks, + allProjectsToLists, allProjectMemberships, allLabels, onUpdate, + onMove, + onTransfer, onDelete, onUserAdd, onUserRemove, + onBoardFetch, onLabelAdd, onLabelRemove, onLabelCreate, @@ -143,17 +150,24 @@ const Card = React.memo( name, dueDate, timer, + listId, + boardId, + projectId, isPersisted, }} + projectsToLists={allProjectsToLists} projectMemberships={allProjectMemberships} currentUserIds={users.map((user) => user.id)} labels={allLabels} currentLabelIds={labels.map((label) => label.id)} onNameEdit={handleNameEdit} onUpdate={onUpdate} + onMove={onMove} + onTransfer={onTransfer} onDelete={onDelete} onUserAdd={onUserAdd} onUserRemove={onUserRemove} + onBoardFetch={onBoardFetch} onLabelAdd={onLabelAdd} onLabelRemove={onLabelRemove} onLabelCreate={onLabelCreate} @@ -184,19 +198,26 @@ Card.propTypes = { dueDate: PropTypes.instanceOf(Date), timer: PropTypes.object, // eslint-disable-line react/forbid-prop-types coverUrl: PropTypes.string, + listId: PropTypes.string.isRequired, + boardId: PropTypes.string.isRequired, + projectId: PropTypes.string.isRequired, isPersisted: PropTypes.bool.isRequired, notificationsTotal: PropTypes.number.isRequired, /* eslint-disable react/forbid-prop-types */ users: PropTypes.array.isRequired, labels: PropTypes.array.isRequired, tasks: PropTypes.array.isRequired, + allProjectsToLists: PropTypes.array.isRequired, allProjectMemberships: PropTypes.array.isRequired, allLabels: PropTypes.array.isRequired, /* eslint-enable react/forbid-prop-types */ onUpdate: PropTypes.func.isRequired, + onMove: PropTypes.func.isRequired, + onTransfer: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, onUserAdd: PropTypes.func.isRequired, onUserRemove: PropTypes.func.isRequired, + onBoardFetch: PropTypes.func.isRequired, onLabelAdd: PropTypes.func.isRequired, onLabelRemove: PropTypes.func.isRequired, onLabelCreate: PropTypes.func.isRequired, diff --git a/client/src/components/CardModal/CardModal.jsx b/client/src/components/CardModal/CardModal.jsx index 7ef4255b..159fba5a 100755 --- a/client/src/components/CardModal/CardModal.jsx +++ b/client/src/components/CardModal/CardModal.jsx @@ -20,6 +20,7 @@ import ProjectMembershipsPopup from '../ProjectMembershipsPopup'; import LabelsPopup from '../LabelsPopup'; import EditDueDatePopup from '../EditDueDatePopup'; import EditTimerPopup from '../EditTimerPopup'; +import MoveCardPopup from '../MoveCardPopup'; import DeletePopup from '../DeletePopup'; import styles from './CardModal.module.css'; @@ -33,18 +34,25 @@ const CardModal = React.memo( isSubscribed, isActionsFetching, isAllActionsFetched, + listId, + boardId, + projectId, users, labels, tasks, attachments, actions, + allProjectsToLists, allProjectMemberships, allLabels, isEditable, onUpdate, + onMove, + onTransfer, onDelete, onUserAdd, onUserRemove, + onBoardFetch, onLabelAdd, onLabelRemove, onLabelCreate, @@ -328,7 +336,9 @@ const CardModal = React.memo( @@ -354,6 +364,26 @@ const CardModal = React.memo( {isSubscribed ? t('action.unsubscribe') : t('action.subscribe')} + + + { + const [t] = useTranslation(); + + const [path, handleFieldChange] = useForm(() => ({ + projectId: null, + boardId: null, + listId: null, + ...defaultPath, + })); + + const selectedProject = useMemo( + () => projectsToLists.find((project) => project.id === path.projectId) || null, + [projectsToLists, path.projectId], + ); + + const selectedBoard = useMemo( + () => + (selectedProject && selectedProject.boards.find((board) => board.id === path.boardId)) || + null, + [selectedProject, path.boardId], + ); + + const selectedList = useMemo( + () => (selectedBoard && selectedBoard.lists.find((list) => list.id === path.listId)) || null, + [selectedBoard, path.listId], + ); + + const handleBoardIdFieldChange = useCallback( + (event, data) => { + if (selectedProject.boards.find((board) => board.id === data.value).isFetching === null) { + onBoardFetch(data.value); + } + + handleFieldChange(event, data); + }, + [onBoardFetch, handleFieldChange, selectedProject], + ); + + const handleSubmit = useCallback(() => { + if (selectedBoard.id !== defaultPath.boardId) { + onTransfer(selectedBoard.id, selectedList.id); + } else if (selectedList.id !== defaultPath.listId) { + onMove(selectedList.id); + } + + onClose(); + }, [defaultPath, onMove, onTransfer, onClose, selectedBoard, selectedList]); + + return ( + <> + + {t('common.moveCard', { + context: 'title', + })} + + +
+
{t('common.project')}
+ ({ + text: project.name, + value: project.id, + }))} + value={selectedProject && selectedProject.id} + placeholder={ + projectsToLists.length === 0 ? t('common.noProjects') : t('common.selectProject') + } + disabled={projectsToLists.length === 0} + className={styles.field} + onChange={handleFieldChange} + /> + {selectedProject && ( + <> +
{t('common.board')}
+ ({ + text: board.name, + value: board.id, + }))} + value={selectedBoard && selectedBoard.id} + placeholder={ + selectedProject.boards.length === 0 + ? t('common.noBoards') + : t('common.selectBoard') + } + disabled={selectedProject.boards.length === 0} + className={styles.field} + onChange={handleBoardIdFieldChange} + /> + + )} + {selectedBoard && ( + <> +
{t('common.list')}
+ ({ + text: list.name, + value: list.id, + }))} + value={selectedList && selectedList.id} + placeholder={ + selectedBoard.isFetching === false && selectedBoard.lists.length === 0 + ? t('common.noLists') + : t('common.selectList') + } + loading={selectedBoard.isFetching !== false} + disabled={selectedBoard.isFetching !== false || selectedBoard.lists.length === 0} + className={styles.field} + onChange={handleFieldChange} + /> + + )} +