@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.
Files changed (158) hide show
  1. package/dist/actions.js +1 -1
  2. package/dist/app.css +102 -63
  3. package/dist/app.theme.css +102 -63
  4. package/dist/component-registry.svelte.d.ts +33 -19
  5. package/dist/component-registry.svelte.js +6 -4
  6. package/dist/components/AdminApp.svelte +55 -53
  7. package/dist/components/AdminApp.svelte.d.ts +2 -2
  8. package/dist/components/ApiSettings.svelte +19 -1
  9. package/dist/components/AuditLogDrawer.svelte +14 -3
  10. package/dist/components/AutoForm.svelte +146 -48
  11. package/dist/components/AutoForm.svelte.d.ts +2 -0
  12. package/dist/components/AutoTable.svelte +358 -258
  13. package/dist/components/AutoTable.svelte.d.ts +1 -0
  14. package/dist/components/BoundRecordDetailDrawer.svelte +166 -0
  15. package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +9 -0
  16. package/dist/components/Breadcrumbs.svelte +7 -4
  17. package/dist/components/CancelTaskButton.svelte +9 -5
  18. package/dist/components/ColumnHeaderFilter.svelte +4 -3
  19. package/dist/components/ColumnSettings.svelte +1 -0
  20. package/dist/components/ComboboxField.svelte +90 -30
  21. package/dist/components/ComboboxField.svelte.d.ts +2 -2
  22. package/dist/components/CreatePage.svelte +3 -1
  23. package/dist/components/DetailDrawer.svelte +4 -0
  24. package/dist/components/DetailDrawer.svelte.d.ts +1 -0
  25. package/dist/components/DevTools.svelte +1 -1
  26. package/dist/components/DraggableGrid.svelte +1 -0
  27. package/dist/components/DraggableHeader.svelte +5 -9
  28. package/dist/components/DraggableHeader.svelte.d.ts +43 -18
  29. package/dist/components/DraggableRowTable.svelte +2 -2
  30. package/dist/components/DynamicFormList.svelte +17 -17
  31. package/dist/components/DynamicFormList.svelte.d.ts +2 -1
  32. package/dist/components/EditPage.svelte +3 -1
  33. package/dist/components/EditableTable.svelte +4 -0
  34. package/dist/components/ErrorPage.svelte +3 -1
  35. package/dist/components/FieldDisplay.svelte +14 -0
  36. package/dist/components/FieldDisplay.svelte.d.ts +7 -0
  37. package/dist/components/FieldRenderer.svelte +9 -5
  38. package/dist/components/FilterBuilder.svelte +19 -8
  39. package/dist/components/FilterDropdown.svelte.d.ts +1 -1
  40. package/dist/components/Header.svelte +5 -6
  41. package/dist/components/ImportWizard.svelte +3 -2
  42. package/dist/components/InfiniteList.svelte +22 -8
  43. package/dist/components/InfiniteList.svelte.d.ts +5 -3
  44. package/dist/components/InlineEdit.svelte +114 -44
  45. package/dist/components/JsonSchemaForm.svelte +19 -14
  46. package/dist/components/Layout.svelte +19 -15
  47. package/dist/components/LazyTaskQueueDrawer.svelte +26 -0
  48. package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +8 -0
  49. package/dist/components/ListPage.svelte +7 -6
  50. package/dist/components/MasterDetailView.svelte +3 -3
  51. package/dist/components/MediaLibraryModal.svelte +5 -2
  52. package/dist/components/MentionsInput.svelte +4 -3
  53. package/dist/components/NavigateToResource.svelte +3 -2
  54. package/dist/components/PermissionMatrix.svelte +4 -3
  55. package/dist/components/ProfilePage.svelte +2 -2
  56. package/dist/components/QuickEditDrawer.svelte +39 -0
  57. package/dist/components/QuickEditDrawer.svelte.d.ts +8 -0
  58. package/dist/components/RecordDetailDrawer.svelte +14 -108
  59. package/dist/components/RecordRowActions.svelte +41 -0
  60. package/dist/components/RecordRowActions.svelte.d.ts +14 -0
  61. package/dist/components/ResourceAccessGuard.svelte +7 -5
  62. package/dist/components/RetryTaskButton.svelte +9 -5
  63. package/dist/components/RowActions.svelte +5 -3
  64. package/dist/components/SensitiveDataMask.svelte +3 -3
  65. package/dist/components/SettingsPage.svelte +1 -1
  66. package/dist/components/ShowPage.svelte +44 -38
  67. package/dist/components/Sidebar.svelte +10 -8
  68. package/dist/components/SplitPaneLayout.svelte +4 -2
  69. package/dist/components/StepsForm.svelte +161 -44
  70. package/dist/components/StepsForm.svelte.d.ts +1 -0
  71. package/dist/components/TaskDetails.svelte +20 -7
  72. package/dist/components/TaskList.svelte +19 -7
  73. package/dist/components/TaskQueueDrawer.svelte +21 -22
  74. package/dist/components/Toast.svelte +4 -2
  75. package/dist/components/TooltipButton.svelte +1 -1
  76. package/dist/components/TreeSelect.svelte +4 -2
  77. package/dist/components/ValidatedField.svelte +27 -0
  78. package/dist/components/ValidatedField.svelte.d.ts +35 -0
  79. package/dist/components/account/ImportMembersPage.svelte +2 -2
  80. package/dist/components/account/SecurityLogPage.svelte +3 -1
  81. package/dist/components/account/SettingsEnterprisePage.svelte +7 -5
  82. package/dist/components/account/TeamMembersPage.svelte +1 -1
  83. package/dist/components/buttons/CloneButton.svelte +6 -4
  84. package/dist/components/buttons/DeleteButton.svelte +95 -29
  85. package/dist/components/buttons/DeleteButton.svelte.d.ts +2 -0
  86. package/dist/components/buttons/EditButton.svelte +6 -4
  87. package/dist/components/buttons/ExportButton.svelte +22 -4
  88. package/dist/components/buttons/ImportButton.svelte +83 -38
  89. package/dist/components/buttons/ImportButton.svelte.d.ts +6 -10
  90. package/dist/components/buttons/RefreshButton.svelte +48 -8
  91. package/dist/components/buttons/ShowButton.svelte +6 -4
  92. package/dist/components/content/ApiKeyList.svelte +3 -1
  93. package/dist/components/content/ContentPageShell.svelte +3 -1
  94. package/dist/components/content/FileList.svelte +3 -1
  95. package/dist/components/content/MemberList.svelte +3 -1
  96. package/dist/components/content/NetworkTable.svelte +3 -1
  97. package/dist/components/content/SecurityEventTable.svelte +3 -1
  98. package/dist/components/content/StatusTabs.svelte +2 -2
  99. package/dist/components/fieldComponentMap.d.ts +18 -13
  100. package/dist/components/fieldComponentMap.js +95 -47
  101. package/dist/components/fields/AvatarField.svelte +1 -1
  102. package/dist/components/fields/ChoiceField.svelte +16 -0
  103. package/dist/components/fields/ChoiceField.svelte.d.ts +10 -0
  104. package/dist/components/fields/CodeField.svelte +11 -12
  105. package/dist/components/fields/CodeField.svelte.d.ts +1 -1
  106. package/dist/components/fields/DateRangeField.svelte +2 -4
  107. package/dist/components/fields/ImageField.svelte +3 -1
  108. package/dist/components/fields/ImagesField.svelte +23 -0
  109. package/dist/components/fields/ImagesField.svelte.d.ts +6 -0
  110. package/dist/components/fields/JsonField.svelte +19 -13
  111. package/dist/components/fields/PasswordField.svelte +5 -0
  112. package/dist/components/fields/PasswordField.svelte.d.ts +6 -0
  113. package/dist/components/fields/json-value.d.ts +11 -0
  114. package/dist/components/fields/json-value.js +28 -0
  115. package/dist/components/profile/ActivityTimeline.svelte +2 -2
  116. package/dist/components/record-detail.svelte.d.ts +16 -0
  117. package/dist/components/record-detail.svelte.js +28 -0
  118. package/dist/components/saved-list-views.js +29 -29
  119. package/dist/components/table-contract.d.ts +13 -0
  120. package/dist/components/table-contract.js +42 -0
  121. package/dist/components/task-utils.js +4 -2
  122. package/dist/components/ui/alert/alert-variants.d.ts +2 -3
  123. package/dist/components/ui/alert/alert-variants.js +1 -2
  124. package/dist/components/ui/alert/alert.svelte +4 -5
  125. package/dist/components/ui/alert/alert.svelte.d.ts +3 -8
  126. package/dist/components/ui/alert/index.js +0 -2
  127. package/dist/components/ui/avatar/avatar-variants.d.ts +2 -3
  128. package/dist/components/ui/avatar/avatar-variants.js +1 -2
  129. package/dist/components/ui/avatar/avatar.svelte +5 -6
  130. package/dist/components/ui/avatar/avatar.svelte.d.ts +3 -8
  131. package/dist/components/ui/badge/badge-variants.d.ts +2 -3
  132. package/dist/components/ui/badge/badge-variants.js +1 -2
  133. package/dist/components/ui/badge/badge.svelte +4 -5
  134. package/dist/components/ui/badge/badge.svelte.d.ts +3 -8
  135. package/dist/components/ui/badge/index.js +0 -2
  136. package/dist/components/ui/button/button-variants.d.ts +2 -3
  137. package/dist/components/ui/button/button-variants.js +1 -2
  138. package/dist/components/ui/button/button.svelte +100 -44
  139. package/dist/components/ui/button/button.svelte.d.ts +8 -12
  140. package/dist/components/ui/button/index.js +0 -2
  141. package/dist/components/ui/command/command-dialog.svelte +3 -1
  142. package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
  143. package/dist/components/ui/command/command-group.svelte +4 -2
  144. package/dist/components/ui/command/command-item.svelte +4 -2
  145. package/dist/components/ui/command/command-separator.svelte +3 -1
  146. package/dist/components/ui/command/index.d.ts +5 -5
  147. package/dist/components/ui/input/input.svelte +40 -20
  148. package/dist/components/ui/input/input.svelte.d.ts +5 -4
  149. package/dist/components/ui/sheet/sheet.svelte +9 -3
  150. package/dist/components/ui/sheet/sheet.svelte.d.ts +1 -0
  151. package/dist/components/ui/tooltip/tooltip.svelte +4 -2
  152. package/dist/index.d.ts +1 -1
  153. package/dist/inline-value.d.ts +5 -0
  154. package/dist/inline-value.js +24 -0
  155. package/dist/numeric-input.d.ts +2 -0
  156. package/dist/numeric-input.js +9 -0
  157. package/dist/types.d.ts +3 -3
  158. package/package.json +11 -5
