From 52d121269195f0e799d0ab4241e4facc3c7c0596 Mon Sep 17 00:00:00 2001 From: soryu Date: Mon, 19 Jan 2026 17:55:22 +0000 Subject: Add right-click context menu for contracts on contracts and board pages (#8) Implement a reusable ContractContextMenu component that provides: - Mark as Complete/Active/Archive status actions (conditionally shown) - Go to Supervisor Task link (when supervisor exists) - Delete action with confirmation Integrate context menu into: - ContractList.tsx on the contracts page - WorkflowBoard on the workflow/board page via PhaseColumn and WorkflowContractCard Features match ElementContextMenu patterns: - Fixed positioning with z-50 - Click outside and Escape key close handlers - Viewport overflow prevention - Dark theme colors (#0a1628, #0d1b2d, #75aafc, #9bc3ff) Co-authored-by: Claude Opus 4.5 --- .../src/components/workflow/PhaseColumn.tsx | 3 + .../src/components/workflow/WorkflowBoard.tsx | 66 ++++++++++++++++++---- .../components/workflow/WorkflowContractCard.tsx | 3 + 3 files changed, 61 insertions(+), 11 deletions(-) (limited to 'makima/frontend/src/components/workflow') diff --git a/makima/frontend/src/components/workflow/PhaseColumn.tsx b/makima/frontend/src/components/workflow/PhaseColumn.tsx index ddea85f..277b04c 100644 --- a/makima/frontend/src/components/workflow/PhaseColumn.tsx +++ b/makima/frontend/src/components/workflow/PhaseColumn.tsx @@ -7,6 +7,7 @@ interface PhaseColumnProps { contracts: ContractSummary[]; onContractClick: (contractId: string) => void; onDrop: (contractId: string, phase: ContractPhase) => void; + onContextMenu?: (e: React.MouseEvent, contract: ContractSummary) => void; } const phaseConfig: Record< @@ -50,6 +51,7 @@ export function PhaseColumn({ contracts, onContractClick, onDrop, + onContextMenu, }: PhaseColumnProps) { const [isDragOver, setIsDragOver] = useState(false); const config = phaseConfig[phase]; @@ -114,6 +116,7 @@ export function PhaseColumn({ e.dataTransfer.setData("contractId", contract.id); e.dataTransfer.effectAllowed = "move"; }} + onContextMenu={onContextMenu ? (e) => onContextMenu(e, contract) : undefined} /> )) )} diff --git a/makima/frontend/src/components/workflow/WorkflowBoard.tsx b/makima/frontend/src/components/workflow/WorkflowBoard.tsx index af4aec7..e36ca21 100644 --- a/makima/frontend/src/components/workflow/WorkflowBoard.tsx +++ b/makima/frontend/src/components/workflow/WorkflowBoard.tsx @@ -1,11 +1,17 @@ -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import type { ContractSummary, ContractPhase } from "../../lib/api"; import { PhaseColumn } from "./PhaseColumn"; +import { ContractContextMenu } from "../contracts/ContractContextMenu"; interface WorkflowBoardProps { contracts: ContractSummary[]; onContractClick: (contractId: string) => void; onPhaseChange: (contractId: string, newPhase: ContractPhase) => void; + onMarkComplete?: (contract: ContractSummary) => void; + onMarkActive?: (contract: ContractSummary) => void; + onArchive?: (contract: ContractSummary) => void; + onDelete?: (contract: ContractSummary) => void; + onGoToSupervisor?: (contract: ContractSummary) => void; } const phases: ContractPhase[] = ["research", "specify", "plan", "execute", "review"]; @@ -14,7 +20,27 @@ export function WorkflowBoard({ contracts, onContractClick, onPhaseChange, + onMarkComplete, + onMarkActive, + onArchive, + onDelete, + onGoToSupervisor, }: WorkflowBoardProps) { + const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null); + const [contextMenuContract, setContextMenuContract] = useState(null); + + const handleContextMenu = (e: React.MouseEvent, contract: ContractSummary) => { + e.preventDefault(); + e.stopPropagation(); // Prevent interference with drag-and-drop + setContextMenuPosition({ x: e.clientX, y: e.clientY }); + setContextMenuContract(contract); + }; + + const closeContextMenu = () => { + setContextMenuPosition(null); + setContextMenuContract(null); + }; + // Group contracts by phase const contractsByPhase = useMemo(() => { const grouped: Record = { @@ -39,16 +65,34 @@ export function WorkflowBoard({ }, [contracts]); return ( -
- {phases.map((phase) => ( - +
+ {phases.map((phase) => ( + + ))} +
+ + {/* Context Menu */} + {contextMenuPosition && contextMenuContract && ( + onMarkComplete?.(contextMenuContract)} + onMarkActive={() => onMarkActive?.(contextMenuContract)} + onArchive={() => onArchive?.(contextMenuContract)} + onDelete={() => onDelete?.(contextMenuContract)} + onGoToSupervisor={() => onGoToSupervisor?.(contextMenuContract)} /> - ))} -
+ )} + ); } diff --git a/makima/frontend/src/components/workflow/WorkflowContractCard.tsx b/makima/frontend/src/components/workflow/WorkflowContractCard.tsx index 61e6d17..86fcd13 100644 --- a/makima/frontend/src/components/workflow/WorkflowContractCard.tsx +++ b/makima/frontend/src/components/workflow/WorkflowContractCard.tsx @@ -5,6 +5,7 @@ interface WorkflowContractCardProps { contract: ContractSummary; onClick: () => void; onDragStart: (e: React.DragEvent) => void; + onContextMenu?: (e: React.MouseEvent) => void; } const statusConfig: Record = { @@ -17,6 +18,7 @@ export function WorkflowContractCard({ contract, onClick, onDragStart, + onContextMenu, }: WorkflowContractCardProps) { const navigate = useNavigate(); const status = statusConfig[contract.status] || statusConfig.active; @@ -33,6 +35,7 @@ export function WorkflowContractCard({ draggable onDragStart={onDragStart} onClick={onClick} + onContextMenu={onContextMenu} className="p-3 bg-[rgba(9,13,20,0.8)] border border-[rgba(117,170,252,0.2)] hover:border-[rgba(117,170,252,0.4)] cursor-pointer transition-colors select-none" > {/* Header row with name and supervisor button */} -- cgit v1.2.3