feat: Add ability to move lists between boards (#1208)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user