@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
@@ -0,0 +1,35 @@
1
+ import type { Component } from 'svelte';
2
+ import type { FieldDisplayProps } from './fieldComponentMap';
3
+ declare function $$render<P extends Record<string, unknown>>(): {
4
+ props: {
5
+ component: Component<P>;
6
+ resolve: (input: FieldDisplayProps) => {
7
+ ok: true;
8
+ props: P;
9
+ } | {
10
+ ok: false;
11
+ };
12
+ input: FieldDisplayProps;
13
+ };
14
+ exports: {};
15
+ bindings: "";
16
+ slots: {};
17
+ events: {};
18
+ };
19
+ declare class __sveltets_Render<P extends Record<string, unknown>> {
20
+ props(): ReturnType<typeof $$render<P>>['props'];
21
+ events(): ReturnType<typeof $$render<P>>['events'];
22
+ slots(): ReturnType<typeof $$render<P>>['slots'];
23
+ bindings(): "";
24
+ exports(): {};
25
+ }
26
+ interface $$IsomorphicComponent {
27
+ new <P extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<P>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<P>['props']>, ReturnType<__sveltets_Render<P>['events']>, ReturnType<__sveltets_Render<P>['slots']>> & {
28
+ $$bindings?: ReturnType<__sveltets_Render<P>['bindings']>;
29
+ } & ReturnType<__sveltets_Render<P>['exports']>;
30
+ <P extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<P>['props']> & {}): ReturnType<__sveltets_Render<P>['exports']>;
31
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
32
+ }
33
+ declare const ValidatedField: $$IsomorphicComponent;
34
+ type ValidatedField<P extends Record<string, unknown>> = InstanceType<typeof ValidatedField<P>>;
35
+ export default ValidatedField;
@@ -40,7 +40,7 @@
40
40
  }
41
41
 
42
42
  function handleFileSelect(e: Event) {
43
- const file = (e.target as HTMLInputElement).files?.[0];
43
+ const file = e.target instanceof HTMLInputElement ? e.target.files?.[0] : undefined;
44
44
  if (file) processFile(file);
45
45
  }
46
46
 
