@svadmin/ui 0.69.1 → 0.72.0
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/actions.js +1 -1
- package/dist/app.css +102 -63
- package/dist/app.theme.css +102 -63
- package/dist/component-registry.svelte.d.ts +33 -19
- package/dist/component-registry.svelte.js +6 -4
- package/dist/components/AdminApp.svelte +55 -53
- package/dist/components/AdminApp.svelte.d.ts +2 -2
- package/dist/components/ApiSettings.svelte +19 -1
- package/dist/components/AuditLogDrawer.svelte +14 -3
- package/dist/components/AutoForm.svelte +146 -48
- package/dist/components/AutoForm.svelte.d.ts +2 -0
- package/dist/components/AutoTable.svelte +358 -258
- package/dist/components/AutoTable.svelte.d.ts +1 -0
- package/dist/components/BoundRecordDetailDrawer.svelte +166 -0
- package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +9 -0
- package/dist/components/Breadcrumbs.svelte +7 -4
- package/dist/components/CancelTaskButton.svelte +9 -5
- package/dist/components/ColumnHeaderFilter.svelte +4 -3
- package/dist/components/ColumnSettings.svelte +1 -0
- package/dist/components/ComboboxField.svelte +90 -30
- package/dist/components/ComboboxField.svelte.d.ts +2 -2
- package/dist/components/CreatePage.svelte +3 -1
- package/dist/components/DetailDrawer.svelte +4 -0
- package/dist/components/DetailDrawer.svelte.d.ts +1 -0
- package/dist/components/DevTools.svelte +1 -1
- package/dist/components/DraggableGrid.svelte +1 -0
- package/dist/components/DraggableHeader.svelte +5 -9
- package/dist/components/DraggableHeader.svelte.d.ts +43 -18
- package/dist/components/DraggableRowTable.svelte +2 -2
- package/dist/components/DynamicFormList.svelte +17 -17
- package/dist/components/DynamicFormList.svelte.d.ts +2 -1
- package/dist/components/EditPage.svelte +3 -1
- package/dist/components/EditableTable.svelte +4 -0
- package/dist/components/ErrorPage.svelte +3 -1
- package/dist/components/FieldDisplay.svelte +14 -0
- package/dist/components/FieldDisplay.svelte.d.ts +7 -0
- package/dist/components/FieldRenderer.svelte +9 -5
- package/dist/components/FilterBuilder.svelte +19 -8
- package/dist/components/FilterDropdown.svelte.d.ts +1 -1
- package/dist/components/Header.svelte +5 -6
- package/dist/components/ImportWizard.svelte +3 -2
- package/dist/components/InfiniteList.svelte +22 -8
- package/dist/components/InfiniteList.svelte.d.ts +5 -3
- package/dist/components/InlineEdit.svelte +114 -44
- package/dist/components/JsonSchemaForm.svelte +19 -14
- package/dist/components/Layout.svelte +19 -15
- package/dist/components/LazyTaskQueueDrawer.svelte +26 -0
- package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +8 -0
- package/dist/components/ListPage.svelte +7 -6
- package/dist/components/MasterDetailView.svelte +3 -3
- package/dist/components/MediaLibraryModal.svelte +5 -2
- package/dist/components/MentionsInput.svelte +4 -3
- package/dist/components/NavigateToResource.svelte +3 -2
- package/dist/components/PermissionMatrix.svelte +4 -3
- package/dist/components/ProfilePage.svelte +2 -2
- package/dist/components/QuickEditDrawer.svelte +39 -0
- package/dist/components/QuickEditDrawer.svelte.d.ts +8 -0
- package/dist/components/RecordDetailDrawer.svelte +14 -108
- package/dist/components/RecordRowActions.svelte +41 -0
- package/dist/components/RecordRowActions.svelte.d.ts +14 -0
- package/dist/components/ResourceAccessGuard.svelte +7 -5
- package/dist/components/RetryTaskButton.svelte +9 -5
- package/dist/components/RowActions.svelte +5 -3
- package/dist/components/SensitiveDataMask.svelte +3 -3
- package/dist/components/SettingsPage.svelte +1 -1
- package/dist/components/ShowPage.svelte +44 -38
- package/dist/components/Sidebar.svelte +10 -8
- package/dist/components/SplitPaneLayout.svelte +4 -2
- package/dist/components/StepsForm.svelte +161 -44
- package/dist/components/StepsForm.svelte.d.ts +1 -0
- package/dist/components/TaskDetails.svelte +20 -7
- package/dist/components/TaskList.svelte +19 -7
- package/dist/components/TaskQueueDrawer.svelte +21 -22
- package/dist/components/Toast.svelte +4 -2
- package/dist/components/TooltipButton.svelte +1 -1
- package/dist/components/TreeSelect.svelte +4 -2
- package/dist/components/ValidatedField.svelte +27 -0
- package/dist/components/ValidatedField.svelte.d.ts +35 -0
- package/dist/components/account/ImportMembersPage.svelte +2 -2
- package/dist/components/account/SecurityLogPage.svelte +3 -1
- package/dist/components/account/SettingsEnterprisePage.svelte +7 -5
- package/dist/components/account/TeamMembersPage.svelte +1 -1
- package/dist/components/buttons/CloneButton.svelte +6 -4
- package/dist/components/buttons/DeleteButton.svelte +95 -29
- package/dist/components/buttons/DeleteButton.svelte.d.ts +2 -0
- package/dist/components/buttons/EditButton.svelte +6 -4
- package/dist/components/buttons/ExportButton.svelte +22 -4
- package/dist/components/buttons/ImportButton.svelte +83 -38
- package/dist/components/buttons/ImportButton.svelte.d.ts +6 -10
- package/dist/components/buttons/RefreshButton.svelte +48 -8
- package/dist/components/buttons/ShowButton.svelte +6 -4
- package/dist/components/content/ApiKeyList.svelte +3 -1
- package/dist/components/content/ContentPageShell.svelte +3 -1
- package/dist/components/content/FileList.svelte +3 -1
- package/dist/components/content/MemberList.svelte +3 -1
- package/dist/components/content/NetworkTable.svelte +3 -1
- package/dist/components/content/SecurityEventTable.svelte +3 -1
- package/dist/components/content/StatusTabs.svelte +2 -2
- package/dist/components/fieldComponentMap.d.ts +18 -13
- package/dist/components/fieldComponentMap.js +95 -47
- package/dist/components/fields/AvatarField.svelte +1 -1
- package/dist/components/fields/ChoiceField.svelte +16 -0
- package/dist/components/fields/ChoiceField.svelte.d.ts +10 -0
- package/dist/components/fields/CodeField.svelte +11 -12
- package/dist/components/fields/CodeField.svelte.d.ts +1 -1
- package/dist/components/fields/DateRangeField.svelte +2 -4
- package/dist/components/fields/ImageField.svelte +3 -1
- package/dist/components/fields/ImagesField.svelte +23 -0
- package/dist/components/fields/ImagesField.svelte.d.ts +6 -0
- package/dist/components/fields/JsonField.svelte +19 -13
- package/dist/components/fields/PasswordField.svelte +5 -0
- package/dist/components/fields/PasswordField.svelte.d.ts +6 -0
- package/dist/components/fields/json-value.d.ts +11 -0
- package/dist/components/fields/json-value.js +28 -0
- package/dist/components/profile/ActivityTimeline.svelte +2 -2
- package/dist/components/record-detail.svelte.d.ts +16 -0
- package/dist/components/record-detail.svelte.js +28 -0
- package/dist/components/saved-list-views.js +29 -29
- package/dist/components/table-contract.d.ts +13 -0
- package/dist/components/table-contract.js +42 -0
- package/dist/components/task-utils.js +4 -2
- package/dist/components/ui/alert/alert-variants.d.ts +2 -3
- package/dist/components/ui/alert/alert-variants.js +1 -2
- package/dist/components/ui/alert/alert.svelte +4 -5
- package/dist/components/ui/alert/alert.svelte.d.ts +3 -8
- package/dist/components/ui/alert/index.js +0 -2
- package/dist/components/ui/avatar/avatar-variants.d.ts +2 -3
- package/dist/components/ui/avatar/avatar-variants.js +1 -2
- package/dist/components/ui/avatar/avatar.svelte +5 -6
- package/dist/components/ui/avatar/avatar.svelte.d.ts +3 -8
- package/dist/components/ui/badge/badge-variants.d.ts +2 -3
- package/dist/components/ui/badge/badge-variants.js +1 -2
- package/dist/components/ui/badge/badge.svelte +4 -5
- package/dist/components/ui/badge/badge.svelte.d.ts +3 -8
- package/dist/components/ui/badge/index.js +0 -2
- package/dist/components/ui/button/button-variants.d.ts +2 -3
- package/dist/components/ui/button/button-variants.js +1 -2
- package/dist/components/ui/button/button.svelte +100 -44
- package/dist/components/ui/button/button.svelte.d.ts +8 -12
- package/dist/components/ui/button/index.js +0 -2
- package/dist/components/ui/command/command-dialog.svelte +3 -1
- package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
- package/dist/components/ui/command/command-group.svelte +4 -2
- package/dist/components/ui/command/command-item.svelte +4 -2
- package/dist/components/ui/command/command-separator.svelte +3 -1
- package/dist/components/ui/command/index.d.ts +5 -5
- package/dist/components/ui/input/input.svelte +40 -20
- package/dist/components/ui/input/input.svelte.d.ts +5 -4
- package/dist/components/ui/sheet/sheet.svelte +9 -3
- package/dist/components/ui/sheet/sheet.svelte.d.ts +1 -0
- package/dist/components/ui/tooltip/tooltip.svelte +4 -2
- package/dist/index.d.ts +1 -1
- package/dist/inline-value.d.ts +5 -0
- package/dist/inline-value.js +24 -0
- package/dist/numeric-input.d.ts +2 -0
- package/dist/numeric-input.js +9 -0
- package/dist/types.d.ts +3 -3
- package/package.json +11 -5
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
3
|
+
|
|
4
|
+
import { useCan, useNavigation, type FieldDefinition } from '@svadmin/core';
|
|
5
|
+
import { useRecordDetail } from './record-detail.svelte';
|
|
6
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
7
|
+
import { Copy, Check, Eye, Pencil, RefreshCw } from '@lucide/svelte';
|
|
8
|
+
import DetailDrawer from './DetailDrawer.svelte';
|
|
9
|
+
import FieldDisplay from './FieldDisplay.svelte';
|
|
10
|
+
import { Button } from './ui/button/index.js';
|
|
11
|
+
import { Skeleton } from './ui/skeleton/index.js';
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
resourceName,
|
|
15
|
+
open = $bindable(false),
|
|
16
|
+
recordId,
|
|
17
|
+
onClose,
|
|
18
|
+
}: {
|
|
19
|
+
resourceName: string;
|
|
20
|
+
open?: boolean;
|
|
21
|
+
recordId: string | number;
|
|
22
|
+
onClose?: () => void;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
const i18n = useTranslation();
|
|
26
|
+
const navigation = useNavigation();
|
|
27
|
+
const detail = useRecordDetail(() => ({ resourceName, id: recordId }));
|
|
28
|
+
const resource = $derived(detail.resource);
|
|
29
|
+
const showFields = $derived(detail.fields);
|
|
30
|
+
const fieldGroups = $derived.by(() => {
|
|
31
|
+
const groups = new Map<string, FieldDefinition[]>();
|
|
32
|
+
for (const field of showFields) {
|
|
33
|
+
const label = field.group ?? '';
|
|
34
|
+
const fields = groups.get(label) ?? [];
|
|
35
|
+
fields.push(field);
|
|
36
|
+
groups.set(label, fields);
|
|
37
|
+
}
|
|
38
|
+
return [...groups].map(([label, fields]) => ({ label, fields }));
|
|
39
|
+
});
|
|
40
|
+
let copyState = $state<'idle' | 'copied' | 'error'>('idle');
|
|
41
|
+
$effect(() => { void recordId; void resourceName; copyState = 'idle'; });
|
|
42
|
+
const permissionPending = $derived(detail.checkingPermission);
|
|
43
|
+
const canReadRecord = $derived(detail.canRead);
|
|
44
|
+
const query = detail.query;
|
|
45
|
+
const editPermission = useCan(() => ({
|
|
46
|
+
resource: resourceName, action: 'edit', id: recordId,
|
|
47
|
+
queryOptions: { enabled: canReadRecord && resource.canEdit !== false },
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
function navigateTo(action: 'show' | 'edit'): void {
|
|
51
|
+
if (!canReadRecord || !query.isSuccess || (action === 'edit' &&
|
|
52
|
+
(resource.canEdit === false || editPermission.allowed !== true))) return;
|
|
53
|
+
navigation[action](resourceName, recordId);
|
|
54
|
+
open = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function copyId(): Promise<void> {
|
|
58
|
+
if (!canReadRecord || !query.isSuccess) return;
|
|
59
|
+
const id = recordId;
|
|
60
|
+
try {
|
|
61
|
+
await navigator.clipboard.writeText(String(id));
|
|
62
|
+
if (recordId === id) copyState = 'copied';
|
|
63
|
+
} catch {
|
|
64
|
+
if (recordId === id) copyState = 'error';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
{#snippet fieldsList(fields: FieldDefinition[])}
|
|
70
|
+
{#if query.isSuccess}
|
|
71
|
+
<dl class="svadmin-u-fa6acbf81d74 svadmin-u-d2c3932343f5">
|
|
72
|
+
{#each fields as field (field.key)}
|
|
73
|
+
<div class="svadmin-u-f3c543ad5fe9 svadmin-u-58284b4ea568 svadmin-u-cb11fec3bb46 svadmin-u-d9bdd3d643a2 svadmin-u-022e8076dfea">
|
|
74
|
+
<dt class="svadmin-u-359090c2d529 svadmin-u-2689f3958069 svadmin-u-bfa603190748 svadmin-u-e2327d142859">{field.label}</dt>
|
|
75
|
+
<dd class="svadmin-u-7e0b7cdf1a94 svadmin-u-170cee3ff4e4 svadmin-u-fc7473ca09eb svadmin-u-d4108abe6359">
|
|
76
|
+
<FieldDisplay type={field.type} value={query.data.data[field.key]} options={field.options} resourceName={field.resource} />
|
|
77
|
+
</dd>
|
|
78
|
+
</div>
|
|
79
|
+
{/each}
|
|
80
|
+
</dl>
|
|
81
|
+
{/if}
|
|
82
|
+
{/snippet}
|
|
83
|
+
|
|
84
|
+
<DetailDrawer
|
|
85
|
+
bind:open
|
|
86
|
+
title="{resource.label} {i18n.t('common.detail')}"
|
|
87
|
+
description={`#${recordId}`}
|
|
88
|
+
closeLabel={i18n.t('common.close')}
|
|
89
|
+
width="w-full sm:max-w-xl"
|
|
90
|
+
{...definedOptions({ onClose })}
|
|
91
|
+
data-svadmin-record-detail
|
|
92
|
+
>
|
|
93
|
+
{#snippet extra()}
|
|
94
|
+
{#if canReadRecord && query.isSuccess}
|
|
95
|
+
<Button type="button" variant="ghost" size="icon" onclick={copyId}
|
|
96
|
+
title={i18n.t('common.copyId')} aria-label={i18n.t('common.copyId')}>
|
|
97
|
+
{#if copyState === 'copied'}
|
|
98
|
+
<Check class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
|
|
99
|
+
{:else}
|
|
100
|
+
<Copy class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
|
|
101
|
+
{/if}
|
|
102
|
+
</Button>
|
|
103
|
+
<span role="status">{copyState === 'copied' ? i18n.t('common.copied') : copyState === 'error' ? i18n.t('common.operationFailed') : ''}</span>
|
|
104
|
+
{/if}
|
|
105
|
+
{/snippet}
|
|
106
|
+
{#if permissionPending || (canReadRecord && query.isLoading)}
|
|
107
|
+
<div class="svadmin-u-fa6acbf81d74 svadmin-u-d2c3932343f5" role="status" aria-live="polite" aria-label={i18n.t('common.loading')} aria-busy="true">
|
|
108
|
+
{#each showFields.slice(0, 6) as field (field.key)}
|
|
109
|
+
<div class="svadmin-u-f3c543ad5fe9 svadmin-u-77a2a20e90d4 svadmin-u-cb11fec3bb46 svadmin-u-d9bdd3d643a2 svadmin-u-022e8076dfea">
|
|
110
|
+
<Skeleton class="svadmin-u-11e59c6d5f6b svadmin-u-69da7e4ff95d" />
|
|
111
|
+
<Skeleton class="svadmin-u-11e59c6d5f6b svadmin-u-6da6a3c3f741 svadmin-u-35b2f3aff60f" />
|
|
112
|
+
</div>
|
|
113
|
+
{/each}
|
|
114
|
+
</div>
|
|
115
|
+
{:else if !canReadRecord}
|
|
116
|
+
<div class="svadmin-u-61357c0c2f29 svadmin-u-ca6bf63030aa" data-svadmin-access-denied>
|
|
117
|
+
<p class="svadmin-u-fc7473ca09eb svadmin-u-bfa603190748">{i18n.t('common.accessDenied')}</p>
|
|
118
|
+
</div>
|
|
119
|
+
{:else if query.isError}
|
|
120
|
+
<div role="alert">
|
|
121
|
+
<p>{i18n.t('common.operationFailed')}</p>
|
|
122
|
+
<Button type="button" variant="ghost" size="icon" title={i18n.t('common.retry')}
|
|
123
|
+
aria-label={i18n.t('common.retry')} disabled={query.isFetching} onclick={detail.refresh}>
|
|
124
|
+
<RefreshCw class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
|
|
125
|
+
</Button>
|
|
126
|
+
</div>
|
|
127
|
+
{:else if query.isSuccess}
|
|
128
|
+
{#key `${resourceName}:${recordId}`}
|
|
129
|
+
{#each fieldGroups as group, index (group.label)}
|
|
130
|
+
{#if group.label}
|
|
131
|
+
<details open={index === 0} class="record-detail-group">
|
|
132
|
+
<summary>{group.label}</summary>
|
|
133
|
+
{@render fieldsList(group.fields)}
|
|
134
|
+
</details>
|
|
135
|
+
{:else}
|
|
136
|
+
{@render fieldsList(group.fields)}
|
|
137
|
+
{/if}
|
|
138
|
+
{/each}
|
|
139
|
+
{/key}
|
|
140
|
+
{:else}
|
|
141
|
+
<div class="svadmin-u-61357c0c2f29 svadmin-u-ca6bf63030aa">
|
|
142
|
+
<p class="svadmin-u-fc7473ca09eb svadmin-u-bfa603190748">{i18n.t('common.noData')}</p>
|
|
143
|
+
</div>
|
|
144
|
+
{/if}
|
|
145
|
+
|
|
146
|
+
{#snippet footer()}
|
|
147
|
+
{#if canReadRecord && query.isSuccess}
|
|
148
|
+
<Button type="button" variant="outline" onclick={() => navigateTo('show')}>
|
|
149
|
+
<Eye class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" data-icon="inline-start" aria-hidden="true" />
|
|
150
|
+
{i18n.t('common.openFullDetail')}
|
|
151
|
+
</Button>
|
|
152
|
+
{/if}
|
|
153
|
+
{#if canReadRecord && query.isSuccess && resource.canEdit !== false && editPermission.allowed === true}
|
|
154
|
+
<Button type="button" onclick={() => navigateTo('edit')}>
|
|
155
|
+
<Pencil class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" data-icon="inline-start" aria-hidden="true" />
|
|
156
|
+
{i18n.t('common.edit')}
|
|
157
|
+
</Button>
|
|
158
|
+
{/if}
|
|
159
|
+
{/snippet}
|
|
160
|
+
</DetailDrawer>
|
|
161
|
+
|
|
162
|
+
<style>
|
|
163
|
+
.record-detail-group { border-bottom: 1px solid var(--border); }
|
|
164
|
+
.record-detail-group summary { cursor: pointer; padding: 12px 0; font-weight: 500; }
|
|
165
|
+
.record-detail-group summary:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
|
|
166
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
resourceName: string;
|
|
3
|
+
open?: boolean;
|
|
4
|
+
recordId: string | number;
|
|
5
|
+
onClose?: () => void;
|
|
6
|
+
};
|
|
7
|
+
declare const BoundRecordDetailDrawer: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
8
|
+
type BoundRecordDetailDrawer = ReturnType<typeof BoundRecordDetailDrawer>;
|
|
9
|
+
export default BoundRecordDetailDrawer;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
3
|
+
|
|
2
4
|
import { captureAdminContext, getResources, type MenuItem } from '@svadmin/core';
|
|
3
5
|
import { getPath } from '../router-state.svelte.js';
|
|
4
6
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
@@ -16,7 +18,7 @@
|
|
|
16
18
|
interface Crumb { label: string; href?: string; }
|
|
17
19
|
|
|
18
20
|
function normalizePath(path: string): string {
|
|
19
|
-
const normalized = path.replace(/^#/, '').
|
|
21
|
+
const normalized = path.replace(/^#/, '').replace(/[?#].*$/, '').replace(/\/$/, '');
|
|
20
22
|
return normalized || '/';
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -39,10 +41,10 @@
|
|
|
39
41
|
const menuTrail = menu ? findMenuTrail(menu, currentPathname) : undefined;
|
|
40
42
|
if (menuTrail) {
|
|
41
43
|
for (const item of menuTrail) {
|
|
42
|
-
result.push({
|
|
44
|
+
result.push(definedOptions({
|
|
43
45
|
label: item.label ?? item.name,
|
|
44
46
|
href: item.href ? adminContext.formatLink(normalizePath(item.href)) : undefined,
|
|
45
|
-
});
|
|
47
|
+
}));
|
|
46
48
|
}
|
|
47
49
|
return result;
|
|
48
50
|
}
|
|
@@ -56,6 +58,7 @@
|
|
|
56
58
|
|
|
57
59
|
for (let i = 0; i < segments.length; i++) {
|
|
58
60
|
const seg = segments[i];
|
|
61
|
+
if (seg === undefined) continue;
|
|
59
62
|
currentPath += `/${seg}`;
|
|
60
63
|
|
|
61
64
|
if (resourceNames.includes(seg)) {
|
|
@@ -73,7 +76,7 @@
|
|
|
73
76
|
currentPath += `/${id}`; // advance path by id
|
|
74
77
|
result.push({ label: `${actionLabel} #${id}`, href: adminContext.formatLink(currentPath) });
|
|
75
78
|
i++; // skip next segment since we consumed the id
|
|
76
|
-
} else if (i > 0 && resourceNames.
|
|
79
|
+
} else if (i > 0 && resourceNames.some((name) => name === segments[i-1])) {
|
|
77
80
|
// This is a parent ID (e.g. /teams/123/users) and we are not an action
|
|
78
81
|
result.push({ label: `#${seg}`, href: adminContext.formatLink(currentPath) });
|
|
79
82
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { captureAdminContext, queryKeyMatches } from '@svadmin/core';
|
|
2
|
+
import { captureAdminContext, queryKeyMatches, withValidatedTaskProvider } from '@svadmin/core';
|
|
3
3
|
import { useQueryClient } from '@tanstack/svelte-query';
|
|
4
4
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
5
5
|
|
|
@@ -34,23 +34,27 @@
|
|
|
34
34
|
|
|
35
35
|
async function handleCancel() {
|
|
36
36
|
if (!taskProvider?.cancel || pending) return;
|
|
37
|
+
const scopedTaskId = taskId;
|
|
38
|
+
const scope = adminContext.queryKeyMatcher();
|
|
37
39
|
pending = true;
|
|
38
40
|
try {
|
|
39
|
-
|
|
41
|
+
const provider = withValidatedTaskProvider(taskProvider);
|
|
42
|
+
if (!provider.cancel) return;
|
|
43
|
+
await provider.cancel(scopedTaskId);
|
|
40
44
|
await Promise.all([
|
|
41
45
|
queryClient.invalidateQueries({
|
|
42
46
|
predicate: (query) => queryKeyMatches(query.queryKey, {
|
|
43
|
-
...
|
|
47
|
+
...scope,
|
|
44
48
|
kind: 'task',
|
|
45
49
|
action: 'list',
|
|
46
50
|
}),
|
|
47
51
|
}),
|
|
48
52
|
queryClient.invalidateQueries({
|
|
49
53
|
predicate: (query) => queryKeyMatches(query.queryKey, {
|
|
50
|
-
...
|
|
54
|
+
...scope,
|
|
51
55
|
kind: 'task',
|
|
52
56
|
action: 'one',
|
|
53
|
-
id:
|
|
57
|
+
id: scopedTaskId,
|
|
54
58
|
}),
|
|
55
59
|
}),
|
|
56
60
|
]);
|
|
@@ -136,14 +136,15 @@
|
|
|
136
136
|
bind:value={filterVal}
|
|
137
137
|
class="svadmin-u-d1c57777d8b6 svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-d5eab218aa34 svadmin-u-359090c2d529 svadmin-u-f10f771f87e9 svadmin-u-3e94a98e1466 svadmin-u-9c1295a6914a"
|
|
138
138
|
>
|
|
139
|
-
<option value="">
|
|
140
|
-
<option value="true">
|
|
141
|
-
<option value="false">
|
|
139
|
+
<option value="">{i18n.t('common.all')}</option>
|
|
140
|
+
<option value="true">{i18n.t('common.yesTrue')}</option>
|
|
141
|
+
<option value="false">{i18n.t('common.noFalse')}</option>
|
|
142
142
|
</select>
|
|
143
143
|
{:else if field.type === 'date'}
|
|
144
144
|
<input
|
|
145
145
|
id="header_filter_val_{field.key}"
|
|
146
146
|
type="date"
|
|
147
|
+
lang={i18n.locale}
|
|
147
148
|
bind:value={filterVal}
|
|
148
149
|
class="svadmin-u-d1c57777d8b6 svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-d5eab218aa34 svadmin-u-359090c2d529 svadmin-u-f10f771f87e9 svadmin-u-3e94a98e1466 svadmin-u-9c1295a6914a"
|
|
149
150
|
/>
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
2
|
+
import { definedReactiveOptions } from '@svadmin/core/options';
|
|
3
|
+
|
|
3
4
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
4
|
-
import { useSelect } from '@svadmin/core';
|
|
5
|
-
import type { Filter } from '@svadmin/core';
|
|
5
|
+
import { useResourceContract, useSelect } from '@svadmin/core';
|
|
6
|
+
import type { BaseRecord, Filter } from '@svadmin/core';
|
|
7
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
8
|
+
import type { ButtonProps } from './ui/button/index.js';
|
|
6
9
|
import { Button } from './ui/button/index.js';
|
|
7
|
-
import TooltipButton from './TooltipButton.svelte';
|
|
8
10
|
import { Command } from './ui/command/index.js';
|
|
9
11
|
import { Skeleton } from './ui/skeleton/index.js';
|
|
10
|
-
import { ChevronsUpDown, Check, X, Search } from '@lucide/svelte';
|
|
12
|
+
import { ChevronsUpDown, Check, X, Search, RotateCw } from '@lucide/svelte';
|
|
11
13
|
|
|
12
14
|
const i18n = useTranslation();
|
|
13
15
|
|
|
14
16
|
function clickOutside(node: HTMLElement) {
|
|
15
17
|
const handler = (e: MouseEvent) => {
|
|
16
|
-
if (!node.contains(e.target
|
|
18
|
+
if (e.target instanceof Node && !node.contains(e.target)) open = false;
|
|
17
19
|
};
|
|
18
20
|
document.addEventListener('click', handler, true);
|
|
19
21
|
return { destroy() { document.removeEventListener('click', handler, true); } };
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
interface Props {
|
|
24
|
+
interface Props extends Omit<HTMLButtonAttributes, 'value' | 'onchange' | 'children' | 'onclick' | 'type'> {
|
|
23
25
|
resource: string;
|
|
24
26
|
value?: string | number | null;
|
|
25
27
|
onchange?: (value: string | number | null) => void;
|
|
@@ -30,7 +32,6 @@
|
|
|
30
32
|
onSearch?: (value: string) => Filter[];
|
|
31
33
|
id?: string;
|
|
32
34
|
class?: string;
|
|
33
|
-
[key: string]: any;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
let {
|
|
@@ -42,29 +43,53 @@
|
|
|
42
43
|
placeholder = 'Select...',
|
|
43
44
|
searchable = true,
|
|
44
45
|
onSearch,
|
|
46
|
+
disabled = false,
|
|
45
47
|
...restProps
|
|
46
48
|
}: Props = $props();
|
|
47
49
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
const buttonRestProps = $derived(restProps as Omit<ButtonProps, 'children' | 'variant' | 'size'>);
|
|
51
|
+
|
|
52
|
+
const binding = useResourceContract(() => resource);
|
|
53
|
+
const defaultSearch = $derived.by(() => {
|
|
54
|
+
const field = optionLabel;
|
|
55
|
+
return (v: string): Filter[] => v ? [{ field, operator: 'contains', value: v }] : [];
|
|
53
56
|
});
|
|
57
|
+
const select = useSelect(definedReactiveOptions({
|
|
58
|
+
get resource() { return binding.resource; },
|
|
59
|
+
get dataProviderName() { return binding.dataProviderName; },
|
|
60
|
+
get optionLabel() { return optionLabel; },
|
|
61
|
+
get optionValue() {
|
|
62
|
+
const field = optionValue;
|
|
63
|
+
return (row: BaseRecord) => {
|
|
64
|
+
const candidate = row[field];
|
|
65
|
+
if (typeof candidate === 'string' || (typeof candidate === 'number' && Number.isFinite(candidate))) return candidate;
|
|
66
|
+
throw new TypeError('Invalid select value field');
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
get projectionKey() { return `combobox:${optionLabel}:${optionValue}`; },
|
|
70
|
+
get defaultValue() { return value === null ? [] : [value]; },
|
|
71
|
+
get onSearch() { return onSearch ?? (searchable ? defaultSearch : undefined); },
|
|
72
|
+
}));
|
|
54
73
|
|
|
55
74
|
let open = $state(false);
|
|
56
75
|
let searchInputValue = $state('');
|
|
76
|
+
$effect(() => {
|
|
77
|
+
void binding.resource;
|
|
78
|
+
void binding.dataProviderName;
|
|
79
|
+
void binding.meta;
|
|
80
|
+
searchInputValue = '';
|
|
81
|
+
open = false;
|
|
82
|
+
});
|
|
57
83
|
|
|
58
84
|
const selectedLabel = $derived(
|
|
59
|
-
(
|
|
60
|
-
(o: { label: string; value: string | number }) => o.value === value
|
|
61
|
-
)?.label ?? ''
|
|
85
|
+
select.options.find(o => o.value === value)?.label ?? ''
|
|
62
86
|
);
|
|
63
87
|
|
|
64
88
|
function handleSelect(optValue: string | number) {
|
|
65
89
|
onchange?.(optValue === value ? null : optValue);
|
|
66
90
|
open = false;
|
|
67
91
|
searchInputValue = '';
|
|
92
|
+
select.onSearchChange('');
|
|
68
93
|
}
|
|
69
94
|
|
|
70
95
|
function handleClear(e: Event) {
|
|
@@ -72,37 +97,58 @@
|
|
|
72
97
|
onchange?.(null);
|
|
73
98
|
open = false;
|
|
74
99
|
searchInputValue = '';
|
|
100
|
+
select.onSearchChange('');
|
|
75
101
|
}
|
|
76
102
|
|
|
77
103
|
function handleSearchInput(e: Event) {
|
|
78
|
-
|
|
104
|
+
if (!(e.currentTarget instanceof HTMLInputElement)) return;
|
|
105
|
+
const v = e.currentTarget.value;
|
|
79
106
|
searchInputValue = v;
|
|
80
|
-
onSearchChange(v);
|
|
107
|
+
select.onSearchChange(v);
|
|
81
108
|
}
|
|
82
109
|
</script>
|
|
83
110
|
|
|
84
111
|
<div class="svadmin-u-d89972fe17d6" use:clickOutside>
|
|
112
|
+
<div class="select-control">
|
|
85
113
|
<Button
|
|
86
114
|
variant="outline"
|
|
87
115
|
type="button"
|
|
88
116
|
class="svadmin-u-6da6a3c3f741 svadmin-u-8ef2268efbbc svadmin-u-8ecebc9f80e6"
|
|
89
117
|
onclick={() => { open = !open; }}
|
|
90
|
-
{...
|
|
118
|
+
{...buttonRestProps}
|
|
119
|
+
{disabled}
|
|
120
|
+
aria-haspopup="listbox"
|
|
121
|
+
aria-expanded={open}
|
|
122
|
+
aria-busy={select.isFetching}
|
|
123
|
+
aria-invalid={select.isError}
|
|
91
124
|
>
|
|
92
125
|
{#if selectedLabel}
|
|
93
126
|
<span class="svadmin-u-f283ea9bea0e">{selectedLabel}</span>
|
|
94
|
-
<TooltipButton tooltip={i18n.t('common.clear')} size="icon-sm" class="svadmin-u-cd0d9c512cdc svadmin-u-72470489ff4e svadmin-u-f58b02572ab2" onclick={handleClear}>
|
|
95
|
-
<X class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
|
|
96
|
-
</TooltipButton>
|
|
97
127
|
{:else}
|
|
98
128
|
<span class="svadmin-u-bfa603190748">{placeholder}</span>
|
|
99
129
|
{/if}
|
|
100
130
|
<ChevronsUpDown class="svadmin-u-fb56d9cff341 svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-012fbd121f37 svadmin-u-0b8c506a0596" />
|
|
101
131
|
</Button>
|
|
132
|
+
{#if value !== null}
|
|
133
|
+
<Button type="button" variant="ghost" size="icon-sm" {disabled}
|
|
134
|
+
title={i18n.t('common.clear')} aria-label={i18n.t('common.clear')} onclick={handleClear}>
|
|
135
|
+
<X class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
|
|
136
|
+
</Button>
|
|
137
|
+
{/if}
|
|
138
|
+
</div>
|
|
139
|
+
{#if select.isError}
|
|
140
|
+
<div role="alert" class="select-error">
|
|
141
|
+
<span>Unable to load options.</span>
|
|
142
|
+
<Button type="button" variant="ghost" size="icon-sm" disabled={disabled || select.isFetching}
|
|
143
|
+
title={i18n.t('common.retry')} aria-label={i18n.t('common.retry')} onclick={() => select.refetch()}>
|
|
144
|
+
<RotateCw class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
|
|
145
|
+
</Button>
|
|
146
|
+
</div>
|
|
147
|
+
{/if}
|
|
102
148
|
|
|
103
149
|
{#if open}
|
|
104
150
|
<div class="svadmin-u-da4dbfbc4fdc svadmin-u-181b286668b5 svadmin-u-b6b02c0ebef6 svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e541d86d1ec8 svadmin-u-06bbb43166db">
|
|
105
|
-
<Command.Root class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-2cd02d11d1af svadmin-u-421ac2be5045 svadmin-u-e541d86d1ec8 svadmin-u-9b13e8ae5c9c">
|
|
151
|
+
<Command.Root shouldFilter={false} class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-2cd02d11d1af svadmin-u-421ac2be5045 svadmin-u-e541d86d1ec8 svadmin-u-9b13e8ae5c9c">
|
|
106
152
|
{#if searchable}
|
|
107
153
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-65fdbade2025 svadmin-u-d5eab218aa34">
|
|
108
154
|
<Search class="svadmin-u-82cc6c6581cd svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-012fbd121f37 svadmin-u-0b8c506a0596" />
|
|
@@ -115,17 +161,19 @@
|
|
|
115
161
|
</div>
|
|
116
162
|
{/if}
|
|
117
163
|
<Command.List class="svadmin-u-558f64349245 svadmin-u-92bf82f493b1 svadmin-u-eb6a3cef9686">
|
|
118
|
-
|
|
119
|
-
No results.
|
|
120
|
-
</Command.Empty>
|
|
121
|
-
{#if query.isLoading}
|
|
164
|
+
{#if select.isLoading}
|
|
122
165
|
{#each Array(3) as _, _i (_i)}
|
|
123
166
|
<div class="svadmin-u-d5eab218aa34 svadmin-u-ec0091ee009b"><Skeleton class="svadmin-u-cd0d9c512cdc svadmin-u-6da6a3c3f741" /></div>
|
|
124
167
|
{/each}
|
|
125
|
-
{:else}
|
|
126
|
-
{#
|
|
168
|
+
{:else if !select.isError}
|
|
169
|
+
{#if !select.options.length}
|
|
170
|
+
<Command.Empty class="svadmin-u-cb11fec3bb46 svadmin-u-ca6bf63030aa svadmin-u-359090c2d529 svadmin-u-bfa603190748">
|
|
171
|
+
No results.
|
|
172
|
+
</Command.Empty>
|
|
173
|
+
{/if}
|
|
174
|
+
{#each select.options as opt (opt.value)}
|
|
127
175
|
<Command.Item
|
|
128
|
-
value={opt.
|
|
176
|
+
value={`${typeof opt.value}:${opt.value}`}
|
|
129
177
|
onSelect={() => handleSelect(opt.value)}
|
|
130
178
|
class="svadmin-u-d89972fe17d6 svadmin-u-60fbb7713999 svadmin-u-50ca6ba56aa3 svadmin-u-7f6912283f11 svadmin-u-3960ffc248d9 svadmin-u-36d4469299aa svadmin-u-d5eab218aa34 svadmin-u-ec0091ee009b svadmin-u-fc7473ca09eb svadmin-u-df37b1fd9495 svadmin-u-99afb1cc3b47 svadmin-u-529780e25268"
|
|
131
179
|
>
|
|
@@ -139,3 +187,15 @@
|
|
|
139
187
|
</div>
|
|
140
188
|
{/if}
|
|
141
189
|
</div>
|
|
190
|
+
|
|
191
|
+
<style>
|
|
192
|
+
.select-control, .select-error {
|
|
193
|
+
display: flex;
|
|
194
|
+
align-items: center;
|
|
195
|
+
gap: 4px;
|
|
196
|
+
}
|
|
197
|
+
.select-error {
|
|
198
|
+
color: var(--destructive);
|
|
199
|
+
font-size: 0.875rem;
|
|
200
|
+
}
|
|
201
|
+
</style>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Filter } from '@svadmin/core';
|
|
2
|
-
|
|
2
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
+
interface Props extends Omit<HTMLButtonAttributes, 'value' | 'onchange' | 'children' | 'onclick' | 'type'> {
|
|
3
4
|
resource: string;
|
|
4
5
|
value?: string | number | null;
|
|
5
6
|
onchange?: (value: string | number | null) => void;
|
|
@@ -10,7 +11,6 @@ interface Props {
|
|
|
10
11
|
onSearch?: (value: string) => Filter[];
|
|
11
12
|
id?: string;
|
|
12
13
|
class?: string;
|
|
13
|
-
[key: string]: any;
|
|
14
14
|
}
|
|
15
15
|
declare const ComboboxField: import("svelte").Component<Props, {}, "">;
|
|
16
16
|
type ComboboxField = ReturnType<typeof ComboboxField>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
3
|
+
|
|
2
4
|
import { getResource, captureAdminContext } from '@svadmin/core';
|
|
3
5
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
4
6
|
import type { Snippet } from 'svelte';
|
|
@@ -55,6 +57,6 @@
|
|
|
55
57
|
{columns}
|
|
56
58
|
showHeader={false}
|
|
57
59
|
onNavigationGuardReady={(guard) => navigateGuard = guard}
|
|
58
|
-
{onSuccess}
|
|
60
|
+
{...definedOptions({ "onSuccess": onSuccess })}
|
|
59
61
|
/>
|
|
60
62
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
2
3
|
import type { Snippet } from "svelte";
|
|
3
4
|
import type { HTMLAttributes } from "svelte/elements";
|
|
4
5
|
import { cn, type WithElementRef } from "../utils.js";
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
side?: "left" | "right";
|
|
15
16
|
width?: string;
|
|
16
17
|
onClose?: () => void;
|
|
18
|
+
onCloseRequest?: (close: () => void) => void;
|
|
17
19
|
children?: Snippet;
|
|
18
20
|
footer?: Snippet;
|
|
19
21
|
extra?: Snippet;
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
descriptionId,
|
|
35
37
|
closeLabel = "Close",
|
|
36
38
|
onClose,
|
|
39
|
+
onCloseRequest,
|
|
37
40
|
children,
|
|
38
41
|
footer,
|
|
39
42
|
extra,
|
|
@@ -51,6 +54,7 @@
|
|
|
51
54
|
bind:open
|
|
52
55
|
{side}
|
|
53
56
|
onClose={handleClose}
|
|
57
|
+
{...definedOptions({ onCloseRequest })}
|
|
54
58
|
{closeLabel}
|
|
55
59
|
class={cn("svadmin-u-63a285be6490 svadmin-u-8a539c7fe216 svadmin-u-2cd02d11d1af", width, className)}
|
|
56
60
|
role="dialog"
|
|
@@ -11,6 +11,7 @@ type Props = WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
|
|
11
11
|
side?: "left" | "right";
|
|
12
12
|
width?: string;
|
|
13
13
|
onClose?: () => void;
|
|
14
|
+
onCloseRequest?: (close: () => void) => void;
|
|
14
15
|
children?: Snippet;
|
|
15
16
|
footer?: Snippet;
|
|
16
17
|
extra?: Snippet;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts" generics="TColumn extends { id: string }">
|
|
2
2
|
/**
|
|
3
3
|
* DraggableHeader — Wraps table header cells with HTML5 Drag and Drop
|
|
4
4
|
* to enable column reorder. The parent owns persistence and restoration.
|
|
@@ -12,11 +12,6 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { Snippet } from 'svelte';
|
|
14
14
|
|
|
15
|
-
interface Column {
|
|
16
|
-
id: string;
|
|
17
|
-
[key: string]: unknown;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
15
|
interface DragHeaderProps {
|
|
21
16
|
draggable: true;
|
|
22
17
|
ondragstart: (event: DragEvent) => void;
|
|
@@ -29,9 +24,9 @@
|
|
|
29
24
|
}
|
|
30
25
|
|
|
31
26
|
interface Props {
|
|
32
|
-
columns:
|
|
33
|
-
onReorder: (newOrder:
|
|
34
|
-
header: Snippet<[
|
|
27
|
+
columns: TColumn[];
|
|
28
|
+
onReorder: (newOrder: TColumn[]) => void;
|
|
29
|
+
header: Snippet<[TColumn, number, DragHeaderProps]>;
|
|
35
30
|
}
|
|
36
31
|
|
|
37
32
|
let { columns, onReorder, header }: Props = $props();
|
|
@@ -65,6 +60,7 @@
|
|
|
65
60
|
|
|
66
61
|
const newColumns = [...columns];
|
|
67
62
|
const [moved] = newColumns.splice(dragIndex, 1);
|
|
63
|
+
if (moved === undefined) return;
|
|
68
64
|
newColumns.splice(index, 0, moved);
|
|
69
65
|
|
|
70
66
|
onReorder(newColumns);
|