@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
|
@@ -31,6 +31,10 @@ interface StatGroupBlockProps extends DefaultComponentProps {
|
|
|
31
31
|
label?: string;
|
|
32
32
|
fetchUrl?: string;
|
|
33
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[];
|
|
34
38
|
/** Shown when there is nothing to list. */
|
|
35
39
|
empty?: BlockEmptyText;
|
|
36
40
|
/**
|
|
@@ -47,6 +51,8 @@ const props = withDefaults(defineProps<StatGroupBlockProps>(), {
|
|
|
47
51
|
label: undefined,
|
|
48
52
|
fetchUrl: undefined,
|
|
49
53
|
fetchUrlMethod: undefined,
|
|
54
|
+
periodScope: undefined,
|
|
55
|
+
realtimeTopic: undefined,
|
|
50
56
|
empty: undefined,
|
|
51
57
|
skeletonCount: undefined,
|
|
52
58
|
});
|
|
@@ -60,6 +66,8 @@ const { items, isPending, hasError, refresh } =
|
|
|
60
66
|
items: () => props.items,
|
|
61
67
|
fetchUrl: props.fetchUrl,
|
|
62
68
|
fetchUrlMethod: props.fetchUrlMethod,
|
|
69
|
+
periodScope: props.periodScope,
|
|
70
|
+
realtimeTopic: props.realtimeTopic,
|
|
63
71
|
routeParams: () => props.routeParams,
|
|
64
72
|
watchActions: props.watchActions,
|
|
65
73
|
componentId: props.componentId,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, shallowRef } from "vue";
|
|
3
|
+
import DmsCopyButton from "../copy/CopyButton.vue";
|
|
4
|
+
import {
|
|
5
|
+
codeLanguageLabel,
|
|
6
|
+
PLAIN_CODE_LANGUAGE,
|
|
7
|
+
resolveCodeLanguage,
|
|
8
|
+
} from "../../build/utils/codeLanguages";
|
|
9
|
+
import type { CodeToken, highlightCode } from "../../build/utils/codeHighlight";
|
|
10
|
+
|
|
11
|
+
// Read-only code in the editor's look: a head with the title, the language
|
|
12
|
+
// and a copy button, then the code highlighted by the editor's languages.
|
|
13
|
+
// Until the highlighter loads (and on the server) the code shows as is.
|
|
14
|
+
interface CodeSnippetProps {
|
|
15
|
+
/** The code, verbatim. */
|
|
16
|
+
code: string;
|
|
17
|
+
/**
|
|
18
|
+
* `json`, `javascript`, `typescript`, `html`, `sql`, `shell` or `text`;
|
|
19
|
+
* `js`, `ts`, `bash`, `sh` and `plain` are read as theirs.
|
|
20
|
+
*/
|
|
21
|
+
language?: string;
|
|
22
|
+
/** A button copies the code. */
|
|
23
|
+
copy?: boolean;
|
|
24
|
+
/** Shown in the head, as written. */
|
|
25
|
+
title?: string;
|
|
26
|
+
/** Lines shown before the code scrolls; all of them when unset. */
|
|
27
|
+
maxLines?: number;
|
|
28
|
+
/** Wraps long lines instead of scrolling sideways. */
|
|
29
|
+
wrap?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const props = withDefaults(defineProps<CodeSnippetProps>(), {
|
|
33
|
+
language: undefined,
|
|
34
|
+
copy: true,
|
|
35
|
+
title: undefined,
|
|
36
|
+
maxLines: undefined,
|
|
37
|
+
wrap: false,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const LINE_HEIGHT_PX = 19;
|
|
41
|
+
// The code's top and bottom padding (`py-2`), around its lines.
|
|
42
|
+
const CODE_PADDING_PX = 16;
|
|
43
|
+
|
|
44
|
+
const highlight = shallowRef<typeof highlightCode | null>(null);
|
|
45
|
+
|
|
46
|
+
const language = computed(() => resolveCodeLanguage(props.language));
|
|
47
|
+
const hasHead = computed(
|
|
48
|
+
() => !!props.title || props.copy || language.value !== PLAIN_CODE_LANGUAGE,
|
|
49
|
+
);
|
|
50
|
+
const tokens = computed<CodeToken[]>(() =>
|
|
51
|
+
highlight.value
|
|
52
|
+
? highlight.value(props.code, language.value)
|
|
53
|
+
: [{ text: props.code }],
|
|
54
|
+
);
|
|
55
|
+
const sizeStyle = computed(() =>
|
|
56
|
+
props.maxLines
|
|
57
|
+
? {
|
|
58
|
+
maxHeight: `${props.maxLines * LINE_HEIGHT_PX + CODE_PADDING_PX}px`,
|
|
59
|
+
}
|
|
60
|
+
: undefined,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
onMounted(async () => {
|
|
64
|
+
const module = await import("../../build/utils/codeHighlight");
|
|
65
|
+
highlight.value = module.highlightCode;
|
|
66
|
+
});
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<div
|
|
71
|
+
class="border-default min-w-0 overflow-hidden rounded-[10px] border bg-(--ui-bg)"
|
|
72
|
+
>
|
|
73
|
+
<div
|
|
74
|
+
v-if="hasHead"
|
|
75
|
+
class="border-default flex min-h-9 items-center gap-2 border-b bg-(--dms-bg-muted) py-1 ps-3.5 pe-1"
|
|
76
|
+
>
|
|
77
|
+
<span
|
|
78
|
+
v-if="props.title"
|
|
79
|
+
class="text-highlighted min-w-0 truncate text-[12.5px] font-semibold"
|
|
80
|
+
>
|
|
81
|
+
{{ props.title }}
|
|
82
|
+
</span>
|
|
83
|
+
<span class="text-dimmed ms-auto font-mono text-[11px]">
|
|
84
|
+
{{ codeLanguageLabel(language) }}
|
|
85
|
+
</span>
|
|
86
|
+
<DmsCopyButton v-if="props.copy" :value="props.code" />
|
|
87
|
+
</div>
|
|
88
|
+
<!-- Focusable: a keyboard scrolls the code when it overflows. -->
|
|
89
|
+
<pre
|
|
90
|
+
class="text-highlighted m-0 overflow-auto px-3.5 py-2 font-mono text-[12.5px] leading-[19px]"
|
|
91
|
+
:class="props.wrap ? 'break-words whitespace-pre-wrap' : 'whitespace-pre'"
|
|
92
|
+
:style="sizeStyle"
|
|
93
|
+
:data-language="language"
|
|
94
|
+
tabindex="0"
|
|
95
|
+
><code><span v-for="(token, index) in tokens" :key="index" :style="token.style">{{ token.text }}</span></code></pre>
|
|
96
|
+
</div>
|
|
97
|
+
</template>
|
|
@@ -35,6 +35,8 @@ interface EmptyStateProps {
|
|
|
35
35
|
interface EmptyStateSlots {
|
|
36
36
|
/** Rich description, replaces `description`. */
|
|
37
37
|
default?: () => unknown;
|
|
38
|
+
/** Under the text, above the actions: a snippet to start with. */
|
|
39
|
+
content?: () => unknown;
|
|
38
40
|
/** Buttons or links under the text. */
|
|
39
41
|
actions?: () => unknown;
|
|
40
42
|
}
|
|
@@ -71,6 +73,7 @@ const theme = tv({
|
|
|
71
73
|
well: "mb-1.5",
|
|
72
74
|
title: "text-highlighted",
|
|
73
75
|
description: "text-muted",
|
|
76
|
+
content: "mt-2.5 w-full max-w-[60ch] text-start",
|
|
74
77
|
actions: "mt-2.5 flex flex-wrap items-center justify-center gap-2",
|
|
75
78
|
},
|
|
76
79
|
variants: {
|
|
@@ -106,8 +109,11 @@ const resolvedIcon = computed(() => props.icon ?? VARIANT_ICONS[props.variant]);
|
|
|
106
109
|
const resolvedTone = computed<Tone>(
|
|
107
110
|
() => props.tone ?? (props.variant === "error" ? "error" : "neutral"),
|
|
108
111
|
);
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
// Read at each render rather than in a computed: the slots object is not
|
|
113
|
+
// reactive, so a computed would keep the slots of the first render and miss
|
|
114
|
+
// a slot the parent adds or removes later.
|
|
115
|
+
const hasDescription = (): boolean => !!props.description || !!slots.default;
|
|
116
|
+
const hasActions = (): boolean => !!slots.actions || !!props.actions?.length;
|
|
111
117
|
</script>
|
|
112
118
|
|
|
113
119
|
<template>
|
|
@@ -122,10 +128,13 @@ const hasActions = computed(() => !!slots.actions || !!props.actions?.length);
|
|
|
122
128
|
:class="ui.well()"
|
|
123
129
|
/>
|
|
124
130
|
<p :class="ui.title()">{{ props.title }}</p>
|
|
125
|
-
<p v-if="hasDescription" :class="ui.description()">
|
|
131
|
+
<p v-if="hasDescription()" :class="ui.description()">
|
|
126
132
|
<slot>{{ props.description }}</slot>
|
|
127
133
|
</p>
|
|
128
|
-
<div v-if="
|
|
134
|
+
<div v-if="slots.content" :class="ui.content()">
|
|
135
|
+
<slot name="content" />
|
|
136
|
+
</div>
|
|
137
|
+
<div v-if="hasActions()" :class="ui.actions()">
|
|
129
138
|
<slot name="actions">
|
|
130
139
|
<UButton
|
|
131
140
|
v-for="(action, index) in props.actions"
|
|
@@ -29,7 +29,9 @@ import { computed, useId, useSlots } from "vue";
|
|
|
29
29
|
// Delete/Backspace remove the selection. Both paths only *emit* the
|
|
30
30
|
// `delete-nodes` intent — the badge for its own node, Delete/Backspace for
|
|
31
31
|
// the whole selection — the wrapper never mutates the consumer's
|
|
32
|
-
// `nodes`/`edges`; the consumer applies the removal.
|
|
32
|
+
// `nodes`/`edges`; the consumer applies the removal. `deleteBadge: false`
|
|
33
|
+
// hides the badge and keeps the keyboard path, for a consumer that draws
|
|
34
|
+
// its own delete control.
|
|
33
35
|
// - `zoomReadout` adds a live zoom percentage to the control bar that doubles
|
|
34
36
|
// as a reset-to-100% button.
|
|
35
37
|
//
|
|
@@ -119,6 +121,11 @@ interface Props {
|
|
|
119
121
|
* Delete + Backspace.
|
|
120
122
|
*/
|
|
121
123
|
deleteKeyCode?: DeleteKeyCode;
|
|
124
|
+
/**
|
|
125
|
+
* Draw the delete badge on selected nodes while `deletableNodes` is on.
|
|
126
|
+
* `false` leaves the keyboard path alone. Defaults to `true`.
|
|
127
|
+
*/
|
|
128
|
+
deleteBadge?: boolean;
|
|
122
129
|
/**
|
|
123
130
|
* Add a live zoom percentage / reset-to-100% control to the control bar.
|
|
124
131
|
* It renders inside the bar, so it requires `controls` to be enabled.
|
|
@@ -141,6 +148,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
141
148
|
height: "100%",
|
|
142
149
|
deletableNodes: false,
|
|
143
150
|
deleteKeyCode: undefined,
|
|
151
|
+
deleteBadge: true,
|
|
144
152
|
zoomReadout: false,
|
|
145
153
|
});
|
|
146
154
|
|
|
@@ -198,7 +206,7 @@ const zoomPercent = computed(() =>
|
|
|
198
206
|
// Project each selected node's top-right corner from flow space to screen
|
|
199
207
|
// space through the live viewport transform, so badges track pan/zoom/drag.
|
|
200
208
|
const deleteAffordances = computed<DeleteAffordance[]>(() => {
|
|
201
|
-
if (!props.deletableNodes) return [];
|
|
209
|
+
if (!props.deletableNodes || !props.deleteBadge) return [];
|
|
202
210
|
const {
|
|
203
211
|
x: panX,
|
|
204
212
|
y: panY,
|
|
@@ -295,7 +303,10 @@ defineExpose(flow);
|
|
|
295
303
|
<slot />
|
|
296
304
|
</VueFlow>
|
|
297
305
|
|
|
298
|
-
<div
|
|
306
|
+
<div
|
|
307
|
+
v-if="deletableNodes && deleteBadge"
|
|
308
|
+
class="dms-flow-canvas__delete-layer"
|
|
309
|
+
>
|
|
299
310
|
<button
|
|
300
311
|
v-for="affordance in deleteAffordances"
|
|
301
312
|
:key="affordance.id"
|
|
@@ -3,6 +3,7 @@ import { getCurrentInstance, h, render } from "vue";
|
|
|
3
3
|
import { useFormField } from "@nuxt/ui/composables/useFormField";
|
|
4
4
|
import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
|
|
5
5
|
import { useControlError } from "../../../build/composables/form/useControlError";
|
|
6
|
+
import { codeLanguageLabel } from "../../../build/utils/codeLanguages";
|
|
6
7
|
import type {
|
|
7
8
|
CodeEditor,
|
|
8
9
|
CodeEditorCompletion,
|
|
@@ -47,13 +48,6 @@ const LINE_HEIGHT_PX = 19;
|
|
|
47
48
|
// The editor's top and bottom padding, around its lines.
|
|
48
49
|
const EDITOR_PADDING_PX = 16;
|
|
49
50
|
const INVALID_JSON = "$dms.field_errors.invalid_json";
|
|
50
|
-
const LANGUAGE_LABELS: Record<string, string> = {
|
|
51
|
-
json: "JSON",
|
|
52
|
-
sql: "SQL",
|
|
53
|
-
html: "HTML",
|
|
54
|
-
javascript: "JavaScript",
|
|
55
|
-
text: "Text",
|
|
56
|
-
};
|
|
57
51
|
|
|
58
52
|
const host = useTemplateRef<HTMLElement>("host");
|
|
59
53
|
const position = ref<CodeEditorPosition>({ line: 1, column: 1 });
|
|
@@ -188,7 +182,7 @@ onBeforeUnmount(() => {
|
|
|
188
182
|
{{ t("dms.form.code.complete") }}
|
|
189
183
|
</span>
|
|
190
184
|
<span class="ms-auto">
|
|
191
|
-
{{
|
|
185
|
+
{{ codeLanguageLabel(props.language) }} ·
|
|
192
186
|
{{ t("dms.form.code.position", position) }}
|
|
193
187
|
</span>
|
|
194
188
|
</div>
|
|
@@ -3,6 +3,7 @@ import { computed } from "vue";
|
|
|
3
3
|
import { regionalDateTimeFormat } from "#dms-core/app/utils/regional";
|
|
4
4
|
import DmsStatusPill from "../status-pill/StatusPill.vue";
|
|
5
5
|
import DmsAutoLink from "../link/AutoLink.vue";
|
|
6
|
+
import DmsCopyButton from "../copy/CopyButton.vue";
|
|
6
7
|
import { DMS_TONE_TEXT } from "../../build/utils/tone";
|
|
7
8
|
import type { Tone } from "../../types/tone";
|
|
8
9
|
|
|
@@ -36,6 +37,10 @@ export interface KeyValueItem {
|
|
|
36
37
|
currency?: string;
|
|
37
38
|
/** This value alone is still loading: a skeleton stands in for it. */
|
|
38
39
|
loading?: boolean;
|
|
40
|
+
/** Draws a button copying the value as it is shown. */
|
|
41
|
+
copy?: boolean;
|
|
42
|
+
/** Text the copy button copies instead of the shown value; implies `copy`. */
|
|
43
|
+
copyValue?: string;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
interface KeyValueListProps {
|
|
@@ -156,6 +161,14 @@ function valueClass(item: KeyValueItem): string[] {
|
|
|
156
161
|
return ["wrap-anywhere", color, TYPE_CLASSES[type] ?? ""];
|
|
157
162
|
}
|
|
158
163
|
|
|
164
|
+
// An empty value shows a dash, which is not worth copying.
|
|
165
|
+
function copyTextOf(item: KeyValueItem): string | undefined {
|
|
166
|
+
if (item.copyValue) return item.copyValue;
|
|
167
|
+
if (!item.copy) return undefined;
|
|
168
|
+
const formatted = format(item);
|
|
169
|
+
return formatted === EMPTY_VALUE ? undefined : formatted;
|
|
170
|
+
}
|
|
171
|
+
|
|
159
172
|
function keyOf(item: KeyValueItem, index: number): string {
|
|
160
173
|
return item.id ?? `${index}-${item.label}`;
|
|
161
174
|
}
|
|
@@ -216,6 +229,12 @@ function keyOf(item: KeyValueItem, index: number): string {
|
|
|
216
229
|
{{ item.detail }}
|
|
217
230
|
</span>
|
|
218
231
|
</template>
|
|
232
|
+
<!-- -my-1: the button is taller than a dense row's text line. -->
|
|
233
|
+
<DmsCopyButton
|
|
234
|
+
v-if="!props.loading && !item.loading && copyTextOf(item)"
|
|
235
|
+
:value="copyTextOf(item)!"
|
|
236
|
+
class="-my-1"
|
|
237
|
+
/>
|
|
219
238
|
</dd>
|
|
220
239
|
</div>
|
|
221
240
|
</template>
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
// left (380px) and a detail panel on the right. The list is driven by `items`
|
|
4
4
|
// + v-model; the detail is rendered by the default slot, which receives the
|
|
5
5
|
// currently selected value.
|
|
6
|
+
//
|
|
7
|
+
// The panes sit side by side from 64rem of the layout's own width (the former
|
|
8
|
+
// `lg` viewport breakpoint), not the viewport's: a docked side panel or a
|
|
9
|
+
// narrow grid cell leaves less room than the screen suggests.
|
|
6
10
|
interface MdItem {
|
|
7
11
|
value: string | number;
|
|
8
12
|
label: string;
|
|
@@ -23,59 +27,61 @@ defineSlots<{
|
|
|
23
27
|
</script>
|
|
24
28
|
|
|
25
29
|
<template>
|
|
26
|
-
<div
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<p
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<span
|
|
50
|
-
v-if="item.icon"
|
|
51
|
-
class="grid size-[26px] shrink-0 place-items-center"
|
|
52
|
-
:class="selected === item.value ? 'text-primary' : 'text-toned'"
|
|
30
|
+
<div class="@container">
|
|
31
|
+
<div
|
|
32
|
+
class="grid grid-cols-1 items-start gap-5 @5xl:grid-cols-[380px_minmax(0,1fr)]"
|
|
33
|
+
>
|
|
34
|
+
<!-- List pane -->
|
|
35
|
+
<div class="dms-card p-2">
|
|
36
|
+
<p
|
|
37
|
+
v-if="listLabel"
|
|
38
|
+
class="text-dimmed px-2.5 pt-2.5 pb-1 font-mono text-[10px] tracking-widest uppercase"
|
|
39
|
+
>
|
|
40
|
+
{{ listLabel }}
|
|
41
|
+
</p>
|
|
42
|
+
<ul class="flex flex-col gap-1.5">
|
|
43
|
+
<li v-for="item in items" :key="item.value">
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
class="flex w-full items-start gap-3 rounded-md border border-transparent px-3.5 py-3 text-left transition-colors"
|
|
47
|
+
:class="
|
|
48
|
+
selected === item.value
|
|
49
|
+
? 'border-primary bg-primary/10'
|
|
50
|
+
: 'hover:bg-elevated'
|
|
51
|
+
"
|
|
52
|
+
@click="selected = item.value"
|
|
53
53
|
>
|
|
54
|
-
<UIcon
|
|
55
|
-
:name="item.icon"
|
|
56
|
-
class="size-[18px]"
|
|
57
|
-
:aria-hidden="true"
|
|
58
|
-
/>
|
|
59
|
-
</span>
|
|
60
|
-
<span class="min-w-0">
|
|
61
|
-
<span class="text-highlighted block text-sm font-semibold">
|
|
62
|
-
{{ item.label }}
|
|
63
|
-
</span>
|
|
64
54
|
<span
|
|
65
|
-
v-if="item.
|
|
66
|
-
class="
|
|
55
|
+
v-if="item.icon"
|
|
56
|
+
class="grid size-[26px] shrink-0 place-items-center"
|
|
57
|
+
:class="selected === item.value ? 'text-primary' : 'text-toned'"
|
|
67
58
|
>
|
|
68
|
-
|
|
59
|
+
<UIcon
|
|
60
|
+
:name="item.icon"
|
|
61
|
+
class="size-[18px]"
|
|
62
|
+
:aria-hidden="true"
|
|
63
|
+
/>
|
|
69
64
|
</span>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
65
|
+
<span class="min-w-0">
|
|
66
|
+
<span class="text-highlighted block text-sm font-semibold">
|
|
67
|
+
{{ item.label }}
|
|
68
|
+
</span>
|
|
69
|
+
<span
|
|
70
|
+
v-if="item.sublabel"
|
|
71
|
+
class="text-muted mt-0.5 block truncate text-xs"
|
|
72
|
+
>
|
|
73
|
+
{{ item.sublabel }}
|
|
74
|
+
</span>
|
|
75
|
+
</span>
|
|
76
|
+
</button>
|
|
77
|
+
</li>
|
|
78
|
+
</ul>
|
|
79
|
+
</div>
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
<!-- Detail pane -->
|
|
82
|
+
<div class="dms-card overflow-hidden">
|
|
83
|
+
<slot :selected="selected" />
|
|
84
|
+
</div>
|
|
79
85
|
</div>
|
|
80
86
|
</div>
|
|
81
87
|
</template>
|
|
@@ -11,28 +11,35 @@ import DmsBlockActions, {
|
|
|
11
11
|
} from "../../build/components/blocks/BlockActions.vue";
|
|
12
12
|
import { useChartFetch } from "../../composables/chart/useChartFetch";
|
|
13
13
|
import { useWatch } from "../../../../dms-core/app/composables/watch/useWatch";
|
|
14
|
+
import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
|
|
14
15
|
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
16
|
+
import type { BlockText } from "../../../../dms-core/app/types/composed-text";
|
|
17
|
+
|
|
18
|
+
/** A segment as the page or the route sends it, its legend still to write. */
|
|
19
|
+
interface MeterBlockSegment extends Omit<MeterSegment, "label"> {
|
|
20
|
+
label?: BlockText;
|
|
21
|
+
}
|
|
15
22
|
|
|
16
23
|
/** What a meter's data source answers: any subset of its figures. */
|
|
17
24
|
interface MeterResponse {
|
|
18
25
|
value?: number;
|
|
19
26
|
max?: number;
|
|
20
|
-
segments?:
|
|
21
|
-
hint?:
|
|
22
|
-
valueLabel?:
|
|
27
|
+
segments?: MeterBlockSegment[];
|
|
28
|
+
hint?: BlockText;
|
|
29
|
+
valueLabel?: BlockText;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
// The page block behind the backend `Meter` builder: static figures or a
|
|
26
33
|
// data source, optionally in a card. DmsMeter draws it.
|
|
27
34
|
interface MeterBlockProps extends DefaultComponentProps {
|
|
28
35
|
label?: string;
|
|
29
|
-
hint?:
|
|
36
|
+
hint?: BlockText;
|
|
30
37
|
value?: number;
|
|
31
38
|
max?: number;
|
|
32
|
-
segments?:
|
|
39
|
+
segments?: MeterBlockSegment[];
|
|
33
40
|
legend?: boolean;
|
|
34
41
|
format?: MeterFormat;
|
|
35
|
-
valueLabel?:
|
|
42
|
+
valueLabel?: BlockText;
|
|
36
43
|
tone?: MeterTone;
|
|
37
44
|
warnAt?: number;
|
|
38
45
|
errorAt?: number;
|
|
@@ -40,6 +47,10 @@ interface MeterBlockProps extends DefaultComponentProps {
|
|
|
40
47
|
/** Endpoint answering `{ value?, max?, segments?, hint?, valueLabel? }`. */
|
|
41
48
|
fetchUrl?: string;
|
|
42
49
|
fetchUrlMethod?: string;
|
|
50
|
+
/** Id of the PeriodSelector the data follows. */
|
|
51
|
+
periodScope?: string;
|
|
52
|
+
/** Topics whose events make the block read `fetchUrl` again. */
|
|
53
|
+
realtimeTopic?: string | string[];
|
|
43
54
|
/** Wraps the meter in a padded card. */
|
|
44
55
|
card?: boolean;
|
|
45
56
|
/** Link buttons pushed right of the legend ("Manage members"). */
|
|
@@ -61,11 +72,13 @@ const props = withDefaults(defineProps<MeterBlockProps>(), {
|
|
|
61
72
|
size: "sm",
|
|
62
73
|
fetchUrl: undefined,
|
|
63
74
|
fetchUrlMethod: undefined,
|
|
75
|
+
periodScope: undefined,
|
|
76
|
+
realtimeTopic: undefined,
|
|
64
77
|
card: false,
|
|
65
78
|
actions: () => [],
|
|
66
79
|
});
|
|
67
80
|
|
|
68
|
-
const {
|
|
81
|
+
const { processText } = useComposedText();
|
|
69
82
|
const { t } = useI18n();
|
|
70
83
|
|
|
71
84
|
const { state: watchState } = useWatch(
|
|
@@ -76,12 +89,14 @@ const { state: watchState } = useWatch(
|
|
|
76
89
|
const { data, isLoading, error, refresh } = useChartFetch<MeterResponse>({
|
|
77
90
|
fetchUrl: props.fetchUrl,
|
|
78
91
|
fetchUrlMethod: props.fetchUrlMethod,
|
|
92
|
+
periodScope: props.periodScope,
|
|
93
|
+
realtimeTopic: props.realtimeTopic,
|
|
79
94
|
routeParams: () => props.routeParams,
|
|
80
95
|
watchSource: () => JSON.stringify(watchState.value),
|
|
81
96
|
});
|
|
82
97
|
|
|
83
|
-
const translate = (text:
|
|
84
|
-
text ?
|
|
98
|
+
const translate = (text: BlockText | undefined): string | undefined =>
|
|
99
|
+
text ? processText(text) : undefined;
|
|
85
100
|
|
|
86
101
|
const figures = computed(() => {
|
|
87
102
|
const fetched = data.value ?? {};
|
|
@@ -20,16 +20,19 @@ const props = withDefaults(defineProps<TabProps>(), {
|
|
|
20
20
|
persistState: false,
|
|
21
21
|
stateKey: "tab",
|
|
22
22
|
badgesUrl: undefined,
|
|
23
|
+
realtimeTopic: undefined,
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
// The badges a module counts for a record's tabs, in one request, again when
|
|
26
|
-
// a watch action fires: each replaces its tab's static
|
|
27
|
+
// a watch action or a realtime topic fires: each replaces its tab's static
|
|
28
|
+
// badge.
|
|
27
29
|
const { state: watchState } = useWatch(
|
|
28
30
|
props.watchActions || [],
|
|
29
31
|
props.componentId,
|
|
30
32
|
);
|
|
31
33
|
const { data: fetchedBadges } = useChartFetch<TabBadges>({
|
|
32
34
|
fetchUrl: props.badgesUrl,
|
|
35
|
+
realtimeTopic: props.realtimeTopic,
|
|
33
36
|
routeParams: () => props.routeParams,
|
|
34
37
|
watchSource: () => JSON.stringify(watchState.value),
|
|
35
38
|
});
|