@svadmin/ui 0.72.0 → 0.73.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 (252) hide show
  1. package/README.md +40 -25
  2. package/THIRD_PARTY_NOTICES.md +52 -0
  3. package/dist/app.css +7694 -5117
  4. package/dist/app.theme.css +7645 -5119
  5. package/dist/class-properties.generated.d.ts +1 -0
  6. package/dist/class-properties.generated.js +2 -0
  7. package/dist/classnames.d.ts +8 -0
  8. package/dist/classnames.js +26 -0
  9. package/dist/components/AutoForm.svelte +20 -6
  10. package/dist/components/AutoForm.svelte.d.ts +2 -0
  11. package/dist/components/AutoTable.svelte +249 -9
  12. package/dist/components/AutoTable.svelte.d.ts +5 -0
  13. package/dist/components/BoundRecordDetailDrawer.svelte +8 -2
  14. package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +2 -0
  15. package/dist/components/ComboboxField.svelte +31 -9
  16. package/dist/components/ComboboxField.svelte.d.ts +3 -2
  17. package/dist/components/CreatePage.svelte +4 -0
  18. package/dist/components/CreatePage.svelte.d.ts +2 -0
  19. package/dist/components/DateRangeInput.svelte +75 -0
  20. package/dist/components/DateRangeInput.svelte.d.ts +25 -0
  21. package/dist/components/DateTimeInput.svelte +65 -0
  22. package/dist/components/DateTimeInput.svelte.d.ts +19 -0
  23. package/dist/components/DevTools.svelte +2 -2
  24. package/dist/components/EditPage.svelte +4 -0
  25. package/dist/components/EditPage.svelte.d.ts +2 -0
  26. package/dist/components/FieldRenderer.svelte +90 -18
  27. package/dist/components/FileUpload.svelte +250 -0
  28. package/dist/components/FileUpload.svelte.d.ts +32 -0
  29. package/dist/components/FilterBuilder.svelte +236 -279
  30. package/dist/components/FilterBuilder.svelte.d.ts +3 -0
  31. package/dist/components/ImportWizard.svelte +181 -94
  32. package/dist/components/JsonSchemaForm.svelte +255 -99
  33. package/dist/components/JsonSchemaForm.svelte.d.ts +12 -1
  34. package/dist/components/Layout.svelte +1 -2
  35. package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +1 -1
  36. package/dist/components/ListPage.svelte +4 -0
  37. package/dist/components/ListPage.svelte.d.ts +2 -0
  38. package/dist/components/LoginPage.svelte +1 -1
  39. package/dist/components/MoneyInput.svelte +25 -0
  40. package/dist/components/MoneyInput.svelte.d.ts +20 -0
  41. package/dist/components/NumberInput.svelte +25 -0
  42. package/dist/components/NumberInput.svelte.d.ts +19 -0
  43. package/dist/components/NumericInput.svelte +110 -0
  44. package/dist/components/NumericInput.svelte.d.ts +23 -0
  45. package/dist/components/PercentInput.svelte +25 -0
  46. package/dist/components/PercentInput.svelte.d.ts +20 -0
  47. package/dist/components/QuickEditDrawer.svelte +4 -1
  48. package/dist/components/QuickEditDrawer.svelte.d.ts +2 -0
  49. package/dist/components/RecordDetailDrawer.svelte +4 -2
  50. package/dist/components/RecordDetailDrawer.svelte.d.ts +2 -0
  51. package/dist/components/ResourceOperationsPage.svelte +25 -19
  52. package/dist/components/ResourceOperationsPage.svelte.d.ts +5 -1
  53. package/dist/components/ShowInferencer.svelte +4 -2
  54. package/dist/components/ShowInferencer.svelte.d.ts +2 -0
  55. package/dist/components/ShowPage.svelte +7 -3
  56. package/dist/components/ShowPage.svelte.d.ts +2 -0
  57. package/dist/components/Sidebar.svelte +5 -5
  58. package/dist/components/SpreadsheetView.svelte +81 -286
  59. package/dist/components/StepsForm.svelte +5 -5
  60. package/dist/components/SvarAutoTable.svelte +39 -0
  61. package/dist/components/SvarAutoTable.svelte.d.ts +13 -0
  62. package/dist/components/SvarDataGrid.svelte +362 -0
  63. package/dist/components/SvarDataGrid.svelte.d.ts +57 -0
  64. package/dist/components/SvarGridCell.svelte +10 -0
  65. package/dist/components/SvarGridCell.svelte.d.ts +7 -0
  66. package/dist/components/SvarResourceRead.svelte +50 -0
  67. package/dist/components/SvarResourceRead.svelte.d.ts +23 -0
  68. package/dist/components/SvarResourceTable.svelte +63 -0
  69. package/dist/components/SvarResourceTable.svelte.d.ts +42 -0
  70. package/dist/components/SvarResourceTableView.svelte +364 -0
  71. package/dist/components/SvarResourceTableView.svelte.d.ts +4 -0
  72. package/dist/components/TaskQueueDrawer.svelte +72 -16
  73. package/dist/components/TreeSelect.svelte +208 -28
  74. package/dist/components/TreeSelect.svelte.d.ts +3 -1
  75. package/dist/components/auto-table-grid.d.ts +26 -0
  76. package/dist/components/auto-table-grid.js +1 -0
  77. package/dist/components/content/ContentPageHeader.svelte +12 -9
  78. package/dist/components/content/ContentPageShell.svelte +6 -9
  79. package/dist/components/content/FileBrowser.svelte +35 -0
  80. package/dist/components/content/FileBrowser.svelte.d.ts +14 -0
  81. package/dist/components/content/MetricBlock.svelte +10 -13
  82. package/dist/components/content/NetworkTable.svelte +3 -3
  83. package/dist/components/content/NetworkTable.svelte.d.ts +1 -0
  84. package/dist/components/content/PageToolbar.svelte +6 -10
  85. package/dist/components/content/SectionHeader.svelte +8 -13
  86. package/dist/components/content/SettingsFieldRow.svelte +9 -23
  87. package/dist/components/content/SettingsGroup.svelte +11 -27
  88. package/dist/components/content/StatusBadge.svelte +3 -8
  89. package/dist/components/content/WorkspaceLayout.svelte +8 -12
  90. package/dist/components/enterprise/filter-tree.d.ts +48 -0
  91. package/dist/components/enterprise/filter-tree.js +224 -0
  92. package/dist/components/enterprise/json-schema-form.d.ts +39 -0
  93. package/dist/components/enterprise/json-schema-form.js +195 -0
  94. package/dist/components/enterprise/recursive-schema-form.d.ts +30 -0
  95. package/dist/components/enterprise/recursive-schema-form.js +196 -0
  96. package/dist/components/enterprise/spreadsheet-formula.d.ts +12 -0
  97. package/dist/components/enterprise/spreadsheet-formula.js +245 -0
  98. package/dist/components/fieldComponentMap.js +5 -0
  99. package/dist/components/fields/DateField.svelte +4 -3
  100. package/dist/components/fields/date-field-value.d.ts +2 -0
  101. package/dist/components/fields/date-field-value.js +13 -0
  102. package/dist/components/json-schema-form-state.d.ts +37 -0
  103. package/dist/components/json-schema-form-state.js +168 -0
  104. package/dist/components/record-detail.svelte.d.ts +3 -0
  105. package/dist/components/record-detail.svelte.js +10 -4
  106. package/dist/components/svar-grid-cells.d.ts +14 -0
  107. package/dist/components/svar-grid-cells.js +2 -0
  108. package/dist/components/svar-grid-contract.d.ts +80 -0
  109. package/dist/components/svar-grid-contract.js +159 -0
  110. package/dist/components/svar-grid-interactions.d.ts +54 -0
  111. package/dist/components/svar-grid-interactions.js +174 -0
  112. package/dist/components/svar-grid-loading.d.ts +40 -0
  113. package/dist/components/svar-grid-loading.js +180 -0
  114. package/dist/components/svar-grid-operations.d.ts +37 -0
  115. package/dist/components/svar-grid-operations.js +186 -0
  116. package/dist/components/svar-grid-panes.d.ts +34 -0
  117. package/dist/components/svar-grid-panes.js +66 -0
  118. package/dist/components/svar-grid-resource-loading.d.ts +18 -0
  119. package/dist/components/svar-grid-resource-loading.js +68 -0
  120. package/dist/components/svar-grid-resource-session.svelte.d.ts +29 -0
  121. package/dist/components/svar-grid-resource-session.svelte.js +61 -0
  122. package/dist/components/ui/badge/badge-variants.js +3 -1
  123. package/dist/components/ui/button/button-variants.js +5 -2
  124. package/dist/components/ui/input/input.svelte +1 -4
  125. package/dist/components/ui/textarea/textarea.svelte +2 -1
  126. package/dist/components.css +3079 -0
  127. package/dist/design-contract.d.ts +13 -0
  128. package/dist/design-contract.js +11 -0
  129. package/dist/design-system.d.ts +15 -0
  130. package/dist/design-system.js +7 -0
  131. package/dist/index.d.ts +14 -0
  132. package/dist/index.js +9 -0
  133. package/dist/page-chrome.css +12 -49
  134. package/dist/recipes.d.ts +4 -0
  135. package/dist/recipes.js +4 -0
  136. package/dist/rendering/index.d.ts +87 -0
  137. package/dist/rendering/index.js +117 -0
  138. package/dist/styled-system/css/conditions.js +36 -0
  139. package/dist/styled-system/css/css.d.ts +22 -0
  140. package/dist/styled-system/css/css.js +45 -0
  141. package/dist/styled-system/css/cva.d.ts +6 -0
  142. package/dist/styled-system/css/cva.js +87 -0
  143. package/dist/styled-system/css/cx.d.ts +5 -0
  144. package/dist/styled-system/css/cx.js +15 -0
  145. package/dist/styled-system/css/index.d.ts +5 -0
  146. package/dist/styled-system/css/index.js +4 -0
  147. package/dist/styled-system/css/sva.d.ts +4 -0
  148. package/dist/styled-system/css/sva.js +46 -0
  149. package/dist/styled-system/helpers.js +316 -0
  150. package/dist/styled-system/patterns/aspect-ratio.d.ts +20 -0
  151. package/dist/styled-system/patterns/aspect-ratio.js +38 -0
  152. package/dist/styled-system/patterns/bleed.d.ts +21 -0
  153. package/dist/styled-system/patterns/bleed.js +24 -0
  154. package/dist/styled-system/patterns/box.d.ts +20 -0
  155. package/dist/styled-system/patterns/box.js +15 -0
  156. package/dist/styled-system/patterns/center.d.ts +20 -0
  157. package/dist/styled-system/patterns/center.js +21 -0
  158. package/dist/styled-system/patterns/circle.d.ts +20 -0
  159. package/dist/styled-system/patterns/circle.js +25 -0
  160. package/dist/styled-system/patterns/container.d.ts +20 -0
  161. package/dist/styled-system/patterns/container.js +21 -0
  162. package/dist/styled-system/patterns/cq.d.ts +21 -0
  163. package/dist/styled-system/patterns/cq.js +21 -0
  164. package/dist/styled-system/patterns/divider.d.ts +22 -0
  165. package/dist/styled-system/patterns/divider.js +25 -0
  166. package/dist/styled-system/patterns/flex.d.ts +26 -0
  167. package/dist/styled-system/patterns/flex.js +26 -0
  168. package/dist/styled-system/patterns/float.d.ts +23 -0
  169. package/dist/styled-system/patterns/float.js +52 -0
  170. package/dist/styled-system/patterns/grid-item.d.ts +25 -0
  171. package/dist/styled-system/patterns/grid-item.js +25 -0
  172. package/dist/styled-system/patterns/grid.d.ts +24 -0
  173. package/dist/styled-system/patterns/grid.js +27 -0
  174. package/dist/styled-system/patterns/hstack.d.ts +21 -0
  175. package/dist/styled-system/patterns/hstack.js +24 -0
  176. package/dist/styled-system/patterns/index.d.ts +21 -0
  177. package/dist/styled-system/patterns/index.js +20 -0
  178. package/dist/styled-system/patterns/link-overlay.d.ts +20 -0
  179. package/dist/styled-system/patterns/link-overlay.js +24 -0
  180. package/dist/styled-system/patterns/spacer.d.ts +20 -0
  181. package/dist/styled-system/patterns/spacer.js +25 -0
  182. package/dist/styled-system/patterns/square.d.ts +20 -0
  183. package/dist/styled-system/patterns/square.js +24 -0
  184. package/dist/styled-system/patterns/stack.d.ts +23 -0
  185. package/dist/styled-system/patterns/stack.js +24 -0
  186. package/dist/styled-system/patterns/visually-hidden.d.ts +20 -0
  187. package/dist/styled-system/patterns/visually-hidden.js +18 -0
  188. package/dist/styled-system/patterns/vstack.d.ts +21 -0
  189. package/dist/styled-system/patterns/vstack.js +24 -0
  190. package/dist/styled-system/patterns/wrap.d.ts +24 -0
  191. package/dist/styled-system/patterns/wrap.js +25 -0
  192. package/dist/styled-system/recipes/content-header.d.ts +31 -0
  193. package/dist/styled-system/recipes/content-header.js +65 -0
  194. package/dist/styled-system/recipes/content-page.d.ts +34 -0
  195. package/dist/styled-system/recipes/content-page.js +43 -0
  196. package/dist/styled-system/recipes/create-recipe.js +82 -0
  197. package/dist/styled-system/recipes/index.d.ts +17 -0
  198. package/dist/styled-system/recipes/index.js +16 -0
  199. package/dist/styled-system/recipes/metric-block.d.ts +34 -0
  200. package/dist/styled-system/recipes/metric-block.js +76 -0
  201. package/dist/styled-system/recipes/product-list.d.ts +31 -0
  202. package/dist/styled-system/recipes/product-list.js +73 -0
  203. package/dist/styled-system/recipes/product-section.d.ts +31 -0
  204. package/dist/styled-system/recipes/product-section.js +49 -0
  205. package/dist/styled-system/recipes/product-settings-row.d.ts +34 -0
  206. package/dist/styled-system/recipes/product-settings-row.js +58 -0
  207. package/dist/styled-system/recipes/product-settings.d.ts +31 -0
  208. package/dist/styled-system/recipes/product-settings.js +57 -0
  209. package/dist/styled-system/recipes/product-status.d.ts +34 -0
  210. package/dist/styled-system/recipes/product-status.js +45 -0
  211. package/dist/styled-system/recipes/product-toolbar.d.ts +31 -0
  212. package/dist/styled-system/recipes/product-toolbar.js +41 -0
  213. package/dist/styled-system/recipes/product-workspace.d.ts +34 -0
  214. package/dist/styled-system/recipes/product-workspace.js +58 -0
  215. package/dist/styled-system/recipes/surface-metric.d.ts +38 -0
  216. package/dist/styled-system/recipes/surface-metric.js +63 -0
  217. package/dist/styled-system/recipes/surface-table.d.ts +34 -0
  218. package/dist/styled-system/recipes/surface-table.js +62 -0
  219. package/dist/styled-system/recipes/ui-badge.d.ts +31 -0
  220. package/dist/styled-system/recipes/ui-badge.js +38 -0
  221. package/dist/styled-system/recipes/ui-button.d.ts +32 -0
  222. package/dist/styled-system/recipes/ui-button.js +43 -0
  223. package/dist/styled-system/recipes/ui-input.d.ts +31 -0
  224. package/dist/styled-system/recipes/ui-input.js +49 -0
  225. package/dist/styled-system/recipes/ui-textarea.d.ts +31 -0
  226. package/dist/styled-system/recipes/ui-textarea.js +24 -0
  227. package/dist/styled-system/styles.css +1257 -0
  228. package/dist/styled-system/tokens/index.d.ts +9 -0
  229. package/dist/styled-system/tokens/index.js +268 -0
  230. package/dist/styled-system/tokens/tokens.d.ts +36 -0
  231. package/dist/styled-system/types/composition.d.ts +227 -0
  232. package/dist/styled-system/types/conditions.d.ts +236 -0
  233. package/dist/styled-system/types/csstype.d.ts +22570 -0
  234. package/dist/styled-system/types/index.d.ts +6 -0
  235. package/dist/styled-system/types/parts.d.ts +8 -0
  236. package/dist/styled-system/types/pattern.d.ts +78 -0
  237. package/dist/styled-system/types/prop-type.d.ts +223 -0
  238. package/dist/styled-system/types/recipe.d.ts +181 -0
  239. package/dist/styled-system/types/selectors.d.ts +59 -0
  240. package/dist/styled-system/types/static-css.d.ts +56 -0
  241. package/dist/styled-system/types/style-props.d.ts +8088 -0
  242. package/dist/styled-system/types/system-types.d.ts +151 -0
  243. package/dist/styles/aliases.css +49 -0
  244. package/dist/styles/compatibility.epilogue.css +384 -0
  245. package/dist/styles/compatibility.prelude.css +5513 -0
  246. package/dist/styles/conditional-states.css +28 -0
  247. package/dist/styles/conditional-utilities.css +22 -0
  248. package/dist/styles/recipes.css +1262 -0
  249. package/dist/utils.d.ts +1 -1
  250. package/dist/utils.js +1 -1
  251. package/package.json +31 -13
  252. package/styles-compatibility.json +12 -0
