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:
@@ -179,7 +179,7 @@ async function load_ae_obj_id__my_obj({ api_cfg, obj_id }) {
|
||||
|
||||
### ID convention — never use `_id_random` fields
|
||||
The V3 API uses random string IDs (e.g. `event_file_id = "aBc123"`). The `*_id_random`
|
||||
fields are legacy aliases. Always use the short form:
|
||||
fields are legacy aliases. The integer version of the ID is never returned by the API. Always use the short form:
|
||||
```ts
|
||||
// ✅ Correct
|
||||
event_file_obj.event_file_id
|
||||
@@ -187,6 +187,7 @@ event_file_obj.event_file_id
|
||||
// ❌ Wrong — legacy alias, don't use
|
||||
event_file_obj.event_file_id_random
|
||||
```
|
||||
The short ".id" is also the randomized string, **not an integer** (autonum).
|
||||
|
||||
### PATCH — only field values in the body
|
||||
```ts
|
||||
@@ -205,6 +206,23 @@ x-aether-api-key: <PUBLIC_AE_API_SECRET_KEY>
|
||||
x-account-id: <account_id>
|
||||
```
|
||||
|
||||
### Dexie queries — always use the object ID index, not `.get()`
|
||||
All `db_core` (and other module) Dexie tables define their schema with `id` as the first
|
||||
field (primary key), followed by the object's string ID (e.g. `person_id`). V3 **never**
|
||||
returns `id`, so every record stored in Dexie has `id = undefined`. Calling `.get(value)`
|
||||
does a primary key lookup — it will always miss when passed a string object ID.
|
||||
|
||||
```ts
|
||||
// ❌ Wrong — .get() uses the primary key (id), which V3 never populates:
|
||||
liveQuery(() => db_core.person.get(person_id))
|
||||
|
||||
// ✅ Correct — use .where() on the indexed object ID field:
|
||||
liveQuery(() => db_core.person.where('person_id').equals(person_id).first())
|
||||
```
|
||||
|
||||
This applies to every table in every module (`db_core`, `db_events`, etc.).
|
||||
When looking up a single object by its string ID, always use `.where().equals().first()`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Naming Conventions
|
||||
@@ -253,7 +271,14 @@ These are real incidents — know them before you start.
|
||||
6. **Deleting files with `rm`** — always move to `~/tmp/agents_trash`. A deleted file may
|
||||
contain context that's not recoverable from git if it was gitignored.
|
||||
|
||||
7. **Treating `$effect` blocks as auth bypass risks** — a `$effect` inside a child
|
||||
8. **Dexie `.get()` with a string object ID returns `undefined`** — Dexie `.get(value)`
|
||||
looks up by the table's **primary key**, which is `id` (the first schema field). The V3
|
||||
API never returns `id`, so it is always `undefined` in stored records. Passing a string
|
||||
object ID (e.g. `person_id`) to `.get()` will silently return nothing. Always use
|
||||
`.where('person_id').equals(person_id).first()` instead. This has caused liveQuery
|
||||
blocks to always produce `undefined` even when the record exists in Dexie.
|
||||
|
||||
9. **Treating `$effect` blocks as auth bypass risks** — a `$effect` inside a child
|
||||
component cannot bypass a parent `+layout.svelte` auth gate. Children only mount if
|
||||
the parent calls `{@render children?.()}`. Adding redundant auth guards to `$effect`
|
||||
blocks that can only run after the parent gate already passed is unnecessary — and
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user