@antelopejs/dms 0.7.9 → 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 (172) hide show
  1. package/CHANGELOG.md +29 -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 +74 -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/table-actions-search-label.test.ts +57 -1
  158. package/frontend-vue/tests/table-clear-filters.test.ts +99 -1
  159. package/frontend-vue/tests/table-density-menu.test.ts +4 -3
  160. package/frontend-vue/tests/table-live-hold.test.ts +95 -5
  161. package/frontend-vue/tests/table-row-action-labels.test.ts +181 -0
  162. package/frontend-vue/tests/table-source-period-scope.test.ts +31 -3
  163. package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +14 -17
  164. package/frontend-vue/tests/table-view-pinned-rows.test.ts +143 -0
  165. package/frontend-vue/tests/table-view-sortable-columns.test.ts +45 -0
  166. package/frontend-vue/tests/table-view-views.test.ts +56 -15
  167. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +107 -0
  168. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  169. package/package.json +2 -2
  170. package/skills/dms/SKILL.md +9 -3
  171. package/skills/dms-module/SKILL.md +5 -0
  172. package/skills/dms-pages/REFERENCE.md +23 -2
@@ -1,35 +1,78 @@
1
- import { computed, type ComputedRef } from "vue";
1
+ import {
2
+ computed,
3
+ getCurrentScope,
4
+ onScopeDispose,
5
+ type ComputedRef,
6
+ } from "vue";
2
7
  import { currentPageLive } from "../../build/composables/live/currentPageLive";
8
+ import type { PageLiveController } from "../../build/composables/live/pageLiveController";
9
+ import type { PageLiveNote, PageLiveNoteInput } from "../../types/live";
3
10
 
4
- /** The Direct / Pause state of the page on screen, for a module's component. */
11
+ /** The Live / Pause state of the page on screen, for a module's component. */
5
12
  export interface PageLiveState {
6
- /** The page declares `live`: it has a Direct / Pause state at all. */
13
+ /** The page declares `live`: it has a Live / Pause state at all. */
7
14
  isEnabled: ComputedRef<boolean>;
8
- /** The page's data follows its events: Direct, or a page without `live`. */
15
+ /** The page's data follows its events: Live, or a page without `live`. */
9
16
  isLive: ComputedRef<boolean>;
10
17
  /** The page is paused: its blocks request nothing on their events. */
11
18
  isPaused: ComputedRef<boolean>;
19
+ /**
20
+ * Why the page paused by itself: a hold's reason, or its tab hidden in the
21
+ * background. `null` while live, and while paused by the user.
22
+ */
23
+ pauseReason: ComputedRef<PageLiveNote | null>;
12
24
  pause: () => void;
13
- /** Leaves the pause: each block that missed an event refetches once. */
25
+ /**
26
+ * Leaves the pause: each block that missed an event refetches once. It also
27
+ * lifts the holds of the moment, until their component releases them.
28
+ */
14
29
  resume: () => void;
15
30
  toggle: () => void;
31
+ /**
32
+ * Holds the page paused for a reason, until `release()` or until the
33
+ * component goes: while the user types in a form whose results the live
34
+ * data would move, say. The toggle reads "Paused · <reason>"; give an i18n
35
+ * key prefixed with `$` (`hold("$api.live.typing_in_tester")`). Calling it
36
+ * again changes the reason. The page resumes once nothing holds it, unless
37
+ * the user paused it.
38
+ */
39
+ hold: (reason: PageLiveNoteInput) => void;
40
+ /** Drops this component's hold, if it holds the page. */
41
+ release: () => void;
16
42
  /** When the page last read its data (ms since the epoch), `null` without `live`. */
17
43
  lastUpdatedAt: ComputedRef<number | null>;
18
- /** Distinct events the page received since the pause. */
44
+ /**
45
+ * What the page missed since the pause, in published rows: a message's
46
+ * `payload.ids` count once per row, its `payload.count` as that many, any
47
+ * other message as one.
48
+ */
19
49
  missedCount: ComputedRef<number>;
50
+ /** The breakdown of what was missed, as `setMissedDetail` gave it. */
51
+ missedDetail: ComputedRef<PageLiveNote | null>;
52
+ /**
53
+ * Gives the toggle a breakdown of what was missed, shown after the count
54
+ * ("Paused · 40 new requests · 2 in 5xx"): a component following the page's
55
+ * topic tallies what it cares about while paused and says it here, e.g.
56
+ * `setMissedDetail({ text: "$api.live.in_5xx", params: { count: 2 }, tone: "error" })`.
57
+ * Cleared on resume; `null` clears it.
58
+ */
59
+ setMissedDetail: (detail: PageLiveNoteInput | null) => void;
60
+ /** The realtime stream is down and the page reads its blocks on a clock. */
61
+ isPolling: ComputedRef<boolean>;
20
62
  }
