@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.
- package/CHANGELOG.md +47 -0
- package/dist/hooks/tenant-instances.d.ts +1 -0
- package/dist/hooks/tenant-instances.js +13 -0
- package/dist/hooks/tenant-instances.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/test/unit/interfaces/dms/page-topics.test.js +67 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js +1 -0
- package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js +46 -0
- package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/column-display.test.js +11 -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 +31 -0
- package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js.map +1 -1
- package/dist/test/unit/utils/tenant-instances.test.d.ts +1 -0
- package/dist/test/unit/utils/tenant-instances.test.js +53 -0
- package/dist/test/unit/utils/tenant-instances.test.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/tenant-instances.d.ts +2 -0
- package/dist/utils/tenant-instances.js +38 -0
- package/dist/utils/tenant-instances.js.map +1 -0
- package/dist/validation/auth.schema.d.ts +2 -2
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +4 -0
- package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +35 -23
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockActions.vue +34 -6
- package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +26 -8
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +3 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/MonoCell.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +24 -13
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +12 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useRefreshTriggers.ts +40 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +15 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +85 -22
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +10 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +94 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +41 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +3 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codeEditor.ts +2 -13
- package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +59 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codeLanguages.ts +36 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codePalette.ts +32 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +77 -20
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +15 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +111 -25
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +95 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/EmptyStateBlock.vue +18 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +8 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +21 -9
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +8 -0
- package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +97 -0
- package/frontend-vue/layers/dms-ui/app/components/empty-state/EmptyState.vue +13 -4
- package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +14 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +2 -8
- package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +19 -0
- package/frontend-vue/layers/dms-ui/app/components/master-detail/MasterDetail.vue +54 -48
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +24 -9
- package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +66 -74
- package/frontend-vue/layers/dms-ui/app/components/top-list/internal/TopListRow.vue +4 -3
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +5 -2
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +2 -29
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +2 -1
- package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +4 -0
- package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +2 -2
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +1 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +1 -0
- package/frontend-vue/package.json +4 -3
- package/frontend-vue/pnpm-lock.yaml +8 -5
- package/frontend-vue/tests/activity-feed-days.test.ts +26 -12
- package/frontend-vue/tests/banner-block-fetch.test.ts +162 -0
- package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +100 -0
- package/frontend-vue/tests/block-items-period-realtime.test.ts +147 -0
- package/frontend-vue/tests/block-route-params.test.ts +48 -0
- package/frontend-vue/tests/code-block.test.ts +155 -0
- package/frontend-vue/tests/code-highlight.test.ts +72 -0
- package/frontend-vue/tests/composed-text-blocks.test.ts +5 -0
- package/frontend-vue/tests/composed-text-more-blocks.test.ts +391 -0
- package/frontend-vue/tests/drawer-target-direction.test.ts +205 -0
- package/frontend-vue/tests/empty-state-late-slots.test.ts +53 -0
- package/frontend-vue/tests/flow-canvas-delete-badge.test.ts +88 -0
- package/frontend-vue/tests/form-redirect-placeholders.test.ts +11 -0
- package/frontend-vue/tests/key-value-list-copy.test.ts +114 -0
- package/frontend-vue/tests/master-detail-layout.test.ts +53 -0
- package/frontend-vue/tests/mono-cell-truncate.test.ts +50 -0
- package/frontend-vue/tests/nav-card-grid-empty-category.test.ts +1 -0
- package/frontend-vue/tests/page-fill-height.test.ts +20 -3
- package/frontend-vue/tests/route-param-occurrences.test.ts +9 -0
- package/frontend-vue/tests/table-actions-search-label.test.ts +147 -0
- package/frontend-vue/tests/table-clear-filters.test.ts +22 -0
- package/frontend-vue/tests/table-empty-body-viewport.test.ts +25 -0
- package/frontend-vue/tests/table-metric-cells.test.ts +22 -0
- package/frontend-vue/tests/table-pills-cell.test.ts +126 -0
- package/frontend-vue/tests/table-source-navigation.test.ts +123 -0
- package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +137 -0
- package/frontend-vue/tests/table-view-refresh.test.ts +84 -0
- package/frontend-vue/tests/table-view-sources.test.ts +67 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/SKILL.md +14 -3
- 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.
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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:
|
|
119
|
-
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
+
banner.value.dismissKey ||
|
|
73
151
|
[props.pageId, props.componentId].filter(Boolean).join(":"),
|
|
74
152
|
);
|
|
75
|
-
const
|
|
153
|
+
const isDismissed = computed(
|
|
76
154
|
() =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
155
|
+
isClosed.value ||
|
|
156
|
+
Boolean(
|
|
157
|
+
banner.value.dismissible &&
|
|
158
|
+
storageKey.value &&
|
|
159
|
+
dismissed.value.includes(storageKey.value),
|
|
82
160
|
),
|
|
83
161
|
);
|
|
84
|
-
const
|
|
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="
|
|
182
|
+
:title="banner.title ? processText(banner.title) : undefined"
|
|
98
183
|
:description="
|
|
99
|
-
|
|
184
|
+
banner.description ? processText(banner.description) : undefined
|
|
100
185
|
"
|
|
101
|
-
:icon="
|
|
102
|
-
:tone="
|
|
103
|
-
:size="
|
|
104
|
-
: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="
|
|
192
|
+
<template v-if="banner.actions.length" #actions>
|
|
108
193
|
<DmsBlockActions
|
|
109
|
-
: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' :
|
|
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:
|
|
25
|
-
description?:
|
|
26
|
+
title: BlockText;
|
|
27
|
+
description?: BlockText;
|
|
26
28
|
icon: string;
|
|
27
29
|
iconTone?: IconWellTone;
|
|
28
30
|
to: string;
|
|
29
|
-
state?:
|
|
31
|
+
state?: BlockText;
|
|
30
32
|
stateTone?: Tone;
|
|
31
|
-
tag?:
|
|
32
|
-
readout?:
|
|
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:
|
|
123
|
-
description: item.description ?
|
|
124
|
-
state: item.state ?
|
|
125
|
-
|
|
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
|