UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: горизонтальное меню карточек без изменения сетки канбана

This commit is contained in:
DCCONSTRUCTIONS 2026-04-29 16:42:52 +03:00
parent 4dbb7b500c
commit 5f9d9c418e
4 changed files with 94 additions and 17 deletions

View File

@ -330,14 +330,14 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
</div>
}
buttonClassName="h-12 w-12"
menuClassName="min-w-[18rem]"
menuClassName="nodedc-work-item-action-menu"
onOpenChange={(isOpen) => {
if (isOpen) void ensureSourceOptions();
}}
items={[]}
menuContent={({ closeDropdown }) => (
<div className="max-h-[calc(100vh-2rem)] space-y-2 overflow-y-auto" onClick={stopCardPropagation}>
<div className="space-y-1">
<div className="nodedc-work-item-action-grid" onClick={stopCardPropagation}>
<div className="nodedc-work-item-action-section space-y-1">
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">
Приоритет
</div>
@ -358,7 +358,7 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
))}
</div>
<div className="space-y-1 border-t border-white/8 pt-2">
<div className="nodedc-work-item-action-section space-y-1">
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">
Статус
</div>
@ -388,7 +388,7 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
)}
</div>
<div className="space-y-1 border-t border-white/8 pt-2">
<div className="nodedc-work-item-action-section space-y-1">
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">
Быстрые действия
</div>

View File

@ -96,7 +96,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
};
const menuContentBefore = ({ closeDropdown }: { closeDropdown: () => void }) => (
<>
<div className="space-y-1">
<div className="nodedc-work-item-action-section space-y-1">
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">Приоритет</div>
{priorityOptions.map((priority) => (
<button
@ -115,7 +115,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
))}
</div>
<div className="space-y-1 border-t border-white/8 pt-2">
<div className="nodedc-work-item-action-section space-y-1">
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">Статус</div>
{stateOptions.map((state) => (
<button
@ -138,12 +138,6 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
</button>
))}
</div>
<div className="border-t border-white/8 pt-2">
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">
Быстрые действия
</div>
</div>
</>
);
const dateButton = (
@ -214,7 +208,7 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
issue,
parentRef: cardRef,
customActionButton,
menuClassName: "min-w-[18rem]",
menuClassName: "nodedc-work-item-action-menu",
menuContentBefore,
placement: "bottom-end",
})}

View File

@ -12,7 +12,8 @@ import { useParams } from "next/navigation";
import { ARCHIVABLE_STATE_GROUPS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import type { TIssue } from "@plane/types";
import { EIssuesStoreType } from "@plane/types";
import { ActionDropdown, ContextMenu } from "@plane/ui";
import { ActionDropdown, ContextMenu, type TContextMenuItem } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useIssues } from "@/hooks/store/use-issues";
import { useProject } from "@/hooks/store/use-project";
@ -102,6 +103,56 @@ export const ProjectIssueQuickActions = observer(function ProjectIssueQuickActio
};
const MENU_ITEMS = useProjectIssueMenuItems(menuItemProps);
const shouldUseNodedcActionGrid = menuClassName?.includes("nodedc-work-item-action-menu") ?? false;
const DROPDOWN_MENU_ITEMS = shouldUseNodedcActionGrid
? MENU_ITEMS.filter((item) => item.key !== "open-in-new-tab")
: MENU_ITEMS;
const renderNodedcMenuItem = (item: TContextMenuItem, closeDropdown: () => void) => {
if (item.shouldRender === false) return null;
const Icon = item.icon;
return (
<button
key={item.key}
type="button"
className={cn(
"flex w-full items-center gap-2 rounded-[0.9rem] px-2.5 py-2 text-left text-12 text-secondary transition-colors",
{
"cursor-not-allowed text-placeholder": item.disabled,
"hover:bg-white/6": !item.disabled,
},
item.className
)}
disabled={item.disabled}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
if (item.disabled) return;
item.action();
closeDropdown();
}}
>
{Icon && <Icon className={cn("h-3.5 w-3.5 shrink-0", item.iconClassName)} />}
<span className="min-w-0 truncate">{item.title}</span>
</button>
);
};
const renderNodedcMenuContent = ({ closeDropdown }: { closeDropdown: () => void }) => (
<div className="nodedc-work-item-action-grid">
{typeof menuContentBefore === "function" ? menuContentBefore({ closeDropdown }) : menuContentBefore}
<div className="nodedc-work-item-action-section space-y-1">
<div className="px-2 text-[10px] font-semibold tracking-[0.16em] text-tertiary uppercase">
Быстрые действия
</div>
{DROPDOWN_MENU_ITEMS.map((item) => renderNodedcMenuItem(item, closeDropdown))}
</div>
</div>
);
const CONTEXT_MENU_ITEMS = MENU_ITEMS.map(function CONTEXT_MENU_ITEMS(item) {
return {
@ -153,9 +204,10 @@ export const ProjectIssueQuickActions = observer(function ProjectIssueQuickActio
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<ActionDropdown
button={customActionButton}
items={MENU_ITEMS}
items={shouldUseNodedcActionGrid ? [] : MENU_ITEMS}
menuClassName={menuClassName}
menuContentBefore={menuContentBefore}
menuContent={shouldUseNodedcActionGrid ? renderNodedcMenuContent : undefined}
menuContentBefore={shouldUseNodedcActionGrid ? undefined : menuContentBefore}
placement={placements}
portalElement={portalElement}
/>

View File

@ -347,6 +347,37 @@
0 6px 18px rgba(0, 0, 0, 0.2);
}
.nodedc-tall-action-menu > div {
max-height: calc(100vh - 24px) !important;
}
.nodedc-work-item-action-menu {
width: min(calc(100vw - 24px), 46rem);
}
.nodedc-work-item-action-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.5rem;
align-items: start;
}
.nodedc-work-item-action-section {
min-width: 0;
}
@media (max-width: 760px) {
.nodedc-work-item-action-menu {
width: min(calc(100vw - 24px), 18rem);
}
.nodedc-work-item-action-grid {
grid-template-columns: 1fr;
max-height: calc(100vh - 24px);
overflow-y: auto;
}
}
.nodedc-bottom-dock {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.024) 0%, rgba(255, 255, 255, 0.008) 100%), rgba(7, 7, 10, 0.72) !important;