@@ -162,7 +162,7 @@
162
162
  {#each importOptions as option, i (option.key)}
163
163
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-0c3bc98565dd">
164
164
  <Label for="import-option-{i}" class="svadmin-u-fc7473ca09eb svadmin-u-8ecebc9f80e6 svadmin-u-d4108abe6359">{i18n.t(option.key)}</Label>
165
- <Switch id="import-option-{i}" bind:checked={optionStates[i]} />
165
+ <Switch id="import-option-{i}" checked={optionStates[i] ?? option.checked} onCheckedChange={(checked) => { optionStates[i] = checked; }} />
166
166
  </div>
167
167
  {/each}
168
168
  <div class="svadmin-u-60fbb7713999 svadmin-u-77c08e015d14 svadmin-u-b950dda299d3 svadmin-u-173fa8f06789">
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { definedOptions } from '@svadmin/core/options';
3
+
2
4
  import { useTranslation } from '@svadmin/core/i18n';
3
5
  import { Download } from '@lucide/svelte';
4
6
  import { Button } from '../ui/button/index.js';
@@ -15,7 +17,7 @@
15
17
  </script>
16
18
 
17
19
  <ContentPageShell pageId="account-security-log" width="wide">
18
- <ContentPageHeader title={i18n.t('account.securityLog')} description={i18n.t('account.securityLogDescription')} actions={undefined} />
20
+ <ContentPageHeader title={i18n.t('account.securityLog')} description={i18n.t('account.securityLogDescription')} {...definedOptions({ "actions": undefined })} />
19
21
  <div class="svadmin-u-60fbb7713999 svadmin-u-77c08e015d14"><Button variant="outline" size="sm"><Download class="svadmin-u-783b0d9d1e2c" />{i18n.t('common.export')}</Button></div>
20
22
  <FilterToolbar bind:query placeholder={i18n.t('common.search')} />
21
23
  <SecurityEventTable events={filtered} />
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { definedOptions } from '@svadmin/core/options';
3
+
2
4
  import {
3
5
  captureAdminContext,
4
6
  notifyWithProvider,
@@ -167,13 +169,13 @@
167
169
  testing = true;
168
170
  try {
169
171
  const selectedProvider = target ?? identityProviders[0];
170
- const result = await provider.testIdentityProvider({
172
+ const result = await provider.testIdentityProvider(definedOptions({
171
173
  id: selectedProvider?.id,
172
174
  protocol: selectedProvider?.protocol,
173
175
  metadataUrl: target
174
176
  ? target.metadataUrl
175
177
  : (metadataUrl.trim() || selectedProvider?.metadataUrl),
176
- }, context);
178
+ }), context);
177
179
  if (currentRequest !== requestId) return;
178
180
  notifyWithProvider({
179
181
  type: result.success ? 'success' : 'error',
@@ -189,14 +191,14 @@
189
191
  }
190
192
  }
191
193
 
192
- const displayEvents = $derived(events.map((event): SecurityEvent => ({
194
+ const displayEvents = $derived(events.map((event): SecurityEvent => (definedOptions({
193
195
  id: event.id,
194
196
  event: event.event,
195
197
  actor: event.actor,
196
198
  location: event.location,
197
199
  createdAt: event.createdAt instanceof Date ? event.createdAt.toLocaleString() : String(event.createdAt),
198
200
  severity: event.severity === 'critical' ? 'danger' : (event.severity ?? 'info'),
199
- })));
201
+ }))));
200
202
  </script>
201
203
 
202
204
  {#snippet auditControl()}
@@ -242,7 +244,7 @@
242
244
  <DataState state="empty" title={isZh ? '尚未配置身份源' : 'No identity provider configured'} description={isZh ? '请在后端 IdentityGovernanceProvider 中创建并验证身份源。' : 'Create and verify an identity provider through the backend IdentityGovernanceProvider.'} />
243
245
  {:else}
244
246
  {#each identityProviders as connection (connection.id)}
245
- <IntegrationCard integration={{ id: connection.id, name: connection.name, account: connection.protocol.toUpperCase(), connected: connection.status === 'connected', description: connection.domain ?? connection.metadataUrl }} onconnect={() => testConnection(connection)} />
247
+ <IntegrationCard integration={definedOptions({ id: connection.id, name: connection.name, account: connection.protocol.toUpperCase(), connected: connection.status === 'connected', description: connection.domain ?? connection.metadataUrl })} onconnect={() => testConnection(connection)} />
246
248
  {/each}
247
249
  {/if}
248
250
  </section>
@@ -17,7 +17,7 @@
17
17
  let inviteStatus = $state('');
18
18
  let members = $state<MemberSummary[]>(referenceDemoData.members.map((member) => ({ ...member, status: member.status === 'active' ? 'success' : member.status === 'invited' ? 'warning' : 'neutral' })));
19
19
  const filtered = $derived(query ? members.filter((member) => `${member.name} ${member.email} ${member.department}`.toLowerCase().includes(query.toLowerCase())) : members);
20
- function sendInvite() { const email = inviteEmail.trim(); if (!email) return; members = [...members, { id: `invite-${Date.now()}`, name: email.split('@')[0], email, role: 'Viewer', department: 'Pending assignment', status: 'warning' }]; inviteStatus = email; inviteEmail = ''; }
20
+ function sendInvite() { const email = inviteEmail.trim(); if (!email) return; members = [...members, { id: `invite-${Date.now()}`, name: email.replace(/@.*$/, ''), email, role: 'Viewer', department: 'Pending assignment', status: 'warning' }]; inviteStatus = email; inviteEmail = ''; }
21
21
  </script>
22
22
 
23
23
  <ContentPageShell pageId="account-team-members" width="wide">
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { definedOptions } from '@svadmin/core/options';
3
+
2
4
  import type { Snippet } from 'svelte';
3
5
  import { getResource, useNavigation, useCan, useTranslation } from '@svadmin/core';
4
6
  import { Button } from '../ui/button/index.js';
@@ -16,7 +18,7 @@
16
18
  hideText = false,
17
19
  accessControl = { enabled: true, hideIfUnauthorized: true },
18
20
  class: className = '',
19
- } = $props<{
21
+ }: {
20
22
  resource: string;
21
23
  recordItemId: string | number;
22
24
  label?: string;
@@ -24,7 +26,7 @@
24
26
  hideText?: boolean;
25
27
  accessControl?: ButtonAccessControl;
26
28
  class?: string;
27
- }>();
29
+ } = $props();
28
30
 
29
31
  const nav = useNavigation();
30
32
  const resourceDefinition = $derived.by(() => {
@@ -34,13 +36,13 @@
34
36
  return null;
35
37
  }
36
38
  });
37
- const can = useCan(() => ({
39
+ const can = useCan(() => (definedOptions({
38
40
  resource,
39
41
  action: 'create',
40
42
  params: withRecordId(accessControl?.params, recordItemId),
41
43
  meta: accessControl?.meta,
42
44
  queryOptions: { enabled: accessControl?.enabled ?? true }
43
- }));
45
+ })));
44
46
  const displayText = $derived(label ?? i18n.t('common.clone'));
45
47
  const hidden = $derived(resourceDefinition?.canCreate === false || (accessControl?.hideIfUnauthorized && !can.allowed));
46
48
  </script>
@@ -1,10 +1,11 @@
1
1
  <script lang="ts">
2
+ import { definedOptions, definedReactiveOptions } from '@svadmin/core/options';
3
+
2
4
  import type { Snippet } from 'svelte';
3
- import { getResource, useDelete, useCan, useTranslation } from '@svadmin/core';
5
+ import { captureAdminContext, captureAuthSession, useResourceContract, useDelete, useCan, useTranslation, UndoError } from '@svadmin/core';
4
6
  import { Button } from '../ui/button/index.js';
5
7
  import { Trash2 } from '@lucide/svelte';
6
8
  import type { ButtonAccessControl } from './access-control';
7
- import { withRecordId } from './access-control';
8
9
 
9
10
  const i18n = useTranslation();
10
11
 
@@ -17,8 +18,10 @@
17
18
  accessControl = { enabled: true, hideIfUnauthorized: true },
18
19
  onSuccess,
19
20
  undoable = false,
21
+ undoableTimeout,
22
+ variables,
20
23
  class: className = '',
21
- } = $props<{
24
+ }: {
22
25
  resource: string;
23
26
  recordItemId: string | number;
24
27
  label?: string;
@@ -27,59 +30,119 @@
27
30
  accessControl?: ButtonAccessControl;
28
31
  onSuccess?: () => void;
29
32
  undoable?: boolean;
33
+ undoableTimeout?: number;
34
+ variables?: unknown;
30
35
  class?: string;
31
- }>();
36
+ } = $props();
32
37
 
