@svadmin/ui 0.70.0 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/dist/actions.js +1 -1
  2. package/dist/app.css +102 -63
  3. package/dist/app.theme.css +102 -63
  4. package/dist/component-registry.svelte.d.ts +33 -19
  5. package/dist/component-registry.svelte.js +6 -4
  6. package/dist/components/AdminApp.svelte +55 -53
  7. package/dist/components/AdminApp.svelte.d.ts +2 -2
  8. package/dist/components/ApiSettings.svelte +19 -1
  9. package/dist/components/AuditLogDrawer.svelte +14 -3
  10. package/dist/components/AutoForm.svelte +146 -48
  11. package/dist/components/AutoForm.svelte.d.ts +2 -0
  12. package/dist/components/AutoTable.svelte +358 -258
  13. package/dist/components/AutoTable.svelte.d.ts +1 -0
  14. package/dist/components/BoundRecordDetailDrawer.svelte +166 -0
  15. package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +9 -0
  16. package/dist/components/Breadcrumbs.svelte +7 -4
  17. package/dist/components/CancelTaskButton.svelte +9 -5
  18. package/dist/components/ColumnHeaderFilter.svelte +4 -3
  19. package/dist/components/ColumnSettings.svelte +1 -0
  20. package/dist/components/ComboboxField.svelte +90 -30
  21. package/dist/components/ComboboxField.svelte.d.ts +2 -2
  22. package/dist/components/CreatePage.svelte +3 -1
  23. package/dist/components/DetailDrawer.svelte +4 -0
  24. package/dist/components/DetailDrawer.svelte.d.ts +1 -0
  25. package/dist/components/DevTools.svelte +6 -19
  26. package/dist/components/DraggableGrid.svelte +1 -0
  27. package/dist/components/DraggableHeader.svelte +5 -9
  28. package/dist/components/DraggableHeader.svelte.d.ts +43 -18
  29. package/dist/components/DraggableRowTable.svelte +2 -2
  30. package/dist/components/DynamicFormList.svelte +17 -17
  31. package/dist/components/DynamicFormList.svelte.d.ts +2 -1
  32. package/dist/components/EditPage.svelte +3 -1
  33. package/dist/components/EditableTable.svelte +4 -0
  34. package/dist/components/ErrorPage.svelte +3 -1
  35. package/dist/components/FieldDisplay.svelte +14 -0
  36. package/dist/components/FieldDisplay.svelte.d.ts +7 -0
  37. package/dist/components/FieldRenderer.svelte +9 -5
  38. package/dist/components/FilterBuilder.svelte +19 -8
  39. package/dist/components/FilterDropdown.svelte.d.ts +1 -1
  40. package/dist/components/Header.svelte +5 -6
  41. package/dist/components/ImportWizard.svelte +3 -2
  42. package/dist/components/InfiniteList.svelte +22 -8
  43. package/dist/components/InfiniteList.svelte.d.ts +5 -3
  44. package/dist/components/InlineEdit.svelte +114 -44
  45. package/dist/components/JsonSchemaForm.svelte +19 -14
  46. package/dist/components/Layout.svelte +19 -15
  47. package/dist/components/LazyTaskQueueDrawer.svelte +26 -0
  48. package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +8 -0
  49. package/dist/components/ListPage.svelte +7 -6
  50. package/dist/components/MasterDetailView.svelte +3 -3
  51. package/dist/components/MediaLibraryModal.svelte +5 -2
  52. package/dist/components/MentionsInput.svelte +4 -3
  53. package/dist/components/NavigateToResource.svelte +3 -2
  54. package/dist/components/PermissionMatrix.svelte +4 -3
  55. package/dist/components/ProfilePage.svelte +2 -2
  56. package/dist/components/QuickEditDrawer.svelte +39 -0
  57. package/dist/components/QuickEditDrawer.svelte.d.ts +8 -0
  58. package/dist/components/RecordDetailDrawer.svelte +14 -108
  59. package/dist/components/RecordRowActions.svelte +41 -0
  60. package/dist/components/RecordRowActions.svelte.d.ts +14 -0
  61. package/dist/components/ResourceAccessGuard.svelte +7 -5
  62. package/dist/components/RetryTaskButton.svelte +9 -5
  63. package/dist/components/RowActions.svelte +5 -3
  64. package/dist/components/SensitiveDataMask.svelte +3 -3
  65. package/dist/components/SettingsPage.svelte +1 -1
  66. package/dist/components/ShowPage.svelte +44 -38
  67. package/dist/components/Sidebar.svelte +10 -8
  68. package/dist/components/SplitPaneLayout.svelte +4 -2
  69. package/dist/components/StepsForm.svelte +161 -44
  70. package/dist/components/StepsForm.svelte.d.ts +1 -0
  71. package/dist/components/TaskDetails.svelte +20 -7
  72. package/dist/components/TaskList.svelte +19 -7
  73. package/dist/components/TaskQueueDrawer.svelte +21 -22
  74. package/dist/components/Toast.svelte +4 -2
  75. package/dist/components/TooltipButton.svelte +1 -1
  76. package/dist/components/TreeSelect.svelte +4 -2
  77. package/dist/components/ValidatedField.svelte +27 -0
  78. package/dist/components/ValidatedField.svelte.d.ts +35 -0
  79. package/dist/components/account/ImportMembersPage.svelte +2 -2
  80. package/dist/components/account/SecurityLogPage.svelte +3 -1
  81. package/dist/components/account/SettingsEnterprisePage.svelte +7 -5
  82. package/dist/components/account/TeamMembersPage.svelte +1 -1
  83. package/dist/components/buttons/CloneButton.svelte +6 -4
  84. package/dist/components/buttons/DeleteButton.svelte +95 -29
  85. package/dist/components/buttons/DeleteButton.svelte.d.ts +2 -0
  86. package/dist/components/buttons/EditButton.svelte +6 -4
  87. package/dist/components/buttons/ExportButton.svelte +22 -4
  88. package/dist/components/buttons/ImportButton.svelte +83 -38
  89. package/dist/components/buttons/ImportButton.svelte.d.ts +6 -10
  90. package/dist/components/buttons/RefreshButton.svelte +48 -8
  91. package/dist/components/buttons/ShowButton.svelte +6 -4
  92. package/dist/components/content/ApiKeyList.svelte +3 -1
  93. package/dist/components/content/ContentPageShell.svelte +3 -1
  94. package/dist/components/content/FileList.svelte +3 -1
  95. package/dist/components/content/MemberList.svelte +3 -1
  96. package/dist/components/content/NetworkTable.svelte +3 -1
  97. package/dist/components/content/SecurityEventTable.svelte +3 -1
  98. package/dist/components/content/StatusTabs.svelte +2 -2
  99. package/dist/components/fieldComponentMap.d.ts +18 -13
  100. package/dist/components/fieldComponentMap.js +95 -47
  101. package/dist/components/fields/AvatarField.svelte +1 -1
  102. package/dist/components/fields/ChoiceField.svelte +16 -0
  103. package/dist/components/fields/ChoiceField.svelte.d.ts +10 -0
  104. package/dist/components/fields/CodeField.svelte +11 -12
  105. package/dist/components/fields/CodeField.svelte.d.ts +1 -1
  106. package/dist/components/fields/DateRangeField.svelte +2 -4
  107. package/dist/components/fields/ImageField.svelte +3 -1
  108. package/dist/components/fields/ImagesField.svelte +23 -0
  109. package/dist/components/fields/ImagesField.svelte.d.ts +6 -0
  110. package/dist/components/fields/JsonField.svelte +19 -13
  111. package/dist/components/fields/PasswordField.svelte +5 -0
  112. package/dist/components/fields/PasswordField.svelte.d.ts +6 -0
  113. package/dist/components/fields/json-value.d.ts +11 -0
  114. package/dist/components/fields/json-value.js +28 -0
  115. package/dist/components/profile/ActivityTimeline.svelte +2 -2
  116. package/dist/components/record-detail.svelte.d.ts +16 -0
  117. package/dist/components/record-detail.svelte.js +28 -0
  118. package/dist/components/saved-list-views.js +29 -29
  119. package/dist/components/table-contract.d.ts +13 -0
  120. package/dist/components/table-contract.js +42 -0
  121. package/dist/components/task-utils.js +4 -2
  122. package/dist/components/ui/alert/alert-variants.d.ts +2 -3
  123. package/dist/components/ui/alert/alert-variants.js +1 -2
  124. package/dist/components/ui/alert/alert.svelte +4 -5
  125. package/dist/components/ui/alert/alert.svelte.d.ts +3 -8
  126. package/dist/components/ui/alert/index.js +0 -2
  127. package/dist/components/ui/avatar/avatar-variants.d.ts +2 -3
  128. package/dist/components/ui/avatar/avatar-variants.js +1 -2
  129. package/dist/components/ui/avatar/avatar.svelte +5 -6
  130. package/dist/components/ui/avatar/avatar.svelte.d.ts +3 -8
  131. package/dist/components/ui/badge/badge-variants.d.ts +2 -3
  132. package/dist/components/ui/badge/badge-variants.js +1 -2
  133. package/dist/components/ui/badge/badge.svelte +4 -5
  134. package/dist/components/ui/badge/badge.svelte.d.ts +3 -8
  135. package/dist/components/ui/badge/index.js +0 -2
  136. package/dist/components/ui/button/button-variants.d.ts +2 -3
  137. package/dist/components/ui/button/button-variants.js +1 -2
  138. package/dist/components/ui/button/button.svelte +100 -44
  139. package/dist/components/ui/button/button.svelte.d.ts +8 -12
  140. package/dist/components/ui/button/index.js +0 -2
  141. package/dist/components/ui/command/command-dialog.svelte +3 -1
  142. package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
  143. package/dist/components/ui/command/command-group.svelte +4 -2
  144. package/dist/components/ui/command/command-item.svelte +4 -2
  145. package/dist/components/ui/command/command-separator.svelte +3 -1
  146. package/dist/components/ui/command/index.d.ts +5 -5
  147. package/dist/components/ui/input/input.svelte +40 -20
  148. package/dist/components/ui/input/input.svelte.d.ts +5 -4
  149. package/dist/components/ui/sheet/sheet.svelte +9 -3
  150. package/dist/components/ui/sheet/sheet.svelte.d.ts +1 -0
  151. package/dist/components/ui/tooltip/tooltip.svelte +4 -2
  152. package/dist/index.d.ts +1 -1
  153. package/dist/inline-value.d.ts +5 -0
  154. package/dist/inline-value.js +24 -0
  155. package/dist/numeric-input.d.ts +2 -0
  156. package/dist/numeric-input.js +9 -0
  157. package/dist/types.d.ts +3 -3
  158. package/package.json +11 -5
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
- import { onDestroy, untrack } from 'svelte';
2
+ import { definedOptions } from '@svadmin/core/options';
3
+
4
+ import { onDestroy, tick, untrack } from 'svelte';
3
5
  import { SvelteMap } from 'svelte/reactivity';
