feat: Add ability to move lists between boards (#1208)

This commit is contained in:
Symon Baikov
2025-09-04 00:07:10 +02:00
committed by GitHub
parent 5f34a737bb
commit 9683227fbc
58 changed files with 950 additions and 263 deletions
@@ -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 <EditColorStep listId={listId} onBack={handleBack} onClose={onClose} />;
case StepTypes.SORT:
return <SortStep listId={listId} onBack={handleBack} onClose={onClose} />;
case StepTypes.MOVE:
return <MoveStep id={listId} onBack={handleBack} onClose={onClose} />;
case StepTypes.ARCHIVE_CARDS:
return <ArchiveCardsStep listId={listId} onBack={handleBack} onClose={onClose} />;
case StepTypes.DELETE:
@@ -157,6 +165,12 @@ const ActionsStep = React.memo(({ listId, onNameEdit, onCardAdd, onClose }) => {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleMoveClick}>
<Icon name="share square outline" className={styles.menuItemIcon} />
{t('action.moveList', {
context: 'title',
})}
</Menu.Item>
{list.type === ListTypes.CLOSED && (
<Menu.Item className={styles.menuItem} onClick={handleArchiveCardsClick}>
<Icon name="folder open outline" className={styles.menuItemIcon} />
@@ -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 (
<>
<Popup.Header onBack={onBack}>
{t('common.moveList', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.project')}</div>
<Dropdown
fluid
selection
name="projectId"
options={projectsToBoards.map((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 && (
<>
<div className={styles.text}>{t('common.board')}</div>
<Dropdown
fluid
selection
name="boardId"
options={selectedProject.boards.map((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}
/>
</>
)}
<Button positive content={t('action.move')} disabled={!selectedBoard} />
</Form>
</Popup.Content>
</>
);
});
MoveStep.propTypes = {
id: PropTypes.string.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
MoveStep.defaultProps = {
onBack: undefined,
};
export default MoveStep;
@@ -0,0 +1,17 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) {
.field {
margin-bottom: 8px;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}
}