@svadmin/ui 0.72.0 → 0.74.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 (276) hide show
  1. package/README.md +62 -22
  2. package/THIRD_PARTY_NOTICES.md +52 -0
  3. package/dist/app.css +8488 -6297
  4. package/dist/app.theme.css +8514 -6374
  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/component-registry.d.ts +14977 -0
  10. package/dist/component-registry.js +216 -0
  11. package/dist/components/AboutSettings.svelte +7 -1
  12. package/dist/components/AboutSettings.svelte.d.ts +4 -1
  13. package/dist/components/ActivityFeed.svelte +9 -3
  14. package/dist/components/ActivityFeed.svelte.d.ts +3 -0
  15. package/dist/components/AdminApp.svelte +19 -10
  16. package/dist/components/AdminApp.svelte.d.ts +6 -0
  17. package/dist/components/ApiSettings.svelte +41 -5
  18. package/dist/components/ApprovalCenter.svelte +283 -0
  19. package/dist/components/ApprovalCenter.svelte.d.ts +10 -0
  20. package/dist/components/AuditLogViewer.svelte +3 -1
  21. package/dist/components/AutoForm.svelte +53 -9
  22. package/dist/components/AutoForm.svelte.d.ts +3 -0
  23. package/dist/components/AutoTable.svelte +846 -43
  24. package/dist/components/AutoTable.svelte.d.ts +22 -1
  25. package/dist/components/BoundRecordDetailDrawer.svelte +17 -2
  26. package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +4 -0
  27. package/dist/components/Breadcrumbs.svelte +11 -1
  28. package/dist/components/CancelTaskButton.svelte +12 -42
  29. package/dist/components/ColorPicker.svelte +73 -0
  30. package/dist/components/ColorPicker.svelte.d.ts +15 -0
  31. package/dist/components/ComboboxField.svelte +143 -34
  32. package/dist/components/ComboboxField.svelte.d.ts +4 -2
  33. package/dist/components/CreatePage.svelte +4 -0
  34. package/dist/components/CreatePage.svelte.d.ts +2 -0
  35. package/dist/components/DashboardView.svelte +99 -0
  36. package/dist/components/DashboardView.svelte.d.ts +14 -0
  37. package/dist/components/DatePicker.svelte +10 -0
  38. package/dist/components/DatePicker.svelte.d.ts +6 -0
  39. package/dist/components/DateRangeInput.svelte +155 -0
  40. package/dist/components/DateRangeInput.svelte.d.ts +35 -0
  41. package/dist/components/DateRangePicker.svelte +10 -0
  42. package/dist/components/DateRangePicker.svelte.d.ts +6 -0
  43. package/dist/components/DateTimeInput.svelte +134 -0
  44. package/dist/components/DateTimeInput.svelte.d.ts +27 -0
  45. package/dist/components/DateTimePicker.svelte +10 -0
  46. package/dist/components/DateTimePicker.svelte.d.ts +6 -0
  47. package/dist/components/DevTools.svelte +36 -28
  48. package/dist/components/DevframePanel.svelte +123 -0
  49. package/dist/components/DevframePanel.svelte.d.ts +3 -0
  50. package/dist/components/EditPage.svelte +4 -0
  51. package/dist/components/EditPage.svelte.d.ts +2 -0
  52. package/dist/components/ErrorSummary.svelte +52 -0
  53. package/dist/components/ErrorSummary.svelte.d.ts +14 -0
  54. package/dist/components/FieldRenderer.svelte +172 -99
  55. package/dist/components/FileUpload.svelte +412 -0
  56. package/dist/components/FileUpload.svelte.d.ts +43 -0
  57. package/dist/components/FilterBuilder.svelte +244 -265
  58. package/dist/components/FilterBuilder.svelte.d.ts +3 -0
  59. package/dist/components/FilterCollectionInput.svelte +80 -0
  60. package/dist/components/FilterCollectionInput.svelte.d.ts +10 -0
  61. package/dist/components/GanttChart.svelte +66 -14
  62. package/dist/components/GanttChart.svelte.d.ts +6 -0
  63. package/dist/components/Header.svelte +18 -2
  64. package/dist/components/ImportWizard.svelte +509 -100
  65. package/dist/components/ImportWizard.svelte.d.ts +11 -0
  66. package/dist/components/JsonSchemaForm.svelte +322 -98
  67. package/dist/components/JsonSchemaForm.svelte.d.ts +18 -1
  68. package/dist/components/KanbanBoard.svelte +195 -40
  69. package/dist/components/KanbanBoard.svelte.d.ts +9 -2
  70. package/dist/components/Layout.svelte +4 -3
  71. package/dist/components/LazyAutoForm.svelte +28 -0
  72. package/dist/components/LazyAutoForm.svelte.d.ts +8 -0
  73. package/dist/components/LazyAutoTable.svelte +28 -0
  74. package/dist/components/LazyAutoTable.svelte.d.ts +8 -0
  75. package/dist/components/LazySettingsPage.svelte +28 -0
  76. package/dist/components/LazySettingsPage.svelte.d.ts +8 -0
  77. package/dist/components/LazyShowPage.svelte +28 -0
  78. package/dist/components/LazyShowPage.svelte.d.ts +8 -0
  79. package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +1 -1
  80. package/dist/components/ListPage.svelte +9 -1
  81. package/dist/components/ListPage.svelte.d.ts +5 -0
  82. package/dist/components/LoginPage.svelte +2 -2
  83. package/dist/components/MoneyInput.svelte +25 -0
  84. package/dist/components/MoneyInput.svelte.d.ts +20 -0
  85. package/dist/components/MultiTabKeepAlive.svelte +11 -6
  86. package/dist/components/NotificationsSettings.svelte +69 -2
  87. package/dist/components/NotificationsSettings.svelte.d.ts +25 -1
  88. package/dist/components/NumberInput.svelte +25 -0
  89. package/dist/components/NumberInput.svelte.d.ts +19 -0
  90. package/dist/components/NumericInput.svelte +110 -0
  91. package/dist/components/NumericInput.svelte.d.ts +23 -0
  92. package/dist/components/PercentInput.svelte +25 -0
  93. package/dist/components/PercentInput.svelte.d.ts +20 -0
  94. package/dist/components/PermissionMatrix.svelte +20 -1
  95. package/dist/components/PivotExportButton.svelte +157 -0
  96. package/dist/components/PivotExportButton.svelte.d.ts +16 -0
  97. package/dist/components/PivotTable.svelte +229 -74
  98. package/dist/components/PivotTable.svelte.d.ts +19 -2
  99. package/dist/components/QuickEditDrawer.svelte +4 -1
  100. package/dist/components/QuickEditDrawer.svelte.d.ts +2 -0
  101. package/dist/components/RadioGroup.svelte +121 -0
  102. package/dist/components/RadioGroup.svelte.d.ts +43 -0
  103. package/dist/components/RangeSlider.svelte +127 -0
  104. package/dist/components/RangeSlider.svelte.d.ts +22 -0
  105. package/dist/components/Rate.svelte +121 -0
  106. package/dist/components/Rate.svelte.d.ts +16 -0
  107. package/dist/components/RecordDetailDrawer.svelte +6 -2
  108. package/dist/components/RecordDetailDrawer.svelte.d.ts +4 -0
  109. package/dist/components/RelationPicker.svelte +341 -0
  110. package/dist/components/RelationPicker.svelte.d.ts +26 -0
  111. package/dist/components/ResourceOperationsPage.svelte +25 -19
  112. package/dist/components/ResourceOperationsPage.svelte.d.ts +5 -1
  113. package/dist/components/RetryTaskButton.svelte +12 -42
  114. package/dist/components/SecuritySettings.svelte +29 -5
  115. package/dist/components/SegmentedControl.svelte +145 -0
  116. package/dist/components/SegmentedControl.svelte.d.ts +38 -0
  117. package/dist/components/SettingsPage.svelte +3 -1
  118. package/dist/components/SettingsPage.svelte.d.ts +1 -0
  119. package/dist/components/ShowInferencer.svelte +4 -2
  120. package/dist/components/ShowInferencer.svelte.d.ts +2 -0
  121. package/dist/components/ShowPage.svelte +25 -6
  122. package/dist/components/ShowPage.svelte.d.ts +5 -0
  123. package/dist/components/Sidebar.svelte +5 -5
  124. package/dist/components/Slider.svelte +100 -0
  125. package/dist/components/Slider.svelte.d.ts +19 -0
  126. package/dist/components/SpreadsheetView.svelte +1037 -124
  127. package/dist/components/SpreadsheetView.svelte.d.ts +19 -0
  128. package/dist/components/StepsForm.svelte +5 -5
  129. package/dist/components/SvarAutoTable.svelte +39 -0
  130. package/dist/components/SvarAutoTable.svelte.d.ts +13 -0
  131. package/dist/components/SvarDataGrid.svelte +362 -0
  132. package/dist/components/SvarDataGrid.svelte.d.ts +57 -0
  133. package/dist/components/SvarGridCell.svelte +10 -0
  134. package/dist/components/SvarGridCell.svelte.d.ts +7 -0
  135. package/dist/components/SvarResourceRead.svelte +50 -0
  136. package/dist/components/SvarResourceRead.svelte.d.ts +23 -0
  137. package/dist/components/SvarResourceTable.svelte +63 -0
  138. package/dist/components/SvarResourceTable.svelte.d.ts +42 -0
  139. package/dist/components/SvarResourceTableView.svelte +364 -0
  140. package/dist/components/SvarResourceTableView.svelte.d.ts +4 -0
  141. package/dist/components/TagsInput.svelte +178 -0
  142. package/dist/components/TagsInput.svelte.d.ts +17 -0
  143. package/dist/components/TaskQueueDrawer.svelte +72 -16
  144. package/dist/components/TimePicker.svelte +10 -0
  145. package/dist/components/TimePicker.svelte.d.ts +6 -0
  146. package/dist/components/TreeSelect.svelte +440 -46
  147. package/dist/components/TreeSelect.svelte.d.ts +6 -1
  148. package/dist/components/TreeTable.svelte +484 -82
  149. package/dist/components/TreeTable.svelte.d.ts +24 -1
  150. package/dist/components/TwoFactorAuthPage.svelte +85 -48
  151. package/dist/components/TwoFactorAuthPage.svelte.d.ts +12 -1
  152. package/dist/components/VirtualTable.svelte +116 -20
  153. package/dist/components/VirtualTable.svelte.d.ts +5 -0
  154. package/dist/components/account/CompanyProfilePage.svelte +74 -38
  155. package/dist/components/account/GetStartedPage.svelte +2 -1
  156. package/dist/components/account/ImportMembersPage.svelte +22 -167
  157. package/dist/components/account/ImportMembersPage.svelte.d.ts +4 -1
  158. package/dist/components/account/MemberDirectory.svelte +156 -0
  159. package/dist/components/account/MemberDirectory.svelte.d.ts +19 -0
  160. package/dist/components/account/MembersStarterPage.svelte +9 -55
  161. package/dist/components/account/MembersStarterPage.svelte.d.ts +5 -1
  162. package/dist/components/account/SecurityLogPage.svelte +4 -1
  163. package/dist/components/account/SettingsPlainPage.svelte +7 -38
  164. package/dist/components/account/SettingsPlainPage.svelte.d.ts +4 -1
  165. package/dist/components/account/SettingsSidebarPage.svelte +7 -20
  166. package/dist/components/account/TeamMembersPage.svelte +4 -20
  167. package/dist/components/account/TeamMembersPage.svelte.d.ts +5 -1
  168. package/dist/components/account/UserProfilePage.svelte +3 -25
  169. package/dist/components/account/UserProfilePage.svelte.d.ts +17 -2
  170. package/dist/components/auto-table-grid.d.ts +26 -0
  171. package/dist/components/auto-table-grid.js +1 -0
  172. package/dist/components/buttons/ExportButton.svelte +56 -12
  173. package/dist/components/buttons/ExportButton.svelte.d.ts +11 -0
  174. package/dist/components/content/ContentPageHeader.svelte +12 -9
  175. package/dist/components/content/ContentPageShell.svelte +6 -9
  176. package/dist/components/content/FileBrowser.svelte +35 -0
  177. package/dist/components/content/FileBrowser.svelte.d.ts +14 -0
  178. package/dist/components/content/MetricBlock.svelte +10 -13
  179. package/dist/components/content/NetworkTable.svelte +3 -3
  180. package/dist/components/content/NetworkTable.svelte.d.ts +1 -0
  181. package/dist/components/content/PageToolbar.svelte +6 -10
  182. package/dist/components/content/ReferenceNotice.svelte +5 -0
  183. package/dist/components/content/ReferenceNotice.svelte.d.ts +18 -0
  184. package/dist/components/content/SectionHeader.svelte +8 -13
  185. package/dist/components/content/SettingsFieldRow.svelte +9 -23
  186. package/dist/components/content/SettingsGroup.svelte +11 -27
  187. package/dist/components/content/StatusBadge.svelte +3 -8
  188. package/dist/components/content/WorkspaceLayout.svelte +8 -12
  189. package/dist/components/enterprise/filter-tree.d.ts +48 -0
  190. package/dist/components/enterprise/filter-tree.js +224 -0
  191. package/dist/components/enterprise/json-schema-form.d.ts +39 -0
  192. package/dist/components/enterprise/json-schema-form.js +195 -0
  193. package/dist/components/enterprise/recursive-schema-form.d.ts +30 -0
  194. package/dist/components/enterprise/recursive-schema-form.js +196 -0
  195. package/dist/components/enterprise/spreadsheet-formula.d.ts +12 -0
  196. package/dist/components/enterprise/spreadsheet-formula.js +245 -0
  197. package/dist/components/export-reference.d.ts +2 -0
  198. package/dist/components/export-reference.js +9 -0
  199. package/dist/components/fieldComponentMap.js +2 -0
  200. package/dist/components/fields/DateField.svelte +12 -12
  201. package/dist/components/fields/date-display.d.ts +6 -0
  202. package/dist/components/fields/date-display.js +24 -0
  203. package/dist/components/fields/date-field-value.d.ts +2 -0
  204. package/dist/components/fields/date-field-value.js +13 -0
  205. package/dist/components/json-schema-form-state.d.ts +37 -0
  206. package/dist/components/json-schema-form-state.js +168 -0
  207. package/dist/components/network/TeamCrewTablePage.svelte +6 -1
  208. package/dist/components/network/UserCardsNFTPage.svelte +2 -0
  209. package/dist/components/profile/PublicProfilePage.svelte +2 -0
  210. package/dist/components/record-detail.svelte.d.ts +3 -0
  211. package/dist/components/record-detail.svelte.js +10 -4
  212. package/dist/components/saved-list-views.d.ts +74 -1
  213. package/dist/components/saved-list-views.js +191 -3
  214. package/dist/components/svar-grid-cells.d.ts +14 -0
  215. package/dist/components/svar-grid-cells.js +2 -0
  216. package/dist/components/svar-grid-contract.d.ts +80 -0
  217. package/dist/components/svar-grid-contract.js +159 -0
  218. package/dist/components/svar-grid-interactions.d.ts +54 -0
  219. package/dist/components/svar-grid-interactions.js +174 -0
  220. package/dist/components/svar-grid-loading.d.ts +40 -0
  221. package/dist/components/svar-grid-loading.js +180 -0
  222. package/dist/components/svar-grid-operations.d.ts +37 -0
  223. package/dist/components/svar-grid-operations.js +186 -0
  224. package/dist/components/svar-grid-panes.d.ts +34 -0
  225. package/dist/components/svar-grid-panes.js +66 -0
  226. package/dist/components/svar-grid-resource-loading.d.ts +18 -0
  227. package/dist/components/svar-grid-resource-loading.js +68 -0
  228. package/dist/components/svar-grid-resource-session.svelte.d.ts +29 -0
  229. package/dist/components/svar-grid-resource-session.svelte.js +61 -0
  230. package/dist/components/table-contract.d.ts +12 -1
  231. package/dist/components/task-action.svelte.d.ts +13 -0
  232. package/dist/components/task-action.svelte.js +73 -0
  233. package/dist/components/ui/DesignSystemShowcase.svelte +164 -0
  234. package/dist/components/ui/DesignSystemShowcase.svelte.d.ts +3 -0
  235. package/dist/components/ui/badge/badge-variants.d.ts +3 -2
  236. package/dist/components/ui/badge/badge-variants.js +2 -1
  237. package/dist/components/ui/badge/badge.svelte +3 -0
  238. package/dist/components/ui/badge/badge.svelte.d.ts +1 -0
  239. package/dist/components/ui/button/button-variants.d.ts +5 -4
  240. package/dist/components/ui/button/button-variants.js +7 -2
  241. package/dist/components/ui/checkbox/checkbox.svelte +5 -1
  242. package/dist/components/ui/checkbox/checkbox.svelte.d.ts +5 -1
  243. package/dist/components/ui/input/input.svelte +8 -6
  244. package/dist/components/ui/input/input.svelte.d.ts +4 -1
  245. package/dist/components/ui/select/select.svelte +4 -1
  246. package/dist/components/ui/select/select.svelte.d.ts +2 -1
  247. package/dist/components/ui/textarea/textarea.svelte +2 -1
  248. package/dist/components.css +3110 -0
  249. package/dist/date-input-policy.d.ts +4 -0
  250. package/dist/date-input-policy.js +35 -0
  251. package/dist/design-contract.d.ts +13 -0
  252. package/dist/design-contract.js +11 -0
  253. package/dist/design-system-components.d.ts +100 -0
  254. package/dist/design-system-components.js +101 -0
  255. package/dist/design-system.d.ts +15 -0
  256. package/dist/design-system.js +7 -0
  257. package/dist/devtools-bridge.d.ts +80 -0
  258. package/dist/devtools-bridge.js +92 -0
  259. package/dist/index.d.ts +50 -1
  260. package/dist/index.js +32 -2
  261. package/dist/page-chrome.css +12 -49
  262. package/dist/recipes.d.ts +960 -0
  263. package/dist/recipes.js +264 -0
  264. package/dist/rendering/index.d.ts +87 -0
  265. package/dist/rendering/index.js +117 -0
  266. package/dist/styles/aliases.css +49 -0
  267. package/dist/styles/compatibility.epilogue.css +384 -0
  268. package/dist/styles/compatibility.prelude.css +5513 -0
  269. package/dist/styles/conditional-states.css +28 -0
  270. package/dist/styles/conditional-utilities.css +22 -0
  271. package/dist/tree-table-model.d.ts +24 -0
  272. package/dist/tree-table-model.js +111 -0
  273. package/dist/utils.d.ts +1 -1
  274. package/dist/utils.js +1 -1
  275. package/package.json +40 -10
  276. package/styles-compatibility.json +12 -0
