@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
|
@@ -42,14 +42,14 @@ export declare const authSchema: {
|
|
|
42
42
|
code: string;
|
|
43
43
|
state: string;
|
|
44
44
|
state_cookie: string;
|
|
45
|
-
invite?: string | undefined;
|
|
46
45
|
language?: string | undefined;
|
|
46
|
+
invite?: string | undefined;
|
|
47
47
|
}, {
|
|
48
48
|
code: string;
|
|
49
49
|
state: string;
|
|
50
50
|
state_cookie: string;
|
|
51
|
-
invite?: string | undefined;
|
|
52
51
|
language?: string | undefined;
|
|
52
|
+
invite?: string | undefined;
|
|
53
53
|
}>;
|
|
54
54
|
refresh: z.ZodObject<{
|
|
55
55
|
token: z.ZodString;
|
|
@@ -494,10 +494,14 @@ if (import.meta.env.DEV) {
|
|
|
494
494
|
:class="stackClass"
|
|
495
495
|
:css="animateLayout"
|
|
496
496
|
>
|
|
497
|
+
<!-- `empty:hidden`: a block with nothing to show (a fetched Banner the
|
|
498
|
+
route answered `null`) renders a bare comment; its wrapper must not
|
|
499
|
+
take a gap of the stack. -->
|
|
497
500
|
<div
|
|
498
501
|
v-for="component in components"
|
|
499
502
|
:key="component.id"
|
|
500
503
|
:class="[
|
|
504
|
+
'empty:hidden',
|
|
501
505
|
{ 'dms-fresh-modified': freshComponentIds.has(component.id) },
|
|
502
506
|
component.id === fillingComponentId && fillColumnClass,
|
|
503
507
|
]"
|
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
regionalDayKey,
|
|
4
4
|
regionalDayNumber,
|
|
5
5
|
} from "#dms-core/app/utils/regional";
|
|
6
|
+
import type {
|
|
7
|
+
BlockText,
|
|
8
|
+
ComposedTextParam,
|
|
9
|
+
} from "#dms-core/app/types/composed-text";
|
|
6
10
|
import type { Tone } from "../../../types/tone";
|
|
7
11
|
|
|
8
12
|
/** One entry of an activity feed, as a page declares it or a source answers. */
|
|
@@ -10,19 +14,19 @@ export interface ActivityFeedItem {
|
|
|
10
14
|
id?: string;
|
|
11
15
|
icon?: string;
|
|
12
16
|
tone?: Tone;
|
|
13
|
-
/** Title (i18n key with
|
|
14
|
-
title:
|
|
17
|
+
/** Title (i18n key with `$`, literal, or composed text). */
|
|
18
|
+
title: BlockText;
|
|
15
19
|
/** Dimmed details, joined by "·". */
|
|
16
|
-
meta?:
|
|
20
|
+
meta?: BlockText[];
|
|
17
21
|
/**
|
|
18
|
-
* Values the title and
|
|
19
|
-
* translated first.
|
|
22
|
+
* Values the `$` title and details interpolate: typed values are formatted
|
|
23
|
+
* as in a composed text; a `$`-prefixed string is translated first.
|
|
20
24
|
*/
|
|
21
|
-
params?: Record<string,
|
|
25
|
+
params?: Record<string, ComposedTextParam>;
|
|
22
26
|
/** ISO date: files the entry under its day and gives its time. */
|
|
23
27
|
date?: string;
|
|
24
|
-
/**
|
|
25
|
-
time?:
|
|
28
|
+
/** Trailing text, in place of the formatted time. */
|
|
29
|
+
time?: BlockText;
|
|
26
30
|
unread?: boolean;
|
|
27
31
|
/** Makes the row a link. */
|
|
28
32
|
to?: string;
|
|
@@ -143,31 +147,39 @@ export function formatActivityTime(date: string, locale: string): string {
|
|
|
143
147
|
}).format(parsed);
|
|
144
148
|
}
|
|
145
149
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
params?: Record<string, string> | null,
|
|
149
|
-
) => string;
|
|
150
|
-
|
|
151
|
-
const isTextKey = (value: string): boolean => value.startsWith("$");
|
|
150
|
+
const isTextKey = (value: ComposedTextParam): value is string =>
|
|
151
|
+
typeof value === "string" && value.startsWith("$");
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
|
-
* The values an entry's title and details interpolate
|
|
155
|
-
* of its own,
|
|
156
|
-
* {device}" with `device` = "{browser} on {os}".
|
|
154
|
+
* The values an entry's title and details interpolate, as composed-text
|
|
155
|
+
* parameters. A `$` string is a text of its own, written with the entry's
|
|
156
|
+
* other values: "Signed in on {device}" with `device` = "{browser} on {os}".
|
|
157
157
|
*/
|
|
158
158
|
export function resolveActivityParams(
|
|
159
|
-
params: Record<string,
|
|
160
|
-
|
|
161
|
-
)
|
|
162
|
-
if (!params) return null;
|
|
159
|
+
params: Record<string, ComposedTextParam> | undefined,
|
|
160
|
+
): Record<string, ComposedTextParam> | undefined {
|
|
161
|
+
if (!params) return undefined;
|
|
163
162
|
const entries = Object.entries(params);
|
|
164
|
-
const
|
|
163
|
+
const values = Object.fromEntries(
|
|
165
164
|
entries.filter(([, value]) => !isTextKey(value)),
|
|
166
165
|
);
|
|
167
166
|
return Object.fromEntries(
|
|
168
167
|
entries.map(([name, value]) => [
|
|
169
168
|
name,
|
|
170
|
-
isTextKey(value) ?
|
|
169
|
+
isTextKey(value) ? { key: value, params: values } : value,
|
|
171
170
|
]),
|
|
172
171
|
);
|
|
173
172
|
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A title or a detail of an entry as the text to write: a `$` key takes the
|
|
176
|
+
* entry's values, so a `count` value picks its plural form; a composed text
|
|
177
|
+
* carries its own values, and a literal is written as is.
|
|
178
|
+
*/
|
|
179
|
+
export function activityText(
|
|
180
|
+
text: BlockText,
|
|
181
|
+
params: Record<string, ComposedTextParam> | undefined,
|
|
182
|
+
): BlockText {
|
|
183
|
+
if (!isTextKey(text)) return text;
|
|
184
|
+
return { key: text, params: resolveActivityParams(params) };
|
|
185
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { ButtonProps } from "@nuxt/ui";
|
|
3
3
|
import { buttonLinkProps } from "../../utils/link";
|
|
4
4
|
import type { Tone } from "../../../types/tone";
|
|
5
|
+
import type { CustomButton } from "../../../composables/table-view/types/custom-button";
|
|
5
6
|
|
|
6
7
|
/** A link button a block option declares (interface-dms `BlockLinkAction`). */
|
|
7
8
|
export interface BlockAction {
|
|
@@ -13,12 +14,26 @@ export interface BlockAction {
|
|
|
13
14
|
color?: Tone;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* A button running a target (interface-dms `BannerButtonAction`): the block
|
|
19
|
+
* that offers it runs it on `press`, with its confirmation.
|
|
20
|
+
*/
|
|
21
|
+
export interface BlockButtonAction
|
|
22
|
+
extends Pick<CustomButton, "label" | "icon" | "target" | "confirm"> {
|
|
23
|
+
variant?: ButtonProps["variant"];
|
|
24
|
+
color?: Tone;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A link or a button a block offers. */
|
|
28
|
+
export type BlockActionItem = BlockAction | BlockButtonAction;
|
|
29
|
+
|
|
30
|
+
// The buttons of a display block (empty state, banner, card head): links, and
|
|
31
|
+
// in a banner, buttons running a target, which the block runs on `press`. One
|
|
17
32
|
// action leads (`leadVariant` / `leadColor`): the first, or the last in a
|
|
18
33
|
// banner where the main action sits right-most. The others are quiet buttons,
|
|
19
34
|
// unless an action sets its own look. Labels follow the `$` i18n convention.
|
|
20
35
|
interface BlockActionsProps {
|
|
21
|
-
actions:
|
|
36
|
+
actions: BlockActionItem[];
|
|
22
37
|
size?: ButtonProps["size"];
|
|
23
38
|
leadVariant?: ButtonProps["variant"];
|
|
24
39
|
leadColor?: Tone;
|
|
@@ -36,8 +51,18 @@ const props = withDefaults(defineProps<BlockActionsProps>(), {
|
|
|
36
51
|
leadPosition: "first",
|
|
37
52
|
});
|
|
38
53
|
|
|
54
|
+
const emit = defineEmits<{ press: [action: BlockButtonAction] }>();
|
|
55
|
+
|
|
39
56
|
const { processI18n } = useTranslation();
|
|
40
57
|
|
|
58
|
+
const isButtonAction = (action: BlockActionItem): action is BlockButtonAction =>
|
|
59
|
+
"target" in action && Boolean(action.target);
|
|
60
|
+
|
|
61
|
+
function actionProps(action: BlockActionItem): Record<string, unknown> {
|
|
62
|
+
if (!isButtonAction(action)) return buttonLinkProps(action.to);
|
|
63
|
+
return { onClick: () => emit("press", action) };
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
function isLead(index: number): boolean {
|
|
42
67
|
return props.leadPosition === "last"
|
|
43
68
|
? index === props.actions.length - 1
|
|
@@ -45,7 +70,7 @@ function isLead(index: number): boolean {
|
|
|
45
70
|
}
|
|
46
71
|
|
|
47
72
|
function buttonVariant(
|
|
48
|
-
action:
|
|
73
|
+
action: BlockActionItem,
|
|
49
74
|
index: number,
|
|
50
75
|
): ButtonProps["variant"] {
|
|
51
76
|
return (
|
|
@@ -53,7 +78,10 @@ function buttonVariant(
|
|
|
53
78
|
);
|
|
54
79
|
}
|
|
55
80
|
|
|
56
|
-
function buttonColor(
|
|
81
|
+
function buttonColor(
|
|
82
|
+
action: BlockActionItem,
|
|
83
|
+
index: number,
|
|
84
|
+
): ButtonProps["color"] {
|
|
57
85
|
return action.color ?? (isLead(index) ? props.leadColor : "neutral");
|
|
58
86
|
}
|
|
59
87
|
</script>
|
|
@@ -61,12 +89,12 @@ function buttonColor(action: BlockAction, index: number): ButtonProps["color"] {
|
|
|
61
89
|
<template>
|
|
62
90
|
<UButton
|
|
63
91
|
v-for="(action, index) in props.actions"
|
|
64
|
-
:key="`${index}-${action.
|
|
92
|
+
:key="`${index}-${action.label}`"
|
|
65
93
|
:label="processI18n(action.label ?? '')"
|
|
66
94
|
:icon="action.icon"
|
|
67
95
|
:size="props.size"
|
|
68
96
|
:variant="buttonVariant(action, index)"
|
|
69
97
|
:color="buttonColor(action, index)"
|
|
70
|
-
v-bind="
|
|
98
|
+
v-bind="actionProps(action)"
|
|
71
99
|
/>
|
|
72
100
|
</template>
|
|
@@ -39,6 +39,26 @@ const props = withDefaults(defineProps<DynamicDrawerProps>(), {
|
|
|
39
39
|
});
|
|
40
40
|
const emit = defineEmits<DynamicDrawerEmits>();
|
|
41
41
|
|
|
42
|
+
// A side drawer is a sheet as wide as a side panel by default (see
|
|
43
|
+
// `useAppSidePanels`), never wider than the window less the drawer's gutter.
|
|
44
|
+
// Its own padding is the gutter: the containers drop theirs.
|
|
45
|
+
const SIDE_DIRECTIONS: readonly DrawerDirection[] = ["left", "right"];
|
|
46
|
+
const SIDE_DRAWER_CONTENT = "w-[440px]";
|
|
47
|
+
const SIDE_CONTAINER_CLASS = "px-0 sm:px-0 lg:px-0";
|
|
48
|
+
const EDGE_CONTAINER_CLASS = "max-sm:px-0";
|
|
49
|
+
|
|
50
|
+
const isSide = computed(() => SIDE_DIRECTIONS.includes(props.direction));
|
|
51
|
+
const containerClass = computed(() =>
|
|
52
|
+
isSide.value ? SIDE_CONTAINER_CLASS : EDGE_CONTAINER_CLASS,
|
|
53
|
+
);
|
|
54
|
+
const drawerUi = computed(() => ({
|
|
55
|
+
...(isSide.value && { content: SIDE_DRAWER_CONTENT }),
|
|
56
|
+
header: "border-b border-default flex shrink-0 items-center gap-4 pb-4",
|
|
57
|
+
// The scroll area's bottom padding: a form's sticky footer sits flush
|
|
58
|
+
// with the drawer's edge (see Form.vue).
|
|
59
|
+
container: "[--dms-form-foot-pb:1rem]",
|
|
60
|
+
}));
|
|
61
|
+
|
|
42
62
|
// Let a DmsForm rendered in the body drop its DmsCard (the drawer is the surface).
|
|
43
63
|
provide("dmsFormContainer", true);
|
|
44
64
|
|
|
@@ -104,19 +124,17 @@ function onContentResolve() {
|
|
|
104
124
|
:description="description"
|
|
105
125
|
:title="title"
|
|
106
126
|
:dismissible="false"
|
|
107
|
-
:ui="
|
|
108
|
-
header: 'border-b border-default flex shrink-0 items-center gap-4 pb-4',
|
|
109
|
-
// The scroll area's bottom padding: a form's sticky footer sits flush
|
|
110
|
-
// with the drawer's edge (see Form.vue).
|
|
111
|
-
container: '[--dms-form-foot-pb:1rem]',
|
|
112
|
-
}"
|
|
127
|
+
:ui="drawerUi"
|
|
113
128
|
@close:prevent="tryClose"
|
|
114
129
|
>
|
|
115
130
|
<template #header>
|
|
116
131
|
<!-- Same container as the body so the title aligns with the form. On
|
|
117
132
|
phones the drawer's own padding is the 16px gutter: the container
|
|
118
133
|
drops its own. -->
|
|
119
|
-
<UContainer
|
|
134
|
+
<UContainer
|
|
135
|
+
class="flex w-full flex-1 items-start gap-3"
|
|
136
|
+
:class="containerClass"
|
|
137
|
+
>
|
|
120
138
|
<DmsIconWell v-if="icon" :icon="icon" :tone="color" />
|
|
121
139
|
<div v-if="headerComponent" class="min-w-0 flex-1">
|
|
122
140
|
<VisuallyHidden>
|
|
@@ -157,7 +175,7 @@ function onContentResolve() {
|
|
|
157
175
|
</UContainer>
|
|
158
176
|
</template>
|
|
159
177
|
<template #body>
|
|
160
|
-
<UContainer class="
|
|
178
|
+
<UContainer :class="containerClass">
|
|
161
179
|
<Suspense @pending="onContentPending" @resolve="onContentResolve">
|
|
162
180
|
<component
|
|
163
181
|
:is="component"
|
|
@@ -424,6 +424,7 @@ const uiTableActions = computed(() => uiTableActionsVariant());
|
|
|
424
424
|
variant="ghost"
|
|
425
425
|
size="sm"
|
|
426
426
|
square
|
|
427
|
+
:aria-label="t('dms.table.search_open')"
|
|
427
428
|
:class="uiTableActions.trigger()"
|
|
428
429
|
@click="toggleSearch"
|
|
429
430
|
/>
|
|
@@ -433,7 +434,8 @@ const uiTableActions = computed(() => uiTableActionsVariant());
|
|
|
433
434
|
v-if="searchActive"
|
|
434
435
|
ref="searchInputRef"
|
|
435
436
|
v-model="tableSharedData!.globalFilterState.value"
|
|
436
|
-
:placeholder="
|
|
437
|
+
:placeholder="searchPlaceholderText"
|
|
438
|
+
:aria-label="searchPlaceholderText"
|
|
437
439
|
icon="i-ph-magnifying-glass"
|
|
438
440
|
size="sm"
|
|
439
441
|
class="w-full"
|
|
@@ -45,7 +45,7 @@ const tableSharedDataRef =
|
|
|
45
45
|
injectLocal<ShallowRef<TableSharedData<T>>>("tableSharedData");
|
|
46
46
|
const tableSharedData = computed(() => tableSharedDataRef?.value);
|
|
47
47
|
|
|
48
|
-
// A search, a filter chip or a
|
|
48
|
+
// A search, a filter chip, a quick filter or a hidden filter narrows the rows.
|
|
49
49
|
const isFiltered = computed(() => !!tableSharedData.value?.isFiltered?.value);
|
|
50
50
|
// What the clear action would clear; nothing (only default filters left at
|
|
51
51
|
// their value) offers no action.
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { useClipboard } from "@vueuse/core";
|
|
3
3
|
|
|
4
4
|
// A value in the mono font (an id, a key, a slug), with a button copying it
|
|
5
|
-
// that shows on hover.
|
|
5
|
+
// that shows on hover. The root is a block flex box held to its cell's
|
|
6
|
+
// width: an inline one grows with the value, which then never truncates.
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
value: string;
|
|
@@ -27,7 +28,7 @@ const copyValue = async () => {
|
|
|
27
28
|
</script>
|
|
28
29
|
|
|
29
30
|
<template>
|
|
30
|
-
<span class="group/mono
|
|
31
|
+
<span class="group/mono flex max-w-full min-w-0 items-center gap-1">
|
|
31
32
|
<span class="text-toned truncate font-mono" :title="props.value">
|
|
32
33
|
{{ props.value }}
|
|
33
34
|
</span>
|
|
@@ -227,6 +227,12 @@ export interface TableProps<T> {
|
|
|
227
227
|
bulkActions?: CustomRowAction[];
|
|
228
228
|
/** What the empty body says, per reason it is empty. */
|
|
229
229
|
emptyStates?: TableViewEmptyStatesConfig;
|
|
230
|
+
/**
|
|
231
|
+
* Filters the toolbar does not show narrow the rows (the page URL's
|
|
232
|
+
* `queryParamFilters`): the table reads as filtered, and its empty body
|
|
233
|
+
* says no row matches rather than that there are none yet.
|
|
234
|
+
*/
|
|
235
|
+
hiddenFiltersApply?: boolean;
|
|
230
236
|
|
|
231
237
|
data?: T[] | null;
|
|
232
238
|
columns?: TableColumn<T>[];
|
|
@@ -330,7 +336,7 @@ export interface TableSharedData<T> {
|
|
|
330
336
|
resetFilters: () => void;
|
|
331
337
|
/** What `resetFilters` would clear (nothing: no clear action to offer). */
|
|
332
338
|
clearableFilters: ComputedRef<ClearableTableFilters>;
|
|
333
|
-
/** A search or a filter
|
|
339
|
+
/** A search, a filter chip or a hidden filter narrows the rows. */
|
|
334
340
|
isFiltered: ComputedRef<boolean>;
|
|
335
341
|
/** Runs after `resetFilters` (e.g. the toolbar folds its search). */
|
|
336
342
|
onFiltersCleared: EventHookOn;
|
|
@@ -485,6 +491,9 @@ const theme = tv({
|
|
|
485
491
|
// never hides past the card edge. Phones drop the content-column indent.
|
|
486
492
|
expandedBody:
|
|
487
493
|
"sticky start-0 w-[var(--dms-table-viewport,auto)] pt-4 pb-[18px] pe-[18px] ps-[18px] sm:ps-[68px]",
|
|
494
|
+
// The empty body centres on the visible part of a table wider than its
|
|
495
|
+
// card, not on the whole scroll width.
|
|
496
|
+
emptyBody: "sticky start-0 w-[var(--dms-table-viewport,auto)]",
|
|
488
497
|
|
|
489
498
|
headCell: `${HEADER_MATCH_BG} ${FIRST_HEAD_CELL_GUTTER} border-b-default text-dimmed group relative touch-none select-none overflow-hidden border-b h-9 px-3.5 py-0 ${EYEBROW_CLASS} last:pe-2.5`,
|
|
490
499
|
headCellInternal: "flex w-full items-center justify-between gap-1",
|
|
@@ -740,15 +749,14 @@ const onTableScroll = (event: Event) => {
|
|
|
740
749
|
if (!hasScrollArea.value) return;
|
|
741
750
|
isScrolled.value = (event.target as HTMLElement).scrollTop > 0;
|
|
742
751
|
};
|
|
743
|
-
// Expanded detail bands are as wide as
|
|
752
|
+
// Expanded detail bands, group headers and the empty body are as wide as
|
|
753
|
+
// the visible scroll area.
|
|
744
754
|
const tableRootRef = useTemplateRef<HTMLElement>("tableRoot");
|
|
745
755
|
const { width: tableViewportWidth } = useElementSize(tableRootRef);
|
|
746
756
|
const tableRootStyle = computed(() => ({
|
|
747
757
|
maxHeight: props.maxHeight,
|
|
748
758
|
"--dms-table-viewport":
|
|
749
|
-
|
|
750
|
-
? `${tableViewportWidth.value}px`
|
|
751
|
-
: undefined,
|
|
759
|
+
tableViewportWidth.value > 0 ? `${tableViewportWidth.value}px` : undefined,
|
|
752
760
|
}));
|
|
753
761
|
|
|
754
762
|
// A clipped cell value reads in full in a native tooltip.
|
|
@@ -1059,6 +1067,7 @@ const narrowingState: TableNarrowingState = {
|
|
|
1059
1067
|
globalFilter: globalFilterState,
|
|
1060
1068
|
pagination: paginationState,
|
|
1061
1069
|
searchApplies: computed(() => resolvedCapabilities.value.search),
|
|
1070
|
+
hiddenFiltersApply: computed(() => !!props.hiddenFiltersApply),
|
|
1062
1071
|
};
|
|
1063
1072
|
const filtersCleared = createEventHook();
|
|
1064
1073
|
const resetFilters = () => {
|
|
@@ -1621,14 +1630,16 @@ defineShortcuts({
|
|
|
1621
1630
|
"
|
|
1622
1631
|
class="p-0"
|
|
1623
1632
|
>
|
|
1624
|
-
<
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1633
|
+
<div :class="uiTable.emptyBody()">
|
|
1634
|
+
<TableEmpty
|
|
1635
|
+
:can-add-row="
|
|
1636
|
+
normalizeActionConfig(rowActions?.add).isEnabled
|
|
1637
|
+
"
|
|
1638
|
+
:load-error="loadError"
|
|
1639
|
+
:archived="isShowingArchived"
|
|
1640
|
+
:empty-states="emptyStates"
|
|
1641
|
+
/>
|
|
1642
|
+
</div>
|
|
1632
1643
|
</td>
|
|
1633
1644
|
</tr>
|
|
1634
1645
|
</tbody>
|
|
@@ -211,7 +211,7 @@ export function useActionTargets(config: ActionTargetsConfig) {
|
|
|
211
211
|
};
|
|
212
212
|
return run.target.type === "modal"
|
|
213
213
|
? openModal({ ...options, size: run.target.size })
|
|
214
|
-
: openDrawer(options);
|
|
214
|
+
: openDrawer({ ...options, direction: run.target.direction });
|
|
215
215
|
};
|
|
216
216
|
|
|
217
217
|
// A container opened on a listed row steps through the rows shown (its
|
|
@@ -19,6 +19,10 @@ export interface UseBlockItemsOptions<T> {
|
|
|
19
19
|
*/
|
|
20
20
|
fetchUrl?: string;
|
|
21
21
|
fetchUrlMethod?: string;
|
|
22
|
+
/** Id of the PeriodSelector whose period `fetchUrl` is requested with. */
|
|
23
|
+
periodScope?: string;
|
|
24
|
+
/** Topics whose events make the block read `fetchUrl` again. */
|
|
25
|
+
realtimeTopic?: string | string[];
|
|
22
26
|
/** The parameters of the page route (the block's `routeParams` prop). */
|
|
23
27
|
routeParams?: () => Record<string, string> | undefined;
|
|
24
28
|
watchActions?: WatchAction[];
|
|
@@ -35,11 +39,12 @@ export interface UseBlockItemsReturn<T> {
|
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
|
-
* The items a list block (StatGroup, KeyValueList, NavCardGrid)
|
|
39
|
-
* either the static ones of its options, or the `items` its
|
|
40
|
-
* answers with — refetched when
|
|
41
|
-
*
|
|
42
|
-
*
|
|
42
|
+
* The items a list block (StatGroup, KeyValueList, NavCardGrid, ActivityFeed)
|
|
43
|
+
* renders: either the static ones of its options, or the `items` its
|
|
44
|
+
* `fetchUrl` answers with — refetched when its period scope changes, a
|
|
45
|
+
* watched event changes the block's state, one of its realtime topics fires,
|
|
46
|
+
* or the page asks its blocks to refresh (refreshPageBlocks), like the KPI
|
|
47
|
+
* and top-list cards.
|
|
43
48
|
*/
|
|
44
49
|
export function useBlockItems<T>(
|
|
45
50
|
options: UseBlockItemsOptions<T>,
|
|
@@ -56,6 +61,8 @@ export function useBlockItems<T>(
|
|
|
56
61
|
>({
|
|
57
62
|
fetchUrl: options.fetchUrl,
|
|
58
63
|
fetchUrlMethod: options.fetchUrlMethod,
|
|
64
|
+
periodScope: options.periodScope,
|
|
65
|
+
realtimeTopic: options.realtimeTopic,
|
|
59
66
|
routeParams: options.routeParams,
|
|
60
67
|
watchSource: () => watchKey.value,
|
|
61
68
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { onBeforeUnmount, onMounted } from "vue";
|
|
2
|
+
import { useRealtimeTopic } from "../../../../../dms-core/app/composables/realtime/useRealtimeTopic";
|
|
3
|
+
import { onPageBlocksRefresh } from "../../../utils/blockRefresh";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Calls `onRefresh` whenever the data behind a block moved without its inputs
|
|
7
|
+
* changing: on each event of one of its realtime topics, and each time the
|
|
8
|
+
* page asks its blocks to refresh (refreshPageBlocks).
|
|
9
|
+
*/
|
|
10
|
+
export function useRefreshTriggers(
|
|
11
|
+
realtimeTopic: string | string[] | undefined,
|
|
12
|
+
onRefresh: () => void,
|
|
13
|
+
): void {
|
|
14
|
+
subscribeRealtimeTopics(realtimeTopic, onRefresh);
|
|
15
|
+
subscribePageRefresh(onRefresh);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function subscribeRealtimeTopics(
|
|
19
|
+
topic: string | string[] | undefined,
|
|
20
|
+
onEvent: () => void,
|
|
21
|
+
): void {
|
|
22
|
+
if (!topic) return;
|
|
23
|
+
const topics = Array.isArray(topic) ? topic : [topic];
|
|
24
|
+
for (const entry of topics) {
|
|
25
|
+
useRealtimeTopic(entry, (event) => {
|
|
26
|
+
if (!("type" in event)) return;
|
|
27
|
+
onEvent();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Mounted only: the page refresh is a browser event, and a block the server
|
|
33
|
+
// renders has nothing to refetch.
|
|
34
|
+
function subscribePageRefresh(onRefresh: () => void): void {
|
|
35
|
+
let unsubscribe: (() => void) | undefined;
|
|
36
|
+
onMounted(() => {
|
|
37
|
+
unsubscribe = onPageBlocksRefresh(onRefresh);
|
|
38
|
+
});
|
|
39
|
+
onBeforeUnmount(() => unsubscribe?.());
|
|
40
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { DataType } from "#dms-core/app/composables/data-types/useDataType"
|
|
|
3
3
|
import StatusPill from "../../../components/status-pill/StatusPill.vue";
|
|
4
4
|
import MonoCell from "../../components/table/MonoCell.vue";
|
|
5
5
|
import { useComposedText } from "../../../../../dms-core/app/composables/translation/useComposedText";
|
|
6
|
+
import { isDmsTone } from "../../utils/tone";
|
|
6
7
|
import { readRowField, readSubline, toneClass, type Row } from "./cellHelpers";
|
|
7
8
|
|
|
8
9
|
const Sparkline = defineAsyncComponent(
|
|
@@ -21,6 +22,8 @@ interface SelectItemOption {
|
|
|
21
22
|
|
|
22
23
|
interface StatusPillOptions {
|
|
23
24
|
tones?: Record<string, string>;
|
|
25
|
+
/** Row field holding the pill's tone, read before `tones`. */
|
|
26
|
+
toneField?: string;
|
|
24
27
|
subField?: string;
|
|
25
28
|
/** Tone of the line under the pill when the row gives none. */
|
|
26
29
|
subTone?: string;
|
|
@@ -29,10 +32,18 @@ interface StatusPillOptions {
|
|
|
29
32
|
typeOptions?: { items?: SelectItemOption[] };
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
// A row field that holds no known tone gives way to the column's map.
|
|
36
|
+
function statusPillTone(opts: StatusPillOptions, key: string, row: Row) {
|
|
37
|
+
const rowTone = readRowField(row, opts.toneField);
|
|
38
|
+
if (typeof rowTone === "string" && isDmsTone(rowTone)) return rowTone;
|
|
39
|
+
return opts.tones?.[key] ?? "neutral";
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
/**
|
|
33
|
-
* `status_pill`: a tinted pill led by a dot, in the tone the
|
|
34
|
-
* with an optional line under it (a
|
|
35
|
-
* 10") in the row's tone, else
|
|
43
|
+
* `status_pill`: a tinted pill led by a dot, in the tone the row gives
|
|
44
|
+
* (`toneField`) or the value maps to, with an optional line under it (a
|
|
45
|
+
* failure's cause, a composed "retry Oct 10") in the row's tone, else
|
|
46
|
+
* `subTone`, else the pill's.
|
|
36
47
|
*/
|
|
37
48
|
function renderStatusPill(value: unknown, options: unknown, row: Row) {
|
|
38
49
|
const { processI18n } = useTranslation();
|
|
@@ -42,7 +53,7 @@ function renderStatusPill(value: unknown, options: unknown, row: Row) {
|
|
|
42
53
|
const item = opts.typeOptions?.items?.find(
|
|
43
54
|
(candidate) => String(candidate.value) === key,
|
|
44
55
|
);
|
|
45
|
-
const tone = opts
|
|
56
|
+
const tone = statusPillTone(opts, key, row);
|
|
46
57
|
const pill = h(StatusPill, {
|
|
47
58
|
tone,
|
|
48
59
|
label: processI18n(item?.label ?? key),
|