@@ -0,0 +1,364 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy, untrack } from 'svelte';
3
+ import { captureAdminContext, captureAuthSession, useResourceContract, useCan, useList, useUpdate, useDelete, getContractFormFields, type Filter, type Sort } from '@svadmin/core';
4
+ import { useTranslation } from '@svadmin/core/i18n';
5
+ import { snapshotPlainData, decodeBaseRecord } from '@svadmin/core/schema';
6
+ import SvarDataGrid from './SvarDataGrid.svelte';
7
+ import SvarResourceRead from './SvarResourceRead.svelte';
8
+ import { createSvarResourceReads } from './svar-grid-resource-session.svelte.js';
9
+ import { loadSvarResourceWindow, loadSvarResourceChildren, appendSvarResourcePage } from './svar-grid-resource-loading.js';
10
+ import { SvarLoadCancelled, type SvarWindowSource, type SvarChildrenLoader } from './svar-grid-loading.js';
11
+ import type { SvarSort, SvarTextFilter } from './svar-grid-contract.js';
12
+ import type { SvarInteractiveColumn, SvarCellEdit } from './svar-grid-interactions.js';
13
+ import { runSvarBatch, checkedSvarSelection, svarRecordIndex, svarRecordKey, prepareSvarExport, authorizeSvarExport, downloadSvarCsv, type SvarRecordId, type SvarBatchResult } from './svar-grid-operations.js';
14
+ import { readSavedListViews, serializeSavedListViews, savedListViewsStorageKey, type SavedListView, type SavedListViewState } from './saved-list-views.js';
15
+ import type { SvarResourceTableProps } from './SvarResourceTable.svelte';
16
+
17
+ let { Grid, Theme, resourceName, pageSize = 25, filters = [], initialSorters = [], height = 420,
18
+ freezeLeft = 0, freezeRight = 0, loadingMode = 'page', lazyTree, density = 'comfortable', childrenKey: providedChildrenKey, dataScopeKey = 0, disabled = false,
19
+ editable = false, selectable = false, batchUpdate = false, batchDelete = false, exportable = false,
20
+ savedViews = false, preferenceScopeKey, migrateAutoTableViews = false, deleteVariables, formats = {},
21
+ }: SvarResourceTableProps = $props();
22
+ const context = captureAdminContext();
23
+ const i18n = useTranslation();
24
+ const zh = $derived(i18n.locale.startsWith('zh'));
25
+ const binding = useResourceContract(() => resourceName);
26
+ const resource = $derived(context.getResource(resourceName));
27
+ const childrenKey = $derived(lazyTree ? providedChildrenKey ?? 'children' : providedChildrenKey);
28
+ const primaryKey = $derived(resource.primaryKey ?? 'id');
29
+ const permission = useCan(() => ({ resource: resourceName, action: 'list', meta: { svadminSvarScope: dataScopeKey } }));
30
+ const editPermission = useCan(() => ({ resource: resourceName, action: 'edit', meta: { svadminSvarScope: dataScopeKey }, queryOptions: { enabled: editable || batchUpdate } }));
31
+ const deletePermission = useCan(() => ({ resource: resourceName, action: 'delete', meta: { svadminSvarScope: dataScopeKey }, queryOptions: { enabled: batchDelete } }));
32
+ const exportPermission = useCan(() => ({ resource: resourceName, action: 'export', meta: { svadminSvarScope: dataScopeKey }, queryOptions: { enabled: exportable } }));
33
+ const writableFields = $derived.by(() => {
34
+ try { return new Set(getContractFormFields(binding.resource, 'edit')); }
35
+ catch { return new Set<string>(); }
36
+ });
37
+ const fields = $derived(resource.fields.filter(field => field.showInList !== false));
38
+ const editableFields = $derived(fields.filter(field => field.key !== primaryKey && field.key !== 'id'
39
+ && field.showInEdit !== false && writableFields.has(field.key) && ['text', 'number', 'email', 'url', 'textarea'].includes(field.type)));
40
+ let columnVisibility = $state<Record<string, boolean>>({});
41
+ let columnOrder = $state<string[]>([]);
42
+ const allColumns = $derived<SvarInteractiveColumn[]>(fields.map(field => {
43
+ const format = formats[field.key];
44
+ const canEdit = editable && resource.canEdit !== false && editPermission.allowed && editableFields.some(item => item.key === field.key);
45
+ return { key: field.key, label: field.label, sortable: field.sortable === true,
46
+ filterable: field.filterable === true && ['text', 'email', 'url', 'textarea'].includes(field.type),
47
+ ...(canEdit ? { editable: field.type === 'number' ? 'number' as const : 'text' as const } : {}),
48
+ ...(format === undefined ? {} : format) };
49
+ }));
50
+ const columns = $derived([...allColumns].sort((a, b) => {
51
+ const ai = columnOrder.indexOf(a.key), bi = columnOrder.indexOf(b.key);
52
+ return (ai < 0 ? allColumns.length : ai) - (bi < 0 ? allColumns.length : bi);
53
+ }).filter(column => columnVisibility[column.key] !== false));
54
+ let current = $state(1);
55
+ let viewPageSize = $state<number | undefined>(undefined);
56
+ const effectivePageSize = $derived(viewPageSize ?? pageSize);
57
+ let sorters = $state<Sort[]>(untrack(() => initialSorters.map(sort => ({ ...sort }))));
58
+ let textFilters = $state<SvarTextFilter[]>([]);
59
+ let viewFilters = $state<Filter[]>([]);
60
+ let viewSearch = $state('');
61
+ const validPageSize = $derived(Number.isSafeInteger(effectivePageSize) && effectivePageSize >= 1 && effectivePageSize <= 1000);
62
+ const searchable = $derived(resource.fields.filter(field => field.searchable));
63
+ const queryFilters = $derived<Filter[]>([...filters, ...viewFilters, ...textFilters,
64
+ ...(viewSearch && searchable.length ? [{ operator: 'or' as const, value: searchable.map(field => ({ field: field.key, operator: 'contains' as const, value: viewSearch })) }] : [])]);
65
+ const rootFilters = $derived<Filter[]>([...queryFilters, ...(lazyTree ? [{ field: lazyTree.parentField, operator: 'eq' as const, value: lazyTree.rootValue ?? null }] : [])]);
66
+ const validLoading = $derived(['page', 'window', 'infinite'].includes(loadingMode)
67
+ && !(loadingMode !== 'page' && childrenKey !== undefined)
68
+ && (!lazyTree || resource.fields.some(field => field.key === lazyTree.parentField)));
69
+ const queryPagination = $derived({ current, pageSize: effectivePageSize });
70
+ const query = useList({
71
+ get resource() { return binding.resource; }, get dataProviderName() { return binding.dataProviderName; },
72
+ get meta() { return { svadminSvarScope: dataScopeKey }; },
73
+ get queryOptions() { return { enabled: permission.allowed && validPageSize && validLoading && !disabled }; },
74
+ get pagination() { return queryPagination; }, get sorters() { return sorters; }, get filters() { return rootFilters; },
75
+ });
76
+ $effect.pre(() => { void pageSize; void filters; void loadingMode; void lazyTree; untrack(() => { current = 1; viewPageSize = undefined; }); });
77
+ const baseRecords = $derived(permission.allowed && !query.isError ? query.data?.data ?? [] : []);
78
+ let loadedRecords = $state.raw<readonly Record<string, unknown>[]>([]);
79
+ let infiniteRecords = $state.raw<readonly Record<string, unknown>[]>([]);
80
+ let refreshVersion = $state(0);
81
+ let dataVersion = $state(0);
82
+ const advanced = $derived(loadingMode !== 'page' || lazyTree !== undefined);
83
+ const records = $derived(advanced ? loadedRecords : baseRecords);
84
+ const gridItems = $derived(loadingMode === 'window' ? [] : loadingMode === 'infinite' ? infiniteRecords : baseRecords);
85
+ const total = $derived(permission.allowed ? query.data?.total ?? 0 : 0);
86
+ const pageCount = $derived(validPageSize ? Math.max(1, Math.ceil(total / effectivePageSize)) : 1);
87
+ const queryIdentity = $derived(JSON.stringify([resourceName, dataScopeKey, queryPagination, rootFilters, sorters, loadingMode, lazyTree]));
88
+ const error = $derived(query.isError ? (query.error instanceof Error ? query.error.message : zh ? '数据加载失败' : 'Data request failed') : undefined);
89
+ let selectedIds = $state<SvarRecordId[]>([]);
90
+ let busy = $state(false);
91
+ let feedback = $state('');
92
+ let result = $state.raw<SvarBatchResult>();
93
+ let confirmation = $state<'delete' | 'update' | undefined>();
94
+ let batchField = $state('');
95
+ let batchValue = $state('');
96
+ let alive = true;
97
+ let activeId = $state<SvarRecordId>(0);
98
+ let authorizedWrite = $state(false);
99
+ onDestroy(() => { alive = false; authorizedWrite = false; });
100
+ const operationScope = $derived({ resource: binding.resource, provider: context.getDataProviderForResource(resourceName),
101
+ auth: captureAuthSession(context.authProvider), access: context.accessControlProvider,
102
+ tenant: context.tenantCacheKey?.__svadminTenant, dataScopeKey, queryIdentity, refreshVersion, columns,
103
+ permission: permission.allowed, disabled, editable, batchUpdate, batchDelete, exportable });
104
+ const update = useUpdate({ get resource() { return binding.resource; }, get id() { return activeId; }, get enabled() { return alive && authorizedWrite && permission.allowed && !disabled; } });
105
+ const deletion = useDelete({ get resource() { return binding.resource; }, get id() { return activeId; }, get enabled() { return alive && authorizedWrite && permission.allowed && !disabled; } });
106
+ $effect.pre(() => { void queryIdentity; untrack(() => { selectedIds = []; confirmation = undefined; }); });
107
+
108
+ const reads = createSvarResourceReads({
109
+ scope: () => operationScope, current: scopeCurrent,
110
+ authorize: (scope, id) => allowed('list', id, scope),
111
+ resourceName: () => resourceName, dataScopeKey: () => dataScopeKey,
112
+ primaryKey: () => primaryKey, childrenKey: () => childrenKey,
113
+ });
114
+ $effect.pre(() => {
115
+ const root = baseRecords; void queryIdentity; void refreshVersion;
116
+ untrack(() => { infiniteRecords = root; loadedRecords = []; dataVersion++; if (advanced) selectedIds = []; });
117
+ });
118
+ const gridScope = $derived(`${queryIdentity}:${refreshVersion}:${dataVersion}`);
119
+ const windowSource = $derived.by((): SvarWindowSource | undefined => {
120
+ if (loadingMode !== 'window') return undefined;
121
+ // 每次分派捕获当前作用域,不在配置创建时固定暂态作用域。
122
+ void gridScope;
123
+ return { total, load: async ({ start, end, signal }) => {
124
+ const scope = operationScope;
125
+ return loadSvarResourceWindow({ start, end }, effectivePageSize,
126
+ pagination => reads.read({ scope, signal, pagination, filters: [...rootFilters], sorters: [...sorters] }), signal, primaryKey);
127
+ } };
128
+ });
129
+ const loadChildren: SvarChildrenLoader = async ({ id, signal }) => {
130
+ if (!lazyTree || !childrenKey) throw new Error('Lazy tree is not configured');
131
+ const scope = operationScope;
132
+ const branchFilters: Filter[] = [...queryFilters, { field: lazyTree.parentField, operator: 'eq', value: id }];
133
+ return loadSvarResourceChildren(effectivePageSize, lazyTree.maxChildren ?? 10_000,
134
+ pagination => reads.read({ scope, signal, pagination, filters: branchFilters, sorters: [...sorters], parentId: id }), signal, primaryKey, childrenKey);
135
+ };
136
+ async function loadMore({ signal }: { signal: AbortSignal }): Promise<void> {
137
+ const scope = operationScope, before = infiniteRecords;
138
+ if (loadingMode !== 'infinite' || before.length >= total) return;
139
+ if (before.length % effectivePageSize !== 0) throw new Error('Provider returned a truncated page; refresh');
140
+ const page = await reads.read({ scope, signal, pagination: { current: before.length / effectivePageSize + 1, pageSize: effectivePageSize }, filters: [...rootFilters], sorters: [...sorters] });
141
+ if (!scopeCurrent(scope) || signal.aborted || infiniteRecords !== before) throw new SvarLoadCancelled();
142
+ infiniteRecords = appendSvarResourcePage(before, page, total, primaryKey);
143
+ }
144
+ function loaded(next: readonly Record<string, unknown>[]): void {
145
+ loadedRecords = next;
146
+ if (!advanced) return;
147
+ const index = svarRecordIndex(next, primaryKey, childrenKey);
148
+ selectedIds = selectedIds.filter(id => index.has(svarRecordKey(id)));
149
+ }
150
+ function refresh(): void { reads.cancelAll(); refreshVersion++; void query.refetch(); }
151
+
152
+ function selection(next: SvarRecordId[]): void {
153
+ if (busy || disabled || !permission.allowed || query.isFetching) return;
154
+ try { selectedIds = checkedSvarSelection(next); feedback = ''; }
155
+ catch { feedback = zh ? '最多选择 100 条记录' : 'Select at most 100 records'; }
156
+ }
157
+ function selectPage(): void {
158
+ try {
159
+ const ids: SvarRecordId[] = [];
160
+ for (const record of svarRecordIndex(records, primaryKey, childrenKey).values()) {
161
+ const id: unknown = Object.getOwnPropertyDescriptor(record, primaryKey)?.value;
162
+ if (typeof id === 'string' || typeof id === 'number') ids.push(id);
163
+ }
164
+ selection(ids);
165
+ } catch { feedback = zh ? '记录标识无效' : 'Invalid record IDs'; }
166
+ }
167
+ function changeSort(next: SvarSort[]): void { if (!busy && !disabled && permission.allowed) { sorters = next; current = 1; } }
168
+ function changeFilter(next: SvarTextFilter[]): void { if (!busy && !disabled && permission.allowed) { textFilters = next; current = 1; } }
169
+ function scopeCurrent(scope: typeof operationScope): boolean {
170
+ return alive && operationScope === scope && scope.auth.available && scope.auth.isCurrent() && !disabled && permission.allowed;
171
+ }
172
+ async function allowed(action: 'edit' | 'delete' | 'export' | 'list', id: SvarRecordId | undefined, scope: typeof operationScope): Promise<boolean> {
173
+ if (!scopeCurrent(scope)) return false;
174
+ try {
175
+ const meta = context.getProviderMeta(resourceName, { svadminSvarScope: scope.dataScopeKey });
176
+ const decision: unknown = scope.access ? await scope.access.can({ resource: resourceName, action,
177
+ ...(id === undefined ? {} : { params: { id } }), ...(meta === undefined ? {} : { meta }),
178
+ }) : { can: true };
179
+ return scopeCurrent(scope) && decision !== null && typeof decision === 'object'
180
+ && Object.getOwnPropertyDescriptor(decision, 'can')?.value === true;
181
+ } catch { return false; }
182
+ }
183
+ async function execute(action: 'edit' | 'delete', ids: readonly SvarRecordId[], variables?: Record<string, unknown>): Promise<void> {
184
+ if (busy || disabled || !permission.allowed || query.isFetching || query.isError) throw new Error('Unavailable');
185
+ if (action === 'edit' && (!(editable || batchUpdate) || resource.canEdit === false)) throw new Error('Editing disabled');
186
+ if (action === 'delete' && (!batchDelete || resource.canDelete === false)) throw new Error('Deletion disabled');
187
+ const scope = operationScope;
188
+ const input = variables === undefined ? undefined : decodeBaseRecord(snapshotPlainData(variables));
189
+ if (input !== undefined && Object.keys(input).some(key => !editableFields.some(field => field.key === key))) throw new Error('Field is not writable');
190
+ const idsCopy = checkedSvarSelection(ids);
191
+ const index = svarRecordIndex(records, primaryKey, childrenKey);
192
+ if (idsCopy.some(id => !index.has(svarRecordKey(id)))) throw new Error('Selection is no longer loaded');
193
+ const removeInput = snapshotPlainData(deleteVariables ?? {});
194
+ const currentScope = () => scopeCurrent(scope);
195
+ busy = true; feedback = ''; result = undefined; confirmation = undefined;
196
+ try {
197
+ const outcome = await runSvarBatch({ ids: idsCopy, current: currentScope, authorize: id => allowed(action, id, scope),
198
+ async write(id) {
199
+ if (!currentScope()) throw new Error('Scope changed');
200
+ activeId = id; authorizedWrite = true;
201
+ try {
202
+ const options = { dataProviderName: binding.dataProviderName, meta: { svadminSvarScope: scope.dataScopeKey } };
203
+ if (action === 'edit') return await update.mutation.mutateAsync({ variables: input ?? {}, ...options });
204
+ return await deletion.mutation.mutateAsync({ variables: removeInput, ...options });
205
+ } finally { authorizedWrite = false; }
206
+ },
207
+ });
208
+ if (currentScope()) {
209
+ result = outcome;
210
+ const done = new Set(outcome.succeeded.map(svarRecordKey));
211
+ selectedIds = selectedIds.filter(id => !done.has(svarRecordKey(id)));
212
+ }
213
+ if (advanced && currentScope() && outcome.succeeded.length) refresh();
214
+ if (outcome.failed.length || outcome.cancelled) throw new Error('Operation incomplete');
215
+ } finally { if (alive) { busy = false; authorizedWrite = false; } }
216
+ }
217
+ async function editCell(edit: SvarCellEdit): Promise<void> { await execute('edit', [edit.id], { [edit.field]: edit.value }); }
218
+ async function confirmBatch(): Promise<void> {
219
+ const action = confirmation;
220
+ if (!action) return;
221
+ try {
222
+ if (action === 'delete') await execute('delete', selectedIds);
223
+ else {
224
+ const field = editableFields.find(field => field.key === batchField);
225
+ if (!field) throw new Error('Choose a writable field');
226
+ const value = field.type === 'number' ? Number(batchValue) : batchValue;
227
+ if (field.type === 'number' && (!batchValue.trim() || !Number.isFinite(value))) throw new Error('Invalid number');
228
+ await execute('edit', selectedIds, { [field.key]: value });
229
+ }
230
+ } catch { if (alive) feedback = zh ? '操作未全部完成,请检查结果并刷新后再重试' : 'Operation incomplete; inspect the results and refresh before retrying'; }
231
+ }
232
+ async function exportPage(): Promise<void> {
233
+ if (busy || !exportable || !permission.allowed || !exportPermission.allowed || disabled || query.isFetching || query.isError) return;
234
+ const scope = operationScope;
235
+ busy = true; feedback = '';
236
+ try {
237
+ const snapshot = prepareSvarExport(records, columns, primaryKey, childrenKey);
238
+ const csv = await authorizeSvarExport(snapshot, { current: () => scopeCurrent(scope), authorize: (action, id) => allowed(action, id, scope) });
239
+ if (scopeCurrent(scope)) downloadSvarCsv(csv, resourceName);
240
+ } catch { if (alive && operationScope === scope) feedback = zh ? '导出失败或没有导出权限' : 'Export failed or access denied'; }
241
+ finally { if (alive) busy = false; }
242
+ }
243
+
244
+ let views = $state<SavedListView[]>([]);
245
+ let viewName = $state('');
246
+ let activeView = $state('');
247
+ const allowedColumns = $derived(new Set(allColumns.map(column => column.key)));
248
+ const preferencesKey = $derived(preferenceScopeKey ? `svadmin-svar-views-v1:${JSON.stringify([resourceName, binding.dataProviderName, context.tenantCacheKey?.__svadminTenant, preferenceScopeKey])}` : undefined);
249
+ onMount(() => {
250
+ if (!savedViews || !preferencesKey) return;
251
+ try { views = readSavedListViews(localStorage.getItem(preferencesKey), allowedColumns); }
252
+ catch { feedback = zh ? '无法读取保存视图' : 'Saved views unavailable'; }
253
+ });
254
+ function persist(): void {
255
+ if (!preferencesKey) return;
256
+ try { localStorage.setItem(preferencesKey, serializeSavedListViews(views)); }
257
+ catch { feedback = zh ? '视图保存在本次会话中,浏览器存储不可用' : 'View is kept in memory; browser storage is unavailable'; }
258
+ }
259
+ function saveView(): void {
260
+ if (!savedViews || !viewName.trim() || views.length >= 25 || busy || disabled) return;
261
+ const state: SavedListViewState = { search: viewSearch, filters: [...viewFilters, ...textFilters], sorters: [...sorters],
262
+ pagination: { current: 1, pageSize: effectivePageSize }, columnVisibility: { ...columnVisibility }, columnOrder: [...columnOrder] };
263
+ const view = { id: crypto.randomUUID(), name: viewName.trim().slice(0, 60), state };
264
+ views = [...views, view]; activeView = view.id; viewName = ''; persist();
265
+ }
266
+ function applyView(id: string): void {
267
+ if (busy || disabled) return;
268
+ const view = views.find(view => view.id === id);
269
+ if (view && (view.state.sorters.some(sort => !allColumns.some(column => column.key === sort.field && column.sortable))
270
+ || !allColumns.some(column => view.state.columnVisibility[column.key] !== false)
271
+ || (view.state.search !== '' && searchable.length === 0))) {
272
+ feedback = zh ? '该视图与当前资源配置不兼容' : 'View is incompatible with the current resource'; return;
273
+ }
274
+ activeView = id; current = 1; textFilters = []; selectedIds = [];
275
+ if (!view) { viewFilters = []; viewSearch = ''; sorters = [...initialSorters]; columnVisibility = {}; columnOrder = []; viewPageSize = undefined; return; }
276
+ viewFilters = [...view.state.filters]; viewSearch = view.state.search; sorters = [...view.state.sorters];
277
+ columnVisibility = { ...view.state.columnVisibility }; columnOrder = [...view.state.columnOrder]; viewPageSize = view.state.pagination.pageSize;
278
+ }
279
+ function importViews(): void {
280
+ if (!migrateAutoTableViews || busy || disabled) return;
281
+ try {
282
+ const matcher = context.queryKeyMatcher(resourceName);
283
+ const oldKey = savedListViewsStorageKey({ resourceName, providerName: binding.dataProviderName, ...(matcher.tenant === undefined ? {} : { tenantIdentity: matcher.tenant }) });
284
+ const incoming = readSavedListViews(localStorage.getItem(oldKey), allowedColumns);
285
+ const existing = new Set(views.map(view => view.id));
286
+ views = [...views, ...incoming.filter(view => !existing.has(view.id))].slice(0, 25); persist();
287
+ } catch { feedback = zh ? '无法导入视图' : 'Unable to import views'; }
288
+ }
289
+ </script>
290
+
291
+ <section class="svar-resource-table" aria-label={resource.label}>
292
+ <header><h2>{resource.label}</h2>
293
+ <button type="button" disabled={!permission.allowed || !validPageSize || query.isFetching || busy || disabled} onclick={refresh}>{zh ? '刷新' : 'Refresh'}</button>
294
+ {#if exportable}<button type="button" disabled={!permission.allowed || !exportPermission.allowed || busy || disabled || query.isFetching || query.isError} onclick={exportPage}>{advanced ? (zh ? '导出已加载记录 CSV' : 'Export loaded CSV') : (zh ? '导出当前页 CSV' : 'Export page CSV')}</button>{/if}
295
+ </header>
296
+ {#if !validLoading}<p role="alert">{zh ? '高级加载配置无效;窗口和无限加载仅支持平面资源,异步树必须声明父字段' : 'Invalid loading configuration: window/infinite modes require flat resources; lazy trees require a declared parent field'}</p>
297
+ {:else if !validPageSize}<p role="alert">{zh ? '每页数量必须为 1–1000 的整数' : 'Page size must be an integer between 1 and 1000'}</p>
298
+ {:else if permission.isLoading}<p role="status">{zh ? '正在检查权限' : 'Checking access'}</p>
299
+ {:else if !permission.allowed}<p role="alert">{permission.reason ?? (zh ? '没有读取权限' : 'Read access denied')}</p>
300
+ {:else}
301
+ {#each [...reads.requests] as [id, request] (id)}<SvarResourceRead {request} />{/each}
302
+ {#if savedViews}
303
+ <div class="toolbar" aria-label={zh ? '保存视图' : 'Saved views'}>
304
+ <select aria-label={zh ? '当前视图' : 'Current view'} value={activeView} disabled={busy || disabled} onchange={event => applyView(event.currentTarget.value)}>
305
+ <option value="">{zh ? '默认视图' : 'Default view'}</option>{#each views as view (view.id)}<option value={view.id}>{view.name}</option>{/each}
306
+ </select>
307
+ <input aria-label={zh ? '视图名称' : 'View name'} bind:value={viewName} maxlength={60} disabled={busy || disabled} />
308
+ <button type="button" disabled={!viewName.trim() || busy || disabled || views.length >= 25} onclick={saveView}>{zh ? '保存视图' : 'Save view'}</button>
309
+ <button type="button" disabled={!activeView || busy || disabled} onclick={() => { views = views.filter(view => view.id !== activeView); activeView = ''; persist(); }}>{zh ? '删除视图' : 'Delete view'}</button>
310
+ {#if migrateAutoTableViews}<button type="button" disabled={busy || disabled} onclick={importViews}>{zh ? '导入 AutoTable 视图' : 'Import AutoTable views'}</button>{/if}
311
+ <details><summary>{zh ? '列设置' : 'Columns'}</summary>{#each allColumns as column (column.key)}
312
+ <label><input type="checkbox" checked={columnVisibility[column.key] !== false} disabled={busy || disabled || (columns.length === 1 && columnVisibility[column.key] !== false)} onchange={event => { columnVisibility = { ...columnVisibility, [column.key]: event.currentTarget.checked }; }} />{column.label}</label>
313
+ {/each}</details>
314
+ </div>
315
+ {/if}
316
+ {#if selectable || batchUpdate || batchDelete}
317
+ <div class="toolbar" aria-label={zh ? '批量操作' : 'Batch actions'}>
318
+ <button type="button" disabled={busy || disabled || query.isFetching || records.length === 0} onclick={selectPage}>{advanced ? (zh ? '选择已加载记录' : 'Select loaded') : (zh ? '选择当前页' : 'Select page')}</button>
319
+ <button type="button" disabled={busy || disabled || selectedIds.length === 0} onclick={() => { selectedIds = []; }}>{zh ? '清除选择' : 'Clear selection'}</button>
320
+ <span role="status">{zh ? '已选' : 'Selected'}: {selectedIds.length}</span>
321
+ {#if batchUpdate}<button type="button" disabled={!selectedIds.length || busy || disabled || query.isFetching || !editPermission.allowed || resource.canEdit === false} onclick={() => { confirmation = 'update'; }}>{zh ? '批量修改' : 'Update selected'}</button>{/if}
322
+ {#if batchDelete}<button type="button" disabled={!selectedIds.length || busy || disabled || query.isFetching || !deletePermission.allowed || resource.canDelete === false} onclick={() => { confirmation = 'delete'; }}>{zh ? '删除所选' : 'Delete selected'}</button>{/if}
323
+ </div>
324
+ {/if}
325
+ {#if confirmation}<div class="confirmation" role="group" aria-label={zh ? '确认批量操作' : 'Confirm batch action'}>
326
+ <p>{zh ? `确认对所选 ${selectedIds.length} 条记录执行操作?` : `Confirm operation on ${selectedIds.length} selected records?`}</p>
327
+ {#if confirmation === 'update'}
328
+ <select aria-label={zh ? '修改字段' : 'Update field'} bind:value={batchField}><option value="">{zh ? '选择字段' : 'Choose field'}</option>{#each editableFields as field (field.key)}<option value={field.key}>{field.label}</option>{/each}</select>
329
+ <input aria-label={zh ? '新值' : 'New value'} bind:value={batchValue} />
330
+ {/if}
331
+ <button type="button" disabled={busy || disabled || (confirmation === 'update' && !batchField)} onclick={confirmBatch}>{zh ? '确认执行' : 'Confirm action'}</button>
332
+ <button type="button" onclick={() => { confirmation = undefined; }}>{zh ? '取消' : 'Cancel'}</button>
333
+ </div>{/if}
334
+ {#if feedback}<p role="alert">{feedback}</p>{/if}
335
+ {#if result}<p role="status">{zh ? '成功 / 失败 / 未执行' : 'Succeeded / failed / skipped'}: {result.succeeded.length} / {result.failed.length} / {result.skipped.length}</p>{/if}
336
+ <SvarDataGrid {Grid} {Theme} items={gridItems} {columns} {primaryKey} {height} freezeRight={Math.min(freezeRight, Math.max(0, columns.length - 1))} freezeLeft={Math.min(freezeLeft, columns.length - Math.min(freezeRight, Math.max(0, columns.length - 1)))} {density}
337
+ queryMode="server" {sorters} filters={textFilters} onSortChange={changeSort} onFilterChange={changeFilter}
338
+ selectable={selectable || batchUpdate || batchDelete} {selectedIds} onSelectionChange={selection} {...advanced ? { onRecordsChange: loaded } : {}} scopeKey={gridScope}
339
+ {...windowSource ? { windowSource } : {}}
340
+ {...lazyTree ? { loadChildren, hasChildrenKey: lazyTree.hasChildrenKey ?? 'hasChildren' } : {}}
341
+ {...loadingMode === 'infinite' ? { onLoadMore: loadMore, hasMore: infiniteRecords.length < total } : {}}
342
+ {...editable ? { onCellEdit: editCell } : {}} disabled={disabled || busy} locale={i18n.locale}
343
+ loading={query.isLoading} {...error === undefined ? {} : { error }} {...childrenKey === undefined ? {} : { childrenKey }}
344
+ label={resource.label} loadingLabel={zh ? '正在加载' : 'Loading'} emptyLabel={zh ? '暂无记录' : 'No records'}
345
+ disabledLabel={zh ? '暂时不可操作' : 'Grid is disabled'} fallbackLabel={zh ? '静态预览(最多 20 行)' : 'Static preview (up to 20 rows)'} />
346
+ {#if loadingMode === 'page'}<footer aria-label={zh ? '分页' : 'Pagination'}>
347
+ <button type="button" disabled={current <= 1 || query.isFetching || busy || disabled} onclick={() => { current -= 1; }}>{zh ? '上一页' : 'Previous'}</button>
348
+ <span>{current} / {pageCount} · {total} {zh ? '条记录' : 'records'}</span>
349
+ <button type="button" disabled={current >= pageCount || query.isFetching || busy || disabled} onclick={() => { current += 1; }}>{zh ? '下一页' : 'Next'}</button>
350
+ </footer>{:else}<p role="status">{zh ? '已加载 / 总计' : 'Loaded / total'}: {records.length} / {total}</p>{/if}
351
+ {/if}
352
+ </section>
353
+
354
+ <style>
355
+ .svar-resource-table { min-width: 0; color: var(--foreground, CanvasText); }
356
+ header, footer, .toolbar, .confirmation { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; padding: 0.75rem 0; }
357
+ header, footer { justify-content: space-between; }
358
+ h2 { font-size: 1.125rem; margin: 0; }
359
+ button, input, select { max-width: 100%; font: inherit; color: inherit; background: var(--card, Canvas); border: 1px solid var(--border, GrayText); border-radius: var(--radius, 6px); padding: 0.375rem 0.75rem; }
360
+ button { cursor: pointer; } button:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid var(--ring, Highlight); outline-offset: 2px; }
361
+ button:disabled, input:disabled, select:disabled { opacity: 0.5; cursor: not-allowed; }
362
+ .confirmation { border-block: 1px solid var(--border, GrayText); } .confirmation p { width: 100%; margin: 0; }
363
+ details label { display: block; } [role='alert'] { color: var(--destructive, CanvasText); }
364
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { SvarResourceTableProps } from './SvarResourceTable.svelte';
2
+ declare const SvarResourceTableView: import("svelte").Component<SvarResourceTableProps, {}, "">;
3
+ type SvarResourceTableView = ReturnType<typeof SvarResourceTableView>;
4
+ export default SvarResourceTableView;
@@ -1,8 +1,9 @@
1
1
  <script lang="ts">
2
+ import { onDestroy } from 'svelte';
2
3
  import { definedReactiveOptions, definedOptions } from '@svadmin/core/options';
3
4
 
4
5
  import { ListTodo, Loader2, Plus, Search, RefreshCw } from '@lucide/svelte';
5
- import { getTaskProvider, useSubmitTask, useTaskList } from '@svadmin/core';
6
+ import { captureAdminContext, captureAuthSession, useSubmitTask, useTaskList } from '@svadmin/core';
6
7
  import { decodeTaskSubmitOptions } from '@svadmin/core/schema';
7
8
  import { useTranslation } from '@svadmin/core/i18n';
8
9
  import type { TaskProvider, TaskRecord } from '@svadmin/core';
@@ -25,13 +26,14 @@
25
26
  } from './task-utils.js';
26
27
 
27
28
  const i18n = useTranslation();
29
+ const context = captureAdminContext();
28
30
 
29
31
  type AutoRefreshValue = '0' | '5000' | '15000' | '30000';
30
32
  type TaskTab = 'tasks' | 'dlq';
31
33
 
32
34
  let {
33
35
  open = $bindable(false),
34
- taskProvider = getTaskProvider({ optional: true }) ?? undefined,
36
+ taskProvider,
35
37
  title,
36
38
  initialTab = 'tasks',
37
39
  }: {
@@ -51,6 +53,45 @@
51
53
  let submitBodyText = $state(i18n.t('task.bodyPlaceholder'));
52
54
  let submitError = $state<string | null>(null);
53
55
  let submitOpen = $state(false);
56
+ let submitAttempt = $state.raw<object | null>(null);
57
+ let mounted = true;
58
+ onDestroy(() => { mounted = false; submitAttempt = null; });
59
+ const activeTaskProvider = $derived(taskProvider ?? context.taskProvider);
60
+ const taskScope = $derived({
61
+ provider: activeTaskProvider,
62
+ tenant: context.tenantCacheKey?.__svadminTenant,
63
+ auth: context.authProvider,
64
+ router: context.routerProvider,
65
+ session: captureAuthSession(context.authProvider),
66
+ });
67
+ let previousTaskScope: typeof taskScope | undefined;
68
+ function taskScopesEqual(left: typeof taskScope, right: typeof taskScope): boolean {
69
+ return left.provider === right.provider
70
+ && left.tenant === right.tenant
71
+ && left.auth === right.auth
72
+ && left.router === right.router
73
+ && left.session.cacheKey === right.session.cacheKey
74
+ && left.session.available === right.session.available;
75
+ }
76
+ function taskScopeIsCurrent(origin: typeof taskScope): boolean {
77
+ return mounted && taskScopesEqual(origin, taskScope) && origin.session.isCurrent();
78
+ }
79
+ $effect.pre(() => {
80
+ const scope = taskScope;
81
+ if (previousTaskScope && !taskScopesEqual(previousTaskScope, scope)) {
82
+ selectedTaskId = null;
83
+ submitAttempt = null;
84
+ submitOpen = false;
85
+ resetSubmitForm();
86
+ }
87
+ previousTaskScope = scope;
88
+ });
89
+ $effect.pre(() => {
90
+ if (!open) {
91
+ submitAttempt = null;
92
+ submitOpen = false;
93
+ }
94
+ });
54
95
 
55
96
  const refreshInterval = $derived.by<number | false>(() =>
56
97
  autoRefresh === '0' ? false : Number(autoRefresh),
@@ -59,11 +100,11 @@
59
100
 
60
101
  const taskQuery = useTaskList(definedReactiveOptions({
61
102
  get taskProvider() {
62
- return taskProvider;
103
+ return activeTaskProvider;
63
104
  },
64
105
  get queryOptions() {
65
106
  return {
66
- enabled: !!taskProvider,
107
+ enabled: !!activeTaskProvider,
67
108
  refetchInterval: refreshInterval,
68
109
  refetchIntervalInBackground: true,
69
110
  };
@@ -75,11 +116,11 @@
75
116
  return true;
76
117
  },
77
118
  get taskProvider() {
78
- return taskProvider;
119
+ return activeTaskProvider;
79
120
  },
80
121
  get queryOptions() {
81
122
  return {
82
- enabled: !!taskProvider && !!taskProvider?.listDlq,
123
+ enabled: !!activeTaskProvider && !!activeTaskProvider?.listDlq,
83
124
  refetchInterval: refreshInterval,
84
125
  refetchIntervalInBackground: true,
85
126
  };
@@ -162,7 +203,14 @@
162
203
  submitError = null;
163
204
  }
164
205
 
206
+ function toggleSubmitForm(): void {
207
+ submitAttempt = null;
208
+ submitError = null;
209
+ submitOpen = !submitOpen;
210
+ }
211
+
165
212
  async function handleSubmitTask() {
213
+ if (!open || !submitOpen || submitAttempt || !activeTaskProvider || !taskScope.session.available) return;
166
214
  submitError = null;
167
215
  if (!submitTaskName.trim()) {
168
216
  submitError = i18n.t('validation.required');
@@ -180,25 +228,33 @@
180
228
  }
181
229
  }
182
230
 
231
+ const origin = taskScope;
232
+ const attempt = {};
233
+ submitAttempt = attempt;
234
+ const current = () => submitAttempt === attempt && taskScopeIsCurrent(origin);
183
235
  try {
184
236
  const handle = await submitTask.mutation.mutateAsync(definedOptions({
185
237
  taskName: submitTaskName.trim(),
186
- taskProvider,
238
+ taskProvider: activeTaskProvider,
187
239
  options: definedOptions({
188
240
  body,
189
241
  idempotencyKey: submitIdempotencyKey.trim() || undefined,
190
242
  }),
191
243
  }));
244
+ if (!current()) return;
192
245
  activeTab = 'tasks';
193
246
  selectedTaskId = handle.id;
194
247
  submitOpen = false;
195
248
  resetSubmitForm();
196
- await Promise.all([
249
+ await Promise.allSettled([
197
250
  taskQuery.refetch?.(),
198
251
  dlqQuery.refetch?.(),
199
252
  ]);
200
253
  } catch (error) {
254
+ if (!current()) return;
201
255
  submitError = error instanceof Error ? error.message : i18n.t('task.submitFailed');
256
+ } finally {
257
+ if (submitAttempt === attempt) submitAttempt = null;
202
258
  }
203
259
  }
204
260
 
@@ -249,7 +305,7 @@
249
305
  </div>
250
306
  </div>
251
307
 
252
- {#if !taskProvider}
308
+ {#if !activeTaskProvider}
253
309
  <div class="svadmin-u-60fbb7713999 svadmin-u-36e579c0b41c svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-0478c89a150f svadmin-u-fc7473ca09eb svadmin-u-bfa603190748">
254
310
  {i18n.t('common.configRequired')}
255
311
  </div>
@@ -276,7 +332,7 @@
276
332
  <RefreshCw class="svadmin-u-82cc6c6581cd svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
277
333
  {i18n.t('common.refresh')}
278
334
  </Button>
279
- <Button size="sm" onclick={() => submitOpen = !submitOpen}>
335
+ <Button size="sm" disabled={!taskScope.session.available} onclick={toggleSubmitForm}>
280
336
  <Plus class="svadmin-u-82cc6c6581cd svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
281
337
  {i18n.t('task.submitAction')}
282
338
  </Button>
@@ -327,14 +383,14 @@
327
383
  <Textarea id="task-body" bind:value={submitBodyText} class="svadmin-u-ee15a477cd9c svadmin-u-0e65706bcccd svadmin-u-359090c2d529" />
328
384
  </div>
329
385
  {#if submitError}
330
- <p class="svadmin-u-fc7473ca09eb svadmin-u-811148b13d1e">{submitError}</p>
386
+ <p role="alert" class="svadmin-u-fc7473ca09eb svadmin-u-811148b13d1e">{submitError}</p>
331
387
  {/if}
332
388
  <div class="svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-77c08e015d14 svadmin-u-77a2a20e90d4">
333
- <Button variant="ghost" onclick={() => { submitOpen = false; submitError = null; }}>
389
+ <Button variant="ghost" onclick={toggleSubmitForm}>
334
390
  {i18n.t('common.cancel')}
335
391
  </Button>
336
- <Button onclick={() => void handleSubmitTask()} disabled={submitTask.mutation.isPending}>
337
- {#if submitTask.mutation.isPending}
392
+ <Button onclick={() => void handleSubmitTask()} disabled={submitAttempt !== null || !taskScope.session.available}>
393
+ {#if submitAttempt !== null}
338
394
  <Loader2 class="svadmin-u-82cc6c6581cd svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-afbdd13a380e" />
339
395
  {/if}
340
396
  {i18n.t('task.submitAction')}
@@ -346,7 +402,7 @@
346
402
 
347
403
  <TaskList
348
404
  tasks={filteredTasks}
349
- {taskProvider}
405
+ taskProvider={activeTaskProvider}
350
406
  dlq={activeTab === 'dlq'}
351
407
  title={activeTab === 'dlq' ? i18n.t('task.dlqTitle') : i18n.t('task.listTitle')}
352
408
  emptyText={activeTab === 'dlq' ? i18n.t('task.noDlq') : i18n.t('task.noTasks')}
@@ -362,7 +418,7 @@
362
418
  {#if selectedTaskId}
363
419
  <TaskDetails
364
420
  taskId={selectedTaskId}
365
- {taskProvider}
421
+ taskProvider={activeTaskProvider}
366
422
  useProviderData
367
423
  queryOptions={{
368
424
  refetchInterval: refreshInterval,