fix(data-store-form): add change detection, fix button class, restore textarea height
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,12 @@
|
|||||||
* Usage contexts:
|
* Usage contexts:
|
||||||
* - /core/data_stores/ management page (full fields, show_for_fields=true)
|
* - /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)
|
* - 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 { ChevronDown, ChevronUp, Code, Eye } from '@lucide/svelte';
|
||||||
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.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';
|
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_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)
|
show_for_fields?: boolean; // show for_type + for_id (false for embedded widget)
|
||||||
readonly_code?: boolean; // lock the code field (non-manager users)
|
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 {
|
let {
|
||||||
@@ -56,11 +66,38 @@ let {
|
|||||||
show_account_field = true,
|
show_account_field = true,
|
||||||
show_for_fields = true,
|
show_for_fields = true,
|
||||||
readonly_code = false,
|
readonly_code = false,
|
||||||
|
original_obj = null,
|
||||||
|
has_changes = $bindable(false),
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// Internal — not bindable; resets automatically when parent uses {#key} on record change
|
// Internal — not bindable; resets automatically when parent uses {#key} on record change
|
||||||
let html_edit_mode = $state<'source' | 'visual'>('source');
|
let html_edit_mode = $state<'source' | 'visual'>('source');
|
||||||
let show_advanced = $state(false);
|
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; });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@@ -224,7 +261,7 @@ let show_advanced = $state(false);
|
|||||||
<textarea
|
<textarea
|
||||||
bind:value={draft_value}
|
bind:value={draft_value}
|
||||||
class="textarea font-mono text-sm"
|
class="textarea font-mono text-sm"
|
||||||
rows="10"
|
rows="15"
|
||||||
placeholder="Enter text content…"></textarea>
|
placeholder="Enter text content…"></textarea>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ let draft_sort = $state('');
|
|||||||
let draft_group = $state('');
|
let draft_group = $state('');
|
||||||
let draft_notes = $state('');
|
let draft_notes = $state('');
|
||||||
let submit_status = $state<'idle' | 'processing' | 'saved' | 'error'>('idle');
|
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 ─────────────────────────────────────────────────────────
|
// ── Bulk rename state ─────────────────────────────────────────────────────────
|
||||||
let show_bulk_rename = $state(false);
|
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 ?? ''}`}
|
title={is_new ? 'New Data Store' : `Edit: ${editing_obj?.code ?? ''}`}
|
||||||
bind:open={show_edit}
|
bind:open={show_edit}
|
||||||
autoclose={false}
|
autoclose={false}
|
||||||
outsideclose={submit_status !== 'processing'}
|
outsideclose={!has_changes}
|
||||||
size="xl"
|
size="xl"
|
||||||
class="w-full max-w-5xl">
|
class="w-full max-w-5xl">
|
||||||
|
|
||||||
@@ -758,7 +759,9 @@ function content_preview(ds: ae_DataStore): string {
|
|||||||
bind:draft_sort
|
bind:draft_sort
|
||||||
bind:draft_group
|
bind:draft_group
|
||||||
bind:draft_notes
|
bind:draft_notes
|
||||||
{is_new} />
|
{is_new}
|
||||||
|
original_obj={is_new ? null : editing_obj}
|
||||||
|
bind:has_changes />
|
||||||
{/key}
|
{/key}
|
||||||
|
|
||||||
<!-- Footer actions -->
|
<!-- Footer actions -->
|
||||||
@@ -785,8 +788,8 @@ function content_preview(ds: ae_DataStore): string {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn preset-filled-primary"
|
class="btn preset-filled-primary-500"
|
||||||
disabled={submit_status === 'processing'}
|
disabled={!has_changes || submit_status === 'processing'}
|
||||||
title={is_new ? 'Create new data store' : 'Save changes'}>
|
title={is_new ? 'Create new data store' : 'Save changes'}>
|
||||||
{#if submit_status === 'processing'}
|
{#if submit_status === 'processing'}
|
||||||
<LoaderCircle size={14} class="mr-2 animate-spin" />
|
<LoaderCircle size={14} class="mr-2 animate-spin" />
|
||||||
|
|||||||
Reference in New Issue
Block a user