@antelopejs/dms 0.7.1 → 0.7.2

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 (110) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/hooks/tenant-instances.d.ts +1 -0
  3. package/dist/hooks/tenant-instances.js +13 -0
  4. package/dist/hooks/tenant-instances.js.map +1 -0
  5. package/dist/index.js +3 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/test/unit/interfaces/dms/page-topics.test.js +67 -0
  8. package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
  9. package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js +1 -0
  10. package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js.map +1 -1
  11. package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.d.ts +1 -0
  12. package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js +46 -0
  13. package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js.map +1 -0
  14. package/dist/test/unit/interfaces/dms-base/column-display.test.js +11 -0
  15. package/dist/test/unit/interfaces/dms-base/column-display.test.js.map +1 -1
  16. package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js +31 -0
  17. package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js.map +1 -1
  18. package/dist/test/unit/utils/tenant-instances.test.d.ts +1 -0
  19. package/dist/test/unit/utils/tenant-instances.test.js +53 -0
  20. package/dist/test/unit/utils/tenant-instances.test.js.map +1 -0
  21. package/dist/utils/index.d.ts +1 -0
  22. package/dist/utils/index.js +1 -0
  23. package/dist/utils/index.js.map +1 -1
  24. package/dist/utils/tenant-instances.d.ts +2 -0
  25. package/dist/utils/tenant-instances.js +38 -0
  26. package/dist/utils/tenant-instances.js.map +1 -0
  27. package/dist/validation/auth.schema.d.ts +2 -2
  28. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +4 -0
  29. package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +35 -23
  30. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockActions.vue +34 -6
  31. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +26 -8
  32. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +3 -1
  33. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +1 -1
  34. package/frontend-vue/layers/dms-ui/app/build/components/table/MonoCell.vue +3 -2
  35. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +24 -13
  36. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +1 -1
  37. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +12 -5
  38. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useRefreshTriggers.ts +40 -0
  39. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +15 -4
  40. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +85 -22
  41. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +10 -2
  42. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +79 -0
  43. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +94 -0
  44. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +41 -5
  45. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +3 -0
  46. package/frontend-vue/layers/dms-ui/app/build/utils/codeEditor.ts +2 -13
  47. package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +59 -0
  48. package/frontend-vue/layers/dms-ui/app/build/utils/codeLanguages.ts +36 -0
  49. package/frontend-vue/layers/dms-ui/app/build/utils/codePalette.ts +32 -0
  50. package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +55 -0
  51. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +77 -20
  52. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +15 -4
  53. package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +111 -25
  54. package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +95 -0
  55. package/frontend-vue/layers/dms-ui/app/components/blocks/EmptyStateBlock.vue +18 -0
  56. package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +8 -0
  57. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +21 -9
  58. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +8 -0
  59. package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +97 -0
  60. package/frontend-vue/layers/dms-ui/app/components/empty-state/EmptyState.vue +13 -4
  61. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +14 -3
  62. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +2 -8
  63. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +19 -0
  64. package/frontend-vue/layers/dms-ui/app/components/master-detail/MasterDetail.vue +54 -48
  65. package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +24 -9
  66. package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +4 -1
  67. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +66 -74
  68. package/frontend-vue/layers/dms-ui/app/components/top-list/internal/TopListRow.vue +4 -3
  69. package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +5 -2
  70. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +2 -29
  71. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +2 -1
  72. package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +2 -0
  73. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +4 -0
  74. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +2 -2
  75. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +1 -0
  76. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +1 -0
  77. package/frontend-vue/package.json +2 -2
  78. package/frontend-vue/pnpm-lock.yaml +5 -5
  79. package/frontend-vue/tests/activity-feed-days.test.ts +26 -12
  80. package/frontend-vue/tests/banner-block-fetch.test.ts +162 -0
  81. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +100 -0
  82. package/frontend-vue/tests/block-items-period-realtime.test.ts +147 -0
  83. package/frontend-vue/tests/block-route-params.test.ts +48 -0
  84. package/frontend-vue/tests/code-block.test.ts +155 -0
  85. package/frontend-vue/tests/code-highlight.test.ts +72 -0
  86. package/frontend-vue/tests/composed-text-blocks.test.ts +5 -0
  87. package/frontend-vue/tests/composed-text-more-blocks.test.ts +391 -0
  88. package/frontend-vue/tests/drawer-target-direction.test.ts +205 -0
  89. package/frontend-vue/tests/empty-state-late-slots.test.ts +53 -0
  90. package/frontend-vue/tests/flow-canvas-delete-badge.test.ts +88 -0
  91. package/frontend-vue/tests/form-redirect-placeholders.test.ts +11 -0
  92. package/frontend-vue/tests/key-value-list-copy.test.ts +114 -0
  93. package/frontend-vue/tests/master-detail-layout.test.ts +53 -0
  94. package/frontend-vue/tests/mono-cell-truncate.test.ts +50 -0
  95. package/frontend-vue/tests/nav-card-grid-empty-category.test.ts +1 -0
  96. package/frontend-vue/tests/page-fill-height.test.ts +20 -3
  97. package/frontend-vue/tests/route-param-occurrences.test.ts +9 -0
  98. package/frontend-vue/tests/table-actions-search-label.test.ts +147 -0
  99. package/frontend-vue/tests/table-clear-filters.test.ts +22 -0
  100. package/frontend-vue/tests/table-empty-body-viewport.test.ts +25 -0
  101. package/frontend-vue/tests/table-metric-cells.test.ts +22 -0
  102. package/frontend-vue/tests/table-pills-cell.test.ts +126 -0
  103. package/frontend-vue/tests/table-source-navigation.test.ts +123 -0
  104. package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +137 -0
  105. package/frontend-vue/tests/table-view-refresh.test.ts +84 -0
  106. package/frontend-vue/tests/table-view-sources.test.ts +67 -0
  107. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  108. package/package.json +2 -2
  109. package/skills/dms/SKILL.md +14 -3
  110. package/skills/dms-pages/REFERENCE.md +17 -1
