"use client"; import { useTranslation } from "react-i18next"; import { Fragment, type ReactNode } from "react"; import { IconCopy, IconEdit, IconPencil, IconPin, IconPinFilled, IconTrash, } from "@tabler/icons-react"; import { ContextMenuItem, ContextMenuSeparator } from "@kandev/ui/context-menu"; import type { TaskMoveWorkflow } from "@/components/task/task-move-context-menu"; import { KanbanCardContextMenuItems, type KanbanCardMenuEntry, } from "@/components/kanban-card-menu-items"; import { TaskNestContextMenuItems } from "@/components/task/task-nest-context-menu"; import { useTaskWorkflowMove } from "@/hooks/use-task-workflow-move"; import { useUpdateTaskPriority } from "@/hooks/use-update-task-priority"; import { TaskColorMenu } from "./task-switcher-color-menu"; import { TaskPluginLinkMenu, selectTaskLinkActions } from "./task-switcher-link-menu"; import { TaskArchiveItem, TaskCreateSubtaskItem, TaskDeleteItem, TaskDetachItem, } from "./task-switcher-action-items"; import { useTaskPluginLinkActions, type PluginLinkMenuAction, } from "./task-session-sidebar-link-actions"; import type { StepDef, TaskSwitcherItem } from "./task-switcher-types"; import { useTaskPluginPrimaryMenuEntries } from "./task-switcher-plugin-menu-items"; import { TaskPriorityContextMenu } from "./task-priority-context-menu"; import type { TaskContextMenuItemsProps } from "./task-switcher-context-menu"; import { taskRowActionAvailability } from "./task-row-action-availability"; import { TaskMoveItems } from "./task-switcher-context-menu-move-items"; type SingleSelectionMenuProps = TaskContextMenuItemsProps & { actingIds: string[]; actingOnSelection: boolean; }; type LinkActions = ReturnType; type SingleGroupProps = Pick< SingleSelectionMenuProps, | "task" | "nestCandidateTasks" | "nestHierarchyTasks" | "isDeleting" | "isArchiving" | "isPinned" | "onTogglePin" | "onEditTask" | "onRenameTask" | "onCreateSubtask" | "onDetachTask" | "onArchiveTask" | "onDeleteTask" | "closeMenu" | "actingIds" | "actingOnSelection" | "onBulkArchive" | "onClearSelection" | "onBulkMove" | "workflows" | "stepsByWorkflowId" | "steps" | "onMoveToStep" | "isMixedWorkflowSelection" | "moveTasks" > & { linkActions?: LinkActions; pluginLinkActions?: PluginLinkMenuAction[]; hasLinkGroup?: boolean; }; type TaskContextMenuGroup = { key: string; visible: boolean; content: ReactNode; }; function TaskContextMenuGroups({ groups }: { groups: readonly TaskContextMenuGroup[] }) { const visibleGroups = groups.filter((group) => group.visible); return ( <> {visibleGroups.map((group, index) => ( {index > 0 && } {group.content} ))} ); } type SingleMenuState = { pluginPrimaryEntries: KanbanCardMenuEntry[]; pluginLinkActions: PluginLinkMenuAction[]; linkActions: LinkActions; hasMoveGroup: boolean; hasLinkGroup: boolean; hasRelationshipsGroup: boolean; onDelete: ((id: string) => void) | undefined; onDetach: ((id: string) => void) | undefined; }; function useSingleMenuState(props: SingleSelectionMenuProps): SingleMenuState { const { task, closeMenu, stepsByWorkflowId, steps, workflows, actingOnSelection } = props; const pluginPrimaryEntries = useTaskPluginPrimaryMenuEntries(task, props.isDeleting); const pluginLinkActions = useTaskPluginLinkActions(task.id, task.repositoryLinks ?? []); const linkActions = selectTaskLinkActions(task, closeMenu, props); const workflowId = task.workflowId; const moveStepsByWorkflowId = stepsByWorkflowId ?? (workflowId && steps ? { [workflowId]: steps } : undefined); const currentMoveSteps = workflowId ? (moveStepsByWorkflowId?.[workflowId] ?? []) : []; const hasMoveGroup = hasSingleMoveGroup({ task, workflows, workflowId, currentMoveSteps, actingOnSelection, isMixedWorkflowSelection: props.isMixedWorkflowSelection, }); const hasLinkGroup = Object.values(linkActions).some(Boolean) || pluginLinkActions.length > 0; const hasRelationshipsGroup = Boolean( props.onCreateSubtask || (!task.isArchived && task.workflowId) || hasLinkGroup || (task.parentTaskId && props.onDetachTask), ); return { pluginPrimaryEntries, pluginLinkActions, linkActions, hasMoveGroup, hasLinkGroup, hasRelationshipsGroup, onDelete: withSelectionClear(actingOnSelection, props.onClearSelection, props.onDeleteTask), onDetach: withSelectionClear(actingOnSelection, props.onClearSelection, props.onDetachTask), }; } function SingleSelectionMenuItems(props: SingleSelectionMenuProps) { return ; } function SingleSelectionMenuGroups(props: SingleSelectionMenuProps & SingleMenuState) { const { task } = props; return ( , }, { key: "edit", visible: Boolean(!task.isArchived || props.onRenameTask), content: , }, { key: "relationships", visible: props.hasRelationshipsGroup, content: , }, { key: "move", visible: props.hasMoveGroup, content: , }, { key: "plugins", visible: props.pluginPrimaryEntries.length > 0, content: , }, { key: "remove", visible: hasSingleRemoveGroup(props), content: , }, ]} /> ); } function hasSingleMoveGroup({ task, workflows, workflowId, currentMoveSteps, actingOnSelection, isMixedWorkflowSelection, }: Pick< SingleSelectionMenuProps, "task" | "workflows" | "actingOnSelection" | "isMixedWorkflowSelection" > & { workflowId: string | undefined; currentMoveSteps: StepDef[]; }) { if (!taskRowActionAvailability(task).move) return false; const hasSameWorkflowMove = currentMoveSteps.length > 1 && (!actingOnSelection || !isMixedWorkflowSelection); const hasCrossWorkflowMove = (workflows ?? []).some( (workflow) => !workflow.hidden && workflow.id !== workflowId, ); return hasSameWorkflowMove || hasCrossWorkflowMove; } function hasSingleRemoveGroup({ actingOnSelection, onArchiveTask, onBulkArchive, onDeleteTask, }: Pick< SingleSelectionMenuProps, "actingOnSelection" | "onArchiveTask" | "onBulkArchive" | "onDeleteTask" >) { return Boolean(onArchiveTask || onDeleteTask || (actingOnSelection && onBulkArchive)); } function SingleMarkGroup({ task, isPinned, isDeleting, actingOnSelection, onClearSelection, onTogglePin, }: Pick< SingleGroupProps, "task" | "isPinned" | "isDeleting" | "actingOnSelection" | "onClearSelection" | "onTogglePin" >) { const updateTaskPriority = useUpdateTaskPriority(); return ( <> {taskRowActionAvailability(task).mark && ( )} {taskRowActionAvailability(task).mark && ( void updateTaskPriority(task.id, priority)} /> )} ); } function SingleEditGroup({ task, isDeleting, onEditTask, onRenameTask, }: Pick) { const { t } = useTranslation(); return ( <> {taskRowActionAvailability(task).mark && ( {t("settings:duplicate")} )} ); } function SingleRelationshipsGroup({ task, nestCandidateTasks, nestHierarchyTasks, isDeleting, onCreateSubtask, onDetachTask, closeMenu, linkActions, pluginLinkActions, hasLinkGroup, }: Pick< SingleGroupProps, | "task" | "nestCandidateTasks" | "nestHierarchyTasks" | "isDeleting" | "onCreateSubtask" | "onDetachTask" | "closeMenu" | "linkActions" | "pluginLinkActions" | "hasLinkGroup" >) { return ( <> {!task.isArchived && ( )} {hasLinkGroup && linkActions && pluginLinkActions && ( )} ); } function SingleRemoveGroup({ task, isDeleting, isArchiving, onArchiveTask, onDeleteTask, actingIds, actingOnSelection, onBulkArchive, }: Pick< SingleGroupProps, | "task" | "isDeleting" | "isArchiving" | "onArchiveTask" | "onDeleteTask" | "actingIds" | "actingOnSelection" | "onBulkArchive" >) { return ( <> ); } function withSelectionClear( actingOnSelection: boolean, onClearSelection: (() => void) | undefined, handler: ((id: string) => void) | undefined, ) { if (!actingOnSelection || !onClearSelection || !handler) return handler; return (id: string) => { onClearSelection(); handler(id); }; } type BulkSelectionMenuProps = { task: TaskSwitcherItem; actingIds: string[]; workflows?: TaskMoveWorkflow[]; stepsByWorkflowId?: Record; steps?: StepDef[]; isMixedWorkflowSelection?: boolean; pinnedTaskIds?: string[]; onBulkPin?: (taskIds: string[]) => void; onBulkArchive?: (taskIds: string[]) => void; onBulkDelete?: (taskIds: string[]) => void; onBulkMove?: (taskIds: string[], targetWorkflowId: string, targetStepId: string) => void; closeMenu: () => void; moveTasks: ReturnType; }; function BulkSelectionMenuItems(props: BulkSelectionMenuProps) { const { task, workflows, stepsByWorkflowId, steps, isMixedWorkflowSelection } = props; const hasMoveGroup = hasBulkMoveGroup({ task, workflows, stepsByWorkflowId, steps, isMixedWorkflowSelection, }); return ( 0, content: ( <> ), }, { key: "move", visible: hasMoveGroup, content: , }, { key: "remove", visible: Boolean(props.onBulkArchive || props.onBulkDelete), content: , }, ]} /> ); } function hasBulkMoveGroup({ task, workflows, stepsByWorkflowId, steps, isMixedWorkflowSelection, }: Pick< BulkSelectionMenuProps, "task" | "workflows" | "stepsByWorkflowId" | "steps" | "isMixedWorkflowSelection" >) { const workflowId = task.workflowId; const moveStepsByWorkflowId = stepsByWorkflowId ?? (workflowId && steps ? { [workflowId]: steps } : undefined); const currentMoveSteps = workflowId ? (moveStepsByWorkflowId?.[workflowId] ?? []) : []; return Boolean( workflowId && ((!isMixedWorkflowSelection && currentMoveSteps.length > 1) || (workflows ?? []).some((workflow) => !workflow.hidden && workflow.id !== workflowId)), ); } function BulkPinMenuItem({ actingIds, pinnedTaskIds, onBulkPin, }: Pick) { const { t } = useTranslation(); if (!onBulkPin) return null; const allPinned = actingIds.length > 0 && actingIds.every((id) => pinnedTaskIds?.includes(id) ?? false); const label = allPinned ? t("task:unpinTasksCount", { count: actingIds.length }) : t("task:pinTasksCount", { count: actingIds.length }); return ( onBulkPin(actingIds)}> {allPinned ? ( ) : ( )} {label} ); } function BulkMoveGroup(props: BulkSelectionMenuProps) { return ( ); } function BulkRemoveGroup({ task, actingIds, onBulkArchive, onBulkDelete, }: Pick) { const { t } = useTranslation(); return ( <> {onBulkDelete && ( onBulkDelete(actingIds)}> {t("task:deleteTasksCount", { count: actingIds.length })} )} ); } function TaskPinItem({ taskId, isPinned, disabled, onTogglePin, }: { taskId: string; isPinned?: boolean; disabled?: boolean; onTogglePin?: (taskId: string) => void; }) { const { t } = useTranslation(); if (!onTogglePin) return null; return ( onTogglePin(taskId)}> {isPinned ? : } {isPinned ? t("task:unpin") : t("task:annotationPin")} ); } function TaskRenameItem({ task, disabled, onRenameTask, }: { task: TaskSwitcherItem; disabled?: boolean; onRenameTask?: (taskId: string, currentTitle: string) => void; }) { const { t } = useTranslation(); if (!onRenameTask) return null; return ( onRenameTask(task.id, task.title)}> {t("task:rename")} ); } function TaskEditItem({ task, disabled, onEditTask, }: { task: TaskSwitcherItem; disabled?: boolean; onEditTask?: (task: TaskSwitcherItem) => void; }) { const { t } = useTranslation(); if (!onEditTask || !taskRowActionAvailability(task).edit) return null; return ( onEditTask(task)}> {t("common:edit")} ); } export { BulkSelectionMenuItems, SingleSelectionMenuItems };