21
63
 
22
64
  /**
23
- * Follows the Direct / Pause state of the page on screen (see the page option
65
+ * Follows the Live / Pause state of the page on screen (see the page option
24
66
  * `live` and the `LiveToggle()` block), so a custom block stays in step with
25
67
  * the built-in ones without reading the DOM: stop a client-side ticker while
26
- * paused, show what was missed, offer its own switch.
68
+ * paused, show what was missed, offer its own switch, hold the page while the
69
+ * user types.
27
70
  *
28
71
  * On a page without `live`, or while server-rendering, `isEnabled` is false,
29
- * the page reads as live and never paused, and `pause` / `resume` do nothing.
72
+ * the page reads as live and never paused, and its actions do nothing.
30
73
  *
31
74
  * ```ts
32
- * const { isPaused, missedCount, resume } = usePageLive();
75
+ * const { isPaused, missedCount, resume, hold, release } = usePageLive();
33
76
  * ```
34
77
  *
35
78
  * A component that refetches on its own realtime topic need not use it:
@@ -37,16 +80,49 @@ export interface PageLiveState {
37
80
  */
38
81
  export function usePageLive(): PageLiveState {
39
82
  const isPaused = computed(() => currentPageLive()?.isPaused.value ?? false);
83
+ const hold = usePageLiveHold();
40
84
  return {
41
85
  isEnabled: computed(() => currentPageLive() !== null),
42
86
  isLive: computed(() => !isPaused.value),
43
87
  isPaused,
88
+ pauseReason: computed(() => currentPageLive()?.pauseReason.value ?? null),
44
89
  pause: () => currentPageLive()?.pause(),
45
90
  resume: () => currentPageLive()?.resume(),
46
91
  toggle: () => currentPageLive()?.toggle(),
92
+ hold: hold.hold,
93
+ release: hold.release,
47
94
  lastUpdatedAt: computed(
48
95
  () => currentPageLive()?.lastUpdatedAt.value ?? null,
49
96
  ),
50
97
  missedCount: computed(() => currentPageLive()?.missedCount.value ?? 0),
98
+ missedDetail: computed(() => currentPageLive()?.missedDetail.value ?? null),
99
+ setMissedDetail: (detail) => currentPageLive()?.setMissedDetail(detail),
100
+ isPolling: computed(() => currentPageLive()?.isPolling.value ?? false),
101
+ };
102
+ }
103
+
104
+ interface PageLiveHold {
105
+ hold: (reason: PageLiveNoteInput) => void;
106
+ release: () => void;
107
+ }
108
+
109
+ // One hold per call of usePageLive, released with the component's scope: a
110
+ // component leaving the page never leaves it paused.
111
+ function usePageLiveHold(): PageLiveHold {
112
+ const owner = Symbol("page-live-hold");
113
+ let holder: PageLiveController | null = null;
114
+ const release = () => {
115
+ holder?.release(owner);
116
+ holder = null;
117
+ };
118
+ if (getCurrentScope()) onScopeDispose(release);
119
+ return {
120
+ hold: (reason) => {
121
+ const controller = currentPageLive();
122
+ if (holder !== controller) release();
123
+ holder = controller;
124
+ controller?.hold(owner, reason);
125
+ },
126
+ release,
51
127
  };
52
128
  }