33
- const deleteMut = useDelete({ get resource() { return resource; }, get mutationMode() { return undoable ? 'undoable' as const : 'pessimistic' as const; } });
34
- const resourceDefinition = $derived.by(() => {
35
- try {
36
- return getResource(resource);
37
- } catch {
38
- return null;
39
- }
40
- });
41
- const can = useCan(() => ({
38
+ const binding = useResourceContract(() => resource);
39
+ const context = captureAdminContext();
40
+ const resourceDefinition = $derived(context.getResource(resource));
41
+ const can = useCan(() => (definedOptions({
42
42
  resource,
43
43
  action: 'delete',
44
- params: withRecordId(accessControl?.params, recordItemId),
44
+ id: recordItemId,
45
+ params: accessControl?.params,
45
46
  meta: accessControl?.meta,
46
47
  queryOptions: { enabled: accessControl?.enabled ?? true }
47
- }));
48
+ })));
48
49
  const displayText = $derived(label ?? i18n.t('common.delete'));
49
- const hidden = $derived(resourceDefinition?.canDelete === false || (accessControl?.hideIfUnauthorized && !can.allowed));
50
+ const session = $derived(captureAuthSession(context.authProvider));
51
+ const policyAllowed = $derived(accessControl.enabled === false || can.allowed === true);
52
+ const allowed = $derived(session.available && policyAllowed);
53
+ const hidden = $derived(resourceDefinition.canDelete === false || (accessControl.hideIfUnauthorized && !allowed));
54
+ const deleteMut = useDelete(definedReactiveOptions({
55
+ get resource() { return binding.resource; },
56
+ get id() { return recordItemId; },
57
+ get enabled() { return policyAllowed && resourceDefinition.canDelete !== false; },
58
+ get undoable() { return undoable; },
59
+ get undoableTimeout() { return undoableTimeout; },
60
+ }));
50
61
  let confirming = $state(false);
