fix(core): modern Svelte 5 cleanup — Dexie .get() bug, typed API calls, inline confirms

- person_view.svelte: fix liveQuery using .get() (primary key, never set by V3)
  → .where('person_id').equals().first()
- people/[person_id]: same Dexie .get() fix for lq__person_obj
- person_view.svelte: replace 4x generic api.update_ae_obj → core_func.update_ae_obj__person
  (removes unused api import)
- Replace all browser confirm()/alert() dialogs (9 occurrences, 6 files) with
  inline two-click confirm state pattern (confirm_action = $state<string|null>)
  Affected: users, accounts, contacts, addresses, people, sites
- Bootstrap doc: add Dexie .get() trap to Section 5 and Mistake #8
This commit is contained in:
Scott Idem
2026-04-30 16:00:20 -04:00
parent 7be60c2b8b
commit 90adb19f5d
8 changed files with 270 additions and 98 deletions

View File

@@ -22,6 +22,8 @@ let account_id = $derived($page.params.account_id ?? '');
let account: any = $state(null);
let loading = $state(true);
let saving = $state(false);
let save_success = $state(false);
let confirm_action = $state<string | null>(null);
async function load_account() {
loading = true;
@@ -59,13 +61,18 @@ async function handle_save() {
});
if (result) {
alert('Account updated successfully');
save_success = true;
setTimeout(() => (save_success = false), 2500);
}
saving = false;
}
async function handle_delete() {
if (!confirm('Are you sure you want to disable this account?')) return;
if (confirm_action !== 'disable_account') {
confirm_action = 'disable_account';
return;
}
confirm_action = null;
const result = await delete_ae_obj_id__account({
api_cfg: $ae_api,
@@ -98,13 +105,33 @@ async function handle_delete() {
</h1>
</div>
</div>
<div class="flex gap-2">
<button
class="btn btn-sm preset-filled-error font-bold shadow-lg"
onclick={handle_delete}
disabled={loading || saving}>
<Trash2 size={16} class="mr-2" /> Disable
</button>
<div class="flex items-center gap-2">
{#if confirm_action === 'disable_account'}
<span class="text-sm font-bold text-red-600">Disable this account?</span>
<button
class="btn btn-sm preset-filled-error font-bold shadow-lg"
onclick={handle_delete}
disabled={loading || saving}>
<Trash2 size={16} class="mr-2" /> Confirm Disable
</button>
<button
class="btn btn-sm preset-tonal-surface"
onclick={() => (confirm_action = null)}>
Cancel
</button>
{:else}
<button
class="btn btn-sm preset-filled-error font-bold shadow-lg"
onclick={handle_delete}
disabled={loading || saving}>
<Trash2 size={16} class="mr-2" /> Disable
</button>
{/if}
{#if save_success}
<span class="flex items-center gap-1 text-sm font-bold text-green-600">
<Info size={16} /> Saved
</span>
{/if}
<button
class="btn btn-sm preset-filled-primary font-bold shadow-lg"
onclick={handle_save}

View File

@@ -31,6 +31,7 @@ let address_id = $derived($page.params.address_id ?? '');
let address: any = $state(null);
let loading = $state(true);
let is_editing = $state(false);
let confirm_action = $state<string | null>(null);
async function load_data() {
loading = true;
@@ -51,7 +52,11 @@ onMount(() => {
});
async function handle_delete() {
if (!confirm('Permanently delete this address?')) return;
if (confirm_action !== 'delete_address') {
confirm_action = 'delete_address';
return;
}
confirm_action = null;
await delete_ae_obj_id__address({
api_cfg: $ae_api,
address_id,
@@ -88,7 +93,7 @@ async function handle_delete() {
</div>
</div>
</div>
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
<button
class="btn btn-sm preset-tonal-secondary font-bold shadow-sm"
onclick={() => (is_editing = !is_editing)}
@@ -99,12 +104,27 @@ async function handle_delete() {
<Edit size={16} class="mr-2" /> Edit Mode
{/if}
</button>
<button
class="btn btn-sm preset-tonal-error font-bold shadow-sm"
onclick={handle_delete}
disabled={loading}>
<Trash2 size={16} class="mr-2" /> Delete
</button>
{#if confirm_action === 'delete_address'}
<span class="self-center text-sm font-bold text-red-600">Delete this address?</span>
<button
class="btn btn-sm preset-filled-error font-bold shadow-sm"
onclick={handle_delete}
disabled={loading}>
<Trash2 size={16} class="mr-2" /> Confirm Delete
</button>
<button
class="btn btn-sm preset-tonal-surface"
onclick={() => (confirm_action = null)}>
Cancel
</button>
{:else}
<button
class="btn btn-sm preset-tonal-error font-bold shadow-sm"
onclick={handle_delete}
disabled={loading}>
<Trash2 size={16} class="mr-2" /> Delete
</button>
{/if}
</div>
</header>

View File

@@ -32,6 +32,7 @@ let contact_id = $derived($page.params.contact_id ?? '');
let contact: any = $state(null);
let loading = $state(true);
let is_editing = $state(false);
let confirm_action = $state<string | null>(null);
async function load_data() {
loading = true;
@@ -52,7 +53,11 @@ onMount(() => {
});
async function handle_delete() {
if (!confirm('Permanently delete this contact?')) return;
if (confirm_action !== 'delete_contact') {
confirm_action = 'delete_contact';
return;
}
confirm_action = null;
await delete_ae_obj_id__contact({
api_cfg: $ae_api,
contact_id,
@@ -87,7 +92,7 @@ async function handle_delete() {
</div>
</div>
</div>
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
<button
class="btn btn-sm preset-tonal-secondary font-bold shadow-sm"
onclick={() => (is_editing = !is_editing)}
@@ -98,12 +103,27 @@ async function handle_delete() {
<Edit size={16} class="mr-2" /> Edit Mode
{/if}
</button>
<button
class="btn btn-sm preset-tonal-error font-bold shadow-sm"
onclick={handle_delete}
disabled={loading}>
<Trash2 size={16} class="mr-2" /> Delete
</button>
{#if confirm_action === 'delete_contact'}
<span class="self-center text-sm font-bold text-red-600">Delete this contact?</span>
<button
class="btn btn-sm preset-filled-error font-bold shadow-sm"
onclick={handle_delete}
disabled={loading}>
<Trash2 size={16} class="mr-2" /> Confirm Delete
</button>
<button
class="btn btn-sm preset-tonal-surface"
onclick={() => (confirm_action = null)}>
Cancel
</button>
{:else}
<button
class="btn btn-sm preset-tonal-error font-bold shadow-sm"
onclick={handle_delete}
disabled={loading}>
<Trash2 size={16} class="mr-2" /> Delete
</button>
{/if}
</div>
</header>

View File

@@ -70,8 +70,10 @@ $effect(() => {
});
let is_editing = $state(false);
let confirm_action = $state<string | null>(null);
let lq__person_obj = liveQuery(() => db_core.person.get($slct.person_id));
// .get() uses the primary key (id), which V3 never populates — always use .where().equals().first()
let lq__person_obj = liveQuery(() => db_core.person.where('person_id').equals($slct.person_id ?? '').first());
$slct.lq__person_obj = lq__person_obj;
let available_users: any[] = $state([]);
@@ -131,7 +133,11 @@ async function load_unlinked_users() {
}
async function handle_link_user(user_id: string) {
if (!confirm('Link this person to this user account?')) return;
if (confirm_action !== `link_user_${user_id}`) {
confirm_action = `link_user_${user_id}`;
return;
}
confirm_action = null;
const result = await update_ae_obj__person({
api_cfg: $ae_api,
@@ -147,7 +153,11 @@ async function handle_link_user(user_id: string) {
}
async function handle_unlink_user() {
if (!confirm('Unlink this person from their user account?')) return;
if (confirm_action !== 'unlink_user') {
confirm_action = 'unlink_user';
return;
}
confirm_action = null;
const result = await update_ae_obj__person({
api_cfg: $ae_api,
@@ -268,11 +278,25 @@ $ae_loc.person.show_content__person_page_help = false;
<span>User Account Linking</span>
</div>
{#if $lq__person_obj?.user_id}
<button
class="btn btn-sm preset-tonal-error"
onclick={handle_unlink_user}>
<Unlink size={14} class="mr-2" /> Unlink User
</button>
{#if confirm_action === 'unlink_user'}
<span class="self-center text-sm font-bold text-red-600">Unlink this user?</span>
<button
class="btn btn-sm preset-filled-error"
onclick={handle_unlink_user}>
<Unlink size={14} class="mr-2" /> Confirm Unlink
</button>
<button
class="btn btn-sm preset-tonal-surface"
onclick={() => (confirm_action = null)}>
Cancel
</button>
{:else}
<button
class="btn btn-sm preset-tonal-error"
onclick={handle_unlink_user}>
<Unlink size={14} class="mr-2" /> Unlink User
</button>
{/if}
{:else}
<button
class="btn btn-sm preset-tonal-primary"
@@ -323,28 +347,46 @@ $ae_loc.person.show_content__person_page_help = false;
<div
class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
{#each available_users as user (user.user_id)}
<button
class="card preset-tonal-primary hover:preset-filled-primary flex flex-col gap-1 p-3 text-left transition-all"
onclick={() =>
handle_link_user(user.user_id)}>
<span
class="flex items-center gap-2 font-bold">
<User size={14} />
{user.username}
</span>
<span class="truncate text-xs opacity-70"
>{user.email}</span>
<div class="mt-1 flex gap-1">
{#if user.super}<span
class="badge preset-filled-error text-[10px]"
>Super</span
>{/if}
{#if user.manager}<span
class="badge preset-filled-warning text-[10px]"
>Manager</span
>{/if}
{#if confirm_action === `link_user_${user.user_id}`}
<div class="card preset-tonal-warning flex flex-col gap-2 p-3">
<span class="text-sm font-bold">Link {user.username}?</span>
<div class="flex gap-2">
<button
class="btn btn-sm preset-filled-primary flex-1"
onclick={() => handle_link_user(user.user_id)}>
Confirm
</button>
<button
class="btn btn-sm preset-tonal-surface"
onclick={() => (confirm_action = null)}>
Cancel
</button>
</div>
</div>
</button>
{:else}
<button
class="card preset-tonal-primary hover:preset-filled-primary flex flex-col gap-1 p-3 text-left transition-all"
onclick={() =>
handle_link_user(user.user_id)}>
<span
class="flex items-center gap-2 font-bold">
<User size={14} />
{user.username}
</span>
<span class="truncate text-xs opacity-70"
>{user.email}</span>
<div class="mt-1 flex gap-1">
{#if user.super}<span
class="badge preset-filled-error text-[10px]"
>Super</span
>{/if}
{#if user.manager}<span
class="badge preset-filled-warning text-[10px]"
>Manager</span
>{/if}
</div>
</button>
{/if}
{/each}
</div>
{/if}

View File

@@ -14,8 +14,6 @@ import { liveQuery } from 'dexie';
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import Element_ae_obj_field_editor from '$lib/elements/element_ae_obj_field_editor.svelte';
import { api } from '$lib/api/api';
import { core_func } from '$lib/ae_core/ae_core_functions';
import {
ae_snip,
@@ -62,11 +60,9 @@ $effect(() => {
}
});
// .get() uses the primary key (id), which V3 never populates — always use .where().equals().first()
let lq__person_obj = $derived(
liveQuery(async () => {
let results = await db_core.person.get(person_id);
return results;
})
liveQuery(() => db_core.person.where('person_id').equals(person_id).first())
);
ae_tmp.value__data_json = null;
@@ -304,11 +300,10 @@ $effect(() => {
<button
type="button"
onclick={async () => {
await api.update_ae_obj({
await core_func.update_ae_obj__person({
api_cfg: $ae_api,
obj_type: 'person',
obj_id: $lq__person_obj?.person_id,
fields: { hide: !$lq__person_obj?.hide }
person_id: $lq__person_obj?.person_id,
data_kv: { hide: !$lq__person_obj?.hide }
});
core_func.load_ae_obj_id__person({
api_cfg: $ae_api,
@@ -337,11 +332,10 @@ $effect(() => {
<button
type="button"
onclick={async () => {
await api.update_ae_obj({
await core_func.update_ae_obj__person({
api_cfg: $ae_api,
obj_type: 'person',
obj_id: $lq__person_obj?.person_id,
fields: { enable: !$lq__person_obj?.enable }
person_id: $lq__person_obj?.person_id,
data_kv: { enable: !$lq__person_obj?.enable }
});
core_func.load_ae_obj_id__person({
api_cfg: $ae_api,
@@ -373,11 +367,10 @@ $effect(() => {
<button
type="button"
onclick={async () => {
await api.update_ae_obj({
await core_func.update_ae_obj__person({
api_cfg: $ae_api,
obj_type: 'person',
obj_id: $lq__person_obj?.person_id,
fields: { priority: !$lq__person_obj?.priority }
person_id: $lq__person_obj?.person_id,
data_kv: { priority: !$lq__person_obj?.priority }
});
core_func.load_ae_obj_id__person({
api_cfg: $ae_api,
@@ -404,13 +397,10 @@ $effect(() => {
<button
type="button"
onclick={async () => {
await api.update_ae_obj({
await core_func.update_ae_obj__person({
api_cfg: $ae_api,
obj_type: 'person',
obj_id: $lq__person_obj?.person_id,
fields: {
allow_auth_key: !$lq__person_obj?.allow_auth_key
}
person_id: $lq__person_obj?.person_id,
data_kv: { allow_auth_key: !$lq__person_obj?.allow_auth_key }
});
core_func.load_ae_obj_id__person({
api_cfg: $ae_api,

View File

@@ -34,6 +34,7 @@ let site: any = $state(null);
let domain_li: any[] = $state([]);
let loading = $state(true);
let saving = $state(false);
let confirm_action = $state<string | null>(null);
async function load_data() {
loading = true;
@@ -106,7 +107,11 @@ async function handle_toggle_domain(dom: any) {
}
async function handle_delete_domain(dom: any) {
if (!confirm(`Remove domain ${dom.fqdn}?`)) return;
if (confirm_action !== `delete_domain_${dom.site_domain_id}`) {
confirm_action = `delete_domain_${dom.site_domain_id}`;
return;
}
confirm_action = null;
await delete_ae_obj_id__site_domain({
api_cfg: $ae_api,
site_id,
@@ -297,7 +302,7 @@ async function handle_delete_domain(dom: any) {
: 'Disabled'}
</span>
</div>
<div class="flex gap-1">
<div class="flex items-center gap-1">
<button
class="btn btn-icon btn-sm {dom.enable
? 'preset-tonal-success'
@@ -307,13 +312,29 @@ async function handle_delete_domain(dom: any) {
title="Toggle Active">
<Save size={14} />
</button>
<button
class="btn btn-icon btn-sm preset-tonal-error"
onclick={() =>
handle_delete_domain(dom)}
title="Delete Domain">
<Trash2 size={14} />
</button>
{#if confirm_action === `delete_domain_${dom.site_domain_id}`}
<span class="text-xs font-bold text-red-600">Remove {dom.fqdn}?</span>
<button
class="btn btn-icon btn-sm preset-filled-error"
onclick={() => handle_delete_domain(dom)}
title="Confirm Delete">
<Trash2 size={14} />
</button>
<button
class="btn btn-icon btn-sm preset-tonal-surface"
onclick={() => (confirm_action = null)}
title="Cancel">
</button>
{:else}
<button
class="btn btn-icon btn-sm preset-tonal-error"
onclick={() =>
handle_delete_domain(dom)}
title="Delete Domain">
<Trash2 size={14} />
</button>
{/if}
</div>
</div>
{/each}

View File

@@ -30,6 +30,8 @@ interface Props {
let { data }: Props = $props();
let user = $state(untrack(() => data.user));
let saving = $state(false);
let save_success = $state(false);
let confirm_action = $state<string | null>(null);
$effect(() => {
user = data.user;
@@ -58,13 +60,18 @@ async function handle_save() {
});
if (result) {
alert('User updated successfully');
save_success = true;
setTimeout(() => (save_success = false), 2500);
}
saving = false;
}
async function handle_delete() {
if (!confirm('Are you sure you want to disable this user account?')) return;
if (confirm_action !== 'disable_user') {
confirm_action = 'disable_user';
return;
}
confirm_action = null;
const result = await delete_ae_obj_id__user({
api_cfg: $ae_api,
@@ -104,13 +111,33 @@ async function handle_delete() {
</div>
</div>
</div>
<div class="flex gap-2">
<button
class="btn btn-sm preset-filled-error font-bold shadow-lg"
onclick={handle_delete}
disabled={saving}>
<Trash2 size={16} class="mr-2" /> Disable
</button>
<div class="flex items-center gap-2">
{#if confirm_action === 'disable_user'}
<span class="text-sm font-bold text-red-600">Disable this user?</span>
<button
class="btn btn-sm preset-filled-error font-bold shadow-lg"
onclick={handle_delete}
disabled={saving}>
<Trash2 size={16} class="mr-2" /> Confirm Disable
</button>
<button
class="btn btn-sm preset-tonal-surface"
onclick={() => (confirm_action = null)}>
Cancel
</button>
{:else}
<button
class="btn btn-sm preset-filled-error font-bold shadow-lg"
onclick={handle_delete}
disabled={saving}>
<Trash2 size={16} class="mr-2" /> Disable
</button>
{/if}
{#if save_success}
<span class="flex items-center gap-1 text-sm font-bold text-green-600">
<CircleCheck size={16} /> Saved
</span>
{/if}
<button
class="btn btn-sm preset-filled-primary font-bold shadow-lg"
onclick={handle_save}