From e321a2c4c5209c4c14d5ce066676ea4a523241bf Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Wed, 17 Jun 2026 15:30:42 -0400 Subject: [PATCH] fix(data-store-form): add change detection, fix button class, restore textarea height MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add original_obj prop + has_changes bindable output to element_data_store_form - Derive has_changes by comparing each draft field to original_obj; null = always dirty (new record) - Wire bind:has_changes + original_obj={is_new ? null : editing_obj} in management page - Fix Save button: preset-filled-primary → preset-filled-primary-500 (matches element_data_store) - Disable Save when !has_changes; block modal outsideclose when changes are pending - Restore textarea rows 10 → 15 to match element_data_store.svelte Co-Authored-By: Claude Sonnet 4.6 --- .../elements/element_data_store_form.svelte | 39 ++++++++++++++++++- src/routes/core/data_stores/+page.svelte | 11 ++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/lib/elements/element_data_store_form.svelte b/src/lib/elements/element_data_store_form.svelte index c927be62..2eb8d782 100644 --- a/src/lib/elements/element_data_store_form.svelte +++ b/src/lib/elements/element_data_store_form.svelte @@ -6,7 +6,12 @@ * Usage contexts: * - /core/data_stores/ management page (full fields, show_for_fields=true) * - element_data_store.svelte inline editor (show_for_fields=false, show_account_field=false) + * + * Change detection: pass original_obj (the unmodified record) to enable has_changes. + * For new records, pass original_obj=null — has_changes will always be true. + * The parent can read changes via bind:has_changes. */ +import type { ae_DataStore } from '$lib/types/ae_types'; import { ChevronDown, ChevronUp, Code, Eye } from '@lucide/svelte'; import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte'; import AE_Comp_Editor_TipTap from '$lib/elements/element_editor_tiptap.svelte'; @@ -36,6 +41,11 @@ interface Props { show_account_field?: boolean; // show account_id text input (false for embedded widget) show_for_fields?: boolean; // show for_type + for_id (false for embedded widget) readonly_code?: boolean; // lock the code field (non-manager users) + + // Change detection — pass the unmodified record; null/undefined = new record (always dirty) + original_obj?: ae_DataStore | null; + // Bindable output: true when any draft field differs from original_obj (or always true for new records) + has_changes?: boolean; } let { @@ -56,11 +66,38 @@ let { show_account_field = true, show_for_fields = true, readonly_code = false, + original_obj = null, + has_changes = $bindable(false), }: Props = $props(); // Internal — not bindable; resets automatically when parent uses {#key} on record change let html_edit_mode = $state<'source' | 'visual'>('source'); let show_advanced = $state(false); + +// Compare each draft field to the original to detect unsaved changes. +// new records (original_obj = null) are always considered dirty so Save is never blocked. +let _has_changes = $derived.by(() => { + if (!original_obj) return true; + const orig_value = original_obj.type === 'json' + ? (typeof original_obj.json === 'string' ? original_obj.json : JSON.stringify(original_obj.json ?? '', null, 2)) + : (original_obj.text || original_obj.html || ''); + return ( + draft_code !== (original_obj.code ?? '') || + draft_name !== (original_obj.name ?? '') || + draft_type !== (original_obj.type ?? 'text') || + draft_value !== orig_value || + draft_account_id !== (original_obj.account_id ?? '') || + draft_enable !== (original_obj.enable !== false) || + draft_hide !== !!original_obj.hide || + draft_priority !== !!original_obj.priority || + draft_sort !== String(original_obj.sort ?? '') || + draft_group !== (original_obj.group ?? '') || + draft_notes !== (original_obj.notes ?? '') + ); +}); + +// Push computed value out to the parent via the bindable prop +$effect(() => { has_changes = _has_changes; });
@@ -224,7 +261,7 @@ let show_advanced = $state(false); {/if}
diff --git a/src/routes/core/data_stores/+page.svelte b/src/routes/core/data_stores/+page.svelte index c89344ec..7675224d 100644 --- a/src/routes/core/data_stores/+page.svelte +++ b/src/routes/core/data_stores/+page.svelte @@ -71,6 +71,7 @@ let draft_sort = $state(''); let draft_group = $state(''); let draft_notes = $state(''); let submit_status = $state<'idle' | 'processing' | 'saved' | 'error'>('idle'); +let has_changes = $state(false); // bound from form component — true when any draft differs from editing_obj // ── Bulk rename state ───────────────────────────────────────────────────────── let show_bulk_rename = $state(false); @@ -734,7 +735,7 @@ function content_preview(ds: ae_DataStore): string { title={is_new ? 'New Data Store' : `Edit: ${editing_obj?.code ?? ''}`} bind:open={show_edit} autoclose={false} - outsideclose={submit_status !== 'processing'} + outsideclose={!has_changes} size="xl" class="w-full max-w-5xl"> @@ -758,7 +759,9 @@ function content_preview(ds: ae_DataStore): string { bind:draft_sort bind:draft_group bind:draft_notes - {is_new} /> + {is_new} + original_obj={is_new ? null : editing_obj} + bind:has_changes /> {/key} @@ -785,8 +788,8 @@ function content_preview(ds: ae_DataStore): string {