feat: Enhance card actions menu with separators and action bar

This commit is contained in:
Maksim Eltyshev
2025-12-09 20:30:05 +01:00
parent 9e6e38fcf7
commit 6da5c7aaca
36 changed files with 291 additions and 21 deletions
@@ -114,6 +114,103 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps(defaultStep || null);
const withActionBar = useMemo(() => {
let menuItemsTotal = 0;
let actionBarItemsTotal = 0;
if (card.type === CardTypes.PROJECT && canUseMembers) menuItemsTotal += 1;
if (canUseLabels) menuItemsTotal += 1;
if (card.type !== CardTypes.PROJECT && canUseMembers) menuItemsTotal += 1;
if (card.type === CardTypes.PROJECT && canEditDueDate) menuItemsTotal += 1;
if (card.type === CardTypes.PROJECT && canEditStopwatch) menuItemsTotal += 1;
if (canEditName) menuItemsTotal += 1;
if (!board.limitCardTypesToDefaultOne && canEditType) menuItemsTotal += 1;
if (canDuplicate) menuItemsTotal += 1;
if (canMove) menuItemsTotal += 1;
if (prevList && canRestore) menuItemsTotal += 1;
if (canCopy) {
menuItemsTotal += 1;
actionBarItemsTotal += 1;
}
if (canCut) {
menuItemsTotal += 1;
actionBarItemsTotal += 1;
}
if (list.type !== ListTypes.ARCHIVE && canArchive) {
menuItemsTotal += 1;
actionBarItemsTotal += 1;
}
if (canDelete) {
menuItemsTotal += 1;
actionBarItemsTotal += 1;
}
return menuItemsTotal > 4 && actionBarItemsTotal > 1;
}, [
board.limitCardTypesToDefaultOne,
card.type,
list.type,
prevList,
canEditType,
canEditName,
canEditDueDate,
canEditStopwatch,
canCopy,
canCut,
canDuplicate,
canMove,
canRestore,
canArchive,
canDelete,
canUseMembers,
canUseLabels,
]);
const hasTopSection = useMemo(() => {
return (
(card.type === CardTypes.PROJECT && canUseMembers) ||
canUseLabels ||
(card.type !== CardTypes.PROJECT && canUseMembers) ||
(card.type === CardTypes.PROJECT && canEditDueDate) ||
(card.type === CardTypes.PROJECT && canEditStopwatch) ||
canEditName ||
(!board.limitCardTypesToDefaultOne && canEditType)
);
}, [
board.limitCardTypesToDefaultOne,
card.type,
canEditType,
canEditName,
canEditDueDate,
canEditStopwatch,
canUseMembers,
canUseLabels,
]);
const hasBottomSection = useMemo(() => {
return (
(canCopy && !withActionBar) ||
(canCut && !withActionBar) ||
canDuplicate ||
canMove ||
(prevList && canRestore) ||
(list.type !== ListTypes.ARCHIVE && canArchive && !withActionBar) ||
(canDelete && !withActionBar)
);
}, [
list.type,
prevList,
canCopy,
canCut,
canDuplicate,
canMove,
canRestore,
canArchive,
canDelete,
withActionBar,
]);
const handleTypeSelect = useCallback(
(type) => {
dispatch(
@@ -299,22 +396,6 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
</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={handleMembersClick}>
<Icon name="user outline" className={styles.menuItemIcon} />
@@ -331,7 +412,7 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
})}
</Menu.Item>
)}
{card.type === CardTypes.STORY && canUseMembers && (
{card.type !== CardTypes.PROJECT && canUseMembers && (
<Menu.Item className={styles.menuItem} onClick={handleMembersClick}>
<Icon name="user outline" className={styles.menuItemIcon} />
{t('common.members', {
@@ -355,7 +436,24 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
})}
</Menu.Item>
)}
{canCopy && (
{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>
)}
{hasTopSection && hasBottomSection && <hr className={styles.divider} />}
{canCopy && !withActionBar && (
<Menu.Item className={styles.menuItem} onClick={handleCopyClick}>
<Icon name="copy outline" className={styles.menuItemIcon} />
{t('action.copyCard', {
@@ -363,7 +461,7 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
})}
</Menu.Item>
)}
{canCut && (
{canCut && !withActionBar && (
<Menu.Item className={styles.menuItem} onClick={handleCutClick}>
<Icon name="cut" className={styles.menuItemIcon} />
{t('action.cutCard', {
@@ -396,7 +494,7 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
})}
</Menu.Item>
)}
{list.type !== ListTypes.ARCHIVE && canArchive && (
{list.type !== ListTypes.ARCHIVE && canArchive && !withActionBar && (
<Menu.Item className={styles.menuItem} onClick={handleArchiveClick}>
<Icon name="folder open outline" className={styles.menuItemIcon} />
{t('action.archiveCard', {
@@ -404,7 +502,7 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
})}
</Menu.Item>
)}
{canDelete && (
{canDelete && !withActionBar && (
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
<Icon name="trash alternate outline" className={styles.menuItemIcon} />
{isInTrashList
@@ -416,6 +514,69 @@ const CardActionsStep = React.memo(({ cardId, defaultStep, onNameEdit, onClose }
})}
</Menu.Item>
)}
{withActionBar && (
<>
<hr className={styles.divider} />
<div className={styles.actionBar}>
{canCopy && (
/* eslint-disable-next-line jsx-a11y/anchor-is-valid,
jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */
<a className={styles.actionBarItem} onClick={handleCopyClick}>
<Icon fitted name="copy outline" />
<span className={styles.actionBarItemText}>
{t('action.copy', {
context: 'title',
})}
</span>
</a>
)}
{canCut && (
/* eslint-disable-next-line jsx-a11y/anchor-is-valid,
jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */
<a className={styles.actionBarItem} onClick={handleCutClick}>
<Icon fitted name="cut" />
<span className={styles.actionBarItemText}>
{t('action.cut', {
context: 'title',
})}
</span>
</a>
)}
{list.type !== ListTypes.ARCHIVE && canArchive && (
/* eslint-disable-next-line jsx-a11y/anchor-is-valid,
jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */
<a className={styles.actionBarItem} onClick={handleArchiveClick}>
<Icon fitted name="archive" />
<span className={styles.actionBarItemText}>
{t('action.archive', {
context: 'title',
})}
</span>
</a>
)}
{canDelete && (
/* eslint-disable-next-line jsx-a11y/anchor-is-valid,
jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */
<a className={styles.actionBarItem} onClick={handleDeleteClick}>
<Icon fitted name="trash alternate outline" />
<span className={styles.actionBarItemText}>
{isInTrashList
? t('action.deleteForever', {
context: 'title',
})
: t('action.delete', {
context: 'title',
})}
</span>
</a>
)}
</div>
</>
)}
</Menu>
</Popup.Content>
</>
@@ -4,6 +4,47 @@
*/
:global(#app) {
.actionBar {
display: flex;
margin: -7px 0;
}
.actionBarItem {
color: rgba(0, 0, 0, 0.87);
cursor: pointer;
display: flex;
flex: 1;
flex-direction: column;
font-size: 13px;
padding: 6px;
row-gap: 6px;
user-select: none;
&:not(:last-child) {
border-right: 1px solid #eee;
}
&:hover {
background: rgba(0, 0, 0, 0.05);
color: rgba(0, 0, 0, 0.95);
}
}
.actionBarItemText {
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
overflow: hidden;
text-align: center;
-webkit-box-orient: vertical;
}
.divider {
background: #eee;
border: 0;
height: 1px;
}
.menu {
margin: 0 -12px -5px;
width: calc(100% + 24px);