@svadmin/ui 0.70.0 → 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 +6 -19
  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
@@ -1,121 +1,27 @@
1
1
  <script lang="ts">
2
- import { captureAdminContext, getResource, useCan, useNavigation, useShow } from '@svadmin/core';
2
+ import { definedOptions } from '@svadmin/core/options';
3
+ import { captureAdminContext } from '@svadmin/core';
3
4
  import { useTranslation } from '@svadmin/core/i18n';
4
- import { Eye, Pencil } from '@lucide/svelte';
5
- import CanAccess from './CanAccess.svelte';
5
+ import BoundRecordDetailDrawer from './BoundRecordDetailDrawer.svelte';
6
6
  import DetailDrawer from './DetailDrawer.svelte';
7
- import { getDisplayComponent } from './fieldComponentMap';
8
- import { Button } from './ui/button/index.js';
9
- import { Skeleton } from './ui/skeleton/index.js';
10
7
 
11
- let {
12
- resourceName,
13
- open = $bindable(false),
14
- recordId,
15
- onClose,
16
- } = $props<{
8
+ let { resourceName, open = $bindable(false), recordId, onClose }: {
17
9
  resourceName: string;
18
10
  open?: boolean;
19
11
  recordId?: string | number;
20
12
  onClose?: () => void;
21
- }>();
22
-
13
+ } = $props();
14
+ const context = captureAdminContext();
23
15
  const i18n = useTranslation();
24
- const adminContext = captureAdminContext();
25
- const navigation = useNavigation();
26
- const resource = $derived(getResource(resourceName));
27
- const showFields = $derived(resource.fields.filter((field) => field.showInShow !== false));
28
- const accessControlEnabled = $derived(!!adminContext.accessControlProvider);
29
- const showPermission = useCan(() => ({
30
- resource: resourceName,
31
- action: 'show',
32
- params: recordId == null ? undefined : { id: recordId },
33
- queryOptions: { enabled: open && recordId != null && accessControlEnabled },
34
- }));
35
- const permissionPending = $derived(
36
- accessControlEnabled && open && recordId != null && showPermission.isLoading
37
- );
38
- const canReadRecord = $derived(
39
- resource.canShow !== false && (!accessControlEnabled || (!permissionPending && showPermission.allowed))
40
- );
41
- const query = useShow({
42
- get resource() { return resourceName; },
43
- get id() { return recordId; },
44
- get queryOptions() { return { enabled: open && recordId != null && canReadRecord }; },
45
- });
46
-
47
- function navigateTo(action: 'show' | 'edit'): void {
48
- if (recordId == null) return;
49
- navigation[action](resourceName, recordId);
50
- open = false;
51
- }
52
16
  </script>
53
17
 