@@ -1,7 +1,16 @@
1
+ /**
2
+ * The key a table view opens its search with when its `searchShortcut`
3
+ * names none.
4
+ */
5
+ export const DEFAULT_TABLE_SEARCH_SHORTCUT = "shift_f";
6
+
1
7
  export const TABLE_VIEW_SHIFT_F_METADATA: ShortcutMetadata = {
2
8
  key: ["$keyboard.shift", "f"],
3
9
  descriptionKey: "$dms.shortcuts.tableview.shift_f.description",
4
10
  component: "$dms.components.tableview",
11
+ condition: {
12
+ descriptionKey: "$dms.shortcuts.tableview.shift_f.condition",
13
+ },
5
14
  };
6
15
 
7
16
  export function createTableViewShiftFShortcut(
@@ -16,3 +25,14 @@ export function createTableViewShiftFShortcut(
16
25
  focusSearchBar();
17
26
  };
18
27
  }
28
+
29
+ /**
30
+ * The `defineShortcuts` map opening a table's search on `key`, ⇧F when the
31
+ * table names none.
32
+ */
33
+ export function tableSearchShortcuts(
34
+ key: string | undefined,
35
+ openSearch: () => void,
36
+ ): Record<string, () => void> {
37
+ return { [key || DEFAULT_TABLE_SEARCH_SHORTCUT]: openSearch };
38
+ }
@@ -1,9 +1,19 @@
1
1
  import type { DrawerDirection } from "../../containers/types";
2
2
 
3
+ /** What a side panel says on a row that is gone (backend `SidePanelNotFoundTexts`). */
4
+ export interface SidePanelNotFoundTexts {
5
+ /** `$`: an i18n key; the DMS's generic title else. */
6
+ title?: string;
7
+ /** `$`: an i18n key; the DMS's generic text else. */
8
+ description?: string;
9
+ }
10
+
3
11
  /** What a `sidePanel` target sets besides its component (backend `SidePanelTargetOptions`). */
4
12
  export interface SidePanelTargetOptions {
5
13
  /** Title of the panel header (`$`: an i18n key); the action's label else. */
6
14
  title?: string;
15
+ /** Title of the header while a row is open, receiving the row's fields. */
16
+ rowTitle?: string;
7
17
  /**
8
18
  * The row's position under the title: an i18n key (`$`-prefixed) receiving
9
19
  * `{ index }` (from 1) and `{ total }`.
@@ -11,6 +21,8 @@ export interface SidePanelTargetOptions {
11
21
  positionLabel?: string;
12
22
  /** The open row is kept in the URL (`?record=`): on unless `false`. */
13
23
  deepLink?: boolean;
24
+ /** The state drawn on a row that no longer exists (a 404). */
25
+ notFound?: SidePanelNotFoundTexts;
14
26
  }
15
27
 
16
28
  export type ActionTarget =
@@ -2,6 +2,7 @@ import type {
2
2
  FormContainer,
3
3
  FormPageUrls,
4
4
  } from "../../../build/composables/table-view/useTableViewConfig";
5
+ import type { RowActionRule } from "#dms-core/app/types/row-action";
5
6
  import type { TableViewColumn } from "./column";
6
7
  import type { TableViewDisplayCapabilities } from "./display";
7
8
  import type { CustomButton } from "./custom-button";
@@ -283,6 +284,8 @@ export interface TableViewConfig<T extends Data>
283
284
  componentId?: string;
284
285
  pageId?: string;
285
286
  defaultSort?: { field: string; desc?: boolean };
287
+ /** Rows listed first whatever the sort: a rule on their fields. */
288
+ pinnedRows?: RowActionRule;
286
289
  queryParamFilters?: QueryParamFilters;
287
290
  routeParamFilters?: RouteParamFilters;
288
291
  routeParams?: Record<string, string>;
@@ -298,6 +301,8 @@ export interface TableViewConfig<T extends Data>
298
301
  /** The controller declares `@Searchable` fields. */