@@ -0,0 +1,63 @@
1
+ <script lang="ts">
2
+ import { captureAdminContext, captureAuthSession, type Filter, type Sort } from '@svadmin/core';
3
+ import { useTranslation } from '@svadmin/core/i18n';
4
+ import type { ComponentProps } from 'svelte';
5
+ import SvarDataGrid from './SvarDataGrid.svelte';
6
+ import SvarResourceTableView from './SvarResourceTableView.svelte';
7
+ import type { SvarLazyTree } from './svar-grid-resource-loading.js';
8
+ import type { SvarValueFormat } from './svar-grid-interactions.js';
9
+
10
+ export interface SvarResourceTableProps {
11
+ Grid: ComponentProps<typeof SvarDataGrid>['Grid'];
12
+ Theme: ComponentProps<typeof SvarDataGrid>['Theme'];
13
+ resourceName: string;
14
+ pageSize?: number;
15
+ filters?: readonly Filter[];
16
+ initialSorters?: readonly Sort[];
17
+ height?: number;
18
+ freezeLeft?: number;
19
+ freezeRight?: number;
20
+ /** 高级读取仍使用资源契约、Provider 和核心查询缓存。 */
21
+ loadingMode?: 'page' | 'window' | 'infinite';
22
+ lazyTree?: SvarLazyTree;
23
+ density?: 'compact' | 'comfortable';
24
+ childrenKey?: string;
25
+ dataScopeKey?: string | number;
26
+ disabled?: boolean;
27
+ /** 所有写入能力默认关闭,必须由可信宿主开启。 */
28
+ editable?: boolean;
29
+ selectable?: boolean;
30
+ batchUpdate?: boolean;
31
+ batchDelete?: boolean;
32
+ exportable?: boolean;
33
+ savedViews?: boolean;
34
+ /** 非敏感的用户/租户偏好分区;省略时只保存在本次挂载的内存。 */
35
+ preferenceScopeKey?: string;
36
+ /** 显式导入 AutoTable 的作用域视图,不修改原视图。 */
37
+ migrateAutoTableViews?: boolean;
38
+ deleteVariables?: unknown;
39
+ formats?: Readonly<Record<string, { format: SvarValueFormat; currency?: string }>>;
40
+ }
41
+ let props: SvarResourceTableProps = $props();
42
+ const context = captureAdminContext();
43
+ const i18n = useTranslation();
44
+ const owner = $derived.by(() => {
45
+ const auth = captureAuthSession(context.authProvider);
46
+ return {
47
+ provider: context.getDataProviderForResource(props.resourceName),
48
+ resourceName: props.resourceName, contract: context.getResource(props.resourceName).contract,
49
+ tenant: context.tenantCacheKey?.__svadminTenant,
50
+ authKey: auth.cacheKey, available: auth.available,
51
+ access: context.accessControlProvider, dataScopeKey: props.dataScopeKey,
52
+ preferenceScopeKey: props.preferenceScopeKey,
53
+ };
54
+ });
55
+ </script>
56
+
57
+ {#key owner}
58
+ {#if owner.available}
59
+ <SvarResourceTableView {...props} />
60
+ {:else}
61
+ <p role="status">{i18n.locale.startsWith('zh') ? '当前会话不可读取数据' : 'Data is unavailable for the current session'}</p>
62
+ {/if}
63
+ {/key}
@@ -0,0 +1,42 @@
1
+ import { type Filter, type Sort } from '@svadmin/core';
2
+ import type { ComponentProps } from 'svelte';
3
+ import SvarDataGrid from './SvarDataGrid.svelte';
4
+ import type { SvarLazyTree } from './svar-grid-resource-loading.js';
5
+ import type { SvarValueFormat } from './svar-grid-interactions.js';
6
+ export interface SvarResourceTableProps {
7
+ Grid: ComponentProps<typeof SvarDataGrid>['Grid'];
8
+ Theme: ComponentProps<typeof SvarDataGrid>['Theme'];
9
+ resourceName: string;
10
+ pageSize?: number;
11
+ filters?: readonly Filter[];
12
+ initialSorters?: readonly Sort[];
13
+ height?: number;
14
+ freezeLeft?: number;
15
+ freezeRight?: number;
16
+ /** 高级读取仍使用资源契约、Provider 和核心查询缓存。 */
17
+ loadingMode?: 'page' | 'window' | 'infinite';
18
+ lazyTree?: SvarLazyTree;
19
+ density?: 'compact' | 'comfortable';
20
+ childrenKey?: string;
21
+ dataScopeKey?: string | number;
22
+ disabled?: boolean;
23
+ /** 所有写入能力默认关闭,必须由可信宿主开启。 */
24
+ editable?: boolean;
25
+ selectable?: boolean;
26
+ batchUpdate?: boolean;
27
+ batchDelete?: boolean;
28
+ exportable?: boolean;
29
+ savedViews?: boolean;
30
+ /** 非敏感的用户/租户偏好分区;省略时只保存在本次挂载的内存。 */
31
+ preferenceScopeKey?: string;
32
+ /** 显式导入 AutoTable 的作用域视图,不修改原视图。 */
33
+ migrateAutoTableViews?: boolean;
34
+ deleteVariables?: unknown;
35
+ formats?: Readonly<Record<string, {
36
+ format: SvarValueFormat;
37
+ currency?: string;
38
+ }>>;
39
+ }
40
+ declare const SvarResourceTable: import("svelte").Component<SvarResourceTableProps, {}, "">;
41
+ type SvarResourceTable = ReturnType<typeof SvarResourceTable>;
42
+ export default SvarResourceTable;
@@ -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;
@@ -0,0 +1,178 @@
1
+ <script lang="ts">
2
+ import { X } from '@lucide/svelte';
3
+ import { cn } from '../utils.js';
4
+
5
+ interface Props {
6
+ value?: string[];
7
+ name?: string;
8
+ placeholder?: string;
9
+ separator?: string;
10
+ maxItems?: number;
11
+ disabled?: boolean;
12
+ required?: boolean;
13
+ invalid?: boolean;
14
+ describedby?: string;
15
+ ariaLabel?: string;
16
+ class?: string;
17
+ onchange?: (value: string[]) => void;
18
+ }
19
+
20
+ let {
21
+ value = $bindable([]),
22
+ name,
23
+ placeholder = 'Add a tag',
24
+ separator = ',',
25
+ maxItems,
26
+ disabled = false,
27
+ required = false,
28
+ invalid = false,
29
+ describedby,
30
+ ariaLabel,
31
+ class: className = '',
32
+ onchange,
33
+ }: Props = $props();
34
+
35
+ let draft = $state('');
36
+ let composing = $state(false);
37
+
38
+ function normalized(values: readonly string[]): string[] {
39
+ return [...new Set(values.map(item => item.trim()).filter(Boolean))];
40
+ }
41
+
42
+ function update(next: string[]): void {
43
+ const result = normalized(next);
44
+ value = result;
45
+ onchange?.(result);
46
+ }
47
+
48
+ function add(raw: string): void {
49
+ if (disabled) return;
50
+ const candidates = raw.split(separator).map(item => item.trim()).filter(Boolean);
51
+ if (candidates.length === 0) return;
52
+ const available = maxItems === undefined ? candidates : candidates.slice(0, Math.max(0, maxItems - value.length));
53
+ update([...value, ...available]);
54
+ draft = '';
55
+ }
56
+
57
+ function remove(index: number): void {
58
+ if (disabled) return;
59
+ update(value.filter((_, itemIndex) => itemIndex !== index));
60
+ }
61
+
62
+ function handleKeydown(event: KeyboardEvent): void {
63
+ if (disabled || composing) return;
64
+ if (event.key === 'Enter' || event.key === separator) {
65
+ event.preventDefault();
66
+ add(draft);
67
+ } else if (event.key === 'Backspace' && draft === '' && value.length > 0) {
68
+ event.preventDefault();
69
+ remove(value.length - 1);
70
+ }
71
+ }
72
+ </script>
73
+
74
+ <div
75
+ class={cn('svadmin-tags-input', className)}
76
+ data-invalid={invalid ? 'true' : undefined}
77
+ data-disabled={disabled ? 'true' : undefined}
78
+ >
79
+ {#if name}
80
+ {#each value as tag (tag)}
81
+ <input type="hidden" {name} value={tag} />
82
+ {/each}
83
+ {/if}
84
+ {#each value as tag, index (tag)}
85
+ <span class="svadmin-tags-input__tag">
86
+ <span>{tag}</span>
87
+ <button
88
+ type="button"
89
+ aria-label={`Remove ${tag}`}
90
+ disabled={disabled}
91
+ onclick={() => remove(index)}
92
+ >
93
+ <X size={14} aria-hidden="true" />
94
+ </button>
95
+ </span>
96
+ {/each}
97
+ <input
98
+ value={draft}
99
+ {placeholder}
100
+ disabled={disabled || (maxItems !== undefined && value.length >= maxItems)}
101
+ required={required && value.length === 0}
102
+ aria-label={ariaLabel}
103
+ aria-invalid={invalid || undefined}
104
+ aria-describedby={describedby}
105
+ oninput={(event) => { draft = (event.currentTarget as HTMLInputElement).value; }}
106
+ onkeydown={handleKeydown}
107
+ oncompositionstart={() => { composing = true; }}
108
+ oncompositionend={(event) => { composing = false; draft = (event.currentTarget as HTMLInputElement).value; }}
109
+ />
110
+ </div>
111
+
112
+ <style>
113
+ .svadmin-tags-input {
114
+ display: flex;
115
+ flex-wrap: wrap;
116
+ align-items: center;
117
+ gap: 4px;
118
+ min-block-size: 36px;
119
+ padding: 4px 8px;
120
+ border: 1px solid var(--border);
121
+ border-radius: 6px;
122
+ background: var(--background);
123
+ }
124
+
125
+ .svadmin-tags-input:focus-within {
126
+ outline: 2px solid var(--ring);
127
+ outline-offset: 1px;
128
+ }
129
+
130
+ .svadmin-tags-input[data-invalid='true'] {
131
+ border-color: var(--destructive);
132
+ }
133
+
134
+ .svadmin-tags-input__tag {
135
+ display: inline-flex;
136
+ align-items: center;
137
+ gap: 4px;
138
+ max-inline-size: 100%;
139
+ padding: 2px 4px 2px 8px;
140
+ border: 1px solid var(--border);
141
+ border-radius: 4px;
142
+ background: var(--muted);
143
+ color: var(--foreground);
144
+ }
145
+
146
+ .svadmin-tags-input__tag span {
147
+ overflow-wrap: anywhere;
148
+ }
149
+
150
+ .svadmin-tags-input__tag button {
151
+ display: inline-grid;
152
+ place-items: center;
153
+ min-block-size: 24px;
154
+ min-inline-size: 24px;
155
+ border: 0;
156
+ border-radius: 4px;
157
+ background: transparent;
158
+ color: var(--muted-foreground);
159
+ cursor: pointer;
160
+ }
161
+
162
+ .svadmin-tags-input__tag button:hover,
163
+ .svadmin-tags-input__tag button:focus-visible {
164
+ background: var(--accent);
165
+ color: var(--accent-foreground);
166
+ }
167
+
168
+ .svadmin-tags-input input {
169
+ min-inline-size: 8rem;
170
+ flex: 1 1 8rem;
171
+ min-block-size: 28px;
172
+ border: 0;
173
+ outline: 0;
174
+ background: transparent;
175
+ color: var(--foreground);
176
+ font: inherit;
177
+ }
178
+ </style>