@antelopejs/dms 0.7.8 → 0.7.10

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 (184) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/config.d.ts +2 -0
  3. package/dist/config.js +9 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/implementations/dms/api-base-url.d.ts +1 -0
  6. package/dist/implementations/dms/api-base-url.js +6 -0
  7. package/dist/implementations/dms/api-base-url.js.map +1 -0
  8. package/dist/implementations/dms/frontend-modules.js +7 -6
  9. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  10. package/dist/implementations/dms/page.js +8 -2
  11. package/dist/implementations/dms/page.js.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/test/attachment-host/capabilities.d.ts +25 -0
  15. package/dist/test/attachment-host/capabilities.js +53 -1
  16. package/dist/test/attachment-host/capabilities.js.map +1 -1
  17. package/dist/test/integration/table-view-capabilities.test.js +13 -0
  18. package/dist/test/integration/table-view-capabilities.test.js.map +1 -1
  19. package/dist/test/unit/api-base-url.test.d.ts +1 -0
  20. package/dist/test/unit/api-base-url.test.js +31 -0
  21. package/dist/test/unit/api-base-url.test.js.map +1 -0
  22. package/dist/test/unit/interfaces/dms/page-layout-query.test.d.ts +1 -0
  23. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +106 -0
  24. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -0
  25. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -0
  26. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  27. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.d.ts +1 -0
  28. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js +46 -0
  29. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js.map +1 -0
  30. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.d.ts +1 -0
  31. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +26 -0
  32. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -0
  33. package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +10 -0
  34. package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
  35. package/dist/test/unit/interfaces/dms-base/guard.test.d.ts +1 -0
  36. package/dist/test/unit/interfaces/dms-base/guard.test.js +53 -0
  37. package/dist/test/unit/interfaces/dms-base/guard.test.js.map +1 -0
  38. package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js +23 -0
  39. package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js.map +1 -1
  40. package/dist/test/unit/interfaces/dms-base/page-live.test.js +55 -0
  41. package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -1
  42. package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js +17 -0
  43. package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js.map +1 -1
  44. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +8 -1
  45. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
  46. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +28 -0
  47. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  48. package/frontend-vue/layers/dms-core/app/build/composables/user/preferencesCookie.ts +149 -0
  49. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +321 -0
  50. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +4 -3
  51. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +4 -3
  52. package/frontend-vue/layers/dms-core/app/composables/period/types.ts +3 -1
  53. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +2 -2
  54. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +9 -2
  55. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +48 -16
  56. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +21 -4
  57. package/frontend-vue/layers/dms-core/app/composables/user/usePreferences.ts +81 -49
  58. package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +35 -0
  59. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
  60. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
  61. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +10 -1
  62. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +32 -1
  63. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +13 -3
  64. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +27 -3
  65. package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +16 -0
  66. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +75 -0
  67. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +8 -1
  68. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +13 -2
  69. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +7 -0
  70. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +7 -0
  71. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -4
  72. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +59 -0
  73. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
  74. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
  75. package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +2 -0
  76. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockInlineStatus.vue +67 -0
  77. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +60 -20
  78. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +15 -8
  79. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +14 -0
  80. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +29 -4
  81. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +76 -28
  82. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +26 -2
  83. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useGuard.ts +115 -0
  84. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +1 -1
  85. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +58 -3
  86. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/secondaryTarget.ts +86 -0
  87. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +5 -0
  88. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +6 -0
  89. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveCadence.ts +42 -0
  90. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveHolds.ts +66 -0
  91. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +42 -0
  92. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveStorage.ts +42 -0
  93. package/frontend-vue/layers/dms-ui/app/build/composables/live/missedTally.ts +65 -0
  94. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +247 -88
  95. package/frontend-vue/layers/dms-ui/app/build/composables/live/useLivePeriodSpan.ts +33 -0
  96. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +79 -5
  97. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparisonControl.ts +70 -0
  98. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +55 -0
  99. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +41 -5
  100. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +22 -9
  101. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +13 -6
  102. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +1 -0
  103. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +27 -2
  104. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +18 -3
  105. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +19 -26
  106. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +66 -0
  107. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +7 -1
  108. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +65 -11
  109. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTablePreferences.ts +149 -0
  110. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +39 -8
  111. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
  112. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +16 -11
  113. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +39 -16
  114. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +29 -0
  115. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +18 -5
  116. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/sortableColumns.ts +17 -0
  117. package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +79 -0
  118. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +46 -1
  119. package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +5 -4
  120. package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +4 -0
  121. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +40 -2
  122. package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +11 -1
  123. package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +12 -2
  124. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +129 -4
  125. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +6 -0
  126. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +6 -1
  127. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +33 -0
  128. package/frontend-vue/layers/dms-ui/app/components/form/components/ChoiceCards.vue +22 -4
  129. package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +74 -0
  130. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -17
  131. package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +15 -0
  132. package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +12 -15
  133. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +104 -16
  134. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +113 -99
  135. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +11 -1
  136. package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +2 -0
  137. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +11 -1
  138. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +15 -1
  139. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +86 -10
  140. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewShiftF.ts +20 -0
  141. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +12 -0
  142. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -0
  143. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +8 -1
  144. package/frontend-vue/layers/dms-ui/app/composables/table-view/useTableViewEmptyState.ts +55 -0
  145. package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
  146. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +2 -0
  147. package/frontend-vue/layers/dms-ui/app/utils/componentId.ts +21 -0
  148. package/frontend-vue/layers/dms-ui/app/utils/rowClickAction.ts +12 -0
  149. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +13 -4
  150. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +12 -3
  151. package/frontend-vue/tests/activity-feed-fill-height.test.ts +1 -0
  152. package/frontend-vue/tests/activity-feed-head.test.ts +140 -0
  153. package/frontend-vue/tests/block-not-found.test.ts +220 -0
  154. package/frontend-vue/tests/choice-cards-columns.test.ts +60 -0
  155. package/frontend-vue/tests/component-id.test.ts +18 -0
  156. package/frontend-vue/tests/confirm-secondary-action.test.ts +269 -0
  157. package/frontend-vue/tests/guard.test.ts +242 -0
  158. package/frontend-vue/tests/header-action-page-query.test.ts +286 -0
  159. package/frontend-vue/tests/live-toggle.test.ts +87 -1
  160. package/frontend-vue/tests/page-layout-registration.test.ts +1 -0
  161. package/frontend-vue/tests/page-live-parity.test.ts +367 -0
  162. package/frontend-vue/tests/page-realtime-remount.test.ts +185 -0
  163. package/frontend-vue/tests/period-selector-parity.test.ts +138 -0
  164. package/frontend-vue/tests/period-selector-url.test.ts +84 -6
  165. package/frontend-vue/tests/period-url-query.test.ts +52 -0
  166. package/frontend-vue/tests/preferences-cookie-size.test.ts +311 -0
  167. package/frontend-vue/tests/row-side-panel.test.ts +109 -10
  168. package/frontend-vue/tests/table-actions-search-label.test.ts +57 -1
  169. package/frontend-vue/tests/table-clear-filters.test.ts +99 -1
  170. package/frontend-vue/tests/table-density-menu.test.ts +4 -3
  171. package/frontend-vue/tests/table-live-hold.test.ts +95 -5
  172. package/frontend-vue/tests/table-row-action-labels.test.ts +181 -0
  173. package/frontend-vue/tests/table-row-hover.test.ts +32 -0
  174. package/frontend-vue/tests/table-source-period-scope.test.ts +31 -3
  175. package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +14 -17
  176. package/frontend-vue/tests/table-view-pinned-rows.test.ts +143 -0
  177. package/frontend-vue/tests/table-view-sortable-columns.test.ts +45 -0
  178. package/frontend-vue/tests/table-view-views.test.ts +56 -15
  179. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +107 -0
  180. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  181. package/package.json +2 -2
  182. package/skills/dms/SKILL.md +9 -3
  183. package/skills/dms-module/SKILL.md +5 -0
  184. package/skills/dms-pages/REFERENCE.md +23 -2
