@antelopejs/dms 0.7.9 → 0.7.11

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 (173) hide show
  1. package/CHANGELOG.md +41 -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/unit/api-base-url.test.d.ts +1 -0
  15. package/dist/test/unit/api-base-url.test.js +31 -0
  16. package/dist/test/unit/api-base-url.test.js.map +1 -0
  17. package/dist/test/unit/interfaces/dms/page-layout-query.test.d.ts +1 -0
  18. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +106 -0
  19. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -0
  20. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -0
  21. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  22. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.d.ts +1 -0
  23. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js +46 -0
  24. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js.map +1 -0
  25. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.d.ts +1 -0
  26. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +26 -0
  27. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -0
  28. package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +10 -0
  29. package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
  30. package/dist/test/unit/interfaces/dms-base/guard.test.d.ts +1 -0
  31. package/dist/test/unit/interfaces/dms-base/guard.test.js +53 -0
  32. package/dist/test/unit/interfaces/dms-base/guard.test.js.map +1 -0
  33. package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js +23 -0
  34. package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js.map +1 -1
  35. package/dist/test/unit/interfaces/dms-base/page-live.test.js +55 -0
  36. package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -1
  37. package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js +17 -0
  38. package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js.map +1 -1
  39. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +2 -0
  40. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
  41. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +28 -0
  42. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  43. package/frontend-vue/layers/dms-core/app/build/composables/user/preferencesCookie.ts +149 -0
  44. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +321 -0
  45. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +4 -3
  46. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +4 -3
  47. package/frontend-vue/layers/dms-core/app/composables/period/types.ts +3 -1
  48. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +2 -2
  49. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +9 -2
  50. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +48 -16
  51. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +21 -4
  52. package/frontend-vue/layers/dms-core/app/composables/user/usePreferences.ts +81 -49
  53. package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +35 -0
  54. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
  55. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
  56. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +10 -1
  57. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +32 -1
  58. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +13 -3
  59. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +27 -3
  60. package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +16 -0
  61. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +75 -0
  62. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +8 -1
  63. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +13 -2
  64. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +7 -0
  65. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +7 -0
  66. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -4
  67. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +59 -0
  68. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
  69. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
  70. package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +2 -0
  71. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockInlineStatus.vue +67 -0
  72. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +60 -20
  73. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +15 -8
  74. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +14 -0
  75. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +29 -4
  76. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +76 -28
  77. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +26 -2
  78. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useGuard.ts +115 -0
  79. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +1 -1
  80. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +58 -3
  81. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/secondaryTarget.ts +86 -0
  82. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +5 -0
  83. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +6 -0
  84. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveCadence.ts +42 -0
  85. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveHolds.ts +66 -0
  86. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +42 -0
  87. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveStorage.ts +42 -0
  88. package/frontend-vue/layers/dms-ui/app/build/composables/live/missedTally.ts +65 -0
  89. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +247 -88
  90. package/frontend-vue/layers/dms-ui/app/build/composables/live/useLivePeriodSpan.ts +33 -0
  91. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +79 -5
  92. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparisonControl.ts +70 -0
  93. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +55 -0
  94. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +41 -5
  95. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +22 -9
  96. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +13 -6
  97. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +1 -0
  98. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +27 -2
  99. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +18 -3
  100. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +19 -26
  101. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +66 -0
  102. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +7 -1
  103. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +65 -11
  104. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTablePreferences.ts +149 -0
  105. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +39 -8
  106. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
  107. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +16 -11
  108. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +39 -16
  109. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +29 -0
  110. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +18 -5
  111. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/sortableColumns.ts +17 -0
  112. package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +79 -0
  113. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +46 -1
  114. package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +4 -0
  115. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +40 -2
  116. package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +11 -1
  117. package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +12 -2
  118. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +129 -4
  119. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +6 -0
  120. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +6 -1
  121. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +33 -0
  122. package/frontend-vue/layers/dms-ui/app/components/form/components/ChoiceCards.vue +22 -4
  123. package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +75 -0
  124. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -17
  125. package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +15 -0
  126. package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +12 -15
  127. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +104 -16
  128. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +104 -98
  129. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +11 -1
  130. package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +2 -0
  131. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +11 -1
  132. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +15 -1
  133. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +86 -10
  134. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewShiftF.ts +20 -0
  135. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +12 -0
  136. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -0
  137. package/frontend-vue/layers/dms-ui/app/composables/table-view/useTableViewEmptyState.ts +55 -0
  138. package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
  139. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +13 -4
  140. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +12 -3
  141. package/frontend-vue/tests/activity-feed-fill-height.test.ts +1 -0
  142. package/frontend-vue/tests/activity-feed-head.test.ts +140 -0
  143. package/frontend-vue/tests/block-not-found.test.ts +220 -0
  144. package/frontend-vue/tests/choice-cards-columns.test.ts +60 -0
  145. package/frontend-vue/tests/confirm-secondary-action.test.ts +269 -0
  146. package/frontend-vue/tests/guard.test.ts +242 -0
  147. package/frontend-vue/tests/header-action-page-query.test.ts +286 -0
  148. package/frontend-vue/tests/live-toggle.test.ts +87 -1
  149. package/frontend-vue/tests/page-layout-registration.test.ts +1 -0
  150. package/frontend-vue/tests/page-live-parity.test.ts +367 -0
  151. package/frontend-vue/tests/page-realtime-remount.test.ts +185 -0
  152. package/frontend-vue/tests/period-selector-parity.test.ts +138 -0
  153. package/frontend-vue/tests/period-selector-url.test.ts +84 -6
  154. package/frontend-vue/tests/period-url-query.test.ts +52 -0
  155. package/frontend-vue/tests/preferences-cookie-size.test.ts +311 -0
  156. package/frontend-vue/tests/row-side-panel.test.ts +109 -10
  157. package/frontend-vue/tests/tab-slot-spacing.test.ts +8 -0
  158. package/frontend-vue/tests/table-actions-search-label.test.ts +57 -1
  159. package/frontend-vue/tests/table-clear-filters.test.ts +99 -1
  160. package/frontend-vue/tests/table-density-menu.test.ts +4 -3
  161. package/frontend-vue/tests/table-live-hold.test.ts +95 -5
  162. package/frontend-vue/tests/table-row-action-labels.test.ts +181 -0
  163. package/frontend-vue/tests/table-source-period-scope.test.ts +31 -3
  164. package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +14 -17
  165. package/frontend-vue/tests/table-view-pinned-rows.test.ts +143 -0
  166. package/frontend-vue/tests/table-view-sortable-columns.test.ts +45 -0
  167. package/frontend-vue/tests/table-view-views.test.ts +56 -15
  168. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +107 -0
  169. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  170. package/package.json +2 -2
  171. package/skills/dms/SKILL.md +9 -3
  172. package/skills/dms-module/SKILL.md +5 -0
  173. package/skills/dms-pages/REFERENCE.md +23 -2
