fix(data_store): ensure changes are discarded on cancel and close

This commit is contained in:
Scott Idem
2026-06-16 18:27:02 -04:00
parent d23f9073c4
commit 50d346a36c

View File

@@ -72,8 +72,28 @@ let {
// Local reactive state
let trigger: null | string = $state(null);
let draft_value = $state('');
let draft_name = $state('');
let draft_code = $state('');
let draft_type = $state('');
let draft_use_account_id = $state(false);
let html_edit_mode = $state<'source' | 'visual'>('source');
// Change detection derived from draft vs current LiveQuery object
let has_changes = $derived.by(() => {
const entry = $lq__ds_obj as ae_DataStore | null;
if (!entry) return true; // Treat new record as having changes
const current_val = entry.type === 'json'
? (typeof entry.json === 'string' ? entry.json : JSON.stringify(entry.json, null, 2))
: (entry.text || entry.html || '');
return draft_value !== current_val ||
draft_name !== (entry.name || '') ||
draft_code !== (entry.code || '') ||
draft_type !== (entry.type || 'text') ||
draft_use_account_id !== (!!entry.account_id);
});
// Dexie LiveQuery for data store
let lq__ds_obj = $derived(
liveQuery(async () => {
@@ -110,6 +130,23 @@ let lq__ds_obj = $derived(
})
);
/**
* reset_drafts: Resets all draft fields to the current object's values.
* Called when the live data changes OR when the editor is closed/cancelled.
*/
function reset_drafts() {
const entry = $lq__ds_obj as ae_DataStore | null;
if (!entry) return;
draft_name = entry.name || '';
draft_code = entry.code || '';
draft_type = entry.type || 'text';
draft_use_account_id = !!entry.account_id;
draft_value = entry.type === 'json'
? (typeof entry.json === 'string' ? entry.json : JSON.stringify(entry.json, null, 2))
: (entry.text || entry.html || '');
}
// Sync status and bound props when the live data changes
$effect(() => {
const entry = $lq__ds_obj as ae_DataStore | null;
@@ -120,16 +157,30 @@ $effect(() => {
if (ds_type === 'sql') {
val_sql = entry.text || entry.html || null;
}
// Initialize draft_value when not editing
// Initialize draft values when not editing
if (!show_edit) {
draft_value = entry.type === 'json'
? (typeof entry.json === 'string' ? entry.json : JSON.stringify(entry.json, null, 2))
: (entry.text || entry.html || '');
reset_drafts();
}
}
});
});
// Reset draft values when the editor is closed (discard changes)
$effect(() => {
if (!show_edit) {
untrack(() => reset_drafts());
}
});
// Reset submit status when opening the editor
$effect(() => {
if (show_edit) {
untrack(() => {
$ae_sess.ds.submit_status = 'idle';
});
}
});
// Context Change Guard
$effect(() => {
void for_id; void for_type; void ds_code;
@@ -204,25 +255,16 @@ async function load_data_store() {
}
async function handle_submit_form(event: Event) {
const target = event.target as HTMLFormElement;
$ae_sess.ds.submit_status = 'processing';
const form_data = new FormData(target);
const data_store_di = ae_util.extract_prefixed_form_data({
prefix: null,
form_data,
trim_values: true,
bool_tf_str: true
});
const data_store_do: key_val = {
code: data_store_di.ds_code ?? ds_code,
name: data_store_di.ds_name ?? ds_name,
type: data_store_di.ds_type ?? ds_type,
for_type: data_store_di.ds_for_type ?? null,
for_id: data_store_di.ds_for_id ?? null,
enable: data_store_di.ds_enable ?? true,
account_id: data_store_di.ds_use_account_id ? (data_store_di.ds_account_id ?? $slct.account_id) : null
code: draft_code,
name: draft_name,
type: draft_type,
for_type: for_type,
for_id: for_id,
enable: true,
account_id: draft_use_account_id ? ($ae_loc.account_id || $slct.account_id) : null
};
if (data_store_do.type === 'json') {
@@ -260,6 +302,12 @@ async function handle_delete() {
show_edit = false;
}
}
function handle_cancel() {
if (has_changes && !confirm('Discard unsaved changes?')) return;
show_edit = false;
// reset_drafts() will be called by the $effect watching show_edit
}
</script>
<div
@@ -277,9 +325,10 @@ async function handle_delete() {
{/if}
<Modal
title="{$lq__ds_obj.name || 'Unnamed'} - {$lq__ds_obj.code}"
title="{draft_name || 'Unnamed'} - {draft_code}"
bind:open={show_edit}
autoclose={false}
outsideclose={!has_changes}
size="xl"
class="w-full max-w-6xl">
<form class="flex flex-col gap-4" onsubmit={(e) => { e.preventDefault(); handle_submit_form(e); }}>
@@ -287,17 +336,17 @@ async function handle_delete() {
<div class="space-y-2">
<label class="label">
<span class="text-xs font-bold opacity-70">Code</span>
<input type="text" name="ds_code" class="input font-mono" value={$lq__ds_obj.code} readonly={!$ae_loc.manager_access} required />
<input type="text" name="ds_code" class="input font-mono" bind:value={draft_code} readonly={!$ae_loc.manager_access} required />
</label>
<label class="label">
<span class="text-xs font-bold opacity-70">Name</span>
<input type="text" name="ds_name" class="input" value={$lq__ds_obj.name} required />
<input type="text" name="ds_name" class="input" bind:value={draft_name} required />
</label>
</div>
<div class="space-y-2">
<label class="label">
<span class="text-xs font-bold opacity-70">Type</span>
<select name="ds_type" class="select" bind:value={$lq__ds_obj.type}>
<select name="ds_type" class="select" bind:value={draft_type}>
<option value="text">Text</option>
<option value="html">HTML</option>
<option value="json">JSON</option>
@@ -306,7 +355,7 @@ async function handle_delete() {
</select>
</label>
<div class="flex items-center gap-2 pt-6">
<input type="checkbox" name="ds_use_account_id" id="ds_use_account_id" class="checkbox" checked={!!$lq__ds_obj.account_id} />
<input type="checkbox" name="ds_use_account_id" id="ds_use_account_id" class="checkbox" bind:checked={draft_use_account_id} />
<label for="ds_use_account_id" class="text-xs cursor-pointer">Account Specific</label>
</div>
</div>
@@ -315,7 +364,7 @@ async function handle_delete() {
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-xs font-bold opacity-70">Content</span>
{#if $lq__ds_obj.type === 'html'}
{#if draft_type === 'html'}
<div class="flex items-center gap-1 rounded bg-black/5 p-0.5">
<button
type="button"
@@ -341,14 +390,14 @@ async function handle_delete() {
{/if}
</div>
{#if $lq__ds_obj.type === 'html'}
{#if draft_type === 'html'}
{#if html_edit_mode === 'source'}
<AE_Comp_Editor_CodeMirror bind:content={draft_value} placeholder="Enter HTML Source..." />
{:else}
<AE_Comp_Editor_TipTap bind:content={draft_value} placeholder="Enter HTML content..." />
{/if}
{:else if $lq__ds_obj.type === 'json' || $lq__ds_obj.type === 'sql' || $lq__ds_obj.type === 'md'}
<AE_Comp_Editor_CodeMirror bind:content={draft_value} placeholder="Enter {$lq__ds_obj.type.toUpperCase()} content..." />
{:else if draft_type === 'json' || draft_type === 'sql' || draft_type === 'md'}
<AE_Comp_Editor_CodeMirror bind:content={draft_value} placeholder="Enter {draft_type.toUpperCase()} content..." />
{:else}
<textarea bind:value={draft_value} class="textarea font-mono text-sm" rows="15" placeholder="Enter text content..."></textarea>
{/if}
@@ -366,13 +415,14 @@ async function handle_delete() {
<button
type="button"
class="btn preset-tonal-surface"
onclick={() => (show_edit = false)}
onclick={handle_cancel}
title="Discard changes and close">
<X size="14" class="mr-2" /> Cancel
</button>
<button
type="submit"
class="btn preset-filled-primary-500"
disabled={!has_changes || $ae_sess.ds.submit_status === 'processing'}
title="Save changes to this data store">
{#if $ae_sess.ds.submit_status === 'processing'}
<LoaderCircle size="14" class="mr-2 animate-spin" />