@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
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { bindResourceRendering, type ResourceRendering } from '../rendering/index.js';
3
+ import type { AutoTableGridState } from './auto-table-grid.js';
2
4
  import { definedOptions } from '@svadmin/core/options';
3
5
 
4
6
  import { onDestroy, tick, untrack } from 'svelte';
@@ -23,6 +25,7 @@
23
25
  import { createAtom, useSelector } from '@tanstack/svelte-store';
24
26
  import {
25
27
  column_getCanSort,
28
+ column_getSize,
26
29
  column_getIsSorted,
27
30
  column_toggleSorting,
28
31
  header_getSize,
@@ -37,9 +40,16 @@
37
40
  cell_getValue,
38
41
  } from '@tanstack/table-core/static-functions';
39
42
 
40
- import { captureAdminContext, DeleteManyPartialError, getAdminOptions, getContractFormFields, useNavigation, useParsed, useResourceContract, useList, useDeleteMany, downloadData } from '@svadmin/core';
43
+ import { captureAdminContext, captureAuthSession, useGetIdentity, DeleteManyPartialError, getAdminOptions, getContractFormFields, useNavigation, useParsed, useResourceContract, useList, useDeleteMany, downloadData, type ExportFormat, type TaskProvider } from '@svadmin/core';
41
44
  import { decodeBaseRecord, snapshotPlainData, parseContractRouteId, formatContractRouteId } from '@svadmin/core/schema';
42
- import { checkedTableRows, copyTableRecord, tableExportRows, tableRowKey, type TableRecord } from './table-contract';
45
+ import {
46
+ checkedTableRows,
47
+ copyTableRecord,
48
+ tableExportRows,
49
+ tableRowKey,
50
+ type BatchSelection,
51
+ type TableRecord,
52
+ } from './table-contract';
43
53
  import type {
44
54
  BaseRecord,
45
55
  FieldDefinition,
@@ -57,6 +67,11 @@
57
67
  cloneSavedListViewState,
58
68
  columnOrderStorageKey,
59
69
  columnVisibilityStorageKey,
70
+ decodeRemoteSavedListViews,
71
+ decodeSavedListViewMutationResult,
72
+ decodeSavedListViewRemoveResult,
73
+ decodeSavedListViewAccess,
74
+ decodeSavedListViewSubjects,
60
75
  legacyActiveSavedListViewStorageKey,
61
76
  legacyColumnOrderStorageKey,
62
77
  legacyColumnVisibilityStorageKey,
@@ -67,7 +82,10 @@
67
82
  serializeSavedListViews,
68
83
  type ListPreferenceScope,
69
84
  type SavedListView,
85
+ type SavedListViewAccess,
86
+ type SavedListViewSubject,
70
87
  type SavedListViewState,
88
+ type SavedListViewProvider,
71
89
  } from './saved-list-views.js';
72
90
 
73
91
  import { fade } from 'svelte/transition';
@@ -83,7 +101,7 @@
83
101
  import * as ContextMenu from './ui/context-menu/index.js';
84
102
  import * as Select from './ui/select/index.js';
85
103
  import {
86
- Plus, Pencil, Trash2,
104
+ Plus, Pencil, Trash2, Star,
87
105
  Search, Download, ChevronDown, ChevronUp, SlidersHorizontal, Filter as FilterIcon,
88
106
  Eye, Copy, RefreshCw, Rows, X, Bookmark, Check
89
107
  } from '@lucide/svelte';
@@ -94,6 +112,7 @@
94
112
  import CanAccess from './CanAccess.svelte';
95
113
  import TooltipButton from './TooltipButton.svelte';
96
114
  import InlineEdit from './InlineEdit.svelte';
115
+ import ExportButton from './buttons/ExportButton.svelte';
97
116
  import FieldDisplay from './FieldDisplay.svelte';
98
117
  import DraggableHeader from './DraggableHeader.svelte';
99
118
  import DataState from './content/DataState.svelte';
@@ -104,7 +123,12 @@
104
123
  // ─── Props with Snippet composability ─────────────────────────
105
124
  interface Props {
106
125
  resourceName: string;
126
+ rendering?: ResourceRendering | undefined;
127
+ /** 可选主体渲染器:不替换资源、权限、工具栏和持久化逻辑。 */
128
+ gridBody?: Snippet<[AutoTableGridState]>;
107
129
  selectable?: boolean;
130
+ /** 仅在业务回调支持按查询执行服务端命令时启用。 */
131
+ allowSelectAllMatching?: boolean;
108
132
  density?: 'compact' | 'comfortable';
109
133
  showHeader?: boolean;
110
134
  title?: string;
@@ -127,16 +151,29 @@
127
151
  pagination?: { current: number; pageSize: number };
128
152
  /** Externally controlled sorters */
129
153
  sorters?: Sort[];
130
- /** Custom batch actions to render when rows are selected */
131
- batchActions?: Snippet<[{ selectedIds: (string | number)[] }]>;
154
+ /** 批量操作必须按 selection.scope 区分明确 ID 与全部查询结果。 */
155
+ batchActions?: Snippet<[{ selectedIds: (string | number)[]; selection: BatchSelection }]>;
132
156
  /** Summary row rendered at table footer */
133
157
  summary?: Snippet<[{ data: BaseRecord[]; total: number; visibleColumnsCount: number }]>;
134
158
  deleteVariables?: unknown;
159
+ /** 团队/系统视图;写入需要 Provider 能力和显式可写标记。 */
160
+ savedViewProvider?: SavedListViewProvider;
161
+ /** 显式允许唯一的远程默认视图在无 URL/个人活动视图时初始化页面。 */
162
+ applyRemoteDefaultView?: boolean;
163
+ /** 提供任务名后按当前筛选/排序提交全量导出;未提供时仍导出当前已加载页。 */
164
+ exportTaskName?: string;
165
+ exportTaskProvider?: TaskProvider;
166
+ exportTaskIdempotencyKey?: string;
167
+ exportFormat?: ExportFormat;
168
+ exportMaxItemCount?: number;
135
169
  }
136
170
 
137
171
  let {
138
172
  resourceName,
173
+ rendering,
174
+ gridBody,
139
175
  selectable = true,
176
+ allowSelectAllMatching = false,
140
177
  density = 'comfortable',
141
178
  showHeader = true,
142
179
  title,
@@ -154,25 +191,43 @@
154
191
  pagination: externalPagination,
155
192
  sorters: externalSorters,
156
193
  deleteVariables,
194
+ savedViewProvider,
195
+ applyRemoteDefaultView = false,
196
+ exportTaskName,
197
+ exportTaskProvider,
198
+ exportTaskIdempotencyKey,
199
+ exportFormat = 'csv',
200
+ exportMaxItemCount,
157
201
  }: Props = $props();
158
202
 
159
203
  let densityOverride = $state<'compact' | 'comfortable' | undefined>(undefined);
204
+ let allMatchingSelected = $state(false);
205
+ const excludedMatchingIds = new SvelteMap<string, string | number>();
160
206
  const currentDensity = $derived(densityOverride ?? density);
161
207
  const adminContext = captureAdminContext();
162
208
  const parsed = useParsed();
163
209
  const navigation = useNavigation();
164
210
 
165
211
  const binding = useResourceContract(() => resourceName);
212
+ const activeRendering = $derived(bindResourceRendering(rendering, binding.resource));
166
213
  const resource = $derived(adminContext.getResource(resourceName));
167
214
  const primaryKey = $derived(resource.primaryKey ?? 'id');
168
215
  const listPermission = useCan(() => ({ resource: resourceName, action: 'list' }));
169
216
  const canRead = $derived(listPermission.allowed);
217
+ const preferenceIdentity = useGetIdentity();
218
+ const preferenceSession = $derived(captureAuthSession(adminContext.authProvider));
219
+ const preferenceIdentityReady = $derived(!adminContext.authProvider || (
220
+ preferenceSession.available && !preferenceIdentity.isLoading && !preferenceIdentity.error
221
+ && typeof preferenceIdentity.data?.id === 'string' && !!preferenceIdentity.data.id.trim()
222
+ ));
170
223
  const listPreferenceScope = $derived.by(() => {
171
- const matcher = adminContext.queryKeyMatcher(resourceName);
172
224
  return definedOptions({
173
225
  resourceName,
174
- providerName: matcher.provider ?? 'default',
175
- tenantIdentity: matcher.tenant,
226
+ providerName: binding.dataProviderName,
227
+ tenantIdentity: adminContext.queryKeyMatcher(resourceName).tenant,
228
+ identityKey: adminContext.authProvider
229
+ ? preferenceIdentityReady ? `id:${preferenceIdentity.data?.id}` : `unresolved:${preferenceSession.cacheKey}`
230
+ : undefined,
176
231
  });
177
232
  });
178
233
 
@@ -186,6 +241,7 @@
186
241
  }
187
242
 
188
243
  function readScopedPreference(scope: ListPreferenceScope, scopedKey: string, legacyKey: string): string | null {
244
+ if (!preferenceIdentityReady) return null;
189
245
  const scoped = readLocalPreference(scopedKey);
190
246
  if (scoped !== null || !canMigrateLegacyListPreferences(scope)) return scoped;
191
247
  const legacy = readLocalPreference(legacyKey);
@@ -213,6 +269,19 @@
213
269
  savedViewColumnIds,
214
270
  ));
