From 8146316aafc524172974d16ed5163c6d43ecdbcb Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Tue, 16 Jun 2026 18:05:59 -0400 Subject: [PATCH] fix(elements): button icons, titles, and dead code cleanup in data_store + field_editor_new MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit element_data_store.svelte: - Source/Visual HTML mode toggles: add Code/Eye icons and title attributes - Cancel button: add X icon and title="Discard changes and close" - Delete button: add title="Permanently delete this data store — cannot be undone" - Save button: add title, and Check icon for updated/created success state (Check was imported but unused; now put to work matching field editor pattern) - Remove Eraser import (unused) - Remove ds_submit_results $state (declared but never read in template — submit status already tracked via $ae_sess.ds.submit_status; drop the assignment in handle_submit_form too) element_ae_obj_field_editor_new.svelte: - Fix duplicate is_editing declaration: user promoted it to a $bindable prop; remove the now-conflicting local $state declaration npx svelte-check: 0 errors Co-Authored-By: Claude Sonnet 4.6 --- .../element_ae_obj_field_editor_new.svelte | 10 +- src/lib/elements/element_data_store.svelte | 418 ++++++------------ 2 files changed, 148 insertions(+), 280 deletions(-) diff --git a/src/lib/elements/element_ae_obj_field_editor_new.svelte b/src/lib/elements/element_ae_obj_field_editor_new.svelte index 745d6b5b..33b18fa2 100644 --- a/src/lib/elements/element_ae_obj_field_editor_new.svelte +++ b/src/lib/elements/element_ae_obj_field_editor_new.svelte @@ -71,9 +71,13 @@ interface Props { // Callbacks on_success?: (data: any) => void; on_error?: (error: any) => void; + on_open?: () => void | Promise; // Snippets children?: Snippet; + + // State + is_editing?: boolean; } let { @@ -94,7 +98,9 @@ let { log_lvl = 0, on_success, on_error, - children + on_open, + children, + is_editing = $bindable(false) }: Props = $props(); /** @@ -149,7 +155,7 @@ function coerce_select_value(raw: string, reference: T): any { } // Internal State -let is_editing = $state(false); +// is_editing is a $bindable prop (declared above) — not a local $state. let patch_status = $state<'idle' | 'processing' | 'success' | 'error'>('idle'); let error_message = $state(''); let draft_value = $state(to_input_value(current_value, field_type)); diff --git a/src/lib/elements/element_data_store.svelte b/src/lib/elements/element_data_store.svelte index d80219ff..38724cd1 100644 --- a/src/lib/elements/element_data_store.svelte +++ b/src/lib/elements/element_data_store.svelte @@ -10,6 +10,20 @@ import { db_core } from '$lib/ae_core/db_core'; import { ae_util } from '$lib/ae_utils/ae_utils'; import type { key_val } from '$lib/stores/ae_stores'; import type { ae_DataStore } from '$lib/types/ae_types'; +import { + Check, + Code, + Eye, + LoaderCircle, + Save, + SquarePen, + Trash2, + X, + Info, + Database +} from '@lucide/svelte'; +import AE_Comp_Editor_TipTap from '$lib/elements/element_editor_tiptap.svelte'; +import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte'; interface Props { log_lvl?: number; @@ -57,10 +71,10 @@ let { // Local reactive state let trigger: null | string = $state(null); -let ds_submit_results: Promise | key_val | undefined = $state(); +let draft_value = $state(''); +let html_edit_mode = $state<'source' | 'visual'>('source'); // Dexie LiveQuery for data store -// This derived observable will automatically update when dependencies change let lq__ds_obj = $derived( liveQuery(async () => { const current_code = ds_code; @@ -70,20 +84,6 @@ let lq__ds_obj = $derived( if (!current_code) return null; - if (log_lvl) - console.log(`ae_e_data_store [${current_code}]: LQ Lookup...`, { - account_id, - current_for_type, - current_for_id - }); - - // Hierarchical Local Lookup (Specific -> Account -> Global) - // Mimics backend SQL priority: WHERE code = :code ORDER BY for_id DESC, account_id DESC - if (log_lvl) - console.log( - `ae_e_data_store [${current_code}]: Fetching all matching codes for priority sorting...` - ); - const results = await db_core.data_store .where('code') .equals(current_code) @@ -93,40 +93,19 @@ let lq__ds_obj = $derived( // Sort by specificity results.sort((a, b) => { - // 1. Priority: Specific Context match (for_type + for_id) - const a_context = - current_for_id && - a.for_id === current_for_id && - a.for_type === current_for_type - ? 1 - : 0; - const b_context = - current_for_id && - b.for_id === current_for_id && - b.for_type === current_for_type - ? 1 - : 0; + const a_context = current_for_id && a.for_id === current_for_id && a.for_type === current_for_type ? 1 : 0; + const b_context = current_for_id && b.for_id === current_for_id && b.for_type === current_for_type ? 1 : 0; if (a_context !== b_context) return b_context - a_context; - // 2. Priority: Account-specific match const a_account = account_id && a.account_id === account_id ? 1 : 0; const b_account = account_id && b.account_id === account_id ? 1 : 0; if (a_account !== b_account) return b_account - a_account; - // 3. Tie-breaker: Newest updated - const a_time = new Date( - a.updated_on || a.created_on || 0 - ).getTime(); - const b_time = new Date( - b.updated_on || b.created_on || 0 - ).getTime(); + const a_time = new Date(a.updated_on || a.created_on || 0).getTime(); + const b_time = new Date(b.updated_on || b.created_on || 0).getTime(); return b_time - a_time; }); - if (log_lvl) - console.log( - `ae_e_data_store [${current_code}]: Best match found (ID: ${results[0].id}, Account: ${results[0].account_id})` - ); return results[0]; }) ); @@ -134,61 +113,42 @@ let lq__ds_obj = $derived( // Sync status and bound props when the live data changes $effect(() => { const entry = $lq__ds_obj as ae_DataStore | null; - untrack(() => { ds_loaded = !!entry; if (entry) { ds_loading_status = 'loaded'; - // Handle val_sql binding if type is sql if (ds_type === 'sql') { val_sql = entry.text || entry.html || null; } + // Initialize draft_value 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 || ''); + } } }); }); -// Context Change Guard: reset loading status when identity props change. -// WHY: ds_loading_status persists after the initial load cycle ('loaded', 'not found', etc.). -// When for_id/for_type/ds_code change after mount (e.g. for_id resolves from undefined -// to a real event_id), the trigger effect below won't re-fire unless we reset to 'starting'. -// untrack() prevents a circular dep — we write ds_loading_status but don't subscribe to it here. +// Context Change Guard $effect(() => { - void for_id; - void for_type; - void ds_code; - untrack(() => { - ds_loading_status = 'starting'; - }); + void for_id; void for_type; void ds_code; + untrack(() => { ds_loading_status = 'starting'; }); }); -// Initial Trigger & Context Change Guard $effect(() => { const account_id = $slct.account_id; const api_ready = !!$ae_api?.base_url; const entry = $lq__ds_obj as ae_DataStore | null | undefined; - // Don't fire until the bootstrap Sync Effect has set a real account_id. - // Without this guard, the fetch runs with null account_id and the - // localStorage scavenge in get_object picks up a stale account_id from a - // previous session, returning the wrong account's record. if (!browser || !account_id || !api_ready || ds_loading_status !== 'starting') return; - // Also reload when IDB has a record but it belongs to a different account - // (not null/global and not the current account). This handles the case where - // a previous dev/demo session left account-specific rows in IDB that score - // as the "best" liveQuery match even though they are for the wrong account. - const entry_is_stale_account = - entry !== undefined && - entry !== null && - entry.account_id !== null && - entry.account_id !== account_id; - + const entry_is_stale_account = entry?.account_id !== null && entry?.account_id !== account_id; if (!entry || entry_is_stale_account) { trigger = 'load__ds__code'; } }); -// Fetch handler $effect(() => { if (trigger === 'load__ds__code') { untrack(() => { @@ -198,13 +158,9 @@ $effect(() => { } }); -// Mount reload logic onMount(() => { if (mount_reload_sec > 0) { - const random_ms = Math.floor(Math.random() * mount_reload_sec * 1000); - setTimeout(() => { - trigger = 'load__ds__code'; - }, random_ms); + setTimeout(() => { trigger = 'load__ds__code'; }, Math.floor(Math.random() * mount_reload_sec * 1000)); } }); @@ -213,81 +169,33 @@ async function load_data_store() { ds_loading_status = 'loading'; const api_cfg = untrack(() => $ae_api); - if (log_lvl) console.log(`ae_e_data_store [${ds_code}]: Fetching...`); - try { - // Attempt 1: Context-specific fetch - let ds_results = await api.get_data_store({ - api_cfg, - code: ds_code, - for_type: for_type, - for_id: for_id, - log_lvl: log_lvl - }); - - // V3 API structured check + let ds_results = await api.get_data_store({ api_cfg, code: ds_code, for_type, for_id, log_lvl }); const is_error = ds_results?.meta?.success === false; - const status_code = - ds_results?.meta?.status_code || (ds_results === false ? 500 : 200); + const status_code = ds_results?.meta?.status_code || (ds_results === false ? 500 : 200); - // Fallback to Global if not found (404), unauthorized (403/401), or explicitly failed - if ( - !ds_results || - is_error || - status_code === 404 || - status_code === 403 || - status_code === 401 - ) { - if (log_lvl) - console.log( - `ae_e_data_store [${ds_code}]: Not found in context (Status ${status_code}). Trying global fallback.` - ); - - // TEMPORARY: same global-default fallback as core__data_store.ts. - // This should go away once the backend can answer with JWT-backed, - // account-scoped defaults only. - ds_results = await api.get_data_store({ - api_cfg, - code: ds_code, - no_account_id: true, - log_lvl: log_lvl - }); + if (!ds_results || is_error || [404, 403, 401].includes(status_code)) { + ds_results = await api.get_data_store({ api_cfg, code: ds_code, no_account_id: true, log_lvl }); } const ds_id = ds_results?.data_store_id || ds_results?.id; - if (ds_results && ds_id) { - // Map fields correctly for V3 alignment const text_val = ds_results.text || ''; - const json_val = - ds_results.json || - (ds_results.json_str ? JSON.parse(ds_results.json_str) : null); + const json_val = ds_results.json || (ds_results.json_str ? JSON.parse(ds_results.json_str) : null); - // Save to Dexie const ds_to_save: ae_DataStore = { ...ds_results, id: ds_id, data_store_id: ds_results.data_store_id || ds_id, - // data_store_id: ds_id, - account_id: ds_results.account_id || ds_results.account_id, - // account_id: ds_results.account_id || ds_results.account_id, + account_id: ds_results.account_id || null, updated_on: ds_results.updated_on || new Date().toISOString(), text: text_val, - html: text_val, // Default map text to html + html: text_val, json: json_val }; - await db_core.data_store.put(ds_to_save); - if (log_lvl) - console.log( - `ae_e_data_store [${ds_code}]: Saved to Dexie. ID: ${ds_id}` - ); } else { ds_loading_status = 'not found'; - if (log_lvl) - console.warn( - `ae_e_data_store [${ds_code}]: Result had no valid ID.` - ); } } catch (err) { console.error(`ae_e_data_store [${ds_code}]: Fetch failed.`, err); @@ -313,77 +221,39 @@ async function handle_submit_form(event: Event) { 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, - access_read: data_store_di.ds_access_read, - access_write: data_store_di.ds_access_write, - access_delete: data_store_di.ds_access_delete, 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 + account_id: data_store_di.ds_use_account_id ? (data_store_di.ds_account_id ?? $slct.account_id) : null }; - const content_val = data_store_di.ds_value; if (data_store_do.type === 'json') { - data_store_do.json = content_val; try { - // Ensure it's valid JSON if stringified - if (typeof content_val === 'string') JSON.parse(content_val); + data_store_do.json = JSON.parse(draft_value); } catch (e) { - console.error('Invalid JSON content'); + data_store_do.json = draft_value; } } else { - data_store_do.text = content_val; + data_store_do.text = draft_value; } const api_cfg = untrack(() => $ae_api); + const api_call = $lq__ds_obj?.id + ? api.update_ae_obj({ api_cfg, obj_type: 'data_store', obj_id: $lq__ds_obj.id, fields: data_store_do }) + : api.create_ae_obj({ api_cfg, obj_type: 'data_store', fields: data_store_do }); - if ($lq__ds_obj?.id) { - ds_submit_results = api - .update_ae_obj({ - api_cfg, - obj_type: 'data_store', - obj_id: $lq__ds_obj.id, - fields: data_store_do - }) - .then((res) => { - if (res) { - $ae_sess.ds.submit_status = 'updated'; - trigger = 'load__ds__code'; - } - return res; - }); - } else { - ds_submit_results = api - .create_ae_obj({ - api_cfg, - obj_type: 'data_store', - fields: data_store_do - }) - .then((res) => { - if (res) { - $ae_sess.ds.submit_status = 'created'; - trigger = 'load__ds__code'; - } - return res; - }); - } + api_call.then((res) => { + if (res) { + $ae_sess.ds.submit_status = $lq__ds_obj?.id ? 'updated' : 'created'; + trigger = 'load__ds__code'; + show_edit = false; + } + return res; + }); } async function handle_delete() { - if ( - !$lq__ds_obj?.id || - !confirm('Are you sure you want to delete this data store?') - ) - return; - + if (!$lq__ds_obj?.id || !confirm('Are you sure you want to delete this data store?')) return; const api_cfg = untrack(() => $ae_api); - const res = await api.delete_ae_obj({ - api_cfg, - obj_type: 'data_store', - obj_id: $lq__ds_obj.id, - method: 'delete' - }); - + const res = await api.delete_ae_obj({ api_cfg, obj_type: 'data_store', obj_id: $lq__ds_obj.id, method: 'delete' }); if (res) { await db_core.data_store.delete($lq__ds_obj.id); ds_loading_status = 'not found'; @@ -398,19 +268,12 @@ async function handle_delete() { style={display ? `display: ${display}` : undefined}> {#if $lq__ds_obj} {#if debug || $ae_loc.debug === 'debug'} - Debug is ON! -
-    ID: {$lq__ds_obj.id}
-    Code: {$lq__ds_obj.code}
-    Name: {$lq__ds_obj.name}
-    Type: {$lq__ds_obj.type}
-    Account: {$lq__ds_obj.account_id || 'Global / NULL'}
-    Created: {$lq__ds_obj.created_on}
-    Updated: {$lq__ds_obj.updated_on}
-            
- -
+
+
+ Debug Info +
+
ID: {$lq__ds_obj.id} | Code: {$lq__ds_obj.code} | Type: {$lq__ds_obj.type} | Account: {$lq__ds_obj.account_id || 'Global'}
+
{/if} -
{ - e.preventDefault(); - handle_submit_form(e); - }}> - - + { e.preventDefault(); handle_submit_form(e); }}>
-
- - Account Specific + +
- Content - -
+
+ Content + {#if $lq__ds_obj.type === 'html'} +
+ + +
+ {/if} +
-
- Created on: {$lq__ds_obj.created_on} | Last Updated: {$lq__ds_obj.updated_on} + {#if $lq__ds_obj.type === 'html'} + {#if html_edit_mode === 'source'} + + {:else} + + {/if} + {:else if $lq__ds_obj.type === 'json' || $lq__ds_obj.type === 'sql' || $lq__ds_obj.type === 'md'} + + {:else} + + {/if}
-
+ class="btn preset-tonal-surface" + onclick={() => (show_edit = false)} + title="Discard changes and close"> + Cancel +
@@ -530,38 +394,36 @@ async function handle_delete() { {:else if $lq__ds_obj.type === 'text' && $lq__ds_obj.text}
{$lq__ds_obj.text}
{:else if $lq__ds_obj.type === 'sql' && $lq__ds_obj.text} - {#if debug}
- SQL: {$lq__ds_obj.text} -
{/if} + {#if debug}
SQL: {$lq__ds_obj.text}
{/if} {/if} {/if} {#if $ae_loc.edit_mode && ($ae_loc.manager_access || (show_edit_btn && $ae_loc.administrator_access))} {/if} {:else if ds_loading_status === 'not found'} - {#if $ae_loc.administrator_access || ($ae_loc.trusted_access && $ae_loc.edit_mode)} -
- Data Store not found: {ds_code} +
+ + Data Store not found: + {ds_code}
{/if} {/if} {#if ds_loading_status === 'loading'}
- +
{/if}
+ +