@svadmin/ui 0.5.10 → 0.5.13
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/package.json +2 -2
- package/src/app.css +1 -1
- package/src/components/AdminApp.svelte +14 -10
- package/src/components/AutoForm.svelte +13 -13
- package/src/components/AutoTable.svelte +36 -25
- package/src/components/ComboboxField.svelte +4 -4
- package/src/components/CreatePage.svelte +2 -2
- package/src/components/DevTools.svelte +3 -3
- package/src/components/EditPage.svelte +4 -4
- package/src/components/FieldRenderer.svelte +3 -2
- package/src/components/InfiniteList.svelte +4 -4
- package/src/components/ListPage.svelte +3 -3
- package/src/components/PasswordInput.svelte +2 -1
- package/src/components/ShowPage.svelte +8 -7
- package/src/components/Sidebar.svelte +16 -15
- package/src/components/StepsForm.svelte +10 -10
- package/src/components/UndoableNotification.svelte +6 -1
- package/src/components/buttons/CloneButton.svelte +1 -1
- package/src/components/buttons/CreateButton.svelte +1 -1
- package/src/components/buttons/DeleteButton.svelte +2 -2
- package/src/components/buttons/EditButton.svelte +1 -1
- package/src/components/buttons/ExportButton.svelte +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"svelte": "^5.0.0",
|
|
24
|
-
"@svadmin/core": "^0.
|
|
24
|
+
"@svadmin/core": "^0.5.0",
|
|
25
25
|
"@tanstack/svelte-query": "^5.0.0",
|
|
26
26
|
"bits-ui": "^2.0.0",
|
|
27
27
|
"tailwind-variants": "^3.0.0",
|
package/src/app.css
CHANGED
|
@@ -126,7 +126,7 @@ body {
|
|
|
126
126
|
* This enables bg-primary, text-muted-foreground, border-input, etc.
|
|
127
127
|
* Without this, Tailwind 4 utility classes referencing these tokens are no-ops.
|
|
128
128
|
*/
|
|
129
|
-
@theme
|
|
129
|
+
@theme {
|
|
130
130
|
--color-background: var(--background);
|
|
131
131
|
--color-foreground: var(--foreground);
|
|
132
132
|
--color-card: var(--card);
|
|
@@ -45,15 +45,17 @@
|
|
|
45
45
|
}: Props = $props();
|
|
46
46
|
|
|
47
47
|
// Resolve router provider (default to hash)
|
|
48
|
-
const resolvedRouter = routerProvider ?? createHashRouterProvider();
|
|
48
|
+
const resolvedRouter = $derived(routerProvider ?? createHashRouterProvider());
|
|
49
49
|
|
|
50
|
-
// Set up context
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
// Set up context — use $effect so prop changes are tracked
|
|
51
|
+
$effect.pre(() => {
|
|
52
|
+
setDataProvider(dataProvider);
|
|
53
|
+
if (authProvider) setAuthProvider(authProvider);
|
|
54
|
+
setResources(resources);
|
|
55
|
+
setRouterProvider(resolvedRouter);
|
|
56
|
+
if (locale) setLocale(locale);
|
|
57
|
+
if (defaultTheme) setTheme(defaultTheme);
|
|
58
|
+
});
|
|
57
59
|
|
|
58
60
|
const queryClient = new QueryClient({
|
|
59
61
|
defaultOptions: {
|
|
@@ -62,7 +64,9 @@
|
|
|
62
64
|
});
|
|
63
65
|
|
|
64
66
|
// Initialize router with provider
|
|
65
|
-
|
|
67
|
+
$effect.pre(() => {
|
|
68
|
+
initRouter(resolvedRouter);
|
|
69
|
+
});
|
|
66
70
|
|
|
67
71
|
// Reactive getters for route state
|
|
68
72
|
const route = $derived(getRoute());
|
|
@@ -107,7 +111,7 @@
|
|
|
107
111
|
{:else if route === '/update-password' && authProvider?.updatePassword}
|
|
108
112
|
<UpdatePasswordPage {title} />
|
|
109
113
|
{:else if route === '/login' || route === '/register' || route === '/forgot-password' || route === '/update-password'}
|
|
110
|
-
<ConfigErrorScreen title="{title} —
|
|
114
|
+
<ConfigErrorScreen title="{title} — {t('common.configRequired')}" />
|
|
111
115
|
{:else if isAuthenticated || !authProvider}
|
|
112
116
|
<Layout {title}>
|
|
113
117
|
{#if route === '/'}
|
|
@@ -37,34 +37,34 @@
|
|
|
37
37
|
headerContent,
|
|
38
38
|
}: Props = $props();
|
|
39
39
|
|
|
40
|
-
const resource = getResource(resourceName);
|
|
41
|
-
const primaryKey = resource.primaryKey ?? 'id';
|
|
40
|
+
const resource = $derived(getResource(resourceName));
|
|
41
|
+
const primaryKey = $derived(resource.primaryKey ?? 'id');
|
|
42
42
|
|
|
43
|
-
const formFields = resource.fields.filter(f => {
|
|
43
|
+
const formFields = $derived(resource.fields.filter(f => {
|
|
44
44
|
if (f.key === primaryKey) return false;
|
|
45
45
|
if (f.showInForm === false) return false;
|
|
46
46
|
if (mode === 'create' && f.showInCreate === false) return false;
|
|
47
47
|
if (mode === 'edit' && f.showInEdit === false) return false;
|
|
48
48
|
return true;
|
|
49
|
-
});
|
|
49
|
+
}));
|
|
50
50
|
|
|
51
|
-
const hasGroups = formFields.some(f => f.group);
|
|
52
|
-
const groups = (() => {
|
|
51
|
+
const hasGroups = $derived(formFields.some(f => f.group));
|
|
52
|
+
const groups = $derived((() => {
|
|
53
53
|
if (!hasGroups) return [];
|
|
54
54
|
const order: string[] = [];
|
|
55
|
-
const map = new Map<string,
|
|
55
|
+
const map = new Map<string, FieldDefinition[]>();
|
|
56
56
|
for (const f of formFields) {
|
|
57
57
|
const g = f.group ?? '';
|
|
58
58
|
if (!map.has(g)) { order.push(g); map.set(g, []); }
|
|
59
59
|
map.get(g)!.push(f);
|
|
60
60
|
}
|
|
61
61
|
return order.map(g => ({ name: g, fields: map.get(g)! }));
|
|
62
|
-
})();
|
|
62
|
+
})());
|
|
63
63
|
|
|
64
64
|
// Load existing data for edit mode
|
|
65
|
-
const existingQuery = mode === 'edit' && id != null
|
|
66
|
-
? useOne({ resource
|
|
67
|
-
: null;
|
|
65
|
+
const existingQuery = $derived.by(() => mode === 'edit' && id != null
|
|
66
|
+
? useOne({ get resource() { return resourceName; }, get id() { return id; } })
|
|
67
|
+
: null);
|
|
68
68
|
|
|
69
69
|
// Form state
|
|
70
70
|
let formData = $state<Record<string, unknown>>({});
|
|
@@ -116,8 +116,8 @@
|
|
|
116
116
|
};
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
-
const createMut = useCreate({ resource
|
|
120
|
-
const updateMut = useUpdate({ resource
|
|
119
|
+
const createMut = useCreate({ get resource() { return resourceName; } });
|
|
120
|
+
const updateMut = useUpdate({ get resource() { return resourceName; } });
|
|
121
121
|
|
|
122
122
|
function validateFields(): boolean {
|
|
123
123
|
const errors: Record<string, string> = {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import {
|
|
3
4
|
createTable,
|
|
4
5
|
getCoreRowModel,
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
Eye, Copy
|
|
34
35
|
} from 'lucide-svelte';
|
|
35
36
|
import ConfirmDialog from './ConfirmDialog.svelte';
|
|
37
|
+
import TooltipButton from './TooltipButton.svelte';
|
|
36
38
|
import type { Snippet } from 'svelte';
|
|
37
39
|
|
|
38
40
|
// ─── Props with Snippet composability ─────────────────────────
|
|
@@ -67,21 +69,25 @@
|
|
|
67
69
|
sorters: externalSorters,
|
|
68
70
|
}: Props = $props();
|
|
69
71
|
|
|
70
|
-
const resource = getResource(resourceName);
|
|
72
|
+
const resource = $derived(getResource(resourceName));
|
|
71
73
|
const primaryKey = $derived(resource.primaryKey ?? 'id');
|
|
72
74
|
|
|
73
75
|
// ─── URL state + server-side state ────────────────────────────
|
|
74
76
|
const urlState = readURLState();
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
// Snapshot resource values for initial state (untrack to avoid reactive tracking)
|
|
79
|
+
const initPageSize = untrack(() => resource.pageSize ?? 10);
|
|
80
|
+
const initDefaultSort = untrack(() => resource.defaultSort);
|
|
81
|
+
|
|
82
|
+
let pagination = $state<{ current: number; pageSize: number }>(untrack(() => externalPagination ?? {
|
|
77
83
|
current: urlState.page ?? 1,
|
|
78
|
-
pageSize: urlState.pageSize ??
|
|
79
|
-
});
|
|
80
|
-
let sorters = $state<Sort[]>(
|
|
84
|
+
pageSize: urlState.pageSize ?? initPageSize,
|
|
85
|
+
}));
|
|
86
|
+
let sorters = $state<Sort[]>(untrack(() =>
|
|
81
87
|
externalSorters ??
|
|
82
88
|
(urlState.sortField
|
|
83
89
|
? [{ field: urlState.sortField, order: urlState.sortOrder ?? 'asc' }]
|
|
84
|
-
:
|
|
90
|
+
: initDefaultSort ? [initDefaultSort] : []))
|
|
85
91
|
);
|
|
86
92
|
let filters = $state<Filter[]>([]);
|
|
87
93
|
let searchText = $state(urlState.search ?? '');
|
|
@@ -98,8 +104,8 @@
|
|
|
98
104
|
});
|
|
99
105
|
|
|
100
106
|
// ─── Build active filters with search ─────────────────────────
|
|
101
|
-
const searchableFields = resource.fields.filter(f => f.searchable);
|
|
102
|
-
const filterableFields = resource.fields.filter(f => f.filterable);
|
|
107
|
+
const searchableFields = $derived(resource.fields.filter(f => f.searchable));
|
|
108
|
+
const filterableFields = $derived(resource.fields.filter(f => f.filterable));
|
|
103
109
|
let filterValues = $state<Record<string, string>>({});
|
|
104
110
|
const activeFilterCount = $derived(Object.values(filterValues).filter(v => v.trim()).length);
|
|
105
111
|
const activeFilters = $derived.by(() => {
|
|
@@ -117,21 +123,26 @@
|
|
|
117
123
|
});
|
|
118
124
|
|
|
119
125
|
// ─── Data fetching ────────────────────────────────────────────
|
|
120
|
-
const listResult = useList({
|
|
126
|
+
const listResult = useList({
|
|
127
|
+
get resource() { return resourceName; },
|
|
128
|
+
get pagination() { return pagination; },
|
|
129
|
+
get sorters() { return sorters; },
|
|
130
|
+
get filters() { return activeFilters; },
|
|
131
|
+
});
|
|
121
132
|
const query = listResult.query;
|
|
122
|
-
const deleteResult = useDelete({ resource
|
|
133
|
+
const deleteResult = useDelete({ get resource() { return resourceName; } });
|
|
123
134
|
const deleteMutation = deleteResult.mutation;
|
|
124
135
|
|
|
125
136
|
// ─── Permissions ──────────────────────────────────────────────
|
|
126
|
-
const canCreate = canAccess(resourceName, 'create').can && resource.canCreate !== false;
|
|
127
|
-
const canEdit = canAccess(resourceName, 'edit').can && resource.canEdit !== false;
|
|
128
|
-
const canDelete = canAccess(resourceName, 'delete').can && resource.canDelete !== false;
|
|
129
|
-
const canExport = canAccess(resourceName, 'export').can;
|
|
137
|
+
const canCreate = $derived(canAccess(resourceName, 'create').can && resource.canCreate !== false);
|
|
138
|
+
const canEdit = $derived(canAccess(resourceName, 'edit').can && resource.canEdit !== false);
|
|
139
|
+
const canDelete = $derived(canAccess(resourceName, 'delete').can && resource.canDelete !== false);
|
|
140
|
+
const canExport = $derived(canAccess(resourceName, 'export').can);
|
|
130
141
|
|
|
131
142
|
// ─── TanStack Table state ────────────────────────────────────
|
|
132
|
-
let sorting = $state<SortingState>(
|
|
143
|
+
let sorting = $state<SortingState>(untrack(() =>
|
|
133
144
|
sorters.map(s => ({ id: s.field, desc: s.order === 'desc' }))
|
|
134
|
-
);
|
|
145
|
+
));
|
|
135
146
|
let columnVisibility = $state<VisibilityState>((() => {
|
|
136
147
|
const vis: VisibilityState = {};
|
|
137
148
|
for (const f of resource.fields) {
|
|
@@ -195,7 +206,7 @@
|
|
|
195
206
|
// ─── Create TanStack Table ────────────────────────────────────
|
|
196
207
|
const tbl = createTable({
|
|
197
208
|
get data() { return query.data?.data ?? []; },
|
|
198
|
-
columns,
|
|
209
|
+
get columns() { return columns; },
|
|
199
210
|
getCoreRowModel: getCoreRowModel(),
|
|
200
211
|
manualPagination: true,
|
|
201
212
|
manualSorting: true,
|
|
@@ -218,8 +229,8 @@
|
|
|
218
229
|
onExpandedChange: (updater) => {
|
|
219
230
|
expanded = typeof updater === 'function' ? updater(expanded) : updater;
|
|
220
231
|
},
|
|
221
|
-
enableRowSelection
|
|
222
|
-
enableExpanding
|
|
232
|
+
get enableRowSelection() { return selectable && canDelete; },
|
|
233
|
+
get enableExpanding() { return !!expandedRowRender; },
|
|
223
234
|
});
|
|
224
235
|
|
|
225
236
|
const selectedCount = $derived(Object.keys(rowSelection).length);
|
|
@@ -481,27 +492,27 @@
|
|
|
481
492
|
onCheckedChange={() => row.toggleSelected()}
|
|
482
493
|
/>
|
|
483
494
|
{:else if cell.column.id === '_expand'}
|
|
484
|
-
<
|
|
495
|
+
<TooltipButton tooltip={row.getIsExpanded() ? t('common.collapse') : t('common.expand')} variant="ghost" size="icon" class="h-7 w-7" onclick={() => row.toggleExpanded()}>
|
|
485
496
|
{#if row.getIsExpanded()}
|
|
486
497
|
<ChevronUp class="h-4 w-4" />
|
|
487
498
|
{:else}
|
|
488
499
|
<ChevronDown class="h-4 w-4" />
|
|
489
500
|
{/if}
|
|
490
|
-
</
|
|
501
|
+
</TooltipButton>
|
|
491
502
|
{:else if cell.column.id === '_actions'}
|
|
492
503
|
<div class="flex items-center justify-end gap-1">
|
|
493
504
|
{#if rowActions}
|
|
494
505
|
{@render rowActions({ record, id })}
|
|
495
506
|
{:else}
|
|
496
507
|
{#if canEdit}
|
|
497
|
-
<
|
|
508
|
+
<TooltipButton tooltip={t('common.edit')} variant="ghost" size="icon-sm" onclick={() => navigate(`/${resourceName}/edit/${id}`)}>
|
|
498
509
|
<Pencil class="h-4 w-4" />
|
|
499
|
-
</
|
|
510
|
+
</TooltipButton>
|
|
500
511
|
{/if}
|
|
501
512
|
{#if canDelete}
|
|
502
|
-
<
|
|
513
|
+
<TooltipButton tooltip={t('common.delete')} variant="ghost" size="icon-sm" onclick={() => confirmDelete(id)} class="hover:text-destructive">
|
|
503
514
|
<Trash2 class="h-4 w-4" />
|
|
504
|
-
</
|
|
515
|
+
</TooltipButton>
|
|
505
516
|
{/if}
|
|
506
517
|
{/if}
|
|
507
518
|
</div>
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
}: Props = $props();
|
|
33
33
|
|
|
34
34
|
const { options, query, onSearchChange } = useSelect({
|
|
35
|
-
resource,
|
|
36
|
-
optionLabel,
|
|
37
|
-
optionValue,
|
|
38
|
-
onSearch
|
|
35
|
+
get resource() { return resource; },
|
|
36
|
+
get optionLabel() { return optionLabel; },
|
|
37
|
+
get optionValue() { return optionValue; },
|
|
38
|
+
get onSearch() { return onSearch ?? (searchable ? (v: string) => v ? [{ field: optionLabel, operator: 'contains' as const, value: v }] : [] : undefined); },
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
let open = $state(false);
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
class: className = '',
|
|
24
24
|
}: Props = $props();
|
|
25
25
|
|
|
26
|
-
const resource = getResource(resourceName);
|
|
27
|
-
const pageTitle = title ?? `${t('common.create')}${resource.label}
|
|
26
|
+
const resource = $derived(getResource(resourceName));
|
|
27
|
+
const pageTitle = $derived(title ?? `${t('common.create')}${resource.label}`);
|
|
28
28
|
</script>
|
|
29
29
|
|
|
30
30
|
<div class="space-y-6 {className}">
|
|
@@ -136,14 +136,14 @@
|
|
|
136
136
|
{/if}
|
|
137
137
|
</div>
|
|
138
138
|
{:else}
|
|
139
|
-
<
|
|
139
|
+
<TooltipButton
|
|
140
|
+
tooltip={t('devtools.title')}
|
|
140
141
|
variant="default"
|
|
141
142
|
size="icon"
|
|
142
143
|
class="fixed bottom-4 right-4 z-[9999] h-9 w-9 rounded-full shadow-lg opacity-60 hover:opacity-100 hover:scale-110 transition-all"
|
|
143
144
|
onclick={toggle}
|
|
144
|
-
title={t('devtools.title')}
|
|
145
145
|
>
|
|
146
146
|
<Bug class="h-4 w-4" />
|
|
147
|
-
</
|
|
147
|
+
</TooltipButton>
|
|
148
148
|
{/if}
|
|
149
149
|
{/if}
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
class: className = '',
|
|
29
29
|
}: Props = $props();
|
|
30
30
|
|
|
31
|
-
const resource = getResource(resourceName);
|
|
32
|
-
const pageTitle = title ?? `${t('common.edit')} ${resource.label} #${id}
|
|
33
|
-
const showDelete = canDelete ?? resource.canDelete !== false;
|
|
31
|
+
const resource = $derived(getResource(resourceName));
|
|
32
|
+
const pageTitle = $derived(title ?? `${t('common.edit')} ${resource.label} #${id}`);
|
|
33
|
+
const showDelete = $derived(canDelete ?? resource.canDelete !== false);
|
|
34
34
|
|
|
35
|
-
const deleteResult = useDelete({ resource
|
|
35
|
+
const deleteResult = useDelete({ get resource() { return resourceName; } });
|
|
36
36
|
const deleteMutation = deleteResult.mutation;
|
|
37
37
|
|
|
38
38
|
let confirmOpen = $state(false);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { Select } from './ui/select/index.js';
|
|
14
14
|
import { Checkbox } from './ui/checkbox/index.js';
|
|
15
15
|
import { Button } from './ui/button/index.js';
|
|
16
|
+
import TooltipButton from './TooltipButton.svelte';
|
|
16
17
|
import ComboboxField from './ComboboxField.svelte';
|
|
17
18
|
import { Plus, X } from 'lucide-svelte';
|
|
18
19
|
import type { Snippet } from 'svelte';
|
|
@@ -283,9 +284,9 @@
|
|
|
283
284
|
{#if url}
|
|
284
285
|
<img src={url} alt="preview" class="h-9 w-9 rounded object-cover border" />
|
|
285
286
|
{/if}
|
|
286
|
-
<
|
|
287
|
+
<TooltipButton tooltip={t('common.removeImage')} variant="ghost" size="icon" class="h-8 w-8 shrink-0" onclick={() => removeImage(i)}>
|
|
287
288
|
<X class="h-3.5 w-3.5" />
|
|
288
|
-
</
|
|
289
|
+
</TooltipButton>
|
|
289
290
|
</div>
|
|
290
291
|
{/each}
|
|
291
292
|
<Button variant="outline" size="sm" type="button" onclick={addImage}>
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
}: Props = $props();
|
|
34
34
|
|
|
35
35
|
const { query } = useInfiniteList({
|
|
36
|
-
resource,
|
|
37
|
-
pagination
|
|
38
|
-
sorters,
|
|
39
|
-
filters,
|
|
36
|
+
get resource() { return resource; },
|
|
37
|
+
get pagination() { return { pageSize }; },
|
|
38
|
+
get sorters() { return sorters; },
|
|
39
|
+
get filters() { return filters; },
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
const allItems = $derived(
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
class: className = '',
|
|
42
42
|
}: Props = $props();
|
|
43
43
|
|
|
44
|
-
const resource = getResource(resourceName);
|
|
45
|
-
const pageTitle = title ?? resource.label;
|
|
46
|
-
const showCreate = canCreate ?? resource.canCreate !== false;
|
|
44
|
+
const resource = $derived(getResource(resourceName));
|
|
45
|
+
const pageTitle = $derived(title ?? resource.label);
|
|
46
|
+
const showCreate = $derived(canCreate ?? resource.canCreate !== false);
|
|
47
47
|
</script>
|
|
48
48
|
|
|
49
49
|
<div class="space-y-6 {className}">
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { Label } from './ui/label/index.js';
|
|
6
6
|
import { Progress } from './ui/progress/index.js';
|
|
7
7
|
import { Lock, Eye, EyeOff } from 'lucide-svelte';
|
|
8
|
+
import { t } from '@svadmin/core/i18n';
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
10
11
|
id: string;
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
autocomplete={autocomplete as any}
|
|
67
68
|
/>
|
|
68
69
|
<TooltipButton
|
|
69
|
-
tooltip={showPassword ? '
|
|
70
|
+
tooltip={showPassword ? t('common.hidePassword') : t('common.showPassword')}
|
|
70
71
|
variant="ghost"
|
|
71
72
|
size="icon"
|
|
72
73
|
type="button"
|
|
@@ -7,23 +7,24 @@
|
|
|
7
7
|
import * as Card from './ui/card/index.js';
|
|
8
8
|
import { Skeleton } from './ui/skeleton/index.js';
|
|
9
9
|
import PageHeader from './PageHeader.svelte';
|
|
10
|
+
import TooltipButton from './TooltipButton.svelte';
|
|
10
11
|
import { getDisplayComponent } from './fieldComponentMap';
|
|
11
|
-
import { Pencil } from 'lucide-svelte';
|
|
12
|
+
import { ArrowLeft, Pencil } from 'lucide-svelte';
|
|
12
13
|
|
|
13
14
|
let { resourceName, id } = $props<{ resourceName: string; id: string | number }>();
|
|
14
15
|
|
|
15
|
-
const resource = getResource(resourceName);
|
|
16
|
-
const showFields = resource.fields.filter(f => f.showInShow !== false);
|
|
16
|
+
const resource = $derived(getResource(resourceName));
|
|
17
|
+
const showFields = $derived(resource.fields.filter(f => f.showInShow !== false));
|
|
17
18
|
|
|
18
|
-
const query = useShow({ resource
|
|
19
|
+
const query = useShow({ get resource() { return resourceName; }, get id() { return id; } });
|
|
19
20
|
</script>
|
|
20
21
|
|
|
21
22
|
<div class="space-y-6">
|
|
22
23
|
<PageHeader title="{resource.label} {t('common.detail')}">
|
|
23
24
|
{#snippet actions()}
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
</
|
|
25
|
+
<TooltipButton tooltip={t('common.back')} onclick={() => navigate(`/${resourceName}`)}>
|
|
26
|
+
<ArrowLeft class="h-5 w-5" />
|
|
27
|
+
</TooltipButton>
|
|
27
28
|
{#if resource.canEdit !== false}
|
|
28
29
|
<Button onclick={() => navigate(`/${resourceName}/edit/${id}`)}>
|
|
29
30
|
<Pencil class="h-4 w-4" data-icon="inline-start" /> {t('common.edit')}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { t, getLocale, setLocale, getAvailableLocales } from '@svadmin/core/i18n';
|
|
6
6
|
import { toggleTheme, getResolvedTheme, colorThemes, getColorTheme, setColorTheme, canAccessAsync } from '@svadmin/core';
|
|
7
7
|
import { Button } from './ui/button/index.js';
|
|
8
|
+
import TooltipButton from './TooltipButton.svelte';
|
|
8
9
|
import * as Tooltip from './ui/tooltip/index.js';
|
|
9
10
|
import * as DropdownMenu from './ui/dropdown-menu/index.js';
|
|
10
11
|
import { Separator } from './ui/separator/index.js';
|
|
@@ -134,13 +135,13 @@
|
|
|
134
135
|
{:else}
|
|
135
136
|
<span class="text-lg font-bold text-sidebar-primary">{title.charAt(0)}</span>
|
|
136
137
|
{/if}
|
|
137
|
-
<
|
|
138
|
+
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle}>
|
|
138
139
|
{#if collapsed}
|
|
139
140
|
<ChevronRight class="h-4 w-4" />
|
|
140
141
|
{:else}
|
|
141
142
|
<ChevronLeft class="h-4 w-4" />
|
|
142
143
|
{/if}
|
|
143
|
-
</
|
|
144
|
+
</TooltipButton>
|
|
144
145
|
</div>
|
|
145
146
|
|
|
146
147
|
<ScrollArea class="flex-1">
|
|
@@ -269,25 +270,25 @@
|
|
|
269
270
|
<div class="flex-1 min-w-0">
|
|
270
271
|
<p class="truncate text-sm font-medium text-sidebar-foreground">{identity.name}</p>
|
|
271
272
|
</div>
|
|
272
|
-
<
|
|
273
|
+
<TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon-sm" onclick={toggleLocale} class="text-sidebar-foreground">
|
|
273
274
|
<span class="text-xs font-bold">{localeLabel}</span>
|
|
274
|
-
</
|
|
275
|
-
<
|
|
275
|
+
</TooltipButton>
|
|
276
|
+
<TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon-sm" onclick={toggleTheme} class="text-sidebar-foreground">
|
|
276
277
|
{#if getResolvedTheme() === 'dark'}
|
|
277
278
|
<Sun class="h-4 w-4" />
|
|
278
279
|
{:else}
|
|
279
280
|
<Moon class="h-4 w-4" />
|
|
280
281
|
{/if}
|
|
281
|
-
</
|
|
282
|
-
<
|
|
282
|
+
</TooltipButton>
|
|
283
|
+
<TooltipButton tooltip={t('common.logout')} variant="ghost" size="icon-sm" onclick={onLogout} class="text-sidebar-foreground hover:text-destructive">
|
|
283
284
|
<LogOut class="h-4 w-4" />
|
|
284
|
-
</
|
|
285
|
+
</TooltipButton>
|
|
285
286
|
</div>
|
|
286
287
|
{:else if collapsed}
|
|
287
288
|
<Tooltip.Root>
|
|
288
289
|
<Tooltip.Trigger>
|
|
289
290
|
{#snippet child({ props })}
|
|
290
|
-
<Button {...props} variant="ghost" size="icon" onclick={toggleLocale} class="w-full text-sidebar-foreground"
|
|
291
|
+
<Button {...props} variant="ghost" size="icon" onclick={toggleLocale} class="w-full text-sidebar-foreground">
|
|
291
292
|
<span class="text-xs font-bold">{localeLabel}</span>
|
|
292
293
|
</Button>
|
|
293
294
|
{/snippet}
|
|
@@ -297,7 +298,7 @@
|
|
|
297
298
|
<Tooltip.Root>
|
|
298
299
|
<Tooltip.Trigger>
|
|
299
300
|
{#snippet child({ props })}
|
|
300
|
-
<Button {...props} variant="ghost" size="icon" onclick={toggleTheme} class="w-full text-sidebar-foreground"
|
|
301
|
+
<Button {...props} variant="ghost" size="icon" onclick={toggleTheme} class="w-full text-sidebar-foreground">
|
|
301
302
|
{#if getResolvedTheme() === 'dark'}
|
|
302
303
|
<Sun class="h-5 w-5" />
|
|
303
304
|
{:else}
|
|
@@ -311,7 +312,7 @@
|
|
|
311
312
|
<Tooltip.Root>
|
|
312
313
|
<Tooltip.Trigger>
|
|
313
314
|
{#snippet child({ props })}
|
|
314
|
-
<Button {...props} variant="ghost" size="icon" onclick={onLogout} class="w-full text-sidebar-foreground hover:text-destructive"
|
|
315
|
+
<Button {...props} variant="ghost" size="icon" onclick={onLogout} class="w-full text-sidebar-foreground hover:text-destructive">
|
|
315
316
|
<LogOut class="h-5 w-5" />
|
|
316
317
|
</Button>
|
|
317
318
|
{/snippet}
|
|
@@ -320,16 +321,16 @@
|
|
|
320
321
|
</Tooltip.Root>
|
|
321
322
|
{:else}
|
|
322
323
|
<div class="flex gap-1">
|
|
323
|
-
<
|
|
324
|
+
<TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon" onclick={toggleLocale} class="flex-1 text-sidebar-foreground">
|
|
324
325
|
<span class="text-xs font-bold">{localeLabel}</span>
|
|
325
|
-
</
|
|
326
|
-
<
|
|
326
|
+
</TooltipButton>
|
|
327
|
+
<TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon" onclick={toggleTheme} class="flex-1 text-sidebar-foreground">
|
|
327
328
|
{#if getResolvedTheme() === 'dark'}
|
|
328
329
|
<Sun class="h-5 w-5" />
|
|
329
330
|
{:else}
|
|
330
331
|
<Moon class="h-5 w-5" />
|
|
331
332
|
{/if}
|
|
332
|
-
</
|
|
333
|
+
</TooltipButton>
|
|
333
334
|
</div>
|
|
334
335
|
{/if}
|
|
335
336
|
</div>
|
|
@@ -27,18 +27,18 @@
|
|
|
27
27
|
|
|
28
28
|
let { resourceName, id, mode = 'create', steps }: Props = $props();
|
|
29
29
|
|
|
30
|
-
const resource = getResource(resourceName);
|
|
31
|
-
const primaryKey = resource.primaryKey ?? 'id';
|
|
30
|
+
const resource = $derived(getResource(resourceName));
|
|
31
|
+
const primaryKey = $derived(resource.primaryKey ?? 'id');
|
|
32
32
|
|
|
33
33
|
const stepsForm = useStepsForm({
|
|
34
|
-
resource
|
|
35
|
-
action
|
|
36
|
-
id,
|
|
34
|
+
get resource() { return resourceName; },
|
|
35
|
+
get action() { return mode; },
|
|
36
|
+
get id() { return id; },
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
const { currentStep, next, prev, gotoStep } = stepsForm;
|
|
40
40
|
|
|
41
|
-
const totalSteps = steps.length;
|
|
41
|
+
const totalSteps = $derived(steps.length);
|
|
42
42
|
const progressValue = $derived(((currentStep + 1) / totalSteps) * 100);
|
|
43
43
|
const isLastStep = $derived(currentStep >= totalSteps - 1);
|
|
44
44
|
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
.filter((f): f is FieldDefinition => f != null && f.key !== primaryKey) ?? []
|
|
50
50
|
);
|
|
51
51
|
|
|
52
|
-
const flatStepsFields = steps.flatMap(s => s.fields)
|
|
52
|
+
const flatStepsFields = $derived(steps.flatMap(s => s.fields)
|
|
53
53
|
.map(key => resource.fields.find(f => f.key === key))
|
|
54
|
-
.filter((f): f is FieldDefinition => f != null);
|
|
54
|
+
.filter((f): f is FieldDefinition => f != null));
|
|
55
55
|
|
|
56
|
-
const existingQuery = mode === 'edit' && id != null
|
|
56
|
+
const existingQuery = $derived(mode === 'edit' && id != null
|
|
57
57
|
? stepsForm.query as { data?: { data: Record<string, unknown> }, isLoading: boolean }
|
|
58
|
-
: null;
|
|
58
|
+
: null);
|
|
59
59
|
|
|
60
60
|
let formData = $state<Record<string, unknown>>({});
|
|
61
61
|
let fieldErrors = $state<Record<string, string>>({});
|
|
@@ -12,9 +12,14 @@
|
|
|
12
12
|
onTimeout: () => void;
|
|
13
13
|
}>();
|
|
14
14
|
|
|
15
|
-
let remaining = $state(
|
|
15
|
+
let remaining = $state(0);
|
|
16
16
|
let dismissed = $state(false);
|
|
17
17
|
|
|
18
|
+
// Initialize remaining from prop
|
|
19
|
+
$effect.pre(() => {
|
|
20
|
+
remaining = duration;
|
|
21
|
+
});
|
|
22
|
+
|
|
18
23
|
$effect(() => {
|
|
19
24
|
const interval = setInterval(() => {
|
|
20
25
|
remaining -= 100;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}>();
|
|
13
13
|
|
|
14
14
|
const nav = useNavigation();
|
|
15
|
-
const can = accessControl?.enabled ? useCan(resource, 'create') : null;
|
|
15
|
+
const can = $derived(accessControl?.enabled ? useCan(resource, 'create') : null);
|
|
16
16
|
const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
|
|
17
17
|
</script>
|
|
18
18
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
}>();
|
|
12
12
|
|
|
13
13
|
const nav = useNavigation();
|
|
14
|
-
const can = accessControl?.enabled ? useCan(resource, 'create') : null;
|
|
14
|
+
const can = $derived(accessControl?.enabled ? useCan(resource, 'create') : null);
|
|
15
15
|
const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
|
|
16
16
|
</script>
|
|
17
17
|
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
class?: string;
|
|
17
17
|
}>();
|
|
18
18
|
|
|
19
|
-
const deleteMut = useDelete({ resource, mutationMode
|
|
20
|
-
const can = accessControl?.enabled ? useCan(resource, 'delete') : null;
|
|
19
|
+
const deleteMut = useDelete({ get resource() { return resource; }, get mutationMode() { return undoable ? 'undoable' as const : 'pessimistic' as const; } });
|
|
20
|
+
const can = $derived(accessControl?.enabled ? useCan(resource, 'delete') : null);
|
|
21
21
|
const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
|
|
22
22
|
let confirming = $state(false);
|
|
23
23
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}>();
|
|
13
13
|
|
|
14
14
|
const nav = useNavigation();
|
|
15
|
-
const can = accessControl?.enabled ? useCan(resource, 'edit') : null;
|
|
15
|
+
const can = $derived(accessControl?.enabled ? useCan(resource, 'edit') : null);
|
|
16
16
|
const hidden = $derived(accessControl?.hideIfUnauthorized && can && !can.allowed);
|
|
17
17
|
</script>
|
|
18
18
|
|