mirror of
https://github.com/plankanban/planka.git
synced 2025-07-21 22:29:42 +02:00
parent
ad7fb51cfa
commit
2ee1166747
1557 changed files with 76832 additions and 47042 deletions
392
client/src/components/cards/Card/ActionsStep.jsx
Normal file
392
client/src/components/cards/Card/ActionsStep.jsx
Normal file
|
@ -0,0 +1,392 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Menu } from 'semantic-ui-react';
|
||||
import { Popup } from '../../../lib/custom-ui';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { useSteps } from '../../../hooks';
|
||||
import { isListArchiveOrTrash } from '../../../utils/record-helpers';
|
||||
import { BoardMembershipRoles, CardTypes, ListTypes } from '../../../constants/Enums';
|
||||
import SelectCardTypeStep from '../SelectCardTypeStep';
|
||||
import EditDueDateStep from '../EditDueDateStep';
|
||||
import EditStopwatchStep from '../EditStopwatchStep';
|
||||
import MoveCardStep from '../MoveCardStep';
|
||||
import ConfirmationStep from '../../common/ConfirmationStep';
|
||||
import BoardMembershipsStep from '../../board-memberships/BoardMembershipsStep';
|
||||
import LabelsStep from '../../labels/LabelsStep';
|
||||
|
||||
import styles from './ActionsStep.module.scss';
|
||||
|
||||
const StepTypes = {
|
||||
EDIT_TYPE: 'EDIT_TYPE',
|
||||
USERS: 'USERS',
|
||||
LABELS: 'LABELS',
|
||||
EDIT_DUE_DATE: 'EDIT_DUE_DATE',
|
||||
EDIT_STOPWATCH: 'EDIT_STOPWATCH',
|
||||
MOVE: 'MOVE',
|
||||
ARCHIVE: 'ARCHIVE',
|
||||
DELETE: 'DELETE',
|
||||
};
|
||||
|
||||
const ActionsStep = React.memo(({ cardId, onNameEdit, onClose }) => {
|
||||
const selectCardById = useMemo(() => selectors.makeSelectCardById(), []);
|
||||
const selectListById = useMemo(() => selectors.makeSelectListById(), []);
|
||||
const selectPrevListById = useMemo(() => selectors.makeSelectListById(), []);
|
||||
const selectUserIdsByCardId = useMemo(() => selectors.makeSelectUserIdsByCardId(), []);
|
||||
const selectLabelIdsByCardId = useMemo(() => selectors.makeSelectLabelIdsByCardId(), []);
|
||||
|
||||
const board = useSelector(selectors.selectCurrentBoard);
|
||||
const card = useSelector((state) => selectCardById(state, cardId));
|
||||
const list = useSelector((state) => selectListById(state, card.listId));
|
||||
|
||||
// TODO: check availability?
|
||||
const prevList = useSelector(
|
||||
(state) => card.prevListId && selectPrevListById(state, card.prevListId),
|
||||
);
|
||||
|
||||
const userIds = useSelector((state) => selectUserIdsByCardId(state, cardId));
|
||||
const labelIds = useSelector((state) => selectLabelIdsByCardId(state, cardId));
|
||||
|
||||
const {
|
||||
canEditType,
|
||||
canEditName,
|
||||
canEditDueDate,
|
||||
canEditStopwatch,
|
||||
canDuplicate,
|
||||
canMove,
|
||||
canRestore,
|
||||
canArchive,
|
||||
canDelete,
|
||||
canUseMembers,
|
||||
canUseLabels,
|
||||
} = useSelector((state) => {
|
||||
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
|
||||
|
||||
if (isListArchiveOrTrash(list)) {
|
||||
return {
|
||||
canEditType: false,
|
||||
canEditName: false,
|
||||
canEditDueDate: false,
|
||||
canEditStopwatch: false,
|
||||
canDuplicate: false,
|
||||
canMove: false,
|
||||
canRestore: isEditor,
|
||||
canArchive: isEditor,
|
||||
canDelete: isEditor,
|
||||
canUseMembers: false,
|
||||
canUseLabels: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
canEditType: isEditor,
|
||||
canEditName: isEditor,
|
||||
canEditDueDate: isEditor,
|
||||
canEditStopwatch: isEditor,
|
||||
canDuplicate: isEditor,
|
||||
canMove: isEditor,
|
||||
canRestore: null,
|
||||
canArchive: isEditor,
|
||||
canDelete: isEditor,
|
||||
canUseMembers: isEditor,
|
||||
canUseLabels: isEditor,
|
||||
};
|
||||
}, shallowEqual);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
|
||||
const handleTypeSelect = useCallback(
|
||||
(type) => {
|
||||
dispatch(
|
||||
entryActions.updateCard(cardId, {
|
||||
type,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[cardId, dispatch],
|
||||
);
|
||||
|
||||
const handleDuplicateClick = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.duplicateCard(cardId, {
|
||||
name: `${card.name} (${t('common.copy', {
|
||||
context: 'inline',
|
||||
})})`,
|
||||
}),
|
||||
);
|
||||
|
||||
onClose();
|
||||
}, [cardId, onClose, card.name, dispatch, t]);
|
||||
|
||||
const handleRestoreClick = useCallback(() => {
|
||||
dispatch(entryActions.moveCard(cardId, card.prevListId, undefined, true));
|
||||
}, [cardId, card.prevListId, dispatch]);
|
||||
|
||||
const handleArchiveConfirm = useCallback(() => {
|
||||
dispatch(entryActions.moveCardToArchive(cardId));
|
||||
}, [cardId, dispatch]);
|
||||
|
||||
const isInTrashList = list.type === ListTypes.TRASH;
|
||||
|
||||
const handleDeleteConfirm = useCallback(() => {
|
||||
if (isInTrashList) {
|
||||
dispatch(entryActions.deleteCard(cardId));
|
||||
} else {
|
||||
dispatch(entryActions.moveCardToTrash(cardId));
|
||||
}
|
||||
}, [cardId, isInTrashList, dispatch]);
|
||||
|
||||
const handleUserSelect = useCallback(
|
||||
(userId) => {
|
||||
dispatch(entryActions.addUserToCard(userId, cardId));
|
||||
},
|
||||
[cardId, dispatch],
|
||||
);
|
||||
|
||||
const handleUserDeselect = useCallback(
|
||||
(userId) => {
|
||||
dispatch(entryActions.removeUserFromCard(userId, cardId));
|
||||
},
|
||||
[cardId, dispatch],
|
||||
);
|
||||
|
||||
const handleLabelSelect = useCallback(
|
||||
(labelId) => {
|
||||
dispatch(entryActions.addLabelToCard(labelId, cardId));
|
||||
},
|
||||
[cardId, dispatch],
|
||||
);
|
||||
|
||||
const handleLabelDeselect = useCallback(
|
||||
(labelId) => {
|
||||
dispatch(entryActions.removeLabelFromCard(labelId, cardId));
|
||||
},
|
||||
[cardId, dispatch],
|
||||
);
|
||||
|
||||
const handleEditNameClick = useCallback(() => {
|
||||
onNameEdit();
|
||||
onClose();
|
||||
}, [onNameEdit, onClose]);
|
||||
|
||||
const handleEditTypeClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_TYPE);
|
||||
}, [openStep]);
|
||||
|
||||
const handleUsersClick = useCallback(() => {
|
||||
openStep(StepTypes.USERS);
|
||||
}, [openStep]);
|
||||
|
||||
const handleLabelsClick = useCallback(() => {
|
||||
openStep(StepTypes.LABELS);
|
||||
}, [openStep]);
|
||||
|
||||
const handleEditDueDateClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_DUE_DATE);
|
||||
}, [openStep]);
|
||||
|
||||
const handleEditStopwatchClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_STOPWATCH);
|
||||
}, [openStep]);
|
||||
|
||||
const handleMoveClick = useCallback(() => {
|
||||
openStep(StepTypes.MOVE);
|
||||
}, [openStep]);
|
||||
|
||||
const handleArchiveClick = useCallback(() => {
|
||||
openStep(StepTypes.ARCHIVE);
|
||||
}, [openStep]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
openStep(StepTypes.DELETE);
|
||||
}, [openStep]);
|
||||
|
||||
if (step) {
|
||||
switch (step.type) {
|
||||
case StepTypes.EDIT_TYPE:
|
||||
return (
|
||||
<SelectCardTypeStep
|
||||
withButton
|
||||
defaultValue={card.type}
|
||||
title="common.editType"
|
||||
buttonContent="action.save"
|
||||
onSelect={handleTypeSelect}
|
||||
onBack={handleBack}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
case StepTypes.USERS:
|
||||
return (
|
||||
<BoardMembershipsStep
|
||||
currentUserIds={userIds}
|
||||
onUserSelect={handleUserSelect}
|
||||
onUserDeselect={handleUserDeselect}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
case StepTypes.LABELS:
|
||||
return (
|
||||
<LabelsStep
|
||||
currentIds={labelIds}
|
||||
cardId={cardId}
|
||||
onSelect={handleLabelSelect}
|
||||
onDeselect={handleLabelDeselect}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
case StepTypes.EDIT_DUE_DATE:
|
||||
return <EditDueDateStep cardId={cardId} onBack={handleBack} onClose={onClose} />;
|
||||
case StepTypes.EDIT_STOPWATCH:
|
||||
return <EditStopwatchStep cardId={cardId} onBack={handleBack} onClose={onClose} />;
|
||||
case StepTypes.MOVE:
|
||||
return <MoveCardStep id={cardId} onBack={handleBack} onClose={onClose} />;
|
||||
case StepTypes.ARCHIVE:
|
||||
return (
|
||||
<ConfirmationStep
|
||||
title="common.archiveCard"
|
||||
content="common.areYouSureYouWantToArchiveThisCard"
|
||||
buttonContent="action.archiveCard"
|
||||
onConfirm={handleArchiveConfirm}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
case StepTypes.DELETE:
|
||||
return (
|
||||
<ConfirmationStep
|
||||
title={isInTrashList ? 'common.deleteCardForever' : 'common.deleteCard'}
|
||||
content={
|
||||
isInTrashList
|
||||
? 'common.areYouSureYouWantToDeleteThisCardForever'
|
||||
: 'common.areYouSureYouWantToDeleteThisCard'
|
||||
}
|
||||
buttonContent={isInTrashList ? 'action.deleteCardForever' : 'action.deleteCard'}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t('common.cardActions', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
{canEditName && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
|
||||
{t('action.editTitle', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{!board.limitCardTypesToDefaultOne && canEditType && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditTypeClick}>
|
||||
{t('action.editType', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{card.type === CardTypes.PROJECT && canUseMembers && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleUsersClick}>
|
||||
{t('common.members', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canUseLabels && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleLabelsClick}>
|
||||
{t('common.labels', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{card.type === CardTypes.STORY && canUseMembers && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleUsersClick}>
|
||||
{t('common.members', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{card.type === CardTypes.PROJECT && canEditDueDate && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditDueDateClick}>
|
||||
{t('action.editDueDate', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{card.type === CardTypes.PROJECT && canEditStopwatch && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEditStopwatchClick}>
|
||||
{t('action.editStopwatch', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canDuplicate && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleDuplicateClick}>
|
||||
{t('action.duplicateCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canMove && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleMoveClick}>
|
||||
{t('action.moveCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{prevList && canRestore && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleRestoreClick}>
|
||||
{t('action.restoreToList', {
|
||||
context: 'title',
|
||||
list: prevList.name || t(`common.${prevList.type}`),
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{list.type !== ListTypes.ARCHIVE && canArchive && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleArchiveClick}>
|
||||
{t('action.archiveCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canDelete && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
|
||||
{isInTrashList
|
||||
? t('action.deleteForever', {
|
||||
context: 'title',
|
||||
})
|
||||
: t('action.deleteCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ActionsStep.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
onNameEdit: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ActionsStep;
|
Loading…
Add table
Add a link
Reference in a new issue