54
- <DetailDrawer
55
- bind:open
56
- title="{resource.label} {i18n.t('common.detail')}"
57
- description={recordId == null ? undefined : `#${recordId}`}
58
- closeLabel={i18n.t('common.close')}
59
- width="w-full sm:max-w-xl"
60
- {onClose}
61
- data-svadmin-record-detail
62
- >
63
- {#if permissionPending || (canReadRecord && query.isLoading)}
64
- <div class="svadmin-u-fa6acbf81d74 svadmin-u-d2c3932343f5" role="status" aria-live="polite" aria-label={i18n.t('common.loading')} aria-busy="true">
65
- {#each showFields.slice(0, 6) as field (field.key)}
66
- <div class="svadmin-u-f3c543ad5fe9 svadmin-u-77a2a20e90d4 svadmin-u-cb11fec3bb46 svadmin-u-d9bdd3d643a2 svadmin-u-022e8076dfea">
67
- <Skeleton class="svadmin-u-11e59c6d5f6b svadmin-u-69da7e4ff95d" />
68
- <Skeleton class="svadmin-u-11e59c6d5f6b svadmin-u-6da6a3c3f741 svadmin-u-35b2f3aff60f" />
69
- </div>
70
- {/each}
71
- </div>
72
- {:else if !canReadRecord}
73
- <div class="svadmin-u-61357c0c2f29 svadmin-u-ca6bf63030aa" data-svadmin-access-denied>
74
- <p class="svadmin-u-fc7473ca09eb svadmin-u-bfa603190748">{i18n.t('common.accessDenied')}</p>
75
- </div>
76
- {:else if query.data?.data}
77
- {@const record = query.data.data as Record<string, unknown>}
78
- <dl class="svadmin-u-fa6acbf81d74 svadmin-u-d2c3932343f5">
79
- {#each showFields as field (field.key)}
80
- {@const value = record[field.key]}
81
- {@const DisplayComponent = getDisplayComponent(field.type)}
82
- <div class="svadmin-u-f3c543ad5fe9 svadmin-u-58284b4ea568 svadmin-u-cb11fec3bb46 svadmin-u-d9bdd3d643a2 svadmin-u-022e8076dfea">
83
- <dt class="svadmin-u-359090c2d529 svadmin-u-2689f3958069 svadmin-u-bfa603190748 svadmin-u-e2327d142859">{field.label}</dt>
84
- <dd class="svadmin-u-7e0b7cdf1a94 svadmin-u-170cee3ff4e4 svadmin-u-fc7473ca09eb svadmin-u-d4108abe6359">
85
- {#if DisplayComponent && value != null}
86
- <DisplayComponent {value} options={field.options} resourceName={field.resource} />
87
- {:else}
88
- {value != null ? String(value) : '—'}
89
- {/if}
90
- </dd>
91
- </div>
92
- {/each}
93
- </dl>
18
+ {#if open}
19
+ {#if recordId != null}
20
+ <BoundRecordDetailDrawer {resourceName} {recordId} bind:open {...definedOptions({ onClose })} />
94
21
  {:else}
95
- <div class="svadmin-u-61357c0c2f29 svadmin-u-ca6bf63030aa">
96
- <p class="svadmin-u-fc7473ca09eb svadmin-u-bfa603190748">{i18n.t('common.noData')}</p>
97
- {#if query.isError}
98
- <p class="svadmin-u-50d0d216a2f8 svadmin-u-fc7473ca09eb svadmin-u-811148b13d1e">{(query.error as Error)?.message ?? i18n.t('common.operationFailed')}</p>
99
- {/if}
100
- </div>
22
+ <DetailDrawer bind:open title="{context.getResource(resourceName).label} {i18n.t('common.detail')}"
23
+ closeLabel={i18n.t('common.close')} {...definedOptions({ onClose })} data-svadmin-record-detail>
24
+ <p>{i18n.t('common.noData')}</p>
25
+ </DetailDrawer>
101
26
  {/if}
102
-
103
- {#snippet footer()}
104
- {#if canReadRecord && recordId != null && resource.canShow !== false}
105
- <CanAccess resource={resourceName} action="show" params={{ id: recordId }}>
106
- <Button variant="outline" onclick={() => navigateTo('show')}>
107
- <Eye class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" data-icon="inline-start" aria-hidden="true" />
108
- {i18n.t('common.openFullDetail')}
109
- </Button>
110
- </CanAccess>
111
- {/if}
112
- {#if canReadRecord && recordId != null && resource.canEdit !== false}
113
- <CanAccess resource={resourceName} action="edit" params={{ id: recordId }}>
114
- <Button onclick={() => navigateTo('edit')}>
115
- <Pencil class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" data-icon="inline-start" aria-hidden="true" />
116
- {i18n.t('common.edit')}
117
- </Button>
118
- </CanAccess>
119
- {/if}
120
- {/snippet}
121
- </DetailDrawer>
27
+ {/if}
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import { useCan } from '@svadmin/core';
3
+ import { useTranslation } from '@svadmin/core/i18n';
4
+ import { Eye, Pencil, PanelRight, Trash2 } from '@lucide/svelte';
5
+ import RowActions, { type RowActionItem } from './RowActions.svelte';
6
+ import TooltipButton from './TooltipButton.svelte';
7
+
8
+ let { resourceName, id, canShow, canEdit, canDelete, onShow, onEdit, onQuickEdit, onDelete }: {
9
+ resourceName: string;
10
+ id: string | number;
11
+ canShow: boolean;
12
+ canEdit: boolean;
13
+ canDelete: boolean;
14
+ onShow: () => void;
15
+ onEdit: () => void;
16
+ onQuickEdit: () => void;
17
+ onDelete: () => void;
18
+ } = $props();
19
+ const i18n = useTranslation();
20
+ const show = useCan(() => ({ resource: resourceName, action: 'show', id, queryOptions: { enabled: canShow } }));
21
+ const edit = useCan(() => ({ resource: resourceName, action: 'edit', id, queryOptions: { enabled: canEdit } }));
22
+ const remove = useCan(() => ({ resource: resourceName, action: 'delete', id, queryOptions: { enabled: canDelete } }));
23
+ // 编辑保持为可见主按钮;删除和快速编辑放入更多菜单。
24
+ const actions = $derived<RowActionItem[]>([
25
+ { label: i18n.t('common.quickEdit'), icon: PanelRight, hidden: !canEdit || edit.allowed !== true, onclick: onQuickEdit },
26
+ { label: i18n.t('common.delete'), icon: Trash2, hidden: !canDelete || remove.allowed !== true, danger: true, onclick: onDelete },
27
+ ]);
28
+ </script>
29
+
30
+ <RowActions {actions} maxVisible={0} moreLabel={i18n.t('common.moreActions')}>
31
+ {#if canShow && show.allowed === true}
32
+ <TooltipButton tooltip={i18n.t('common.detail')} variant="ghost" size="icon-sm" onclick={onShow}>
33
+ <Eye class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
34
+ </TooltipButton>
35
+ {/if}
36
+ {#if canEdit && edit.allowed === true}
37
+ <TooltipButton tooltip={i18n.t('common.edit')} variant="ghost" size="icon-sm" onclick={onEdit}>
38
+ <Pencil class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
39
+ </TooltipButton>
40
+ {/if}
41
+ </RowActions>
@@ -0,0 +1,14 @@
1
+ type $$ComponentProps = {
2
+ resourceName: string;
3
+ id: string | number;
4
+ canShow: boolean;
5
+ canEdit: boolean;
6
+ canDelete: boolean;
7
+ onShow: () => void;
8
+ onEdit: () => void;
9
+ onQuickEdit: () => void;
10
+ onDelete: () => void;
11
+ };
12
+ declare const RecordRowActions: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type RecordRowActions = ReturnType<typeof RecordRowActions>;
14
+ export default RecordRowActions;
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { definedOptions } from '@svadmin/core/options';
3
+
2
4
  import type { Action } from '@svadmin/core';
3
5
  import { captureAdminContext, useCan } from '@svadmin/core';
4
6
  import type { Snippet } from 'svelte';
@@ -34,18 +36,18 @@
34
36
  );
35
37
  const sourceReadAllowedByResource = $derived(!requireSourceRead || resource.canShow !== false);
36
38
  const params = $derived(id === undefined ? undefined : { id });
37
- const permission = useCan(() => ({
39
+ const permission = useCan(() => (definedOptions({
38
40
  resource: resourceName,
39
41
  action,
40
42
  params,
41
43
  queryOptions: { enabled: accessControlEnabled },
42
- }));
43
- const sourceReadPermission = useCan(() => ({
44
+ })));
45
+ const sourceReadPermission = useCan(() => (definedOptions({
44
46
  resource: resourceName,
45
47
  action: 'show',
46
48
  params,
47
49
  queryOptions: { enabled: accessControlEnabled && requireSourceRead },
48
- }));
50
+ })));
49
51
  const permissionPending = $derived(
50
52
  accessControlEnabled
51
53
  && (permission.isLoading || (requireSourceRead && sourceReadPermission.isLoading)),
@@ -64,5 +66,5 @@
64
66
  {:else if allowed}
65
67
  {@render children()}
66
68
  {:else}
67
- <DataState state="forbidden" description={denialReason} />
69
+ <DataState state="forbidden" {...definedOptions({ "description": denialReason })} />
68
70
  {/if}
@@ -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 handleRetry() {
36
36
  if (!taskProvider?.retry || pending) return;
37
+ const scopedTaskId = taskId;
38
+ const scope = adminContext.queryKeyMatcher();
37
39
  pending = true;
38
40
  try {
39
- await taskProvider.retry(taskId);
41
+ const provider = withValidatedTaskProvider(taskProvider);
42
+ if (!provider.retry) return;
43
+ await provider.retry(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
  ]);
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { definedOptions } from '@svadmin/core/options';
3
+
2
4
  import { Ellipsis } from '@lucide/svelte';
3
5
  import { tick, type Component, type Snippet } from 'svelte';
4
6
  import type { HTMLAttributes } from 'svelte/elements';
@@ -143,7 +145,7 @@
143
145
  {#each visibleActions as action (action)}
144
146
  {#if action.href}
145
147
  <Button
146
- variant={resolvedVariant(action)}
148
+ {...definedOptions({ "variant": resolvedVariant(action) })}
147
149
  size="sm"
148
150
  href={action.href}
149
151
  disabled={action.disabled}
@@ -158,7 +160,7 @@
158
160
  </Button>
159
161
  {:else}
160
162
  <Button
161
- variant={resolvedVariant(action)}
163
+ {...definedOptions({ "variant": resolvedVariant(action) })}
162
164
  size="sm"
163
165
  onclick={action.onclick}
164
166
  disabled={action.disabled}
@@ -188,7 +190,7 @@
188
190
  {#if action.href}
189
191
  <Button
190
192
  href={action.href}
191
- variant={resolvedVariant(action)}
193
+ {...definedOptions({ "variant": resolvedVariant(action) })}
192
194
  size="sm"
193
195
  disabled={action.disabled}
194
196
  onclick={(event) => handleOverflowAction(action, event)}
@@ -48,10 +48,10 @@
48
48
  : '********';
49
49
  case 'email': {
50
50
  const parts = val.split('@');
51
- if (parts.length === 2) {
52
- const name = parts[0];
51
+ const [name, domain] = parts;
52
+ if (parts.length === 2 && name && domain) {
53
53
  const maskedName = name.length > 2 ? `${name[0]}***${name.slice(-1)}` : `${name[0]}***`;
54
- return `${maskedName}@${parts[1]}`;
54
+ return `${maskedName}@${domain}`;
55
55
  }
56
56
  return '***@***.***';
57
57
  }
@@ -108,7 +108,7 @@
108
108
  const i18n = useTranslation();
109
109
 
110
110
  let activeKey = $derived.by(() => {
111
- const tab = getParams().tab;
111
+ const tab = getParams()['tab'];
112
112
  if (!tab) return "profile";
113
113
  const alias = tab === "api-keys" ? "api" : tab === "audit-logs" ? "audit" : tab;
114
114
  if (sectionKeys.has(alias)) return alias;
@@ -1,15 +1,17 @@
1
1
  <script lang="ts">
2
- import { useNavigation, useShow, getResource } from '@svadmin/core';
2
+ import { useNavigation } from '@svadmin/core';
3
+ import { useRecordDetail } from './record-detail.svelte';
3
4
  import { useTranslation } from '@svadmin/core/i18n';
4
5
  import type { Snippet } from 'svelte';
5
6
  import * as Card from './ui/card/index.js';
6
7
  import { Skeleton } from './ui/skeleton/index.js';
7
8
  import PageHeader from './PageHeader.svelte';
8
- import { getDisplayComponent } from './fieldComponentMap.js';
9
+ import FieldDisplay from './FieldDisplay.svelte';
9
10
  import EditButton from './buttons/EditButton.svelte';
10
11
  import DeleteButton from './buttons/DeleteButton.svelte';
11
12
  import CloneButton from './buttons/CloneButton.svelte';
12
- import RefreshButton from './buttons/RefreshButton.svelte';
13
+ import { Button } from './ui/button/index.js';
14
+ import { RefreshCw } from '@lucide/svelte';
13
15
 
14
16
  const i18n = useTranslation();
15
17
 
@@ -40,10 +42,10 @@
40
42
  const navigation = useNavigation();
41
43
  const isCompact = $derived(density === 'compact');
42
44
 
43
- const resource = $derived(getResource(resourceName));
44
- const showFields = $derived(resource.fields.filter(f => f.showInShow !== false));
45
-
46
- const query = useShow({ get resource() { return resourceName; }, get id() { return id; } });
45
+ const detail = useRecordDetail(() => ({ resourceName, id }));
46
+ const resource = $derived(detail.resource);
47
+ const showFields = $derived(detail.fields);
48
+ const query = detail.query;
47
49
 
48
50
  const gridColumnClass = $derived.by(() => {
49
51
  switch (columns) {
@@ -63,23 +65,27 @@
63
65
  backLabel={i18n.t('common.backToList')}
64
66
  >
65
67
  {#snippet actions()}
66
- {#if resource.canEdit !== false}
68
+ {#if detail.canRead && query.isSuccess && resource.canEdit !== false}
67
69
  <EditButton resource={resourceName} recordItemId={id} hideText />
68
70
  {/if}
69
- {#if resource.canCreate !== false}
71
+ {#if detail.canRead && query.isSuccess && resource.canCreate !== false}
70
72
  <CloneButton resource={resourceName} recordItemId={id} hideText />
71
73
  {/if}
72
- {#if resource.canDelete !== false}
74
+ {#if detail.canRead && query.isSuccess && resource.canDelete !== false}
73
75
  <DeleteButton resource={resourceName} recordItemId={id} hideText onSuccess={() => navigation.list(resourceName)} />
74
76
  {/if}
75
- <RefreshButton resource={resourceName} hideText />
77
+ <Button type="button" variant="ghost" size="icon" title={i18n.t('common.refresh')}
78
+ aria-label={i18n.t('common.refresh')} aria-busy={query.isFetching}
79
+ disabled={!detail.canRead || query.isFetching} onclick={detail.refresh}>
80
+ <RefreshCw class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
81
+ </Button>
76
82
  {#if headerActions}
77
83
  {@render headerActions()}
78
84
  {/if}
79
85
  {/snippet}
80
86
  </PageHeader>
81
87
 
82
- {#if query.isLoading}
88
+ {#if detail.checkingPermission || query.isLoading}
83
89
  <Card.Root class="svadmin-u-2cd02d11d1af svadmin-u-6ee2d41e2d2d svadmin-u-438b2237b8d6">
84
90
  <Card.Content class="svadmin-u-8a539c7fe216">
85
91
  {#each showFields.slice(0, 6) as _, i (i)}
@@ -90,26 +96,33 @@
90
96
  {/each}
91
97
  </Card.Content>
92
98
  </Card.Root>
93
- {:else if query.data?.data}
99
+ {:else if !detail.canRead}
100
+ <p role="status" data-svadmin-access-denied>{i18n.t('common.accessDenied')}</p>
101
+ {:else if query.isError}
102
+ <div role="alert">
103
+ <p>{i18n.t('common.operationFailed')}</p>
104
+ <Button type="button" variant="ghost" size="icon" title={i18n.t('common.retry')}
105
+ aria-label={i18n.t('common.retry')} disabled={query.isFetching} onclick={detail.refresh}>
106
+ <RefreshCw class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
107
+ </Button>
108
+ </div>
109
+ {:else if query.isSuccess}
110
+ {@const record = query.data.data}
94
111
  {#if layout === 'grid'}
95
112
  <Card.Root class="svadmin-u-2cd02d11d1af svadmin-u-6ee2d41e2d2d svadmin-u-438b2237b8d6">
96
113
  <Card.Content class="svadmin-u-8a539c7fe216">
97
114
  <dl class="svadmin-u-f3c543ad5fe9 svadmin-u-fa6acbf81d74 svadmin-u-3533cbce9e93 svadmin-u-4d9da416df16 {gridColumnClass}">
98
115
  {#each showFields as field, i (i)}
99
- {@const value = ((query.data as { data: Record<string, unknown> }).data as Record<string, unknown>)[field.key]}
100
- {@const DisplayComponent = getDisplayComponent(field.type)}
116
+ {@const value = record[field.key]}
101
117
  <div class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed {bordered ? 'svadmin-u-65fdbade2025 svadmin-u-5ceb636bd9f3 svadmin-u-cdf6e054e5e0 svadmin-u-b4cf72cd1cd8 svadmin-u-eb6e8b881acd svadmin-u-1884518576f2' : 'svadmin-u-eb6e8b881acd svadmin-u-1884518576f2 svadmin-u-65fdbade2025 svadmin-u-945ecb9a9005'}">
102
118
  <dt class="svadmin-u-359090c2d529 svadmin-u-2689f3958069 svadmin-u-bfa603190748 svadmin-u-65281709dacf">{field.label}</dt>
103
119
  <dd class="svadmin-u-359090c2d529 svadmin-u-e2327d142859 svadmin-u-d4108abe6359 svadmin-u-2689f3958069 svadmin-u-170cee3ff4e4">
104
- {#if DisplayComponent && value != null}
105
- <DisplayComponent
106
- {value}
107
- options={field.options}
108
- resourceName={field.resource}
109
- />
110
- {:else}
111
- {value != null ? String(value) : '—'}
112
- {/if}
120
+ <FieldDisplay
121
+ type={field.type}
122
+ {value}
123
+ options={field.options}
124
+ resourceName={field.resource}
125
+ />
113
126
  </dd>
114
127
  </div>
115
128
  {/each}
@@ -120,20 +133,16 @@
120
133
  <Card.Root class="svadmin-u-2cd02d11d1af svadmin-u-6ee2d41e2d2d svadmin-u-438b2237b8d6">
121
134
  <Card.Content class="svadmin-u-8a539c7fe216 svadmin-u-fa6acbf81d74 svadmin-u-3533cbce9e93">
122
135
  {#each showFields as field, i (i)}
123
- {@const value = ((query.data as { data: Record<string, unknown> }).data as Record<string, unknown>)[field.key]}
124
- {@const DisplayComponent = getDisplayComponent(field.type)}
136
+ {@const value = record[field.key]}
125
137
  <div class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-020ba687fa12 {isCompact ? 'svadmin-u-f0faeb26d656 svadmin-u-03b4dd7f172b svadmin-u-cc06a6575385 svadmin-u-7209fd2a90e7' : 'svadmin-u-f0faeb26d656 svadmin-u-7a9aabfcd059 svadmin-u-1b2d54a3fd12 svadmin-u-4701b613ff95'} {i % 2 === 1 ? 'svadmin-u-8a25a995eb8e' : ''}">
126
138
  <div class="svadmin-u-b03f3c253900 {isCompact ? 'svadmin-u-359090c2d529' : 'svadmin-u-359090c2d529 svadmin-u-e2327d142859'} svadmin-u-2689f3958069 svadmin-u-bfa603190748 svadmin-u-65281709dacf svadmin-u-84bdd56b0e81">{field.label}</div>
127
139
  <div class="svadmin-u-6b3526212209 {isCompact ? 'svadmin-u-359090c2d529 svadmin-u-e2327d142859' : 'svadmin-u-fc7473ca09eb'} svadmin-u-d4108abe6359">
128
- {#if DisplayComponent && value != null}
129
- <DisplayComponent
130
- {value}
131
- options={field.options}
132
- resourceName={field.resource}
133
- />
134
- {:else}
135
- {value != null ? String(value) : '—'}
136
- {/if}
140
+ <FieldDisplay
141
+ type={field.type}
142
+ {value}
143
+ options={field.options}
144
+ resourceName={field.resource}
145
+ />
137
146
  </div>
138
147
  </div>
139
148
  {/each}
@@ -147,9 +156,6 @@
147
156
  <Card.Root class="svadmin-u-2cd02d11d1af svadmin-u-6ee2d41e2d2d svadmin-u-438b2237b8d6">
148
157
  <Card.Content class="svadmin-u-845f53365c8d svadmin-u-ca6bf63030aa">
149
158
  <p class="svadmin-u-bfa603190748">{i18n.t('common.noData')}</p>
150
- {#if query.isError}
151
- <p class="svadmin-u-fc7473ca09eb svadmin-u-811148b13d1e svadmin-u-50d0d216a2f8">{(query.error as Error)?.message ?? i18n.t('common.operationFailed')}</p>
152
- {/if}
153
159
  </Card.Content>
154
160
  </Card.Root>
155
161
  {/if}
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { definedOptions } from '@svadmin/core/options';
3
+
2
4
  import { captureAdminContext } from '@svadmin/core';
3
5
  import type { AccessControlProvider, Action, Identity, MenuItem } from '@svadmin/core';
4
6
  import { getPath } from '../router-state.svelte.js';
@@ -107,11 +109,11 @@
107
109
  async function checkAccess(provider: AccessControlProvider | null, resource: string, action: Action): Promise<boolean> {
108
110
  if (!provider) return true;
109
111
 
110
- const result = await provider.can({
112
+ const result = await provider.can(definedOptions({
111
113
  resource,
112
114
  action,
113
115
  meta: adminContext.getProviderMeta(resource),
114
- });
116
+ }));
115
117
  return Array.isArray(result) ? (result[0]?.can ?? false) : result.can;
116
118
  }
117
119
 
@@ -132,11 +134,11 @@
132
134
  }
133
135
 
134
136
  function snapshotCustomMenuItems(menuItems: MenuItem[]): MenuItem[] {
135
- return menuItems.map((menuItem) => ({
137
+ return menuItems.map((menuItem) => (definedOptions({
136
138
  ...menuItem,
137
139
  meta: menuItem.meta ? { ...menuItem.meta } : undefined,
138
140
  children: menuItem.children ? snapshotCustomMenuItems(menuItem.children) : undefined,
139
- }));
141
+ })));
140
142
  }
141
143
 
142
144
  async function visibleCustomMenuItem(menuItem: MenuItem, provider: AccessControlProvider | null): Promise<MenuItem | null> {
@@ -188,12 +190,12 @@
188
190
  const items: NavItem[] = [{ path: '/', label: homeLabel, Icon: LayoutDashboard }];
189
191
  for (const { r, can } of results) {
190
192
  if (can) {
191
- items.push({
193
+ items.push(definedOptions({
192
194
  path: `/${r.name}`,
193
195
  label: r.label,
194
196
  Icon: (typeof r.icon === 'string' ? iconMap[r.icon] : r.icon) ?? iconMap[r.name] ?? Settings,
195
197
  group: r.group,
196
- });
198
+ }));
197
199
  }
198
200
  }
199
201
  navItems = items;
@@ -454,14 +456,14 @@
454
456
  <div class="svadmin-u-0e17f2bd9074 svadmin-u-7fcf9124b5df svadmin-u-6b7d6e21ccbd">
455
457
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-7e9a2a250cc3 svadmin-u-5f22e64f2282 svadmin-u-7660b450905a svadmin-u-ceb69a6b0e5f svadmin-u-ace81495deee svadmin-u-34516836730d">
456
458
  <Avatar
457
- src={(identity as Record<string, unknown>).avatar as string | undefined}
459
+ src={(identity as Record<string, unknown>)['avatar'] as string | undefined}
458
460
  alt={identity.name ?? 'User'}
459
461
  fallback={identity.name?.charAt(0).toUpperCase() ?? 'U'}
460
462
  size="sm"
461
463
  />
462
464
  <div class="svadmin-u-36e579c0b41c svadmin-u-7e0b7cdf1a94">
463
465
  <p class="svadmin-u-f283ea9bea0e svadmin-u-a14daebf7748 svadmin-u-2689f3958069 svadmin-u-a7a63217e098">{identity.name}</p>
464
- <p class="svadmin-u-f283ea9bea0e svadmin-u-d058ca6de60f svadmin-u-5f1ff8fe8768">{((identity as Record<string, unknown>).role || (identity as Record<string, unknown>).roleName) ?? 'User'}</p>
466
+ <p class="svadmin-u-f283ea9bea0e svadmin-u-d058ca6de60f svadmin-u-5f1ff8fe8768">{((identity as Record<string, unknown>)['role'] || (identity as Record<string, unknown>)['roleName']) ?? 'User'}</p>
465
467
  </div>
466
468
  <button
467
469
  class="svadmin-u-d0a52b312f7d svadmin-u-cbbf90f9a828 svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-421ac2be5045 svadmin-u-3e3534b4c5df svadmin-u-646e10356266 svadmin-u-0b48b877be2a svadmin-u-ceb69a6b0e5f"
@@ -35,10 +35,12 @@
35
35
  if (!containerEl) return;
36
36
  const rect = containerEl.getBoundingClientRect();
37
37
 
38
+ const pointer = 'touches' in ev ? ev.touches.item(0) : ev;
39
+ if (!pointer) return;
38
40
  const clientPos =
39
41
  direction === 'horizontal'
40
- ? ('touches' in ev ? ev.touches[0].clientX - rect.left : ev.clientX - rect.left)
41
- : ('touches' in ev ? ev.touches[0].clientY - rect.top : ev.clientY - rect.top);
42
+ ? pointer.clientX - rect.left
43
+ : pointer.clientY - rect.top;
42
44
  const totalSize = direction === 'horizontal' ? rect.width : rect.height;
43
45
 
44
46
  if (totalSize > 0) {