feat(data-stores): bulk rename, wildcard hint, theme fixes
- Add Bulk Rename panel: code filter (% wildcard), find/replace text, preview table before apply, sequential PATCH with IDB cache update - Fix Code filter label/placeholder to show % wildcard syntax - Add note that API results are scoped to the active account (backend behavior) - Replace bg-black/5 with bg-surface-500/10 for light/dark compatibility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { untrack } from 'svelte';
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { Modal } from 'flowbite-svelte';
|
import { Modal } from 'flowbite-svelte';
|
||||||
import {
|
import {
|
||||||
|
ArrowRight,
|
||||||
Check,
|
Check,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
Eye,
|
Eye,
|
||||||
Filter,
|
Filter,
|
||||||
LoaderCircle,
|
LoaderCircle,
|
||||||
|
Pencil,
|
||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Save,
|
Save,
|
||||||
@@ -69,6 +71,16 @@ let draft_notes = $state('');
|
|||||||
let html_edit_mode = $state<'source' | 'visual'>('source');
|
let html_edit_mode = $state<'source' | 'visual'>('source');
|
||||||
let submit_status = $state<'idle' | 'processing' | 'saved' | 'error'>('idle');
|
let submit_status = $state<'idle' | 'processing' | 'saved' | 'error'>('idle');
|
||||||
|
|
||||||
|
// ── Bulk rename state ─────────────────────────────────────────────────────────
|
||||||
|
let show_bulk_rename = $state(false);
|
||||||
|
let rename_filter = $state(''); // code LIKE filter — use % as wildcard
|
||||||
|
let rename_find_text = $state(''); // literal substring to find within matching codes
|
||||||
|
let rename_replace_text = $state(''); // literal replacement for find_text
|
||||||
|
let rename_preview: { id: string; old_code: string; new_code: string }[] = $state([]);
|
||||||
|
let rename_loading = $state(false);
|
||||||
|
let rename_apply_status = $state<'idle' | 'applying' | 'done' | 'error'>('idle');
|
||||||
|
let rename_applied = $state(0);
|
||||||
|
|
||||||
// ── Search ────────────────────────────────────────────────────────────────────
|
// ── Search ────────────────────────────────────────────────────────────────────
|
||||||
async function do_search(reset = true) {
|
async function do_search(reset = true) {
|
||||||
if (reset) page_offset = 0;
|
if (reset) page_offset = 0;
|
||||||
@@ -258,6 +270,67 @@ async function handle_delete() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Bulk rename ───────────────────────────────────────────────────────────────
|
||||||
|
async function do_rename_preview() {
|
||||||
|
if (!rename_filter.trim()) return;
|
||||||
|
rename_loading = true;
|
||||||
|
rename_preview = [];
|
||||||
|
rename_apply_status = 'idle';
|
||||||
|
rename_applied = 0;
|
||||||
|
|
||||||
|
const result_li = await api.search_ae_obj({
|
||||||
|
api_cfg: $ae_api,
|
||||||
|
obj_type: 'data_store',
|
||||||
|
search_query: { and: [{ field: 'code', op: 'like', value: rename_filter.trim() }] },
|
||||||
|
order_by_li: [{ code: 'ASC' }],
|
||||||
|
limit: 200,
|
||||||
|
offset: 0,
|
||||||
|
log_lvl: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result_li) {
|
||||||
|
const find = rename_find_text.trim();
|
||||||
|
const repl = rename_replace_text.trim();
|
||||||
|
rename_preview = result_li
|
||||||
|
.map((ds: ae_DataStore) => {
|
||||||
|
const old_code = ds.code ?? '';
|
||||||
|
const new_code = find ? old_code.split(find).join(repl) : old_code;
|
||||||
|
return { id: (ds.id ?? (ds as any).data_store_id) as string, old_code, new_code };
|
||||||
|
})
|
||||||
|
.filter((r: { id: string; old_code: string; new_code: string }) => r.old_code !== r.new_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
rename_loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function do_rename_apply() {
|
||||||
|
if (!rename_preview.length) return;
|
||||||
|
if (!confirm(`Rename ${rename_preview.length} data store code(s)?\n\nThis cannot be undone.`)) return;
|
||||||
|
|
||||||
|
rename_apply_status = 'applying';
|
||||||
|
rename_applied = 0;
|
||||||
|
const api_cfg = untrack(() => $ae_api);
|
||||||
|
|
||||||
|
for (const item of rename_preview) {
|
||||||
|
const result = await api.update_ae_obj({
|
||||||
|
api_cfg,
|
||||||
|
obj_type: 'data_store',
|
||||||
|
obj_id: item.id,
|
||||||
|
fields: { code: item.new_code }
|
||||||
|
});
|
||||||
|
if (result) {
|
||||||
|
rename_applied++;
|
||||||
|
await db_core.data_store.update(item.id, { code: item.new_code });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rename_apply_status = rename_applied === rename_preview.length ? 'done' : 'error';
|
||||||
|
if (rename_apply_status === 'done') {
|
||||||
|
rename_preview = [];
|
||||||
|
do_search(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
function type_badge(type: string | null | undefined) {
|
function type_badge(type: string | null | undefined) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -293,6 +366,14 @@ function content_preview(ds: ae_DataStore): string {
|
|||||||
<p class="text-xs font-bold tracking-widest uppercase opacity-50">Content & Configuration Storage</p>
|
<p class="text-xs font-bold tracking-widest uppercase opacity-50">Content & Configuration Storage</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => { show_bulk_rename = !show_bulk_rename; }}
|
||||||
|
class="btn preset-tonal-surface font-bold"
|
||||||
|
title="Bulk rename data store codes">
|
||||||
|
<Pencil size={14} class="mr-1" /> Bulk Rename
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={open_new}
|
onclick={open_new}
|
||||||
@@ -300,6 +381,7 @@ function content_preview(ds: ae_DataStore): string {
|
|||||||
class:hidden={!$ae_loc.manager_access}>
|
class:hidden={!$ae_loc.manager_access}>
|
||||||
<Plus size={16} class="mr-2" /> New Data Store
|
<Plus size={16} class="mr-2" /> New Data Store
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- ── Filter bar ──────────────────────────────────────────────────────── -->
|
<!-- ── Filter bar ──────────────────────────────────────────────────────── -->
|
||||||
@@ -310,18 +392,21 @@ function content_preview(ds: ae_DataStore): string {
|
|||||||
|
|
||||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<label class="text-xs font-bold opacity-70">Code</label>
|
<label class="text-xs font-bold opacity-70">
|
||||||
|
Code
|
||||||
|
<span class="font-normal opacity-60">— use <code>%</code> as wildcard</span>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
bind:value={qry_code}
|
bind:value={qry_code}
|
||||||
placeholder="event__pres_mgmt__*"
|
placeholder="event__pres_mgmt__%"
|
||||||
class="input input-sm w-full font-mono text-xs"
|
class="input input-sm w-full font-mono text-xs"
|
||||||
onkeydown={(e) => e.key === 'Enter' && do_search()} />
|
onkeydown={(e) => e.key === 'Enter' && do_search()} />
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<label class="text-xs font-bold opacity-70">
|
<label class="text-xs font-bold opacity-70">
|
||||||
Account ID
|
Account ID
|
||||||
<span class="font-normal opacity-60">— type <code>global</code> for null</span>
|
<span class="font-normal opacity-60">— <code>global</code> for null</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
@@ -329,6 +414,7 @@ function content_preview(ds: ae_DataStore): string {
|
|||||||
placeholder="random ID or 'global'"
|
placeholder="random ID or 'global'"
|
||||||
class="input input-sm w-full font-mono text-xs"
|
class="input input-sm w-full font-mono text-xs"
|
||||||
onkeydown={(e) => e.key === 'Enter' && do_search()} />
|
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>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<label class="text-xs font-bold opacity-70">For Type</label>
|
<label class="text-xs font-bold opacity-70">For Type</label>
|
||||||
@@ -392,6 +478,104 @@ function content_preview(ds: ae_DataStore): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Bulk Rename Panel ─────────────────────────────────────────────────── -->
|
||||||
|
{#if show_bulk_rename}
|
||||||
|
<div class="card border-surface-500/10 space-y-4 border p-4 shadow-xl preset-tonal-surface">
|
||||||
|
<div class="flex items-center gap-2 text-[10px] font-bold tracking-widest uppercase opacity-50">
|
||||||
|
<Pencil size={11} /> Bulk Rename Codes
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={rename_replace_text}
|
||||||
|
placeholder="new_code"
|
||||||
|
class="input input-sm w-full font-mono text-xs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={do_rename_preview}
|
||||||
|
class="btn btn-sm preset-tonal-primary"
|
||||||
|
disabled={rename_loading || !rename_filter.trim()}>
|
||||||
|
{#if rename_loading}
|
||||||
|
<LoaderCircle size={13} class="mr-1 animate-spin" />
|
||||||
|
{:else}
|
||||||
|
<Search size={13} class="mr-1" />
|
||||||
|
{/if}
|
||||||
|
Preview Changes
|
||||||
|
</button>
|
||||||
|
{#if rename_preview.length > 0}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={do_rename_apply}
|
||||||
|
class="btn btn-sm preset-filled-warning font-bold"
|
||||||
|
disabled={rename_apply_status === 'applying'}>
|
||||||
|
{#if rename_apply_status === 'applying'}
|
||||||
|
<LoaderCircle size={13} class="mr-1 animate-spin" /> Applying {rename_applied}/{rename_preview.length}…
|
||||||
|
{:else if rename_apply_status === 'done'}
|
||||||
|
<Check size={13} class="mr-1" /> Done
|
||||||
|
{:else}
|
||||||
|
<Pencil size={13} class="mr-1" /> Apply to {rename_preview.length} record{rename_preview.length !== 1 ? 's' : ''}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if rename_apply_status === 'error'}
|
||||||
|
<span class="text-xs text-error-500">Applied {rename_applied}/{rename_preview.length} — check console for errors.</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if rename_preview.length > 0}
|
||||||
|
<div class="overflow-x-auto rounded-lg border border-surface-500/20">
|
||||||
|
<table class="w-full text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-surface-500/20 text-left text-[10px] font-bold uppercase tracking-wider opacity-50">
|
||||||
|
<th class="px-3 py-2">Current Code</th>
|
||||||
|
<th class="px-3 py-2"></th>
|
||||||
|
<th class="px-3 py-2">New Code</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each rename_preview as item (item.id)}
|
||||||
|
<tr class="border-b border-surface-500/10">
|
||||||
|
<td class="px-3 py-1.5 font-mono opacity-60">{item.old_code}</td>
|
||||||
|
<td class="px-2 py-1.5 opacity-40"><ArrowRight size={12} /></td>
|
||||||
|
<td class="px-3 py-1.5 font-mono font-bold text-success-600 dark:text-success-400">{item.new_code}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{:else if !rename_loading && rename_filter.trim()}
|
||||||
|
<p class="text-xs opacity-40">No changes to preview — either no records matched or find/replace text produced no differences.</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- ── Results ─────────────────────────────────────────────────────────── -->
|
<!-- ── Results ─────────────────────────────────────────────────────────── -->
|
||||||
{#if results.length > 0}
|
{#if results.length > 0}
|
||||||
<div class="card preset-tonal-surface border-surface-500/10 border shadow-xl">
|
<div class="card preset-tonal-surface border-surface-500/10 border shadow-xl">
|
||||||
@@ -611,7 +795,7 @@ function content_preview(ds: ae_DataStore): string {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Flags row -->
|
<!-- 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">
|
<div class="border-surface-500/20 flex flex-wrap items-center gap-x-6 gap-y-2 rounded-lg border bg-surface-500/10 px-4 py-3">
|
||||||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||||
<input type="checkbox" class="checkbox" bind:checked={draft_enable} />
|
<input type="checkbox" class="checkbox" bind:checked={draft_enable} />
|
||||||
Enable
|
Enable
|
||||||
@@ -649,7 +833,7 @@ function content_preview(ds: ae_DataStore): string {
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-xs font-bold opacity-70">Content</span>
|
<span class="text-xs font-bold opacity-70">Content</span>
|
||||||
{#if draft_type === 'html'}
|
{#if draft_type === 'html'}
|
||||||
<div class="flex items-center gap-1 rounded bg-black/5 p-0.5">
|
<div class="flex items-center gap-1 rounded bg-surface-500/10 p-0.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-bold uppercase transition-all"
|
class="flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-bold uppercase transition-all"
|
||||||
|
|||||||
Reference in New Issue
Block a user