@@ -35,6 +35,8 @@ interface EmptyStateProps {
35
35
  interface EmptyStateSlots {
36
36
  /** Rich description, replaces `description`. */
37
37
  default?: () => unknown;
38
+ /** Under the text, above the actions: a snippet to start with. */
39
+ content?: () => unknown;
38
40
  /** Buttons or links under the text. */
39
41
  actions?: () => unknown;
40
42
  }
@@ -71,6 +73,7 @@ const theme = tv({
71
73
  well: "mb-1.5",
72
74
  title: "text-highlighted",
73
75
  description: "text-muted",
76
+ content: "mt-2.5 w-full max-w-[60ch] text-start",
74
77
  actions: "mt-2.5 flex flex-wrap items-center justify-center gap-2",
75
78
  },
76
79
  variants: {
@@ -106,8 +109,11 @@ const resolvedIcon = computed(() => props.icon ?? VARIANT_ICONS[props.variant]);
106
109
  const resolvedTone = computed<Tone>(
107
110
  () => props.tone ?? (props.variant === "error" ? "error" : "neutral"),
108
111
  );
109
- const hasDescription = computed(() => !!props.description || !!slots.default);
110
- const hasActions = computed(() => !!slots.actions || !!props.actions?.length);
112
+ // Read at each render rather than in a computed: the slots object is not
113
+ // reactive, so a computed would keep the slots of the first render and miss
114
+ // a slot the parent adds or removes later.
115
+ const hasDescription = (): boolean => !!props.description || !!slots.default;
116
+ const hasActions = (): boolean => !!slots.actions || !!props.actions?.length;
111
117
  </script>
112
118
 
113
119
  <template>
@@ -122,10 +128,13 @@ const hasActions = computed(() => !!slots.actions || !!props.actions?.length);
122
128
  :class="ui.well()"
123
129
  />
124
130
  <p :class="ui.title()">{{ props.title }}</p>
125
- <p v-if="hasDescription" :class="ui.description()">
131
+ <p v-if="hasDescription()" :class="ui.description()">
126
132
  <slot>{{ props.description }}</slot>
127
133
  </p>
128
- <div v-if="hasActions" :class="ui.actions()">
134
+ <div v-if="slots.content" :class="ui.content()">
135
+ <slot name="content" />
136
+ </div>
137
+ <div v-if="hasActions()" :class="ui.actions()">
129
138
  <slot name="actions">
130
139
  <UButton
131
140
  v-for="(action, index) in props.actions"
@@ -29,7 +29,9 @@ import { computed, useId, useSlots } from "vue";
29
29
  // Delete/Backspace remove the selection. Both paths only *emit* the
30
30
  // `delete-nodes` intent — the badge for its own node, Delete/Backspace for
31
31
  // the whole selection — the wrapper never mutates the consumer's
32
- // `nodes`/`edges`; the consumer applies the removal.
32
+ // `nodes`/`edges`; the consumer applies the removal. `deleteBadge: false`
33
+ // hides the badge and keeps the keyboard path, for a consumer that draws
34
+ // its own delete control.
33
35
  // - `zoomReadout` adds a live zoom percentage to the control bar that doubles
34
36
  // as a reset-to-100% button.
35
37
  //
@@ -119,6 +121,11 @@ interface Props {
119
121
  * Delete + Backspace.
120
122
  */
121
123
  deleteKeyCode?: DeleteKeyCode;
124
+ /**
125
+ * Draw the delete badge on selected nodes while `deletableNodes` is on.
126
+ * `false` leaves the keyboard path alone. Defaults to `true`.
127
+ */
128
+ deleteBadge?: boolean;
122
129
  /**
123
130
  * Add a live zoom percentage / reset-to-100% control to the control bar.
124
131
  * It renders inside the bar, so it requires `controls` to be enabled.
@@ -141,6 +148,7 @@ const props = withDefaults(defineProps<Props>(), {
141
148
  height: "100%",
142
149
  deletableNodes: false,
143
150
  deleteKeyCode: undefined,
151
+ deleteBadge: true,
144
152
  zoomReadout: false,
145
153
  });
146
154
 
@@ -198,7 +206,7 @@ const zoomPercent = computed(() =>
198
206
  // Project each selected node's top-right corner from flow space to screen
199
207
  // space through the live viewport transform, so badges track pan/zoom/drag.
200
208
  const deleteAffordances = computed<DeleteAffordance[]>(() => {
201
- if (!props.deletableNodes) return [];
209
+ if (!props.deletableNodes || !props.deleteBadge) return [];
202
210
  const {
203
211
  x: panX,
204
212
  y: panY,
@@ -295,7 +303,10 @@ defineExpose(flow);
295
303
  <slot />
296
304
  </VueFlow>
297
305
 
298
- <div v-if="deletableNodes" class="dms-flow-canvas__delete-layer">
306
+ <div
307
+ v-if="deletableNodes && deleteBadge"
308
+ class="dms-flow-canvas__delete-layer"
309
+ >
299
310
  <button
300
311
  v-for="affordance in deleteAffordances"
301
312
  :key="affordance.id"
@@ -3,6 +3,7 @@ import { getCurrentInstance, h, render } from "vue";
3
3
  import { useFormField } from "@nuxt/ui/composables/useFormField";
4
4
  import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
5
5
  import { useControlError } from "../../../build/composables/form/useControlError";
6
+ import { codeLanguageLabel } from "../../../build/utils/codeLanguages";
6
7
  import type {
7
8
  CodeEditor,
8
9
  CodeEditorCompletion,
@@ -47,13 +48,6 @@ const LINE_HEIGHT_PX = 19;
47
48
  // The editor's top and bottom padding, around its lines.
48
49
  const EDITOR_PADDING_PX = 16;
49
50
  const INVALID_JSON = "$dms.field_errors.invalid_json";
50
- const LANGUAGE_LABELS: Record<string, string> = {
51
- json: "JSON",
52
- sql: "SQL",
53
- html: "HTML",
54
- javascript: "JavaScript",
55
- text: "Text",
56
- };
57
51
 
58
52
  const host = useTemplateRef<HTMLElement>("host");
59
53
  const position = ref<CodeEditorPosition>({ line: 1, column: 1 });
@@ -188,7 +182,7 @@ onBeforeUnmount(() => {
188
182
  {{ t("dms.form.code.complete") }}
189
183
  </span>
190
184
  <span class="ms-auto">
191
- {{ LANGUAGE_LABELS[props.language] ?? props.language }} ·
185
+ {{ codeLanguageLabel(props.language) }} ·
192
186
  {{ t("dms.form.code.position", position) }}
193
187
  </span>
194
188
  </div>
@@ -3,6 +3,7 @@ import { computed } from "vue";
3
3
  import { regionalDateTimeFormat } from "#dms-core/app/utils/regional";
4
4
  import DmsStatusPill from "../status-pill/StatusPill.vue";
5
5
  import DmsAutoLink from "../link/AutoLink.vue";
6
+ import DmsCopyButton from "../copy/CopyButton.vue";
6
7
  import { DMS_TONE_TEXT } from "../../build/utils/tone";
7
8
  import type { Tone } from "../../types/tone";
8
9
 
@@ -36,6 +37,10 @@ export interface KeyValueItem {
36
37
  currency?: string;
37
38
  /** This value alone is still loading: a skeleton stands in for it. */
38
39
  loading?: boolean;
40
+ /** Draws a button copying the value as it is shown. */
41
+ copy?: boolean;
42
+ /** Text the copy button copies instead of the shown value; implies `copy`. */
43
+ copyValue?: string;
39
44
  }
40
45
 
41
46
  interface KeyValueListProps {
@@ -156,6 +161,14 @@ function valueClass(item: KeyValueItem): string[] {
156
161
  return ["wrap-anywhere", color, TYPE_CLASSES[type] ?? ""];
157
162
  }
158
163
 
164
+ // An empty value shows a dash, which is not worth copying.
165
+ function copyTextOf(item: KeyValueItem): string | undefined {
166
+ if (item.copyValue) return item.copyValue;
167
+ if (!item.copy) return undefined;
168
+ const formatted = format(item);
169
+ return formatted === EMPTY_VALUE ? undefined : formatted;
170
+ }
171
+
159
172
  function keyOf(item: KeyValueItem, index: number): string {
160
173
  return item.id ?? `${index}-${item.label}`;
161
174
  }
@@ -216,6 +229,12 @@ function keyOf(item: KeyValueItem, index: number): string {
216
229
  {{ item.detail }}
217
230
  </span>
218
231
  </template>
232
+ <!-- -my-1: the button is taller than a dense row's text line. -->
233
+ <DmsCopyButton
234
+ v-if="!props.loading && !item.loading && copyTextOf(item)"
235
+ :value="copyTextOf(item)!"
236
+ class="-my-1"
237
+ />
219
238
  </dd>
220
239
  </div>
221
240
  </template>
@@ -3,6 +3,10 @@
3
3
  // left (380px) and a detail panel on the right. The list is driven by `items`
4
4
  // + v-model; the detail is rendered by the default slot, which receives the
5
5
  // currently selected value.
6
+ //
7
+ // The panes sit side by side from 64rem of the layout's own width (the former
8
+ // `lg` viewport breakpoint), not the viewport's: a docked side panel or a
9
+ // narrow grid cell leaves less room than the screen suggests.
6
10
  interface MdItem {
7
11
  value: string | number;
8
12
  label: string;
@@ -23,59 +27,61 @@ defineSlots<{
23
27
  </script>
24
28
 
25
29
  <template>
26
- <div
27
- class="grid grid-cols-1 items-start gap-5 lg:grid-cols-[380px_minmax(0,1fr)]"
28
- >
29
- <!-- List pane -->
30
- <div class="dms-card p-2">
31
- <p
32
- v-if="listLabel"
33
- class="text-dimmed px-2.5 pt-2.5 pb-1 font-mono text-[10px] tracking-widest uppercase"
34
- >
35
- {{ listLabel }}
36
- </p>
37
- <ul class="flex flex-col gap-1.5">
38
- <li v-for="item in items" :key="item.value">
39
- <button
40
- type="button"
41
- class="flex w-full items-start gap-3 rounded-md border border-transparent px-3.5 py-3 text-left transition-colors"
42
- :class="
43
- selected === item.value
44
- ? 'border-primary bg-primary/10'
45
- : 'hover:bg-elevated'
46
- "
47
- @click="selected = item.value"
48
- >
49
- <span
50
- v-if="item.icon"
51
- class="grid size-[26px] shrink-0 place-items-center"
52
- :class="selected === item.value ? 'text-primary' : 'text-toned'"
30
+ <div class="@container">
31
+ <div
32
+ class="grid grid-cols-1 items-start gap-5 @5xl:grid-cols-[380px_minmax(0,1fr)]"
33
+ >
34
+ <!-- List pane -->
35
+ <div class="dms-card p-2">
36
+ <p
37
+ v-if="listLabel"
38
+ class="text-dimmed px-2.5 pt-2.5 pb-1 font-mono text-[10px] tracking-widest uppercase"
39
+ >
40
+ {{ listLabel }}
41
+ </p>
42
+ <ul class="flex flex-col gap-1.5">
43
+ <li v-for="item in items" :key="item.value">
44
+ <button
45
+ type="button"
46
+ class="flex w-full items-start gap-3 rounded-md border border-transparent px-3.5 py-3 text-left transition-colors"
47
+ :class="
48
+ selected === item.value
49
+ ? 'border-primary bg-primary/10'
50
+ : 'hover:bg-elevated'
51
+ "
52
+ @click="selected = item.value"
53
53
  >
54
- <UIcon
55
- :name="item.icon"
56
- class="size-[18px]"
57
- :aria-hidden="true"
58
- />
59
- </span>
60
- <span class="min-w-0">
61
- <span class="text-highlighted block text-sm font-semibold">
62
- {{ item.label }}
63
- </span>
64
54
  <span
65
- v-if="item.sublabel"
66
- class="text-muted mt-0.5 block truncate text-xs"
55
+ v-if="item.icon"
56
+ class="grid size-[26px] shrink-0 place-items-center"
57
+ :class="selected === item.value ? 'text-primary' : 'text-toned'"
67
58
  >
68
- {{ item.sublabel }}
59
+ <UIcon
60
+ :name="item.icon"
61
+ class="size-[18px]"
62
+ :aria-hidden="true"
63
+ />
69
64
  </span>
70
- </span>
71
- </button>
72
- </li>
73
- </ul>
74
- </div>
65
+ <span class="min-w-0">
66
+ <span class="text-highlighted block text-sm font-semibold">
67
+ {{ item.label }}
68
+ </span>
69
+ <span
70
+ v-if="item.sublabel"
71
+ class="text-muted mt-0.5 block truncate text-xs"
72
+ >
73
+ {{ item.sublabel }}
74
+ </span>
75
+ </span>
76
+ </button>
77
+ </li>
78
+ </ul>
79
+ </div>
75
80
 
76
- <!-- Detail pane -->
77
- <div class="dms-card overflow-hidden">
78
- <slot :selected="selected" />
81
+ <!-- Detail pane -->
82
+ <div class="dms-card overflow-hidden">
83
+ <slot :selected="selected" />
84
+ </div>
79
85
  </div>
80
86
  </div>
81
87
  </template>
@@ -11,28 +11,35 @@ import DmsBlockActions, {
11
11
  } from "../../build/components/blocks/BlockActions.vue";
12
12
  import { useChartFetch } from "../../composables/chart/useChartFetch";
13
13
  import { useWatch } from "../../../../dms-core/app/composables/watch/useWatch";
14
+ import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
14
15
  import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
16
+ import type { BlockText } from "../../../../dms-core/app/types/composed-text";
17
+
18
+ /** A segment as the page or the route sends it, its legend still to write. */
19
+ interface MeterBlockSegment extends Omit<MeterSegment, "label"> {
20
+ label?: BlockText;
21
+ }
15
22
 
16
23
  /** What a meter's data source answers: any subset of its figures. */
17
24
  interface MeterResponse {
18
25
  value?: number;
19
26
  max?: number;
20
- segments?: MeterSegment[];
21
- hint?: string;
22
- valueLabel?: string;
27
+ segments?: MeterBlockSegment[];
28
+ hint?: BlockText;
29
+ valueLabel?: BlockText;
23
30
  }
24
31
 
25
32
  // The page block behind the backend `Meter` builder: static figures or a
26
33
  // data source, optionally in a card. DmsMeter draws it.
27
34
  interface MeterBlockProps extends DefaultComponentProps {
28
35
  label?: string;
29
- hint?: string;
36
+ hint?: BlockText;
30
37
  value?: number;
31
38
  max?: number;
32
- segments?: MeterSegment[];
39
+ segments?: MeterBlockSegment[];
33
40
  legend?: boolean;
34
41
  format?: MeterFormat;
35
- valueLabel?: string;
42
+ valueLabel?: BlockText;
36
43
  tone?: MeterTone;
37
44
  warnAt?: number;
38
45
  errorAt?: number;
@@ -40,6 +47,10 @@ interface MeterBlockProps extends DefaultComponentProps {
40
47
  /** Endpoint answering `{ value?, max?, segments?, hint?, valueLabel? }`. */
41
48
  fetchUrl?: string;
42
49
  fetchUrlMethod?: string;
50
+ /** Id of the PeriodSelector the data follows. */
51
+ periodScope?: string;
52
+ /** Topics whose events make the block read `fetchUrl` again. */
53
+ realtimeTopic?: string | string[];
43
54
  /** Wraps the meter in a padded card. */
44
55
  card?: boolean;
45
56
  /** Link buttons pushed right of the legend ("Manage members"). */
@@ -61,11 +72,13 @@ const props = withDefaults(defineProps<MeterBlockProps>(), {
61
72
  size: "sm",
62
73
  fetchUrl: undefined,
63
74
  fetchUrlMethod: undefined,
75
+ periodScope: undefined,
76
+ realtimeTopic: undefined,
64
77
  card: false,
65
78
  actions: () => [],
66
79
  });
67
80
 
68
- const { processI18n } = useTranslation();
81
+ const { processText } = useComposedText();
69
82
  const { t } = useI18n();
70
83
 
71
84
  const { state: watchState } = useWatch(
@@ -76,12 +89,14 @@ const { state: watchState } = useWatch(
76
89
  const { data, isLoading, error, refresh } = useChartFetch<MeterResponse>({
77
90
  fetchUrl: props.fetchUrl,
78
91
  fetchUrlMethod: props.fetchUrlMethod,
92
+ periodScope: props.periodScope,
93
+ realtimeTopic: props.realtimeTopic,
79
94
  routeParams: () => props.routeParams,
80
95
  watchSource: () => JSON.stringify(watchState.value),
81
96
  });
82
97
 
83
- const translate = (text: string | undefined): string | undefined =>
84
- text ? processI18n(text) : undefined;
98
+ const translate = (text: BlockText | undefined): string | undefined =>
99
+ text ? processText(text) : undefined;
85
100
 
86
101
  const figures = computed(() => {
87
102
  const fetched = data.value ?? {};
@@ -20,16 +20,19 @@ const props = withDefaults(defineProps<TabProps>(), {
20
20
  persistState: false,
21
21
  stateKey: "tab",
22
22
  badgesUrl: undefined,
23
+ realtimeTopic: undefined,
23
24
  });
24
25
 
25
26
  // The badges a module counts for a record's tabs, in one request, again when
26
- // a watch action fires: each replaces its tab's static badge.
27
+ // a watch action or a realtime topic fires: each replaces its tab's static
28
+ // badge.
27
29
  const { state: watchState } = useWatch(
28
30
  props.watchActions || [],
29
31
  props.componentId,
30
32
  );
31
33
  const { data: fetchedBadges } = useChartFetch<TabBadges>({
32
34
  fetchUrl: props.badgesUrl,
35
+ realtimeTopic: props.realtimeTopic,
33
36
  routeParams: () => props.routeParams,
34
37
  watchSource: () => JSON.stringify(watchState.value),
35
38
  });
@@ -47,7 +47,6 @@ import {
47
47
  } from "../../composables/table-view/kanban";
48
48
  import { useTableRowActions } from "../../build/composables/table-view/useTableViewRowActions";
49
49
  import {
50
- buildTableDataKey,
51
50
  buildTableQuery,
52
51
  isFilterEffective,
53
52
  } from "../../build/composables/table-view/utils/tableQuery";
@@ -107,7 +106,9 @@ import { useTableViews } from "../../build/composables/table-view/useTableViews"
107
106
  import { useGroupedRows } from "../../build/composables/table-view/useGroupedRows";
108
107
  import { useTableFooter } from "../../build/composables/table-view/useTableFooter";
109
108
  import { useAccumulatedPages } from "../../build/composables/table-view/useAccumulatedPages";
109
+ import { usePersistedPagination } from "../../build/composables/table-view/usePersistedPagination";
110
110
  import { useTableRows } from "../../build/composables/table-view/useTableRows";
111
+ import { useRefreshTriggers } from "../../build/composables/blocks/useRefreshTriggers";
111
112
  import { useTableReorder } from "../../build/composables/table-view/useTableReorder";
112
113
  import {
113
114
  groupedOptionsFor,
@@ -118,7 +119,10 @@ import { readTableUrlKey } from "../../build/composables/table-view/utils/views"
118
119
  import TableViews, {
119
120
  type TableViewItem,
120
121
  } from "../../build/components/table/Views.vue";
121
- import { useServerRenderedAsyncData } from "../../build/composables/table-view/useServerRenderedAsyncData";
122
+ import {
123
+ useTableCountsData,
124
+ useTableListData,
125
+ } from "../../build/composables/table-view/useTableListData";
122
126
  import { useTableDataChanges } from "../../composables/table-view/useTableDataChanges";
123
127
 
124
128
  const REALTIME_ROW_TOPIC_PREFIX = "tableview:row:";
@@ -172,6 +176,8 @@ interface TableViewProps<T extends Data> extends TableViewConfig<T> {
172
176
  tableId?: string;
173
177
  /** The page carries no other table view: `?view=` / `?tab=` are its own. */
174
178
  isSoleTableView?: boolean;
179
+ /** Topics whose events make the table read its rows again. */
180
+ realtimeTopic?: string | string[];
175
181
  }
176
182
 
177
183
  const props = defineProps<TableViewProps<T>>();
@@ -347,12 +353,42 @@ const resolvedChrome = resolveTableChrome(props.layout, {
347
353
  });
348
354
  const GLOBAL_FILTER_DEBOUNCE_MS = 400;
349
355
 
350
- const paginationState = ref<PaginationState>(
351
- getPreference<PaginationState>(
352
- getTablePreferenceKey("pagination"),
353
- DEFAULT_PAGINATION,
354
- ),
355
- );
356
+ const route = useDmsRoute();
357
+
358
+ const DEFAULT_FILTER_MODE = "is";
359
+
360
+ const queryParamHiddenFilters = computed<TableFilter[]>(() => {
361
+ if (!queryParamFilters) return [];
362
+
363
+ return Object.entries(queryParamFilters)
364
+ .filter(([param]) => route.query[param] !== undefined)
365
+ .map(([param, config]) => ({
366
+ accessorKey: config.field,
367
+ mode: config.mode || DEFAULT_FILTER_MODE,
368
+ value: route.query[param] as string,
369
+ }));
370
+ });
371
+
372
+ const routeParamHiddenFilters = computed<TableFilter[]>(() => {
373
+ if (!routeParamFilters || !routeParams) return [];
374
+
375
+ return Object.entries(routeParamFilters)
376
+ .filter(([param]) => routeParams[param] !== undefined)
377
+ .map(([param, config]) => ({
378
+ accessorKey: config.field,
379
+ mode: config.mode || DEFAULT_FILTER_MODE,
380
+ value: routeParams[param] as string,
381
+ }));
382
+ });
383
+
384
+ // A hidden filter's value changed with the URL, in place or through a
385
+ // remount: the page reached among the previous rows lists from page 1.
386
+ const paginationState = usePersistedPagination({
387
+ preferences: { getPreference, setPreference },
388
+ preferenceKey: getTablePreferenceKey,
389
+ defaults: DEFAULT_PAGINATION,
390
+ scope: () => [queryParamHiddenFilters.value, routeParamHiddenFilters.value],
391
+ });
356
392
  const rowSelect = ref<RowSelectionState>({});
357
393
  // A column's grid header, for the sort messages.
358
394
  const columnLabel = (id: string): string => {
@@ -615,35 +651,11 @@ const tableRows = useTableRows<T>({
615
651
  location,
616
652
  source: props.source,
617
653
  columns: props.columns,
654
+ route: () => ({
655
+ routeParams: props.routeParams,
656
+ routeQuery: route.query as Record<string, unknown>,
657
+ }),
618
658
  });
619
- const route = useDmsRoute();
620
-
621
- const DEFAULT_FILTER_MODE = "is";
622
-
623
- const queryParamHiddenFilters = computed<TableFilter[]>(() => {
624
- if (!queryParamFilters) return [];
625
-
626
- return Object.entries(queryParamFilters)
627
- .filter(([param]) => route.query[param] !== undefined)
628
- .map(([param, config]) => ({
629
- accessorKey: config.field,
630
- mode: config.mode || DEFAULT_FILTER_MODE,
631
- value: route.query[param] as string,
632
- }));
633
- });
634
-
635
- const routeParamHiddenFilters = computed<TableFilter[]>(() => {
636
- if (!routeParamFilters || !routeParams) return [];
637
-
638
- return Object.entries(routeParamFilters)
639
- .filter(([param]) => routeParams[param] !== undefined)
640
- .map(([param, config]) => ({
641
- accessorKey: config.field,
642
- mode: config.mode || DEFAULT_FILTER_MODE,
643
- value: routeParams[param] as string,
644
- }));
645
- });
646
-
647
659
  // Quick filters: dropdowns of the toolbar over a column's values (a select's
648
660
  // items, a boolean, the rows a relation points to), writing that column's
649
661
  // filter like the filters row does.
@@ -788,34 +800,15 @@ const archiveQuery = computed(() =>
788
800
  archiveMode ? { showArchived: showArchived.value } : {},
789
801
  );
790
802
 
791
- const EMPTY_LIST_RESULT: TableViewListResponse<never> = {
792
- results: [],
793
- total: 0,
794
- offset: 0,
795
- limit: 0,
796
- };
797
-
798
- const tableDataKey = buildTableDataKey({
803
+ const { data, status, error, refresh } = await useTableListData<T>({
799
804
  componentId,
800
805
  pageId,
801
- query: queryRequest.value,
802
- archiveQuery: archiveQuery.value,
803
- isSelfManaged: isActiveDisplaySelfManaged.value,
806
+ rows: tableRows,
807
+ query: queryRequest,
808
+ archiveQuery,
809
+ isSelfManaged: isActiveDisplaySelfManaged,
804
810
  });
805
811
 
806
- const { data, status, error, refresh } = await useServerRenderedAsyncData(
807
- tableDataKey,
808
- (): Promise<TableViewListResponse<T>> => {
809
- // Self-managed displays (e.g. kanban) fetch their own data; skip the
810
- // shared list query for them.
811
- if (isActiveDisplaySelfManaged.value) {
812
- return Promise.resolve(EMPTY_LIST_RESULT);
813
- }
814
- return tableRows.list({ ...queryRequest.value, ...archiveQuery.value });
815
- },
816
- { watch: [queryRequest, archiveQuery, isActiveDisplaySelfManaged] },
817
- );
818
-
819
812
  // Nothing listed yet (a client navigation paints before the first page
820
813
  // arrives): the table draws its skeleton, never the empty state.
821
814
  const isFirstPageLoading = computed(() => data.value === null && !error.value);
@@ -949,19 +942,13 @@ const tabCountsQuery = computed(() => [
949
942
  // No default: `null` until the counts arrive, so the tabs draw placeholders
950
943
  // instead of a count of nothing.
951
944
  const { data: tabCountsData, refresh: refreshTabCounts } =
952
- await useServerRenderedAsyncData<Record<string, number>>(
953
- `table-view-${componentId}-${pageId}-tab-counts`,
954
- async () => {
955
- if (tabCountsQuery.value.length === 0) return {};
956
- return await tableRows.countBatch(
957
- tabCountsQuery.value.map(({ id, query }) => ({
958
- id,
959
- query: { ...query, ...archiveQuery.value },
960
- })),
961
- );
962
- },
963
- { watch: [tabCountsQuery, archiveQuery] },
964
- );
945
+ await useTableCountsData<T>({
946
+ componentId,
947
+ pageId,
948
+ rows: tableRows,
949
+ queries: tabCountsQuery,
950
+ archiveQuery,
951
+ });
965
952
 
966
953
  const viewItems = computed<TableViewItem[]>(() =>
967
954
  tableViewItems.value.map((view) => ({
@@ -1131,6 +1118,11 @@ const refreshAll = async () => {
1131
1118
  ]);
1132
1119
  };
1133
1120
 
1121
+ // The rows can move without the table seeing it (a batch, a job, another
1122
+ // block's action): a topic of the backend or a page refresh reads them again,
1123
+ // keeping the current rows on screen until the answer lands.
1124
+ useRefreshTriggers(props.realtimeTopic, () => void refreshAll());
1125
+
1134
1126
  // "Preview as role": a link tab to a page the previewed role could not open
1135
1127
  // is drawn locked, one to a page it opens partially is drawn partially
1136
1128
  // locked. Never outside a preview (`entryState` is null then).
@@ -1847,7 +1839,6 @@ defineShortcuts(
1847
1839
 
1848
1840
  const tableStatePreferences = {
1849
1841
  sorting,
1850
- pagination: paginationState,
1851
1842
  columnFilters: columnFilters,
1852
1843
  columnVisibility,
1853
1844
  filtersOpen: filtersRowOpen,
@@ -1980,6 +1971,7 @@ onMounted(() => {
1980
1971
  :quick-filters="resolvedQuickFilters"
1981
1972
  :footer="resolvedFooter"
1982
1973
  :empty-states="props.emptyStates"
1974
+ :hidden-filters-apply="queryParamHiddenFilters.length > 0"
1983
1975
  :default-page-size="props.pageSize"
1984
1976
  :archive-toggle="canToggleArchived"
1985
1977
  :displays="availableDisplays"
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from "vue";
3
3
  import type { TopListItem } from "../../../composables/chart/types";
4
+ import { useComposedText } from "../../../../../dms-core/app/composables/translation/useComposedText";
4
5
  // Imported rather than resolved from the registry: a registered component is
5
6
  // a lazy chunk of its own, fetched only when the data first shows it, so the
6
7
  // trend would pop in a beat after the row.
@@ -31,11 +32,11 @@ const props = withDefaults(defineProps<Props>(), {
31
32
 
32
33
  const PERCENT_UNIT = "%";
33
34
 
34
- const { processI18n } = useTranslation();
35
+ const { processText } = useComposedText();
35
36
 
36
- const titleDisplay = computed(() => processI18n(props.item.title));
37
+ const titleDisplay = computed(() => processText(props.item.title));
37
38
  const descriptionDisplay = computed(() =>
38
- props.item.description ? processI18n(props.item.description) : "",
39
+ props.item.description ? processText(props.item.description) : "",
39
40
  );
40
41
 
41
42
  const hasSparklineData = computed(
@@ -1,3 +1,5 @@
1
+ import type { BlockText } from "#dms-core/app/types/composed-text";
2
+
1
3
  /** Serializable X-axis settings, matching the DMS server contract. */
2
4
  export interface ChartXAxis {
3
5
  /** Requested intervals; ignored by ApexCharts on datetime axes. */
@@ -100,8 +102,9 @@ export interface KpiCardResponse {
100
102
 
101
103
  export interface TopListItem {
102
104
  id: string | number;
103
- title: string;
104
- description?: string;
105
+ /** A literal, a `$`-prefixed i18n key, or a composed text. */
106
+ title: BlockText;
107
+ description?: BlockText;
105
108
  value: number;
106
109
  delta?: number | null;
107
110
  sparkline?: number[];