@@ -21,6 +21,17 @@ export const ROW_SIDE_PANEL_ID = "dms:row-side-panel";
21
21
  /** Registered name of `RowSidePanel.vue` (the layer's `Dms` prefix applied). */
22
22
  const ROW_SIDE_PANEL_COMPONENT = "DmsRowSidePanel";
23
23
 
24
+ /**
25
+ * The state the panel draws in place of the component on a row that is gone,
26
+ * its texts translated.
27
+ *
28
+ * @internal
29
+ */
30
+ export interface RowSidePanelNotFound {
31
+ title: string;
32
+ description: string;
33
+ }
34
+
24
35
  /**
25
36
  * What the row side panel shows: the header the DMS draws, the module's
26
37
  * component and the row it receives.
@@ -32,7 +43,12 @@ export interface RowSidePanelContent {
32
43
  session: symbol;
33
44
  /** The table (or button bar) that opened the panel; see {@link rowSidePanelOwnerKey}. */
34
45
  ownerKey: string;
46
+ /** The panel's title: its accessible name, and its header without a row. */
35
47
  title: string;
48
+ /** The header's title while `row` is open (the target's `rowTitle`). */
49
+ rowTitle?: string;
50
+ /** The row the link names is gone: this state replaces the component. */
51
+ notFound?: RowSidePanelNotFound;
36
52
  /** i18n key (`$`-prefixed) of the position, receiving `{ index, total }`. */
