1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 20:59:44 +02:00
planka/client/src/components/cards/Card/ActionsStep.jsx
2025-07-17 15:23:53 +02:00

404 lines
13 KiB
JavaScript

/*!
* 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 { Icon, 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}>
<Icon name="edit outline" className={styles.menuItemIcon} />
{t('action.editTitle', {
context: 'title',
})}
</Menu.Item>
)}
{!board.limitCardTypesToDefaultOne && canEditType && (
<Menu.Item className={styles.menuItem} onClick={handleEditTypeClick}>
<Icon name="map outline" className={styles.menuItemIcon} />
{t('action.editType', {
context: 'title',
})}
</Menu.Item>
)}
{card.type === CardTypes.PROJECT && canUseMembers && (
<Menu.Item className={styles.menuItem} onClick={handleUsersClick}>
<Icon name="user outline" className={styles.menuItemIcon} />
{t('common.members', {
context: 'title',
})}
</Menu.Item>
)}
{canUseLabels && (
<Menu.Item className={styles.menuItem} onClick={handleLabelsClick}>
<Icon name="bookmark outline" className={styles.menuItemIcon} />
{t('common.labels', {
context: 'title',
})}
</Menu.Item>
)}
{card.type === CardTypes.STORY && canUseMembers && (
<Menu.Item className={styles.menuItem} onClick={handleUsersClick}>
<Icon name="user outline" className={styles.menuItemIcon} />
{t('common.members', {
context: 'title',
})}
</Menu.Item>
)}
{card.type === CardTypes.PROJECT && canEditDueDate && (
<Menu.Item className={styles.menuItem} onClick={handleEditDueDateClick}>
<Icon name="calendar check outline" className={styles.menuItemIcon} />
{t('action.editDueDate', {
context: 'title',
})}
</Menu.Item>
)}
{card.type === CardTypes.PROJECT && canEditStopwatch && (
<Menu.Item className={styles.menuItem} onClick={handleEditStopwatchClick}>
<Icon name="clock outline" className={styles.menuItemIcon} />
{t('action.editStopwatch', {
context: 'title',
})}
</Menu.Item>
)}
{canDuplicate && (
<Menu.Item className={styles.menuItem} onClick={handleDuplicateClick}>
<Icon name="copy outline" className={styles.menuItemIcon} />
{t('action.duplicateCard', {
context: 'title',
})}
</Menu.Item>
)}
{canMove && (
<Menu.Item className={styles.menuItem} onClick={handleMoveClick}>
<Icon name="share square outline" className={styles.menuItemIcon} />
{t('action.moveCard', {
context: 'title',
})}
</Menu.Item>
)}
{prevList && canRestore && (
<Menu.Item className={styles.menuItem} onClick={handleRestoreClick}>
<Icon name="undo alternate" className={styles.menuItemIcon} />
{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}>
<Icon name="folder open outline" className={styles.menuItemIcon} />
{t('action.archiveCard', {
context: 'title',
})}
</Menu.Item>
)}
{canDelete && (
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
<Icon name="trash alternate outline" className={styles.menuItemIcon} />
{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;