feat(core): add Data Stores management page at /core/data_stores

Full CRUD for all data_store records: search by code, account_id,
for_type, for_id; create/edit/delete via modal with type-aware content
editor (CodeMirror / TipTap / textarea). Wired into core nav and
dashboard. for_id shown read-only on edit (DB integer FK constraint).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-17 13:55:17 -04:00
parent 7941c07a88
commit 34736c05a0
4 changed files with 767 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ import { goto } from '$app/navigation';
import { ae_loc, ae_sess, ae_api, slct } from '$lib/stores/ae_stores'; import { ae_loc, ae_sess, ae_api, slct } from '$lib/stores/ae_stores';
import { import {
Building, Building,
Database,
Globe, Globe,
History, History,
LayoutDashboard, LayoutDashboard,
@@ -71,6 +72,9 @@ onMount(() => {
<a href="/core/lookups" class="btn btn-sm preset-tonal-surface"> <a href="/core/lookups" class="btn btn-sm preset-tonal-surface">
<List size={14} class="mr-1" /> Lookups <List size={14} class="mr-1" /> Lookups
</a> </a>
<a href="/core/data_stores" class="btn btn-sm preset-tonal-surface">
<Database size={14} class="mr-1" /> Data Stores
</a>
</nav> </nav>
<section class="main_content grow px-1 pb-28 md:px-2"> <section class="main_content grow px-1 pb-28 md:px-2">

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { import {
Building, Building,
Database,
Globe, Globe,
History, History,
Landmark, Landmark,
@@ -213,6 +214,28 @@ $effect(() => {
</a> </a>
</div> </div>
<!-- Data Stores Card -->
<div
class="card preset-tonal-surface group border-surface-500/10 flex flex-col justify-between space-y-4 border p-6 shadow-lg transition-all hover:brightness-110">
<div class="space-y-4">
<div class="flex items-center gap-3">
<div
class="bg-surface-500/20 rounded-lg p-2 transition-transform group-hover:scale-110">
<Database size={24} />
</div>
<h3 class="h4 font-black">Data Stores</h3>
</div>
<p class="text-sm leading-relaxed opacity-80">
Manage all content and configuration data stores across accounts.
</p>
</div>
<a
class="btn preset-filled-surface mt-4 w-full font-bold shadow-md"
href="/core/data_stores">
Manage
</a>
</div>
<!-- Lookups Card --> <!-- Lookups Card -->
<div <div
class="card preset-tonal-surface group border-surface-500/10 flex flex-col justify-between space-y-4 border p-6 shadow-lg transition-all hover:brightness-110"> class="card preset-tonal-surface group border-surface-500/10 flex flex-col justify-between space-y-4 border p-6 shadow-lg transition-all hover:brightness-110">

View File

@@ -0,0 +1,735 @@
<script lang="ts">
import { onMount } from 'svelte';
import { untrack } from 'svelte';
import { goto } from '$app/navigation';
import { Modal } from 'flowbite-svelte';
import {
Check,
ChevronLeft,
ChevronRight,
Code,
Database,
Eye,
Filter,
LoaderCircle,
Plus,
RefreshCw,
Save,
Search,
Trash2,
X
} from '@lucide/svelte';
import { api } from '$lib/api/api';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { db_core } from '$lib/ae_core/db_core';
import { ae_util } from '$lib/ae_utils/ae_utils';
import type { ae_DataStore } from '$lib/types/ae_types';
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
import AE_Comp_Editor_TipTap from '$lib/elements/element_editor_tiptap.svelte';
onMount(() => {
if (!$ae_loc.manager_access) {
goto('/core');
}
});
// ── Filter state ──────────────────────────────────────────────────────────────
let qry_code = $state('');
let qry_account_id = $state('');
let qry_for_type = $state('');
let qry_for_id = $state('');
let qry_enabled = $state<'all' | 'enabled' | 'not_enabled'>('all');
let page_limit = $state(50);
let page_offset = $state(0);
// ── Results ───────────────────────────────────────────────────────────────────
let results: ae_DataStore[] = $state([]);
let loading = $state(false);
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 html_edit_mode = $state<'source' | 'visual'>('source');
let submit_status = $state<'idle' | 'processing' | 'saved' | 'error'>('idle');
// ── Search ────────────────────────────────────────────────────────────────────
async function do_search(reset = true) {
if (reset) page_offset = 0;
loading = true;
const search_query: any = { and: [] };
if (qry_code.trim()) {
search_query.and.push({ field: 'code', op: 'like', value: qry_code.trim() });
}
if (qry_account_id.trim() === 'global') {
search_query.and.push({ field: 'account_id', op: 'is', value: null });
} else if (qry_account_id.trim()) {
search_query.and.push({ field: 'account_id_random', op: 'eq', value: qry_account_id.trim() });
}
if (qry_for_type.trim()) {
search_query.and.push({ field: 'for_type', op: 'like', value: qry_for_type.trim() });
}
if (qry_for_id.trim()) {
search_query.and.push({ field: 'for_id_random', op: 'eq', value: qry_for_id.trim() });
}
if (qry_enabled === 'enabled') {
search_query.and.push({ field: 'enable', op: 'eq', value: true });
} else if (qry_enabled === 'not_enabled') {
search_query.and.push({ field: 'enable', op: 'eq', value: false });
}
const result_li = await api.search_ae_obj({
api_cfg: $ae_api,
obj_type: 'data_store',
search_query,
order_by_li: [{ code: 'ASC' }, { updated_on: 'DESC' }],
limit: page_limit,
offset: page_offset,
log_lvl: 0
});
results = result_li ?? [];
searched = true;
loading = false;
}
function clear_filters() {
qry_code = '';
qry_account_id = '';
qry_for_type = '';
qry_for_id = '';
qry_enabled = 'all';
results = [];
searched = false;
}
// ── Open edit ─────────────────────────────────────────────────────────────────
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 ?? '');
html_edit_mode = 'source';
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 = '';
html_edit_mode = 'source';
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);
}
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function type_badge(type: string | null | undefined) {
switch (type) {
case 'json': return 'preset-tonal-warning';
case 'html': return 'preset-tonal-primary';
case 'sql': return 'preset-tonal-error';
case 'md': return 'preset-tonal-success';
default: return 'preset-tonal-surface';
}
}
function fmt_date(val: string | Date | null | undefined) {
if (!val) return '—';
return ae_util.iso_datetime_formatter(val as any, 'datetime_12_short');
}
function content_preview(ds: ae_DataStore): string {
if (ds.type === 'json') return ds.json ? JSON.stringify(ds.json).slice(0, 60) : '';
return (ds.text ?? ds.html ?? '').slice(0, 60).replace(/\s+/g, ' ');
}
</script>
<div class="container mx-auto space-y-6 p-4">
<!-- ── Header ──────────────────────────────────────────────────────────── -->
<header class="bg-surface-50-900-token border-surface-500/10 flex flex-wrap items-center justify-between gap-4 rounded-xl border p-4 shadow-lg">
<div class="flex items-center gap-3">
<div class="bg-primary-500/10 rounded-lg p-2">
<Database size={24} class="text-primary-500" />
</div>
<div>
<h1 class="h2 font-black tracking-tight">Data Stores</h1>
<p class="text-xs font-bold tracking-widest uppercase opacity-50">Content &amp; Configuration Storage</p>
</div>
</div>
<button
type="button"
onclick={open_new}
class="btn preset-filled-primary font-bold shadow-lg"
class:hidden={!$ae_loc.manager_access}>
<Plus size={16} class="mr-2" /> New Data Store
</button>
</header>
<!-- ── Filter bar ──────────────────────────────────────────────────────── -->
<div class="card preset-tonal-surface border-surface-500/10 space-y-3 border p-4 shadow-xl">
<div class="flex items-center gap-2 text-[10px] font-bold tracking-widest uppercase opacity-50">
<Filter size={11} /> Filters
</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</label>
<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>
<div class="space-y-1">
<label class="text-xs font-bold opacity-70">
Account ID
<span class="font-normal opacity-60">— type <code>global</code> for null</span>
</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()} />
</div>
<div class="space-y-1">
<label class="text-xs font-bold opacity-70">For Type</label>
<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>
<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>
</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>
<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>
<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>
<div class="flex flex-1 items-end justify-end gap-2">
<button
type="button"
onclick={clear_filters}
class="btn btn-sm preset-tonal-surface text-xs"
title="Clear all filters">
<X size={13} class="mr-1" /> Clear
</button>
<button
type="button"
onclick={() => do_search()}
class="btn preset-filled-primary"
disabled={loading}>
{#if loading}
<LoaderCircle size={16} class="mr-2 animate-spin" />
{:else}
<Search size={16} class="mr-2" />
{/if}
Search
</button>
</div>
</div>
</div>
<!-- ── Results ─────────────────────────────────────────────────────────── -->
{#if results.length > 0}
<div class="card preset-tonal-surface border-surface-500/10 border shadow-xl">
<!-- table header row -->
<div class="border-surface-500/20 flex items-center justify-between border-b px-4 py-2">
<span class="text-xs font-bold opacity-60">
{results.length} record{results.length !== 1 ? 's' : ''}
{#if results.length === page_limit}
<span class="opacity-50"> (may be more — increase per-page or paginate)</span>
{/if}
</span>
<div class="flex items-center gap-2">
{#if page_offset > 0}
<button type="button" class="btn btn-sm preset-tonal-surface"
onclick={() => { page_offset = Math.max(0, page_offset - page_limit); do_search(false); }}>
<ChevronLeft size={14} />
</button>
{/if}
{#if results.length === page_limit}
<button type="button" class="btn btn-sm preset-tonal-surface"
onclick={() => { page_offset += page_limit; do_search(false); }}>
<ChevronRight size={14} />
</button>
{/if}
<button type="button" onclick={() => do_search(false)} class="btn btn-sm preset-tonal-surface" title="Refresh">
<RefreshCw size={13} />
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-xs">
<thead>
<tr class="border-surface-500/20 border-b text-left text-[10px] font-bold uppercase tracking-wider opacity-50">
<th class="px-3 py-2">Code</th>
<th class="px-3 py-2">Name</th>
<th class="px-3 py-2">Type</th>
<th class="px-3 py-2">Account</th>
<th class="px-3 py-2">For</th>
<th class="px-3 py-2 hidden md:table-cell">Preview</th>
<th class="px-3 py-2 hidden lg:table-cell">Updated</th>
<th class="px-3 py-2"></th>
</tr>
</thead>
<tbody>
{#each results as ds (ds.id ?? ds.data_store_id)}
<tr
class="border-surface-500/10 hover:bg-surface-500/5 cursor-pointer border-b transition-colors"
onclick={() => open_edit(ds)}>
<td class="px-3 py-2">
<span class="font-mono" class:opacity-40={!ds.enable}>{ds.code}</span>
{#if !ds.enable}
<span class="badge preset-tonal-error ml-1 text-[9px]">off</span>
{/if}
{#if ds.hide}
<span class="badge preset-tonal-surface ml-1 text-[9px]">hidden</span>
{/if}
</td>
<td class="max-w-40 truncate px-3 py-2">{ds.name ?? '—'}</td>
<td class="px-3 py-2">
<span class="badge {type_badge(ds.type)} font-mono text-[9px] uppercase">{ds.type ?? '?'}</span>
</td>
<td class="px-3 py-2 font-mono opacity-60">
{#if ds.account_id}
<span title={ds.account_id}>{ds.account_id.slice(0, 8)}…</span>
{:else}
<span class="italic opacity-50">global</span>
{/if}
</td>
<td class="px-3 py-2 font-mono opacity-60">
{#if ds.for_type}
{ds.for_type}
{#if ds.for_id}
<span class="opacity-50"> / {ds.for_id.slice(0, 6)}</span>
{/if}
{:else}
{/if}
</td>
<td class="hidden max-w-48 truncate px-3 py-2 opacity-40 md:table-cell">
{content_preview(ds)}
</td>
<td class="hidden px-3 py-2 opacity-50 lg:table-cell whitespace-nowrap">
{fmt_date(ds.updated_on)}
</td>
<td class="px-3 py-2">
<button
type="button"
class="btn btn-sm preset-tonal-primary text-xs"
onclick={(e) => { e.stopPropagation(); open_edit(ds); }}>
Edit
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{:else if searched && !loading}
<div class="py-12 text-center opacity-50">
<Database size={48} class="mx-auto mb-4 opacity-30" />
<p class="text-lg font-bold">No data stores found</p>
<p class="text-sm">Try different filters, or create one above.</p>
</div>
{:else if !searched}
<div class="py-12 text-center opacity-40">
<Search size={40} class="mx-auto mb-3 opacity-30" />
<p class="text-sm">Enter filters and press Search, or just press Search to list all.</p>
</div>
{/if}
</div>
<!-- ── Edit / Create Modal ───────────────────────────────────────────────────── -->
<Modal
title={is_new ? 'New Data Store' : `Edit: ${editing_obj?.code ?? ''}`}
bind:open={show_edit}
autoclose={false}
outsideclose={submit_status !== 'processing'}
size="xl"
class="w-full max-w-5xl">
<form
class="space-y-4"
onsubmit={(e) => { e.preventDefault(); handle_save(); }}>
<!-- Code + Name -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<label class="label space-y-1">
<span class="text-xs font-bold opacity-70">Code <span class="text-error-500">*</span></span>
<input
type="text"
class="input font-mono text-sm"
bind:value={draft_code}
required
placeholder="my_data_store_code" />
</label>
<label class="label space-y-1">
<span class="text-xs font-bold opacity-70">Name <span class="text-error-500">*</span></span>
<input
type="text"
class="input"
bind:value={draft_name}
required
placeholder="Human-readable name" />
</label>
</div>
<!-- Type + Account ID -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<label class="label space-y-1">
<span class="text-xs font-bold opacity-70">Type</span>
<select class="select" bind:value={draft_type}>
<option value="text">Text</option>
<option value="html">HTML</option>
<option value="json">JSON</option>
<option value="md">Markdown</option>
<option value="sql">SQL</option>
</select>
</label>
<label class="label space-y-1">
<span class="text-xs font-bold opacity-70">
Account ID
<span class="font-normal opacity-60">— blank = global</span>
</span>
<input
type="text"
class="input font-mono text-sm"
bind:value={draft_account_id}
placeholder="Leave blank for global (no account)" />
</label>
</div>
<!-- For Type + For ID -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<label class="label space-y-1">
<span class="text-xs font-bold opacity-70">
For Type
<span class="font-normal opacity-60">— polymorphic parent object type</span>
</span>
<input
type="text"
class="input font-mono text-sm"
bind:value={draft_for_type}
placeholder="event, event_session, person, journal_entry…" />
</label>
<div class="space-y-1">
<span class="text-xs font-bold opacity-70">
For ID
<span class="font-normal opacity-60">— random string ID of parent</span>
</span>
{#if is_new}
<input
type="text"
class="input font-mono text-sm"
bind:value={draft_for_id}
placeholder="random string ID" />
{:else}
<!-- for_id is stored as an integer FK in DB; backend resolves string→int on create
but does not accept the string on PATCH. Shown read-only here. -->
<div class="input bg-surface-200-700-token flex items-center font-mono text-sm opacity-70">
{#if editing_obj?.for_id}
{editing_obj.for_id}
{:else}
<span class="italic opacity-50">not set</span>
{/if}
</div>
<p class="text-[10px] opacity-50">
For ID is set at creation time. To change it, use phpMyAdmin (DB stores the integer FK).
</p>
{/if}
</div>
</div>
<!-- Flags row -->
<div class="border-surface-500/20 flex flex-wrap items-center gap-x-6 gap-y-2 rounded-lg border bg-black/5 px-4 py-3">
<label class="flex cursor-pointer items-center gap-2 text-sm">
<input type="checkbox" class="checkbox" bind:checked={draft_enable} />
Enable
</label>
<label class="flex cursor-pointer items-center gap-2 text-sm">
<input type="checkbox" class="checkbox" bind:checked={draft_hide} />
Hide
</label>
<label class="flex cursor-pointer items-center gap-2 text-sm">
<input type="checkbox" class="checkbox" bind:checked={draft_priority} />
Priority
</label>
<label class="flex items-center gap-2 text-sm">
<span class="opacity-70 shrink-0">Sort:</span>
<input type="text" class="input input-sm w-20 font-mono text-xs" bind:value={draft_sort} placeholder="0" />
</label>
<label class="flex items-center gap-2 text-sm">
<span class="opacity-70 shrink-0">Group:</span>
<input type="text" class="input input-sm w-28 font-mono text-xs" bind:value={draft_group} placeholder="group" />
</label>
</div>
<!-- Notes -->
<label class="label space-y-1">
<span class="text-xs font-bold opacity-70">Notes</span>
<input
type="text"
class="input text-sm"
bind:value={draft_notes}
placeholder="Optional notes" />
</label>
<!-- Content editor -->
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-xs font-bold opacity-70">Content</span>
{#if draft_type === 'html'}
<div class="flex items-center gap-1 rounded bg-black/5 p-0.5">
<button
type="button"
class="flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-bold uppercase transition-all"
class:bg-primary-500={html_edit_mode === 'source'}
class:text-white={html_edit_mode === 'source'}
class:opacity-50={html_edit_mode !== 'source'}
onclick={() => (html_edit_mode = 'source')}
title="Edit raw HTML source">
<Code size="10" /> Source
</button>
<button
type="button"
class="flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-bold uppercase transition-all"
class:bg-primary-500={html_edit_mode === 'visual'}
class:text-white={html_edit_mode === 'visual'}
class:opacity-50={html_edit_mode !== 'visual'}
onclick={() => (html_edit_mode = 'visual')}
title="Visual / WYSIWYG editor">
<Eye size="10" /> Visual
</button>
</div>
{/if}
</div>
{#if draft_type === 'html'}
{#if html_edit_mode === 'source'}
<AE_Comp_Editor_CodeMirror bind:content={draft_value} placeholder="Enter HTML source…" />
{:else}
<AE_Comp_Editor_TipTap bind:content={draft_value} placeholder="Enter HTML content…" />
{/if}
{:else if draft_type === 'json' || draft_type === 'sql' || draft_type === 'md'}
<AE_Comp_Editor_CodeMirror bind:content={draft_value} placeholder="Enter {draft_type.toUpperCase()} content…" />
{:else}
<textarea
bind:value={draft_value}
class="textarea font-mono text-sm"
rows="12"
placeholder="Enter text content…"></textarea>
{/if}
</div>
<!-- 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"
disabled={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>

View File

@@ -0,0 +1,5 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
return {};
};