@@ -50,6 +50,7 @@ interface Props {
50
50
  total: number;
51
51
  visibleColumnsCount: number;
52
52
  }]>;
53
+ deleteVariables?: unknown;
53
54
  }
54
55
  declare const AutoTable: import("svelte").Component<Props, {}, "">;
55
56
  type AutoTable = ReturnType<typeof AutoTable>;
@@ -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(/^#/, '').split(/[?#]/)[0].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.includes(segments[i-1])) {
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
- await taskProvider.cancel(taskId);
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
- ...adminContext.queryKeyMatcher(),
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
- ...adminContext.queryKeyMatcher(),
54
+ ...scope,
51
55
  kind: 'task',
52
56
  action: 'one',
53
- id: taskId,
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="">All</option>
140
- <option value="true">Yes / True</option>
141
- <option value="false">No / False</option>
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
  />
@@ -82,6 +82,7 @@
82
82
 
83
83
  const next = [...columns];
84
84
  const [moved] = next.splice(index, 1);
85
+ if (moved === undefined) return;
85
86
  next.splice(targetIndex, 0, moved);
86
87
 
87
88
  columns = next;
@@ -1,25 +1,27 @@
1
1
  <script lang="ts">
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
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 as Node)) open = false;
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 { options, query, onSearchChange } = useSelect({
49
- get resource() { return resource; },
50
- get optionLabel() { return optionLabel; },
51
- get optionValue() { return optionValue; },
52
- get onSearch() { return onSearch ?? (searchable ? (v: string) => v ? [{ field: optionLabel, operator: 'contains' as const, value: v }] : [] : undefined); },
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
- (options as { label: string; value: string | number }[])?.find(
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
- const v = (e.currentTarget as HTMLInputElement).value;
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
- {...restProps}
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
- <Command.Empty class="svadmin-u-cb11fec3bb46 svadmin-u-ca6bf63030aa svadmin-u-359090c2d529 svadmin-u-bfa603190748">
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
- {#each (options as { label: string; value: string | number }[]) ?? [] as opt, _i (_i)}
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.label}
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
- interface Props {
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;
@@ -200,7 +200,7 @@
200
200
  {
201
201
  name: 'Audit',
202
202
  configured: Boolean(audit),
203
- capabilities: audit ? 'create, get, update' : 'not configured',
203
+ capabilities: audit ? 'create, get' : 'not configured',
204
204
  },
205
205
  {
206
206
  name: 'Notification',
@@ -53,6 +53,7 @@
53
53
 
54
54
  const newModules = [...modules];
55
55
  const [dragged] = newModules.splice(sourceIdx, 1);
56
+ if (dragged === undefined) return;
56
57
  newModules.splice(targetIdx, 0, dragged);
57
58
 
58
59
  modules = newModules;
@@ -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: Column[];
33
- onReorder: (newOrder: Column[]) => void;
34
- header: Snippet<[Column, number, DragHeaderProps]>;
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);