215
271
  let savedViews = $state<SavedListView[]>(storedSavedViews);
272
+ let remoteSavedViews = $state<SavedListView[]>([]);
273
+ let remoteViewsLoading = $state(false);
274
+ let remoteViewsFailed = $state(false);
275
+ let remoteViewsReload = $state(0);
276
+ let remoteDefaultEligible = true;
277
+ const availableSavedViews = $derived.by(() => {
278
+ const ids = new Set<string>();
279
+ return [...remoteSavedViews, ...savedViews].filter(view => {
280
+ if (ids.has(view.id)) return false;
281
+ ids.add(view.id);
282
+ return true;
283
+ });
284
+ });
216
285
  const storedActiveSavedViewId = untrack(() => {
217
286
  const candidate = readScopedPreference(
218
287
  listPreferenceScope,
@@ -223,6 +292,16 @@
223
292
  });
224
293
  let savedViewName = $state('');
225
294
  let savedViewsOpen = $state(false);
295
+ let savedViewSource = $state<'local' | 'team' | 'system'>('local');
296
+ let savedViewMutationPending = $state(false);
297
+ let savedViewMutationError = $state<'failure' | 'conflict' | undefined>();
298
+ const accessDrafts = new SvelteMap<string, SavedListViewAccess>();
299
+ const accessSubjects = new SvelteMap<string, SavedListViewSubject[]>();
300
+ const accessSubjectsLoading = new SvelteMap<string, boolean>();
301
+ const accessSubjectsFailed = new SvelteMap<string, boolean>();
302
+ const accessSubjectQueries = new SvelteMap<string, string>();
303
+ const accessSubjectRequests = new Map<string, object>();
304
+ let savedViewMutationEpoch = 0;
226
305
 
227
306
  const hasExplicitURLState = untrack(() => (
228
307
  urlState.page !== undefined
@@ -232,7 +311,7 @@
232
311
  || urlState.filters !== undefined
233
312
  ));
234
313
  let activeSavedViewId = $state<string | undefined>(hasExplicitURLState ? undefined : storedActiveSavedViewId);
235
- const activeSavedViewName = $derived(savedViews.find((view) => view.id === activeSavedViewId)?.name);
314
+ const activeSavedViewName = $derived(availableSavedViews.find((view) => view.id === activeSavedViewId)?.name);
236
315
  const initialSavedView = untrack(() => (
237
316
  !hasExplicitURLState && storedActiveSavedViewId
238
317
  ? storedSavedViews.find((view) => view.id === storedActiveSavedViewId)
@@ -362,6 +441,7 @@
362
441
 
363
442
  function scheduleSearch(event: Event) {
364
443
  if (!(event.currentTarget instanceof HTMLInputElement)) return;
444
+ markSavedViewDirty();
365
445
  searchText = event.currentTarget.value;
366
446
  if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
367
447
  searchDebounceTimer = setTimeout(() => {
@@ -522,7 +602,7 @@
522
602
 
523
603
  // ─── Data fetching ────────────────────────────────────────────
524
604
  const listResult = useList({
525
- get resource() { return binding.resource; },
605
+ get resource() { void activeRendering; return binding.resource; },
526
606
  get dataProviderName() { return binding.dataProviderName; },
527
607
  get queryOptions() { return { enabled: canRead }; },
528
608
  get pagination() { return queryPagination; },
@@ -530,10 +610,25 @@
530
610
  get filters() { return queryFilters; },
531
611
  });
532
612
  const query = listResult;
533
- const pageRecords = $derived.by(() => {
534
- try { return { ok: true as const, data: canRead ? checkedTableRows(query.data?.data ?? []) : [] }; }
535
- catch { return { ok: false as const, data: [] }; }
536
- });
613
+ const pageRecords = {
614
+ get ok(): boolean {
615
+ if (!canRead) return true;
616
+ try {
617
+ checkedTableRows(activeRendering ? activeRendering.records(query.data?.data ?? []) : query.data?.data ?? []);
618
+ return true;
619
+ } catch {
620
+ return false;
621
+ }
622
+ },
623
+ get data(): TableRecord[] {
624
+ if (!canRead) return [];
625
+ try {
626
+ return checkedTableRows(activeRendering ? activeRendering.records(query.data?.data ?? []) : query.data?.data ?? []);
627
+ } catch {
628
+ return [];
629
+ }
630
+ },
631
+ };
537
632
  let deleteRequest = $state<{ ids: (string | number)[]; batch: boolean } | null>(null);
538
633
  const deletePermission = useCan(() => definedOptions({
539
634
  resource: resourceName, action: 'delete',
@@ -543,7 +638,7 @@
543
638
  }));
544
639
  const deleteAllowed = $derived(canRead && resource.canDelete !== false && deleteRequest !== null && deletePermission.allowed);
545
640
  const deleteManyResult = useDeleteMany({
546
- get resource() { return binding.resource; },
641
+ get resource() { void activeRendering; return binding.resource; },
547
642
  get enabled() { return deleteAllowed; },
548
643
  });
549
644
  const deleteManyMutation = deleteManyResult.mutation;
@@ -598,7 +693,7 @@
598
693
  const selectedIdValueByKey = new SvelteMap<string, string | number>();
599
694
 
600
695
  function preferenceScopeIsLoaded(): boolean {
601
- return loadedPreferenceScopeId === listPreferenceScopeId(listPreferenceScope);
696
+ return preferenceIdentityReady && loadedPreferenceScopeId === listPreferenceScopeId(listPreferenceScope);
602
697
  }
603
698
 
604
699
  function rowIsExpanded(rowId: string): boolean {
@@ -607,7 +702,7 @@
607
702
  }
608
703
 
609
704
  function rowIsSelected(rowId: string): boolean {
610
- return rowSelection[rowId] === true;
705
+ return allMatchingSelected ? !excludedMatchingIds.has(rowId) : rowSelection[rowId] === true;
611
706
  }
612
707
 
613
708
  function rowIdValue(row: Row<TableFeatures, TableRecord>): string | number {
@@ -616,6 +711,11 @@
616
711
 
617
712
  function toggleRowSelection(row: Row<TableFeatures, TableRecord>): void {
618
713
  if (confirmPending || !canRead) return;
714
+ if (allMatchingSelected) {
715
+ if (excludedMatchingIds.has(row.id)) excludedMatchingIds.delete(row.id);
716
+ else excludedMatchingIds.set(row.id, rowIdValue(row));
717
+ return;
718
+ }
619
719
  const wasSelected = rowIsSelected(row.id);
620
720
  if (!wasSelected) {
621
721
  selectedIdValueByKey.set(row.id, rowIdValue(row));
@@ -670,6 +770,7 @@
670
770
  }
671
771
 
672
772
  function markSavedViewDirty(): void {
773
+ remoteDefaultEligible = false;
673
774
  if (!activeSavedViewId) return;
674
775
  activeSavedViewId = undefined;
675
776
  persistActiveSavedView();
@@ -690,6 +791,7 @@
690
791
  }
691
792
 
692
793
  function applySavedView(view: SavedListView): void {
794
+ remoteDefaultEligible = false;
693
795
  if (searchDebounceTimer) {
694
796
  clearTimeout(searchDebounceTimer);
695
797
  searchDebounceTimer = undefined;
@@ -724,6 +826,8 @@
724
826
  columnOrderAtom.set(state.columnOrder);
725
827
  persistColumnOrder(state.columnOrder);
726
828
  rowSelection = {};
829
+ allMatchingSelected = false;
830
+ excludedMatchingIds.clear();
727
831
  selectedIdValueByKey.clear();
728
832
  }
729
833
 
@@ -751,8 +855,12 @@
751
855
  const scopeId = listPreferenceScopeId(scope);
752
856
  if (scopeId === loadedPreferenceScopeId) return;
753
857
 
858
+ remoteDefaultEligible = true;
754
859
  const preferences = readScopedSavedViewPreferences(scope);
860
+ savedViewName = '';
861
+ savedViewsOpen = false;
755
862
  savedViews = preferences.savedViews;
863
+ remoteSavedViews = [];
756
864
  activeSavedViewId = preferences.activeSavedViewId;
757
865
 
758
866
  if (!hasExplicitURLState && preferences.activeSavedView) {
@@ -762,13 +870,68 @@
762
870
  columnVisibilityAtom.set(preferences.columnVisibility);
763
871
  columnOrderAtom.set(preferences.columnOrder);
764
872
  rowSelection = {};
873
+ allMatchingSelected = false;
874
+ excludedMatchingIds.clear();
765
875
  selectedIdValueByKey.clear();
766
876
  }
767
877
 
768
878
  loadedPreferenceScopeId = scopeId;
769
879
  });
770
880
 
881
+ let remoteViewEpoch = 0;
882
+ $effect(() => {
883
+ const provider = savedViewProvider;
884
+ const scope = listPreferenceScope;
885
+ const columns = savedViewColumnIds;
886
+ void remoteViewsReload;
887
+ remoteSavedViews = [];
888
+ remoteViewsLoading = false;
889
+ remoteViewsFailed = false;
890
+ accessDrafts.clear();
891
+ accessSubjects.clear();
892
+ accessSubjectsLoading.clear();
893
+ accessSubjectsFailed.clear();
894
+ accessSubjectQueries.clear();
895
+ accessSubjectRequests.clear();
896
+ savedViewMutationEpoch += 1;
897
+ savedViewMutationPending = false;
898
+ savedViewMutationError = undefined;
899
+ savedViewSource = 'local';
900
+ if (!provider || !preferenceScopeIsLoaded()) {
901
+ remoteSavedViews = [];
902
+ return;
903
+ }
904
+ const epoch = ++remoteViewEpoch;
905
+ remoteViewsLoading = true;
906
+ void Promise.resolve().then(() => provider.list({ ...scope })).then(value => {
907
+ if (epoch !== remoteViewEpoch || !preferenceScopeIsLoaded()) return;
908
+ remoteSavedViews = decodeRemoteSavedListViews(value, columns, !!(provider.save || provider.remove || provider.setDefault || provider.updateAccess));
909
+ if (applyRemoteDefaultView && remoteDefaultEligible) {
910
+ const defaults = remoteSavedViews.filter(view => view.default === true);
911
+ if (!hasExplicitURLState && !activeSavedViewId && !externalPagination && !externalSorters
912
+ && defaults.length === 1 && defaults[0]) {
913
+ applySavedView(defaults[0]);
914
+ remoteDefaultEligible = false;
915
+ }
916
+ }
917
+ }).catch(() => {
918
+ if (epoch === remoteViewEpoch) {
919
+ remoteSavedViews = [];
920
+ remoteViewsFailed = true;
921
+ }
922
+ }).finally(() => {
923
+ if (epoch === remoteViewEpoch) remoteViewsLoading = false;
924
+ });
925
+ return () => {
926
+ remoteViewEpoch += 1;
927
+ savedViewMutationEpoch += 1;
928
+ accessSubjectRequests.clear();
929
+ };
930
+ });
931
+
771
932
  function saveCurrentView(): void {
933
+ if (!preferenceScopeIsLoaded() || savedViewMutationPending) return;
934
+ remoteDefaultEligible = false;
772
935
  const name = savedViewName.trim().slice(0, 60);
773
936
  if (!name) return;
774
937
  if (searchDebounceTimer) {
@@ -776,6 +939,16 @@
776
939
  searchDebounceTimer = undefined;
777
940
  appliedSearchText = searchText;
778
941
  }
942
+ if (savedViewSource !== 'local') {
943
+ const existing = remoteSavedViews.find(view => view.source === savedViewSource
944
+ && view.name.toLocaleLowerCase() === name.toLocaleLowerCase());
945
+ if (!savedViewProvider?.save || (existing && (existing.readOnly || !existing.version))) return;
946
+ void mutateRemoteView({
947
+ id: existing?.id ?? crypto.randomUUID(), name, state: getCurrentSavedViewState(),
948
+ source: savedViewSource, expectedVersion: existing?.version ?? null,
949
+ });
950
+ return;
951
+ }
779
952
  const existing = savedViews.find((view) => view.name.toLocaleLowerCase() === name.toLocaleLowerCase());
780
953
  const view: SavedListView = {
781
954
  id: existing?.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
@@ -792,12 +965,184 @@
792
965
  }
793
966
 
794
967
  function deleteSavedView(id: string): void {
968
+ if (!preferenceScopeIsLoaded() || savedViewMutationPending) return;
969
+ const remote = remoteSavedViews.find(view => view.id === id);
970
+ if (remote) {
971
+ if (!savedViewProvider?.remove || remote.readOnly || !remote.version) return;
972
+ void mutateRemoteView({
973
+ id, expectedVersion: remote.version, source: remote.source === 'system' ? 'system' : 'team',
974
+ name: remote.name, state: remote.state,
975
+ }, true);
976
+ return;
977
+ }
795
978
  savedViews = savedViews.filter((view) => view.id !== id);
796
979
  if (activeSavedViewId === id) activeSavedViewId = undefined;
797
980
  persistSavedViews();
798
981
  persistActiveSavedView();
799
982
  }
800
983
 
984
+ async function mutateRemoteView(
985
+ mutation: import('./saved-list-views').SavedListViewMutation, remove = false,
986
+ ): Promise<void> {
987
+ const provider = savedViewProvider;
988
+ if (!provider || savedViewMutationPending || !preferenceScopeIsLoaded()
989
+ || (remove && (mutation.expectedVersion === null || !provider.remove))
990
+ || (!remove && !provider.save)) return;
991
+ const scope = { ...listPreferenceScope };
992
+ const scopeId = listPreferenceScopeId(scope);
993
+ const columns = savedViewColumnIds;
994
+ const epoch = ++savedViewMutationEpoch;
995
+ const current = () => epoch === savedViewMutationEpoch && provider === savedViewProvider
996
+ && scopeId === listPreferenceScopeId(listPreferenceScope) && preferenceScopeIsLoaded();
997
+ savedViewMutationPending = true;
998
+ savedViewMutationError = undefined;
999
+ try {
1000
+ const result = remove
1001
+ ? await provider.remove?.(scope, { id: mutation.id, expectedVersion: mutation.expectedVersion ?? 0 })
1002
+ : await provider.save?.(scope, { ...mutation, state: cloneSavedListViewState(mutation.state) });
1003
+ if (!current()) return;
1004
+ if (remove && mutation.expectedVersion !== null && decodeSavedListViewRemoveResult(result, {
1005
+ id: mutation.id, expectedVersion: mutation.expectedVersion,
1006
+ })) {
1007
+ remoteSavedViews = remoteSavedViews.filter(view => view.id !== mutation.id);
1008
+ if (activeSavedViewId === mutation.id) activeSavedViewId = undefined;
1009
+ persistActiveSavedView();
1010
+ return;
1011
+ }
1012
+ const decoded = decodeSavedListViewMutationResult(result, columns, mutation);
1013
+ if (!decoded || (remove && decoded.ok)) {
1014
+ savedViewMutationError = 'failure';
1015
+ return;
1016
+ }
1017
+ if (!decoded.ok) {
1018
+ savedViewMutationError = 'conflict';
1019
+ // 保留本地查询;用户必须重新读取后再决定如何修改。
1020
+ return;
1021
+ }
1022
+ const view = { ...decoded.view, readOnly: decoded.view.readOnly !== false };
1023
+ remoteSavedViews = [view, ...remoteSavedViews.filter(candidate => candidate.id !== view.id)];
1024
+ savedViewName = '';
1025
+ } catch {
1026
+ if (current()) savedViewMutationError = 'failure';
1027
+ } finally {
1028
+ if (current()) savedViewMutationPending = false;
1029
+ }
1030
+ }
1031
+
1032
+ function setRemoteDefault(view: SavedListView): void {
1033
+ const provider = savedViewProvider;
1034
+ if (!provider?.setDefault || savedViewMutationPending || view.readOnly || !view.version
1035
+ || savedViewMutationError === 'conflict' || !preferenceScopeIsLoaded()
1036
+ || (view.source !== 'team' && view.source !== 'system')) return;
1037
+ const source = view.source;
1038
+ const expectedVersion = view.version;
1039
+ const defaultValue = view.default !== true;
1040
+ remoteDefaultEligible = false;
1041
+ const scope = { ...listPreferenceScope };
1042
+ const scopeId = listPreferenceScopeId(scope);
1043
+ const epoch = ++savedViewMutationEpoch;
1044
+ const current = () => epoch === savedViewMutationEpoch && provider === savedViewProvider
1045
+ && scopeId === listPreferenceScopeId(listPreferenceScope) && preferenceScopeIsLoaded();
1046
+ savedViewMutationPending = true;
1047
+ savedViewMutationError = undefined;
1048
+ void Promise.resolve().then(() => provider.setDefault?.(scope, {
1049
+ id: view.id, source, expectedVersion, default: defaultValue,
1050
+ })).then(result => {
1051
+ if (!current()) return;
1052
+ const decoded = decodeSavedListViewMutationResult(result, savedViewColumnIds, {
1053
+ id: view.id, source, expectedVersion,
1054
+ });
1055
+ if (!decoded) {
1056
+ savedViewMutationError = 'failure';
1057
+ } else if (!decoded.ok) {
1058
+ savedViewMutationError = 'conflict';
1059
+ } else if (decoded.view.default !== defaultValue) {
1060
+ savedViewMutationError = 'failure';
1061
+ } else {
1062
+ // 默认标记会同时改变同一作用域的其他视图,成功后重新读取完整集合。
1063
+ remoteViewsReload += 1;
1064
+ }
1065
+ }).catch(() => {
1066
+ if (current()) savedViewMutationError = 'failure';
1067
+ }).finally(() => {
1068
+ if (current()) savedViewMutationPending = false;
1069
+ });
1070
+ }
1071
+
1072
+ function saveRemoteAccess(view: SavedListView): void {
1073
+ const provider = savedViewProvider;
1074
+ const draft = accessDrafts.get(view.id);
1075
+ if (!provider?.updateAccess || !draft || view.readOnly !== false || !view.version
1076
+ || savedViewMutationPending || savedViewMutationError === 'conflict' || !preferenceScopeIsLoaded()
1077
+ || (view.source !== 'team' && view.source !== 'system')) return;
1078
+ const access = decodeSavedListViewAccess({
1079
+ mode: draft.mode,
1080
+ subjectIds: draft.mode === 'restricted' ? draft.subjectIds : [],
1081
+ });
1082
+ if (!access) { savedViewMutationError = 'failure'; return; }
1083
+ const source = view.source;
1084
+ const expectedVersion = view.version;
1085
+ const scope = { ...listPreferenceScope };
1086
+ const scopeId = listPreferenceScopeId(scope);
1087
+ const epoch = ++savedViewMutationEpoch;
1088
+ const current = () => epoch === savedViewMutationEpoch && provider === savedViewProvider
1089
+ && scopeId === listPreferenceScopeId(listPreferenceScope) && preferenceScopeIsLoaded();
1090
+ remoteDefaultEligible = false;
1091
+ savedViewMutationPending = true;
1092
+ savedViewMutationError = undefined;
1093
+ void Promise.resolve().then(() => {
1094
+ if (!current()) return;
1095
+ return provider.updateAccess?.(scope, {
1096
+ id: view.id, source, expectedVersion, access: { ...access, subjectIds: [...access.subjectIds] },
1097
+ });
1098
+ }).then(result => {
1099
+ if (!current()) return;
1100
+ const decoded = decodeSavedListViewMutationResult(result, savedViewColumnIds, {
1101
+ id: view.id, source, expectedVersion,
1102
+ });
1103
+ if (!decoded) savedViewMutationError = 'failure';
1104
+ else if (!decoded.ok) savedViewMutationError = 'conflict';
1105
+ else if (decoded.view.access?.mode !== access.mode
1106
+ || decoded.view.access.subjectIds.length !== access.subjectIds.length
1107
+ || !decoded.view.access.subjectIds.every(id => access.subjectIds.includes(id))) {
1108
+ savedViewMutationError = 'failure';
1109
+ } else remoteViewsReload += 1;
1110
+ }).catch(() => {
1111
+ if (current()) savedViewMutationError = 'failure';
1112
+ }).finally(() => {
1113
+ if (current()) savedViewMutationPending = false;
1114
+ });
1115
+ }
1116
+
1117
+ function loadAccessSubjects(view: SavedListView, query = ''): void {
1118
+ const provider = savedViewProvider;
1119
+ if (!provider?.listAccessSubjects || view.readOnly !== false
1120
+ || view.source === 'local' || !preferenceScopeIsLoaded()) return;
1121
+ const scope = { ...listPreferenceScope };
1122
+ const scopeId = listPreferenceScopeId(scope);
1123
+ const request = {};
1124
+ accessSubjectRequests.set(view.id, request);
1125
+ const current = () => accessSubjectRequests.get(view.id) === request
1126
+ && scopeId === listPreferenceScopeId(listPreferenceScope)
1127
+ && provider === savedViewProvider && preferenceScopeIsLoaded();
1128
+ accessSubjectsLoading.set(view.id, true);
1129
+ accessSubjectsFailed.delete(view.id);
1130
+ accessSubjects.delete(view.id);
1131
+ void Promise.resolve().then(() => {
1132
+ if (current()) return provider.listAccessSubjects?.(scope, { query: query.trim(), limit: 50 });
1133
+ })
1134
+ .then(result => {
1135
+ if (!current()) return;
1136
+ const subjects = decodeSavedListViewSubjects(result);
1137
+ if (!subjects) accessSubjectsFailed.set(view.id, true);
1138
+ else accessSubjects.set(view.id, subjects);
1139
+ })
1140
+ .catch(() => {
1141
+ if (current()) accessSubjectsFailed.set(view.id, true);
1142
+ })
1143
+ .finally(() => { if (current()) accessSubjectsLoading.delete(view.id); });
1144
+ }
1145
+
801
1146
  function setColumnVisibility(columnId: string, visible: boolean): void {
802
1147
  markSavedViewDirty();
803
1148
  columnVisibilityAtom.set({ ...tableColumnVisibility.current, [columnId]: visible });
@@ -915,6 +1260,10 @@
915
1260
 
916
1261
  function toggleAllRowsSelection(): void {
917
1262
  if (confirmPending || !canRead) return;
1263
+ if (allMatchingSelected) {
1264
+ clearSelection();
1265
+ return;
1266
+ }
918
1267
  const willSelect = !table_getIsAllRowsSelected(tbl);
919
1268
  for (const row of tableView.rows) {
920
1269
  if (willSelect) {
@@ -933,7 +1282,58 @@
933
1282
  return id === undefined ? [] : [id];
934
1283
  })
935
1284
  );
936
- const selectedCount = $derived(Object.keys(rowSelection).filter(key => rowSelection[key] === true).length);
1285
+ const matchingTotal = $derived(query.data?.total ?? 0);
1286
+ const selectedCount = $derived(allMatchingSelected
1287
+ ? Math.max(0, matchingTotal - excludedMatchingIds.size)
1288
+ : selectedIds.length);
1289
+ const selectionLabel = $derived(i18n.t(
1290
+ allMatchingSelected ? 'common.allMatchingSelected' : 'common.selectedCount',
1291
+ { count: selectedCount },
1292
+ ));
1293
+ const selectionReady = $derived(
1294
+ canRead && selectable && pageRecords.ok && !query.isError && !query.isFetching
1295
+ );
1296
+ const canSelectAllMatching = $derived(
1297
+ allowSelectAllMatching && !!batchActions && selectionReady
1298
+ && Number.isSafeInteger(matchingTotal) && matchingTotal > 0
1299
+ );
1300
+ function batchSelectionSnapshot(): BatchSelection {
1301
+ if (allMatchingSelected) return {
1302
+ scope: 'all', filters: queryFilters.map(cloneFilter),
1303
+ sorters: querySorters.map(sorter => ({ ...sorter })), total: matchingTotal,
1304
+ excludedIds: [...excludedMatchingIds.values()],
1305
+ };
1306
+ return {
1307
+ scope: 'selected', ids: [...selectedIds],
1308
+ currentPageIds: tableView.rows.filter(row => rowSelection[row.id] === true).map(rowIdValue),
1309
+ };
1310
+ }
1311
+ const selectionCriteria = $derived(JSON.stringify(queryFilters));
1312
+ let previousSelectionCriteria = untrack(() => selectionCriteria);
1313
+ $effect.pre(() => {
1314
+ const criteria = selectionCriteria;
1315
+ if (criteria !== previousSelectionCriteria) {
1316
+ allMatchingSelected = false;
1317
+ excludedMatchingIds.clear();
1318
+ rowSelection = {};
1319
+ selectedIdValueByKey.clear();
1320
+ if (deleteRequest?.batch && !confirmPending) {
1321
+ confirmOpen = false;
1322
+ deleteRequest = null;
1323
+ }
1324
+ }
1325
+ previousSelectionCriteria = criteria;
1326
+ });
1327
+ $effect.pre(() => {
1328
+ if (!selectable || !allowSelectAllMatching || !batchActions) {
1329
+ allMatchingSelected = false;
1330
+ excludedMatchingIds.clear();
1331
+ }
1332
+ if (!selectable) {
1333
+ rowSelection = {};
1334
+ selectedIdValueByKey.clear();
1335
+ }
1336
+ });
937
1337
  const batchDeletePerm = useCan(() => ({
938
1338
  resource: resourceName,
939
1339
  action: 'delete',
@@ -941,16 +1341,26 @@
941
1341
  queryOptions: { enabled: acEnabled && canDelete && selectedIds.length > 0 },
942
1342
  }));
943
1343
  const canBatchDelete = $derived(
944
- canDelete && (!acEnabled || batchDeletePerm.isLoading || batchDeletePerm.allowed)
1344
+ !allMatchingSelected && canDelete && (!acEnabled || batchDeletePerm.isLoading || batchDeletePerm.allowed)
945
1345
  );
946
1346
 
947
1347
  function clearSelection(): void {
948
1348
  if (!deleteManyMutation.isPending) {
1349
+ allMatchingSelected = false;
1350
+ excludedMatchingIds.clear();
949
1351
  selectedIdValueByKey.clear();
950
1352
  table_resetRowSelection(tbl, true);
951
1353
  }
952
1354
  }
953
1355
 
1356
+ function selectAllMatching(): void {
1357
+ if (!canSelectAllMatching || confirmPending || confirmOpen) return;
1358
+ selectedIdValueByKey.clear();
1359
+ excludedMatchingIds.clear();
1360
+ rowSelection = {};
1361
+ allMatchingSelected = true;
1362
+ }
1363
+
954
1364
  function isColumnVisible(columnId: string): boolean {
955
1365
  return tableColumnVisibility.current[columnId] !== false;
956
1366
  }
@@ -1025,6 +1435,19 @@
1025
1435
  variables: deleteVariables,
1026
1436
  });
1027
1437
  let previousTableScope: typeof tableScope | undefined;
1438
+ function tableScopesEqual(left: typeof tableScope, right: typeof tableScope): boolean {
1439
+ return left.contract === right.contract
1440
+ && left.resourceName === right.resourceName
1441
+ && left.provider === right.provider
1442
+ && left.meta === right.meta
1443
+ && left.tenant === right.tenant
1444
+ && left.auth === right.auth
1445
+ && left.router === right.router
1446
+ && left.permissionProvider === right.permissionProvider
1447
+ && left.canRead === right.canRead
1448
+ && left.canDelete === right.canDelete
1449
+ && left.variables === right.variables;
1450
+ }
1028
1451
  const detailState = $derived.by(() => {
1029
1452
  const route = parsed.params['detail'];
1030
1453
  if (route === undefined || !canRead) return { id: undefined, invalid: false };
@@ -1035,12 +1458,14 @@
1035
1458
  const detailOpen = $derived(detailRecordId != null);
1036
1459
  $effect.pre(() => {
1037
1460
  const scope = tableScope;
1038
- if (previousTableScope && previousTableScope !== scope) {
1461
+ if (previousTableScope && !tableScopesEqual(previousTableScope, scope)) {
1039
1462
  activeDelete = undefined;
1040
1463
  confirmOpen = false;
1041
1464
  confirmPending = false;
1042
1465
  deleteRequest = null;
1043
1466
  operationError = null;
1467
+ allMatchingSelected = false;
1468
+ excludedMatchingIds.clear();
1044
1469
  selectedIdValueByKey.clear();
1045
1470
  rowSelection = {};
1046
1471
  expandedAtom.set({});
@@ -1177,8 +1602,205 @@
1177
1602
  function refreshList() {
1178
1603
  if (canRead && !query.isFetching) void listResult.refetch();
1179
1604
  }
1605
+ const gridRowIndex = $derived(new Map(tableView.rows.map(row => [rowIdValue(row), row])));
1606
+ const gridColumns = $derived.by(() => {
1607
+ void tableColumnOrder.current; void tableColumnVisibility.current;
1608
+ return table_getAllLeafColumns(tbl).filter(column => isColumnVisible(column.id)).map(column => ({
1609
+ key: column.id,
1610
+ label: column.id === '_actions' ? i18n.t('common.actions') : column.id === '_select' ? i18n.t('common.selectAll') : column.id === '_expand' ? i18n.t('common.expand') : visibleFields.find(field => field.key === column.id)?.label ?? column.id,
1611
+ width: Math.max(40, Math.min(4096, column_getSize(column))),
1612
+ sortable: column_getCanSort(column),
1613
+ }));
1614
+ });
1615
+ function changeGridSort(next: readonly Sort[]): void {
1616
+ if (!canRead || query.isFetching || deleteManyMutation.isPending) return;
1617
+ markSavedViewDirty();
1618
+ const accepted = next.filter(sort => gridColumns.some(column => column.key === sort.field && column.sortable));
1619
+ sorters = accepted.map(sort => ({ ...sort }));
1620
+ sortingAtom.set(accepted.map(sort => ({ id: sort.field, desc: sort.order === 'desc' })));
1621
+ pagination = { ...pagination, current: 1 };
1622
+ }
1180
1623
  </script>
1181
1624
 
1625
+ {#snippet renderGridCell({ id, field }: { id: string | number; field: string })}
1626
+ {@const row = gridRowIndex.get(id)}
1627
+ {#if row}
1628
+ {@const record = copyTableRecord(row.original)}
1629
+ {@const cell = row_getVisibleCells(row).find(item => item.column.id === field)}
1630
+ {#if cell}
1631
+ <ContextMenu.Root>
1632
+ <ContextMenu.Trigger>
1633
+ {#snippet child({ props })}
1634
+ <div {...props}>
1635
+ {#if cell.column.id === '_select'}
1636
+ {#if batchActions}
1637
+ <Checkbox
1638
+ aria-label={i18n.t('common.selectRow', { id })}
1639
+ checked={rowIsSelected(row.id)}
1640
+ onCheckedChange={() => toggleRowSelection(row)}
1641
+ />
1642
+ {:else}
1643
+ <CanAccess resource={resourceName} action="delete" params={{ id }}>
1644
+ <Checkbox
1645
+ aria-label={i18n.t('common.selectRow', { id })}
1646
+ checked={rowIsSelected(row.id)}
1647
+ onCheckedChange={() => toggleRowSelection(row)}
1648
+ />
1649
+ </CanAccess>
1650
+ {/if}
1651
+ {:else if cell.column.id === '_expand'}
1652
+ <TooltipButton tooltip={rowIsExpanded(row.id) ? i18n.t('common.collapse') : i18n.t('common.expand')} variant="ghost" size="icon" class="svadmin-u-d0a52b312f7d svadmin-u-cbbf90f9a828" onclick={() => toggleRowExpanded(row.id)}>
1653
+ {#if rowIsExpanded(row.id)}
1654
+ <ChevronUp class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
1655
+ {:else}
1656
+ <ChevronDown class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
1657
+ {/if}
1658
+ </TooltipButton>
1659
+ {:else if cell.column.id === '_actions'}
1660
+ <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77c08e015d14 svadmin-u-44ee8ba0a421">
1661
+ {#if rowActions}
1662
+ {@render rowActions({ record, id })}
1663
+ {:else}
1664
+ {@render defaultRowActions(id)}
1665
+ {/if}
1666
+ </div>
1667
+ {:else}
1668
+ {@const field = visibleFields.find(f => f.key === cell.column.id)}
1669
+ {@const customColumn = field ? customColumns?.[field.key] : undefined}
1670
+ {#if customColumn}
1671
+ {@render customColumn({ value: clonePlainValue(cell_getValue(cell)), record: copyTableRecord(record) })}
1672
+ {:else if defaultCellRenderer && field}
1673
+ {@render defaultCellRenderer({ field, value: clonePlainValue(cell_getValue(cell)), record: copyTableRecord(record) })}
1674
+ {:else if canEdit && field && field.showInEdit !== false && writableFields.has(field.key) && field.key !== primaryKey && field.key !== 'id' && ['text', 'number', 'email', 'url'].includes(field.type)}
1675
+ <CanAccess resource={resourceName} action="edit" params={{ id }}>
1676
+ <InlineEdit
1677
+ {resourceName}
1678
+ recordId={id}
1679
+ {field}
1680
+ value={cell_getValue(cell)}
1681
+ />
1682
+ {#snippet fallback()}
1683
+ <FieldDisplay type={field.type} value={cell_getValue(cell)} options={field.options} />
1684
+ {/snippet}
1685
+ </CanAccess>
1686
+ {:else if field?.key === 'id'}
1687
+ <span class="svadmin-u-0214b4b355d1 svadmin-u-f283ea9bea0e svadmin-u-0e65706bcccd svadmin-u-359090c2d529" title={String(cell_getValue(cell) ?? '—')}>{cell_getValue(cell) ?? '—'}</span>
1688
+ {:else if field}
1689
+ <FieldDisplay type={field.type} value={cell_getValue(cell)} options={field.options} resourceName={field.resource} />
1690
+ {/if}
1691
+ {/if}
1692
+ </div>
1693
+ {/snippet}
1694
+ </ContextMenu.Trigger>
1695
+ <ContextMenu.Content class="svadmin-u-74b2435a1d40">
1696
+ {#if canEdit}
1697
+ <CanAccess resource={resourceName} action="edit" params={{ id }}>
1698
+ <ContextMenu.Item onclick={() => navigation.edit(resourceName, id)} class="svadmin-u-77a2a20e90d4">
1699
+ <Pencil class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" /> {i18n.t('common.edit')}
1700
+ </ContextMenu.Item>
1701
+ </CanAccess>
1702
+ {/if}
1703
+ {#if canShow}
1704
+ <CanAccess resource={resourceName} action="show" params={{ id }}>
1705
+ <ContextMenu.Item onclick={() => openDetail(id)} class="svadmin-u-77a2a20e90d4">
1706
+ <Eye class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" /> {i18n.t('common.detail')}
1707
+ </ContextMenu.Item>
1708
+ </CanAccess>
1709
+ {/if}
1710
+ <ContextMenu.Item onclick={() => navigator.clipboard?.writeText(String(id))} class="svadmin-u-77a2a20e90d4">
1711
+ <Copy class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" /> {i18n.t('common.copyId')}
1712
+ </ContextMenu.Item>
1713
+ {#if canDelete}
1714
+ <CanAccess resource={resourceName} action="delete" params={{ id }}>
1715
+ <ContextMenu.Separator />
1716
+ <ContextMenu.Item onclick={() => confirmDelete(id)} class="svadmin-u-77a2a20e90d4 svadmin-u-811148b13d1e">
1717
+ <Trash2 class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" /> {i18n.t('common.delete')}
1718
+ </ContextMenu.Item>
1719
+ </CanAccess>
1720
+ {/if}
1721
+ </ContextMenu.Content>
1722
+ </ContextMenu.Root>
1723
+ {/if}
1724
+ {/if}
1725
+ {/snippet}
1726
+
1727
+ {#snippet renderGridControls()}
1728
+ <Table.Root density={currentDensity}>
1729
+ <Table.Header>
1730
+ {#each tableView.headerGroups as headerGroup, _i (_i)}
1731
+ {@const visibleHeaders = headerGroup.headers.filter((header: Header<TableFeatures, TableRecord, unknown>) => isColumnVisible(header.column.id))}
1732
+ <DraggableHeader
1733
+ columns={visibleHeaders.map((header) => ({ id: header.column.id, header }))}
1734
+ onReorder={setColumnOrder}
1735
+ >
1736
+ {#snippet header(col, _index, dragProps)}
1737
+ {@const header = col.header}
1738
+ <Table.Head
1739
+ {...dragProps}
1740
+ class={cn('svadmin-u-65fdbade2025 svadmin-u-18049387f0af svadmin-u-b247a17a0d75 svadmin-u-2689f3958069 svadmin-u-d9256981a032 svadmin-u-bfa603190748 svadmin-u-f6e31b39b8e4', dragProps.class)}
1741
+ style={header_getSize(header) != null && header_getSize(header) !== 150 ? `width:${header_getSize(header)}px` : undefined}
1742
+ >
1743
+ {#if header.id === '_select'}
1744
+ {#if batchActions}
1745
+ <Checkbox
1746
+ aria-label={i18n.t('common.selectAll')}
1747
+ checked={table_getIsAllRowsSelected(tbl)}
1748
+ onCheckedChange={toggleAllRowsSelection}
1749
+ />
1750
+ {:else if !acEnabled || deleteResourcePermission.isLoading || deleteResourcePermission.allowed}
1751
+ <Checkbox
1752
+ aria-label={i18n.t('common.selectAll')}
1753
+ checked={table_getIsAllRowsSelected(tbl)}
1754
+ onCheckedChange={toggleAllRowsSelection}
1755
+ />
1756
+ {/if}
1757
+ {:else if header.id === '_expand'}
1758
+ <!-- empty -->
1759
+ {:else if header.id === '_actions'}
1760
+ <span class="svadmin-u-308fc069e46e svadmin-u-0214b4b355d1">{i18n.t('common.actions')}</span>
1761
+ {:else if column_getCanSort(header.column)}
1762
+ <Button
1763
+ variant="ghost"
1764
+ size="sm"
1765
+ class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-44ee8ba0a421 svadmin-u-ea7b2e9e070e svadmin-u-8ffe7a29179a svadmin-u-b8f0a08ece1e svadmin-u-660d2effb880 svadmin-u-d5eab218aa34 uppercase svadmin-u-8baf13a3e9d7 svadmin-u-7357df2b2e0c svadmin-u-e83a7042bc91"
1766
+ onclick={() => toggleColumnSort(header.column)}
1767
+ >
1768
+ {visibleFields.find(f => f.key === header.id)?.label ?? header.id}
1769
+ <span class="svadmin-u-359090c2d529 svadmin-u-0b8c506a0596">
1770
+ {#if column_getIsSorted(header.column) === 'asc'}↑
1771
+ {:else if column_getIsSorted(header.column) === 'desc'}↓
1772
+ {:else}⇅
1773
+ {/if}
1774
+ </span>
1775
+ </Button>
1776
+ {:else}
1777
+ {visibleFields.find(f => f.key === header.id)?.label ?? header.id}
1778
+ {/if}
1779
+ </Table.Head>
1780
+ {/snippet}
1781
+ </DraggableHeader>
1782
+ {/each}
1783
+ </Table.Header>
1784
+ </Table.Root>
1785
+ {/snippet}
1786
+
1787
+ {#snippet renderGridAfter()}
1788
+ <Table.Root density={currentDensity}>
1789
+ <Table.Body>
1790
+ {#each tableView.rows as row (row.id)}
1791
+ {#if expandedRowRender && rowIsExpanded(row.id)}
1792
+ <Table.Row><Table.Cell colspan={Math.max(1, gridColumns.length)}>
1793
+ {@render expandedRowRender({ record: copyTableRecord(row.original) })}
1794
+ </Table.Cell></Table.Row>
1795
+ {/if}
1796
+ {/each}
1797
+ </Table.Body>
1798
+ {#if summary}<Table.Footer>
1799
+ {@render summary({ data: pageRecords.data.map(copyTableRecord), total: query.data?.total ?? 0, visibleColumnsCount: gridColumns.length })}
1800
+ </Table.Footer>{/if}
1801
+ </Table.Root>
1802
+ {/snippet}
1803
+
1182
1804
  {#snippet defaultRowActions(id: string | number)}
1183
1805
  <RecordRowActions
1184
1806
  {resourceName} {id} {canShow} {canEdit} {canDelete}
@@ -1208,7 +1830,7 @@
1208
1830
  </DataState>
1209
1831
  {/snippet}
1210
1832
 
1211
- <div class="svadmin-u-6ed543e2fbbb">
1833
+ <div class="svadmin-u-6ed543e2fbbb" data-svadmin-rendering-resource={activeRendering?.resource.name} data-svadmin-rendering-kind={activeRendering ? 'table' : undefined}>
1212
1834
  {#if operationError}<p role="alert">{operationError}</p>{/if}
1213
1835
  {#if detailState.invalid}<p role="alert">{i18n.t('common.operationFailed')}</p>{/if}
1214
1836
  {#if showHeader}
@@ -1217,9 +1839,21 @@
1217
1839
  <h1 class="svadmin-u-42536e69e639 svadmin-u-998e0b29fe9e svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{title ?? resource.label}</h1>
1218
1840
  <div class="svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
1219
1841
  {#if canExport}
1220
- <Button variant="outline" size="sm" onclick={exportCSV}>
1221
- <Download class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" data-icon="inline-start" /> {i18n.t("common.export")}
1222
- </Button>
1842
+ {#if exportTaskName}
1843
+ <ExportButton
1844
+ resource={resourceName}
1845
+ taskName={exportTaskName}
1846
+ {...definedOptions({ taskProvider: exportTaskProvider, taskIdempotencyKey: exportTaskIdempotencyKey, maxItemCount: exportMaxItemCount })}
1847
+ format={exportFormat}
1848
+ filters={activeFilters}
1849
+ sorters={sorters}
1850
+ accessControl={{ enabled: acEnabled, hideIfUnauthorized: true }}
1851
+ />
1852
+ {:else}
1853
+ <Button variant="outline" size="sm" onclick={exportCSV}>
1854
+ <Download class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" data-icon="inline-start" /> {i18n.t("common.export")}
1855
+ </Button>
1856
+ {/if}
1223
1857
  {/if}
1224
1858
  {#if headerActions}
1225
1859
  {@render headerActions()}
@@ -1234,21 +1868,27 @@
1234
1868
  {/if}
1235
1869
 
1236
1870
  <!-- Selection Banner (Enterprise Batch Actions) -->
1237
- {#if selectedCount > 0}
1871
+ {#if selectedCount > 0 || allMatchingSelected}
1238
1872
  <div
1239
1873
  class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-1004c0c3954c svadmin-u-ca6bcd4b6f3f svadmin-u-a6afccfc915b svadmin-u-375dc44df6e9 svadmin-u-e0d9cc7f0647 svadmin-u-03b4dd7f172b svadmin-u-5f22e64f2282 svadmin-u-fc7473ca09eb svadmin-u-d4108abe6359 svadmin-u-40137e897961 fade-in svadmin-u-625a4c3fbeb2 svadmin-u-259ce51fc8f3 svadmin-u-020ba687fa12 svadmin-u-9f76a62f4f44 svadmin-u-3b9871a0bf93"
1240
- aria-label={i18n.t("common.selectedCount", { count: selectedCount })}
1874
+ aria-label={selectionLabel}
1241
1875
  data-svadmin-batch-toolbar
1242
1876
  >
1243
1877
  <div class="svadmin-u-60fbb7713999 svadmin-u-7e0b7cdf1a94 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 svadmin-u-fc7473ca09eb">
1244
- <span class="svadmin-u-2689f3958069 svadmin-u-d4108abe6359 svadmin-u-3032cae0badb">{i18n.t("common.selectedCount", { count: selectedCount })}</span>
1878
+ <span class="svadmin-u-2689f3958069 svadmin-u-d4108abe6359 svadmin-u-3032cae0badb">{selectionLabel}</span>
1245
1879
  {#if deleteManyMutation.isPending}
1246
1880
  <span class="svadmin-u-bfa603190748" role="status">{i18n.t("common.processing")}</span>
1247
1881
  {/if}
1248
1882
  </div>
1249
1883
  <div class="svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
1250
- {#if batchActions}
1251
- {@render batchActions({ selectedIds })}
1884
+ {#if batchActions && selectionReady && selectedCount > 0}
1885
+ {@render batchActions({ selectedIds: [...selectedIds], selection: batchSelectionSnapshot() })}
1886
+ {/if}
1887
+ {#if !allMatchingSelected && canSelectAllMatching && selectedCount < matchingTotal}
1888
+ <Button variant="outline" size="sm" disabled={confirmPending || confirmOpen} onclick={selectAllMatching}>
1889
+ <Rows class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" data-icon="inline-start" aria-hidden="true" />
1890
+ {i18n.t('common.selectAllMatching', { count: matchingTotal })}
1891
+ </Button>
1252
1892
  {/if}
1253
1893
  {#if canBatchDelete}
1254
1894
  <Button
@@ -1351,9 +1991,22 @@
1351
1991
  <!-- Right: Table Controls (Density, Columns, Saved Views, Refresh, Export if !showHeader) -->
1352
1992
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-58284b4ea568 svadmin-u-012fbd121f37">
1353
1993
  {#if !showHeader && canExport}
1354
- <TooltipButton tooltip={i18n.t("common.export")} variant="outline" size="sm" class="svadmin-u-e7a768f922d2 svadmin-u-0b91436debbd" onclick={exportCSV}>
1355
- <Download class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" aria-hidden="true" />
1356
- </TooltipButton>
1994
+ {#if exportTaskName}
1995
+ <ExportButton
1996
+ resource={resourceName}
1997
+ taskName={exportTaskName}
1998
+ {...definedOptions({ taskProvider: exportTaskProvider, taskIdempotencyKey: exportTaskIdempotencyKey, maxItemCount: exportMaxItemCount })}
1999
+ format={exportFormat}
2000
+ filters={activeFilters}
2001
+ sorters={sorters}
2002
+ hideText
2003
+ accessControl={{ enabled: acEnabled, hideIfUnauthorized: true }}
2004
+ />
2005
+ {:else}
2006
+ <TooltipButton tooltip={i18n.t("common.export")} variant="outline" size="sm" class="svadmin-u-e7a768f922d2 svadmin-u-0b91436debbd" onclick={exportCSV}>
2007
+ <Download class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" aria-hidden="true" />
2008
+ </TooltipButton>
2009
+ {/if}
1357
2010
  {/if}
1358
2011
 
1359
2012
  {#if showDensitySwitcher}
@@ -1401,7 +2054,7 @@
1401
2054
  <Popover.Root bind:open={savedViewsOpen}>
1402
2055
  <Popover.Trigger>
1403
2056
  {#snippet child({ props })}
1404
- <Button variant="outline" size="sm" class="svadmin-u-e7a768f922d2 svadmin-u-0b91436debbd" {...props} aria-label={i18n.t("common.savedViews")}>
2057
+ <Button variant="outline" size="sm" class="svadmin-u-e7a768f922d2 svadmin-u-0b91436debbd" {...props} disabled={!preferenceIdentityReady} aria-label={i18n.t("common.savedViews")}>
1405
2058
  <Bookmark class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" data-icon="inline-start" /> {activeSavedViewName ?? i18n.t("common.savedViews")}
1406
2059
  </Button>
1407
2060
  {/snippet}
@@ -1412,6 +2065,34 @@
1412
2065
  <h4 class="svadmin-u-2689f3958069 svadmin-u-fc7473ca09eb">{i18n.t("common.savedViews")}</h4>
1413
2066
  <p class="svadmin-u-b6b02c0ebef6 svadmin-u-359090c2d529 svadmin-u-bfa603190748">{i18n.t("common.savedViewsHint")}</p>
1414
2067
  </div>
2068
+ {#if remoteViewsLoading}
2069
+ <p role="status">{i18n.t('common.loading')}</p>
2070
+ {:else if remoteViewsFailed}
2071
+ <p role="alert">{i18n.t('common.operationFailed')}</p>
2072
+ <Button type="button" variant="outline" size="sm" onclick={() => remoteViewsReload += 1}>
2073
+ {i18n.t('common.retry')}
2074
+ </Button>
2075
+ {/if}
2076
+ {#if savedViewMutationPending}
2077
+ <p role="status">{i18n.t('common.loading')}</p>
2078
+ {:else if savedViewMutationError}
2079
+ <p role="alert">{i18n.t(savedViewMutationError === 'conflict' ? 'common.viewConflict' : 'common.operationFailed')}</p>
2080
+ <Button type="button" variant="outline" size="sm" onclick={() => remoteViewsReload += 1}>
2081
+ {i18n.t('common.refresh')}
2082
+ </Button>
2083
+ {/if}
2084
+ {#if savedViewProvider?.save}
2085
+ <Select.Root aria-label={i18n.t('common.viewSource')} value={savedViewSource} disabled={savedViewMutationPending}
2086
+ onchange={(event: Event) => {
2087
+ if (!(event.currentTarget instanceof HTMLSelectElement)) return;
2088
+ const value = event.currentTarget.value;
2089
+ if (value === 'local' || value === 'team' || value === 'system') savedViewSource = value;
2090
+ }}>
2091
+ <option value="local">{i18n.t('common.personalView')}</option>
2092
+ <option value="team">{i18n.t('common.teamView')}</option>
2093
+ <option value="system">{i18n.t('common.systemView')}</option>
2094
+ </Select.Root>
2095
+ {/if}
1415
2096
  <div class="svadmin-u-da7c36cd8867">
1416
2097
  <label class="svadmin-u-359090c2d529 svadmin-u-bfa603190748" for="saved-list-view">{i18n.t("common.currentView")}</label>
1417
2098
  <Select.Root
@@ -1421,16 +2102,17 @@
1421
2102
  onchange={(event: Event) => {
1422
2103
  if (!(event.currentTarget instanceof HTMLSelectElement)) return;
1423
2104
  const id = event.currentTarget.value;
1424
- const view = savedViews.find((candidate) => candidate.id === id);
2105
+ const view = availableSavedViews.find((candidate) => candidate.id === id);
1425
2106
  if (view) applySavedView(view);
1426
2107
  else {
2108
+ remoteDefaultEligible = false;
1427
2109
  activeSavedViewId = undefined;
1428
2110
  persistActiveSavedView();
1429
2111
  }
1430
2112
  }}
1431
2113
  >
1432
2114
  <option value="">{i18n.t("common.currentView")}</option>
1433
- {#each savedViews as view (view.id)}
2115
+ {#each availableSavedViews as view (view.id)}
1434
2116
  <option value={view.id}>{view.name}</option>
1435
2117
  {/each}
1436
2118
  </Select.Root>
@@ -1441,22 +2123,132 @@
1441
2123
  placeholder={i18n.t("common.viewName")}
1442
2124
  maxlength={60}
1443
2125
  bind:value={savedViewName}
2126
+ disabled={savedViewMutationPending}
1444
2127
  class="svadmin-u-e7a768f922d2"
1445
2128
  />
1446
- <Button size="sm" class="svadmin-u-012fbd121f37" disabled={!savedViewName.trim()} onclick={saveCurrentView}>
2129
+ <Button size="sm" class="svadmin-u-012fbd121f37" disabled={!savedViewName.trim() || savedViewMutationPending || savedViewMutationError === 'conflict'} onclick={saveCurrentView}>
1447
2130
  <Check class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" data-icon="inline-start" /> {i18n.t("common.saveView")}
1448
2131
  </Button>
1449
2132
  </div>
1450
- {#if savedViews.length > 0}
2133
+ {#if availableSavedViews.length > 0}
1451
2134
  <div class="svadmin-u-da7c36cd8867 svadmin-u-b950dda299d3 svadmin-u-18049387f0af svadmin-u-f46b61a9b310">
1452
- {#each savedViews as view (view.id)}
2135
+ {#each availableSavedViews as view (view.id)}
1453
2136
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-77a2a20e90d4 svadmin-u-fc7473ca09eb">
1454
2137
  <span class="svadmin-u-7e0b7cdf1a94 svadmin-u-f283ea9bea0e">{view.name}</span>
2138
+ {#if (view.source === 'team' || view.source === 'system') && savedViewProvider?.setDefault}
2139
+ <button
2140
+ type="button"
2141
+ class="svadmin-u-52083e7da442 svadmin-u-012fbd121f37 svadmin-u-bfa603190748"
2142
+ aria-label={`${i18n.t(view.default ? 'common.unsetDefaultView' : 'common.setDefaultView')} ${view.name}`}
2143
+ aria-pressed={view.default === true}
2144
+ title={i18n.t(view.default ? 'common.unsetDefaultView' : 'common.setDefaultView')}
2145
+ disabled={view.readOnly === true || !view.version || savedViewMutationPending || savedViewMutationError === 'conflict'}
2146
+ onclick={() => setRemoteDefault(view)}
2147
+ >
2148
+ <Star class={view.default ? "svadmin-u-e83a7042bc91" : ""} fill={view.default ? "currentColor" : "none"} />
2149
+ </button>
2150
+ {/if}
2151
+ {#if (view.source === 'team' || view.source === 'system') && savedViewProvider?.updateAccess}
2152
+ {@const draft = accessDrafts.get(view.id)}
2153
+ {@const accessMode = draft?.mode ?? view.access?.mode ?? ''}
2154
+ <Select.Root
2155
+ aria-label={`${i18n.t('common.viewAccess')} ${view.name}`}
2156
+ value={accessMode}
2157
+ disabled={view.readOnly !== false || !view.version || savedViewMutationPending || savedViewMutationError === 'conflict'}
2158
+ onchange={(event: Event) => {
2159
+ if (!(event.currentTarget instanceof HTMLSelectElement)) return;
2160
+ const mode = event.currentTarget.value;
2161
+ if (mode === 'team' || mode === 'organization' || mode === 'restricted') {
2162
+ accessDrafts.set(view.id, {
2163
+ mode,
2164
+ subjectIds: draft?.subjectIds ?? view.access?.subjectIds ?? [],
2165
+ });
2166
+ if (mode === 'restricted') loadAccessSubjects(view);
2167
+ }
2168
+ }}
2169
+ >
2170
+ <option value="" disabled>{i18n.t('common.selectOption')}</option>
2171
+ <option value="team">{i18n.t('common.teamView')}</option>
2172
+ <option value="organization">{i18n.t('common.accessOrganization')}</option>
2173
+ <option value="restricted">{i18n.t('common.accessRestricted')}</option>
2174
+ </Select.Root>
2175
+ {#if accessMode === 'restricted'}
2176
+ {#if savedViewProvider.listAccessSubjects}
2177
+ {@const selected = draft?.subjectIds ?? view.access?.subjectIds ?? []}
2178
+ <div>
2179
+ <Input
2180
+ aria-label={`${i18n.t('common.search')} ${view.name}`}
2181
+ value={accessSubjectQueries.get(view.id) ?? ''}
2182
+ disabled={view.readOnly !== false || savedViewMutationPending}
2183
+ oninput={(event: Event) => {
2184
+ if (!(event.currentTarget instanceof HTMLInputElement)) return;
2185
+ accessSubjectQueries.set(view.id, event.currentTarget.value);
2186
+ loadAccessSubjects(view, event.currentTarget.value);
2187
+ }}
2188
+ />
2189
+ <Button type="button" size="sm" variant="outline"
2190
+ aria-label={`${i18n.t('common.refresh')} ${view.name}`}
2191
+ disabled={view.readOnly !== false || savedViewMutationPending}
2192
+ onclick={() => loadAccessSubjects(view, accessSubjectQueries.get(view.id) ?? '')}>
2193
+ <RefreshCw />
2194
+ </Button>
2195
+ {#if accessSubjectsLoading.get(view.id)}
2196
+ <span role="status">{i18n.t('common.loading')}</span>
2197
+ {:else if accessSubjectsFailed.get(view.id)}
2198
+ <span role="alert">{i18n.t('common.operationFailed')}</span>
2199
+ {:else if accessSubjects.has(view.id) && !accessSubjects.get(view.id)?.length}
2200
+ <span role="status">{i18n.t('common.noData')}</span>
2201
+ {/if}
2202
+ {#each selected as id (id)}
2203
+ <label>
2204
+ <input type="checkbox" checked
2205
+ disabled={view.readOnly !== false || savedViewMutationPending || savedViewMutationError === 'conflict'}
2206
+ onchange={() => accessDrafts.set(view.id, {
2207
+ mode: 'restricted', subjectIds: selected.filter(candidate => candidate !== id),
2208
+ })} />
2209
+ {accessSubjects.get(view.id)?.find(subject => subject.id === id)?.label ?? id}
2210
+ </label>
2211
+ {/each}
2212
+ {#each (accessSubjects.get(view.id) ?? []).filter(subject => !selected.includes(subject.id)) as subject (subject.id)}
2213
+ <label>
2214
+ <input type="checkbox"
2215
+ disabled={view.readOnly !== false || savedViewMutationPending || savedViewMutationError === 'conflict' || selected.length >= 200}
2216
+ onchange={() => accessDrafts.set(view.id, {
2217
+ mode: 'restricted', subjectIds: [...selected, subject.id],
2218
+ })} />
2219
+ {subject.label}
2220
+ </label>
2221
+ {/each}
2222
+ </div>
2223
+ {:else}
2224
+ <Input
2225
+ aria-label={`${i18n.t('common.accessSubjects')} ${view.name}`}
2226
+ value={(draft?.subjectIds ?? view.access?.subjectIds ?? []).join(', ')}
2227
+ disabled={view.readOnly !== false || !view.version || savedViewMutationPending || savedViewMutationError === 'conflict'}
2228
+ oninput={(event: Event) => {
2229
+ if (event.currentTarget instanceof HTMLInputElement) {
2230
+ accessDrafts.set(view.id, { mode: 'restricted', subjectIds: event.currentTarget.value.split(',').map(id => id.trim()) });
2231
+ }
2232
+ }}
2233
+ />
2234
+ {/if}
2235
+ {/if}
2236
+ <Button type="button" size="sm"
2237
+ aria-label={`${i18n.t('common.saveAccess')} ${view.name}`}
2238
+ disabled={!draft || view.readOnly !== false || !view.version
2239
+ || savedViewMutationPending || savedViewMutationError === 'conflict'
2240
+ || (draft.mode === 'restricted' && !decodeSavedListViewAccess(draft))}
2241
+ onclick={() => saveRemoteAccess(view)}>
2242
+ {i18n.t('common.saveAccess')}
2243
+ </Button>
2244
+ {/if}
1455
2245
  <button
1456
2246
  type="button"
1457
2247
  class="svadmin-u-52083e7da442 svadmin-u-cc46d0fa277d svadmin-u-012fbd121f37 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-421ac2be5045 svadmin-u-bfa603190748 svadmin-u-8e551981c8d7 svadmin-u-ea7b2e9e070e svadmin-u-f10f771f87e9 svadmin-u-793c80e97ffb svadmin-u-9c1295a6914a"
1458
2248
  aria-label="{i18n.t("common.delete")} {view.name}"
1459
2249
  title="{i18n.t("common.delete")} {view.name}"
2250
+ disabled={view.readOnly === true || savedViewMutationPending || savedViewMutationError === 'conflict'
2251
+ || (!!view.source && !savedViewProvider?.remove)}
1460
2252
  onclick={() => deleteSavedView(view.id)}
1461
2253
  >
1462
2254
  <Trash2 class="svadmin-u-783b0d9d1e2c" />
@@ -1589,6 +2381,12 @@
1589
2381
  description={i18n.t('common.operationFailed')}
1590
2382
  retry={refreshList}
1591
2383
  />
2384
+ {:else if gridBody && pageRecords.data.length > 0}
2385
+ {@render gridBody({ records: pageRecords.data.map(copyTableRecord), columns: gridColumns, primaryKey: 'id',
2386
+ sorters: querySorters, density: currentDensity, locale: i18n.locale, label: resource.label,
2387
+ scopeKey: JSON.stringify([resourceName, queryPagination, querySorters, queryFilters, adminContext.tenantCacheKey?.__svadminTenant]),
2388
+ disabled: query.isFetching || deleteManyMutation.isPending,
2389
+ onSortChange: changeGridSort, cell: renderGridCell, controls: renderGridControls, after: renderGridAfter })}
1592
2390
  {:else}
1593
2391
  <div in:fade={{ duration: 150 }}>
1594
2392
  <!-- Desktop Table (hidden on mobile) -->
@@ -1611,14 +2409,14 @@
1611
2409
  {#if header.id === '_select'}
1612
2410
  {#if batchActions}
1613
2411
  <Checkbox
1614
- aria-label={i18n.t('common.selectAll')}
1615
- checked={table_getIsAllRowsSelected(tbl)}
2412
+ aria-label={i18n.t(allMatchingSelected ? 'common.clearSelection' : 'common.selectAllOnPage')}
2413
+ checked={allMatchingSelected || table_getIsAllRowsSelected(tbl)}
1616
2414
  onCheckedChange={toggleAllRowsSelection}
1617
2415
  />
1618
2416
  {:else if !acEnabled || deleteResourcePermission.isLoading || deleteResourcePermission.allowed}
1619
2417
  <Checkbox
1620
- aria-label={i18n.t('common.selectAll')}
1621
- checked={table_getIsAllRowsSelected(tbl)}
2418
+ aria-label={i18n.t(allMatchingSelected ? 'common.clearSelection' : 'common.selectAllOnPage')}
2419
+ checked={allMatchingSelected || table_getIsAllRowsSelected(tbl)}
1622
2420
  onCheckedChange={toggleAllRowsSelection}
1623
2421
  />
1624
2422
  {/if}
@@ -1665,6 +2463,7 @@
1665
2463
  <Checkbox
1666
2464
  aria-label={i18n.t('common.selectRow', { id })}
1667
2465
  checked={rowIsSelected(row.id)}
2466
+ disabled={confirmPending}
1668
2467
  onCheckedChange={() => toggleRowSelection(row)}
1669
2468
  />
1670
2469
  {:else}
@@ -1672,6 +2471,7 @@
1672
2471
  <Checkbox
1673
2472
  aria-label={i18n.t('common.selectRow', { id })}
1674
2473
  checked={rowIsSelected(row.id)}
2474
+ disabled={confirmPending}
1675
2475
  onCheckedChange={() => toggleRowSelection(row)}
1676
2476
  />
1677
2477
  </CanAccess>
@@ -1793,6 +2593,7 @@
1793
2593
  <Checkbox
1794
2594
  aria-label={i18n.t('common.selectRow', { id })}
1795
2595
  checked={rowIsSelected(row.id)}
2596
+ disabled={confirmPending}
1796
2597
  onCheckedChange={() => toggleRowSelection(row)}
1797
2598
  />
1798
2599
  {:else}
@@ -1800,6 +2601,7 @@
1800
2601
  <Checkbox
1801
2602
  aria-label={i18n.t('common.selectRow', { id })}
1802
2603
  checked={rowIsSelected(row.id)}
2604
+ disabled={confirmPending}
1803
2605
  onCheckedChange={() => toggleRowSelection(row)}
1804
2606
  />
1805
2607
  </CanAccess>
@@ -1921,6 +2723,7 @@
1921
2723
 
1922
2724
  {#if detailRecordId != null}
1923
2725
  <RecordDetailDrawer
2726
+ rendering={activeRendering}
1924
2727
  resourceName={resourceName}
1925
2728
  open={detailOpen}
1926
2729
  recordId={detailRecordId}
@@ -1930,6 +2733,6 @@
1930
2733
 
1931
2734
  {#if quickEditId != null && canRead && canEdit}
1932
2735
  {#key quickEditId}
1933
- <QuickEditDrawer {resourceName} recordId={quickEditId} onClose={() => quickEditId = undefined} />
2736
+ <QuickEditDrawer rendering={activeRendering} {resourceName} recordId={quickEditId} onClose={() => quickEditId = undefined} />
1934
2737
  {/key}
1935
2738
  {/if}