From 9683227fbc574359dd032b3bd0e5c13eccc69a1a Mon Sep 17 00:00:00 2001 From: Symon Baikov Date: Thu, 4 Sep 2025 01:07:10 +0300 Subject: [PATCH] feat: Add ability to move lists between boards (#1208) --- client/src/actions/lists.js | 26 +- .../src/components/lists/List/ActionsStep.jsx | 14 + client/src/components/lists/List/MoveStep.jsx | 133 +++++++++ .../lists/List/MoveStep.module.scss | 17 ++ client/src/constants/EntryActionTypes.js | 1 + client/src/entry-actions/lists.js | 10 + client/src/locales/ar-YE/core.js | 2 + client/src/locales/bg-BG/core.js | 2 + client/src/locales/cs-CZ/core.js | 2 + client/src/locales/da-DK/core.js | 2 + client/src/locales/de-DE/core.js | 2 + client/src/locales/el-GR/core.js | 2 + client/src/locales/en-GB/core.js | 2 + client/src/locales/en-US/core.js | 2 + client/src/locales/es-ES/core.js | 2 + client/src/locales/et-EE/core.js | 2 + client/src/locales/fa-IR/core.js | 2 + client/src/locales/fi-FI/core.js | 2 + client/src/locales/fr-FR/core.js | 2 + client/src/locales/hu-HU/core.js | 2 + client/src/locales/id-ID/core.js | 2 + client/src/locales/it-IT/core.js | 2 + client/src/locales/ja-JP/core.js | 2 + client/src/locales/ko-KR/core.js | 2 + client/src/locales/nl-NL/core.js | 2 + client/src/locales/pl-PL/core.js | 2 + client/src/locales/pt-BR/core.js | 2 + client/src/locales/pt-PT/core.js | 2 + client/src/locales/ro-RO/core.js | 2 + client/src/locales/ru-RU/core.js | 2 + client/src/locales/sk-SK/core.js | 2 + client/src/locales/sr-Cyrl-RS/core.js | 2 + client/src/locales/sr-Latn-RS/core.js | 2 + client/src/locales/sv-SE/core.js | 2 + client/src/locales/tr-TR/core.js | 2 + client/src/locales/uk-UA/core.js | 2 + client/src/locales/uz-UZ/core.js | 2 + client/src/locales/zh-CN/core.js | 2 + client/src/locales/zh-TW/core.js | 2 + client/src/models/Attachment.js | 1 + client/src/models/Card.js | 1 + client/src/models/CustomField.js | 1 + client/src/models/CustomFieldGroup.js | 1 + client/src/models/CustomFieldValue.js | 1 + client/src/models/List.js | 89 +++--- client/src/models/Task.js | 1 + client/src/models/TaskList.js | 1 + client/src/models/User.js | 1 + client/src/sagas/core/services/lists.js | 81 ++++- client/src/sagas/core/watchers/lists.js | 3 + client/src/selectors/lists.js | 18 ++ client/src/selectors/users.js | 30 ++ server/api/controllers/lists/update.js | 37 ++- .../api/helpers/cards/detach-custom-fields.js | 279 ++++++++++++++++++ server/api/helpers/cards/update-one.js | 194 +----------- server/api/helpers/lists/update-one.js | 154 +++++++++- .../query-methods/models/CustomFieldValue.js | 37 ++- server/api/hooks/query-methods/models/List.js | 16 +- 58 files changed, 950 insertions(+), 263 deletions(-) create mode 100644 client/src/components/lists/List/MoveStep.jsx create mode 100644 client/src/components/lists/List/MoveStep.module.scss create mode 100644 server/api/helpers/cards/detach-custom-fields.js diff --git a/client/src/actions/lists.js b/client/src/actions/lists.js index eca13e7b..1c705d27 100644 --- a/client/src/actions/lists.js +++ b/client/src/actions/lists.js @@ -58,10 +58,34 @@ updateList.failure = (id, error) => ({ }, }); -const handleListUpdate = (list) => ({ +const handleListUpdate = ( + list, + isFetched, + users, + cards, + cardMemberships, + cardLabels, + taskLists, + tasks, + attachments, + customFieldGroups, + customFields, + customFieldValues, +) => ({ type: ActionTypes.LIST_UPDATE_HANDLE, payload: { list, + isFetched, + users, + cards, + cardMemberships, + cardLabels, + taskLists, + tasks, + attachments, + customFieldGroups, + customFields, + customFieldValues, }, }); diff --git a/client/src/components/lists/List/ActionsStep.jsx b/client/src/components/lists/List/ActionsStep.jsx index fc1a91f0..3e196dae 100755 --- a/client/src/components/lists/List/ActionsStep.jsx +++ b/client/src/components/lists/List/ActionsStep.jsx @@ -16,6 +16,7 @@ import { useSteps } from '../../../hooks'; import { ListTypes } from '../../../constants/Enums'; import EditColorStep from './EditColorStep'; import SortStep from './SortStep'; +import MoveStep from './MoveStep'; import SelectListTypeStep from '../SelectListTypeStep'; import ConfirmationStep from '../../common/ConfirmationStep'; import ArchiveCardsStep from '../../cards/ArchiveCardsStep'; @@ -26,6 +27,7 @@ const StepTypes = { EDIT_TYPE: 'EDIT_TYPE', EDIT_COLOR: 'EDIT_COLOR', SORT: 'SORT', + MOVE: 'MOVE', ARCHIVE_CARDS: 'ARCHIVE_CARDS', DELETE: 'DELETE', }; @@ -76,6 +78,10 @@ const ActionsStep = React.memo(({ listId, onNameEdit, onCardAdd, onClose }) => { openStep(StepTypes.SORT); }, [openStep]); + const handleMoveClick = useCallback(() => { + openStep(StepTypes.MOVE); + }, [openStep]); + const handleArchiveCardsClick = useCallback(() => { openStep(StepTypes.ARCHIVE_CARDS); }, [openStep]); @@ -102,6 +108,8 @@ const ActionsStep = React.memo(({ listId, onNameEdit, onCardAdd, onClose }) => { return ; case StepTypes.SORT: return ; + case StepTypes.MOVE: + return ; case StepTypes.ARCHIVE_CARDS: return ; case StepTypes.DELETE: @@ -157,6 +165,12 @@ const ActionsStep = React.memo(({ listId, onNameEdit, onCardAdd, onClose }) => { context: 'title', })} + + + {t('action.moveList', { + context: 'title', + })} + {list.type === ListTypes.CLOSED && ( diff --git a/client/src/components/lists/List/MoveStep.jsx b/client/src/components/lists/List/MoveStep.jsx new file mode 100644 index 00000000..efd25fe9 --- /dev/null +++ b/client/src/components/lists/List/MoveStep.jsx @@ -0,0 +1,133 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import React, { useMemo, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { Button, Dropdown, Form } from 'semantic-ui-react'; +import { Popup } from '../../../lib/custom-ui'; + +import selectors from '../../../selectors'; +import entryActions from '../../../entry-actions'; +import { useForm } from '../../../hooks'; + +import styles from './MoveStep.module.scss'; + +const MoveStep = React.memo(({ id, onBack, onClose }) => { + const selectListById = useMemo(() => selectors.makeSelectListById(), []); + const selectBoardById = useMemo(() => selectors.makeSelectBoardById(), []); + + const projectsToBoards = useSelector( + selectors.selectProjectsToBoardsWithEditorRightsForCurrentUser, + ); + + const list = useSelector((state) => selectListById(state, id)); + const projectId = useSelector((state) => selectBoardById(state, list.boardId).projectId); + + const dispatch = useDispatch(); + const [t] = useTranslation(); + + const defaultPath = useMemo( + () => ({ + projectId, + boardId: list.boardId, + }), + [list.boardId, projectId], + ); + + const [path, handleFieldChange] = useForm(() => ({ + projectId: null, + boardId: null, + ...defaultPath, + })); + + const selectedProject = useMemo( + () => projectsToBoards.find((project) => project.id === path.projectId) || null, + [projectsToBoards, path.projectId], + ); + + const selectedBoard = useMemo( + () => + (selectedProject && selectedProject.boards.find((board) => board.id === path.boardId)) || + null, + [selectedProject, path.boardId], + ); + + const handleSubmit = useCallback(() => { + if (selectedBoard.id !== defaultPath.boardId) { + dispatch(entryActions.transferList(id, selectedBoard.id)); + } + + onClose(); + }, [id, onClose, dispatch, defaultPath, selectedBoard]); + + return ( + <> + + {t('common.moveList', { + context: 'title', + })} + + +
+
{t('common.project')}
+ ({ + text: project.name, + value: project.id, + }))} + value={selectedProject && selectedProject.id} + placeholder={ + projectsToBoards.length === 0 ? t('common.noProjects') : t('common.selectProject') + } + disabled={projectsToBoards.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={handleFieldChange} + /> + + )} +