37
53
  positionLabel?: string;
38
54
  component: Component;
@@ -0,0 +1,75 @@
1
+ import { computed, shallowRef, watch, type ComputedRef } from "vue";
2
+ import type { LayoutHeaderAction } from "../../build/components/layout/PageHeaderActionBar.vue";
3
+ import type { PageLayout } from "../../types/page";
4
+ import {
5
+ isSamePageQuery,
6
+ layoutUrlWithQuery,
7
+ pageQueryValues,
8
+ } from "../../utils/pageQuery";
9
+
10
+ interface ServedHeaderActionsOptions {
11
+ /** The buttons the page layout was served with. */
12
+ actions: () => LayoutHeaderAction[];
13
+ /**
14
+ * The page query their availability was resolved for, set when one of them
15
+ * declares an `availability` (`headerActionsQuery`).
16
+ */
17
+ servedQuery: () => Record<string, string> | undefined;
18
+ }
19
+
20
+ /**
21
+ * The page header's buttons for the page query of the moment. A button whose
22
+ * `availability` depends on the query (`?workspace=` names the record the
23
+ * page acts on) was resolved for the query the layout was loaded with: when
24
+ * the URL's query moves away from it, the header asks the page layout again
25
+ * for that query and takes its buttons, keeping the last ones meanwhile.
26
+ * Buttons without an availability never cause a request.
27
+ */
28
+ export function useServedHeaderActions({
29
+ actions,
30
+ servedQuery,
31
+ }: ServedHeaderActionsOptions): ComputedRef<LayoutHeaderAction[]> {
32
+ const route = useDmsRoute();
33
+ const siteLayout = useSiteLayout();
34
+ const refreshed = shallowRef<LayoutHeaderAction[] | null>(null);
35
+ let latestRequest = 0;
36
+
37
+ const layoutUrl = () =>
38
+ siteLayout.findMatchingRoute(route.path)?.metadata?.layoutUrl;
39
+
40
+ async function askFor(url: string, query: Record<string, string>) {
41
+ const request = ++latestRequest;
42
+ const { $authFetch } = useAuthFetch();
43
+ try {
44
+ const layout = await $authFetch<PageLayout>(
45
+ layoutUrlWithQuery(url, query),
46
+ );
47
+ if (request !== latestRequest) return;
48
+ const options = layout.layout.options as
49
+ | { headerActions?: LayoutHeaderAction[] }
50
+ | undefined;
51
+ refreshed.value = options?.headerActions ?? [];
52
+ } catch {
53
+ // The buttons resolved for the previous query stay: the operation
54
+ // behind each one still refuses on its own.
55
+ }
56
+ }
57
+
58
+ function follow() {
59
+ const served = servedQuery();
60
+ const current = pageQueryValues(route.query as Record<string, unknown>);
61
+ const url = layoutUrl();
62
+ if (!served || !url || isSamePageQuery(served, current)) {
63
+ latestRequest++;
64
+ refreshed.value = null;
65
+ return;
66
+ }
67
+ void askFor(url, current);
68
+ }
69
+
70
+ if (!import.meta.env.SSR) {
71
+ watch([() => route.query, servedQuery], follow, { immediate: true });
72
+ }
73
+
74
+ return computed(() => refreshed.value ?? actions());
75
+ }
@@ -1,3 +1,5 @@
1
+ import { pageQueryValues } from "../../utils/pageQuery";
2
+
1
3
  /**
2
4
  * Reads the same page cache that Inertia hydrates before navigation. Missing
3
5
  * layouts (including newly registered dev pages) use the shared deduped loader.
@@ -17,7 +19,12 @@ export async function usePageLayout() {
17
19
  );
18
20
  const load = async () => {
19
21
  if (layoutUrl.value && !pageLayout.value) {
20
- await siteLayout.loadPageLayout(layoutUrl.value);
22
+ // Asked with the page query, for the header buttons whose availability
23
+ // depends on it; cached for the page whatever its query.
24
+ await siteLayout.loadPageLayout(
25
+ layoutUrl.value,
26
+ pageQueryValues(route.query),
27
+ );
21
28
  }
22
29
  };
23
30
  watch([layoutUrl, () => siteLayout.pageLayouts.value], load);
@@ -1,3 +1,4 @@
1
+ import { layoutUrlWithQuery } from "../../utils/pageQuery";
1
2
  import { withoutTrailingSlash } from "ufo";
2
3
 
3
4
  const STALE_THRESHOLD_MS = 300_000;
@@ -282,7 +283,15 @@ export const useSiteLayout = () => {
282
283
  await refresh();
283
284
  };
284
285
 
285
- const loadPageLayout = async (slug: string): Promise<PageLayout> => {
286
+ /**
287
+ * The layout of a page, cached by its layout URL. `query` is the page
288
+ * URL's, which the first request carries for the header buttons whose
289
+ * availability reads it (the header asks again for another query).
290
+ */
291
+ const loadPageLayout = async (
292
+ slug: string,
293
+ query?: Record<string, string>,
294
+ ): Promise<PageLayout> => {
286
295
  if (pageLayouts.value[slug]) {
287
296
  return pageLayouts.value[slug];
288
297
  }
@@ -294,7 +303,9 @@ export const useSiteLayout = () => {
294
303
  }
295
304
 
296
305
  const { $authFetch } = useAuthFetch();
297
- const request = $authFetch<PageLayout>(slug).then((result) => {
306
+ const request = $authFetch<PageLayout>(
307
+ layoutUrlWithQuery(slug, query ?? {}),
308
+ ).then((result) => {
298
309
  if (pageLayouts.value === cache) cache[slug] = result;
299
310
  return result;
300
311
  });
@@ -12,6 +12,11 @@ interface Props {
12
12
  description?: string;
13
13
  /** Header buttons declared by the backend page (`headerActions`). */
14
14
  headerActions?: LayoutHeaderAction[];
15
+ /**
16
+ * The page query the server resolved the buttons' availability for, set
17
+ * when one declares an `availability` (`headerActionsQuery`).
18
+ */
19
+ headerActionsQuery?: Record<string, string>;
15
20
  }
16
21
 
17
22
  const props = withDefaults(defineProps<Props>(), {
@@ -22,6 +27,7 @@ const props = withDefaults(defineProps<Props>(), {
22
27
  title: undefined,
23
28
  description: undefined,
24
29
  headerActions: () => [],
30
+ headerActionsQuery: undefined,
25
31
  });
26
32
  </script>
27
33
 
@@ -33,6 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
33
39
  :title="props.title"
34
40
  :description="props.description"
35
41
  :header-actions="props.headerActions"
42
+ :header-actions-query="props.headerActionsQuery"
36
43
  >
37
44
  <slot />
38
45
  </DashboardPageBody>
@@ -12,6 +12,11 @@ interface Props {
12
12
  description?: string;
13
13
  /** Header buttons declared by the backend page (`headerActions`). */
14
14
  headerActions?: LayoutHeaderAction[];
15
+ /**
16
+ * The page query the server resolved the buttons' availability for, set
17
+ * when one declares an `availability` (`headerActionsQuery`).
18
+ */
19
+ headerActionsQuery?: Record<string, string>;
15
20
  }
16
21
 
17
22
  const props = withDefaults(defineProps<Props>(), {
@@ -21,6 +26,7 @@ const props = withDefaults(defineProps<Props>(), {
21
26
  title: undefined,
22
27
  description: undefined,
23
28
  headerActions: () => [],
29
+ headerActionsQuery: undefined,
24
30
  });
25
31
  </script>
26
32
 
@@ -33,6 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
33
39
  :title="props.title"
34
40
  :description="props.description"
35
41
  :header-actions="props.headerActions"
42
+ :header-actions-query="props.headerActionsQuery"
36
43
  header-class="pb-5"
37
44
  >
38
45
  <slot />
@@ -218,10 +218,13 @@ const realtimePageId = computed(() => pagelayoutMetadata.value?.fullId ?? "");
218
218
  const realtime = usePageRealtime(realtimePageId);
219
219
  providePageRealtime(realtime);
220
220
  // Before the blocks set up: their refetches go through the page's live gate.
221
- usePageLiveHost(() => ({
222
- pageId: pagelayoutMetadata.value?.fullId,
223
- live: pagelayoutMetadata.value?.live,
224
- }));
221
+ usePageLiveHost(
222
+ () => ({
223
+ pageId: pagelayoutMetadata.value?.fullId,
224
+ live: pagelayoutMetadata.value?.live,
225
+ }),
226
+ { isStreamConnected: () => !!realtime.sessionId.value },
227
+ );
225
228
 
226
229
  function buildPageSetupContext(
227
230
  pageInfo: PageInfo,
@@ -0,0 +1,59 @@
1
+ // The page URL's query as the server reads it for header-button availability.
2
+
3
+ /** The page query as the server reads it: one value per name, the last one. */
4
+ export function pageQueryValues(
5
+ query: Record<string, unknown> | undefined,
6
+ ): Record<string, string> {
7
+ return Object.fromEntries(
8
+ Object.entries(query ?? {}).flatMap(([name, value]) => {
9
+ const last = Array.isArray(value) ? value.at(-1) : value;
10
+ return typeof last === "string" ? [[name, last]] : [];
11
+ }),
12
+ );
13
+ }
14
+
15
+ // Query keys the DMS writes for the view itself, never for the record a page
16
+ // acts on: the row open in a side panel, the paused live mode, and a period
17
+ // selector's range (`<id>.period`, `<id>.compare`, `<id>.from`…).
18
+ const VIEW_STATE_KEYS = new Set(["record", "live"]);
19
+ const PERIOD_KEY = /\.(?:period|compare|from|to|compareFrom|compareTo)$/;
20
+
21
+ /** Whether a query key only records the view's own state. */
22
+ export function isViewStateKey(name: string): boolean {
23
+ return VIEW_STATE_KEYS.has(name) || PERIOD_KEY.test(name);
24
+ }
25
+
26
+ function withoutViewState(
27
+ query: Record<string, string>,
28
+ ): Record<string, string> {
29
+ return Object.fromEntries(
30
+ Object.entries(query).filter(([name]) => !isViewStateKey(name)),
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Whether two page queries hold the same values, in any order, leaving out
36
+ * the keys that only record the view's state: moving through rows, pausing
37
+ * the live mode or changing a period never asks the layout again.
38
+ */
39
+ export function isSamePageQuery(
40
+ left: Record<string, string>,
41
+ right: Record<string, string>,
42
+ ): boolean {
43
+ const kept = withoutViewState(left);
44
+ const other = withoutViewState(right);
45
+ const names = Object.keys(kept);
46
+ return (
47
+ names.length === Object.keys(other).length &&
48
+ names.every((name) => kept[name] === other[name])
49
+ );
50
+ }
51
+
52
+ /** The layout URL of a page, asked for the given query. */
53
+ export function layoutUrlWithQuery(
54
+ layoutUrl: string,
55
+ query: Record<string, string>,
56
+ ): string {
57
+ const search = new URLSearchParams(query).toString();
58
+ return search ? `${layoutUrl}?${search}` : layoutUrl;
59
+ }
@@ -899,6 +899,10 @@
899
899
  "position": "{index} of {total}",
900
900
  "previous": "Previous",
901
901
  "next": "Next",
902
- "close": "Close"
902
+ "close": "Close",
903
+ "not_found": {
904
+ "title": "This entry is no longer available",
905
+ "description": "It was deleted, or the link is wrong."
906
+ }
903
907
  }
904
908
  }
@@ -903,6 +903,10 @@
903
903
  "position": "{index} sur {total}",
904
904
  "previous": "Précédent",
905
905
  "next": "Suivant",
906
- "close": "Fermer"
906
+ "close": "Fermer",
907
+ "not_found": {
908
+ "title": "Cette entrée n'est plus disponible",
909
+ "description": "Elle a été supprimée, ou le lien est erroné."
910
+ }
907
911
  }
908
912
  }
@@ -18,6 +18,8 @@ export interface ActivityFeedItem {
18
18
  title: BlockText;
19
19
  /** Dimmed details, joined by "·". */
20
20
  meta?: BlockText[];
21
+ /** Third line, dimmed and mono (the entry's evidence). */
22
+ detail?: BlockText;
21
23
  /**
22
24
  * Values the `$` title and details interpolate: typed values are formatted
23
25
  * as in a composed text; a `$`-prefixed string is translated first.
@@ -0,0 +1,67 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import {
4
+ type FetchNotFound,
5
+ NOT_FOUND_TITLE_KEY,
6
+ } from "../../utils/fetchNotFound";
7
+
8
+ // The one-line status a compact block (a KPI card, a meter) shows in place of
9
+ // its value: a first load that failed, with a retry, or a source that
10
+ // answered 404 — nothing to retry there, the value does not exist.
11
+ interface BlockInlineStatusProps {
12
+ state: "error" | "not-found";
13
+ /** What the 404 said; its `emptyState.title` replaces the default text. */
14
+ notFound?: FetchNotFound | null;
15
+ }
16
+
17
+ interface BlockInlineStatusEmits {
18
+ (e: "retry"): void;
19
+ }
20
+
21
+ const props = withDefaults(defineProps<BlockInlineStatusProps>(), {
22
+ notFound: undefined,
23
+ });
24
+ const emit = defineEmits<BlockInlineStatusEmits>();
25
+
26
+ const NOT_FOUND_ICON = "i-ph-magnifying-glass";
27
+ const ERROR_ICON = "i-ph-warning-circle";
28
+
29
+ const { t } = useI18n();
30
+ const { processI18n } = useTranslation();
31
+
32
+ const isError = computed(() => props.state === "error");
33
+ const label = computed(() => {
34
+ if (isError.value) return t("dms.table.load_error_title");
35
+ const title = props.notFound?.emptyState?.title;
36
+ return title ? processI18n(title) : t(NOT_FOUND_TITLE_KEY);
37
+ });
38
+ const icon = computed(() =>
39
+ isError.value
40
+ ? ERROR_ICON
41
+ : (props.notFound?.emptyState?.icon ?? NOT_FOUND_ICON),
42
+ );
43
+ </script>
44
+
45
+ <template>
46
+ <div
47
+ class="flex items-center justify-between gap-2"
48
+ :role="isError ? 'alert' : 'status'"
49
+ >
50
+ <p
51
+ class="flex min-w-0 items-center gap-1.5 text-[12.5px]"
52
+ :class="isError ? 'text-error' : 'text-muted'"
53
+ >
54
+ <UIcon :name="icon" class="size-4 shrink-0" />
55
+ <span class="truncate">{{ label }}</span>
56
+ </p>
57
+ <UButton
58
+ v-if="isError"
59
+ :label="t('dms.table.load_error_retry')"
60
+ icon="i-ph-arrows-clockwise"
61
+ color="neutral"
62
+ variant="outline"
63
+ size="xs"
64
+ @click="emit('retry')"
65
+ />
66
+ </div>
67
+ </template>
@@ -1,5 +1,8 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from "vue";
2
3
  import DmsEmptyState from "../../../components/empty-state/EmptyState.vue";
4
+ import DmsBlockActions from "./BlockActions.vue";
5
+ import { type FetchNotFound, notFoundTexts } from "../../utils/fetchNotFound";
3
6
 
4
7
  /** What a block shows when it has nothing to list (interface-dms `BlockEmptyText`). */
5
8
  export interface BlockEmptyText {
@@ -8,14 +11,22 @@ export interface BlockEmptyText {
8
11
  description?: string;
9
12
  }
10
13
 
11
- // What a data-driven block shows instead of its content: an empty list, or a
12
- // first load that failed (with a retry). Compact v2 .c-empty in a card, so it
13
- // keeps the footprint of the block it stands in for.
14
+ /**
15
+ * `empty`: nothing to show. `error`: the first load failed. `not-found`: the
16
+ * source answered 404, what the block shows does not exist.
17
+ */
18
+ export type BlockStatusState = "empty" | "error" | "not-found";
19
+
20
+ // What a data-driven block shows instead of its content: an empty list, a
21
+ // first load that failed (with a retry), or a source that answered 404.
22
+ // Compact v2 .c-empty in a card, so it keeps the footprint of the block it
23
+ // stands in for.
14
24
  interface BlockStatusProps {
15
- /** `empty`: nothing to show. `error`: the first load failed. */
16
- state: "empty" | "error";
25
+ state: BlockStatusState;
17
26
  /** Replaces the default texts of the empty state. */
18
27
  empty?: BlockEmptyText;
28
+ /** What the 404 said, laid over the default not-found texts. */
29
+ notFound?: FetchNotFound | null;
19
30
  /** Draw the card surface around it (off inside a card). */
20
31
  card?: boolean;
21
32
  }
@@ -26,33 +37,59 @@ interface BlockStatusEmits {
26
37
 
27
38
  const props = withDefaults(defineProps<BlockStatusProps>(), {
28
39
  empty: undefined,
40
+ notFound: undefined,
29
41
  card: true,
30
42
  });
31
43
  const emit = defineEmits<BlockStatusEmits>();
32
44
 
45
+ const VARIANTS = {
46
+ empty: "no-data",
47
+ error: "error",
48
+ "not-found": "no-result",
49
+ } as const;
50
+
33
51
  const { t } = useI18n();
34
52
  const { processI18n } = useTranslation();
53
+
54
+ const translate = (text: string | undefined) =>
55
+ text ? processI18n(text) : undefined;
56
+
57
+ const notFoundState = computed(() => {
58
+ const answered = props.notFound?.emptyState ?? {};
59
+ const defaults = notFoundTexts(props.notFound, t);
60
+ return {
61
+ title: translate(answered.title) ?? defaults.title,
62
+ description: translate(answered.description) ?? defaults.description,
63
+ icon: answered.icon,
64
+ tone: answered.tone,
65
+ actions: answered.actions ?? [],
66
+ };
67
+ });
68
+
69
+ const isNotFound = computed(() => props.state === "not-found");
70
+
71
+ const title = computed(() => {
72
+ if (props.state === "error") return t("dms.blocks.load_error");
73
+ if (isNotFound.value) return notFoundState.value.title;
74
+ return props.empty ? processI18n(props.empty.title) : t("dms.blocks.empty");
75
+ });
76
+
77
+ const description = computed(() => {
78
+ if (props.state === "error") return t("dms.blocks.load_error_hint");
79
+ if (isNotFound.value) return notFoundState.value.description;
80
+ return translate(props.empty?.description);
81
+ });
35
82
  </script>
36
83
 
37
84
  <template>
38
85
  <div :class="props.card && 'dms-card'">
39
86
  <DmsEmptyState
40
87
  size="sm"
41
- :variant="props.state === 'error' ? 'error' : 'no-data'"
42
- :title="
43
- props.state === 'error'
44
- ? t('dms.blocks.load_error')
45
- : props.empty
46
- ? processI18n(props.empty.title)
47
- : t('dms.blocks.empty')
48
- "
49
- :description="
50
- props.state === 'error'
51
- ? t('dms.blocks.load_error_hint')
52
- : props.empty?.description
53
- ? processI18n(props.empty.description)
54
- : undefined
55
- "
88
+ :variant="VARIANTS[props.state]"
89
+ :icon="isNotFound ? notFoundState.icon : undefined"
90
+ :tone="isNotFound ? notFoundState.tone : undefined"
91
+ :title="title"
92
+ :description="description"
56
93
  >
57
94
  <template v-if="props.state === 'error'" #actions>
58
95
  <UButton
@@ -64,6 +101,9 @@ const { processI18n } = useTranslation();
64
101
  @click="emit('retry')"
65
102
  />
66
103
  </template>
104
+ <template v-else-if="isNotFound && notFoundState.actions.length" #actions>
105
+ <DmsBlockActions :actions="notFoundState.actions" size="sm" />
106
+ </template>
67
107
  </DmsEmptyState>
68
108
  </div>
69
109
  </template>
@@ -9,7 +9,10 @@ import type { Data, TableSharedData } from "./Table.vue";
9
9
  import { KANBAN_DISPLAY_ID } from "../../../composables/table-view/types";
10
10
  import type { CustomButton } from "../../../composables/table-view/types/custom-button";
11
11
  import { useTableContext } from "../../composables/table/useTableContext";
12
- import { createTableViewShiftFShortcut } from "../../../composables/table-view/shortcuts/tableViewShiftF";
12
+ import {
13
+ createTableViewShiftFShortcut,
14
+ tableSearchShortcuts,
15
+ } from "../../../composables/table-view/shortcuts/tableViewShiftF";
13
16
  import {
14
17
  FULL_TABLE_CHROME,
15
18
  type ResolvedTableChrome,
@@ -111,6 +114,8 @@ interface TableActionsProps {
111
114
  chrome?: ResolvedTableChrome;
112
115
  /** Placeholder of the search field. */
113
116
  searchPlaceholder?: string;
117
+ /** Key opening the search (`defineShortcuts` notation); `shift_f` else. */
118
+ searchShortcut?: string;
114
119
  /** Dropdown filters writing their column's filter. */
115
120
  quickFilters?: ResolvedQuickFilter[];
116
121
  }
@@ -193,7 +198,6 @@ watch(searchAreaFocused, async (isFocused) => {
193
198
  searchActive.value = false;
194
199
  });
195
200
 
196
-
197
201
  const searchFieldRef = ref<{ $el: HTMLElement }>();
198
202
 
199
203
  const focusSearchBar = () => {
@@ -273,13 +277,16 @@ const closeSearch = () => {
273
277
  searchActive.value = false;
274
278
  };
275
279
 
276
- defineShortcuts({
277
- shift_f: createTableViewShiftFShortcut(
278
- searchActive,
279
- tableSharedData!.globalFilterState,
280
- focusSearchBar,
280
+ const openSearchFromShortcut = createTableViewShiftFShortcut(
281
+ searchActive,
282
+ tableSharedData!.globalFilterState,
283
+ focusSearchBar,
284
+ );
285
+ defineShortcuts(
286
+ computed(() =>
287
+ tableSearchShortcuts(props.searchShortcut, openSearchFromShortcut),
281
288
  ),
282
- });
289
+ );
283
290
 
284
291
  const hasCustomFilterConfig = computed(() => {
285
292
  const filters = tableSharedData?.columnFiltersState.value || [];
@@ -80,6 +80,18 @@ const customComponent = computed(() => {
80
80
  return name ? resolveDmsComponent(name) || name : undefined;
81
81
  });
82
82
  const refresh = () => tableSharedData.value?.emits("refresh");
83
+ // A custom state clears what narrows the rows as the built-in one does; each
84
+ // is unset while there is nothing of it to clear.
85
+ const clearSearch = computed(() =>
86
+ clearable.value?.search
87
+ ? () => tableSharedData.value?.clearSearch()
88
+ : undefined,
89
+ );
90
+ const clearFilters = computed(() =>
91
+ clearable.value?.filters
92
+ ? () => tableSharedData.value?.clearFilters()
93
+ : undefined,
94
+ );
83
95
 
84
96
  interface EmptyStateContent {
85
97
  title: string;
@@ -221,6 +233,8 @@ const builtInActions = computed(() => {
221
233
  :state="kind"
222
234
  :search="search"
223
235
  :refresh="refresh"
236
+ :clear-search="clearSearch"
237
+ :clear-filters="clearFilters"
224
238
  />
225
239
  <DmsEmptyState
226
240
  v-else