/** * ScriptDiffReview — side-by-side diff view for a pending .next.js patch. * * Design choice: .next.js files appear as sibling rows in the FileTree just like * any other file. Clicking a .next.js file opens this component (instead of * MonacoFileEditor). The diff view shows the current .js on the left (original) * and the candidate .next.js on the right (modified), read-only. * * Accept: archives scripts/{name}.js to trash, renames .next.js into place. * Reject: moves .next.js to trash; original is untouched. * Both actions invalidate the scripts listing and navigate back to scripts/{name}.js. */ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { DiffEditor } from '@monaco-editor/react'; type ShowToast = (message: string, variant?: 'success' | 'error') => void; interface ScriptDiffReviewProps { /** The bare script name without extension, e.g. "myscript" */ scriptName: string; onClose: (acceptedScript?: string) => void; showToast?: ShowToast; } interface DiffResponse { current: string | null; candidate: string; candidateMtime: string; } async function fetchDiff(name: string): Promise { const res = await fetch(`/api/users/me/browser-macros/${encodeURIComponent(name)}/diff`, { credentials: 'include', }); if (res.status === 404) throw new Error('No pending patch found.'); if (!res.ok) throw new Error(`Diff fetch failed: ${res.status}`); return res.json() as Promise; } async function postAccept(name: string): Promise { const res = await fetch(`/api/users/me/browser-macros/${encodeURIComponent(name)}/accept`, { method: 'POST', credentials: 'include', }); if (!res.ok) throw new Error(`Accept failed: ${res.status}`); } async function postReject(name: string): Promise { const res = await fetch(`/api/users/me/browser-macros/${encodeURIComponent(name)}/reject`, { method: 'POST', credentials: 'include', }); if (!res.ok) throw new Error(`Reject failed: ${res.status}`); } export function ScriptDiffReview({ scriptName, onClose, showToast }: ScriptDiffReviewProps) { const qc = useQueryClient(); const notifyError = (label: string, err: unknown) => { const msg = `${label}: ${err instanceof Error ? err.message : 'Unknown error'}`; if (showToast) showToast(msg, 'error'); else console.error(msg); }; const diffQuery = useQuery({ queryKey: ['userfolder', 'diff', scriptName], queryFn: () => fetchDiff(scriptName), staleTime: 10_000, refetchOnWindowFocus: false, }); const candidateMtimeLabel = diffQuery.data?.candidateMtime ? new Date(diffQuery.data.candidateMtime).toLocaleString() : ''; async function handleAccept() { try { await postAccept(scriptName); // Invalidate browser-macros listing so the .next.js row disappears qc.invalidateQueries({ queryKey: ['userfolder', 'list', 'browser-macros'] }); qc.removeQueries({ queryKey: ['userfolder', 'diff', scriptName] }); // Navigate back to the now-accepted script onClose(`${scriptName}.js`); } catch (err) { notifyError('Accept failed', err); } } async function handleReject() { try { await postReject(scriptName); qc.invalidateQueries({ queryKey: ['userfolder', 'list', 'browser-macros'] }); qc.removeQueries({ queryKey: ['userfolder', 'diff', scriptName] }); // Navigate back to the original (unchanged) script if it exists; otherwise close const hasOriginal = diffQuery.data?.current !== null; onClose(hasOriginal ? `${scriptName}.js` : undefined); } catch (err) { notifyError('Reject failed', err); } } return (
{/* Header */}
Patch review: {scriptName}.next.js {candidateMtimeLabel && ( {candidateMtimeLabel} )}
original → patch
{/* Body */}
{diffQuery.isLoading && (
Loading diff…
)} {diffQuery.isError && (
{diffQuery.error?.message ?? 'Failed to load diff.'}
)} {diffQuery.data && ( )}
); }