feat: Add ability to copy/cut cards with shortcut support
This commit is contained in:
@@ -16,6 +16,7 @@ import { closePopup, usePopup } from '../../../lib/popup';
|
||||
import selectors from '../../../selectors';
|
||||
import { BoardShortcutsContext } from '../../../contexts';
|
||||
import Paths from '../../../constants/Paths';
|
||||
import ClipboardTypes from '../../../constants/ClipboardTypes';
|
||||
import { BoardMembershipRoles, CardTypes } from '../../../constants/Enums';
|
||||
import ProjectContent from './ProjectContent';
|
||||
import StoryContent from './StoryContent';
|
||||
@@ -44,14 +45,25 @@ const Card = React.memo(({ id, isInline }) => {
|
||||
return selectIsCardWithIdRecent(state, id);
|
||||
});
|
||||
|
||||
const isCut = useSelector((state) => {
|
||||
const clipboard = selectors.selectClipboard(state);
|
||||
return clipboard && clipboard.type === ClipboardTypes.CUT && card.id === clipboard.cardId;
|
||||
});
|
||||
|
||||
const canUseActions = useSelector((state) => {
|
||||
const isManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
|
||||
|
||||
if (isManager) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
|
||||
});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [isEditNameOpened, setIsEditNameOpened] = useState(false);
|
||||
const [handleCardMouseEnter, handleCardMouseLeave] = useContext(BoardShortcutsContext);
|
||||
const [, , handleCardMouseEnter, handleCardMouseLeave] = useContext(BoardShortcutsContext);
|
||||
|
||||
const actionsPopupRef = useRef(null);
|
||||
|
||||
@@ -139,7 +151,11 @@ const Card = React.memo(({ id, isInline }) => {
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
|
||||
jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={classNames(styles.content, card.isClosed && styles.contentDisabled)}
|
||||
className={classNames(
|
||||
styles.content,
|
||||
card.isClosed && styles.contentDisabled,
|
||||
isCut && styles.contentCut,
|
||||
)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleCardMouseLeave}
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -44,6 +44,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.contentCut {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.contentDisabled {
|
||||
filter: saturate(0.5);
|
||||
opacity: 0.64;
|
||||
|
||||
@@ -60,6 +60,8 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
|
||||
canEditName,
|
||||
canEditDueDate,
|
||||
canEditStopwatch,
|
||||
canCopy,
|
||||
canCut,
|
||||
canDuplicate,
|
||||
canMove,
|
||||
canRestore,
|
||||
@@ -68,6 +70,8 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
|
||||
canUseMembers,
|
||||
canUseLabels,
|
||||
} = useSelector((state) => {
|
||||
const isManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
|
||||
|
||||
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
|
||||
|
||||
@@ -77,6 +81,8 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
|
||||
canEditName: false,
|
||||
canEditDueDate: false,
|
||||
canEditStopwatch: false,
|
||||
canCopy: isManager || isEditor,
|
||||
canCut: isEditor,
|
||||
canDuplicate: false,
|
||||
canMove: false,
|
||||
canRestore: isEditor,
|
||||
@@ -92,6 +98,8 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
|
||||
canEditName: isEditor,
|
||||
canEditDueDate: isEditor,
|
||||
canEditStopwatch: isEditor,
|
||||
canCopy: isManager || isEditor,
|
||||
canCut: isEditor,
|
||||
canDuplicate: isEditor,
|
||||
canMove: isEditor,
|
||||
canRestore: null,
|
||||
@@ -117,17 +125,20 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
|
||||
[cardId, dispatch],
|
||||
);
|
||||
|
||||
const handleDuplicateClick = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.duplicateCard(cardId, {
|
||||
name: `${card.name} (${t('common.copy', {
|
||||
context: 'inline',
|
||||
})})`,
|
||||
}),
|
||||
);
|
||||
|
||||
const handleCopyClick = useCallback(() => {
|
||||
dispatch(entryActions.copyCard(cardId));
|
||||
onClose();
|
||||
}, [cardId, onClose, card.name, dispatch, t]);
|
||||
}, [cardId, onClose, dispatch]);
|
||||
|
||||
const handleCutClick = useCallback(() => {
|
||||
dispatch(entryActions.cutCard(cardId));
|
||||
onClose();
|
||||
}, [cardId, onClose, dispatch]);
|
||||
|
||||
const handleDuplicateClick = useCallback(() => {
|
||||
dispatch(entryActions.duplicateCard(cardId));
|
||||
onClose();
|
||||
}, [cardId, onClose, dispatch]);
|
||||
|
||||
const handleRestoreClick = useCallback(() => {
|
||||
dispatch(entryActions.moveCard(cardId, card.prevListId, undefined, true));
|
||||
@@ -344,6 +355,22 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canCopy && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleCopyClick}>
|
||||
<Icon name="copy outline" className={styles.menuItemIcon} />
|
||||
{t('action.copyCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canCut && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleCutClick}>
|
||||
<Icon name="cut" className={styles.menuItemIcon} />
|
||||
{t('action.cutCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canDuplicate && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleDuplicateClick}>
|
||||
<Icon name="copy outline" className={styles.menuItemIcon} />
|
||||
|
||||
@@ -34,17 +34,17 @@ const MoreActionsStep = React.memo(({ onClose }) => {
|
||||
const { canEditType, canDuplicate, canMove } = useSelector((state) => {
|
||||
const list = selectListById(state, card.listId);
|
||||
|
||||
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
|
||||
|
||||
if (isListArchiveOrTrash(list)) {
|
||||
return {
|
||||
canEditType: false,
|
||||
canDuplicate: false,
|
||||
canMove: false,
|
||||
canMove: isEditor,
|
||||
};
|
||||
}
|
||||
|
||||
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
const isEditor = !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
|
||||
|
||||
return {
|
||||
canEditType: isEditor,
|
||||
canDuplicate: isEditor,
|
||||
@@ -68,14 +68,8 @@ const MoreActionsStep = React.memo(({ onClose }) => {
|
||||
);
|
||||
|
||||
const handleDuplicateClick = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.duplicateCurrentCard({
|
||||
name: `${card.name} (${t('common.copy', {
|
||||
context: 'inline',
|
||||
})})`,
|
||||
}),
|
||||
);
|
||||
}, [card.name, dispatch, t]);
|
||||
dispatch(entryActions.duplicateCurrentCard());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleEditTypeClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_TYPE);
|
||||
|
||||
@@ -105,7 +105,7 @@ const ProjectContent = React.memo(() => {
|
||||
canSubscribe: isMember,
|
||||
canJoin: false,
|
||||
canDuplicate: false,
|
||||
canMove: false,
|
||||
canMove: isEditor,
|
||||
canRestore: isEditor,
|
||||
canArchive: isEditor,
|
||||
canDelete: isEditor,
|
||||
|
||||
@@ -104,7 +104,7 @@ const StoryContent = React.memo(() => {
|
||||
canSubscribe: isMember,
|
||||
canJoin: false,
|
||||
canDuplicate: false,
|
||||
canMove: false,
|
||||
canMove: isEditor,
|
||||
canRestore: isEditor,
|
||||
canArchive: isEditor,
|
||||
canDelete: isEditor,
|
||||
|
||||
Reference in New Issue
Block a user