@@ -70,6 +70,7 @@ import {
70
70
  defaultSortingState,
71
71
  resolveDefaultSortConfig,
72
72
  sanitizeSorting,
73
+ sortingOrDefault,
73
74
  sortableColumnIds,
74
75
  sortValueKind,
75
76
  } from "../../build/composables/table-view/utils/sortableColumns";
@@ -95,8 +96,12 @@ import {
95
96
  staticQuickFilterItems,
96
97
  } from "../../build/composables/table-view/utils/quickFilters";
97
98
  import { useNavBadges } from "../../build/composables/navigation/useNavBadges";
98
- import { resolveRowClickAction } from "../../utils/rowClickAction";
99
+ import {
100
+ resolveRowClickAction,
101
+ rowCustomActions,
102
+ } from "../../utils/rowClickAction";
99
103
  import { usePermissionPreview } from "#dms-core/app/build/composables/auth/usePermissionPreview";
104
+ import type { RowActionRule } from "#dms-core/app/types/row-action";
100
105
 
101
106
  import UTable from "../../build/components/table/Table.vue";
102
107
  import ExpandedRowDetail from "../../build/components/table-view/ExpandedRowDetail.vue";
@@ -111,7 +116,9 @@ import type {
111
116
  } from "../../build/components/table/Table.vue";
