@svadmin/ui 0.32.1 → 0.32.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AdminApp.svelte +2 -2
- package/dist/components/AuditLogViewer.svelte +5 -7
- package/dist/components/AutoForm.svelte +3 -1
- package/dist/components/AutoForm.svelte.d.ts +1 -0
- package/dist/components/AutoTable.svelte +10 -10
- package/dist/components/Breadcrumbs.svelte +1 -1
- package/dist/components/ChatDialog.svelte +47 -15
- package/dist/components/ComboboxField.svelte +11 -1
- package/dist/components/ConfirmDialog.svelte +1 -1
- package/dist/components/DrawerForm.svelte +2 -2
- package/dist/components/FieldRenderer.svelte +18 -4
- package/dist/components/InlineEdit.svelte +14 -4
- package/dist/components/Layout.svelte +7 -3
- package/dist/components/ModalForm.svelte +2 -2
- package/dist/components/SidebarItem.svelte +1 -2
- package/dist/components/StepsForm.svelte +1 -0
- package/dist/components/UndoableNotification.svelte +5 -7
- package/dist/components/fields/RelationField.svelte +7 -1
- package/package.json +11 -2
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
|
|
92
92
|
setComponentRegistry({ ...defaultComponents, ...userComponents });
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
const resolvedRouter = $derived(routerProvider ??
|
|
94
|
+
const defaultRouter = createHashRouterProvider();
|
|
95
|
+
const resolvedRouter = $derived(routerProvider ?? defaultRouter);
|
|
96
96
|
const resolvedRouteMode = $derived(routeMode ?? (routerProvider ? 'auto' : 'hash'));
|
|
97
97
|
|
|
98
98
|
// Set up context — use $effect.pre so prop changes are tracked and children can access during first render
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
let drawerOpen = $state(false);
|
|
24
24
|
let drawerLog = $state<AuditLog | null>(null);
|
|
25
25
|
|
|
26
|
-
let
|
|
26
|
+
let requestId = 0;
|
|
27
27
|
|
|
28
28
|
async function loadLogs() {
|
|
29
29
|
if (!authProvider?.getAuditLogs) {
|
|
@@ -31,27 +31,25 @@
|
|
|
31
31
|
loading = false;
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
+
const currentId = ++requestId;
|
|
34
35
|
loading = true;
|
|
35
36
|
error = null;
|
|
36
37
|
try {
|
|
37
38
|
const result = await authProvider.getAuditLogs({ page, pageSize });
|
|
38
|
-
if (
|
|
39
|
+
if (currentId !== requestId) return;
|
|
39
40
|
logs = result.data;
|
|
40
41
|
total = result.total;
|
|
41
42
|
} catch (e) {
|
|
42
|
-
if (
|
|
43
|
+
if (currentId !== requestId) return;
|
|
43
44
|
toast.error((e as Error).message);
|
|
44
45
|
} finally {
|
|
45
|
-
if (
|
|
46
|
+
if (currentId === requestId) loading = false;
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
$effect(() => {
|
|
50
51
|
void page;
|
|
51
|
-
cancelled = true;
|
|
52
|
-
cancelled = false;
|
|
53
52
|
loadLogs();
|
|
54
|
-
return () => { cancelled = true; };
|
|
55
53
|
});
|
|
56
54
|
|
|
57
55
|
let totalPages = $derived(Math.ceil(total / pageSize) || 1);
|
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
fieldRenderer?: Snippet<[{ field: FieldDefinition; value: unknown; onchange: (v: unknown) => void }]>;
|
|
23
23
|
formActions?: Snippet<[{ isLoading: boolean; onSubmit: () => void }]>;
|
|
24
24
|
headerContent?: Snippet;
|
|
25
|
+
onSuccess?: () => void;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
let { resourceName, id, mode = 'create', fieldRenderer, formActions, headerContent }: Props = $props();
|
|
28
|
+
let { resourceName, id, mode = 'create', fieldRenderer, formActions, headerContent, onSuccess }: Props = $props();
|
|
28
29
|
const isReadonly = $derived(mode === 'show');
|
|
29
30
|
|
|
30
31
|
// ─── Resource metadata ────────────────────────────────────────────
|
|
@@ -104,6 +105,7 @@
|
|
|
104
105
|
submitError = null;
|
|
105
106
|
try {
|
|
106
107
|
await form.submit();
|
|
108
|
+
onSuccess?.();
|
|
107
109
|
} catch (e) {
|
|
108
110
|
submitError = e instanceof Error ? e.message : t('common.operationFailed');
|
|
109
111
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
} from '@tanstack/svelte-table';
|
|
12
12
|
import { getCoreRowModel } from '@tanstack/table-core';
|
|
13
13
|
|
|
14
|
-
import { useList, useDelete, getResource, notify } from '@svadmin/core';
|
|
14
|
+
import { useList, useDelete, useDeleteMany, getResource, notify } from '@svadmin/core';
|
|
15
15
|
import type { Pagination as PaginationState, Sort, Filter, FieldDefinition } from '@svadmin/core';
|
|
16
16
|
import { navigate } from '@svadmin/core/router';
|
|
17
17
|
import { useCan, getAccessControlProvider } from '@svadmin/core';
|
|
@@ -170,6 +170,8 @@
|
|
|
170
170
|
const query = listResult;
|
|
171
171
|
const deleteResult = useDelete({ get resource() { return resourceName; } });
|
|
172
172
|
const deleteMutation = deleteResult.mutation;
|
|
173
|
+
const deleteManyResult = useDeleteMany({ get resource() { return resourceName; } });
|
|
174
|
+
const deleteManyMutation = deleteManyResult.mutation;
|
|
173
175
|
|
|
174
176
|
// ─── Permissions ──────────────────────────────────────────────
|
|
175
177
|
const acEnabled = $derived(!!getAccessControlProvider());
|
|
@@ -347,13 +349,11 @@
|
|
|
347
349
|
const ids = Object.keys(rowSelection);
|
|
348
350
|
confirmMessage = t('common.batchDeleteConfirm', { count: ids.length });
|
|
349
351
|
confirmAction = async () => {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
notify({ type: 'error', message: t('common.batchDeletePartialFail', { failed:
|
|
355
|
-
} else {
|
|
356
|
-
notify({ type: 'success', message: t('common.batchDeleteSuccess', { count: succeeded }) });
|
|
352
|
+
try {
|
|
353
|
+
await deleteManyMutation.mutateAsync({ ids, resource: resourceName, successNotification: false });
|
|
354
|
+
notify({ type: 'success', message: t('common.batchDeleteSuccess', { count: ids.length }) });
|
|
355
|
+
} catch {
|
|
356
|
+
notify({ type: 'error', message: t('common.batchDeletePartialFail', { failed: 1, total: ids.length }) });
|
|
357
357
|
}
|
|
358
358
|
rowSelection = {};
|
|
359
359
|
confirmOpen = false;
|
|
@@ -374,13 +374,13 @@
|
|
|
374
374
|
return String(val);
|
|
375
375
|
})
|
|
376
376
|
);
|
|
377
|
-
const csv = [headers.join(','), ...rows.map(r => r.map(v => `"${v.replace(
|
|
377
|
+
const csv = [headers.map(h => `"${h.replace(/"/g, '""')}"`).join(','), ...rows.map(r => r.map(v => `"${v.replace(/"/g, '""')}"`).join(','))].join('\n');
|
|
378
378
|
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
|
|
379
379
|
const link = document.createElement('a');
|
|
380
380
|
link.href = URL.createObjectURL(blob);
|
|
381
381
|
link.download = `${resourceName}_${new Date().toISOString().slice(0, 10)}.csv`;
|
|
382
382
|
link.click();
|
|
383
|
-
URL.revokeObjectURL(link.href);
|
|
383
|
+
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
function goToPage(page: number) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { t } from '@svadmin/core/i18n';
|
|
6
6
|
import * as Breadcrumb from './ui/breadcrumb/index.js';
|
|
7
7
|
|
|
8
|
-
const resources = getResources();
|
|
8
|
+
const resources = $derived((() => { try { return getResources(); } catch { return []; } })());
|
|
9
9
|
|
|
10
10
|
interface Crumb { label: string; href: string; }
|
|
11
11
|
|
|
@@ -89,26 +89,45 @@
|
|
|
89
89
|
|
|
90
90
|
// ─── Persist on change (debounced) ──────────────────────────
|
|
91
91
|
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
92
|
+
let lastPersistSnapshot: ChatMessage[] | null = null;
|
|
93
|
+
|
|
94
|
+
function flushPersist(snapshot: ChatMessage[]) {
|
|
95
|
+
if (onPersist) {
|
|
96
|
+
onPersist(snapshot);
|
|
97
|
+
} else if (persistKey) {
|
|
98
|
+
try {
|
|
99
|
+
localStorage.setItem(persistKey, JSON.stringify(snapshot));
|
|
100
|
+
} catch {
|
|
101
|
+
// storage full or unavailable
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
92
105
|
|
|
93
106
|
$effect(() => {
|
|
94
|
-
// Access `messages` to subscribe to changes
|
|
95
107
|
const snapshot = messages;
|
|
96
108
|
if (!initialized) return;
|
|
97
109
|
|
|
98
110
|
if (persistTimer) clearTimeout(persistTimer);
|
|
111
|
+
lastPersistSnapshot = snapshot;
|
|
99
112
|
persistTimer = setTimeout(() => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
} else if (persistKey) {
|
|
103
|
-
try {
|
|
104
|
-
localStorage.setItem(persistKey, JSON.stringify(snapshot));
|
|
105
|
-
} catch {
|
|
106
|
-
// storage full or unavailable
|
|
107
|
-
}
|
|
108
|
-
}
|
|
113
|
+
persistTimer = null;
|
|
114
|
+
flushPersist(snapshot);
|
|
109
115
|
}, 300);
|
|
110
116
|
|
|
111
|
-
return () => {
|
|
117
|
+
return () => {
|
|
118
|
+
if (persistTimer) {
|
|
119
|
+
clearTimeout(persistTimer);
|
|
120
|
+
persistTimer = null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
$effect(() => {
|
|
126
|
+
return () => {
|
|
127
|
+
if (lastPersistSnapshot != null) {
|
|
128
|
+
flushPersist(lastPersistSnapshot);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
112
131
|
});
|
|
113
132
|
|
|
114
133
|
function genId(): string {
|
|
@@ -192,6 +211,7 @@
|
|
|
192
211
|
const collectedActions: ChatAction[] = [];
|
|
193
212
|
|
|
194
213
|
for await (const event of stream) {
|
|
214
|
+
if (!isStreaming) break;
|
|
195
215
|
switch (event.type) {
|
|
196
216
|
case 'text':
|
|
197
217
|
assistantMsg.content += event.content;
|
|
@@ -216,7 +236,6 @@
|
|
|
216
236
|
break;
|
|
217
237
|
|
|
218
238
|
case 'approval_request': {
|
|
219
|
-
// Register in the core approval system
|
|
220
239
|
const reqId = event.id;
|
|
221
240
|
pendingApprovalIds = [...pendingApprovalIds, reqId];
|
|
222
241
|
|
|
@@ -227,7 +246,9 @@
|
|
|
227
246
|
content: approved ? `User approved execution of tool '${event.tool}'` : `User rejected execution of tool '${event.tool}'`,
|
|
228
247
|
timestamp: Date.now()
|
|
229
248
|
}];
|
|
230
|
-
|
|
249
|
+
if (agent?.approveToolCall) {
|
|
250
|
+
agent.approveToolCall(reqId, approved);
|
|
251
|
+
}
|
|
231
252
|
});
|
|
232
253
|
|
|
233
254
|
collectedActions.push(
|
|
@@ -269,6 +290,7 @@
|
|
|
269
290
|
|
|
270
291
|
if (result && typeof result === 'object' && Symbol.asyncIterator in result) {
|
|
271
292
|
for await (const chunk of result as AsyncGenerator<string>) {
|
|
293
|
+
if (!isStreaming) break;
|
|
272
294
|
assistantMsg.content += chunk;
|
|
273
295
|
messages = [...messages.slice(0, -1), { ...assistantMsg }];
|
|
274
296
|
scrollToBottom();
|
|
@@ -359,12 +381,22 @@
|
|
|
359
381
|
|
|
360
382
|
/** Simple markdown→html: bold, italic, code blocks, inline code, line breaks */
|
|
361
383
|
function renderMarkdown(text: string): string {
|
|
362
|
-
|
|
363
|
-
|
|
384
|
+
const escaped = escapeHtml(text);
|
|
385
|
+
const codeBlocks: string[] = [];
|
|
386
|
+
let processed = escaped.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
|
|
387
|
+
const idx = codeBlocks.length;
|
|
388
|
+
codeBlocks.push(`<pre class="chat-code-block"><code>${code}</code></pre>`);
|
|
389
|
+
return `\x00CODEBLOCK${idx}\x00`;
|
|
390
|
+
});
|
|
391
|
+
processed = processed
|
|
364
392
|
.replace(/`([^`]+)`/g, '<code class="chat-inline-code">$1</code>')
|
|
365
393
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
366
394
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
367
395
|
.replace(/\n/g, '<br>');
|
|
396
|
+
for (let i = 0; i < codeBlocks.length; i++) {
|
|
397
|
+
processed = processed.replace(`\x00CODEBLOCK${i}\x00`, codeBlocks[i]);
|
|
398
|
+
}
|
|
399
|
+
return processed;
|
|
368
400
|
}
|
|
369
401
|
|
|
370
402
|
const suggestions = $derived([
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
import { Skeleton } from './ui/skeleton/index.js';
|
|
10
10
|
import { ChevronsUpDown, Check, X, Search } from '@lucide/svelte';
|
|
11
11
|
|
|
12
|
+
function clickOutside(node: HTMLElement) {
|
|
13
|
+
const handler = (e: MouseEvent) => {
|
|
14
|
+
if (!node.contains(e.target as Node)) open = false;
|
|
15
|
+
};
|
|
16
|
+
document.addEventListener('click', handler, true);
|
|
17
|
+
return { destroy() { document.removeEventListener('click', handler, true); } };
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
interface Props {
|
|
13
21
|
resource: string;
|
|
14
22
|
value?: string | number | null;
|
|
@@ -60,6 +68,8 @@
|
|
|
60
68
|
function handleClear(e: Event) {
|
|
61
69
|
e.stopPropagation();
|
|
62
70
|
onchange?.(null);
|
|
71
|
+
open = false;
|
|
72
|
+
searchInputValue = '';
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
function handleSearchInput(e: Event) {
|
|
@@ -69,7 +79,7 @@
|
|
|
69
79
|
}
|
|
70
80
|
</script>
|
|
71
81
|
|
|
72
|
-
<div class="relative">
|
|
82
|
+
<div class="relative" use:clickOutside>
|
|
73
83
|
<Button
|
|
74
84
|
variant="outline"
|
|
75
85
|
type="button"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
const resource = $derived(getResource(resourceName));
|
|
17
17
|
|
|
18
|
-
function
|
|
18
|
+
function handleSuccess() {
|
|
19
19
|
open = false;
|
|
20
20
|
onSuccess?.();
|
|
21
21
|
}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
</Sheet.Title>
|
|
30
30
|
</Sheet.Header>
|
|
31
31
|
<div class="flex-1 overflow-y-auto">
|
|
32
|
-
<AutoForm {resourceName} {mode} {id} />
|
|
32
|
+
<AutoForm {resourceName} {mode} {id} onSuccess={handleSuccess} />
|
|
33
33
|
</div>
|
|
34
34
|
</Sheet.Content>
|
|
35
35
|
</Sheet.Root>
|
|
@@ -30,11 +30,18 @@
|
|
|
30
30
|
|
|
31
31
|
// Typed accessors
|
|
32
32
|
const strVal = $derived((value as string) ?? '');
|
|
33
|
-
const numVal = $derived(
|
|
33
|
+
const numVal = $derived(value as number | null | undefined);
|
|
34
34
|
const boolVal = $derived((value as boolean) ?? false);
|
|
35
35
|
const tagsVal = $derived((value as string[]) ?? []);
|
|
36
36
|
const multiVal = $derived((value as (string | number)[]) ?? []);
|
|
37
37
|
const imagesVal = $derived((value as string[]) ?? []);
|
|
38
|
+
let jsonEditText = $state('');
|
|
39
|
+
let jsonEditing = false;
|
|
40
|
+
|
|
41
|
+
$effect(() => {
|
|
42
|
+
if (jsonEditing) return;
|
|
43
|
+
jsonEditText = typeof value === 'string' ? String(value) : JSON.stringify(value, null, 2);
|
|
44
|
+
});
|
|
38
45
|
|
|
39
46
|
function handleTagKeydown(e: KeyboardEvent) {
|
|
40
47
|
if (e.key === 'Enter') {
|
|
@@ -165,7 +172,7 @@
|
|
|
165
172
|
<Input
|
|
166
173
|
id={field.key}
|
|
167
174
|
type="number"
|
|
168
|
-
value={String(numVal)}
|
|
175
|
+
value={numVal == null ? '' : String(numVal)}
|
|
169
176
|
oninput={(e) => {
|
|
170
177
|
const v = (e.target as HTMLInputElement).value;
|
|
171
178
|
onchange(v === '' ? null : Number(v));
|
|
@@ -359,10 +366,17 @@
|
|
|
359
366
|
{:else if field.type === 'json'}
|
|
360
367
|
<Textarea
|
|
361
368
|
id={field.key}
|
|
362
|
-
value={
|
|
369
|
+
value={jsonEditText}
|
|
370
|
+
onfocus={() => { jsonEditing = true; }}
|
|
371
|
+
onblur={() => {
|
|
372
|
+
jsonEditing = false;
|
|
373
|
+
jsonEditText = typeof value === 'string' ? String(value) : JSON.stringify(value, null, 2);
|
|
374
|
+
}}
|
|
363
375
|
oninput={(e) => {
|
|
376
|
+
const raw = (e.target as HTMLTextAreaElement).value;
|
|
377
|
+
jsonEditText = raw;
|
|
364
378
|
try {
|
|
365
|
-
onchange(JSON.parse(
|
|
379
|
+
onchange(JSON.parse(raw));
|
|
366
380
|
} catch {
|
|
367
381
|
// keep raw text until valid JSON
|
|
368
382
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
let editValue = $state('');
|
|
18
18
|
let inputRef = $state<HTMLInputElement | null>(null);
|
|
19
19
|
let saving = $state(false);
|
|
20
|
+
let savePending = false;
|
|
20
21
|
const displayValue = $derived(String(value ?? ''));
|
|
21
22
|
|
|
22
23
|
const { mutation } = useUpdate({ get resource() { return resourceName; } });
|
|
@@ -31,12 +32,13 @@
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
async function save() {
|
|
34
|
-
if (!editing || saving) return;
|
|
35
|
-
const newValue = field.type === 'number' ? Number(editValue) : editValue;
|
|
35
|
+
if (!editing || saving || savePending) return;
|
|
36
|
+
const newValue = field.type === 'number' ? (editValue.trim() === '' ? null : (() => { const n = Number(editValue); return isNaN(n) ? value : n; })()) : editValue;
|
|
36
37
|
if (newValue === value) {
|
|
37
38
|
editing = false;
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
41
|
+
savePending = true;
|
|
40
42
|
saving = true;
|
|
41
43
|
try {
|
|
42
44
|
await mutation.mutateAsync({
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
// Keep editing on error
|
|
51
53
|
} finally {
|
|
52
54
|
saving = false;
|
|
55
|
+
savePending = false;
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -59,9 +62,16 @@
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
function handleKeydown(e: KeyboardEvent) {
|
|
62
|
-
if (e.key === 'Enter')
|
|
65
|
+
if (e.key === 'Enter') {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
save();
|
|
68
|
+
}
|
|
63
69
|
if (e.key === 'Escape') cancel();
|
|
64
70
|
}
|
|
71
|
+
|
|
72
|
+
function handleBlur() {
|
|
73
|
+
if (!savePending) save();
|
|
74
|
+
}
|
|
65
75
|
</script>
|
|
66
76
|
|
|
67
77
|
{#if editing}
|
|
@@ -70,7 +80,7 @@
|
|
|
70
80
|
type={field.type === 'number' ? 'number' : 'text'}
|
|
71
81
|
bind:value={editValue}
|
|
72
82
|
onkeydown={handleKeydown}
|
|
73
|
-
onblur={
|
|
83
|
+
onblur={handleBlur}
|
|
74
84
|
disabled={saving}
|
|
75
85
|
class="h-7 w-full rounded border bg-background px-2 text-sm outline-none ring-1 ring-primary/50 focus:ring-2 focus:ring-primary transition-all"
|
|
76
86
|
/>
|
|
@@ -52,9 +52,13 @@
|
|
|
52
52
|
|
|
53
53
|
async function handleLogout() {
|
|
54
54
|
if (!auth) return;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
try {
|
|
56
|
+
const result = await auth.logout();
|
|
57
|
+
if (result.success) {
|
|
58
|
+
navigate(result.redirectTo ?? '/login');
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
navigate('/login');
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
const fieldCount = $derived(resource.fields.filter(f => f.showInForm !== false).length);
|
|
19
19
|
const dialogWidth = $derived(fieldCount > 8 ? 'sm:max-w-3xl' : fieldCount > 5 ? 'sm:max-w-2xl' : 'sm:max-w-lg');
|
|
20
20
|
|
|
21
|
-
function
|
|
21
|
+
function handleSuccess() {
|
|
22
22
|
open = false;
|
|
23
23
|
onSuccess?.();
|
|
24
24
|
}
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
{mode === 'create' ? `${t('common.create')}${resource.label}` : `${t('common.edit')}${resource.label}`}
|
|
33
33
|
</Dialog.DialogTitle>
|
|
34
34
|
</Dialog.DialogHeader>
|
|
35
|
-
<AutoForm {resourceName} {mode} {id} />
|
|
35
|
+
<AutoForm {resourceName} {mode} {id} onSuccess={handleSuccess} />
|
|
36
36
|
</Dialog.DialogContent>
|
|
37
37
|
</Dialog.Dialog>
|
|
38
38
|
{/if}
|
|
@@ -39,8 +39,7 @@
|
|
|
39
39
|
function isActive(itemHref: string | undefined): boolean {
|
|
40
40
|
if (!itemHref) return false;
|
|
41
41
|
if (itemHref === '/') return currentPath === '/';
|
|
42
|
-
|
|
43
|
-
return currentPath.startsWith(itemHref);
|
|
42
|
+
return currentPath.startsWith(itemHref) && (currentPath.length === itemHref.length || currentPath[itemHref.length] === '/');
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
/** Check if any child (recursively) is active — used to auto-open parent groups */
|
|
@@ -12,18 +12,16 @@
|
|
|
12
12
|
onTimeout: () => void;
|
|
13
13
|
}>();
|
|
14
14
|
|
|
15
|
-
let remaining = $state(
|
|
15
|
+
let remaining = $state(duration);
|
|
16
16
|
let dismissed = $state(false);
|
|
17
17
|
|
|
18
|
-
// Initialize remaining from prop
|
|
19
|
-
$effect.pre(() => {
|
|
20
|
-
remaining = duration;
|
|
21
|
-
});
|
|
22
|
-
|
|
23
18
|
$effect(() => {
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
const totalMs = duration;
|
|
24
21
|
const interval = setInterval(() => {
|
|
25
|
-
remaining
|
|
22
|
+
remaining = totalMs - (Date.now() - startTime);
|
|
26
23
|
if (remaining <= 0) {
|
|
24
|
+
remaining = 0;
|
|
27
25
|
clearInterval(interval);
|
|
28
26
|
if (!dismissed) {
|
|
29
27
|
dismissed = true;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { ExternalLink } from '@lucide/svelte';
|
|
3
|
+
import { getRouterProvider } from '@svadmin/core';
|
|
3
4
|
|
|
4
5
|
let { value, resourceName, displayValue } = $props<{
|
|
5
6
|
value: string | number | null | undefined;
|
|
@@ -8,7 +9,12 @@
|
|
|
8
9
|
}>();
|
|
9
10
|
|
|
10
11
|
const display = $derived(displayValue ?? (value != null ? `#${value}` : '—'));
|
|
11
|
-
const href = $derived(
|
|
12
|
+
const href = $derived.by(() => {
|
|
13
|
+
if (!resourceName || value == null) return undefined;
|
|
14
|
+
const router = getRouterProvider();
|
|
15
|
+
const path = `/${resourceName}/show/${value}`;
|
|
16
|
+
return router?.formatLink?.(path) ?? path;
|
|
17
|
+
});
|
|
12
18
|
</script>
|
|
13
19
|
|
|
14
20
|
{#if href}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.3",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"./components/*": "./dist/components/*"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@svadmin/core": "^0.23.
|
|
28
|
+
"@svadmin/core": "^0.23.3",
|
|
29
29
|
"@tanstack/svelte-query": "^6.0.0",
|
|
30
30
|
"highlight.js": "^11.11.1",
|
|
31
31
|
"isomorphic-dompurify": "^3.7.1",
|
|
@@ -48,6 +48,15 @@
|
|
|
48
48
|
},
|
|
49
49
|
"@svadmin/editor": {
|
|
50
50
|
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"@svadmin/core": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@tanstack/svelte-query": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"svelte": {
|
|
59
|
+
"optional": true
|
|
51
60
|
}
|
|
52
61
|
},
|
|
53
62
|
"dependencies": {
|