import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type RefObject, } from "react"; import { useTranslation } from "react-i18next"; import { ContextMenu, ContextMenuContent, ContextMenuTrigger } from "@kandev/ui/context-menu"; import { useAppStore, useAppStoreApi } from "@/components/state-provider"; import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; import { useTaskManagementFlow } from "@/hooks/use-task-management-flow"; import { useTaskMenuActions } from "@/hooks/use-task-menu-actions"; import { useTaskWorkflowMove } from "@/hooks/use-task-workflow-move"; import { useUpdateTaskPriority } from "@/hooks/use-update-task-priority"; import type { TaskPriority } from "@/lib/types/http"; import { TaskManagementMenu, type TaskManagementMenuProps } from "./task-management-menu"; import { TaskManagementDrawer, TaskManagementSheet } from "./task-management-drawer"; import { TaskArchiveConfirmation } from "./task-archive-confirmation"; import { cleanupSharesParentWorkspace } from "./task-cleanup-summary"; import { TaskDeleteConfirmDialog } from "./task-delete-confirm-dialog"; import { useSidebarLinkActions } from "./task-session-sidebar-link-actions"; import { useSidebarTaskLinking } from "./task-session-sidebar-task-linking"; import { SidebarLinkDialogs } from "./task-session-sidebar-dialogs"; import { selectTaskLinkActions, type TaskLinkHandlers } from "./task-switcher-link-menu"; type Flow = ReturnType; export type TaskMenuPoint = { x: number; y: number }; type SurfaceProps = { flow: Flow; point: TaskMenuPoint; anchorRef: RefObject; focusReturnRef: RefObject; onReturnFocus: () => void; }; function DesktopMenu({ point, onDismiss, onCloseAutoFocus, ...props }: TaskManagementMenuProps & { point: TaskMenuPoint; onDismiss: () => void; onCloseAutoFocus: (event: Event) => void; }) { const trigger = useRef(null); useLayoutEffect(() => { trigger.current?.dispatchEvent( new MouseEvent("contextmenu", { bubbles: true, clientX: point.x, clientY: point.y }), ); }, [point]); return ( { if (!open) onDismiss(); }} > ); } function useManagementMutations(flow: Flow) { const removal = useTaskMenuActions({ stayOnListing: true }); const updatePriority = useUpdateTaskPriority(); const move = useTaskWorkflowMove(); const pending = useRef(false); const [busy, setBusy] = useState(false); const run = async (operation: () => Promise) => { if (pending.current || removal.pendingTaskId) return; pending.current = true; setBusy(true); flow.close(); try { await operation(); } catch { /* Priority and move hooks own their error toasts. */ } finally { pending.current = false; setBusy(false); } }; return { removal, disabled: busy || removal.pendingTaskId !== null, onPriority: (priority: TaskPriority) => { const task = flow.getTarget(); if (task) void run(() => updatePriority(task.id, priority)); }, onMove: (workflowId: string, stepId: string) => { const task = flow.getTarget(); const destination = flow.workflows.find((workflow) => workflow.id === workflowId); if ( !task || !destination || (destination.hidden && destination.id !== task.workflowId) || !flow.stepsByWorkflowId[workflowId]?.some((step) => step.id === stepId) ) return; if (task.workflowId === workflowId && task.workflowStepId === stepId) return; void run(() => move([task.id], workflowId, stepId, workflowId === task.workflowId ? "step" : "workflow"), ); }, }; } function useManagementLinks(flow: Flow) { const store = useAppStoreApi(); const actions = useSidebarLinkActions(store); const handlers = useSidebarTaskLinking(flow.identity?.workspaceId ?? null, actions); const linking = Boolean( actions.linkingPullRequestTask || actions.linkingIssueTask || actions.linkingMergeRequestTask || actions.linkingExternalIssueTask, ); useEffect(() => { if (flow.stage === "link" && !linking) flow.close(); }, [flow.stage, flow.close, linking]); const guard = (handler: TaskLinkHandlers[keyof TaskLinkHandlers]) => handler ? (taskId: string, title?: string) => { if (flow.getTarget()?.id === taskId) handler(taskId, title); } : undefined; const guardedHandlers = { onLinkPullRequest: guard(handlers.onLinkPullRequest), onLinkIssue: guard(handlers.onLinkIssue), onLinkMergeRequest: guard(handlers.onLinkMergeRequest), onLinkJiraTicket: guard(handlers.onLinkJiraTicket), onLinkLinearIssue: guard(handlers.onLinkLinearIssue), onLinkSentryIssue: guard(handlers.onLinkSentryIssue), } satisfies Record; const linkActions = flow.task ? selectTaskLinkActions(flow.task, () => flow.setStage("link"), guardedHandlers) : {}; return { actions, linkActions }; } function ManagementConfirmations({ flow, mutations, anchorRef, focusReturnRef, onCloseAutoFocus, touch, }: Pick & { mutations: ReturnType; onCloseAutoFocus: (event: Event) => void; touch: boolean; }) { const { t } = useTranslation(); const confirmationAnchor = useMemo( () => ({ get current() { return anchorRef.current?.isConnected ? anchorRef.current : focusReturnRef.current; }, }), [anchorRef, focusReturnRef], ); const task = flow.task; if (!task) return null; return ( <> { if (!open) flow.close(); }} taskId={task.id} taskTitle={task.title} executorType={task.remoteExecutorType} anchorRef={confirmationAnchor} focusReturnRef={focusReturnRef} isArchiving={mutations.disabled} inline={touch} renderInline={ touch ? (content) => ( {content} ) : undefined } onConfirm={async (opts) => { const target = flow.getTarget(); if (!target) return; flow.close(); await mutations.removal.runArchive(target.id, opts); }} /> { if (!open) flow.close(); }} taskId={task.id} taskTitle={task.title} executorType={task.remoteExecutorType} sharesParentWorkspace={cleanupSharesParentWorkspace(task.workspaceMode)} isDeleting={mutations.disabled} onCloseAutoFocus={onCloseAutoFocus} confirmTestId="thread-delete-confirm" onConfirm={(opts) => { const target = flow.getTarget(); if (target) void mutations.removal.runDelete(target.id, opts); }} /> ); } /** Owns task flows outside removable columns. All mutations and provider forms remain task-owned. */ export function TaskManagementSurface({ flow, point, anchorRef, focusReturnRef, onReturnFocus, }: SurfaceProps) { const { isMobile, isFinePointer } = useResponsiveBreakpoint(); const touch = isMobile || !isFinePointer; const mutations = useManagementMutations(flow); const stageRef = useRef(flow.stage); const returnFocusRef = useRef(onReturnFocus); useLayoutEffect(() => { stageRef.current = flow.stage; returnFocusRef.current = onReturnFocus; }); const onCloseAutoFocus = useCallback((event: Event) => { event.preventDefault(); if (stageRef.current === "closed") returnFocusRef.current(); }, []); useEffect(() => { if (flow.stage !== "closed" || !flow.identity) return; const frame = requestAnimationFrame(() => returnFocusRef.current()); return () => cancelAnimationFrame(frame); }, [flow.stage, flow.identity]); return flow.task ? ( ) : null; } function TaskManagementChoices({ flow, point, anchorRef, focusReturnRef, mutations, touch, onCloseAutoFocus, }: Omit & { mutations: ReturnType; touch: boolean; onCloseAutoFocus: (event: Event) => void; }) { const links = useManagementLinks(flow); const repositoriesByWorkspace = useAppStore((state) => state.repositories.itemsByWorkspaceId); const closeMenu = useCallback( () => flow.setStage((stage) => (stage === "menu" ? "closed" : stage)), [flow.setStage], ); const task = flow.task; const props = task ? { task, workflows: flow.workflows, stepsByWorkflowId: flow.stepsByWorkflowId, disabled: mutations.disabled, onPriority: mutations.onPriority, onMove: mutations.onMove, onArchive: () => flow.setStage("archive"), onDelete: () => flow.setStage("delete"), closeMenu, linkActions: links.linkActions, } : null; return ( <> {flow.stage === "menu" && props && (touch ? ( ) : ( ))} {flow.stage === "link" && ( )} ); }