112
117
  import {
113
118
  defineAsyncComponent,
119
+ useTemplateRef,
114
120
  type Component,
121
+ type ComponentPublicInstance,
115
122
  type WatchOptions,
116
123
  type WatchSource,
117
124
  } from "vue";
@@ -121,9 +128,13 @@ import { useGroupedRows } from "../../build/composables/table-view/useGroupedRow
121
128
  import { useTableFooter } from "../../build/composables/table-view/useTableFooter";
122
129
  import { useAccumulatedPages } from "../../build/composables/table-view/useAccumulatedPages";
123
130
  import { usePersistedPagination } from "../../build/composables/table-view/usePersistedPagination";
131
+ import { useTablePreferences } from "../../build/composables/table-view/useTablePreferences";
132
+ import type { Ref } from "vue";
133
+ import type { TableField } from "#dms-core/app/build/composables/user/tablePreferences";
124
134
  import { useTableRows } from "../../build/composables/table-view/useTableRows";
125
135
  import { useRefreshTriggers } from "../../build/composables/blocks/useRefreshTriggers";
126
136
  import { useTableLiveHold } from "../../build/composables/table-view/useTableLiveHold";
137
+ import type { TableViewLiveHoldOptions } from "../../types/live";
127
138
  import { usePageLive } from "../../composables/live/usePageLive";
128
139
  import LiveHoldBanner from "../../build/components/table-view/LiveHoldBanner.vue";
129
140
  import { useTableReorder } from "../../build/composables/table-view/useTableReorder";
