@antelopejs/dms 0.6.0 → 0.6.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.
- package/CHANGELOG.md +33 -0
- package/dist/implementations/dms/layout-banners.d.ts +4 -1
- package/dist/implementations/dms/layout-banners.js +33 -18
- package/dist/implementations/dms/layout-banners.js.map +1 -1
- package/dist/test/unit/implementations/dms/layout-banners.test.js +76 -0
- package/dist/test/unit/implementations/dms/layout-banners.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/column-display.test.js +70 -0
- package/dist/test/unit/interfaces/dms-base/column-display.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js +38 -0
- package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/composables/translation/useComposedText.ts +25 -0
- package/frontend-vue/layers/dms-core/app/types/composed-text.ts +64 -0
- package/frontend-vue/layers/dms-core/app/utils/composedText.ts +227 -0
- package/frontend-vue/layers/dms-core/app/utils/formatter.ts +49 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +17 -3
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +5 -1
- package/frontend-vue/layers/dms-layout/app/types/page.ts +7 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/IdentityCell.vue +36 -7
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/registerBlockFunctions.ts +7 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +11 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/cellHelpers.ts +57 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +19 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +22 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/twoLineCell.ts +118 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/useColumnValueRenderer.ts +5 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/utils/relativeDate.ts +4 -35
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +63 -0
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +8 -5
- package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +38 -17
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +26 -22
- package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +78 -16
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +21 -53
- package/frontend-vue/layers/dms-ui/app/plugins/register.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +28 -0
- package/frontend-vue/tests/block-page-refresh.test.ts +128 -0
- package/frontend-vue/tests/block-route-params.test.ts +190 -0
- package/frontend-vue/tests/composed-text-blocks.test.ts +205 -0
- package/frontend-vue/tests/composed-text.test.ts +265 -0
- package/frontend-vue/tests/layout-banners.test.ts +56 -0
- package/frontend-vue/tests/table-cell-sublines.test.ts +466 -0
- package/frontend-vue/tests/table-metric-cells.test.ts +4 -0
- package/package.json +2 -2
- package/skills/dms/SKILL.md +10 -0
|
@@ -10,11 +10,14 @@ import { mapRelationBeforeState, sameRelationValue } from "./relationValue";
|
|
|
10
10
|
import {
|
|
11
11
|
isSet,
|
|
12
12
|
readRowField,
|
|
13
|
+
readSubline,
|
|
13
14
|
type Row,
|
|
14
15
|
stringOf,
|
|
15
16
|
toneClass,
|
|
16
17
|
} from "./cellHelpers";
|
|
17
18
|
import { registerMetricCellTypes } from "./metricCells";
|
|
19
|
+
import { registerTwoLineCellType } from "./twoLineCell";
|
|
20
|
+
import { useComposedText } from "../../../../../dms-core/app/composables/translation/useComposedText";
|
|
18
21
|
import StatusPill from "../../../components/status-pill/StatusPill.vue";
|
|
19
22
|
import IdentityCell from "../../components/table-view/IdentityCell.vue";
|
|
20
23
|
import {
|
|
@@ -381,10 +384,15 @@ interface IdentityOptions {
|
|
|
381
384
|
avatarField?: string;
|
|
382
385
|
icon?: string;
|
|
383
386
|
subtitleField?: string;
|
|
387
|
+
subtitleTone?: string;
|
|
384
388
|
selfField?: string;
|
|
385
389
|
selfLabel?: string;
|
|
386
390
|
badges?: IdentityBadgeOption[];
|
|
387
391
|
storage?: string;
|
|
392
|
+
/** Name drawn, muted, for a row without a value. */
|
|
393
|
+
emptyLabel?: string;
|
|
394
|
+
/** Icon tile of a row without a value. */
|
|
395
|
+
emptyIcon?: string;
|
|
388
396
|
}
|
|
389
397
|
|
|
390
398
|
const matchesBadge = (badge: IdentityBadgeOption, value: unknown): boolean =>
|
|
@@ -394,17 +402,28 @@ const matchesBadge = (badge: IdentityBadgeOption, value: unknown): boolean =>
|
|
|
394
402
|
|
|
395
403
|
/**
|
|
396
404
|
* `identity`: avatar or icon tile, the value as the name, a "You" tag on the
|
|
397
|
-
* signed-in user's row, badges and a secondary line
|
|
405
|
+
* signed-in user's row, badges and a secondary line (an address as written,
|
|
406
|
+
* or a composed text in its tone), all read off the row; `emptyLabel` and
|
|
407
|
+
* `emptyIcon` stand for a row without a value.
|
|
398
408
|
*/
|
|
399
409
|
function renderIdentity(value: unknown, options: unknown, row: Row) {
|
|
400
410
|
const { processI18n } = useTranslation();
|
|
411
|
+
const { processText } = useComposedText();
|
|
401
412
|
const opts = (options ?? {}) as IdentityOptions;
|
|
402
413
|
const badges = (opts.badges ?? [])
|
|
403
414
|
.filter((badge) => matchesBadge(badge, readRowField(row, badge.field)))
|
|
404
415
|
.map((badge) => ({ label: processI18n(badge.label), color: badge.tone }));
|
|
416
|
+
// An address or an id is data: only a composed text is translated.
|
|
417
|
+
const subtitle = readSubline(readRowField(row, opts.subtitleField), {
|
|
418
|
+
processText,
|
|
419
|
+
isStringTranslated: false,
|
|
420
|
+
});
|
|
405
421
|
return h(IdentityCell, {
|
|
406
422
|
title: stringOf(value) ?? "",
|
|
407
|
-
subtitle:
|
|
423
|
+
subtitle: subtitle?.text ?? "",
|
|
424
|
+
subtitleClass: toneClass(subtitle?.tone ?? opts.subtitleTone, "dimmed"),
|
|
425
|
+
emptyLabel: opts.emptyLabel ? processI18n(opts.emptyLabel) : undefined,
|
|
426
|
+
emptyIcon: opts.emptyIcon,
|
|
408
427
|
avatar: readRowField(row, opts.avatarField) as
|
|
409
428
|
| { key: string }
|
|
410
429
|
| string
|
|
@@ -843,5 +862,6 @@ export function registerDefaultDataTypes() {
|
|
|
843
862
|
registerDisplayOnlyTypes(registerDataType);
|
|
844
863
|
registerCellTypes(registerDataType);
|
|
845
864
|
registerMetricCellTypes(registerDataType);
|
|
865
|
+
registerTwoLineCellType(registerDataType);
|
|
846
866
|
registerFieldTypes(registerDataType);
|
|
847
867
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { h, type VNodeChild } from "vue";
|
|
2
|
+
import type { DataType } from "#dms-core/app/composables/data-types/useDataType";
|
|
3
|
+
import { useComposedText } from "../../../../../dms-core/app/composables/translation/useComposedText";
|
|
4
|
+
import {
|
|
5
|
+
type ComposableText,
|
|
6
|
+
isComposedText,
|
|
7
|
+
} from "../../../../../dms-core/app/utils/composedText";
|
|
8
|
+
import { readRowField, readSubline, toneClass, type Row } from "./cellHelpers";
|
|
9
|
+
|
|
10
|
+
const EMPTY = "—";
|
|
11
|
+
const PRIMARY_CLASS = "text-highlighted truncate text-[13px] font-medium";
|
|
12
|
+
const EMPTY_PRIMARY_CLASS = "text-dimmed truncate text-[13px]";
|
|
13
|
+
const SUB_CLASS = "truncate text-xs";
|
|
14
|
+
|
|
15
|
+
interface TwoLineOptions {
|
|
16
|
+
/** Row field holding the primary line, in place of the column's value. */
|
|
17
|
+
primaryField?: string;
|
|
18
|
+
/** Row field holding the secondary line. */
|
|
19
|
+
subField?: string;
|
|
20
|
+
/** Tone of the secondary line when the row gives none. */
|
|
21
|
+
subTone?: string;
|
|
22
|
+
/** Text of an empty primary line. */
|
|
23
|
+
emptyLabel?: string;
|
|
24
|
+
/** The column's own data type, which writes its value. */
|
|
25
|
+
columnType?: string;
|
|
26
|
+
/** The column's own type options (a select's items). */
|
|
27
|
+
typeOptions?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isBlank = (value: unknown): boolean =>
|
|
31
|
+
value === null || value === undefined || value === "";
|
|
32
|
+
|
|
33
|
+
/** The column's value written by the column's own data type. */
|
|
34
|
+
function formatColumnValue(
|
|
35
|
+
value: unknown,
|
|
36
|
+
locale: string,
|
|
37
|
+
opts: TwoLineOptions,
|
|
38
|
+
row: Row,
|
|
39
|
+
): VNodeChild {
|
|
40
|
+
const { getDataType } = useDataTypes();
|
|
41
|
+
const formatter = opts.columnType
|
|
42
|
+
? getDataType(opts.columnType)?.formatter
|
|
43
|
+
: undefined;
|
|
44
|
+
const format = formatter?.cell ?? formatter?.default;
|
|
45
|
+
if (!format) return String(value);
|
|
46
|
+
return format(value, locale, opts.typeOptions, row) as VNodeChild;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The primary line, or nothing when the row has none to show. */
|
|
50
|
+
function readPrimary(
|
|
51
|
+
value: unknown,
|
|
52
|
+
locale: string,
|
|
53
|
+
opts: TwoLineOptions,
|
|
54
|
+
row: Row,
|
|
55
|
+
): VNodeChild | undefined {
|
|
56
|
+
const { processText } = useComposedText();
|
|
57
|
+
if (opts.primaryField) {
|
|
58
|
+
const text = processText(
|
|
59
|
+
readRowField(row, opts.primaryField) as ComposableText,
|
|
60
|
+
);
|
|
61
|
+
return text || undefined;
|
|
62
|
+
}
|
|
63
|
+
if (isBlank(value)) return undefined;
|
|
64
|
+
if (isComposedText(value)) return processText(value);
|
|
65
|
+
return formatColumnValue(value, locale, opts, row);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `two_line`: a primary line — the column's value in its own format, or a row
|
|
70
|
+
* field's text — over a secondary line read off the row, in the row's tone,
|
|
71
|
+
* else `subTone`, else muted. No avatar.
|
|
72
|
+
*/
|
|
73
|
+
function renderTwoLine(
|
|
74
|
+
value: unknown,
|
|
75
|
+
locale: string,
|
|
76
|
+
options: unknown,
|
|
77
|
+
row: Row,
|
|
78
|
+
) {
|
|
79
|
+
const { processText } = useComposedText();
|
|
80
|
+
const opts = (options ?? {}) as TwoLineOptions;
|
|
81
|
+
const primary = readPrimary(value, locale, opts, row);
|
|
82
|
+
const sub = readSubline(readRowField(row, opts.subField), {
|
|
83
|
+
processText,
|
|
84
|
+
isStringTranslated: true,
|
|
85
|
+
});
|
|
86
|
+
return h("span", { class: "flex min-w-0 flex-col leading-tight" }, [
|
|
87
|
+
primary === undefined
|
|
88
|
+
? h(
|
|
89
|
+
"span",
|
|
90
|
+
{ class: EMPTY_PRIMARY_CLASS },
|
|
91
|
+
opts.emptyLabel ? processText(opts.emptyLabel) : EMPTY,
|
|
92
|
+
)
|
|
93
|
+
: h("span", { class: PRIMARY_CLASS }, [primary]),
|
|
94
|
+
sub
|
|
95
|
+
? h(
|
|
96
|
+
"span",
|
|
97
|
+
{
|
|
98
|
+
class: [SUB_CLASS, toneClass(sub.tone ?? opts.subTone, "muted")],
|
|
99
|
+
title: sub.text,
|
|
100
|
+
},
|
|
101
|
+
sub.text,
|
|
102
|
+
)
|
|
103
|
+
: null,
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The `two_line` cell data type. It draws an empty value too: its primary
|
|
109
|
+
* line may come from another field, and its secondary line always does.
|
|
110
|
+
*/
|
|
111
|
+
export const registerTwoLineCellType = (
|
|
112
|
+
registerDataType: (dataType: DataType) => void,
|
|
113
|
+
) => {
|
|
114
|
+
registerDataType({
|
|
115
|
+
id: "two_line",
|
|
116
|
+
formatter: { default: renderTwoLine, empty: renderTwoLine },
|
|
117
|
+
});
|
|
118
|
+
};
|
package/frontend-vue/layers/dms-ui/app/build/composables/data-types/useColumnValueRenderer.ts
CHANGED
|
@@ -45,8 +45,10 @@ export function useColumnValueRenderer() {
|
|
|
45
45
|
const { locale } = useI18n();
|
|
46
46
|
|
|
47
47
|
// A display also reads the column's own type options (`typeOptions`): a
|
|
48
|
-
// select's items name the values it draws
|
|
49
|
-
//
|
|
48
|
+
// select's items name the values it draws; and the column's own type id
|
|
49
|
+
// (`columnType`), so it can write the value as the column would (an amount
|
|
50
|
+
// under `two_line`). A display declared without options reads the type
|
|
51
|
+
// options at the top level, as it always did.
|
|
50
52
|
const resolveRenderer = (column: RenderableColumn) => {
|
|
51
53
|
const typeOptions = column.type?.inputComponent?.options as
|
|
52
54
|
| Record<string, unknown>
|
|
@@ -56,7 +58,7 @@ export function useColumnValueRenderer() {
|
|
|
56
58
|
const displayOptions = column.display.options ?? typeOptions;
|
|
57
59
|
return {
|
|
58
60
|
typeId: column.display.type,
|
|
59
|
-
options: { ...displayOptions, typeOptions },
|
|
61
|
+
options: { ...displayOptions, typeOptions, columnType: column.type?.id },
|
|
60
62
|
};
|
|
61
63
|
};
|
|
62
64
|
|
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LocationQueryValue } from "#dms/frontend-module";
|
|
2
|
-
import { replaceUrlVariables } from "
|
|
2
|
+
import { replaceUrlVariables } from "../../utils/urlVariables";
|
|
3
3
|
|
|
4
4
|
type RouteQuery = Record<string, LocationQueryValue | LocationQueryValue[]>;
|
|
5
5
|
|
|
@@ -1,29 +1,6 @@
|
|
|
1
|
+
import { formatRelativeDistance } from "#dms-core/app/utils/formatter";
|
|
1
2
|
import { regionalDateTimeFormat } from "#dms-core/app/utils/regional";
|
|
2
3
|
|
|
3
|
-
const SECOND_MS = 1000;
|
|
4
|
-
const MINUTE_MS = 60 * SECOND_MS;
|
|
5
|
-
const HOUR_MS = 60 * MINUTE_MS;
|
|
6
|
-
const DAY_MS = 24 * HOUR_MS;
|
|
7
|
-
const WEEK_MS = 7 * DAY_MS;
|
|
8
|
-
const MONTH_MS = 30 * DAY_MS;
|
|
9
|
-
const YEAR_MS = 365 * DAY_MS;
|
|
10
|
-
|
|
11
|
-
interface RelativeUnit {
|
|
12
|
-
unit: Intl.RelativeTimeFormatUnit;
|
|
13
|
-
ms: number;
|
|
14
|
-
/** The unit applies below this elapsed time. */
|
|
15
|
-
below: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const RELATIVE_UNITS: RelativeUnit[] = [
|
|
19
|
-
{ unit: "minute", ms: MINUTE_MS, below: HOUR_MS },
|
|
20
|
-
{ unit: "hour", ms: HOUR_MS, below: DAY_MS },
|
|
21
|
-
{ unit: "day", ms: DAY_MS, below: WEEK_MS },
|
|
22
|
-
{ unit: "week", ms: WEEK_MS, below: MONTH_MS },
|
|
23
|
-
{ unit: "month", ms: MONTH_MS, below: YEAR_MS },
|
|
24
|
-
{ unit: "year", ms: YEAR_MS, below: Number.POSITIVE_INFINITY },
|
|
25
|
-
];
|
|
26
|
-
|
|
27
4
|
function capitalize(text: string): string {
|
|
28
5
|
return text ? `${text[0]!.toLocaleUpperCase()}${text.slice(1)}` : text;
|
|
29
6
|
}
|
|
@@ -37,17 +14,9 @@ export function formatRelativeDate(
|
|
|
37
14
|
locale: string,
|
|
38
15
|
now: number = Date.now(),
|
|
39
16
|
): string {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
RELATIVE_UNITS.find((candidate) => distance < candidate.below) ??
|
|
44
|
-
RELATIVE_UNITS[RELATIVE_UNITS.length - 1]!;
|
|
45
|
-
const value = Math.round(deltaMs / unit.ms);
|
|
46
|
-
const format = new Intl.RelativeTimeFormat(locale, {
|
|
47
|
-
numeric: "auto",
|
|
48
|
-
style: "short",
|
|
49
|
-
});
|
|
50
|
-
return capitalize(format.format(value, unit.unit));
|
|
17
|
+
return capitalize(
|
|
18
|
+
formatRelativeDistance(date, locale, { style: "short", now }),
|
|
19
|
+
);
|
|
51
20
|
}
|
|
52
21
|
|
|
53
22
|
/** "Sep 27": day and short month. */
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// `{{params.id}}` takes the bare name, which on a route repeating a placeholder
|
|
2
|
+
// is its last occurrence — the row id of a form page. The `:<n>` suffix reaches
|
|
3
|
+
// a specific occurrence, `{{params.id:1}}` being the id of the page carrying the
|
|
4
|
+
// table view (see extractRouteParams).
|
|
5
|
+
const PARAM_TOKEN = /\{\{params\.(\w+(?::\d+)?)\}\}/g;
|
|
6
|
+
const QUERY_TOKEN = /\{\{query\.(\w+)\}\}/g;
|
|
7
|
+
const RESPONSE_TOKEN = /\{\{response\.(\w+)\}\}/g;
|
|
8
|
+
const TOKEN_OPENING = "{{";
|
|
9
|
+
|
|
10
|
+
export interface ReplaceUrlVariablesContext {
|
|
11
|
+
routeParams?: Record<string, string>;
|
|
12
|
+
routeQuery: Record<string, unknown>;
|
|
13
|
+
response?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 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.
|
|
19
|
+
*/
|
|
20
|
+
export function replaceUrlVariables(
|
|
21
|
+
url: string,
|
|
22
|
+
context: ReplaceUrlVariablesContext,
|
|
23
|
+
): string {
|
|
24
|
+
let processedUrl = url.replace(
|
|
25
|
+
PARAM_TOKEN,
|
|
26
|
+
(match, key) => context.routeParams?.[key] || match,
|
|
27
|
+
);
|
|
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
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Whether a URL still names a token: written so, or left unresolved. */
|
|
47
|
+
export function hasUrlVariables(url: string): boolean {
|
|
48
|
+
return url.includes(TOKEN_OPENING);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The URL with its tokens filled, or `undefined` while one is left: a URL
|
|
53
|
+
* naming a value the page does not carry is not a route, and must not be
|
|
54
|
+
* requested as one.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveUrlVariables(
|
|
57
|
+
url: string | undefined,
|
|
58
|
+
context: ReplaceUrlVariablesContext,
|
|
59
|
+
): string | undefined {
|
|
60
|
+
if (!url) return undefined;
|
|
61
|
+
const resolved = replaceUrlVariables(url, context);
|
|
62
|
+
return hasUrlVariables(resolved) ? undefined : resolved;
|
|
63
|
+
}
|
|
@@ -5,7 +5,9 @@ import DmsBlockActions, {
|
|
|
5
5
|
type BlockAction,
|
|
6
6
|
} from "../../build/components/blocks/BlockActions.vue";
|
|
7
7
|
import { useWatch } from "../../../../dms-core/app/composables/watch/useWatch";
|
|
8
|
+
import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
|
|
8
9
|
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
10
|
+
import type { BlockText } from "../../../../dms-core/app/types/composed-text";
|
|
9
11
|
|
|
10
12
|
type BannerTone = "info" | "success" | "warning" | "error" | "primary";
|
|
11
13
|
|
|
@@ -14,8 +16,9 @@ type BannerTone = "info" | "success" | "warning" | "error" | "primary";
|
|
|
14
16
|
// Dismissals live in a cookie (like the dashboard banners) so the server
|
|
15
17
|
// render already leaves a dismissed banner out instead of flashing it.
|
|
16
18
|
interface BannerBlockProps extends Partial<DefaultComponentProps> {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
/** A string (`$` for an i18n key) or a composed text. */
|
|
20
|
+
title?: BlockText;
|
|
21
|
+
description?: BlockText;
|
|
19
22
|
tone?: BannerTone;
|
|
20
23
|
/** Overrides the tone's icon. */
|
|
21
24
|
icon?: string;
|
|
@@ -56,7 +59,7 @@ const TONE_ICONS: Record<BannerTone, string> = {
|
|
|
56
59
|
|
|
57
60
|
useWatch(props.watchActions || [], props.componentId);
|
|
58
61
|
|
|
59
|
-
const {
|
|
62
|
+
const { processText } = useComposedText();
|
|
60
63
|
const dismissed = useDmsCookie<string[]>(DISMISSED_COOKIE, {
|
|
61
64
|
default: () => [],
|
|
62
65
|
maxAge: DISMISSED_MAX_AGE_S,
|
|
@@ -91,9 +94,9 @@ function dismiss(): void {
|
|
|
91
94
|
<template>
|
|
92
95
|
<DmsBanner
|
|
93
96
|
v-if="isVisible"
|
|
94
|
-
:title="props.title ?
|
|
97
|
+
:title="props.title ? processText(props.title) : undefined"
|
|
95
98
|
:description="
|
|
96
|
-
props.description ?
|
|
99
|
+
props.description ? processText(props.description) : undefined
|
|
97
100
|
"
|
|
98
101
|
:icon="props.icon ?? TONE_ICONS[props.tone]"
|
|
99
102
|
:tone="props.tone"
|
|
@@ -8,13 +8,24 @@ import DmsBlockStatus, {
|
|
|
8
8
|
type BlockEmptyText,
|
|
9
9
|
} from "../../build/components/blocks/BlockStatus.vue";
|
|
10
10
|
import { useBlockItems } from "../../build/composables/blocks/useBlockItems";
|
|
11
|
+
import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
|
|
11
12
|
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
13
|
+
import type { BlockText } from "../../../../dms-core/app/types/composed-text";
|
|
14
|
+
import { isComposedText } from "../../../../dms-core/app/utils/composedText";
|
|
12
15
|
|
|
13
16
|
// `KeyValueList` block (interface-dms `base/key-value-list`): label / value
|
|
14
17
|
// rows (v2 .c-dl), static or from `fetchUrl` (`{ items }`), in a card by
|
|
15
|
-
// default. Labels, details and text values follow the `$` i18n convention
|
|
18
|
+
// default. Labels, details and text values follow the `$` i18n convention or
|
|
19
|
+
// are composed texts.
|
|
20
|
+
interface KeyValueListBlockItem
|
|
21
|
+
extends Omit<KeyValueItem, "label" | "value" | "detail"> {
|
|
22
|
+
label: BlockText;
|
|
23
|
+
value?: BlockText | number | null;
|
|
24
|
+
detail?: BlockText;
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
interface KeyValueListBlockProps extends DefaultComponentProps {
|
|
17
|
-
items?:
|
|
28
|
+
items?: KeyValueListBlockItem[];
|
|
18
29
|
dense?: boolean;
|
|
19
30
|
columns?: number;
|
|
20
31
|
currency?: string;
|
|
@@ -50,29 +61,39 @@ const props = withDefaults(defineProps<KeyValueListBlockProps>(), {
|
|
|
50
61
|
|
|
51
62
|
const TRANSLATED_TYPES = new Set([undefined, "text", "status", "link"]);
|
|
52
63
|
|
|
53
|
-
const {
|
|
64
|
+
const { processText } = useComposedText();
|
|
54
65
|
|
|
55
|
-
const { items, isPending, hasError, refresh } =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
const { items, isPending, hasError, refresh } =
|
|
67
|
+
useBlockItems<KeyValueListBlockItem>({
|
|
68
|
+
items: () => props.items,
|
|
69
|
+
fetchUrl: props.fetchUrl,
|
|
70
|
+
fetchUrlMethod: props.fetchUrlMethod,
|
|
71
|
+
routeParams: () => props.routeParams,
|
|
72
|
+
watchActions: props.watchActions,
|
|
73
|
+
componentId: props.componentId,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// A composed value already says how each of its parameters is written, so it
|
|
77
|
+
// is composed whatever the row's type; a plain one is translated only where
|
|
78
|
+
// the type draws text.
|
|
79
|
+
function resolveValue(item: KeyValueListBlockItem): KeyValueItem["value"] {
|
|
80
|
+
if (isComposedText(item.value)) return processText(item.value);
|
|
81
|
+
if (typeof item.value === "string" && TRANSLATED_TYPES.has(item.type)) {
|
|
82
|
+
return processText(item.value);
|
|
83
|
+
}
|
|
84
|
+
return item.value as KeyValueItem["value"];
|
|
85
|
+
}
|
|
62
86
|
|
|
63
87
|
const resolvedItems = computed<KeyValueItem[]>(() =>
|
|
64
88
|
items.value.map((item) => ({
|
|
65
89
|
...item,
|
|
66
|
-
label:
|
|
67
|
-
detail: item.detail ?
|
|
68
|
-
value:
|
|
69
|
-
typeof item.value === "string" && TRANSLATED_TYPES.has(item.type)
|
|
70
|
-
? processI18n(item.value)
|
|
71
|
-
: item.value,
|
|
90
|
+
label: processText(item.label),
|
|
91
|
+
detail: item.detail ? processText(item.detail) : undefined,
|
|
92
|
+
value: resolveValue(item),
|
|
72
93
|
})),
|
|
73
94
|
);
|
|
74
95
|
const title = computed(() =>
|
|
75
|
-
props.title ?
|
|
96
|
+
props.title ? processText(props.title) : undefined,
|
|
76
97
|
);
|
|
77
98
|
const isEmpty = computed(
|
|
78
99
|
() => !isPending.value && !hasError.value && resolvedItems.value.length === 0,
|
|
@@ -8,13 +8,23 @@ import DmsBlockStatus, {
|
|
|
8
8
|
type BlockEmptyText,
|
|
9
9
|
} from "../../build/components/blocks/BlockStatus.vue";
|
|
10
10
|
import { useBlockItems } from "../../build/composables/blocks/useBlockItems";
|
|
11
|
+
import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
|
|
11
12
|
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
13
|
+
import type { BlockText } from "../../../../dms-core/app/types/composed-text";
|
|
12
14
|
|
|
13
15
|
// `StatGroup` block (interface-dms `base/stat-group`): the generic StatGroup
|
|
14
16
|
// fed from the block options or from `fetchUrl` (`{ items }`). Texts follow
|
|
15
|
-
// the `$` i18n-key convention; numeric values are
|
|
17
|
+
// the `$` i18n-key convention or are composed texts; numeric values are
|
|
18
|
+
// formatted for the locale.
|
|
19
|
+
interface StatGroupBlockItem
|
|
20
|
+
extends Omit<StatGroupItem, "eyebrow" | "value" | "detail"> {
|
|
21
|
+
eyebrow: BlockText;
|
|
22
|
+
value: BlockText | number;
|
|
23
|
+
detail?: BlockText;
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
interface StatGroupBlockProps extends DefaultComponentProps {
|
|
17
|
-
items?:
|
|
27
|
+
items?: StatGroupBlockItem[];
|
|
18
28
|
layout?: StatGroupLayout;
|
|
19
29
|
columns?: number;
|
|
20
30
|
/** Accessible name of the group. */
|
|
@@ -43,30 +53,24 @@ const props = withDefaults(defineProps<StatGroupBlockProps>(), {
|
|
|
43
53
|
|
|
44
54
|
const DEFAULT_SKELETON_COUNT = 4;
|
|
45
55
|
|
|
46
|
-
const {
|
|
47
|
-
const { processI18n } = useTranslation();
|
|
56
|
+
const { processText } = useComposedText();
|
|
48
57
|
|
|
49
|
-
const { items, isPending, hasError, refresh } =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (typeof value === "number") {
|
|
59
|
-
return new Intl.NumberFormat(locale.value).format(value);
|
|
60
|
-
}
|
|
61
|
-
return processI18n(String(value ?? ""));
|
|
62
|
-
}
|
|
58
|
+
const { items, isPending, hasError, refresh } =
|
|
59
|
+
useBlockItems<StatGroupBlockItem>({
|
|
60
|
+
items: () => props.items,
|
|
61
|
+
fetchUrl: props.fetchUrl,
|
|
62
|
+
fetchUrlMethod: props.fetchUrlMethod,
|
|
63
|
+
routeParams: () => props.routeParams,
|
|
64
|
+
watchActions: props.watchActions,
|
|
65
|
+
componentId: props.componentId,
|
|
66
|
+
});
|
|
63
67
|
|
|
64
68
|
const resolvedItems = computed<StatGroupItem[]>(() =>
|
|
65
69
|
items.value.map((item) => ({
|
|
66
70
|
...item,
|
|
67
|
-
eyebrow:
|
|
68
|
-
value:
|
|
69
|
-
detail: item.detail ?
|
|
71
|
+
eyebrow: processText(item.eyebrow),
|
|
72
|
+
value: processText(item.value),
|
|
73
|
+
detail: item.detail ? processText(item.detail) : undefined,
|
|
70
74
|
})),
|
|
71
75
|
);
|
|
72
76
|
</script>
|
|
@@ -87,6 +91,6 @@ const resolvedItems = computed<StatGroupItem[]>(() =>
|
|
|
87
91
|
:skeleton-count="
|
|
88
92
|
props.skeletonCount ?? props.columns ?? DEFAULT_SKELETON_COUNT
|
|
89
93
|
"
|
|
90
|
-
:label="props.label ?
|
|
94
|
+
:label="props.label ? processText(props.label) : undefined"
|
|
91
95
|
/>
|
|
92
96
|
</template>
|
|
@@ -132,6 +132,7 @@ const isNested = computed(() => !!nestedContext);
|
|
|
132
132
|
const { data, isLoading } = useChartFetch<ChartResponse>({
|
|
133
133
|
fetchUrl: isNested.value ? undefined : props.fetchUrl,
|
|
134
134
|
fetchUrlMethod: props.fetchUrlMethod,
|
|
135
|
+
routeParams: () => props.routeParams,
|
|
135
136
|
periodScope: props.periodScope,
|
|
136
137
|
realtimeTopic: props.realtimeTopic,
|
|
137
138
|
staticData: null,
|
|
@@ -69,6 +69,7 @@ const watchKey = computed(() => JSON.stringify(watchState.value));
|
|
|
69
69
|
const { data, isLoading } = useChartFetch<ChartCardResponse>({
|
|
70
70
|
fetchUrl: props.fetchUrl,
|
|
71
71
|
fetchUrlMethod: props.fetchUrlMethod,
|
|
72
|
+
routeParams: () => props.routeParams,
|
|
72
73
|
periodScope: props.periodScope,
|
|
73
74
|
watchSource: () => watchKey.value,
|
|
74
75
|
});
|
|
@@ -79,6 +79,7 @@ const staticData = computed<KpiCardResponse | null>(() => {
|
|
|
79
79
|
const { data, isLoading, error, refresh } = useChartFetch<KpiCardResponse>({
|
|
80
80
|
fetchUrl: props.fetchUrl,
|
|
81
81
|
fetchUrlMethod: props.fetchUrlMethod,
|
|
82
|
+
routeParams: () => props.routeParams,
|
|
82
83
|
periodScope: props.periodScope,
|
|
83
84
|
staticData: () => staticData.value,
|
|
84
85
|
watchSource: () => watchKey.value,
|
|
@@ -76,6 +76,7 @@ const { state: watchState } = useWatch(
|
|
|
76
76
|
const { data, isLoading, error, refresh } = useChartFetch<MeterResponse>({
|
|
77
77
|
fetchUrl: props.fetchUrl,
|
|
78
78
|
fetchUrlMethod: props.fetchUrlMethod,
|
|
79
|
+
routeParams: () => props.routeParams,
|
|
79
80
|
watchSource: () => JSON.stringify(watchState.value),
|
|
80
81
|
});
|
|
81
82
|
|
|
@@ -30,6 +30,7 @@ const { state: watchState } = useWatch(
|
|
|
30
30
|
);
|
|
31
31
|
const { data: fetchedBadges } = useChartFetch<TabBadges>({
|
|
32
32
|
fetchUrl: props.badgesUrl,
|
|
33
|
+
routeParams: () => props.routeParams,
|
|
33
34
|
watchSource: () => JSON.stringify(watchState.value),
|
|
34
35
|
});
|
|
35
36
|
const { locale } = useI18n();
|
|
@@ -108,6 +108,7 @@ const staticData = computed<TopListCardResponse | null>(() =>
|
|
|
108
108
|
const { data, isLoading } = useChartFetch<TopListCardResponse>({
|
|
109
109
|
fetchUrl: props.fetchUrl,
|
|
110
110
|
fetchUrlMethod: props.fetchUrlMethod,
|
|
111
|
+
routeParams: () => props.routeParams,
|
|
111
112
|
periodScope: props.periodScope,
|
|
112
113
|
staticData: () => staticData.value,
|
|
113
114
|
watchSource: () => watchKey.value,
|