diff options
| author | soryu <soryu@soryu.co> | 2026-01-19 20:05:28 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-19 20:05:28 +0000 |
| commit | 79ece9163afdb2d1c692f3c6e27a07dcc7a6e5b0 (patch) | |
| tree | 03945695425261df9a3448928468b64a02bf4a9d | |
| parent | 307d261dacb8520f9c6ca58bcee2d38a2346acd7 (diff) | |
| parent | 9d418d4dabef392b16fd79ee84fa7c3214bfcbe3 (diff) | |
| download | soryu-79ece9163afdb2d1c692f3c6e27a07dcc7a6e5b0.tar.gz soryu-79ece9163afdb2d1c692f3c6e27a07dcc7a6e5b0.zip | |
Fix context menu for workflow board contracts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
5 files changed, 132 insertions, 134 deletions
diff --git a/makima/frontend/src/components/contracts/ContractContextMenu.tsx b/makima/frontend/src/components/contracts/ContractContextMenu.tsx index f31beb5..6e21641 100644 --- a/makima/frontend/src/components/contracts/ContractContextMenu.tsx +++ b/makima/frontend/src/components/contracts/ContractContextMenu.tsx @@ -1,18 +1,24 @@ import { useEffect, useRef } from "react"; -import type { ContractSummary } from "../../lib/api"; +import type { ContractSummary, ContractStatus } from "../../lib/api"; interface ContractContextMenuProps { x: number; y: number; contract: ContractSummary; onClose: () => void; - onMarkComplete: () => void; - onMarkActive: () => void; - onArchive: () => void; - onDelete: () => void; - onGoToSupervisor: () => void; + onMarkComplete: (contractId: string) => void; + onMarkActive: (contractId: string) => void; + onArchive: (contractId: string) => void; + onGoToSupervisor: (supervisorTaskId: string) => void; + onDelete: (contractId: string) => void; } +const statusConfig: Record<ContractStatus, { label: string; color: string }> = { + active: { label: "Active", color: "text-green-400" }, + completed: { label: "Completed", color: "text-blue-400" }, + archived: { label: "Archived", color: "text-[#555]" }, +}; + export function ContractContextMenu({ x, y, @@ -21,8 +27,8 @@ export function ContractContextMenu({ onMarkComplete, onMarkActive, onArchive, - onDelete, onGoToSupervisor, + onDelete, }: ContractContextMenuProps) { const menuRef = useRef<HTMLDivElement>(null); @@ -65,78 +71,80 @@ export function ContractContextMenu({ } }, [x, y]); + const status = statusConfig[contract.status] || statusConfig.active; + const menuItemClass = "w-full px-3 py-1.5 text-left text-xs font-mono text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] flex items-center gap-2"; const dividerClass = "border-t border-[rgba(117,170,252,0.2)] my-1"; - const showMarkComplete = contract.status !== "completed"; - const showMarkActive = contract.status !== "active"; - const showArchive = contract.status !== "archived"; - const showGoToSupervisor = !!contract.supervisorTaskId; - return ( <div ref={menuRef} - className="fixed z-50 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-lg min-w-[180px]" + className="fixed z-50 bg-[#0a0a0a] border border-[rgba(117,170,252,0.3)] shadow-lg min-w-[180px]" style={{ left: x, top: y }} > - {/* Header showing contract name */} - <div className="px-3 py-1.5 text-[10px] font-mono text-[#555] uppercase border-b border-[rgba(117,170,252,0.2)] truncate max-w-[200px]"> - {contract.name} + {/* Header showing contract info */} + <div className="px-3 py-1.5 border-b border-[rgba(117,170,252,0.2)]"> + <div className="text-[10px] font-mono text-[#555] uppercase truncate"> + {contract.name} + </div> + <div className={`text-[10px] font-mono ${status.color}`}> + {status.label} | {contract.phase} + </div> </div> {/* Status actions */} - {showMarkComplete && ( + {contract.status !== "completed" && ( <button className={menuItemClass} onClick={() => { - onMarkComplete(); + onMarkComplete(contract.id); onClose(); }} > - <span className="text-[#75aafc]">✓</span> + <span className="text-blue-400">✓</span> Mark as Complete </button> )} - {showMarkActive && ( + {contract.status !== "active" && ( <button className={menuItemClass} onClick={() => { - onMarkActive(); + onMarkActive(contract.id); onClose(); }} > - <span className="text-[#75aafc]">●</span> + <span className="text-green-400">▶</span> Mark as Active </button> )} - {showArchive && ( + {contract.status !== "archived" && ( <button className={menuItemClass} onClick={() => { - onArchive(); + onArchive(contract.id); onClose(); }} > - <span className="text-[#75aafc]">▣</span> + <span className="text-[#555]">🗃</span> Archive </button> )} - {/* Supervisor link */} - {showGoToSupervisor && ( + {/* Supervisor task link */} + {contract.supervisorTaskId && ( <> <div className={dividerClass} /> <button className={menuItemClass} onClick={() => { - onGoToSupervisor(); + onGoToSupervisor(contract.supervisorTaskId!); onClose(); }} > - <span className="text-[#75aafc]">▶</span> + <span className="text-[#75aafc]">▶</span> Go to Supervisor Task </button> </> @@ -148,11 +156,11 @@ export function ContractContextMenu({ <button className={`${menuItemClass} text-red-400 hover:bg-red-400/10`} onClick={() => { - onDelete(); + onDelete(contract.id); onClose(); }} > - <span className="text-red-400">✕</span> + <span className="text-red-400">x</span> Delete </button> </div> diff --git a/makima/frontend/src/components/workflow/PhaseColumn.tsx b/makima/frontend/src/components/workflow/PhaseColumn.tsx index 277b04c..fb15ae3 100644 --- a/makima/frontend/src/components/workflow/PhaseColumn.tsx +++ b/makima/frontend/src/components/workflow/PhaseColumn.tsx @@ -116,7 +116,7 @@ export function PhaseColumn({ e.dataTransfer.setData("contractId", contract.id); e.dataTransfer.effectAllowed = "move"; }} - onContextMenu={onContextMenu ? (e) => onContextMenu(e, contract) : undefined} + onContextMenu={onContextMenu} /> )) )} diff --git a/makima/frontend/src/components/workflow/WorkflowBoard.tsx b/makima/frontend/src/components/workflow/WorkflowBoard.tsx index e36ca21..ee2ff75 100644 --- a/makima/frontend/src/components/workflow/WorkflowBoard.tsx +++ b/makima/frontend/src/components/workflow/WorkflowBoard.tsx @@ -1,17 +1,12 @@ -import { useMemo, useState } from "react"; +import { useMemo } 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; + onContextMenu?: (e: React.MouseEvent, contract: ContractSummary) => void; } const phases: ContractPhase[] = ["research", "specify", "plan", "execute", "review"]; @@ -20,27 +15,8 @@ export function WorkflowBoard({ contracts, onContractClick, onPhaseChange, - onMarkComplete, - onMarkActive, - onArchive, - onDelete, - onGoToSupervisor, + onContextMenu, }: WorkflowBoardProps) { - const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null); - const [contextMenuContract, setContextMenuContract] = useState<ContractSummary | null>(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<ContractPhase, ContractSummary[]> = { @@ -65,34 +41,17 @@ export function WorkflowBoard({ }, [contracts]); return ( - <> - <div className="flex gap-2 h-full overflow-x-auto"> - {phases.map((phase) => ( - <PhaseColumn - key={phase} - phase={phase} - contracts={contractsByPhase[phase]} - onContractClick={onContractClick} - onDrop={onPhaseChange} - onContextMenu={handleContextMenu} - /> - ))} - </div> - - {/* Context Menu */} - {contextMenuPosition && contextMenuContract && ( - <ContractContextMenu - x={contextMenuPosition.x} - y={contextMenuPosition.y} - contract={contextMenuContract} - onClose={closeContextMenu} - onMarkComplete={() => onMarkComplete?.(contextMenuContract)} - onMarkActive={() => onMarkActive?.(contextMenuContract)} - onArchive={() => onArchive?.(contextMenuContract)} - onDelete={() => onDelete?.(contextMenuContract)} - onGoToSupervisor={() => onGoToSupervisor?.(contextMenuContract)} + <div className="flex gap-2 h-full overflow-x-auto"> + {phases.map((phase) => ( + <PhaseColumn + key={phase} + phase={phase} + contracts={contractsByPhase[phase]} + onContractClick={onContractClick} + onDrop={onPhaseChange} + onContextMenu={onContextMenu} /> - )} - </> + ))} + </div> ); } diff --git a/makima/frontend/src/components/workflow/WorkflowContractCard.tsx b/makima/frontend/src/components/workflow/WorkflowContractCard.tsx index 86fcd13..d62f4b0 100644 --- a/makima/frontend/src/components/workflow/WorkflowContractCard.tsx +++ b/makima/frontend/src/components/workflow/WorkflowContractCard.tsx @@ -5,7 +5,7 @@ interface WorkflowContractCardProps { contract: ContractSummary; onClick: () => void; onDragStart: (e: React.DragEvent) => void; - onContextMenu?: (e: React.MouseEvent) => void; + onContextMenu?: (e: React.MouseEvent, contract: ContractSummary) => void; } const statusConfig: Record<ContractStatus, { label: string; color: string }> = { @@ -30,12 +30,20 @@ export function WorkflowContractCard({ } }; + const handleContextMenu = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (onContextMenu) { + onContextMenu(e, contract); + } + }; + return ( <div draggable onDragStart={onDragStart} onClick={onClick} - onContextMenu={onContextMenu} + onContextMenu={handleContextMenu} 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 */} diff --git a/makima/frontend/src/routes/workflow.tsx b/makima/frontend/src/routes/workflow.tsx index e122092..79148e2 100644 --- a/makima/frontend/src/routes/workflow.tsx +++ b/makima/frontend/src/routes/workflow.tsx @@ -2,12 +2,19 @@ import { useState, useCallback, useEffect, useMemo } from "react"; import { useNavigate } from "react-router"; import { Masthead } from "../components/Masthead"; import { WorkflowBoard } from "../components/workflow/WorkflowBoard"; +import { ContractContextMenu } from "../components/contracts/ContractContextMenu"; import { useContracts } from "../hooks/useContracts"; import { useAuth } from "../contexts/AuthContext"; import type { ContractPhase, ContractStatus, ContractSummary } from "../lib/api"; type StatusFilter = "all" | ContractStatus; +interface ContextMenuState { + x: number; + y: number; + contract: ContractSummary; +} + export default function WorkflowPage() { const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); const navigate = useNavigate(); @@ -45,6 +52,7 @@ function WorkflowPageContent() { const [statusFilter, setStatusFilter] = useState<StatusFilter>("all"); const [isCreating, setIsCreating] = useState(false); const [newContractName, setNewContractName] = useState(""); + const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null); // Filter contracts by status const filteredContracts = useMemo(() => { @@ -68,46 +76,6 @@ function WorkflowPageContent() { [changePhase] ); - // Context menu handlers - const handleContextMarkComplete = useCallback( - async (contract: ContractSummary) => { - await editContract(contract.id, { status: "completed", version: contract.version }); - }, - [editContract] - ); - - const handleContextMarkActive = useCallback( - async (contract: ContractSummary) => { - await editContract(contract.id, { status: "active", version: contract.version }); - }, - [editContract] - ); - - const handleContextArchive = useCallback( - async (contract: ContractSummary) => { - await editContract(contract.id, { status: "archived", version: contract.version }); - }, - [editContract] - ); - - const handleContextDelete = useCallback( - async (contract: ContractSummary) => { - if (confirm(`Are you sure you want to delete "${contract.name}"?`)) { - await removeContract(contract.id); - } - }, - [removeContract] - ); - - const handleContextGoToSupervisor = useCallback( - (contract: ContractSummary) => { - if (contract.supervisorTaskId) { - navigate(`/mesh/${contract.supervisorTaskId}`); - } - }, - [navigate] - ); - const handleCreateContract = useCallback(async () => { if (!newContractName.trim()) return; const contract = await saveContract({ @@ -125,6 +93,50 @@ function WorkflowPageContent() { setIsCreating(false); }, []); + // Context menu handlers + const handleContextMenu = useCallback((e: React.MouseEvent, contract: ContractSummary) => { + setContextMenu({ + x: e.clientX, + y: e.clientY, + contract, + }); + }, []); + + const handleCloseContextMenu = useCallback(() => { + setContextMenu(null); + }, []); + + const handleMarkComplete = useCallback(async (contractId: string) => { + const contract = contracts.find(c => c.id === contractId); + if (contract) { + await editContract(contractId, { status: "completed", version: contract.version }); + } + }, [contracts, editContract]); + + const handleMarkActive = useCallback(async (contractId: string) => { + const contract = contracts.find(c => c.id === contractId); + if (contract) { + await editContract(contractId, { status: "active", version: contract.version }); + } + }, [contracts, editContract]); + + const handleArchive = useCallback(async (contractId: string) => { + const contract = contracts.find(c => c.id === contractId); + if (contract) { + await editContract(contractId, { status: "archived", version: contract.version }); + } + }, [contracts, editContract]); + + const handleGoToSupervisor = useCallback((supervisorTaskId: string) => { + navigate(`/mesh/${supervisorTaskId}`); + }, [navigate]); + + const handleDelete = useCallback(async (contractId: string) => { + if (confirm("Are you sure you want to delete this contract?")) { + await removeContract(contractId); + } + }, [removeContract]); + return ( <div className="relative z-10 h-screen flex flex-col bg-[#0a1628]"> <Masthead showNav /> @@ -236,15 +248,26 @@ function WorkflowPageContent() { contracts={filteredContracts} onContractClick={handleContractClick} onPhaseChange={handlePhaseChange} - onMarkComplete={handleContextMarkComplete} - onMarkActive={handleContextMarkActive} - onArchive={handleContextArchive} - onDelete={handleContextDelete} - onGoToSupervisor={handleContextGoToSupervisor} + onContextMenu={handleContextMenu} /> )} </div> </main> + + {/* Context Menu */} + {contextMenu && ( + <ContractContextMenu + x={contextMenu.x} + y={contextMenu.y} + contract={contextMenu.contract} + onClose={handleCloseContextMenu} + onMarkComplete={handleMarkComplete} + onMarkActive={handleMarkActive} + onArchive={handleArchive} + onGoToSupervisor={handleGoToSupervisor} + onDelete={handleDelete} + /> + )} </div> ); } |