4
6
  import { cn } from '../utils.js';
5
7
  import {
@@ -28,13 +30,16 @@
28
30
  table_getRowModel,
29
31
  table_getAllLeafColumns,
30
32
  table_getIsAllRowsSelected,
33
+ table_resetRowSelection,
31
34
  table_toggleAllRowsSelected,
32
35
  row_toggleSelected,
33
36
  row_getVisibleCells,
34
37
  cell_getValue,
35
38
  } from '@tanstack/table-core/static-functions';
36
39
 
37
- import { captureAdminContext, DeleteManyPartialError, getAdminOptions, useList, useDelete, useDeleteMany, getResource, useNavigation, useParsed } from '@svadmin/core';
40
+ import { captureAdminContext, DeleteManyPartialError, getAdminOptions, getContractFormFields, useNavigation, useParsed, useResourceContract, useList, useDeleteMany, downloadData } from '@svadmin/core';
41
+ import { decodeBaseRecord, snapshotPlainData, parseContractRouteId, formatContractRouteId } from '@svadmin/core/schema';
42
+ import { checkedTableRows, copyTableRecord, tableExportRows, tableRowKey, type TableRecord } from './table-contract';
38
43
  import type {
39
44
  BaseRecord,
40
45
  FieldDefinition,
@@ -84,9 +89,12 @@
84
89
  } from '@lucide/svelte';
85
90
  import ConfirmDialog from './ConfirmDialog.svelte';
86
91
  import RecordDetailDrawer from './RecordDetailDrawer.svelte';
92
+ import QuickEditDrawer from './QuickEditDrawer.svelte';
93
+ import RecordRowActions from './RecordRowActions.svelte';
87
94
  import CanAccess from './CanAccess.svelte';
88
95
  import TooltipButton from './TooltipButton.svelte';
89
96
  import InlineEdit from './InlineEdit.svelte';
97
+ import FieldDisplay from './FieldDisplay.svelte';
90
98
  import DraggableHeader from './DraggableHeader.svelte';
91
99
  import DataState from './content/DataState.svelte';
92
100
  import type { Snippet } from 'svelte';
@@ -123,6 +131,7 @@
123
131
  batchActions?: Snippet<[{ selectedIds: (string | number)[] }]>;
124
132
  /** Summary row rendered at table footer */
125
133
  summary?: Snippet<[{ data: BaseRecord[]; total: number; visibleColumnsCount: number }]>;
134
+ deleteVariables?: unknown;
126
135
  }
127
136
 
128
137
  let {
@@ -144,25 +153,27 @@
144
153
  summary,
145
154
  pagination: externalPagination,
146
155
  sorters: externalSorters,
156
+ deleteVariables,
147
157
  }: Props = $props();
148
158
 
149
159
  let densityOverride = $state<'compact' | 'comfortable' | undefined>(undefined);
150
160
  const currentDensity = $derived(densityOverride ?? density);
151
161
  const adminContext = captureAdminContext();
152
- const adminOptions = getAdminOptions();
153
- const mutationMode = $derived(adminOptions.mutationMode ?? 'pessimistic');
154
162
  const parsed = useParsed();
155
163
  const navigation = useNavigation();
156
164
 
157
- const resource = $derived(getResource(resourceName));
165
+ const binding = useResourceContract(() => resourceName);
166
+ const resource = $derived(adminContext.getResource(resourceName));
158
167
  const primaryKey = $derived(resource.primaryKey ?? 'id');
168
+ const listPermission = useCan(() => ({ resource: resourceName, action: 'list' }));
169
+ const canRead = $derived(listPermission.allowed);
159
170
  const listPreferenceScope = $derived.by(() => {
160
171
  const matcher = adminContext.queryKeyMatcher(resourceName);
161
- return {
172
+ return definedOptions({
162
173
  resourceName,
163
174
  providerName: matcher.provider ?? 'default',
164
175
  tenantIdentity: matcher.tenant,
165
- };
176
+ });
166
177
  });
167
178
 
