import { useCallback, useRef, useState } from 'react'; import type { CellValue } from '@wellformed/shared'; import { api } from '../api/client'; /** * Undo/redo as inverse operations against the server. * * There is no local shadow copy of the data to roll back to — the database is * the only source of truth — so an undo is a real write, recorded in the audit * trail like any other. That is the honest behaviour for a multi-user tool: * undoing your change is itself a change other people will see. */ export type UndoOp = | { kind: 'update'; table: string; rowId: string; before: Record; after: Record; label: string; } | { kind: 'insert'; table: string; rowId: string; values: Record; label: string; } | { kind: 'delete'; table: string; rowId: string; restore: Record; label: string; }; const LIMIT = 100; async function applyInverse(op: UndoOp): Promise { switch (op.kind) { case 'update': await api.updateRow(op.table, op.rowId, op.before); return; case 'insert': await api.deleteRow(op.table, op.rowId); return; case 'delete': await api.createRow(op.table, op.restore, op.rowId); return; } } async function applyForward(op: UndoOp): Promise { switch (op.kind) { case 'update': await api.updateRow(op.table, op.rowId, op.after); return; case 'insert': await api.createRow(op.table, op.values, op.rowId); return; case 'delete': await api.deleteRow(op.table, op.rowId); return; } } export interface UndoStack { record(op: UndoOp): void; undo(): Promise; redo(): Promise; canUndo: boolean; canRedo: boolean; nextUndoLabel: string | null; nextRedoLabel: string | null; clear(): void; } export function useUndoStack(onChanged: () => Promise | void): UndoStack { const [undoStack, setUndoStack] = useState([]); const [redoStack, setRedoStack] = useState([]); const busy = useRef(false); const record = useCallback((op: UndoOp) => { setUndoStack((s) => [...s, op].slice(-LIMIT)); // A fresh edit forks history; anything redone from here would conflict. setRedoStack([]); }, []); const undo = useCallback(async () => { if (busy.current) return null; const op = undoStack[undoStack.length - 1]; if (!op) return null; busy.current = true; try { await applyInverse(op); setUndoStack((s) => s.slice(0, -1)); setRedoStack((s) => [...s, op].slice(-LIMIT)); await onChanged(); return op; } finally { busy.current = false; } }, [undoStack, onChanged]); const redo = useCallback(async () => { if (busy.current) return null; const op = redoStack[redoStack.length - 1]; if (!op) return null; busy.current = true; try { await applyForward(op); setRedoStack((s) => s.slice(0, -1)); setUndoStack((s) => [...s, op].slice(-LIMIT)); await onChanged(); return op; } finally { busy.current = false; } }, [redoStack, onChanged]); const clear = useCallback(() => { setUndoStack([]); setRedoStack([]); }, []); return { record, undo, redo, canUndo: undoStack.length > 0, canRedo: redoStack.length > 0, nextUndoLabel: undoStack[undoStack.length - 1]?.label ?? null, nextRedoLabel: redoStack[redoStack.length - 1]?.label ?? null, clear, }; } /** Which stored columns actually changed, as a before/after pair. */ export function diffValues( before: Record, after: Record, storedColumns: string[], ): { before: Record; after: Record } { const b: Record = {}; const a: Record = {}; for (const name of storedColumns) { const bv = before[name] ?? null; const av = after[name] ?? null; if (String(bv ?? '') === String(av ?? '')) continue; b[name] = bv; a[name] = av; } return { before: b, after: a }; }