51
-
52
- async function handleDelete() {
53
- if (!confirming) {
54
- confirming = true;
55
- return;
62
+ let pending = $state(false);
63
+ let failed = $state(false);
64
+ const scope = $derived({
65
+ contract: binding.resource, id: recordItemId, variables,
66
+ provider: context.providers?.[binding.dataProviderName],
67
+ tenant: context.tenantCacheKey?.__svadminTenant,
68
+ meta: binding.meta, allowed, hidden, auth: context.authProvider, session,
69
+ });
70
+ let confirmationScope = $state.raw<typeof scope>();
71
+ let previousScope: typeof scope | undefined;
72
+ let active: object | undefined;
73
+ $effect(() => {
74
+ if (previousScope !== scope) {
75
+ // The hook owns session retirement; do not cancel its own checked logout delegate.
76
+ if (active && previousScope?.session.isCurrent()) deleteMut.mutation.reset();
77
+ previousScope = scope;
78
+ active = undefined;
79
+ confirming = false;
80
+ confirmationScope = undefined;
81
+ pending = false;
82
+ failed = false;
56
83
  }
84
+ });
85
+ $effect(() => () => { active = undefined; });
86
+
87
+ function beginConfirmation() {
88
+ if (pending || !allowed || hidden || !session.isCurrent()) return;
89
+ confirmationScope = scope;
90
+ confirming = true;
91
+ }
92
+
93
+ const confirmDelete = $derived.by(() => {
94
+ const captured = scope;
95
+ return () => handleDelete(captured);
96
+ });
97
+ async function handleDelete(capturedScope: typeof scope) {
98
+ if (pending || !allowed || hidden || !capturedScope.session.isCurrent() ||
99
+ capturedScope !== scope || !confirming || confirmationScope !== capturedScope) return;
100
+ const token = {};
101
+ const success = onSuccess;
102
+ previousScope = capturedScope;
103
+ active = token;
57
104
  confirming = false;
58
- await deleteMut.mutation.mutateAsync({ id: recordItemId, resource });
59
- onSuccess?.();
105
+ confirmationScope = undefined;
106
+ pending = true;
107
+ failed = false;
108
+ try {
109
+ await deleteMut.mutation.mutateAsync(definedOptions({
110
+ variables, dataProviderName: binding.dataProviderName,
111
+ }));
112
+ if (active === token && scope === capturedScope && capturedScope.session.isCurrent()) success?.();
113
+ } catch (error) {
114
+ if (!(error instanceof UndoError) && active === token && scope === capturedScope && capturedScope.session.isCurrent()) failed = true;
115
+ } finally {
116
+ if (active === token && scope === capturedScope && capturedScope.session.isCurrent()) {
117
+ active = undefined;
118
+ pending = false;
119
+ }
120
+ }
60
121
  }
61
122
 
62
- function cancel() { confirming = false; }
123
+ function cancel() { confirming = false; confirmationScope = undefined; }
63
124
  </script>
64
125
 
65
126
  {#if !hidden}
66
- {#if confirming}
127
+ {#if confirming && confirmationScope === scope && session.isCurrent()}
67
128
  <div class="svadmin-u-52083e7da442 svadmin-u-3960ffc248d9 svadmin-u-44ee8ba0a421">
68
- <Button variant="destructive" size="sm" onclick={handleDelete}>
129
+ <Button type="button" variant="destructive" size="sm" disabled={pending || !allowed} onclick={confirmDelete}>
69
130
  {i18n.t('common.confirm')}
70
131
  </Button>
71
- <Button variant="ghost" size="sm" onclick={cancel}>
132
+ <Button type="button" variant="ghost" size="sm" disabled={pending} onclick={cancel}>
72
133
  {i18n.t('common.cancel')}
73
134
  </Button>
74
135
  </div>
75
136
  {:else}
76
137
  <Button
138
+ type="button"
77
139
  variant="ghost"
78
140
  size={hideText ? 'icon' : 'sm'}
79
141
  class="svadmin-u-811148b13d1e svadmin-u-51e95020d6f2 {className}"
80
142
  aria-label={hideText ? displayText : undefined}
81
- disabled={!can.allowed}
82
- onclick={handleDelete}
143
+ disabled={pending || !allowed}
144
+ aria-busy={pending}
145
+ onclick={beginConfirmation}
83
146
  >
84
147
  <Trash2 class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
85
148
  {#if !hideText}
@@ -93,4 +156,7 @@
93
156
  {/if}
94
157
  </Button>
95
158
  {/if}
159
+ {#if failed}
160
+ <p role="alert">{i18n.t('common.error')}</p>
161
+ {/if}
96
162
  {/if}
@@ -9,6 +9,8 @@ type $$ComponentProps = {
9
9
  accessControl?: ButtonAccessControl;
10
10
  onSuccess?: () => void;
11
11
  undoable?: boolean;
12
+ undoableTimeout?: number;
13
+ variables?: unknown;
12
14
  class?: string;
13
15
  };
14
16
  declare const DeleteButton: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { definedOptions } from '@svadmin/core/options';
3
+
2
4
  import type { Snippet } from 'svelte';
3
5
  import { getResource, useNavigation, useCan, useTranslation } from '@svadmin/core';
4
6
  import { Button } from '../ui/button/index.js';
@@ -16,7 +18,7 @@
16
18
  hideText = false,
17
19
  accessControl = { enabled: true, hideIfUnauthorized: true },
18
20
  class: className = '',
19
- } = $props<{
21
+ }: {
20
22
  resource: string;
21
23
  recordItemId: string | number;
22
24
  label?: string;
@@ -24,7 +26,7 @@
24
26
  hideText?: boolean;
25
27
  accessControl?: ButtonAccessControl;
26
28
  class?: string;
27
- }>();
29
+ } = $props();
28
30
 
29
31
  const nav = useNavigation();
30
32
  const resourceDefinition = $derived.by(() => {
@@ -34,13 +36,13 @@
34
36
  return null;
35
37
  }
36
38
  });
37
- const can = useCan(() => ({
39
+ const can = useCan(() => (definedOptions({
38
40
  resource,
39
41
  action: 'edit',
40
42
  params: withRecordId(accessControl?.params, recordItemId),
41
43
  meta: accessControl?.meta,
42
44
  queryOptions: { enabled: accessControl?.enabled ?? true }
43
- }));
45
+ })));
44
46
  const displayText = $derived(label ?? i18n.t('common.edit'));
45
47
  const hidden = $derived(resourceDefinition?.canEdit === false || (accessControl?.hideIfUnauthorized && !can.allowed));
46
48
  </script>
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import { useExport, useCan, useTranslation } from '@svadmin/core';
3
+ import { useCan, useTranslation, useExport, useResourceContract } from '@svadmin/core';
4
+ import { definedReactiveOptions } from '@svadmin/core/options';
4
5
  import { Button } from '../ui/button/index.js';
5
6
  import { Download } from '@lucide/svelte';
6
7
  import type { ButtonAccessControl } from './access-control';
@@ -23,7 +24,12 @@
23
24
  class?: string;
24
25
  }>();
25
26
 
26
- const { triggerExport, isLoading } = useExport({ get resource() { return resource; } });
27
+ const binding = useResourceContract(() => resource);
28
+ const exporter = useExport(definedReactiveOptions({
29
+ get resource() { return binding.resource; },
30
+ get dataProviderName() { return binding.dataProviderName; },
31
+ get meta() { return binding.meta; },
32
+ }));
27
33
  const can = useCan(() => ({
28
34
  resource,
29
35
  action: 'export',
@@ -33,6 +39,14 @@
33
39
  }));
34
40
  const hidden = $derived(accessControl?.hideIfUnauthorized && !can.allowed);
35
41
  const displayText = $derived(label ?? i18n.t('common.export'));
42
+
43
+ async function exportRecords() {
44
+ if (!can.allowed || exporter.isLoading) return;
45
+ try { await exporter.triggerExport(); }
46
+ catch {
47
+ // The hook exposes a sanitized error; stale-scope cancellations remain silent.
48
+ }
49
+ }
36
50
  </script>
37
51
 
38
52
  {#if !hidden}
@@ -41,8 +55,9 @@
41
55
  size={hideText ? 'icon' : 'sm'}
42
56
  class={className}
43
57
  aria-label={hideText ? displayText : undefined}
44
- disabled={isLoading || !can.allowed}
45
- onclick={triggerExport}
58
+ disabled={exporter.isLoading || !can.allowed}
59
+ aria-busy={exporter.isLoading}
60
+ onclick={exportRecords}
46
61
  >
47
62
  <Download class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
48
63
  {#if !hideText}
@@ -55,4 +70,7 @@
55
70
  </span>
56
71
  {/if}
57
72
  </Button>
73
+ {#if exporter.error}
74
+ <p role="alert">{i18n.t('common.error')}</p>
75
+ {/if}
58
76
  {/if}
@@ -1,76 +1,118 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import { useImport, useCan, useTranslation } from '@svadmin/core';
3
+ import { captureAdminContext, captureAuthSession, useCan, useTranslation, useImport, useResourceContract,
4
+ type ImportResult, type ContractSchemas } from '@svadmin/core';
5
+ import { definedOptions, definedReactiveOptions } from '@svadmin/core/options';
4
6
  import { Button } from '../ui/button/index.js';
5
7
  import { Upload } from '@lucide/svelte';
6
8
  import type { ButtonAccessControl } from './access-control';
7
9
 
8
10
  const i18n = useTranslation();
9
11
 
10
- let {
11
- resource,
12
- label,
13
- children,
14
- hideText = false,
15
- onFinish,
16
- accessControl = { enabled: true, hideIfUnauthorized: true },
17
- class: className = '',
18
- } = $props<{
12
+ interface Props {
19
13
  resource: string;
20
14
  label?: string;
21
15
  children?: Snippet;
22
16
  hideText?: boolean;
23
- onFinish?: (result: { succeeded: unknown[]; errored: { request: unknown; error: unknown }[] }) => void;
17
+ onFinish?: (result: ImportResult<ContractSchemas>) => void | Promise<void>;
18
+ mapData?: (row: Record<string, unknown>) => unknown;
24
19
  accessControl?: ButtonAccessControl;
25
20
  class?: string;
26
- }>();
21
+ }
22
+ let {
23
+ resource, label, children, hideText = false, onFinish, mapData,
24
+ accessControl = { enabled: true, hideIfUnauthorized: true }, class: className = '',
25
+ }: Props = $props();
27
26
 
28
27
  let fileInput: HTMLInputElement | undefined = $state();
29
28
 
30
- const importHook = useImport({
31
- get resource() { return resource; },
32
- onFinish: (result) => onFinish?.(result),
33
- });
34
-
35
- const can = useCan(() => ({
29
+ const context = captureAdminContext();
30
+ const binding = useResourceContract(() => resource);
31
+ const can = useCan(() => definedOptions({
36
32
  resource,
37
33
  action: 'import',
38
34
  params: accessControl?.params,
39
35
  meta: accessControl?.meta,
40
36
  queryOptions: { enabled: accessControl?.enabled ?? true },
41
37
  }));
42
- const hidden = $derived(accessControl?.hideIfUnauthorized && !can.allowed);
38
+ const session = $derived(captureAuthSession(context.authProvider));
39
+ const policyAllowed = $derived(accessControl.enabled === false || can.allowed === true);
40
+ const allowed = $derived(policyAllowed && session.available);
41
+ const canCreate = $derived(context.getResource(resource).canCreate !== false);
42
+ const hidden = $derived(!canCreate ||
43
+ (accessControl.hideIfUnauthorized && !allowed));
44
+ const importHook = useImport(definedReactiveOptions({
45
+ get resource() { return binding.resource; },
46
+ get dataProviderName() { return binding.dataProviderName; },
47
+ get enabled() { return policyAllowed && canCreate; },
48
+ get onFinish() { return onFinish; },
49
+ get mapData() { return mapData; },
50
+ }));
51
+ const scope = $derived({
52
+ contract: binding.resource, provider: context.providers?.[binding.dataProviderName],
53
+ tenant: context.tenantCacheKey?.__svadminTenant, meta: binding.meta, allowed, hidden,
54
+ auth: context.authProvider, router: context.routerProvider, session,
55
+ });
56
+ let mounted = true;
57
+ let picker: { scope: typeof scope; input: HTMLInputElement } | undefined;
58
+ let selection: object | undefined;
59
+ const currentScope = (origin: typeof scope) => mounted && scope === origin && origin.session.isCurrent();
60
+ $effect.pre(() => { void scope; picker = undefined; selection = undefined; });
61
+ $effect(() => () => { mounted = false; picker = undefined; selection = undefined; });
43
62
 
44
- function triggerImport() {
45
- fileInput?.click();
46
- }
63
+ const triggerImport = $derived.by(() => {
64
+ const origin = scope;
65
+ return () => {
66
+ if (!currentScope(origin) || !allowed || hidden || importHook.isLoading || !fileInput) return;
67
+ picker = { scope: origin, input: fileInput };
68
+ fileInput.click();
69
+ };
70
+ });
47
71
 
48
- function handleFileChange(e: Event) {
49
- const input = e.target as HTMLInputElement;
50
- const file = input.files?.[0];
51
- if (file) {
52
- importHook.handleChange({ file });
53
- input.value = ''; // reset for re-import
54
- }
55
- }
72
+ const handleFileChange = $derived.by(() => {
73
+ const origin = scope;
74
+ return async (event: Event) => {
75
+ const input = event.currentTarget;
76
+ if (!(input instanceof HTMLInputElement) || !currentScope(origin) || picker?.scope !== origin ||
77
+ picker.input !== input || !allowed || hidden || importHook.isLoading) return;
78
+ const token = {};
79
+ selection = token;
80
+ picker = undefined;
81
+ try {
82
+ const file = input.files?.[0];
83
+ if (file) await importHook.handleChange({ file });
84
+ } catch {
85
+ // The hook exposes a sanitized error; event handlers must not reject globally.
86
+ } finally {
87
+ if (currentScope(origin) && selection === token && fileInput === input) {
88
+ input.value = '';
89
+ selection = undefined;
90
+ }
91
+ }
92
+ };
93
+ });
56
94
 
57
95
  const displayText = $derived(label ?? i18n.t('common.import'));
58
96
  </script>
59
97
 
60
98
  {#if !hidden}
61
- <input
62
- type="file"
63
- accept={importHook.inputProps.accept}
64
- class="svadmin-u-99d72c7fc3e2"
65
- bind:this={fileInput}
66
- onchange={handleFileChange}
67
- />
99
+ {#key scope}
100
+ <input
101
+ type="file"
102
+ accept={importHook.inputProps.accept}
103
+ class="svadmin-u-99d72c7fc3e2"
104
+ bind:this={fileInput}
105
+ onchange={handleFileChange}
106
+ />
107
+ {/key}
68
108
  <Button
109
+ type="button"
69
110
  variant="outline"
70
111
  size={hideText ? 'icon' : 'sm'}
71
112
  class={className}
72
113
  aria-label={hideText ? displayText : undefined}
73
- disabled={importHook.isLoading || !can.allowed}
114
+ disabled={importHook.isLoading || !allowed}
115
+ aria-busy={importHook.isLoading}
74
116
  onclick={triggerImport}
75
117
  >
76
118
  <Upload class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
@@ -84,4 +126,7 @@
84
126
  </span>
85
127
  {/if}
86
128
  </Button>
129
+ {#if importHook.error}
130
+ <p role="alert">{i18n.t('common.operationFailed')}</p>
131
+ {/if}
87
132
  {/if}
@@ -1,20 +1,16 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import { type ImportResult, type ContractSchemas } from '@svadmin/core';
2
3
  import type { ButtonAccessControl } from './access-control';
3
- type $$ComponentProps = {
4
+ interface Props {
4
5
  resource: string;
5
6
  label?: string;
6
7
  children?: Snippet;
7
8
  hideText?: boolean;
8
- onFinish?: (result: {
9
- succeeded: unknown[];
10
- errored: {
11
- request: unknown;
12
- error: unknown;
13
- }[];
14
- }) => void;
9
+ onFinish?: (result: ImportResult<ContractSchemas>) => void | Promise<void>;
10
+ mapData?: (row: Record<string, unknown>) => unknown;
15
11
  accessControl?: ButtonAccessControl;
16
12
  class?: string;
17
- };
18
- declare const ImportButton: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ }
14
+ declare const ImportButton: import("svelte").Component<Props, {}, "">;
19
15
  type ImportButton = ReturnType<typeof ImportButton>;
20
16
  export default ImportButton;