168
179
  function readLocalPreference(key: string): string | null {
@@ -189,10 +200,10 @@
189
200
  // ─── URL state + server-side state ────────────────────────────
190
201
  const urlState = readURLState(adminContext);
191
202
 
192
- const savedViewColumnIds = new Set([
193
- ...untrack(() => resource.fields.map((field) => field.key)),
203
+ const savedViewColumnIds = $derived(new Set([
204
+ ...resource.fields.map((field) => field.key),
194
205
  '_select', '_expand', '_actions',
195
- ]);
206
+ ]));
196
207
  const storedSavedViews = untrack(() => readSavedListViews(
197
208
  readScopedPreference(
198
209
  listPreferenceScope,
@@ -244,7 +255,7 @@
244
255
  const parsed: unknown = JSON.parse(stored);
245
256
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
246
257
  const visibility: ColumnVisibilityState = {};
247
- for (const [columnId, visible] of Object.entries(parsed as Record<string, unknown>)) {
258
+ for (const [columnId, visible] of Object.entries(decodeBaseRecord(parsed))) {
248
259
  if (savedViewColumnIds.has(columnId) && typeof visible === 'boolean') {
249
260
  visibility[columnId] = visible;
250
261
  }
@@ -271,7 +282,7 @@
271
282
  try {
272
283
  const parsed: unknown = JSON.parse(stored);
273
284
  return Array.isArray(parsed)
274
- ? [...new Set(parsed.filter((columnId): columnId is string => (
285
+ ? [...new Set(parsed.filter((columnId: unknown): columnId is string => (
275
286
  typeof columnId === 'string' && savedViewColumnIds.has(columnId)
276
287
  )))]
277
288
  : [];
@@ -301,19 +312,19 @@
301
312
  legacyActiveSavedListViewStorageKey(resourceName),
302
313
  );
303
314
  const activeSavedView = scopedSavedViews.find((view) => view.id === candidate);
304
- return {
315
+ return definedOptions({
305
316
  savedViews: scopedSavedViews,
306
317
  activeSavedViewId: activeSavedView?.id,
307
318
  activeSavedView,
308
319
  columnVisibility: readColumnVisibilityPreference(scope),
309
320
  columnOrder: readColumnOrderPreference(scope),
310
- };
321
+ });
311
322
  }
312
323
 
313
324
  // Snapshot resource values for initial state (untrack to avoid reactive tracking)
314
325
  const storedPageSize = parseInt(readLocalPreference('svadmin-default-page-size') ?? '', 10);
315
- const initPageSize = untrack(() => resource.pageSize ?? (isNaN(storedPageSize) ? 10 : storedPageSize));
316
- const initDefaultSort = untrack(() => resource.defaultSort);
326
+ const initPageSize = $derived(resource.pageSize ?? (Number.isSafeInteger(storedPageSize) && storedPageSize > 0 ? storedPageSize : 10));
327
+ const initDefaultSort = $derived(resource.defaultSort);
317
328
 
318
329
  let pagination = $state<PaginationState>(untrack(() => externalPagination ?? {
319
330
  current: urlState.page ?? initialViewState?.pagination.current ?? 1,
@@ -326,10 +337,10 @@
326
337
  : initialViewState?.sorters ?? (initDefaultSort ? [initDefaultSort] : [])))
327
338
  );
328
339
  const initialURLFilters = untrack(() => urlState.filters ?? initialViewState?.filters ?? []);
329
- const editableFilterKeys = new Set(untrack(() => resource.fields.filter((field) => field.filterable).map((field) => field.key)));
340
+ const editableFilterKeys = $derived(new Set(resource.fields.filter((field) => field.filterable).map((field) => field.key)));
330
341
  const editableFilterCounts: Record<string, number> = {};
331
342
  for (const filter of initialURLFilters) {
332
- if ('field' in filter && filter.operator === 'contains' && typeof filter.value === 'string' && editableFilterKeys.has(filter.field)) {
343
+ if ('field' in filter && filter.operator === 'contains' && typeof filter.value === 'string' && untrack(() => editableFilterKeys.has(filter.field))) {
333
344
  editableFilterCounts[filter.field] = (editableFilterCounts[filter.field] ?? 0) + 1;
334
345
  }
335
346
  }
@@ -350,7 +361,8 @@
350
361
  let searchDebounceTimer: ReturnType<typeof setTimeout> | undefined;
351
362
 
352
363
  function scheduleSearch(event: Event) {
353
- searchText = (event.currentTarget as HTMLInputElement).value;
364
+ if (!(event.currentTarget instanceof HTMLInputElement)) return;
365
+ searchText = event.currentTarget.value;
354
366
  if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
355
367
  searchDebounceTimer = setTimeout(() => {
356
368
  appliedSearchText = searchText;
@@ -395,8 +407,9 @@
395
407
  const activeFilters = $derived.by(() => {
396
408
  const result: Filter[] = [...locationFilters];
397
409
  if (appliedSearchText.trim() && searchableFields.length > 0) {
398
- if (searchableFields.length === 1) {
399
- result.push({ field: searchableFields[0].key, operator: 'contains', value: appliedSearchText });
410
+ const firstSearchableField = searchableFields[0];
411
+ if (searchableFields.length === 1 && firstSearchableField) {
412
+ result.push({ field: firstSearchableField.key, operator: 'contains', value: appliedSearchText });
400
413
  } else {
401
414
  const searchFilter: LogicalFilter = {
402
415
  operator: 'or',
@@ -426,6 +439,10 @@
426
439
 
427
440
  function clearFilters(): void {
428
441
  markSavedViewDirty();
442
+ if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
443
+ searchDebounceTimer = undefined;
444
+ searchText = '';
445
+ appliedSearchText = '';
429
446
  filters = [];
430
447
  filterValues = {};
431
448
  pagination = { ...pagination, current: 1 };
@@ -447,6 +464,12 @@
447
464
  pagination = { ...pagination, current: 1 };
448
465
  }
449
466
 
467
+ function setFilterFromEvent(field: string, event: Event): void {
468
+ if (event.currentTarget instanceof HTMLInputElement || event.currentTarget instanceof HTMLSelectElement) {
469
+ setFilterValue(field, event.currentTarget.value);
470
+ }
471
+ }
472
+
450
473
  function removeActiveFilter(index: number): void {
451
474
  markSavedViewDirty();
452
475
  const filter = locationFilters[index];
@@ -460,21 +483,16 @@
460
483
  && candidate.operator === 'contains'
461
484
  && typeof candidate.value === 'string'
462
485
  ));
463
- filterValues[filter.field] = remaining.length === 1 && 'field' in remaining[0]
464
- ? String(remaining[0].value)
486
+ const remainingFilter = remaining[0];
487
+ filterValues[filter.field] = remaining.length === 1 && remainingFilter && 'field' in remainingFilter
488
+ ? String(remainingFilter.value)
465
489
  : '';
466
490
  }
467
491
  pagination = { ...pagination, current: 1 };
468
492
  }
469
493
 
470
494
  function clonePlainValue(value: unknown): unknown {
471
- if (Array.isArray(value)) return value.map(clonePlainValue);
472
- if (value && typeof value === 'object') {
473
- return Object.fromEntries(
474
- Object.entries(value as Record<string, unknown>).map(([key, entry]) => [key, clonePlainValue(entry)])
475
- );
476
- }
477
- return value;
495
+ return value === undefined ? undefined : snapshotPlainData(value);
478
496
  }
479
497
 
480
498
  function cloneFilter(filter: Filter): Filter {
@@ -491,11 +509,11 @@
491
509
  };
492
510
  }
493
511
 
494
- const queryPagination = $derived<PaginationState>({
512
+ const queryPagination = $derived<PaginationState>(definedOptions({
495
513
  current: pagination.current,
496
514
  pageSize: pagination.pageSize,
497
515
  mode: pagination.mode,
498
- });
516
+ }));
499
517
  const querySorters = $derived<Sort[]>(sorters.map(sorter => ({
500
518
  field: sorter.field,
501
519
  order: sorter.order,
@@ -504,38 +522,48 @@
504
522
 
505
523
  // ─── Data fetching ────────────────────────────────────────────
506
524
  const listResult = useList({
507
- get resource() { return resourceName; },
525
+ get resource() { return binding.resource; },
526
+ get dataProviderName() { return binding.dataProviderName; },
527
+ get queryOptions() { return { enabled: canRead }; },
508
528
  get pagination() { return queryPagination; },
509
529
  get sorters() { return querySorters; },
510
530
  get filters() { return queryFilters; },
511
531
  });
512
532
  const query = listResult;
513
- const deleteResult = useDelete({ get resource() { return resourceName; } });
514
- const deleteMutation = deleteResult.mutation;
515
- let batchOperationCount = $state(0);
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
+ });
537
+ let deleteRequest = $state<{ ids: (string | number)[]; batch: boolean } | null>(null);
538
+ const deletePermission = useCan(() => definedOptions({
539
+ resource: resourceName, action: 'delete',
540
+ id: deleteRequest?.batch === false ? deleteRequest.ids[0] : undefined,
541
+ params: deleteRequest?.batch ? { ids: deleteRequest.ids } : undefined,
542
+ queryOptions: { enabled: deleteRequest !== null },
543
+ }));
544
+ const deleteAllowed = $derived(canRead && resource.canDelete !== false && deleteRequest !== null && deletePermission.allowed);
516
545
  const deleteManyResult = useDeleteMany({
517
- get resource() { return resourceName; },
518
- successNotification: () => ({
519
- message: i18n.t('common.batchDeleteSuccess', { count: batchOperationCount }),
520
- }),
521
- errorNotification: (error) => ({
522
- type: 'error',
523
- message: error instanceof DeleteManyPartialError
524
- ? i18n.t('common.batchDeletePartialFail', { failed: error.failedIds.length, total: batchOperationCount })
525
- : i18n.t('common.batchDeleteFailed', { count: batchOperationCount }),
526
- }),
546
+ get resource() { return binding.resource; },
547
+ get enabled() { return deleteAllowed; },
527
548
  });
528
549
  const deleteManyMutation = deleteManyResult.mutation;
529
550
 
530
551
  // ─── Permissions ──────────────────────────────────────────────
531
552
  const acEnabled = $derived(!!adminContext.accessControlProvider);
553
+ const deleteResourcePermission = useCan(() => ({
554
+ resource: resourceName, action: 'delete', queryOptions: { enabled: acEnabled },
555
+ }));
532
556
  const canCreatePerm = useCan(() => ({ resource: resourceName, action: 'create', queryOptions: { enabled: acEnabled } }));
533
557
  const canExportPerm = useCan(() => ({ resource: resourceName, action: 'export', queryOptions: { enabled: acEnabled } }));
534
558
  const canCreate = $derived(resource.canCreate !== false && (!acEnabled || canCreatePerm.allowed));
535
559
  const canEdit = $derived(resource.canEdit !== false);
560
+ const writableFields = $derived.by(() => {
561
+ try { return new Set(getContractFormFields(binding.resource, 'edit')); }
562
+ catch { return new Set<string>(); }
563
+ });
536
564
  const canShow = $derived(resource.canShow !== false);
537
565
  const canDelete = $derived(resource.canDelete !== false);
538
- const canExport = $derived(canExportPerm.allowed);
566
+ const canExport = $derived(!acEnabled || canExportPerm.allowed);
539
567
 
540
568
  // ─── TanStack Table state ────────────────────────────────────
541
569
  const initialSorting = untrack<SortingState>(() =>
@@ -546,8 +574,8 @@
546
574
  ));
547
575
  const sortingAtom = createAtom(initialSorting);
548
576
  const columnVisibilityAtom = createAtom(initialColumnVisibility);
549
- const rowSelectionAtom = createAtom({} as RowSelectionState);
550
- const expandedAtom = createAtom({} as ExpandedState);
577
+ let rowSelection = $state<RowSelectionState>({});
578
+ const expandedAtom = createAtom<ExpandedState>({});
551
579
  const initialColumnOrder = untrack(() => (
552
580
  initialViewState?.columnOrder?.length
553
581
  ? initialViewState.columnOrder
@@ -565,7 +593,6 @@
565
593
 
566
594
  const tableSorting = useSelector(sortingAtom);
567
595
  const tableColumnVisibility = useSelector(columnVisibilityAtom);
568
- const tableRowSelection = useSelector(rowSelectionAtom);
569
596
  const tableExpanded = useSelector(expandedAtom);
570
597
  const tableColumnOrder = useSelector(columnOrderAtom);
571
598
  const selectedIdValueByKey = new SvelteMap<string, string | number>();
@@ -580,15 +607,15 @@
580
607
  }
581
608
 
582
609
  function rowIsSelected(rowId: string): boolean {
583
- return tableRowSelection.current[rowId] === true;
610
+ return rowSelection[rowId] === true;
584
611
  }
585
612
 
586
- function rowIdValue(row: Row<TableFeatures, BaseRecord>): string | number {
587
- const value = row.original[primaryKey];
588
- return typeof value === 'string' || typeof value === 'number' ? value : row.id;
613
+ function rowIdValue(row: Row<TableFeatures, TableRecord>): string | number {
614
+ return row.original.id;
589
615
  }
590
616
 
591
- function toggleRowSelection(row: Row<TableFeatures, BaseRecord>): void {
617
+ function toggleRowSelection(row: Row<TableFeatures, TableRecord>): void {
618
+ if (confirmPending || !canRead) return;
592
619
  const wasSelected = rowIsSelected(row.id);
593
620
  if (!wasSelected) {
594
621
  selectedIdValueByKey.set(row.id, rowIdValue(row));
@@ -696,7 +723,8 @@
696
723
  columnVisibilityAtom.set(state.columnVisibility);
697
724
  columnOrderAtom.set(state.columnOrder);
698
725
  persistColumnOrder(state.columnOrder);
699
- rowSelectionAtom.set({});
726
+ rowSelection = {};
727
+ selectedIdValueByKey.clear();
700
728
  }
701
729
 
702
730
  function resetToDefaultListState(): void {
@@ -733,7 +761,8 @@
733
761
  if (!hasExplicitURLState) resetToDefaultListState();
734
762
  columnVisibilityAtom.set(preferences.columnVisibility);
735
763
  columnOrderAtom.set(preferences.columnOrder);
736
- rowSelectionAtom.set({});
764
+ rowSelection = {};
765
+ selectedIdValueByKey.clear();
737
766
  }
738
767
 
739
768
  loadedPreferenceScopeId = scopeId;
@@ -785,7 +814,7 @@
785
814
  columnOrderAtom.set(ids);
786
815
  }
787
816
 
788
- function toggleColumnSort(column: Column<TableFeatures, BaseRecord, unknown>): void {
817
+ function toggleColumnSort(column: Column<TableFeatures, TableRecord, unknown>): void {
789
818
  markSavedViewDirty();
790
819
  column_toggleSorting(column);
791
820
  }
@@ -808,7 +837,7 @@
808
837
  resource.fields.filter(f => f.showInList !== false)
809
838
  );
810
839
 
811
- const columns = $derived<ColumnDef<TableFeatures, BaseRecord, unknown>[]>([
840
+ const columns = $derived<ColumnDef<TableFeatures, TableRecord, unknown>[]>([
812
841
  // Selection column
813
842
  ...(selectable && (canDelete || batchActions) ? [{
814
843
  id: '_select',
@@ -816,7 +845,7 @@
816
845
  cell: () => '',
817
846
  size: 40,
818
847
  enableSorting: false,
819
- } satisfies ColumnDef<TableFeatures, BaseRecord, unknown>] : []),
848
+ } satisfies ColumnDef<TableFeatures, TableRecord, unknown>] : []),
820
849
  // Expand column
821
850
  ...(expandedRowRender ? [{
822
851
  id: '_expand',
@@ -824,14 +853,14 @@
824
853
  cell: () => '',
825
854
  size: 40,
826
855
  enableSorting: false,
827
- } satisfies ColumnDef<TableFeatures, BaseRecord, unknown>] : []),
856
+ } satisfies ColumnDef<TableFeatures, TableRecord, unknown>] : []),
828
857
  // Data columns
829
- ...visibleFields.map((field): ColumnDef<TableFeatures, BaseRecord, unknown> => ({
858
+ ...visibleFields.map((field): ColumnDef<TableFeatures, TableRecord, unknown> => (definedOptions({
830
859
  id: field.key,
831
860
  accessorKey: field.key,
832
861
  header: () => field.label,
833
862
  size: field.width ? parseInt(String(field.width)) : undefined,
834
- })),
863
+ }))),
835
864
  // Actions column
836
865
  {
837
866
  id: '_actions',
@@ -856,22 +885,25 @@
856
885
  });
857
886
 
858
887
  // ─── Create TanStack Table ────────────────────────────────────
859
- const tbl = createTable<TableFeatures, BaseRecord>(
888
+ const tbl = createTable<TableFeatures, TableRecord>(
860
889
  {
861
890
  features,
862
- get data() { return query.data?.data ?? []; },
891
+ get data() { return pageRecords.data; },
863
892
  get columns() { return orderedColumns; },
864
893
  manualSorting: true,
865
- getRowId: (row: BaseRecord) => String(row[primaryKey]),
894
+ getRowId: (row: TableRecord) => tableRowKey(row.id),
866
895
  state: {
867
896
  get sorting() { return tableSorting.current; },
868
897
  get columnVisibility() { return tableColumnVisibility.current; },
898
+ get rowSelection() { return rowSelection; },
869
899
  },
870
900
  onSortingChange: (updater) => {
871
901
  sortingAtom.set(typeof updater === 'function' ? updater(tableSorting.current) : updater);
872
902
  },
903
+ onRowSelectionChange: (updater) => {
904
+ rowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
905
+ },
873
906
  atoms: {
874
- rowSelection: rowSelectionAtom,
875
907
  expanded: expandedAtom,
876
908
  },
877
909
  autoResetExpanded: false,
@@ -882,6 +914,7 @@
882
914
  );
883
915
 
884
916
  function toggleAllRowsSelection(): void {
917
+ if (confirmPending || !canRead) return;
885
918
  const willSelect = !table_getIsAllRowsSelected(tbl);
886
919
  for (const row of tableView.rows) {
887
920
  if (willSelect) {
@@ -894,9 +927,13 @@
894
927
  }
895
928
 
896
929
  const selectedIds = $derived(
897
- Object.keys(tableRowSelection.current).map((key) => selectedIdValueByKey.get(key) ?? key)
930
+ Object.keys(rowSelection).filter(key => rowSelection[key] === true)
931
+ .flatMap(key => {
932
+ const id = selectedIdValueByKey.get(key);
933
+ return id === undefined ? [] : [id];
934
+ })
898
935
  );
899
- const selectedCount = $derived(selectedIds.length);
936
+ const selectedCount = $derived(Object.keys(rowSelection).filter(key => rowSelection[key] === true).length);
900
937
  const batchDeletePerm = useCan(() => ({
901
938
  resource: resourceName,
902
939
  action: 'delete',
@@ -904,13 +941,13 @@
904
941
  queryOptions: { enabled: acEnabled && canDelete && selectedIds.length > 0 },
905
942
  }));
906
943
  const canBatchDelete = $derived(
907
- canDelete && (!acEnabled || (!batchDeletePerm.isLoading && batchDeletePerm.allowed))
944
+ canDelete && (!acEnabled || batchDeletePerm.isLoading || batchDeletePerm.allowed)
908
945
  );
909
946
 
910
947
  function clearSelection(): void {
911
948
  if (!deleteManyMutation.isPending) {
912
949
  selectedIdValueByKey.clear();
913
- rowSelectionAtom.set({});
950
+ table_resetRowSelection(tbl, true);
914
951
  }
915
952
  }
916
953
 
@@ -922,7 +959,7 @@
922
959
  void tableSorting.current;
923
960
  void tableColumnVisibility.current;
924
961
  void orderedColumns;
925
- void tableRowSelection.current;
962
+ void rowSelection;
926
963
  void tableExpanded.current;
927
964
  void tableColumnOrder.current;
928
965
  const headerGroups = table_getHeaderGroups(tbl);
@@ -951,7 +988,8 @@
951
988
  expandedAtom.set({ ...current, [rowId]: true });
952
989
  }
953
990
 
954
- const totalPages = $derived(Math.ceil((query.data?.total ?? 0) / (pagination.pageSize ?? 10)));
991
+ const totalPages = $derived(canRead && pageRecords.ok && !query.isError
992
+ ? Math.ceil((query.data?.total ?? 0) / (pagination.pageSize ?? 10)) : 0);
955
993
 
956
994
  // ─── Pagination helpers ───────────────────────────────────────
957
995
  const currentPage = $derived(pagination.current ?? 1);
@@ -974,68 +1012,137 @@
974
1012
  // ─── Confirm dialog ───────────────────────────────────────────
975
1013
  let confirmOpen = $state(false);
976
1014
  let confirmMessage = $state('');
977
- let confirmAction = $state<() => void>(() => {});
978
1015
  let confirmPending = $state(false);
1016
+ let operationError = $state<string | null>(null);
1017
+ let activeDelete: object | undefined;
979
1018
  let detailOpenedInHistory = $state(false);
980
- const detailRecordId = $derived(parsed.params.detail);
1019
+ let quickEditId = $state<string | number | undefined>();
1020
+ const tableScope = $derived({
1021
+ contract: binding.resource, resourceName, provider: adminContext.providers?.[binding.dataProviderName],
1022
+ meta: JSON.stringify(binding.meta), tenant: adminContext.tenantCacheKey?.__svadminTenant,
1023
+ auth: adminContext.authProvider, router: adminContext.routerProvider,
1024
+ permissionProvider: adminContext.accessControlProvider, canRead, canDelete,
1025
+ variables: deleteVariables,
1026
+ });
1027
+ let previousTableScope: typeof tableScope | undefined;
1028
+ const detailState = $derived.by(() => {
1029
+ const route = parsed.params['detail'];
1030
+ if (route === undefined || !canRead) return { id: undefined, invalid: false };
1031
+ try { return { id: parseContractRouteId(binding.resource, route), invalid: false }; }
1032
+ catch { return { id: undefined, invalid: true }; }
1033
+ });
1034
+ const detailRecordId = $derived(detailState.id);
981
1035
  const detailOpen = $derived(detailRecordId != null);
1036
+ $effect.pre(() => {
1037
+ const scope = tableScope;
1038
+ if (previousTableScope && previousTableScope !== scope) {
1039
+ activeDelete = undefined;
1040
+ confirmOpen = false;
1041
+ confirmPending = false;
1042
+ deleteRequest = null;
1043
+ operationError = null;
1044
+ selectedIdValueByKey.clear();
1045
+ rowSelection = {};
1046
+ expandedAtom.set({});
1047
+ detailOpenedInHistory = false;
1048
+ quickEditId = undefined;
1049
+ if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
1050
+ searchDebounceTimer = undefined;
1051
+ if (previousTableScope.resourceName !== scope.resourceName) resetToDefaultListState();
1052
+ }
1053
+ previousTableScope = scope;
1054
+ });
1055
+ $effect(() => {
1056
+ if (deleteRequest && !deletePermission.isLoading && !deletePermission.allowed) {
1057
+ confirmOpen = false;
1058
+ deleteRequest = null;
1059
+ operationError = i18n.t('common.operationFailed');
1060
+ }
1061
+ });
1062
+ onDestroy(() => { activeDelete = undefined; });
982
1063
 
983
1064
  function confirmDelete(id: string | number) {
1065
+ if (!canRead || !pageRecords.ok || query.isError || !canDelete || confirmPending) return;
984
1066
  confirmMessage = i18n.t('common.deleteConfirm');
985
- confirmAction = async () => {
986
- confirmPending = true;
987
- if (mutationMode !== 'pessimistic') confirmOpen = false;
988
- try {
989
- await deleteMutation.mutateAsync({
990
- id,
991
- resource: resourceName,
992
- onCancel: () => { confirmOpen = false; },
993
- });
994
- confirmOpen = false;
995
- } catch {
996
- // useDelete owns error feedback and restores optimistic cache state.
997
- } finally {
998
- confirmPending = false;
999
- }
1000
- };
1067
+ deleteRequest = { ids: [id], batch: false };
1068
+ operationError = null;
1001
1069
  confirmOpen = true;
1002
1070
  }
1003
1071
 
1004
1072
  function confirmBatchDelete() {
1005
1073
  const ids = [...selectedIds];
1006
- if (ids.length === 0 || deleteManyMutation.isPending) return;
1007
- batchOperationCount = ids.length;
1074
+ if (!canRead || !pageRecords.ok || query.isError || !canBatchDelete || ids.length === 0 || confirmPending) return;
1008
1075
  confirmMessage = i18n.t('common.batchDeleteConfirm', { count: ids.length });
1009
- confirmAction = async () => {
1010
- confirmPending = true;
1011
- if (mutationMode !== 'pessimistic') confirmOpen = false;
1012
- try {
1013
- await deleteManyMutation.mutateAsync({
1014
- ids,
1015
- resource: resourceName,
1016
- onCancel: () => { confirmOpen = false; },
1017
- });
1076
+ deleteRequest = { ids, batch: true };
1077
+ operationError = null;
1078
+ confirmOpen = true;
1079
+ }
1080
+
1081
+ async function confirmAction() {
1082
+ if (!confirmOpen || !pageRecords.ok || query.isError || !deleteAllowed || confirmPending || !deleteRequest) return;
1083
+ const request = deleteRequest;
1084
+ if (request.batch && JSON.stringify(request.ids) !== JSON.stringify(selectedIds)) {
1085
+ confirmOpen = false;
1086
+ deleteRequest = null;
1087
+ return;
1088
+ }
1089
+ const scope = tableScope;
1090
+ const token = {};
1091
+ activeDelete = token;
1092
+ confirmPending = true;
1093
+ confirmOpen = false;
1094
+ const clearBatchSelectionBeforeMutation = request.batch && getAdminOptions().mutationMode === 'undoable';
1095
+ const selectionBeforeDelete = clearBatchSelectionBeforeMutation ? new Map(selectedIdValueByKey) : undefined;
1096
+ if (clearBatchSelectionBeforeMutation) {
1097
+ selectedIdValueByKey.clear();
1098
+ table_resetRowSelection(tbl, true);
1099
+ }
1100
+ const current = () => activeDelete === token &&
1101
+ tableScope.contract === scope.contract &&
1102
+ tableScope.resourceName === scope.resourceName &&
1103
+ tableScope.provider === scope.provider &&
1104
+ tableScope.meta === scope.meta &&
1105
+ tableScope.tenant === scope.tenant &&
1106
+ tableScope.auth === scope.auth &&
1107
+ tableScope.router === scope.router &&
1108
+ tableScope.permissionProvider === scope.permissionProvider;
1109
+ try {
1110
+ await deleteManyMutation.mutateAsync({
1111
+ ids: [...request.ids],
1112
+ ...definedOptions({ variables: deleteVariables, dataProviderName: binding.dataProviderName }),
1113
+ });
1114
+ if (!current()) return;
1115
+ selectedIdValueByKey.clear();
1116
+ table_resetRowSelection(tbl, true);
1117
+ await tick();
1118
+ confirmOpen = false;
1119
+ } catch (error) {
1120
+ if (!current()) return;
1121
+ if (error instanceof DeleteManyPartialError) {
1122
+ rowSelection = Object.fromEntries(error.failedIds.map(id => [tableRowKey(id), true as const]));
1018
1123
  selectedIdValueByKey.clear();
1019
- rowSelectionAtom.set({});
1020
- confirmOpen = false;
1021
- } catch (error) {
1022
- if (error instanceof DeleteManyPartialError) {
1023
- const failedSelection: RowSelectionState = {};
1024
- for (const id of error.failedIds) failedSelection[String(id)] = true;
1025
- for (const id of error.succeededIds) selectedIdValueByKey.delete(String(id));
1026
- rowSelectionAtom.set(failedSelection);
1124
+ for (const id of error.failedIds) selectedIdValueByKey.set(tableRowKey(id), id);
1125
+ operationError = i18n.t('common.batchDeletePartialFail', { failed: error.failedIds.length, total: request.ids.length });
1126
+ } else {
1127
+ if (selectionBeforeDelete) {
1128
+ selectedIdValueByKey.clear();
1129
+ for (const [key, id] of selectionBeforeDelete) selectedIdValueByKey.set(key, id);
1130
+ rowSelection = Object.fromEntries([...selectionBeforeDelete.keys()].map(key => [key, true as const]));
1027
1131
  }
1028
- // useDeleteMany owns the error notification; keep failed selection for retry.
1029
- } finally {
1030
- confirmPending = false;
1132
+ operationError = i18n.t('common.operationFailed');
1031
1133
  }
1032
- };
1033
- confirmOpen = true;
1134
+ confirmOpen = false;
1135
+ } finally {
1136
+ if (activeDelete === token) { activeDelete = undefined; confirmPending = false; deleteRequest = null; }
1137
+ await tick();
1138
+ await tick();
1139
+ }
1034
1140
  }
1035
1141
 
1036
1142
  function openDetail(id: string | number): void {
1143
+ if (!canRead || !canShow) return;
1037
1144
  detailOpenedInHistory = true;
1038
- writeURLState({ detailId: String(id) }, adminContext, 'push');
1145
+ writeURLState({ detailId: formatContractRouteId(binding.resource, id) }, adminContext, 'push');
1039
1146
  }
1040
1147
 
1041
1148
  function closeDetail(): void {
@@ -1050,33 +1157,60 @@
1050
1157
 
1051
1158
  // ─── CSV Export ───────────────────────────────────────────────
1052
1159
  function exportCSV() {
1053
- const data = query.data?.data ?? [];
1054
- if (data.length === 0) return;
1055
- const headers = visibleFields.map(f => f.label);
1056
- const rows = data.map(record =>
1057
- visibleFields.map(f => {
1058
- const val = record[f.key];
1059
- if (val == null) return '';
1060
- if (typeof val === 'object') return JSON.stringify(val);
1061
- return String(val);
1062
- })
1063
- );
1064
- const csv = [headers.map(h => `"${h.replace(/"/g, '""')}"`).join(','), ...rows.map(r => r.map(v => `"${v.replace(/"/g, '""')}"`).join(','))].join('\n');
1065
- const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
1066
- const link = document.createElement('a');
1067
- link.href = URL.createObjectURL(blob);
1068
- link.download = `${resourceName}_${new Date().toISOString().slice(0, 10)}.csv`;
1069
- link.click();
1070
- setTimeout(() => URL.revokeObjectURL(link.href), 1000);
1160
+ if (!canRead || !canExport || !pageRecords.ok || query.isError || query.isFetching || !pageRecords.data.length) return;
1161
+ try {
1162
+ const exportFields = table_getAllLeafColumns(tbl).flatMap(column => {
1163
+ const field = visibleFields.find(field => field.key === column.id);
1164
+ return field && isColumnVisible(field.key) ? [field] : [];
1165
+ });
1166
+ if (!exportFields.length) return;
1167
+ const rows = tableExportRows(pageRecords.data, exportFields);
1168
+ downloadData(rows, resourceName, 'csv');
1169
+ } catch { operationError = i18n.t('common.operationFailed'); }
1071
1170
  }
1072
1171
 
1073
1172
  function goToPage(page: number) {
1173
+ if (!Number.isSafeInteger(page) || page < 1 || page > Math.max(1, totalPages)) return;
1074
1174
  markSavedViewDirty();
1075
1175
  pagination = { ...pagination, current: page };
1076
1176
  }
1177
+ function refreshList() {
1178
+ if (canRead && !query.isFetching) void listResult.refetch();
1179
+ }
1077
1180
  </script>
1078
1181
 
1182
+ {#snippet defaultRowActions(id: string | number)}
1183
+ <RecordRowActions
1184
+ {resourceName} {id} {canShow} {canEdit} {canDelete}
1185
+ onShow={() => openDetail(id)}
1186
+ onEdit={() => navigation.edit(resourceName, id)}
1187
+ onQuickEdit={() => quickEditId = id}
1188
+ onDelete={() => confirmDelete(id)}
1189
+ />
1190
+ {/snippet}
1191
+
1192
+ {#snippet defaultEmptyState()}
1193
+ {@const hasCriteria = !!(searchText.trim() || appliedSearchText.trim() || activeFilterCount > 0)}
1194
+ <DataState state="empty" description={i18n.t(hasCriteria ? 'empty.description' : 'common.noDataHint')}>
1195
+ {#snippet action()}
1196
+ {#if hasCriteria}
1197
+ <Button variant="outline" size="sm" onclick={clearFilters}>
1198
+ <X class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
1199
+ {i18n.t('common.clearAllFilters')}
1200
+ </Button>
1201
+ {:else if canCreate}
1202
+ <Button variant="outline" size="sm" onclick={() => navigation.create(resourceName)}>
1203
+ <Plus class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
1204
+ {i18n.t('common.create')}
1205
+ </Button>
1206
+ {/if}
1207
+ {/snippet}
1208
+ </DataState>
1209
+ {/snippet}
1210
+
1079
1211
  <div class="svadmin-u-6ed543e2fbbb">
1212
+ {#if operationError}<p role="alert">{operationError}</p>{/if}
1213
+ {#if detailState.invalid}<p role="alert">{i18n.t('common.operationFailed')}</p>{/if}
1080
1214
  {#if showHeader}
1081
1215
  <!-- Header -->
1082
1216
  <div class="svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-77a2a20e90d4">
@@ -1101,7 +1235,7 @@
1101
1235
 
1102
1236
  <!-- Selection Banner (Enterprise Batch Actions) -->
1103
1237
  {#if selectedCount > 0}
1104
- <div
1238
+ <div
1105
1239
  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"
1106
1240
  aria-label={i18n.t("common.selectedCount", { count: selectedCount })}
1107
1241
  data-svadmin-batch-toolbar
@@ -1181,7 +1315,7 @@
1181
1315
  id="filter-{field.key}"
1182
1316
  class="svadmin-u-e7a768f922d2 svadmin-u-fc7473ca09eb"
1183
1317
  value={filterValues[field.key] ?? ""}
1184
- onchange={(e) => setFilterValue(field.key, (e.currentTarget as HTMLSelectElement).value)}
1318
+ onchange={(e: Event) => setFilterFromEvent(field.key, e)}
1185
1319
  >
1186
1320
  <option value="">{i18n.t("common.all")}</option>
1187
1321
  {#each field.options as opt, _i (_i)}
@@ -1193,7 +1327,7 @@
1193
1327
  id="filter-{field.key}"
1194
1328
  type="text"
1195
1329
  value={filterValues[field.key] ?? ""}
1196
- oninput={(e) => setFilterValue(field.key, e.currentTarget.value)}
1330
+ oninput={(e: Event) => setFilterFromEvent(field.key, e)}
1197
1331
  placeholder={field.label}
1198
1332
  class="svadmin-u-e7a768f922d2 svadmin-u-fc7473ca09eb"
1199
1333
  />
@@ -1225,7 +1359,7 @@
1225
1359
  {#if showDensitySwitcher}
1226
1360
  <DropdownMenu.Root>
1227
1361
  <DropdownMenu.Trigger>
1228
- {#snippet child({ props })}
1362
+ {#snippet child({ props }: { props: Record<string, unknown> })}
1229
1363
  <TooltipButton tooltip={i18n.t("common.density")} variant="outline" size="sm" class="svadmin-u-e7a768f922d2 svadmin-u-0b91436debbd" {...props}>
1230
1364
  <Rows class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" aria-hidden="true" />
1231
1365
  </TooltipButton>
@@ -1245,7 +1379,7 @@
1245
1379
  <!-- Column Visibility Picker -->
1246
1380
  <DropdownMenu.Root>
1247
1381
  <DropdownMenu.Trigger>
1248
- {#snippet child({ props })}
1382
+ {#snippet child({ props }: { props: Record<string, unknown> })}
1249
1383
  <TooltipButton tooltip={i18n.t("common.columns")} variant="outline" size="sm" class="svadmin-u-e7a768f922d2 svadmin-u-0b91436debbd" {...props}>
1250
1384
  <SlidersHorizontal class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
1251
1385
  </TooltipButton>
@@ -1255,7 +1389,7 @@
1255
1389
  {#each table_getAllLeafColumns(tbl).filter((column) => !column.id.startsWith("_")) as column, _i (_i)}
1256
1390
  <DropdownMenu.CheckboxItem
1257
1391
  checked={tableColumnVisibility.current[column.id] ?? true}
1258
- onCheckedChange={(v) => setColumnVisibility(column.id, !!v)}
1392
+ onCheckedChange={(v: boolean) => setColumnVisibility(column.id, v)}
1259
1393
  >
1260
1394
  {visibleFields.find(f => f.key === column.id)?.label ?? column.id}
1261
1395
  </DropdownMenu.CheckboxItem>
@@ -1284,8 +1418,9 @@
1284
1418
  id="saved-list-view"
1285
1419
  class="svadmin-u-e7a768f922d2 svadmin-u-6da6a3c3f741"
1286
1420
  value={activeSavedViewId ?? ""}
1287
- onchange={(event) => {
1288
- const id = (event.currentTarget as HTMLSelectElement).value;
1421
+ onchange={(event: Event) => {
1422
+ if (!(event.currentTarget instanceof HTMLSelectElement)) return;
1423
+ const id = event.currentTarget.value;
1289
1424
  const view = savedViews.find((candidate) => candidate.id === id);
1290
1425
  if (view) applySavedView(view);
1291
1426
  else {
@@ -1340,7 +1475,7 @@
1340
1475
  variant="outline"
1341
1476
  size="sm"
1342
1477
  class="svadmin-u-e7a768f922d2 svadmin-u-0b91436debbd"
1343
- onclick={() => listResult.refetch()}
1478
+ onclick={refreshList}
1344
1479
  >
1345
1480
  <RefreshCw class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 {query.isFetching ? "svadmin-u-afbdd13a380e" : ""}" />
1346
1481
  </TooltipButton>
@@ -1398,7 +1533,40 @@
1398
1533
 
1399
1534
  <!-- Table (TanStack-powered) -->
1400
1535
  <div class="svadmin-u-2cd02d11d1af svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 svadmin-u-438b2237b8d6" role="region" aria-label="{resource.label} {i18n.t('common.list')}" data-table-density={currentDensity}>
1401
- {#if query.isLoading}
1536
+ {#if listPermission.isLoading || (query.isLoading && !query.isError)}
1537
+ <Table.Root density={currentDensity}>
1538
+ <Table.Header>
1539
+ {#each tableView.headerGroups as headerGroup, _i (_i)}
1540
+ {@const visibleHeaders = headerGroup.headers.filter((header: Header<TableFeatures, TableRecord, unknown>) => isColumnVisible(header.column.id))}
1541
+ <Table.Row>
1542
+ {#each visibleHeaders as header (header.id)}
1543
+ <Table.Head>
1544
+ {#if header.id === '_actions'}
1545
+ {i18n.t('common.actions')}
1546
+ {:else if column_getCanSort(header.column)}
1547
+ <Button
1548
+ variant="ghost"
1549
+ size="sm"
1550
+ 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"
1551
+ onclick={() => toggleColumnSort(header.column)}
1552
+ >
1553
+ {visibleFields.find(field => field.key === header.id)?.label ?? header.id}
1554
+ <span class="svadmin-u-359090c2d529 svadmin-u-0b8c506a0596">
1555
+ {#if column_getIsSorted(header.column) === 'asc'}↑
1556
+ {:else if column_getIsSorted(header.column) === 'desc'}↓
1557
+ {:else}⇅
1558
+ {/if}
1559
+ </span>
1560
+ </Button>
1561
+ {:else}
1562
+ {visibleFields.find(field => field.key === header.id)?.label ?? header.id}
1563
+ {/if}
1564
+ </Table.Head>
1565
+ {/each}
1566
+ </Table.Row>
1567
+ {/each}
1568
+ </Table.Header>
1569
+ </Table.Root>
1402
1570
  <div class="svadmin-u-8e63407b5ceb svadmin-u-6ed543e2fbbb">
1403
1571
  <div class="svadmin-u-60fbb7713999 svadmin-u-0c3bc98565dd svadmin-u-a77ed4d908c0">
1404
1572
  {#each visibleFields.slice(0, 4) as _, _i (_i)}
@@ -1413,11 +1581,13 @@
1413
1581
  </div>
1414
1582
  {/each}
1415
1583
  </div>
1416
- {:else if query.error}
1584
+ {:else if !canRead}
1585
+ <p role="alert">{i18n.t('common.operationFailed')}</p>
1586
+ {:else if query.error || query.isError || !pageRecords.ok}
1417
1587
  <DataState
1418
1588
  state="error"
1419
- description={i18n.t('common.loadFailed', { message: (query.error as Error).message })}
1420
- retry={() => listResult.refetch()}
1589
+ description={i18n.t('common.operationFailed')}
1590
+ retry={refreshList}
1421
1591
  />
1422
1592
  {:else}
1423
1593
  <div in:fade={{ duration: 150 }}>
@@ -1426,13 +1596,13 @@
1426
1596
  <Table.Root density={currentDensity}>
1427
1597
  <Table.Header>
1428
1598
  {#each tableView.headerGroups as headerGroup, _i (_i)}
1429
- {@const visibleHeaders = headerGroup.headers.filter((header: Header<TableFeatures, BaseRecord, unknown>) => isColumnVisible(header.column.id))}
1599
+ {@const visibleHeaders = headerGroup.headers.filter((header: Header<TableFeatures, TableRecord, unknown>) => isColumnVisible(header.column.id))}
1430
1600
  <DraggableHeader
1431
1601
  columns={visibleHeaders.map((header) => ({ id: header.column.id, header }))}
1432
1602
  onReorder={setColumnOrder}
1433
1603
  >
1434
1604
  {#snippet header(col, _index, dragProps)}
1435
- {@const header = col.header as typeof headerGroup.headers[0]}
1605
+ {@const header = col.header}
1436
1606
  <Table.Head
1437
1607
  {...dragProps}
1438
1608
  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)}
@@ -1445,14 +1615,12 @@
1445
1615
  checked={table_getIsAllRowsSelected(tbl)}
1446
1616
  onCheckedChange={toggleAllRowsSelection}
1447
1617
  />
1448
- {:else}
1449
- <CanAccess resource={resourceName} action="delete" params={{ ids: tableView.rows.map(rowIdValue) }}>
1618
+ {:else if !acEnabled || deleteResourcePermission.isLoading || deleteResourcePermission.allowed}
1450
1619
  <Checkbox
1451
1620
  aria-label={i18n.t('common.selectAll')}
1452
1621
  checked={table_getIsAllRowsSelected(tbl)}
1453
1622
  onCheckedChange={toggleAllRowsSelection}
1454
1623
  />
1455
- </CanAccess>
1456
1624
  {/if}
1457
1625
  {:else if header.id === '_expand'}
1458
1626
  <!-- empty -->
@@ -1483,8 +1651,8 @@
1483
1651
  </Table.Header>
1484
1652
  <Table.Body>
1485
1653
  {#each tableView.rows as row (row.id)}
1486
- {@const record = row.original}
1487
- {@const id = record[primaryKey] as string | number}
1654
+ {@const record = copyTableRecord(row.original)}
1655
+ {@const id = rowIdValue(row)}
1488
1656
  {@const visibleCells = row_getVisibleCells(row).filter((cell) => isColumnVisible(cell.column.id))}
1489
1657
  <ContextMenu.Root>
1490
1658
  <ContextMenu.Trigger>
@@ -1521,65 +1689,32 @@
1521
1689
  {#if rowActions}
1522
1690
  {@render rowActions({ record, id })}
1523
1691
  {:else}
1524
- {#if canShow}
1525
- <CanAccess resource={resourceName} action="show" params={{ id }}>
1526
- <TooltipButton tooltip={i18n.t('common.detail')} variant="ghost" size="icon-sm" onclick={() => openDetail(id)}>
1527
- <Eye class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
1528
- </TooltipButton>
1529
- </CanAccess>
1530
- {/if}
1531
- {#if canEdit}
1532
- <CanAccess resource={resourceName} action="edit" params={{ id }}>
1533
- <TooltipButton tooltip={i18n.t('common.edit')} variant="ghost" size="icon-sm" onclick={() => navigation.edit(resourceName, id)}>
1534
- <Pencil class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
1535
- </TooltipButton>
1536
- </CanAccess>
1537
- {/if}
1538
- {#if canDelete}
1539
- <CanAccess resource={resourceName} action="delete" params={{ id }}>
1540
- <TooltipButton tooltip={i18n.t('common.delete')} variant="ghost" size="icon-sm" onclick={() => confirmDelete(id)} class="svadmin-u-51e95020d6f2">
1541
- <Trash2 class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
1542
- </TooltipButton>
1543
- </CanAccess>
1544
- {/if}
1692
+ {@render defaultRowActions(id)}
1545
1693
  {/if}
1546
1694
  </div>
1547
1695
  {:else}
1548
1696
  {@const field = visibleFields.find(f => f.key === cell.column.id)}
1549
- {#if customColumns && field && customColumns[field.key]}
1550
- {@render customColumns[field.key]({ value: cell_getValue(cell), record })}
1697
+ {@const customColumn = field ? customColumns?.[field.key] : undefined}
1698
+ {#if customColumn}
1699
+ {@render customColumn({ value: clonePlainValue(cell_getValue(cell)), record: copyTableRecord(record) })}
1551
1700
  {:else if defaultCellRenderer && field}
1552
- {@render defaultCellRenderer({ field, value: cell_getValue(cell), record })}
1553
- {:else if field?.type === 'boolean'}
1554
- <span class="svadmin-u-bb0c4bfc52bd svadmin-u-2f2a842e50fa svadmin-u-940924b6e2d9 svadmin-u-ac204c108886 {cell_getValue(cell) ? 'svadmin-u-3355648fe22b' : 'svadmin-u-2d56f1807e51'}"></span>
1555
- {:else if field?.type === 'date' && cell_getValue(cell)}
1556
- {new Date(cell_getValue(cell) as string).toLocaleDateString()}
1557
- {:else if field?.type === 'tags' && Array.isArray(cell_getValue(cell))}
1558
- <div class="svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-44ee8ba0a421">
1559
- {#each (cell_getValue(cell) as string[]).slice(0, 3) as tag, _i (_i)}
1560
- <Badge variant="secondary">{tag}</Badge>
1561
- {/each}
1562
- </div>
1563
- {:else if field?.type === 'select' && field.options}
1564
- {@const opt = field.options.find(o => o.value === cell_getValue(cell))}
1565
- <Badge variant="outline">{opt?.label ?? cell_getValue(cell) ?? '—'}</Badge>
1566
- {:else if canEdit && field && ['text', 'number', 'email', 'url'].includes(field.type)}
1701
+ {@render defaultCellRenderer({ field, value: clonePlainValue(cell_getValue(cell)), record: copyTableRecord(record) })}
1702
+ {:else if canEdit && field && field.showInEdit !== false && writableFields.has(field.key) && field.key !== primaryKey && field.key !== 'id' && ['text', 'number', 'email', 'url'].includes(field.type)}
1567
1703
  <CanAccess resource={resourceName} action="edit" params={{ id }}>
1568
1704
  <InlineEdit
1569
1705
  {resourceName}
1570
1706
  recordId={id}
1571
1707
  {field}
1572
1708
  value={cell_getValue(cell)}
1573
- onSave={() => listResult.refetch()}
1574
1709
  />
1575
1710
  {#snippet fallback()}
1576
- <span class="svadmin-u-0214b4b355d1 svadmin-u-f283ea9bea0e" title={String(cell_getValue(cell) ?? '—')}>{cell_getValue(cell) ?? '—'}</span>
1711
+ <FieldDisplay type={field.type} value={cell_getValue(cell)} options={field.options} />
1577
1712
  {/snippet}
1578
1713
  </CanAccess>
1579
- {:else if field?.key === primaryKey}
1714
+ {:else if field?.key === 'id'}
1580
1715
  <span class="svadmin-u-0214b4b355d1 svadmin-u-f283ea9bea0e svadmin-u-0e65706bcccd svadmin-u-359090c2d529" title={String(cell_getValue(cell) ?? '—')}>{cell_getValue(cell) ?? '—'}</span>
1581
- {:else}
1582
- <span class="svadmin-u-0214b4b355d1 svadmin-u-f283ea9bea0e" title={String(cell_getValue(cell) ?? '—')}>{cell_getValue(cell) ?? '—'}</span>
1716
+ {:else if field}
1717
+ <FieldDisplay type={field.type} value={cell_getValue(cell)} options={field.options} resourceName={field.resource} />
1583
1718
  {/if}
1584
1719
  {/if}
1585
1720
  </Table.Cell>
@@ -1628,19 +1763,7 @@
1628
1763
  {#if emptyState}
1629
1764
  {@render emptyState()}
1630
1765
  {:else}
1631
- <div class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-a1f611f027dd">
1632
- <svg class="svadmin-u-acaee62117b1 svadmin-u-baceed3462fd svadmin-u-106b502aac96 svadmin-u-da019856f2cc" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor">
1633
- <path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
1634
- </svg>
1635
- <p class="svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-bfa603190748 svadmin-u-65281709dacf">{i18n.t('common.noData')}</p>
1636
- <p class="svadmin-u-359090c2d529 svadmin-u-7be4d67a6256 svadmin-u-da019856f2cc">{i18n.t('common.noDataHint')}</p>
1637
- {#if canCreate}
1638
- <Button variant="outline" size="sm" class="svadmin-u-77a2a20e90d4" onclick={() => navigation.create(resourceName)}>
1639
- <Plus class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
1640
- {i18n.t('common.create')}
1641
- </Button>
1642
- {/if}
1643
- </div>
1766
+ {@render defaultEmptyState()}
1644
1767
  {/if}
1645
1768
  </Table.Cell>
1646
1769
  </Table.Row>
@@ -1648,7 +1771,7 @@
1648
1771
  </Table.Body>
1649
1772
  {#if summary}
1650
1773
  <Table.Footer class="svadmin-u-2859c861d7de svadmin-u-2689f3958069">
1651
- {@render summary({ data: (query.data?.data as BaseRecord[]) ?? [], total: query.data?.total ?? 0, visibleColumnsCount: columns.filter((c) => c.id != null && isColumnVisible(c.id)).length })}
1774
+ {@render summary({ data: pageRecords.data.map(copyTableRecord), total: query.data?.total ?? 0, visibleColumnsCount: columns.filter((c) => c.id != null && isColumnVisible(c.id)).length })}
1652
1775
  </Table.Footer>
1653
1776
  {/if}
1654
1777
  </Table.Root>
@@ -1657,8 +1780,8 @@
1657
1780
  <!-- Mobile Card View (visible only on small screens) -->
1658
1781
  <div class="svadmin-u-e477a6af4cb6 svadmin-u-6ed543e2fbbb svadmin-u-7660b450905a">
1659
1782
  {#each tableView.rows as row, _i (_i)}
1660
- {@const record = row.original}
1661
- {@const id = record[primaryKey] as string | number}
1783
+ {@const record = copyTableRecord(row.original)}
1784
+ {@const id = rowIdValue(row)}
1662
1785
  <div
1663
1786
  class="svadmin-u-f5c8cc114f47 svadmin-u-633ef3c47872 svadmin-u-3daca9af0861 svadmin-u-a3158643e114 svadmin-u-cd0ad9a56558 svadmin-u-c07e54fd1439 svadmin-u-0fe7d7d814d0 {rowIsSelected(row.id) ? 'svadmin-u-16b1efa5875e svadmin-u-f42e9fee68a1 svadmin-u-989c466fdbe7' : ''}"
1664
1787
  >
@@ -1688,27 +1811,7 @@
1688
1811
  {#if rowActions}
1689
1812
  {@render rowActions({ record, id })}
1690
1813
  {:else}
1691
- {#if canEdit}
1692
- <CanAccess resource={resourceName} action="edit" params={{ id }}>
1693
- <TooltipButton tooltip={i18n.t('common.edit')} variant="ghost" size="icon-sm" onclick={() => navigation.edit(resourceName, id)}>
1694
- <Pencil class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
1695
- </TooltipButton>
1696
- </CanAccess>
1697
- {/if}
1698
- {#if canShow}
1699
- <CanAccess resource={resourceName} action="show" params={{ id }}>
1700
- <TooltipButton tooltip={i18n.t('common.detail')} variant="ghost" size="icon-sm" onclick={() => openDetail(id)}>
1701
- <Eye class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
1702
- </TooltipButton>
1703
- </CanAccess>
1704
- {/if}
1705
- {#if canDelete}
1706
- <CanAccess resource={resourceName} action="delete" params={{ id }}>
1707
- <TooltipButton tooltip={i18n.t('common.delete')} variant="ghost" size="icon-sm" onclick={() => confirmDelete(id)} class="svadmin-u-51e95020d6f2">
1708
- <Trash2 class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
1709
- </TooltipButton>
1710
- </CanAccess>
1711
- {/if}
1814
+ {@render defaultRowActions(id)}
1712
1815
  {/if}
1713
1816
  </div>
1714
1817
  </div>
@@ -1716,28 +1819,18 @@
1716
1819
  <div class="svadmin-u-6f7e013d6499">
1717
1820
  {#each visibleFields.filter((field) => isColumnVisible(field.key)).slice(0, 6) as field (field.key)}
1718
1821
  {@const value = record[field.key]}
1822
+ {@const customColumn = customColumns?.[field.key]}
1719
1823
  <div class="svadmin-u-60fbb7713999 svadmin-u-60541e1e26f8 svadmin-u-8ef2268efbbc svadmin-u-0c3bc98565dd">
1720
1824
  <span class="svadmin-u-359090c2d529 svadmin-u-2689f3958069 svadmin-u-bfa603190748 svadmin-u-012fbd121f37">{field.label}</span>
1721
1825
  <span class="svadmin-u-fc7473ca09eb svadmin-u-308fc069e46e svadmin-u-f283ea9bea0e svadmin-u-897d497e57b6">
1722
- {#if customColumns && field && customColumns[field.key]}
1723
- {@render customColumns[field.key]({ value, record })}
1826
+ {#if customColumn}
1827
+ {@render customColumn({ value: clonePlainValue(value), record: copyTableRecord(record) })}
1724
1828
  {:else if defaultCellRenderer && field}
1725
- {@render defaultCellRenderer({ field, value, record })}
1726
- {:else if field.type === 'boolean'}
1727
- <span class="svadmin-u-bb0c4bfc52bd svadmin-u-2f2a842e50fa svadmin-u-940924b6e2d9 svadmin-u-ac204c108886 {value ? 'svadmin-u-3355648fe22b' : 'svadmin-u-2d56f1807e51'}"></span>
1728
- {:else if field.type === 'date' && value}
1729
- {new Date(value as string).toLocaleDateString()}
1730
- {:else if field.type === 'tags' && Array.isArray(value)}
1731
- <div class="svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-44ee8ba0a421 svadmin-u-77c08e015d14">
1732
- {#each (value as string[]).slice(0, 2) as tag, _i (_i)}
1733
- <Badge variant="secondary" class="svadmin-u-1dc571a3609f">{tag}</Badge>
1734
- {/each}
1735
- </div>
1736
- {:else if field.type === 'select' && field.options}
1737
- {@const opt = field.options.find(o => o.value === value)}
1738
- <Badge variant="outline" class="svadmin-u-1dc571a3609f">{opt?.label ?? value ?? '—'}</Badge>
1829
+ {@render defaultCellRenderer({ field, value: clonePlainValue(value), record: copyTableRecord(record) })}
1830
+ {:else if field.key === 'id'}
1831
+ {id}
1739
1832
  {:else}
1740
- {value ?? '—'}
1833
+ <FieldDisplay type={field.type} {value} options={field.options} resourceName={field.resource} />
1741
1834
  {/if}
1742
1835
  </span>
1743
1836
  </div>
@@ -1749,7 +1842,7 @@
1749
1842
  {#if emptyState}
1750
1843
  {@render emptyState()}
1751
1844
  {:else}
1752
- <DataState state="empty" class="svadmin-u-119b2aa0b8f6 svadmin-u-7f19cdf4c5bb svadmin-u-d5eab218aa34 svadmin-u-cb11fec3bb46" />
1845
+ {@render defaultEmptyState()}
1753
1846
  {/if}
1754
1847
  </div>
1755
1848
  {/each}
@@ -1767,9 +1860,10 @@
1767
1860
  aria-label={i18n.t('common.perPage')}
1768
1861
  class="svadmin-u-ed8a5df7b2fb svadmin-u-d043cad8e3fa svadmin-u-012fbd121f37 svadmin-u-3032cae0badb"
1769
1862
  value={String(pagination.pageSize ?? 10)}
1770
- onchange={(e) => {
1771
- const size = Number((e.currentTarget as HTMLSelectElement).value);
1772
- if (!isNaN(size)) {
1863
+ onchange={(e: Event) => {
1864
+ if (!(e.currentTarget instanceof HTMLSelectElement)) return;
1865
+ const size = Number(e.currentTarget.value);
1866
+ if (Number.isSafeInteger(size) && size > 0) {
1773
1867
  markSavedViewDirty();
1774
1868
  pagination = { ...pagination, pageSize: size, current: 1 };
1775
1869
  }
@@ -1797,7 +1891,7 @@
1797
1891
  <PaginationUI.Link
1798
1892
  class="svadmin-u-3032cae0badb"
1799
1893
  isActive={page === currentPage}
1800
- onclick={() => goToPage(page as number)}
1894
+ onclick={() => { if (typeof page === 'number') goToPage(page); }}
1801
1895
  >
1802
1896
  {page}
1803
1897
  </PaginationUI.Link>
@@ -1820,9 +1914,9 @@
1820
1914
  open={confirmOpen}
1821
1915
  message={confirmMessage}
1822
1916
  confirmText={i18n.t('common.delete')}
1823
- confirming={confirmPending}
1917
+ confirming={confirmPending || deletePermission.isLoading}
1824
1918
  onconfirm={confirmAction}
1825
- oncancel={() => { confirmOpen = false; }}
1919
+ oncancel={() => { if (!confirmPending) { confirmOpen = false; deleteRequest = null; } }}
1826
1920
  />
1827
1921
 
1828
1922
  {#if detailRecordId != null}
@@ -1833,3 +1927,9 @@
1833
1927
  onClose={closeDetail}
1834
1928
  />
1835
1929
  {/if}
1930
+
1931
+ {#if quickEditId != null && canRead && canEdit}
1932
+ {#key quickEditId}
1933
+ <QuickEditDrawer {resourceName} recordId={quickEditId} onClose={() => quickEditId = undefined} />
1934
+ {/key}
1935
+ {/if}