1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-28 01:29:44 +02:00

Move cards between boards and projects

This commit is contained in:
Maksim Eltyshev 2020-05-05 01:30:06 +05:00
parent ba2017705b
commit b1d187476d
24 changed files with 474 additions and 16 deletions

View file

@ -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 (
<MoveCardStep
projectsToLists={projectsToLists}
defaultPath={pick(card, ['projectId', 'boardId', 'listId'])}
onMove={onMove}
onTransfer={onTransfer}
onBoardFetch={onBoardFetch}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.DELETE:
return (
<DeleteStep
@ -180,6 +203,11 @@ const ActionsStep = React.memo(
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleMoveClick}>
{t('action.moveCard', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{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,

View file

@ -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,

View file

@ -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(
<EditDueDatePopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}>
<Button fluid className={styles.actionButton}>
<Icon name="calendar check outline" className={styles.actionIcon} />
{t('common.dueDate')}
{t('common.dueDate', {
context: 'title',
})}
</Button>
</EditDueDatePopup>
<EditTimerPopup defaultValue={timer} onUpdate={handleTimerUpdate}>
@ -354,6 +364,26 @@ const CardModal = React.memo(
<Icon name="paper plane outline" className={styles.actionIcon} />
{isSubscribed ? t('action.unsubscribe') : t('action.subscribe')}
</Button>
<MoveCardPopup
projectsToLists={allProjectsToLists}
defaultPath={{
projectId,
boardId,
listId,
}}
onMove={onMove}
onTransfer={onTransfer}
onBoardFetch={onBoardFetch}
>
<Button
fluid
className={styles.actionButton}
onClick={handleToggleSubscribeClick}
>
<Icon name="share square outline" className={styles.actionIcon} />
{t('action.move')}
</Button>
</MoveCardPopup>
<DeletePopup
title={t('common.deleteCard', {
context: 'title',
@ -385,20 +415,27 @@ CardModal.propTypes = {
isSubscribed: PropTypes.bool.isRequired,
isActionsFetching: PropTypes.bool.isRequired,
isAllActionsFetched: PropTypes.bool.isRequired,
listId: PropTypes.string.isRequired,
boardId: PropTypes.string.isRequired,
projectId: PropTypes.string.isRequired,
/* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
tasks: PropTypes.array.isRequired,
attachments: PropTypes.array.isRequired,
actions: PropTypes.array.isRequired,
allProjectsToLists: PropTypes.array.isRequired,
allProjectMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
isEditable: PropTypes.bool.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,

View file

@ -0,0 +1,5 @@
import { withPopup } from '../lib/popup';
import MoveCardStep from './MoveCardStep';
export default withPopup(MoveCardStep);

View file

@ -0,0 +1,161 @@
import React, { useMemo, useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Dropdown, Form } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { useForm } from '../../hooks';
import styles from './MoveCardStep.module.css';
const MoveCardStep = React.memo(
({ projectsToLists, defaultPath, onMove, onTransfer, onBoardFetch, onBack, onClose }) => {
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 (
<>
<Popup.Header onBack={onBack}>
{t('common.moveCard', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.project')}</div>
<Dropdown
fluid
selection
name="projectId"
options={projectsToLists.map((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 && (
<>
<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={handleBoardIdFieldChange}
/>
</>
)}
{selectedBoard && (
<>
<div className={styles.text}>{t('common.list')}</div>
<Dropdown
fluid
selection
name="listId"
options={selectedBoard.lists.map((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}
/>
</>
)}
<Button
positive
content={t('action.move')}
disabled={(selectedBoard && selectedBoard.isFetching !== false) || !selectedList}
/>
</Form>
</Popup.Content>
</>
);
},
);
MoveCardStep.propTypes = {
/* eslint-disable react/forbid-prop-types */
projectsToLists: PropTypes.array.isRequired,
defaultPath: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */
onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired,
onBoardFetch: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
MoveCardStep.defaultProps = {
onBack: undefined,
};
export default MoveCardStep;

View file

@ -0,0 +1,10 @@
.field {
margin-bottom: 8px;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}

View file

@ -0,0 +1,3 @@
import MoveCardStep from './MoveCardStep';
export default MoveCardStep;