299
302
  searchable?: boolean;
300
303
  searchPlaceholder?: string;
304
+ /** Key opening the search (`defineShortcuts` notation); `shift_f` else. */
305
+ searchShortcut?: string;
301
306
  quickFilters?: TableViewQuickFilter[];
302
307
  /** Rows per page while the user picked none. */
303
308
  pageSize?: number;
@@ -0,0 +1,55 @@
1
+ import { injectLocal } from "@vueuse/core";
2
+ import { computed, type ComputedRef, type ShallowRef } from "vue";
3
+ import type {
4
+ Data,
5
+ TableSharedData,
6
+ } from "../../build/components/table/Table.vue";
7
+
8
+ /** What narrows a table's rows, and the ways out, for its empty state. */
9
+ export interface TableViewEmptyStateControls {
10
+ /** The text searched, empty when none. */
11
+ search: ComputedRef<string>;
12
+ /** A search, a filter chip, a quick filter or a URL filter narrows the rows. */
13
+ isFiltered: ComputedRef<boolean>;
14
+ /** A search narrows the rows: `clearSearch` has something to clear. */
15
+ canClearSearch: ComputedRef<boolean>;
16
+ /** A filter chip (a quick filter's included) narrows the rows. */
17
+ canClearFilters: ComputedRef<boolean>;
18
+ /** Empties the search, as the table's own "Clear search" does. */
19
+ clearSearch: () => void;
20
+ /** Clears the filter chips (pinned ones back to their value), the search kept. */
21
+ clearFilters: () => void;
22
+ /** Clears both, as the table's "Clear filters and search" does. */
23
+ clearAll: () => void;
24
+ /** Lists the rows again. */
25
+ refresh: () => void;
26
+ }
27
+
28
+ /**
29
+ * The search and filters of the table a component is drawn in, for a custom
30
+ * empty state (`emptyStates.*.component`) or anything else inside the table:
31
+ * offer "Clear search" and "Clear filters" without reading the table's
32
+ * internals. A filter the page URL applies (`queryParamFilters`) counts in
33
+ * `isFiltered` but is not the table's to clear: the page clears its own.
34
+ *
35
+ * Outside a table, nothing narrows and every action does nothing.
36
+ *
37
+ * ```ts
38
+ * const { canClearSearch, clearSearch } = useTableViewEmptyState();
39
+ * ```
40
+ */
41
+ export function useTableViewEmptyState(): TableViewEmptyStateControls {
42
+ const shared =
43
+ injectLocal<ShallowRef<TableSharedData<Data>>>("tableSharedData");
44
+ const table = () => shared?.value;
45
+ return {
46
+ search: computed(() => table()?.globalFilterState.value ?? ""),
47
+ isFiltered: computed(() => !!table()?.isFiltered.value),
48
+ canClearSearch: computed(() => !!table()?.clearableFilters.value.search),
49
+ canClearFilters: computed(() => !!table()?.clearableFilters.value.filters),
50
+ clearSearch: () => table()?.clearSearch(),
51
+ clearFilters: () => table()?.clearFilters(),
52
+ clearAll: () => table()?.resetFilters(),
53
+ refresh: () => table()?.emits("refresh"),
54
+ };
55
+ }
@@ -1,8 +1,54 @@
1
+ /**
2
+ * Mirror of `PageLivePeriod` from `@antelopejs/interface-dms/page`: whole
3
+ * minutes, hours or days (`"30m"`, `"24h"`, `"7d"`).
4
+ */
5
+ export type PageLivePeriod = `${number}${"m" | "h" | "d"}`;
6
+
7
+ /** Mirror of `PageLiveSlowRate`: the cadence of a page showing a long period. */
8
+ export interface PageLiveSlowRate {
9
+ period: PageLivePeriod;
10
+ maxRate: number;
11
+ }
12
+
1
13
  /**
2
14
  * Mirror of `PageLiveOptions` from `@antelopejs/interface-dms/page`: the
3
- * Direct / Pause valve a page declares with `live`.
15
+ * Live / Pause valve a page declares with `live`.
4
16
  */
