@antelopejs/dms 0.7.1 → 0.7.3

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 +47 -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 +4 -3
  78. package/frontend-vue/pnpm-lock.yaml +8 -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
@@ -13,34 +13,78 @@ export interface ReplaceUrlVariablesContext {
13
13
  response?: Record<string, unknown>;
14
14
  }
15
15
 
16
+ type EncodeValue = (value: string) => string;
17
+
18
+ /** How each kind of route value is written into the text. */
19
+ interface TokenEncoding {
20
+ param: EncodeValue;
21
+ query: EncodeValue;
22
+ }
23
+
24
+ const keepValue: EncodeValue = (value) => value;
25
+
26
+ // A route parameter is a segment of the page path, which the router keeps
27
+ // percent-encoded: it is decoded first so that it is not encoded twice.
28
+ const encodeParam: EncodeValue = (value) => {
29
+ try {
30
+ return encodeURIComponent(decodeURIComponent(value));
31
+ } catch {
32
+ return encodeURIComponent(value);
33
+ }
34
+ };
35
+
36
+ const URL_ENCODING: TokenEncoding = {
37
+ param: encodeParam,
38
+ query: encodeURIComponent,
39
+ };
40
+ const TEXT_ENCODING: TokenEncoding = { param: keepValue, query: keepValue };
41
+
42
+ function fillRouteTokens(
43
+ text: string,
44
+ context: ReplaceUrlVariablesContext,
45
+ encoding: TokenEncoding,
46
+ ): string {
47
+ return text
48
+ .replace(PARAM_TOKEN, (match, key) => {
49
+ const value = context.routeParams?.[key];
50
+ return value ? encoding.param(value) : match;
51
+ })
52
+ .replace(QUERY_TOKEN, (match, key) => {
53
+ const value = context.routeQuery[key];
54
+ return typeof value === "string" ? encoding.query(value) : match;
55
+ });
56
+ }
57
+
58
+ // Written as answered: a response may hand back a whole path to open next
59
+ // (`redirectOnSuccess: "{{response.redirectPath}}"`), which encoding would break.
60
+ function fillResponseTokens(
61
+ text: string,
62
+ response: Record<string, unknown> | undefined,
63
+ ): string {
64
+ if (!response) return text;
65
+ return text.replace(RESPONSE_TOKEN, (match, key) => {
66
+ const value = response[key];
67
+ return typeof value === "string" || typeof value === "number"
68
+ ? String(value)
69
+ : match;
70
+ });
71
+ }
72
+
16
73
  /**
17
74
  * Fills the `{{params.X}}`, `{{query.X}}` and (with a response)
18
- * `{{response.X}}` tokens of a URL. A token with no value is left as written.
75
+ * `{{response.X}}` tokens of a URL. Route values are percent-encoded, so a
76
+ * value such as `GET /api/a?b=c` stays one query value or one path segment;
77
+ * response values are written as answered. A token with no value is left as
78
+ * written.
19
79
  */
