diff options
Diffstat (limited to 'makima/frontend/src/components/mesh/PatchesListPanel.tsx')
| -rw-r--r-- | makima/frontend/src/components/mesh/PatchesListPanel.tsx | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/makima/frontend/src/components/mesh/PatchesListPanel.tsx b/makima/frontend/src/components/mesh/PatchesListPanel.tsx new file mode 100644 index 0000000..b63d7e7 --- /dev/null +++ b/makima/frontend/src/components/mesh/PatchesListPanel.tsx @@ -0,0 +1,236 @@ +import { useState, useEffect, useCallback } from "react"; +import type { PatchSummary } from "../../lib/api"; +import { listTaskPatches } from "../../lib/api"; + +interface PatchesListPanelProps { + taskId: string; + contractId: string; +} + +/** Format a date for display */ +function formatDate(dateStr: string): string { + const date = new Date(dateStr); + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +/** Copy text to clipboard and show feedback */ +async function copyToClipboard(text: string): Promise<boolean> { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + // Fallback for older browsers + const textArea = document.createElement("textarea"); + textArea.value = text; + textArea.style.position = "fixed"; + textArea.style.left = "-999999px"; + textArea.style.top = "-999999px"; + document.body.appendChild(textArea); + textArea.select(); + try { + document.execCommand("copy"); + return true; + } catch { + return false; + } finally { + document.body.removeChild(textArea); + } + } +} + +export function PatchesListPanel({ taskId, contractId }: PatchesListPanelProps) { + const [patches, setPatches] = useState<PatchSummary[]>([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + const [copiedPatchId, setCopiedPatchId] = useState<string | null>(null); + const [expandedPatchId, setExpandedPatchId] = useState<string | null>(null); + + const fetchPatches = useCallback(async () => { + try { + setLoading(true); + setError(null); + const patchList = await listTaskPatches(taskId, contractId); + setPatches(patchList); + } catch (e) { + console.error("Failed to fetch patches:", e); + setError(e instanceof Error ? e.message : "Failed to fetch patches"); + } finally { + setLoading(false); + } + }, [taskId, contractId]); + + useEffect(() => { + fetchPatches(); + }, [fetchPatches]); + + const handleCopyApplyCommand = useCallback(async (patch: PatchSummary) => { + // Generate the apply command for this patch + const command = `makima patch apply ${patch.id}`; + const success = await copyToClipboard(command); + if (success) { + setCopiedPatchId(patch.id); + setTimeout(() => setCopiedPatchId(null), 2000); + } + }, []); + + const handleViewPatch = useCallback((patchId: string) => { + setExpandedPatchId(expandedPatchId === patchId ? null : patchId); + }, [expandedPatchId]); + + if (loading) { + return ( + <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3"> + <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase mb-2"> + Exported Patches + </div> + <div className="font-mono text-xs text-[#555] animate-pulse"> + Loading patches... + </div> + </div> + ); + } + + if (error) { + return ( + <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3"> + <div className="flex items-center justify-between mb-2"> + <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase"> + Exported Patches + </div> + <button + onClick={fetchPatches} + className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] transition-colors" + > + Retry + </button> + </div> + <div className="font-mono text-xs text-red-400"> + {error} + </div> + </div> + ); + } + + if (patches.length === 0) { + return ( + <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3"> + <div className="flex items-center justify-between mb-2"> + <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase"> + Exported Patches + </div> + <button + onClick={fetchPatches} + className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] transition-colors" + > + Refresh + </button> + </div> + <div className="font-mono text-xs text-[#555]"> + No patches exported yet + </div> + </div> + ); + } + + return ( + <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)]"> + {/* Header */} + <div className="flex items-center justify-between p-3 border-b border-[rgba(117,170,252,0.1)]"> + <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase"> + Exported Patches + </div> + <div className="flex items-center gap-2"> + <span className="font-mono text-[10px] text-[#75aafc]"> + {patches.length} patch{patches.length !== 1 ? "es" : ""} + </span> + <button + onClick={fetchPatches} + className="font-mono text-[10px] text-[#555] hover:text-[#75aafc] transition-colors" + title="Refresh" + > + <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> + </svg> + </button> + </div> + </div> + + {/* Patch list */} + <div className="divide-y divide-[rgba(117,170,252,0.1)]"> + {patches.map((patch) => ( + <div + key={patch.id} + className="p-3" + > + {/* Patch header */} + <div className="flex items-start justify-between gap-2 mb-2"> + <div className="flex-1 min-w-0"> + <div className="font-mono text-sm text-[#dbe7ff] truncate" title={patch.name}> + {patch.name} + </div> + <div className="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1 font-mono text-[10px] text-[#555]"> + <span title={new Date(patch.createdAt).toLocaleString()}> + {formatDate(patch.createdAt)} + </span> + <span> + {patch.filesCount} file{patch.filesCount !== 1 ? "s" : ""} + </span> + <span className="text-green-400">+{patch.linesAdded}</span> + <span className="text-red-400">-{patch.linesRemoved}</span> + </div> + </div> + </div> + + {/* Action buttons */} + <div className="flex flex-wrap gap-2"> + <button + onClick={() => handleViewPatch(patch.id)} + className="px-2 py-1 font-mono text-[10px] text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors" + > + {expandedPatchId === patch.id ? "Hide" : "View"} + </button> + <button + onClick={() => handleCopyApplyCommand(patch)} + className={`px-2 py-1 font-mono text-[10px] border transition-colors ${ + copiedPatchId === patch.id + ? "text-green-400 border-green-400/30 bg-green-400/10" + : "text-[#9bc3ff] border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)]" + }`} + > + {copiedPatchId === patch.id ? "Copied!" : "Copy Apply Command"} + </button> + </div> + + {/* Expanded patch content */} + {expandedPatchId === patch.id && patch.description && ( + <div className="mt-3 p-2 bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.1)]"> + <pre className="font-mono text-[10px] text-[#75aafc] whitespace-pre-wrap overflow-x-auto"> + {patch.description} + </pre> + </div> + )} + + {/* Expanded - show files if available */} + {expandedPatchId === patch.id && patch.files && patch.files.length > 0 && ( + <div className="mt-2"> + <div className="font-mono text-[10px] text-[#555] mb-1">Changed files:</div> + <div className="space-y-0.5"> + {patch.files.map((file, idx) => ( + <div key={idx} className="font-mono text-[10px] text-[#75aafc] pl-2"> + {file} + </div> + ))} + </div> + </div> + )} + </div> + ))} + </div> + </div> + ); +} |