5
17
  export interface PageLiveOptions {
6
18
  /** Shortest interval, in seconds, between two refetches of one block. */
7
19
  maxRate?: number;
20
+ /** The slower cadence once the page's period spans `period` or more. */
21
+ slowRateAfter?: PageLiveSlowRate;
22
+ /** Key the remembered Live / Pause choice is shared under. */
23
+ rememberKey?: string;
24
+ }
25
+
26
+ /** How the live toggle draws a note: a breakdown in 5xx reads as an error. */
27
+ export type PageLiveNoteTone = "neutral" | "warning" | "error";
28
+
29
+ /**
30
+ * A short text the live toggle shows: why the page holds ("typing in the
31
+ * Tester"), or what its missed events hold ("2 in 5xx"). `text` is an i18n
32
+ * key prefixed with `$`, translated with `params` (pluralised on
33
+ * `params.count` when it is a number), or a text shown as is.
34
+ */
35
+ export interface PageLiveNote {
36
+ text: string;
37
+ params?: Record<string, unknown>;
38
+ tone?: PageLiveNoteTone;
39
+ }
40
+
41
+ /** A {@link PageLiveNote}, or its `text` alone. */
42
+ export type PageLiveNoteInput = string | PageLiveNote;
43
+
44
+ /**
45
+ * Mirror of `TableViewLiveHoldOptions` from
46
+ * `@antelopejs/interface-dms/base/table-view`: when a `liveHold` table also
47
+ * holds its rows while its page is live.
48
+ */
49
+ export interface TableViewLiveHoldOptions {
50
+ /** While one of its rows is open in the side panel. */
51
+ whileRowOpen?: boolean;
52
+ /** While the user has scrolled its first row out of view. */
53
+ whileScrolled?: boolean;
8
54
  }
@@ -120,6 +120,8 @@
120
120
  "clear_selection": "Clear selection",
121
121
  "expand_row": "Expand row",
122
122
  "collapse_row": "Collapse row",
123
+ "row_actions": "Row actions",
124
+ "row_actions_for": "Actions for {label}",
123
125
  "tabs": {
124
126
  "all": "All"
125
127
  },
@@ -464,7 +466,8 @@
464
466
  "condition": "Only when enableRowSelection is enabled"
465
467
  },
466
468
  "shift_f": {
467
- "description": "Open/focus search bar"
469
+ "description": "Open/focus search bar",
470
+ "condition": "Unless the table names another key (searchShortcut), such as /"
468
471
  },
469
472
  "delete": {
470
473
  "description": "Delete hovered row",
@@ -722,21 +725,27 @@
722
725
  "empty": "Nothing to show yet",
723
726
  "load_error": "Couldn't load this block",
724
727
  "load_error_hint": "Check your connection, then try again.",
725
- "retry": "Try again"
728
+ "retry": "Try again",
729
+ "not_found": "Not found",
730
+ "not_found_hint": "What this shows doesn’t exist, or no longer does."
726
731
  },
727
732
  "action_error": {
728
733
  "forbidden": "You don’t have permission to do this.",
729
734
  "generic": "Something went wrong and the action couldn’t be completed. Try again."
730
735
  },
731
736
  "live": {
732
- "direct": "Direct",
737
+ "direct": "Live",
733
738
  "pause": "Pause",
734
739
  "aria_label": "Live updates",
735
740
  "updated_seconds": "Updated {count} s ago",
736
741
  "updated_minutes": "Updated {count} min ago",
737
742
  "updated_hours": "Updated {count} h ago",
738
743
  "new_rows": "{count} new",
739
- "show_new": "Show"
744
+ "show_new": "Show",
745
+ "paused": "Paused",
746
+ "resume": "Resume",
747
+ "reason_hidden": "tab hidden",
748
+ "polling": "no realtime, polling"
740
749
  }
741
750
  }
742
751
  }
@@ -120,6 +120,8 @@
120
120
  "clear_selection": "Effacer la sélection",
121
121
  "expand_row": "Déplier la ligne",
