fix(data_store): ensure changes are discarded on cancel and close
This commit is contained in:
@@ -72,8 +72,28 @@ let {
|
|||||||
// Local reactive state
|
// Local reactive state
|
||||||
let trigger: null | string = $state(null);
|
let trigger: null | string = $state(null);
|
||||||
let draft_value = $state('');
|
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');
|
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
|
// Dexie LiveQuery for data store
|
||||||
let lq__ds_obj = $derived(
|
let lq__ds_obj = $derived(
|
||||||
liveQuery(async () => {
|
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
|
// Sync status and bound props when the live data changes
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const entry = $lq__ds_obj as ae_DataStore | null;
|
const entry = $lq__ds_obj as ae_DataStore | null;
|
||||||
@@ -120,16 +157,30 @@ $effect(() => {
|
|||||||
if (ds_type === 'sql') {
|
if (ds_type === 'sql') {
|
||||||
val_sql = entry.text || entry.html || null;
|
val_sql = entry.text || entry.html || null;
|
||||||
}
|
}
|
||||||
// Initialize draft_value when not editing
|
// Initialize draft values when not editing
|
||||||
if (!show_edit) {
|
if (!show_edit) {
|
||||||
draft_value = entry.type === 'json'
|
reset_drafts();
|
||||||
? (typeof entry.json === 'string' ? entry.json : JSON.stringify(entry.json, null, 2))
|
|
||||||
: (entry.text || entry.html || '');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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
|
// Context Change Guard
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
void for_id; void for_type; void ds_code;
|
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) {
|
async function handle_submit_form(event: Event) {
|
||||||
const target = event.target as HTMLFormElement;
|
|
||||||
$ae_sess.ds.submit_status = 'processing';
|
$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 = {
|
const data_store_do: key_val = {
|
||||||
code: data_store_di.ds_code ?? ds_code,
|
code: draft_code,
|
||||||
name: data_store_di.ds_name ?? ds_name,
|
name: draft_name,
|
||||||
type: data_store_di.ds_type ?? ds_type,
|
type: draft_type,
|
||||||
for_type: data_store_di.ds_for_type ?? null,
|
for_type: for_type,
|
||||||
for_id: data_store_di.ds_for_id ?? null,
|
for_id: for_id,
|
||||||
enable: data_store_di.ds_enable ?? true,
|
enable: true,
|
||||||
account_id: data_store_di.ds_use_account_id ? (data_store_di.ds_account_id ?? $slct.account_id) : null
|
account_id: draft_use_account_id ? ($ae_loc.account_id || $slct.account_id) : null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data_store_do.type === 'json') {
|
if (data_store_do.type === 'json') {
|
||||||
@@ -260,6 +302,12 @@ async function handle_delete() {
|
|||||||
show_edit = false;
|
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>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -277,9 +325,10 @@ async function handle_delete() {
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="{$lq__ds_obj.name || 'Unnamed'} - {$lq__ds_obj.code}"
|
title="{draft_name || 'Unnamed'} - {draft_code}"
|
||||||
bind:open={show_edit}
|
bind:open={show_edit}
|
||||||
autoclose={false}
|
autoclose={false}
|
||||||
|
outsideclose={!has_changes}
|
||||||
size="xl"
|
size="xl"
|
||||||
class="w-full max-w-6xl">
|
class="w-full max-w-6xl">
|
||||||
<form class="flex flex-col gap-4" onsubmit={(e) => { e.preventDefault(); handle_submit_form(e); }}>
|
<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">
|
<div class="space-y-2">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="text-xs font-bold opacity-70">Code</span>
|
<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>
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="text-xs font-bold opacity-70">Name</span>
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="text-xs font-bold opacity-70">Type</span>
|
<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="text">Text</option>
|
||||||
<option value="html">HTML</option>
|
<option value="html">HTML</option>
|
||||||
<option value="json">JSON</option>
|
<option value="json">JSON</option>
|
||||||
@@ -306,7 +355,7 @@ async function handle_delete() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-center gap-2 pt-6">
|
<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>
|
<label for="ds_use_account_id" class="text-xs cursor-pointer">Account Specific</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -315,7 +364,7 @@ async function handle_delete() {
|
|||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-xs font-bold opacity-70">Content</span>
|
<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">
|
<div class="flex items-center gap-1 rounded bg-black/5 p-0.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -341,14 +390,14 @@ async function handle_delete() {
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $lq__ds_obj.type === 'html'}
|
{#if draft_type === 'html'}
|
||||||
{#if html_edit_mode === 'source'}
|
{#if html_edit_mode === 'source'}
|
||||||
<AE_Comp_Editor_CodeMirror bind:content={draft_value} placeholder="Enter HTML Source..." />
|
<AE_Comp_Editor_CodeMirror bind:content={draft_value} placeholder="Enter HTML Source..." />
|
||||||
{:else}
|
{:else}
|
||||||
<AE_Comp_Editor_TipTap bind:content={draft_value} placeholder="Enter HTML content..." />
|
<AE_Comp_Editor_TipTap bind:content={draft_value} placeholder="Enter HTML content..." />
|
||||||
{/if}
|
{/if}
|
||||||
{:else if $lq__ds_obj.type === 'json' || $lq__ds_obj.type === 'sql' || $lq__ds_obj.type === 'md'}
|
{:else if draft_type === 'json' || draft_type === 'sql' || draft_type === 'md'}
|
||||||
<AE_Comp_Editor_CodeMirror bind:content={draft_value} placeholder="Enter {$lq__ds_obj.type.toUpperCase()} content..." />
|
<AE_Comp_Editor_CodeMirror bind:content={draft_value} placeholder="Enter {draft_type.toUpperCase()} content..." />
|
||||||
{:else}
|
{:else}
|
||||||
<textarea bind:value={draft_value} class="textarea font-mono text-sm" rows="15" placeholder="Enter text content..."></textarea>
|
<textarea bind:value={draft_value} class="textarea font-mono text-sm" rows="15" placeholder="Enter text content..."></textarea>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -366,13 +415,14 @@ async function handle_delete() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn preset-tonal-surface"
|
class="btn preset-tonal-surface"
|
||||||
onclick={() => (show_edit = false)}
|
onclick={handle_cancel}
|
||||||
title="Discard changes and close">
|
title="Discard changes and close">
|
||||||
<X size="14" class="mr-2" /> Cancel
|
<X size="14" class="mr-2" /> Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn preset-filled-primary-500"
|
class="btn preset-filled-primary-500"
|
||||||
|
disabled={!has_changes || $ae_sess.ds.submit_status === 'processing'}
|
||||||
title="Save changes to this data store">
|
title="Save changes to this data store">
|
||||||
{#if $ae_sess.ds.submit_status === 'processing'}
|
{#if $ae_sess.ds.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