20
80
  export function replaceUrlVariables(
21
81
  url: string,
22
82
  context: ReplaceUrlVariablesContext,
23
83
  ): string {
24
- let processedUrl = url.replace(
25
- PARAM_TOKEN,
26
- (match, key) => context.routeParams?.[key] || match,
84
+ return fillResponseTokens(
85
+ fillRouteTokens(url, context, URL_ENCODING),
86
+ context.response,
27
87
  );
28
-
29
- processedUrl = processedUrl.replace(QUERY_TOKEN, (match, key) => {
30
- const value = context.routeQuery[key];
31
- return typeof value === "string" ? value : match;
32
- });
33
-
34
- if (context.response) {
35
- processedUrl = processedUrl.replace(RESPONSE_TOKEN, (match, key) => {
36
- const value = context.response?.[key];
37
- return typeof value === "string" || typeof value === "number"
38
- ? String(value)
39
- : match;
40
- });
41
- }
42
-
43
- return processedUrl;
44
88
  }
45
89
 
46
90
  /** Whether a URL still names a token: written so, or left unresolved. */
@@ -61,3 +105,16 @@ export function resolveUrlVariables(
61
105
  const resolved = replaceUrlVariables(url, context);
62
106
  return hasUrlVariables(resolved) ? undefined : resolved;
63
107
  }
108
+
109
+ /**
110
+ * A plain value (not a URL) with its route tokens filled as they are, without
111
+ * encoding, or `undefined` while one is left: a submit default such as
112
+ * `{{query.status}}` is a field value.
113
+ */
114
+ export function resolveTextVariables(
115
+ text: string,
116
+ context: ReplaceUrlVariablesContext,
117
+ ): string | undefined {
118
+ const resolved = fillRouteTokens(text, context, TEXT_ENCODING);
119
+ return hasUrlVariables(resolved) ? undefined : resolved;
120
+ }
@@ -13,10 +13,12 @@ import {
13
13
  type ActivityFeedDay,
14
14
  type ActivityFeedItem,
15
15
  formatActivityTime,
16
+ activityText,
16
17
  groupActivityByDay,
17
- resolveActivityParams,
18
18
  } from "../../build/components/activity-feed/activityFeedDays";
19
+ import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
19
20
  import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
21
+ import type { BlockText } from "../../../../dms-core/app/types/composed-text";
20
22
  import type { BlockEmptyText } from "../../build/components/blocks/BlockStatus.vue";
21
23
 
22
24
  // The renderer's props are optional: DmsActivityFeed is both the backend
@@ -29,6 +31,10 @@ interface ActivityFeedProps extends Partial<DefaultComponentProps> {
29
31
  /** Endpoint answering `{ items }`, newest first. */
30
32
  fetchUrl?: string;
31
33
  fetchUrlMethod?: string;
34
+ /** Id of the PeriodSelector the data follows. */
35
+ periodScope?: string;
36
+ /** Topics whose events make the block read `fetchUrl` again. */
37
+ realtimeTopic?: string | string[];
32
38
  /**
33
39
  * Files the entries under day separators ("Today · Sep 29") with their
34
40
  * time; off, each entry shows how long ago it happened.
@@ -62,6 +68,8 @@ const props = withDefaults(defineProps<ActivityFeedProps>(), {
62
68
  items: undefined,
63
69
  fetchUrl: undefined,
64
70
  fetchUrlMethod: undefined,
71
+ periodScope: undefined,
72
+ realtimeTopic: undefined,
65
73
  groupByDay: true,
66
74
  maxItems: undefined,
67
75
  fillHeight: false,
@@ -79,6 +87,7 @@ const SKELETON_TITLE_WIDTHS = ["w-3/5", "w-1/2", "w-2/5"];
79
87
 
80
88
  const { t, locale } = useI18n();
81
89
  const { processI18n } = useTranslation();
90
+ const { processText } = useComposedText();
82
91
 
83
92
  const {
84
93
  items: list,
@@ -89,6 +98,8 @@ const {
89
98
  items: () => props.items,
90
99
  fetchUrl: props.fetchUrl,
91
100
  fetchUrlMethod: props.fetchUrlMethod,
101
+ periodScope: props.periodScope,
102
+ realtimeTopic: props.realtimeTopic,
92
103
  routeParams: () => props.routeParams,
93
104
  watchActions: props.watchActions,
94
105
  componentId: props.componentId,
@@ -108,15 +119,15 @@ const days = computed<ActivityFeedDay[]>(() =>
108
119
  );
109
120
 
110
121
  const trailingOf = (item: ActivityFeedItem): string | undefined => {
111
- if (item.time) return processI18n(item.time);
122
+ if (item.time) return processText(item.time);
112
123
  if (!item.date) return undefined;
113
124
  return props.groupByDay
114
125
  ? formatActivityTime(item.date, locale.value)
115
126
  : formatRelativeTime(item.date, t, locale.value);
116
127
  };
117
128
 
118
- const textOf = (item: ActivityFeedItem, text: string): string =>
119
- processI18n(text, resolveActivityParams(item.params, processI18n));
129
+ const textOf = (item: ActivityFeedItem, text: BlockText): string =>
130
+ processText(activityText(text, item.params));
120
131
 
121
132
  const metaOf = (item: ActivityFeedItem): string | undefined =>
122
133
  item.meta?.length
@@ -2,8 +2,13 @@
2
2
  import { computed, ref } from "vue";
3
3
  import DmsBanner from "../banner/Banner.vue";
4
4
  import DmsBlockActions, {
5
- type BlockAction,
5
+ type BlockActionItem,
6
+ type BlockButtonAction,
6
7
  } from "../../build/components/blocks/BlockActions.vue";
8
+ import { useActionTargets } from "../../build/composables/actions/useActionTargets";
9
+ import { useChartFetch } from "../../composables/chart/useChartFetch";
10
+ import { refreshPageBlocks } from "../../utils/blockRefresh";
11
+ import type { CustomButton } from "../../composables/table-view/types/custom-button";
7
12
  import { useWatch } from "../../../../dms-core/app/composables/watch/useWatch";
8
13
  import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
9
14
  import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
@@ -11,11 +16,8 @@ import type { BlockText } from "../../../../dms-core/app/types/composed-text";
11
16
 
12
17
  type BannerTone = "info" | "success" | "warning" | "error" | "primary";
13
18
 
14
- // `Banner` block (interface-dms `base/banner`): the v2 notice banner placed
15
- // on a page, with link actions and an optional dismiss that is remembered.
16
- // Dismissals live in a cookie (like the dashboard banners) so the server
17
- // render already leaves a dismissed banner out instead of flashing it.
18
- interface BannerBlockProps extends Partial<DefaultComponentProps> {
19
+ /** What a banner shows (interface-dms `BannerContent`). */
20
+ interface BannerContent {
19
21
  /** A string (`$` for an i18n key) or a composed text. */
20
22
  title?: BlockText;
21
23
  description?: BlockText;
@@ -24,7 +26,8 @@ interface BannerBlockProps extends Partial<DefaultComponentProps> {
24
26
  icon?: string;
25
27
  /** `md` boxed banner, `sm` compact one-line banner. */
26
28
  size?: "sm" | "md";
27
- actions?: BlockAction[];
29
+ /** Links, and buttons running a target when a route answers them. */
30
+ actions?: BlockActionItem[];
28
31
  dismissible?: boolean;
29
32
  /**
30
33
  * Remembers the dismissal under this key; change it to show the banner
@@ -33,6 +36,28 @@ interface BannerBlockProps extends Partial<DefaultComponentProps> {
33
36
  dismissKey?: string;
34
37
  }
35
38
 
39
+ /** The content drawn: the fetched fields over the options. */
40
+ interface ResolvedBanner extends BannerContent {
41
+ tone: BannerTone;
42
+ size: "sm" | "md";
43
+ actions: BlockActionItem[];
44
+ }
45
+
46
+ // `Banner` block (interface-dms `base/banner`): the v2 notice banner placed
47
+ // on a page, with actions and an optional dismiss that is remembered.
48
+ // Dismissals live in a cookie (like the dashboard banners) so the server
49
+ // render already leaves a dismissed banner out instead of flashing it. With a
50
+ // `fetchUrl` the route answers the content, or nothing for no banner.
51
+ interface BannerBlockProps
52
+ extends Partial<DefaultComponentProps>,
53
+ BannerContent {
54
+ /** Route answering a `BannerContent`, or `null` / `{}` / 204 to hide. */
55
+ fetchUrl?: string;
56
+ fetchUrlMethod?: string;
57
+ periodScope?: string;
58
+ realtimeTopic?: string | string[];
59
+ }
60
+
36
61
  const props = withDefaults(defineProps<BannerBlockProps>(), {
37
62
  title: undefined,
38
63
  description: undefined,
@@ -42,6 +67,10 @@ const props = withDefaults(defineProps<BannerBlockProps>(), {
42
67
  actions: () => [],
43
68
  dismissible: false,
44
69
  dismissKey: undefined,
70
+ fetchUrl: undefined,
71
+ fetchUrlMethod: undefined,
72
+ periodScope: undefined,
73
+ realtimeTopic: undefined,
45
74
  });
46
75
 
47
76
  const DISMISSED_COOKIE = "dms-dismissed-blocks";
@@ -49,6 +78,11 @@ const DISMISSED_MAX_AGE_S = 60 * 60 * 24 * 365;
49
78
  // Keeps the cookie well under the 4 KB limit; the oldest dismissals go first.
50
79
  const DISMISSED_MAX_KEYS = 40;
51
80
 
81
+ // Keys the actions' drawers and modals are opened under, for a banner placed
82
+ // outside a page (a preview).
83
+ const FALLBACK_COMPONENT_ID = "banner";
84
+ const FALLBACK_PAGE_ID = "page";
85
+
52
86
  const TONE_ICONS: Record<BannerTone, string> = {
53
87
  info: "i-ph-info",
54
88
  success: "i-ph-check-circle",
@@ -57,7 +91,51 @@ const TONE_ICONS: Record<BannerTone, string> = {
57
91
  primary: "i-ph-sparkle",
58
92
  };
59
93
 
60
- useWatch(props.watchActions || [], props.componentId);
94
+ const { state: watchState } = useWatch(
95
+ props.watchActions || [],
96
+ props.componentId,
97
+ );
98
+
99
+ const { data } = useChartFetch<BannerContent | null>({
100
+ fetchUrl: props.fetchUrl,
101
+ fetchUrlMethod: props.fetchUrlMethod,
102
+ periodScope: props.periodScope,
103
+ realtimeTopic: props.realtimeTopic,
104
+ routeParams: () => props.routeParams,
105
+ watchSource: () => JSON.stringify(watchState.value),
106
+ });
107
+
108
+ // A button that changed something changes what the page shows: its blocks,
109
+ // this banner included, read their data again.
110
+ const { handleCustomButton } = useActionTargets({
111
+ api: useAuthFetch().$authFetch,
112
+ componentId: props.componentId ?? FALLBACK_COMPONENT_ID,
113
+ pageId: props.pageId ?? FALLBACK_PAGE_ID,
114
+ refreshCallback: refreshPageBlocks,
115
+ handleApiError: (error, title) => useApiError(error, { title }),
116
+ });
117
+
118
+ // A fetched banner is an alert: it draws nothing until its route answered
119
+ // something (a skeleton would flash a banner that may not exist), and nothing
120
+ // when the answer is empty.
121
+ const hasFetchedContent = computed(
122
+ () => Boolean(data.value) && Object.keys(data.value as object).length > 0,
123
+ );
124
+
125
+ // Field by field, the route's answer over the options.
126
+ const banner = computed<ResolvedBanner>(() => {
127
+ const fetched: BannerContent = data.value ?? {};
128
+ return {
129
+ title: fetched.title ?? props.title,
130
+ description: fetched.description ?? props.description,
131
+ tone: fetched.tone ?? props.tone,
132
+ icon: fetched.icon ?? props.icon,
133
+ size: fetched.size ?? props.size,
134
+ actions: fetched.actions ?? props.actions,
135
+ dismissible: fetched.dismissible ?? props.dismissible,
136
+ dismissKey: fetched.dismissKey ?? props.dismissKey,
137
+ };
138
+ });
61
139
 
62
140
  const { processText } = useComposedText();
63
141
  const dismissed = useDmsCookie<string[]>(DISMISSED_COOKIE, {
@@ -69,19 +147,22 @@ const isClosed = ref(false);
69
147
 
70
148
  const storageKey = computed(
71
149
  () =>
72
- props.dismissKey ||
150
+ banner.value.dismissKey ||
73
151
  [props.pageId, props.componentId].filter(Boolean).join(":"),
74
152
  );
75
- const isVisible = computed(
153
+ const isDismissed = computed(
76
154
  () =>
77
- !isClosed.value &&
78
- !(
79
- props.dismissible &&
80
- storageKey.value &&
81
- dismissed.value.includes(storageKey.value)
155
+ isClosed.value ||
156
+ Boolean(
157
+ banner.value.dismissible &&
158
+ storageKey.value &&
159
+ dismissed.value.includes(storageKey.value),
82
160
  ),
83
161
  );
84
- const isCompact = computed(() => props.size === "sm");
162
+ const isVisible = computed(
163
+ () => !isDismissed.value && (!props.fetchUrl || hasFetchedContent.value),
164
+ );
165
+ const isCompact = computed(() => banner.value.size === "sm");
85
166
 
86
167
  function dismiss(): void {
87
168
  isClosed.value = true;
@@ -89,31 +170,36 @@ function dismiss(): void {
89
170
  if (!key || dismissed.value.includes(key)) return;
90
171
  dismissed.value = [...dismissed.value, key].slice(-DISMISSED_MAX_KEYS);
91
172
  }
173
+
174
+ function press(action: BlockButtonAction): void {
175
+ handleCustomButton(action as CustomButton);
176
+ }
92
177
  </script>
93
178
 
94
179
  <template>
95
180
  <DmsBanner
96
181
  v-if="isVisible"
97
- :title="props.title ? processText(props.title) : undefined"
182
+ :title="banner.title ? processText(banner.title) : undefined"
98
183
  :description="
99
- props.description ? processText(props.description) : undefined
184
+ banner.description ? processText(banner.description) : undefined
100
185
  "
101
- :icon="props.icon ?? TONE_ICONS[props.tone]"
102
- :tone="props.tone"
103
- :size="props.size"
104
- :dismissible="props.dismissible"
186
+ :icon="banner.icon ?? TONE_ICONS[banner.tone]"
187
+ :tone="banner.tone"
188
+ :size="banner.size"
189
+ :dismissible="banner.dismissible"
105
190
  @dismiss="dismiss"
106
191
  >
107
- <template v-if="props.actions.length" #actions>
192
+ <template v-if="banner.actions.length" #actions>
108
193
  <DmsBlockActions
109
- :actions="props.actions"
194
+ :actions="banner.actions"
110
195
  :size="isCompact ? 'xs' : 'sm'"
111
196
  lead-position="last"
112
197
  :lead-variant="isCompact ? 'ghost' : 'solid'"
113
198
  :lead-color="
114
- isCompact ? 'neutral' : props.tone === 'error' ? 'error' : 'primary'
199
+ isCompact ? 'neutral' : banner.tone === 'error' ? 'error' : 'primary'
115
200
  "
116
201
  :rest-variant="isCompact ? 'ghost' : 'outline'"
202
+ @press="press"
117
203
  />
118
204
  </template>
119
205
  </DmsBanner>
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import DmsCodeSnippet from "../code-snippet/CodeSnippet.vue";
4
+ import DmsBlockStatus from "../../build/components/blocks/BlockStatus.vue";
5
+ import { useChartFetch } from "../../composables/chart/useChartFetch";
6
+ import { useWatch } from "../../../../dms-core/app/composables/watch/useWatch";
7
+ import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
8
+ import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
9
+ import type { BlockText } from "../../../../dms-core/app/types/composed-text";
10
+
11
+ /** What the block's data source answers (interface-dms `CodeBlockResponse`). */
12
+ interface CodeBlockResponse {
13
+ code?: string;
14
+ language?: string;
15
+ }
16
+
17
+ // `CodeBlock` block (interface-dms `base/code-block`): read-only code with
18
+ // highlighting and a copy button, written in the options or read from a
19
+ // route answering `{ code, language? }`. DmsCodeSnippet draws it.
20
+ interface CodeBlockProps extends DefaultComponentProps {
21
+ code?: string;
22
+ language?: string;
23
+ copy?: boolean;
24
+ /** A string (`$` for an i18n key) or a composed text. */
25
+ title?: BlockText;
26
+ maxLines?: number;
27
+ wrap?: boolean;
28
+ fetchUrl?: string;
29
+ fetchUrlMethod?: string;
30
+ }
31
+
32
+ const props = withDefaults(defineProps<CodeBlockProps>(), {
33
+ code: "",
34
+ language: undefined,
35
+ copy: true,
36
+ title: undefined,
37
+ maxLines: undefined,
38
+ wrap: false,
39
+ fetchUrl: undefined,
40
+ fetchUrlMethod: undefined,
41
+ });
42
+
43
+ // The placeholder lines while the route answers, at the code's line height.
44
+ const SKELETON_LINE_WIDTHS = ["w-2/3", "w-1/2", "w-3/4"];
45
+
46
+ const { processText } = useComposedText();
47
+ const { state: watchState } = useWatch(
48
+ props.watchActions || [],
49
+ props.componentId,
50
+ );
51
+
52
+ const { data, isLoading, error, refresh } = useChartFetch<CodeBlockResponse>({
53
+ fetchUrl: props.fetchUrl,
54
+ fetchUrlMethod: props.fetchUrlMethod,
55
+ routeParams: () => props.routeParams,
56
+ watchSource: () => JSON.stringify(watchState.value),
57
+ });
58
+
59
+ const code = computed(() => data.value?.code ?? props.code);
60
+ const language = computed(() => data.value?.language ?? props.language);
61
+ const title = computed(() =>
62
+ props.title ? processText(props.title) : undefined,
63
+ );
64
+ const showSkeleton = computed(() => isLoading.value && !data.value);
65
+ // A failed first load shows a retry in place of the code; a failed refetch
66
+ // keeps the code on screen.
67
+ const hasError = computed(
68
+ () => !isLoading.value && !data.value && Boolean(error.value),
69
+ );
70
+ </script>
71
+
72
+ <template>
73
+ <DmsBlockStatus v-if="hasError" state="error" @retry="refresh()" />
74
+ <div
75
+ v-else-if="showSkeleton"
76
+ class="border-default grid gap-2 rounded-[10px] border px-3.5 py-3"
77
+ aria-busy="true"
78
+ >
79
+ <USkeleton
80
+ v-for="width in SKELETON_LINE_WIDTHS"
81
+ :key="width"
82
+ class="h-3"
83
+ :class="width"
84
+ />
85
+ </div>
86
+ <DmsCodeSnippet
87
+ v-else
88
+ :code="code"
89
+ :language="language"
90
+ :copy="props.copy"
91
+ :title="title"
92
+ :max-lines="props.maxLines"
93
+ :wrap="props.wrap"
94
+ />
95
+ </template>
@@ -4,6 +4,7 @@ import DmsEmptyState, {
4
4
  type EmptyStateSize,
5
5
  type EmptyStateVariant,
6
6
  } from "../empty-state/EmptyState.vue";
7
+ import DmsCodeSnippet from "../code-snippet/CodeSnippet.vue";
7
8
  import DmsBlockActions, {
8
9
  type BlockAction,
9
10
  } from "../../build/components/blocks/BlockActions.vue";
@@ -15,6 +16,13 @@ import type { DefaultComponentProps } from "../../../../dms-core/app/types/compo
15
16
  // .c-empty placed on a page — a first-run screen, a "nothing here yet" panel
16
17
  // — with link actions. In a card by default. Texts follow the `$` i18n-key
17
18
  // convention.
19
+ /** A snippet under the text (interface-dms `EmptyStateCode`). */
20
+ interface EmptyStateCode {
21
+ content: string;
22
+ language?: string;
23
+ copy?: boolean;
24
+ }
25
+
18
26
  interface EmptyStateBlockProps extends Partial<DefaultComponentProps> {
19
27
  title?: string;
20
28
  description?: string;
@@ -25,6 +33,8 @@ interface EmptyStateBlockProps extends Partial<DefaultComponentProps> {
25
33
  hatched?: boolean;
26
34
  size?: EmptyStateSize;
27
35
  actions?: BlockAction[];
36
+ /** A snippet between the text and the actions. */
37
+ code?: EmptyStateCode;
28
38
  /** Card surface around it; off when it sits inside a Card block. */
29
39
  card?: boolean;
30
40
  }
@@ -38,6 +48,7 @@ const props = withDefaults(defineProps<EmptyStateBlockProps>(), {
38
48
  hatched: false,
39
49
  size: "md",
40
50
  actions: () => [],
51
+ code: undefined,
41
52
  card: true,
42
53
  });
43
54
 
@@ -61,6 +72,13 @@ const description = computed(() =>
61
72
  :hatched="props.hatched"
62
73
  :size="props.size"
63
74
  >
75
+ <template v-if="props.code" #content>
76
+ <DmsCodeSnippet
77
+ :code="props.code.content"
78
+ :language="props.code.language"
79
+ :copy="props.code.copy ?? true"
80
+ />
81
+ </template>
64
82
  <template v-if="props.actions.length" #actions>
65
83
  <DmsBlockActions :actions="props.actions" size="sm" />
66
84
  </template>
@@ -35,6 +35,10 @@ interface KeyValueListBlockProps extends DefaultComponentProps {
35
35
  card?: boolean;
36
36
  fetchUrl?: string;
37
37
  fetchUrlMethod?: string;
38
+ /** Id of the PeriodSelector the data follows. */
39
+ periodScope?: string;
40
+ /** Topics whose events make the block read `fetchUrl` again. */
41
+ realtimeTopic?: string | string[];
38
42
  /** Shown when there is nothing to list. */
39
43
  empty?: BlockEmptyText;
40
44
  /**
@@ -53,6 +57,8 @@ const props = withDefaults(defineProps<KeyValueListBlockProps>(), {
53
57
  card: true,
54
58
  fetchUrl: undefined,
55
59
  fetchUrlMethod: undefined,
60
+ periodScope: undefined,
61
+ realtimeTopic: undefined,
56
62
  empty: undefined,
57
63
  // A fetched list's length is unknown until it lands: draw the rows a record
58
64
  // card usually lists (customer, ids, status, amounts, dates), not a stub.
@@ -68,6 +74,8 @@ const { items, isPending, hasError, refresh } =
68
74
  items: () => props.items,
69
75
  fetchUrl: props.fetchUrl,
70
76
  fetchUrlMethod: props.fetchUrlMethod,
77
+ periodScope: props.periodScope,
78
+ realtimeTopic: props.realtimeTopic,
71
79
  routeParams: () => props.routeParams,
72
80
  watchActions: props.watchActions,
73
81
  componentId: props.componentId,
@@ -14,22 +14,24 @@ import DmsBlockStatus, {
14
14
  import { useBlockItems } from "../../build/composables/blocks/useBlockItems";
15
15
  import type { IconWellTone } from "../icon-well/IconWell.vue";
16
16
  import type { Tone } from "../../types/tone";
17
+ import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
17
18
  import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
19
+ import type { BlockText } from "../../../../dms-core/app/types/composed-text";
18
20
 
19
21
  // `NavCardGrid` block (interface-dms `base/nav-card-grid`): a responsive grid
20
22
  // of navigation cards (v2 settings overview, a module's home), static or from
21
23
  // `fetchUrl` (`{ items }`) so each card can carry its target's live state.
22
24
  interface NavCardGridItem {
23
25
  id?: string;
24
- title: string;
25
- description?: string;
26
+ title: BlockText;
27
+ description?: BlockText;
26
28
  icon: string;
27
29
  iconTone?: IconWellTone;
28
30
  to: string;
29
- state?: string;
31
+ state?: BlockText;
30
32
  stateTone?: Tone;
31
- tag?: string;
32
- readout?: string[];
33
+ tag?: BlockText;
34
+ readout?: BlockText[];
33
35
  /** The page's navigation badge (a category's cards), in the top corner. */
34
36
  badge?: string;
35
37
  badgeTone?: Tone;
@@ -49,6 +51,10 @@ interface NavCardGridBlockProps extends DefaultComponentProps {
49
51
  description?: string;
50
52
  fetchUrl?: string;
51
53
  fetchUrlMethod?: string;
54
+ /** Id of the PeriodSelector the data follows. */
55
+ periodScope?: string;
56
+ /** Topics whose events make the block read `fetchUrl` again. */
57
+ realtimeTopic?: string | string[];
52
58
  /** Shown when there is nothing to list. */
53
59
  empty?: BlockEmptyText;
54
60
  /**
@@ -66,6 +72,8 @@ const props = withDefaults(defineProps<NavCardGridBlockProps>(), {
66
72
  description: undefined,
67
73
  fetchUrl: undefined,
68
74
  fetchUrlMethod: undefined,
75
+ periodScope: undefined,
76
+ realtimeTopic: undefined,
69
77
  empty: undefined,
70
78
  skeletonCount: undefined,
71
79
  });
@@ -77,6 +85,7 @@ const MIN_CARD_WIDTH = "16rem";
77
85
  const CARD_GAP = "0.75rem";
78
86
 
79
87
  const { processI18n } = useTranslation();
88
+ const { processText } = useComposedText();
80
89
 
81
90
  const { cards: categoryCards } = useCategoryNavCards(() => props.categoryId);
82
91
  const veil = usePreviewEntryVeil();
@@ -90,6 +99,8 @@ const {
90
99
  items: () => props.items,
91
100
  fetchUrl: props.fetchUrl,
92
101
  fetchUrlMethod: props.fetchUrlMethod,
102
+ periodScope: props.periodScope,
103
+ realtimeTopic: props.realtimeTopic,
93
104
  routeParams: () => props.routeParams,
94
105
  watchActions: props.watchActions,
95
106
  componentId: props.componentId,
@@ -119,10 +130,11 @@ const cards = computed(() =>
119
130
  items.value.map((item, index) => ({
120
131
  ...item,
121
132
  key: item.id ?? `${index}-${item.to}`,
122
- title: processI18n(item.title ?? ""),
123
- description: item.description ? processI18n(item.description) : undefined,
124
- state: item.state ? processI18n(item.state) : undefined,
125
- readout: item.readout?.map((line) => processI18n(line)),
133
+ title: processText(item.title),
134
+ description: item.description ? processText(item.description) : undefined,
135
+ state: item.state ? processText(item.state) : undefined,
136
+ tag: item.tag ? processText(item.tag) : undefined,
137
+ readout: item.readout?.map((line) => processText(line)),
126
138
  })),
127
139
  );
128
140
  // v2 .sx-grid: cards closing on a state line run a tighter rhythm, the whole