122
122
  "collapse_row": "Replier la ligne",
123
+ "row_actions": "Actions de la ligne",
124
+ "row_actions_for": "Actions pour {label}",
123
125
  "tabs": {
124
126
  "all": "Tous"
125
127
  },
@@ -465,7 +467,8 @@
465
467
  "condition": "Uniquement quand enableRowSelection est activé"
466
468
  },
467
469
  "shift_f": {
468
- "description": "Ouvrir/focus la barre de recherche"
470
+ "description": "Ouvrir/focus la barre de recherche",
471
+ "condition": "Sauf si le tableau nomme une autre touche (searchShortcut), comme /"
469
472
  },
470
473
  "delete": {
471
474
  "description": "Supprimer la ligne survolée",
@@ -723,7 +726,9 @@
723
726
  "empty": "Rien à afficher pour l'instant",
724
727
  "load_error": "Impossible de charger ce bloc",
725
728
  "load_error_hint": "Vérifiez votre connexion, puis réessayez.",
726
- "retry": "Réessayer"
729
+ "retry": "Réessayer",
730
+ "not_found": "Introuvable",
731
+ "not_found_hint": "Ce qui est affiché ici n’existe pas, ou plus."
727
732
  },
728
733
  "action_error": {
729
734
  "forbidden": "Vous n’avez pas l’autorisation d’effectuer cette action.",
@@ -737,7 +742,11 @@
737
742
  "updated_minutes": "Mis à jour il y a {count} min",
738
743
  "updated_hours": "Mis à jour il y a {count} h",
739
744
  "new_rows": "{count} nouvelle | {count} nouvelles",
740
- "show_new": "Afficher"
745
+ "show_new": "Afficher",
746
+ "paused": "En pause",
747
+ "resume": "Reprendre",
748
+ "reason_hidden": "onglet masqué",
749
+ "polling": "sans temps réel, relecture régulière"
741
750
  }
742
751
  }
743
752
  }