@@ -166,6 +177,9 @@ interface RealtimePresenceActor {
166
177
 
167
178
  type RealtimePresenceMap = Record<string, RealtimePresenceActor[]>;
168
179
 
180
+ /** A kept field of the table, its state, and its default. */
181
+ type TableStatePreference = [TableField, Ref<unknown>, unknown];
182
+
169
183
  // The backend options of a table view, declared here as well so the
170
184
  // component's runtime props never depend on resolving the imported config.
171
185
  interface TableViewProps<T extends Data> extends TableViewConfig<T> {
@@ -173,6 +187,8 @@ interface TableViewProps<T extends Data> extends TableViewConfig<T> {
173
187
  expandable?: TableViewExpandableConfig;
174
188
  /** Placeholder of the search field. */
175
189
  searchPlaceholder?: string;
190
+ /** Key opening the search (`defineShortcuts` notation); `shift_f` else. */
191
+ searchShortcut?: string;
176
192
  /** One-click dropdown filters of the toolbar. */
177
193
  quickFilters?: TableViewQuickFilter[];
178
194
  /** Rows per page while the user picked none. */
@@ -183,6 +199,8 @@ interface TableViewProps<T extends Data> extends TableViewConfig<T> {
183
199
  source?: TableViewSourceConfig;
184
200
  /** Rows ordered by hand on a number column (backend `reorder`). */
185
201
  reorder?: TableViewReorderConfig;
202
+ /** Rows listed first whatever the sort (backend `pinnedRows`). */
203
+ pinnedRows?: RowActionRule;
186
204
  /** Footer texts and figures: row count, hint, summaries and legend. */
187
205
  footer?: TableViewFooter;
188
206
  /** What the empty body says, per reason it is empty. */
@@ -196,7 +214,7 @@ interface TableViewProps<T extends Data> extends TableViewConfig<T> {
196
214
  /** Topics whose events make the table read its rows again. */
197
215
  realtimeTopic?: string | string[];
198
216
  /** Keeps the rows while the live page is paused (backend `liveHold`). */
199
- liveHold?: boolean;
217
+ liveHold?: boolean | TableViewLiveHoldOptions;
200
218
  }
201
219
 
202
220
  const props = defineProps<TableViewProps<T>>();
@@ -227,14 +245,10 @@ const {
227
245
  defaultDisplay,
228
246
  } = useTableViewConfig<T>(props);
229
247
 
230
- const { getPreference, setPreference } = usePreferences();
248
+ const tablePreferences = useTablePreferences(componentId, pageId);
231
249
  const { sendComponentEvent } = useComponentEvent(componentId);
232
250
  const { processI18n } = useTranslation();
233
251
 
234
- const getTablePreferenceKey = (suffix: string) => {
235
- return `tables.${componentId}.${pageId}.${suffix}`;
236
- };
237
-
238
252
  const kanbanEligibleColumns = allColumns.filter(isEligibleKanbanColumn);
239
253
  const kanbanGroupByOptions = kanbanEligibleColumns.map((column) => ({
240
254
  label: processI18n(column.header),
@@ -257,8 +271,8 @@ const groupedOptions = optionsForDisplay(GROUPED_DISPLAY_ID) as
257
271
 
258
272
  // Stored under the legacy "viewMode" key (free migration of "table"/"kanban");
259
273
  // clamped to the offered set so a stale/removed id never renders an un-offered display.
260
- const persistedDisplay = getPreference<string>(
261
- getTablePreferenceKey("viewMode"),
274
+ const persistedDisplay = tablePreferences.get<string>(
275
+ "viewMode",
262
276
  defaultDisplay,
263
277
  );
264
278
  // A default display that draws its whole interface (no table chrome, hence no
@@ -335,30 +349,24 @@ const activeDisplayComponent = computed<Component | string | undefined>(() => {
335
349
  return STATIC_DISPLAY_COMPONENTS[id];
336
350
  });
337
351
 
338
- const kanbanGroupBy = ref<string>(
339
- getPreference<string>(
340
- getTablePreferenceKey("kanbanGroupBy"),
341
- kanbanOptions?.groupByField ?? "",
342
- ),
343
- );
352
+ const defaultKanbanGroupBy = kanbanOptions?.groupByField ?? "";
353
+ const kanbanGroupBy = ref<string>(defaultKanbanGroupBy);
344
354
  // A saved preference may point to a column that no longer exists or is no
345
355
  // longer eligible after a backend config change.
346
- if (
347
- kanbanOptions &&
348
- !kanbanEligibleColumns.some((c) => c.accessorKey === kanbanGroupBy.value)
349
- ) {
350
- kanbanGroupBy.value = kanbanOptions.groupByField;
351
- }
356
+ tablePreferences.restore(
357
+ "kanbanGroupBy",
358
+ kanbanGroupBy,
359
+ (field) =>
360
+ !kanbanOptions ||
361
+ kanbanEligibleColumns.some((c) => c.accessorKey === field),
362
+ );
352
363
 
353
364
  const DEFAULT_DENSITY = "default";
354
365
  // The module's density is the default; the user's pick in the ⋯ menu is kept
355
366
  // with the rest of the table's state.
356
- const density = ref<"default" | "compact">(
357
- getPreference(
358
- getTablePreferenceKey("density"),
359
- props.density ?? DEFAULT_DENSITY,
360
- ),
361
- );
367
+ const defaultDensity = props.density ?? DEFAULT_DENSITY;
368
+ const density = ref<"default" | "compact">(defaultDensity);
369
+ tablePreferences.restore("density", density);
362
370
 
363
371
  const DEFAULT_PAGE_SIZE = 10;
364
372
  const DEFAULT_PAGINATION: PaginationState = {
@@ -403,8 +411,7 @@ const routeParamHiddenFilters = computed<TableFilter[]>(() => {
403
411
  // A hidden filter's value changed with the URL, in place or through a
404
412
  // remount: the page reached among the previous rows lists from page 1.
405
413
  const paginationState = usePersistedPagination({
406
- preferences: { getPreference, setPreference },
407
- preferenceKey: getTablePreferenceKey,
414
+ preferences: tablePreferences,
408
415
  defaults: DEFAULT_PAGINATION,
409
416
  scope: () => [queryParamHiddenFilters.value, routeParamHiddenFilters.value],
410
417
  });
@@ -446,18 +453,14 @@ const groupedGroupByOptions = groupedOptions
446
453
  value: column.accessorKey,
447
454
  }))
448
455
  : [];
449
- const groupedGroupBy = ref<string>(
450
- getPreference<string>(
451
- getTablePreferenceKey("groupedGroupBy"),
452
- groupedOptions?.groupByField ?? "",
453
- ),
456
+ const defaultGroupedGroupBy = groupedOptions?.groupByField ?? "";
457
+ const groupedGroupBy = ref<string>(defaultGroupedGroupBy);
458
+ tablePreferences.restore(
459
+ "groupedGroupBy",
460
+ groupedGroupBy,
461
+ (field) =>
462
+ !groupedOptions || groupedGroupByOptions.some((o) => o.value === field),
454
463
  );
455
- if (
456
- groupedOptions &&
457
- !groupedGroupByOptions.some((o) => o.value === groupedGroupBy.value)
458
- ) {
459
- groupedGroupBy.value = groupedOptions.groupByField;
460
- }
461
464
  const activeGroupedOptions = computed<TableViewGroupedConfig | undefined>(
462
465
  () =>
463
466
  groupedOptions &&
@@ -484,19 +487,14 @@ const defaultSortKind = tableDefaultSort
484
487
  )
485
488
  : undefined;
486
489
  const sorting = ref<SortingState>(
487
- sanitizeSorting(
488
- getPreference<SortingState>(
489
- getTablePreferenceKey("sorting"),
490
- defaultSortState,
491
- ),
490
+ sortingOrDefault(
491
+ tablePreferences.get<SortingState>("sorting", defaultSortState),
492
492
  declaredSortableIds,
493
+ defaultSortState,
493
494
  ),
494
495
  );
495
496
 
496
- const savedFilters = getPreference<TableFilter[]>(
497
- getTablePreferenceKey("columnFilters"),
498
- [],
499
- );
497
+ const savedFilters = tablePreferences.get<TableFilter[]>("columnFilters", []);
500
498
 
501
499
  const buildPinnedFilters = (): TableFilter[] =>
502
500
  (defaultFilters || []).map((def) => {
@@ -518,26 +516,17 @@ const initialFilters = [...buildPinnedFilters(), ...manualFilters];
518
516
 
519
517
  const columnFilters = ref<TableFilter[]>(initialFilters);
520
518
  const columnVisibility = ref<VisibilityState>(
521
- getPreference<VisibilityState>(
522
- getTablePreferenceKey("columnVisibility"),
523
- initialVisibility,
524
- ),
525
- );
526
- const columnSizing = ref<ColumnSizingState>(
527
- getPreference<ColumnSizingState>(getTablePreferenceKey("columnSizing"), {}),
528
- );
529
- const columnOrder = ref<string[]>(
530
- getPreference<string[]>(getTablePreferenceKey("columnOrder"), []),
519
+ tablePreferences.get<VisibilityState>("columnVisibility", initialVisibility),
531
520
  );
521
+ const columnSizing = ref<ColumnSizingState>({});
522
+ tablePreferences.restore("columnSizing", columnSizing);
523
+ const columnOrder = ref<string[]>([]);
524
+ tablePreferences.restore("columnOrder", columnOrder, Array.isArray);
532
525
  const hasEmptyDefaultFilter = (defaultFilters || []).some(
533
526
  (f) => f.value === undefined || f.value === null || f.value === "",
534
527
  );
535
- const filtersRowOpen = ref<boolean>(
536
- getPreference<boolean>(
537
- getTablePreferenceKey("filtersOpen"),
538
- hasEmptyDefaultFilter,
539
- ),
540
- );
528
+ const filtersRowOpen = ref<boolean>(hasEmptyDefaultFilter);
529
+ tablePreferences.restore("filtersOpen", filtersRowOpen);
541
530
  const globalFilter = shallowRef<string>();
542
531
  const globalFilterDebounced = refDebounced(
543
532
  globalFilter,
@@ -592,7 +581,7 @@ const {
592
581
  density,
593
582
  pagination: paginationState,
594
583
  },
595
- preferenceKey: getTablePreferenceKey,
584
+ preferences: tablePreferences,
596
585
  urlScope,
597
586
  });
598
587
 
@@ -604,8 +593,7 @@ const TAB_URL_KEY = "tab";
604
593
  const urlTab = readTableUrlKey(useDmsRoute().query, urlScope, TAB_URL_KEY);
605
594
  // A tab named by the URL wins over the one kept from the last visit.
606
595
  const activeTabId = ref<string>(
607
- urlTab.value ??
608
- getPreference<string>(getTablePreferenceKey("activeTab"), ALL_TAB_ID),
596
+ urlTab.value ?? tablePreferences.get<string>("activeTab", ALL_TAB_ID),
609
597
  );
610
598
 
611
599
  interface ResolvedTab {
@@ -674,6 +662,8 @@ const tableRows = useTableRows<T>({
674
662
  routeParams: props.routeParams,
675
663
  routeQuery: route.query as Record<string, unknown>,
676
664
  }),
665
+ // Groups keep their rows together: the grouped display pins nothing.
666
+ pinnedRows: () => (isGroupedDisplay.value ? undefined : props.pinnedRows),
677
667
  });
678
668
  // Quick filters: dropdowns of the toolbar over a column's values (a select's
679
669
  // items, a boolean, the rows a relation points to), writing that column's
@@ -819,12 +809,26 @@ const archiveQuery = computed(() =>
819
809
  archiveMode ? { showArchived: showArchived.value } : {},
820
810
  );
821
811
 
812
+ // The row a side panel has open, which the grid highlights.
813
+ const rowSidePanel = useRowSidePanelContent();
814
+ const rowSidePanelOwner = rowSidePanelOwnerKey(pageId!, componentId!);
815
+ const openRowId = computed(() =>
816
+ rowSidePanel.value?.ownerKey === rowSidePanelOwner
817
+ ? rowSidePanel.value.rowKey
818
+ : undefined,
819
+ );
820
+
822
821
  // The gate is set up with the other refresh triggers below; the hold only
823
822
  // reads it once the table is on screen.
823
+ const tableRoot = useTemplateRef<ComponentPublicInstance>("tableRoot");
824
824
  const liveHold = useTableLiveHold({
825
- isEnabled: !!props.liveHold,
825
+ liveHold: props.liveHold,
826
826
  isSource: tableRows.isSource,
827
827
  gate: () => liveGate,
828
+ signals: {
829
+ isRowOpen: () => openRowId.value !== undefined,
830
+ tableElement: () => tableRoot.value?.$el ?? null,
831
+ },
828
832
  });
829
833
 
830
834
  const { data, status, error, refresh } = await useTableListData<T>({
@@ -878,18 +882,27 @@ watch(listLoadError, (message) => {
878
882
  });
879
883
 
880
884
  // Whatever sets the sort (a header, the sort menu, a pasted table config), the
881
- // route only ever receives a sort it accepts. A new order lists from page 1:
885
+ // route only ever receives a sort it accepts, and a cleared sort goes back to
886
+ // the default one rather than being remembered empty. A new order lists from page 1:
882
887
  // the page reached in the previous order shows unrelated rows in the new one.
883
888
  // Synchronous, so the list is queried once, with both changes.
884
889
  watch(
885
890
  sorting,
886
891
  (next, previous) => {
887
- const sanitized = sanitizeSorting(next, sortableIds.value);
892
+ const sanitized = sortingOrDefault(
893
+ next,
894
+ sortableIds.value,
895
+ defaultSortState,
896
+ );
888
897
  if (!isSameSorting(sanitized, next)) {
889
898
  sorting.value = sanitized;
890
899
  return;
891
900
  }
892
- const before = sanitizeSorting(previous, sortableIds.value);
901
+ const before = sortingOrDefault(
902
+ previous,
903
+ sortableIds.value,
904
+ defaultSortState,
905
+ );
893
906
  if (
894
907
  !isSameSorting(sanitized, before) &&
895
908
  paginationState.value.pageIndex !== 0
@@ -1235,6 +1248,7 @@ const {
1235
1248
  handleCustomButton,
1236
1249
  handleCustomRowAction,
1237
1250
  handleBulkCustomAction,
1251
+ handleMissingRow,
1238
1252
  runConfirmedBuiltIn,
1239
1253
  } = useTableRowActions<T>({
1240
1254
  api: $authFetch,
@@ -1665,25 +1679,17 @@ const openRecordFromUrl = async () => {
1665
1679
  const id = deepLinkAction ? readRecordId(route.query, urlScope) : undefined;
1666
1680
  if (!deepLinkAction || !id) return;
1667
1681
  await until(isListLoading).toBe(false);
1668
- const row = await resolveDeepLinkedRow(id, {
1682
+ const { row, isMissing } = await resolveDeepLinkedRow(id, {
1669
1683
  listed: shownResults.value ?? undefined,
1670
1684
  idOf: getRowId,
1671
1685
  getRow: tableRows.getRow,
1672
1686
  });
1673
1687
  if (row) handleCustomRowAction(deepLinkAction, row as Data);
1688
+ else if (isMissing) handleMissingRow(deepLinkAction, id);
1674
1689
  };
1675
1690
 
1676
1691
  const selectedIds = computed(() => selectedRowIds(rowSelect.value));
1677
1692
 
1678
- // The row a side panel has open, which the grid highlights.
1679
- const rowSidePanel = useRowSidePanelContent();
1680
- const rowSidePanelOwner = rowSidePanelOwnerKey(pageId!, componentId!);
1681
- const openRowId = computed(() =>
1682
- rowSidePanel.value?.ownerKey === rowSidePanelOwner
1683
- ? rowSidePanel.value.rowKey
1684
- : undefined,
1685
- );
1686
-
1687
1693
  const isRowActionEnabled = (
1688
1694
  config: boolean | RowActionConfig | undefined,
1689
1695
  item: T,
@@ -1778,6 +1784,11 @@ const displayRowActions = {
1778
1784
  isRowActionEnabled(tableProps.value.rowActions?.edit, item),
1779
1785
  canDeleteRow: (item: T) =>
1780
1786
  isRowActionEnabled(tableProps.value.rowActions?.delete, item),
1787
+ customFor: (item: T) =>
1788
+ rowCustomActions(
1789
+ tableProps.value.rowActions?.custom,
1790
+ item as Record<string, unknown>,
1791
+ ),
1781
1792
  };
1782
1793
 
1783
1794
  // The TanStack instance is only available as a #body slot prop; captured here so
@@ -1914,24 +1925,25 @@ defineShortcuts(
1914
1925
  }),
1915
1926
  );
1916
1927
 
1917
- const tableStatePreferences = {
1918
- sorting,
1919
- columnFilters: columnFilters,
1920
- columnVisibility,
1921
- filtersOpen: filtersRowOpen,
1922
- activeTab: activeTabId,
1923
- viewMode: activeDisplayId,
1924
- columnOrder,
1925
- kanbanGroupBy,
1926
- groupedGroupBy,
1927
- density,
1928
- } as const;
1929
-
1930
- Object.entries(tableStatePreferences).forEach(([key, ref]) => {
1928
+ // Each kept field with its default: a value back to it is forgotten.
1929
+ const tableStatePreferences: TableStatePreference[] = [
1930
+ ["sorting", sorting, defaultSortState],
1931
+ ["columnFilters", columnFilters, []],
1932
+ ["columnVisibility", columnVisibility, initialVisibility],
1933
+ ["filtersOpen", filtersRowOpen, hasEmptyDefaultFilter],
1934
+ ["activeTab", activeTabId, ALL_TAB_ID],
1935
+ ["viewMode", activeDisplayId, defaultDisplay],
1936
+ ["columnOrder", columnOrder, []],
1937
+ ["kanbanGroupBy", kanbanGroupBy, defaultKanbanGroupBy],
1938
+ ["groupedGroupBy", groupedGroupBy, defaultGroupedGroupBy],
1939
+ ["density", density, defaultDensity],
1940
+ ];
1941
+
1942
+ tableStatePreferences.forEach(([field, state, defaultValue]) => {
1931
1943
  watch(
1932
- ref,
1944
+ state,
1933
1945
  (newValue) => {
1934
- setPreference(getTablePreferenceKey(key), newValue);
1946
+ tablePreferences.set(field, newValue, defaultValue);
1935
1947
  },
1936
1948
  { deep: true },
1937
1949
  );
@@ -1943,7 +1955,7 @@ const COLUMN_SIZING_PERSIST_DEBOUNCE_MS = 500;
1943
1955
  let pendingColumnSizingWrite = false;
1944
1956
 
1945
1957
  const persistColumnSizing = () => {
1946
- setPreference(getTablePreferenceKey("columnSizing"), columnSizing.value);
1958
+ tablePreferences.set("columnSizing", columnSizing.value, {});
1947
1959
  pendingColumnSizingWrite = false;
1948
1960
  };
1949
1961
 
@@ -2022,6 +2034,7 @@ onMounted(() => {
2022
2034
 
2023
2035
  <template>
2024
2036
  <UTable
2037
+ ref="tableRoot"
2025
2038
  v-bind="tableProps"
2026
2039
  v-model:pagination="paginationState"
2027
2040
  v-model:global-filter="globalFilter"
@@ -2045,6 +2058,7 @@ onMounted(() => {
2045
2058
  :reorder="reorderState"
2046
2059
  :views-placement="viewsLayout === 'menu' ? 'header' : 'band'"
2047
2060
  :search-placeholder="props.searchPlaceholder"
2061
+ :search-shortcut="props.searchShortcut"
2048
2062
  :quick-filters="resolvedQuickFilters"
2049
2063
  :footer="resolvedFooter"
2050
2064
  :empty-states="props.emptyStates"
@@ -22,6 +22,7 @@ import type {
22
22
  ValuePrecision,
23
23
  } from "../../composables/chart/types";
24
24
  import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
25
+ import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
25
26
 
26
27
  interface Props extends DefaultComponentProps {
27
28
  title: string;
@@ -32,6 +33,8 @@ interface Props extends DefaultComponentProps {
32
33
  valueFormat?: ValueFormat;
33
34
  currencyCode?: string;
34
35
  valuePrecision?: ValuePrecision;
36
+ /** Unit after each formatted value, space included (" ms"); `$` key or literal. */
37
+ valueSuffix?: string;
35
38
  showRank?: boolean;
36
39
  highlightTopN?: number;
37
40
  rankColor?: UiColor;
@@ -100,6 +103,7 @@ const props = withDefaults(defineProps<Props>(), {
100
103
 
101
104
  const { locale, t } = useI18n();
102
105
  const { processI18n } = useTranslation();
106
+ const { processText } = useComposedText();
103
107
  useComponentEvent(props.componentId);
104
108
  const { state: watchState } = useWatch(
105
109
  props.watchActions || [],
@@ -167,14 +171,20 @@ function barClass(index: number): string {
167
171
  return `${rankFillClass.value} ${QUIET_BAR_OPACITY_CLASS}`;
168
172
  }
169
173
 
174
+ const valueSuffixText = computed(() =>
175
+ props.valueSuffix ? processI18n(props.valueSuffix) : "",
176
+ );
177
+
170
178
  function formattedValue(item: TopListItem): string {
171
- return formatValue(
179
+ if (item.valueLabel) return processText(item.valueLabel);
180
+ const formatted = formatValue(
172
181
  item.value,
173
182
  props.valueFormat,
174
183
  locale.value,
175
184
  props.currencyCode,
176
185
  props.valuePrecision,
177
186
  );
187
+ return `${formatted}${valueSuffixText.value}`;
178
188
  }
179
189
 
180
190
  function sparklineAccentFor(item: TopListItem): string {
@@ -106,6 +106,8 @@ export interface TopListItem {
106
106
  title: BlockText;
107
107
  description?: BlockText;
108
108
  value: number;
109
+ /** The value as written, in place of the formatted one. */
110
+ valueLabel?: BlockText;
109
111
  delta?: number | null;
110
112
  sparkline?: number[];
111
113
  icon?: string;
@@ -16,6 +16,7 @@ import {
16
16
  resolveUrlVariables,
17
17
  } from "../../build/utils/urlVariables";
18
18
  import { useRefreshTriggers } from "../../build/composables/blocks/useRefreshTriggers";
19
+ import { readFetchNotFound } from "../../build/utils/fetchNotFound";
19
20
 
20
21
  export interface UseChartFetchOptions<T> {
21
22
  /**
@@ -41,6 +42,10 @@ export interface UseChartFetchReturn<T> {
41
42
  * (period, watched state, realtime event) until the new answer lands.
42
43
  */
43
44
  isLoading: Ref<boolean>;
45
+ /**
46
+ * Why the last request failed. A 404 also clears `data`: read it with
47
+ * `readFetchNotFound` to draw a not-found state rather than a load error.
48
+ */
44
49
  error: Ref<unknown>;
45
50
  refresh: () => Promise<void>;
46
51
  }
@@ -111,7 +116,12 @@ async function runFetch<T>(
111
116
  data.value = response;
112
117
  } catch (err) {
113
118
  if (runner.activeAbort !== controller) return;
114
- if ((err as { name?: string })?.name !== "AbortError") error.value = err;
119
+ if ((err as { name?: string })?.name === "AbortError") return;
120
+ error.value = err;
121
+ // A 404 says what the block showed is gone (another tab deleted it, the
122
+ // URL now names another record): its last values must not stay on screen
123
+ // as if they still belonged to it. Any other failure keeps them.
124
+ if (readFetchNotFound(err)) data.value = null;
115
125
  } finally {
116
126
  // A request a newer one superseded leaves the loading flag to that one:
117
127
  // clearing it here would show the empty state while the newer answer is
@@ -24,7 +24,14 @@ export type ConfirmValues = Record<string, unknown>;
24
24
  * whose texts are already resolved, plus what only a caller in the browser
25
25
  * can give (render functions, handlers).
26
26
  */
27
- export interface ConfirmOptions extends Omit<ConfirmDialog, "params" | "icon"> {
27
+ export interface ConfirmOptions
28
+ extends Omit<ConfirmDialog, "params" | "icon" | "secondaryAction"> {
29
+ /**
30
+ * A button next to cancel and confirm that leaves the dialog open (a
31
+ * download before removing): its `run` is awaited, the button loading,
32
+ * and a rejection is shown in the dialog's alert.
33
+ */
34
+ secondaryAction?: ConfirmSecondaryAction;
28
35
  /**
29
36
  * The button focused on open (`"cancel"` for a dialog whose safe answer
30
37
  * is to stay); left out, the dialog's first focusable element.
@@ -63,6 +70,13 @@ export interface ConfirmOptions extends Omit<ConfirmDialog, "params" | "icon"> {
63
70
  ) => Promise<void | boolean | ConfirmPartialOutcome>;
64
71
  }
65
72
 
73
+ /** A secondary button of the confirm modal: its text resolved, its handler. */
74
+ export interface ConfirmSecondaryAction {
75
+ label: string;
76
+ icon?: string;
77
+ run: () => Promise<void> | void;
78
+ }
79
+
66
80
  /** A summary shown in the confirm modal's alert: a title and its detail. */
67
81
  export interface ConfirmNotice {
68
82
  title: string;