From 7eb24a46409b7278a5c35989cdaff65c28c6c96b Mon Sep 17 00:00:00 2001 From: Naomi Gilbert Date: Thu, 30 Jul 2026 22:30:58 -0400 Subject: [PATCH 1/2] fix(builder): warn before leaving with unsaved schema changes The structure builder held every edit in local state and only persisted it through the explicit Save button, with no dirty tracking, no router blocker and no beforeunload handler. Navigating away discarded the work silently. Track a snapshot of what the server holds, keyed by draft field id, and compare the live draft against it. The baseline is rebased on load, after a successful save-all, after a field deletion (which persists immediately) and after a confirmed destructive type change, so only genuinely unsaved work triggers the guard. useBlocker covers in-app navigation and, via enableBeforeUnload, tab close and reload. Also replace the canvas' 'Autosaved draft' label, which claimed a behaviour that does not exist, with a live saved/unsaved indicator. Co-Authored-By: Claude Fable 5 --- .claude/launch.json | 11 ++ apps/client/src/components/FieldCanvas.tsx | 19 +++- .../src/components/UnsavedChangesModal.tsx | 49 +++++++++ apps/client/src/contexts/I18nContext.tsx | 16 ++- .../__tests__/builder-unsaved-guard.test.tsx | 104 ++++++++++++++++++ .../routes/builder.$databaseId.$tableId.tsx | 64 ++++++++++- 6 files changed, 258 insertions(+), 5 deletions(-) create mode 100644 .claude/launch.json create mode 100644 apps/client/src/components/UnsavedChangesModal.tsx create mode 100644 apps/client/src/routes/__tests__/builder-unsaved-guard.test.tsx diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..1f99702 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "client", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["--filter", "client", "dev"], + "port": 5173 + } + ] +} diff --git a/apps/client/src/components/FieldCanvas.tsx b/apps/client/src/components/FieldCanvas.tsx index a24785e..4738afb 100644 --- a/apps/client/src/components/FieldCanvas.tsx +++ b/apps/client/src/components/FieldCanvas.tsx @@ -30,6 +30,7 @@ interface FieldCanvasProps { onAdd: (type: FieldType, insertIndex?: number) => void; onReorder: (fields: BuilderField[]) => void; onRemove: (id: string) => void; + isDirty?: boolean; } interface BuilderDndProviderProps { @@ -201,6 +202,7 @@ export function FieldCanvas({ selectedId, onSelect, onRemove, + isDirty = false, }: Omit) { const { t } = useI18n(); const { active, over } = useDndContext(); @@ -226,7 +228,22 @@ export function FieldCanvas({

{t('builder.canvas.title')}

- {t('builder.saveDraft.label')} + + {isDirty ? ( + <> +