@@ -8,6 +8,7 @@ const items = ref<Array<Record<string, unknown>>>([]);
8
8
  vi.mock("../layers/dms-ui/app/build/composables/blocks/useBlockItems", () => ({
9
9
  useBlockItems: () => ({
10
10
  items: computed(() => items.value),
11
+ count: computed(() => undefined),
11
12
  isPending: computed(() => false),
12
13
  hasError: computed(() => false),
13
14
  refresh: async () => {},
@@ -0,0 +1,140 @@
1
+ /**
2
+ * An ActivityFeed's head carries a count beside its title (the option, or the
3
+ * `count` its route answers) and a soft badge before its actions; each entry
4
+ * may add a third, dimmed mono line under its details.
5
+ */
6
+ import { computed, createSSRApp, defineComponent, h, ref } from "vue";
7
+ import { renderToString } from "vue/server-renderer";
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
+ import ActivityFeed from "../layers/dms-ui/app/components/activity-feed/ActivityFeed.vue";
10
+
11
+ const items = ref<Array<Record<string, unknown>>>([]);
12
+ const fetchedCount = ref<number | undefined>(undefined);
13
+
14
+ vi.mock("../layers/dms-ui/app/build/composables/blocks/useBlockItems", () => ({
15
+ useBlockItems: () => ({
16
+ items: computed(() => items.value),
17
+ count: computed(() => fetchedCount.value),
18
+ isPending: computed(() => false),
19
+ hasError: computed(() => false),
20
+ refresh: async () => {},
21
+ }),
22
+ }));
23
+
24
+ const Card = defineComponent({
25
+ props: {
26
+ title: { type: String, default: undefined },
27
+ count: { type: [Number, String], default: undefined },
28
+ },
29
+ setup:
30
+ (props, { slots }) =>
31
+ () =>
32
+ h("div", { "data-card": "" }, [
33
+ h("header", [
34
+ props.title,
35
+ props.count === undefined
36
+ ? null
37
+ : h("span", { "data-count": "" }, String(props.count)),
38
+ slots.actions?.(),
39
+ ]),
40
+ slots.default?.(),
41
+ ]),
42
+ });
43
+
44
+ const Badge = defineComponent({
45
+ props: { label: String },
46
+ setup: (props) => () => h("span", { "data-badge": "" }, props.label),
47
+ });
48
+
49
+ const Slot = defineComponent({
50
+ setup:
51
+ (_, { slots }) =>
52
+ () =>
53
+ h("div", slots.default?.()),
54
+ });
55
+
56
+ async function render(props: Record<string, unknown>): Promise<string> {
57
+ const app = createSSRApp({
58
+ render: () =>
59
+ h(ActivityFeed, {
60
+ componentId: "attention",
61
+ pageId: "overview",
62
+ title: "Needs attention",
63
+ fetchUrl: "/api/attention",
64
+ groupByDay: false,
65
+ ...props,
66
+ }),
67
+ });
68
+ app.component("DmsCard", Card);
69
+ app.component("UBadge", Badge);
70
+ for (const name of ["UIcon", "USkeleton", "UButton", "ULink", "NuxtLink"]) {
71
+ app.component(name, Slot);
72
+ }
73
+ return renderToString(app);
74
+ }
75
+
76
+ beforeEach(() => {
77
+ items.value = [
78
+ {
79
+ id: "cart",
80
+ title: "POST /cart/checkout",
81
+ meta: ["Client errors"],
82
+ detail: '400 "Cart is empty" × 16',
83
+ },
84
+ ];
85
+ fetchedCount.value = undefined;
86
+ vi.stubGlobal("useI18n", () => ({ locale: ref("en-GB"), t: String }));
87
+ vi.stubGlobal("useTranslation", () => ({
88
+ processI18n: (text: string) => (text.startsWith("$") ? `t(${text})` : text),
89
+ }));
90
+ });
91
+
92
+ afterEach(() => vi.unstubAllGlobals());
93
+
94
+ describe("ActivityFeed head", () => {
95
+ it("draws no count and no badge unless asked", async () => {
96
+ const html = await render({});
97
+ expect(html).not.toContain("data-count");
98
+ expect(html).not.toContain("data-badge");
99
+ });
100
+
101
+ it("shows the count option beside the title", async () => {
102
+ const html = await render({ count: 7 });
103
+ expect(html).toMatch(/<span data-count[^>]*>7<\/span>/);
104
+ });
105
+
106
+ it("prefers the count its route answers", async () => {
107
+ fetchedCount.value = 4;
108
+ const html = await render({ count: 7 });
109
+ expect(html).toMatch(/<span data-count[^>]*>4<\/span>/);
110
+ });
111
+
112
+ it("draws a translated badge in the head", async () => {
113
+ // `t` is stubbed as String: a translated key reads as the bare key.
114
+ const html = await render({ badge: "$api.overview.last_24h" });
115
+ expect(html).toMatch(
116
+ /<span data-badge[^>]*>api\.overview\.last_24h<\/span>/,
117
+ );
118
+ });
119
+
120
+ it("draws a literal badge as written", async () => {
121
+ const html = await render({ badge: "Last 24 h" });
122
+ expect(html).toMatch(/<span data-badge[^>]*>Last 24 h<\/span>/);
123
+ });
124
+ });
125
+
126
+ describe("ActivityFeed entry detail", () => {
127
+ it("writes the detail as a third, dimmed mono line", async () => {
128
+ const html = await render({});
129
+ const line = /<p class="([^"]*)"[^>]*>([^<]*)<\/p>/.exec(html);
130
+ expect(line?.[2]?.trim()).toBe("400 &quot;Cart is empty&quot; × 16");
131
+ expect(line?.[1]).toContain("font-mono");
132
+ expect(line?.[1]).toContain("text-dimmed");
133
+ });
134
+
135
+ it("draws no detail line for an entry without one", async () => {
136
+ items.value = [{ id: "a", title: "GET /health" }];
137
+ const html = await render({});
138
+ expect(html).not.toMatch(/<p class="[^"]*font-mono/);
139
+ });
140
+ });