diff options
Diffstat (limited to 'makima/frontend/src/routes/document-directives.tsx')
| -rw-r--r-- | makima/frontend/src/routes/document-directives.tsx | 466 |
1 files changed, 298 insertions, 168 deletions
diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx index 479dcd8..801e397 100644 --- a/makima/frontend/src/routes/document-directives.tsx +++ b/makima/frontend/src/routes/document-directives.tsx @@ -20,8 +20,8 @@ import { listDirectiveContractTasks as listContractTasks, startDirectiveContract, pauseDirectiveContract, - completeDirectiveContract, unlockDirectiveContract, + reopenDirectiveContract, reorderDirectiveContract, createDirectiveTask, startDirective, @@ -32,9 +32,11 @@ import { advanceDirective, cleanupDirective, pickUpOrders, + stopTask, + skipDirectiveStep, } from "../lib/api"; -import { DirectiveContextMenu } from "../components/directives/DirectiveContextMenu"; -import { DocumentTaskStream } from "../components/directives/DocumentTaskStream"; +import { SidebarContextMenu, type ContextMenuItem } from "../components/SidebarContextMenu"; +import { TaskPage } from "../components/directives/TaskPage"; // Status dot color, matching the existing tabular UI's badge palette so the // document mode feels like a sibling of the existing list, not a foreign UI. @@ -182,11 +184,12 @@ interface DirectiveFolderProps { onHeaderClick: () => void; selection: SidebarSelection | null; onSelectDocument: (directiveId: string, doc: Contract) => void; - onCreateDocument: (directive: DirectiveSummary) => Promise<void>; - /** Open the inline "+ New ephemeral task" form for this directive. */ - onCreateEphemeralTask: (directive: DirectiveSummary) => void; - /** Right-click handler — opens DirectiveContextMenu with start/pause/PR/etc. */ - onContextMenu: (e: React.MouseEvent, directive: DirectiveSummary) => void; + /** Right-click handler — opens the generic context menu with items + * built for the given entity type. */ + onContextMenuDirective: (e: React.MouseEvent, directive: DirectiveSummary) => void; + onContextMenuContract: (e: React.MouseEvent, contract: Contract) => void; + onContextMenuStep: (e: React.MouseEvent, step: DirectiveStep, directiveId: string) => void; + onContextMenuTask: (e: React.MouseEvent, task: Task, directiveId: string) => void; /** Click handler for task/step rows — navigates to the live transcript. */ onSelectTask: (directiveId: string, taskId: string) => void; /** @@ -204,9 +207,10 @@ function DirectiveFolder({ onHeaderClick, selection, onSelectDocument, - onCreateDocument, - onCreateEphemeralTask, - onContextMenu, + onContextMenuDirective, + onContextMenuContract, + onContextMenuStep, + onContextMenuTask, onSelectTask, refreshNonce, }: DirectiveFolderProps) { @@ -226,9 +230,6 @@ function DirectiveFolder({ // shipped/ subfolder open state — independent of the directive folder. const [shippedOpen, setShippedOpen] = useState(false); - // Whether a "+ New document" call is in flight (disables the button). - const [creating, setCreating] = useState(false); - const refresh = useCallback(async () => { setDocsLoading(true); setDocsError(null); @@ -271,18 +272,6 @@ function DirectiveFolder({ // so it can rename itself to `tasks - <contract name>/` for clarity. const multipleContracts = activeDocs.length + shippedDocs.length > 1; - const handleCreate = useCallback(async () => { - if (creating) return; - setCreating(true); - try { - await onCreateDocument(directive); - // Refresh after creating so the new doc appears in the list. - await refresh(); - } finally { - setCreating(false); - } - }, [creating, onCreateDocument, directive, refresh]); - // Selection helpers — used to highlight the currently-selected doc row. const selectedDocumentId = selection && selection.directiveId === directive.id @@ -318,16 +307,29 @@ function DirectiveFolder({ {/* Directive folder header. Status is shown as a colored dot on the RIGHT (per the user's spec — flat list, no per-status grouping). Right-click opens the context menu (start / pause / archive / - delete / create-PR / update-PR / etc.). */} - <button - type="button" + delete / create-PR / update-PR / etc.). + + Row is a div with onClick (not a <button>) so we can nest a + real `+` button inside that surfaces on hover for quick + contract / ephemeral-task creation without leaving the file + tree. */} + <div + role="button" + tabIndex={0} onClick={() => { onToggle(); onHeaderClick(); }} - onContextMenu={(e) => onContextMenu(e, directive)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onToggle(); + onHeaderClick(); + } + }} + onContextMenu={(e) => onContextMenuDirective(e, directive)} title={`${directive.title} — ${directive.status}`} - className="w-full flex items-center gap-1.5 pl-3 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)]" + className="group w-full flex items-center gap-1.5 pl-3 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] cursor-pointer" > <Caret open={open} /> <FolderIcon open={open} /> @@ -337,6 +339,23 @@ function DirectiveFolder({ : directive.id.slice(0, 8)} / </span> + {/* Hover-only quick-add affordance — replaces the old inline + "+ New document" / "+ New ephemeral task" buttons that used + to sit inside the folder body. Builds the same items array + the directive context menu would, so right-click and `+` + stay in sync. */} + <button + type="button" + onClick={(e) => { + e.stopPropagation(); + onContextMenuDirective(e, directive); + }} + className="opacity-0 group-hover:opacity-100 text-[12px] leading-none text-emerald-300 hover:text-white px-1" + title="New contract / ephemeral task" + aria-label="New contract or ephemeral task" + > + + + </button> {orchestratorRunning && ( <span className="inline-block w-1.5 h-1.5 rounded-full shrink-0 bg-yellow-400 animate-pulse" @@ -350,7 +369,7 @@ function DirectiveFolder({ aria-label={`status: ${directive.status}`} title={`status: ${directive.status}`} /> - </button> + </div> {/* Folder body — rendered only when open */} {open && ( @@ -369,36 +388,16 @@ function DirectiveFolder({ {/* Active group */} {docs && ( <> - {/* + New document affordance — sits at the top of the active list - so the user can always reach it without scrolling past - existing docs. */} - <button - type="button" - onClick={handleCreate} - disabled={creating} - className="w-full flex items-center gap-1.5 pl-14 pr-3 py-1 font-mono text-[11px] text-emerald-400 hover:bg-[rgba(74,222,128,0.06)] disabled:opacity-50" - title="Create a new document under this directive" - > - <span className="text-[12px] leading-none">+</span> - <span>New document</span> - </button> - - {/* + New ephemeral task — sibling affordance for spawning a - one-off task under this directive that's NOT part of the - DAG. Useful for sidebar scratch work, debugging, etc. */} - <button - type="button" - onClick={() => onCreateEphemeralTask(directive)} - className="w-full flex items-center gap-1.5 pl-14 pr-3 py-1 font-mono text-[11px] text-[#c084fc] hover:bg-[rgba(192,132,252,0.06)]" - title="Spawn a one-off ephemeral task under this directive" - > - <span className="text-[12px] leading-none">+</span> - <span>New ephemeral task</span> - </button> + {/* "New contract" / "New ephemeral task" used to be rendered + here as inline buttons. They're now reachable via the + directive folder's right-click context menu and the `+` + hover-button on the directive header (see DirectiveFolder + row above). Keeping the file tree free of action rows + makes the hierarchy easier to scan. */} {activeDocs.length === 0 && !docsLoading && ( <div className="pl-14 pr-3 py-1 font-mono text-[10px] text-[#556677] italic"> - no active documents + no contracts yet — right-click to add </div> )} @@ -413,6 +412,7 @@ function DirectiveFolder({ directive={directive} selected={doc.id === selectedDocumentId} onSelect={() => onSelectDocument(directive.id, doc)} + onContextMenu={onContextMenuContract} draggable onDragStart={() => setDragId(doc.id)} onDragEnd={() => { @@ -438,6 +438,8 @@ function DirectiveFolder({ refreshNonce={refreshNonce} selectedTaskId={selectedTaskIdForFolder} onSelectTask={onSelectTask} + onContextMenuStep={onContextMenuStep} + onContextMenuTask={onContextMenuTask} contractLabel={fileLabel(doc, directive)} multipleContracts={multipleContracts} /> @@ -474,6 +476,7 @@ function DirectiveFolder({ directive={directive} selected={doc.id === selectedDocumentId} onSelect={() => onSelectDocument(directive.id, doc)} + onContextMenu={onContextMenuContract} indent="deep" /> <DocumentTasksFolder @@ -484,6 +487,8 @@ function DirectiveFolder({ refreshNonce={refreshNonce} selectedTaskId={selectedTaskIdForFolder} onSelectTask={onSelectTask} + onContextMenuStep={onContextMenuStep} + onContextMenuTask={onContextMenuTask} contractLabel={fileLabel(doc, directive)} multipleContracts={multipleContracts} /> @@ -509,6 +514,7 @@ interface DocumentRowProps { directive: DirectiveSummary; selected: boolean; onSelect: () => void; + onContextMenu: (e: React.MouseEvent, contract: Contract) => void; indent?: "normal" | "deep"; // ----- Drag-to-reorder props (optional — only wired by the active list) --- /** Whether this row participates in HTML5 drag (active docs only). */ @@ -527,6 +533,7 @@ function DocumentRow({ directive, selected, onSelect, + onContextMenu, indent = "normal", draggable = false, onDragStart, @@ -549,6 +556,7 @@ function DocumentRow({ <button type="button" onClick={onSelect} + onContextMenu={(e) => onContextMenu(e, doc)} title={name} draggable={draggable} onDragStart={(e) => { @@ -636,6 +644,8 @@ interface DocumentTasksFolderProps { selectedTaskId: string | null; /** Click handler for step/task rows — navigates to the live transcript. */ onSelectTask: (directiveId: string, taskId: string) => void; + onContextMenuStep: (e: React.MouseEvent, step: DirectiveStep, directiveId: string) => void; + onContextMenuTask: (e: React.MouseEvent, task: Task, directiveId: string) => void; /** Human-readable contract label (already resolved via fileLabel). Used to * disambiguate multiple tasks/ folders under the same directive. */ contractLabel: string; @@ -653,6 +663,8 @@ function DocumentTasksFolder({ refreshNonce, selectedTaskId, onSelectTask, + onContextMenuStep, + onContextMenuTask, contractLabel, multipleContracts, }: DocumentTasksFolderProps) { @@ -688,7 +700,20 @@ function DocumentTasksFolder({ void refresh(); }, [open, refresh, refreshNonce]); - const total = (data?.steps.length ?? 0) + (data?.tasks.length ?? 0); + // De-duplicate: a step that has spawned a task appears in `steps[]` + // with step.taskId === task.id, and the same task ALSO appears in + // `tasks[]`. Rendering both produces a double-row + double-highlight + // when the user clicks. Keep the step row (it carries the step name, + // which is the meaningful label) and drop the matching task row. + const stepTaskIds = useMemo( + () => new Set((data?.steps ?? []).map((s) => s.taskId).filter((id): id is string => !!id)), + [data], + ); + const ephemeralTasks = useMemo( + () => (data?.tasks ?? []).filter((t) => !stepTaskIds.has(t.id)), + [data, stepTaskIds], + ); + const total = (data?.steps.length ?? 0) + ephemeralTasks.length; // Folder always renders (even when empty) so the user can click into a // fresh contract's tasks/ folder and see it stay visible. The empty state @@ -742,9 +767,10 @@ function DocumentTasksFolder({ selected={!!selectedTaskId && step.taskId === selectedTaskId} padLeft={rowPadLeft} onSelect={onSelectTask} + onContextMenu={onContextMenuStep} /> ))} - {data?.tasks.map((task) => ( + {ephemeralTasks.map((task) => ( <TaskRow key={`task-${task.id}`} task={task} @@ -752,6 +778,7 @@ function DocumentTasksFolder({ selected={task.id === selectedTaskId} padLeft={rowPadLeft} onSelect={onSelectTask} + onContextMenu={onContextMenuTask} /> ))} </div> @@ -790,6 +817,7 @@ interface StepRowProps { selected: boolean; padLeft: string; onSelect: (directiveId: string, taskId: string) => void; + onContextMenu: (e: React.MouseEvent, step: DirectiveStep, directiveId: string) => void; } function StepRow({ @@ -798,6 +826,7 @@ function StepRow({ selected, padLeft, onSelect, + onContextMenu, }: StepRowProps) { const dot = STEP_STATUS_DOT[step.status] ?? "bg-[#556677]"; // Steps without an underlying task can't be opened — the executor @@ -811,6 +840,7 @@ function StepRow({ type="button" disabled={!clickable} onClick={() => clickable && onSelect(directiveId, taskId!)} + onContextMenu={(e) => onContextMenu(e, step, directiveId)} title={ clickable ? `${step.name} (${step.status})` @@ -844,6 +874,7 @@ interface TaskRowProps { selected: boolean; padLeft: string; onSelect: (directiveId: string, taskId: string) => void; + onContextMenu: (e: React.MouseEvent, task: Task, directiveId: string) => void; } function TaskRow({ @@ -852,6 +883,7 @@ function TaskRow({ selected, padLeft, onSelect, + onContextMenu, }: TaskRowProps) { const dot = TASK_STATUS_DOT[task.status] ?? "bg-[#556677]"; // Supervisor tasks get a small "sup" tag so the user can spot @@ -861,6 +893,7 @@ function TaskRow({ <button type="button" onClick={() => onSelect(directiveId, task.id)} + onContextMenu={(e) => onContextMenu(e, task, directiveId)} title={`${task.name} (${task.status})`} className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] transition-colors ${ selected @@ -892,10 +925,14 @@ interface SidebarProps { selection: SidebarSelection | null; onSelectDocument: (directiveId: string, doc: Contract) => void; onSelectDirective: (directiveId: string) => void; - onCreateDocument: (directive: DirectiveSummary) => Promise<void>; + /** Top-level "Contracts" header `+ New` button — opens the + * NewContractModal to create a brand-new directive (along with its + * first contract). */ onCreateContract: () => void; - onCreateEphemeralTask: (directive: DirectiveSummary) => void; - onContextMenu: (e: React.MouseEvent, directive: DirectiveSummary) => void; + onContextMenuDirective: (e: React.MouseEvent, directive: DirectiveSummary) => void; + onContextMenuContract: (e: React.MouseEvent, contract: Contract) => void; + onContextMenuStep: (e: React.MouseEvent, step: DirectiveStep, directiveId: string) => void; + onContextMenuTask: (e: React.MouseEvent, task: Task, directiveId: string) => void; onSelectTask: (directiveId: string, taskId: string) => void; refreshNonce: number; } @@ -906,10 +943,11 @@ function DocumentSidebar({ selection, onSelectDocument, onSelectDirective, - onCreateDocument, onCreateContract, - onCreateEphemeralTask, - onContextMenu, + onContextMenuDirective, + onContextMenuContract, + onContextMenuStep, + onContextMenuTask, onSelectTask, refreshNonce, }: SidebarProps) { @@ -997,9 +1035,10 @@ function DocumentSidebar({ onHeaderClick={() => onSelectDirective(d.id)} selection={selection} onSelectDocument={onSelectDocument} - onCreateDocument={onCreateDocument} - onCreateEphemeralTask={onCreateEphemeralTask} - onContextMenu={onContextMenu} + onContextMenuDirective={onContextMenuDirective} + onContextMenuContract={onContextMenuContract} + onContextMenuStep={onContextMenuStep} + onContextMenuTask={onContextMenuTask} onSelectTask={onSelectTask} refreshNonce={refreshNonce} /> @@ -1043,7 +1082,7 @@ function ContractHeader({ docTitle, onContractChanged, }: ContractHeaderProps) { - const [busy, setBusy] = useState<null | "start" | "pause" | "complete" | "unlock" | "merge_mode">( + const [busy, setBusy] = useState<null | "start" | "pause" | "unlock" | "reopen" | "merge_mode">( null, ); const [error, setError] = useState<string | null>(null); @@ -1072,14 +1111,14 @@ function ContractHeader({ () => wrap("pause", () => pauseDirectiveContract(doc.id)), [doc.id, wrap], ); - const onComplete = useCallback( - () => wrap("complete", () => completeDirectiveContract(doc.id)), - [doc.id, wrap], - ); const onUnlock = useCallback( () => wrap("unlock", () => unlockDirectiveContract(doc.id)), [doc.id, wrap], ); + const onReopen = useCallback( + () => wrap("reopen", () => reopenDirectiveContract(doc.id)), + [doc.id, wrap], + ); const onMergeMode = useCallback( (mode: ContractMergeMode) => wrap("merge_mode", () => updateContract(doc.id, { mergeMode: mode })), @@ -1127,14 +1166,20 @@ function ContractHeader({ <ContractActionButton onClick={onPause} disabled={busy !== null}> {busy === "pause" ? "Pausing…" : "Pause"} </ContractActionButton> - <ContractActionButton onClick={onComplete} disabled={busy !== null} variant="primary"> - {busy === "complete" ? "Completing…" : "Mark complete"} - </ContractActionButton> <ContractActionButton onClick={onUnlock} disabled={busy !== null}> {busy === "unlock" ? "Unlocking…" : "Unlock"} </ContractActionButton> + {/* No "Mark complete" — the contract auto-ships when the + orchestrator raises a PR (server-side; see + update_directive's PR-detection branch in handlers/ + directives.rs). */} </> )} + {doc.status === "shipped" && ( + <ContractActionButton onClick={onReopen} disabled={busy !== null} variant="primary"> + {busy === "reopen" ? "Reopening…" : "Reopen for amendment"} + </ContractActionButton> + )} {/* Merge mode radios — visible always, editable only in draft/queued */} <div className="ml-auto flex items-center gap-2 text-[#7788aa]"> @@ -1352,7 +1397,7 @@ function EditorShell({ } // --- Task path: task row clicked in the sidebar ------------------------ - // Renders the live transcript via DocumentTaskStream. Selection wins over + // Renders the live transcript via TaskPage. Selection wins over // the document path when both are somehow present (defensive). if (selection?.taskId) { const taskId = selection.taskId; @@ -1386,7 +1431,7 @@ function EditorShell({ <span className="text-white">{label}</span> </div> </div> - <DocumentTaskStream + <TaskPage taskId={taskId} label={label} ephemeral={!isStepBound} @@ -1617,7 +1662,7 @@ export default function DocumentDirectivesPage() { ); // Click on a task or step row → open the live transcript pane via - // ?task=<id>. EditorShell switches to DocumentTaskStream when this is set. + // ?task=<id>. EditorShell switches to TaskPage when this is set. const handleSelectTask = useCallback( (directiveId: string, taskId: string) => { navigate(`/directives/${directiveId}?task=${taskId}`); @@ -1657,25 +1702,18 @@ export default function DocumentDirectivesPage() { [newEphemeralFor, bumpRefresh, navigate], ); - // Right-click context menu state. Right-clicking any directive header - // opens the menu; menu actions (start/pause/archive/delete/PR/etc.) hit - // the directives API and trigger a sidebar refresh on success. + // Right-click context menu — generic. The state holds whichever items + // array the entity's builder produced; the SidebarContextMenu just + // renders them. Each entity type has its own builder (directive, + // contract, step, task) hung off the page so all the action callbacks + // (start/pause/archive/delete/etc.) live in one place. const { refresh: refreshDirectiveList } = useDirectives(); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; - directive: DirectiveSummary; + items: ContextMenuItem[]; } | null>(null); - const handleContextMenu = useCallback( - (e: React.MouseEvent, directive: DirectiveSummary) => { - e.preventDefault(); - e.stopPropagation(); - setContextMenu({ x: e.clientX, y: e.clientY, directive }); - }, - [], - ); - const closeContextMenu = useCallback(() => setContextMenu(null), []); const runAction = useCallback( @@ -1695,6 +1733,166 @@ export default function DocumentDirectivesPage() { [refreshDirectiveList, bumpRefresh], ); + const openMenu = useCallback( + (e: React.MouseEvent, items: ContextMenuItem[]) => { + e.preventDefault(); + e.stopPropagation(); + setContextMenu({ x: e.clientX, y: e.clientY, items }); + }, + [], + ); + + // ---- Per-entity menu builders. Each returns its items array; the + // handler wraps them with openMenu so each row can fire its own + // right-click with one line of wiring. ---- + const handleContextMenuDirective = useCallback( + (e: React.MouseEvent, d: DirectiveSummary) => { + const items: ContextMenuItem[] = [ + { + label: "+ New contract here", + onClick: () => { + void handleCreateDocument(d); + }, + }, + { label: "+ New ephemeral task", onClick: () => setNewEphemeralFor(d) }, + { label: "", separator: true }, + { + label: "Start", + onClick: () => runAction(() => startDirective(d.id), "Failed to start directive"), + disabled: d.status === "active" || d.status === "archived", + }, + { + label: "Pause", + onClick: () => runAction(() => pauseDirective(d.id), "Failed to pause directive"), + disabled: d.status !== "active", + }, + { label: "", separator: true }, + { + label: d.prUrl ? "Update PR" : "Create PR", + onClick: () => + runAction( + () => createDirectivePR(d.id), + d.prUrl ? "Failed to update PR" : "Failed to create PR", + ), + }, + ...(d.prUrl + ? [{ + label: "Go to PR", + onClick: () => window.open(d.prUrl!, "_blank", "noreferrer"), + } as ContextMenuItem] + : []), + { + label: "Advance DAG", + onClick: () => runAction(() => advanceDirective(d.id), "Failed to advance DAG"), + }, + { + label: "Cleanup merged steps", + onClick: () => runAction(() => cleanupDirective(d.id), "Failed to clean up"), + }, + { + label: "Pick up orders", + onClick: () => runAction(() => pickUpOrders(d.id), "Failed to pick up orders"), + }, + { label: "", separator: true }, + { + label: "Archive", + danger: true, + onClick: () => + runAction( + () => updateDirective(d.id, { status: "archived" }), + "Failed to archive", + ), + disabled: d.status === "archived", + }, + { + label: "Delete", + danger: true, + onClick: async () => { + if (!window.confirm(`Delete "${d.title}"? This cannot be undone.`)) return; + await runAction(() => deleteDirective(d.id), "Failed to delete"); + if (selection?.directiveId === d.id) navigate("/directives"); + }, + }, + ]; + openMenu(e, items); + }, + [openMenu, runAction, navigate, selection], + ); + + const handleContextMenuContract = useCallback( + (e: React.MouseEvent, c: Contract) => { + const items: ContextMenuItem[] = [ + { + label: "Lock & Start", + onClick: () => + runAction(() => startDirectiveContract(c.id), "Failed to start contract"), + disabled: c.status !== "draft", + }, + { + label: "Pause", + onClick: () => + runAction(() => pauseDirectiveContract(c.id), "Failed to pause contract"), + disabled: c.status !== "active", + }, + { + label: "Unlock", + onClick: () => + runAction(() => unlockDirectiveContract(c.id), "Failed to unlock contract"), + disabled: c.status !== "active" && c.status !== "queued", + }, + { + label: "Reopen for amendment", + onClick: () => + runAction(() => reopenDirectiveContract(c.id), "Failed to reopen contract"), + disabled: c.status !== "shipped", + }, + ]; + openMenu(e, items); + }, + [openMenu, runAction], + ); + + const handleContextMenuStep = useCallback( + (e: React.MouseEvent, step: DirectiveStep, directiveId: string) => { + const items: ContextMenuItem[] = [ + { + label: "Stop task", + onClick: () => + step.taskId + ? runAction(() => stopTask(step.taskId!), "Failed to stop task") + : undefined, + disabled: !step.taskId || step.status !== "running", + }, + { + label: "Skip step", + danger: true, + onClick: () => + runAction( + () => skipDirectiveStep(directiveId, step.id), + "Failed to skip step", + ), + disabled: step.status === "completed" || step.status === "skipped", + }, + ]; + openMenu(e, items); + }, + [openMenu, runAction], + ); + + const handleContextMenuTask = useCallback( + (e: React.MouseEvent, task: Task, _directiveId: string) => { + const items: ContextMenuItem[] = [ + { + label: "Stop task", + onClick: () => runAction(() => stopTask(task.id), "Failed to stop task"), + disabled: task.status === "done" || task.status === "failed" || task.status === "merged", + }, + ]; + openMenu(e, items); + }, + [openMenu, runAction], + ); + if (authLoading) { return ( <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> @@ -1725,10 +1923,11 @@ export default function DocumentDirectivesPage() { selection={selection} onSelectDocument={handleSelectDocument} onSelectDirective={handleSelectDirective} - onCreateDocument={handleCreateDocument} onCreateContract={() => setShowNewContract(true)} - onCreateEphemeralTask={(d) => setNewEphemeralFor(d)} - onContextMenu={handleContextMenu} + onContextMenuDirective={handleContextMenuDirective} + onContextMenuContract={handleContextMenuContract} + onContextMenuStep={handleContextMenuStep} + onContextMenuTask={handleContextMenuTask} onSelectTask={handleSelectTask} refreshNonce={refreshNonce} /> @@ -1758,80 +1957,11 @@ export default function DocumentDirectivesPage() { )} {contextMenu && ( - <DirectiveContextMenu + <SidebarContextMenu x={contextMenu.x} y={contextMenu.y} - directive={contextMenu.directive} + items={contextMenu.items} onClose={closeContextMenu} - onStart={() => - runAction( - () => startDirective(contextMenu.directive.id), - "Failed to start contract", - ) - } - onPause={() => - runAction( - () => pauseDirective(contextMenu.directive.id), - "Failed to pause contract", - ) - } - onArchive={() => - runAction( - () => - updateDirective(contextMenu.directive.id, { - status: "archived", - }), - "Failed to archive contract", - ) - } - onDelete={async () => { - if ( - !window.confirm( - `Delete "${contextMenu.directive.title}"? This cannot be undone.`, - ) - ) { - return; - } - await runAction( - () => deleteDirective(contextMenu.directive.id), - "Failed to delete contract", - ); - // If the deleted contract was selected, clear the URL. - if (selection?.directiveId === contextMenu.directive.id) { - navigate("/directives"); - } - }} - onGoToPR={() => { - if (contextMenu.directive.prUrl) { - window.open(contextMenu.directive.prUrl, "_blank", "noreferrer"); - } - }} - onCreatePR={() => - runAction( - () => createDirectivePR(contextMenu.directive.id), - contextMenu.directive.prUrl - ? "Failed to update PR" - : "Failed to create PR", - ) - } - onAdvance={() => - runAction( - () => advanceDirective(contextMenu.directive.id), - "Failed to advance DAG", - ) - } - onCleanup={() => - runAction( - () => cleanupDirective(contextMenu.directive.id), - "Failed to clean up contract", - ) - } - onPickUpOrders={() => - runAction( - () => pickUpOrders(contextMenu.directive.id), - "Failed to pick up orders", - ) - } /> )} </div> |
