refactor(data-store): consolidate into one self-contained edit modal
element_data_store_form.svelte is now the single source of truth for editing data stores — owns the modal, all form fields, change detection, save/delete API calls, and IDB cache update. - element_data_store.svelte: remove ~120 lines of duplicated form/handler code; now delegates to AE_DataStore_Form via bind:open + callbacks - data_stores/+page.svelte: open_edit/open_new are now 2 lines each; remove all draft state, submit_status, handle_save/delete; fix label a11y (wrapping labels on all filter + bulk-rename inputs) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { untrack } from 'svelte';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Modal } from 'flowbite-svelte';
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpDown,
|
||||
@@ -17,9 +15,7 @@ import {
|
||||
Pencil,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Save,
|
||||
Search,
|
||||
Trash2,
|
||||
X
|
||||
} from '@lucide/svelte';
|
||||
|
||||
@@ -55,23 +51,6 @@ let searched = $state(false);
|
||||
// ── Edit modal ────────────────────────────────────────────────────────────────
|
||||
let show_edit = $state(false);
|
||||
let editing_obj: ae_DataStore | null = $state(null);
|
||||
let is_new = $state(false);
|
||||
|
||||
let draft_code = $state('');
|
||||
let draft_name = $state('');
|
||||
let draft_type = $state('text');
|
||||
let draft_account_id = $state('');
|
||||
let draft_for_type = $state('');
|
||||
let draft_for_id = $state('');
|
||||
let draft_value = $state('');
|
||||
let draft_enable = $state(true);
|
||||
let draft_hide = $state(false);
|
||||
let draft_priority = $state(false);
|
||||
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);
|
||||
@@ -145,141 +124,17 @@ function toggle_sort(col: string) {
|
||||
if (searched) do_search(false);
|
||||
}
|
||||
|
||||
// ── Open edit ─────────────────────────────────────────────────────────────────
|
||||
// ── Open edit / new ───────────────────────────────────────────────────────────
|
||||
function open_edit(obj: ae_DataStore) {
|
||||
editing_obj = obj;
|
||||
is_new = false;
|
||||
draft_code = obj.code ?? '';
|
||||
draft_name = obj.name ?? '';
|
||||
draft_type = obj.type ?? 'text';
|
||||
draft_account_id = obj.account_id ?? '';
|
||||
draft_for_type = obj.for_type ?? '';
|
||||
draft_for_id = obj.for_id ?? '';
|
||||
draft_enable = obj.enable !== false;
|
||||
draft_hide = !!obj.hide;
|
||||
draft_priority = !!obj.priority;
|
||||
draft_sort = String(obj.sort ?? '');
|
||||
draft_group = obj.group ?? '';
|
||||
draft_notes = obj.notes ?? '';
|
||||
draft_value =
|
||||
obj.type === 'json'
|
||||
? typeof obj.json === 'string'
|
||||
? obj.json
|
||||
: JSON.stringify(obj.json ?? '', null, 2)
|
||||
: (obj.text ?? obj.html ?? '');
|
||||
submit_status = 'idle';
|
||||
show_edit = true;
|
||||
}
|
||||
|
||||
function open_new() {
|
||||
editing_obj = null;
|
||||
is_new = true;
|
||||
draft_code = '';
|
||||
draft_name = '';
|
||||
draft_type = 'text';
|
||||
draft_account_id = $ae_loc.account_id ?? '';
|
||||
draft_for_type = '';
|
||||
draft_for_id = '';
|
||||
draft_value = '';
|
||||
draft_enable = true;
|
||||
draft_hide = false;
|
||||
draft_priority = false;
|
||||
draft_sort = '';
|
||||
draft_group = '';
|
||||
draft_notes = '';
|
||||
submit_status = 'idle';
|
||||
show_edit = true;
|
||||
}
|
||||
|
||||
// ── Save ──────────────────────────────────────────────────────────────────────
|
||||
async function handle_save() {
|
||||
submit_status = 'processing';
|
||||
const api_cfg = untrack(() => $ae_api);
|
||||
|
||||
const fields: Record<string, any> = {
|
||||
code: draft_code.trim(),
|
||||
name: draft_name.trim(),
|
||||
type: draft_type,
|
||||
account_id: draft_account_id.trim() || null,
|
||||
for_type: draft_for_type.trim() || null,
|
||||
enable: draft_enable,
|
||||
hide: draft_hide,
|
||||
priority: draft_priority,
|
||||
sort: draft_sort.trim() || null,
|
||||
group: draft_group.trim() || null,
|
||||
notes: draft_notes.trim() || null
|
||||
};
|
||||
|
||||
// for_id can only be meaningfully set on create — backend stores it as an integer FK
|
||||
// and doesn't accept the random string on PATCH.
|
||||
if (is_new && draft_for_id.trim()) {
|
||||
fields.for_id = draft_for_id.trim();
|
||||
}
|
||||
|
||||
if (draft_type === 'json') {
|
||||
try { fields.json = JSON.parse(draft_value); }
|
||||
catch { fields.json = draft_value; }
|
||||
} else {
|
||||
fields.text = draft_value;
|
||||
}
|
||||
|
||||
let result: any;
|
||||
if (is_new) {
|
||||
result = await api.create_ae_obj({ api_cfg, obj_type: 'data_store', fields });
|
||||
} else {
|
||||
result = await api.update_ae_obj({
|
||||
api_cfg,
|
||||
obj_type: 'data_store',
|
||||
obj_id: editing_obj!.id,
|
||||
fields
|
||||
});
|
||||
}
|
||||
|
||||
if (result) {
|
||||
const ds_id = result.data_store_id || result.id;
|
||||
if (ds_id) {
|
||||
const text_val = result.text ?? '';
|
||||
await db_core.data_store.put({
|
||||
...result,
|
||||
id: ds_id,
|
||||
data_store_id: ds_id,
|
||||
updated_on: result.updated_on ?? new Date().toISOString(),
|
||||
text: text_val,
|
||||
html: text_val,
|
||||
json: result.json ?? null
|
||||
});
|
||||
}
|
||||
submit_status = 'saved';
|
||||
setTimeout(() => {
|
||||
show_edit = false;
|
||||
do_search(false);
|
||||
}, 600);
|
||||
} else {
|
||||
submit_status = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Delete ────────────────────────────────────────────────────────────────────
|
||||
async function handle_delete() {
|
||||
if (
|
||||
!editing_obj?.id ||
|
||||
!confirm(`Delete data store "${editing_obj.code}"?\n\nThis cannot be undone.`)
|
||||
)
|
||||
return;
|
||||
const api_cfg = untrack(() => $ae_api);
|
||||
const result = await api.delete_ae_obj({
|
||||
api_cfg,
|
||||
obj_type: 'data_store',
|
||||
obj_id: editing_obj.id,
|
||||
method: 'delete'
|
||||
});
|
||||
if (result) {
|
||||
await db_core.data_store.delete(editing_obj.id);
|
||||
show_edit = false;
|
||||
do_search(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bulk rename ───────────────────────────────────────────────────────────────
|
||||
async function do_rename_preview() {
|
||||
if (!rename_filter.trim()) return;
|
||||
@@ -401,69 +256,69 @@ function content_preview(ds: ae_DataStore): string {
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-bold opacity-70">
|
||||
Code
|
||||
<span class="font-normal opacity-60">— use <code>%</code> as wildcard</span>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-xs font-bold opacity-70">
|
||||
Code <span class="font-normal opacity-60">— use <code>%</code> as wildcard</span>
|
||||
</span>
|
||||
<input
|
||||
type="search"
|
||||
bind:value={qry_code}
|
||||
placeholder="event__pres_mgmt__%"
|
||||
class="input input-sm w-full font-mono text-xs"
|
||||
onkeydown={(e) => e.key === 'Enter' && do_search()} />
|
||||
</div>
|
||||
</label>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-bold opacity-70">
|
||||
Account ID
|
||||
<span class="font-normal opacity-60">— <code>global</code> for null</span>
|
||||
<label class="space-y-1">
|
||||
<span class="text-xs font-bold opacity-70">
|
||||
Account ID <span class="font-normal opacity-60">— <code>global</code> for null</span>
|
||||
</span>
|
||||
<input
|
||||
type="search"
|
||||
bind:value={qry_account_id}
|
||||
placeholder="random ID or 'global'"
|
||||
class="input input-sm w-full font-mono text-xs"
|
||||
onkeydown={(e) => e.key === 'Enter' && do_search()} />
|
||||
</label>
|
||||
<input
|
||||
type="search"
|
||||
bind:value={qry_account_id}
|
||||
placeholder="random ID or 'global'"
|
||||
class="input input-sm w-full font-mono text-xs"
|
||||
onkeydown={(e) => e.key === 'Enter' && do_search()} />
|
||||
<p class="text-[10px] opacity-40">Results are scoped to the active account by the API.</p>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-bold opacity-70">For Type</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-xs font-bold opacity-70">For Type</span>
|
||||
<input
|
||||
type="search"
|
||||
bind:value={qry_for_type}
|
||||
placeholder="event, event_session, person…"
|
||||
class="input input-sm w-full font-mono text-xs"
|
||||
onkeydown={(e) => e.key === 'Enter' && do_search()} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-bold opacity-70">For ID</label>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-xs font-bold opacity-70">For ID</span>
|
||||
<input
|
||||
type="search"
|
||||
bind:value={qry_for_id}
|
||||
placeholder="random string ID"
|
||||
class="input input-sm w-full font-mono text-xs"
|
||||
onkeydown={(e) => e.key === 'Enter' && do_search()} />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3 pt-1">
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-bold opacity-70">Status</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-xs font-bold opacity-70">Status</span>
|
||||
<select class="select select-sm text-xs" bind:value={qry_enabled}>
|
||||
<option value="all">All</option>
|
||||
<option value="enabled">Enabled</option>
|
||||
<option value="not_enabled">Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-bold opacity-70">Per page</label>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-xs font-bold opacity-70">Per page</span>
|
||||
<select class="select select-sm text-xs" bind:value={page_limit}>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={200}>200</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<div class="flex flex-1 items-end justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -496,34 +351,33 @@ function content_preview(ds: ae_DataStore): string {
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-bold opacity-70">
|
||||
Code filter
|
||||
<span class="font-normal opacity-60">— use <code>%</code> as wildcard</span>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-xs font-bold opacity-70">
|
||||
Code filter <span class="font-normal opacity-60">— use <code>%</code> as wildcard</span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={rename_filter}
|
||||
placeholder="event__old_code__%"
|
||||
class="input input-sm w-full font-mono text-xs"
|
||||
onkeydown={(e) => e.key === 'Enter' && do_rename_preview()} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-bold opacity-70">Find text in code</label>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-xs font-bold opacity-70">Find text in code</span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={rename_find_text}
|
||||
placeholder="old_code"
|
||||
class="input input-sm w-full font-mono text-xs" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-bold opacity-70">Replace with</label>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="text-xs font-bold opacity-70">Replace with</span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={rename_replace_text}
|
||||
placeholder="new_code"
|
||||
class="input input-sm w-full font-mono text-xs" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -731,78 +585,9 @@ function content_preview(ds: ae_DataStore): string {
|
||||
</div>
|
||||
|
||||
<!-- ── Edit / Create Modal ───────────────────────────────────────────────────── -->
|
||||
<Modal
|
||||
title={is_new ? 'New Data Store' : `Edit: ${editing_obj?.code ?? ''}`}
|
||||
<AE_DataStore_Form
|
||||
bind:open={show_edit}
|
||||
autoclose={false}
|
||||
outsideclose={!has_changes}
|
||||
size="xl"
|
||||
class="w-full max-w-5xl">
|
||||
|
||||
<form
|
||||
class="space-y-4"
|
||||
onsubmit={(e) => { e.preventDefault(); handle_save(); }}>
|
||||
|
||||
<!-- {#key} recreates the form on each new record, resetting internal state (html_edit_mode, show_advanced) -->
|
||||
{#key editing_obj?.id ?? 'new'}
|
||||
<AE_DataStore_Form
|
||||
bind:draft_code
|
||||
bind:draft_name
|
||||
bind:draft_type
|
||||
bind:draft_value
|
||||
bind:draft_account_id
|
||||
bind:draft_for_type
|
||||
bind:draft_for_id
|
||||
bind:draft_enable
|
||||
bind:draft_hide
|
||||
bind:draft_priority
|
||||
bind:draft_sort
|
||||
bind:draft_group
|
||||
bind:draft_notes
|
||||
{is_new}
|
||||
original_obj={is_new ? null : editing_obj}
|
||||
bind:has_changes />
|
||||
{/key}
|
||||
|
||||
<!-- Footer actions -->
|
||||
<div class="flex items-center justify-between border-t border-surface-500/20 pt-4">
|
||||
{#if !is_new}
|
||||
<button
|
||||
type="button"
|
||||
class="btn preset-tonal-error"
|
||||
onclick={handle_delete}
|
||||
title="Permanently delete this data store — cannot be undone">
|
||||
<Trash2 size={14} class="mr-2" /> Delete
|
||||
</button>
|
||||
{:else}
|
||||
<div></div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn preset-tonal-surface"
|
||||
onclick={() => (show_edit = false)}
|
||||
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 || submit_status === 'processing'}
|
||||
title={is_new ? 'Create new data store' : 'Save changes'}>
|
||||
{#if submit_status === 'processing'}
|
||||
<LoaderCircle size={14} class="mr-2 animate-spin" />
|
||||
{:else if submit_status === 'saved'}
|
||||
<Check size={14} class="mr-2" />
|
||||
{:else if submit_status === 'error'}
|
||||
<X size={14} class="mr-2 text-error-500" />
|
||||
{:else}
|
||||
<Save size={14} class="mr-2" />
|
||||
{/if}
|
||||
{is_new ? 'Create' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
editing_obj={editing_obj}
|
||||
default_account_id={$ae_loc.account_id ?? ''}
|
||||
on_saved={() => do_search(false)}
|
||||
on_deleted={() => do_search(false)} />
|
||||
|
||||
Reference in New Issue
Block a user