@antelopejs/dms 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -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 +2 -2
- package/frontend-vue/pnpm-lock.yaml +5 -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
|
@@ -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
|
});
|
|
@@ -47,7 +47,6 @@ import {
|
|
|
47
47
|
} from "../../composables/table-view/kanban";
|
|
48
48
|
import { useTableRowActions } from "../../build/composables/table-view/useTableViewRowActions";
|
|
49
49
|
import {
|
|
50
|
-
buildTableDataKey,
|
|
51
50
|
buildTableQuery,
|
|
52
51
|
isFilterEffective,
|
|
53
52
|
} from "../../build/composables/table-view/utils/tableQuery";
|
|
@@ -107,7 +106,9 @@ import { useTableViews } from "../../build/composables/table-view/useTableViews"
|
|
|
107
106
|
import { useGroupedRows } from "../../build/composables/table-view/useGroupedRows";
|
|
108
107
|
import { useTableFooter } from "../../build/composables/table-view/useTableFooter";
|
|
109
108
|
import { useAccumulatedPages } from "../../build/composables/table-view/useAccumulatedPages";
|
|
109
|
+
import { usePersistedPagination } from "../../build/composables/table-view/usePersistedPagination";
|
|
110
110
|
import { useTableRows } from "../../build/composables/table-view/useTableRows";
|
|
111
|
+
import { useRefreshTriggers } from "../../build/composables/blocks/useRefreshTriggers";
|
|
111
112
|
import { useTableReorder } from "../../build/composables/table-view/useTableReorder";
|
|
112
113
|
import {
|
|
113
114
|
groupedOptionsFor,
|
|
@@ -118,7 +119,10 @@ import { readTableUrlKey } from "../../build/composables/table-view/utils/views"
|
|
|
118
119
|
import TableViews, {
|
|
119
120
|
type TableViewItem,
|
|
120
121
|
} from "../../build/components/table/Views.vue";
|
|
121
|
-
import {
|
|
122
|
+
import {
|
|
123
|
+
useTableCountsData,
|
|
124
|
+
useTableListData,
|
|
125
|
+
} from "../../build/composables/table-view/useTableListData";
|
|
122
126
|
import { useTableDataChanges } from "../../composables/table-view/useTableDataChanges";
|
|
123
127
|
|
|
124
128
|
const REALTIME_ROW_TOPIC_PREFIX = "tableview:row:";
|
|
@@ -172,6 +176,8 @@ interface TableViewProps<T extends Data> extends TableViewConfig<T> {
|
|
|
172
176
|
tableId?: string;
|
|
173
177
|
/** The page carries no other table view: `?view=` / `?tab=` are its own. */
|
|
174
178
|
isSoleTableView?: boolean;
|
|
179
|
+
/** Topics whose events make the table read its rows again. */
|
|
180
|
+
realtimeTopic?: string | string[];
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
const props = defineProps<TableViewProps<T>>();
|
|
@@ -347,12 +353,42 @@ const resolvedChrome = resolveTableChrome(props.layout, {
|
|
|
347
353
|
});
|
|
348
354
|
const GLOBAL_FILTER_DEBOUNCE_MS = 400;
|
|
349
355
|
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
);
|
|
356
|
+
const route = useDmsRoute();
|
|
357
|
+
|
|
358
|
+
const DEFAULT_FILTER_MODE = "is";
|
|
359
|
+
|
|
360
|
+
const queryParamHiddenFilters = computed<TableFilter[]>(() => {
|
|
361
|
+
if (!queryParamFilters) return [];
|
|
362
|
+
|
|
363
|
+
return Object.entries(queryParamFilters)
|
|
364
|
+
.filter(([param]) => route.query[param] !== undefined)
|
|
365
|
+
.map(([param, config]) => ({
|
|
366
|
+
accessorKey: config.field,
|
|
367
|
+
mode: config.mode || DEFAULT_FILTER_MODE,
|
|
368
|
+
value: route.query[param] as string,
|
|
369
|
+
}));
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const routeParamHiddenFilters = computed<TableFilter[]>(() => {
|
|
373
|
+
if (!routeParamFilters || !routeParams) return [];
|
|
374
|
+
|
|
375
|
+
return Object.entries(routeParamFilters)
|
|
376
|
+
.filter(([param]) => routeParams[param] !== undefined)
|
|
377
|
+
.map(([param, config]) => ({
|
|
378
|
+
accessorKey: config.field,
|
|
379
|
+
mode: config.mode || DEFAULT_FILTER_MODE,
|
|
380
|
+
value: routeParams[param] as string,
|
|
381
|
+
}));
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// A hidden filter's value changed with the URL, in place or through a
|
|
385
|
+
// remount: the page reached among the previous rows lists from page 1.
|
|
386
|
+
const paginationState = usePersistedPagination({
|
|
387
|
+
preferences: { getPreference, setPreference },
|
|
388
|
+
preferenceKey: getTablePreferenceKey,
|
|
389
|
+
defaults: DEFAULT_PAGINATION,
|
|
390
|
+
scope: () => [queryParamHiddenFilters.value, routeParamHiddenFilters.value],
|
|
391
|
+
});
|
|
356
392
|
const rowSelect = ref<RowSelectionState>({});
|
|
357
393
|
// A column's grid header, for the sort messages.
|
|
358
394
|
const columnLabel = (id: string): string => {
|
|
@@ -615,35 +651,11 @@ const tableRows = useTableRows<T>({
|
|
|
615
651
|
location,
|
|
616
652
|
source: props.source,
|
|
617
653
|
columns: props.columns,
|
|
654
|
+
route: () => ({
|
|
655
|
+
routeParams: props.routeParams,
|
|
656
|
+
routeQuery: route.query as Record<string, unknown>,
|
|
657
|
+
}),
|
|
618
658
|
});
|
|
619
|
-
const route = useDmsRoute();
|
|
620
|
-
|
|
621
|
-
const DEFAULT_FILTER_MODE = "is";
|
|
622
|
-
|
|
623
|
-
const queryParamHiddenFilters = computed<TableFilter[]>(() => {
|
|
624
|
-
if (!queryParamFilters) return [];
|
|
625
|
-
|
|
626
|
-
return Object.entries(queryParamFilters)
|
|
627
|
-
.filter(([param]) => route.query[param] !== undefined)
|
|
628
|
-
.map(([param, config]) => ({
|
|
629
|
-
accessorKey: config.field,
|
|
630
|
-
mode: config.mode || DEFAULT_FILTER_MODE,
|
|
631
|
-
value: route.query[param] as string,
|
|
632
|
-
}));
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
const routeParamHiddenFilters = computed<TableFilter[]>(() => {
|
|
636
|
-
if (!routeParamFilters || !routeParams) return [];
|
|
637
|
-
|
|
638
|
-
return Object.entries(routeParamFilters)
|
|
639
|
-
.filter(([param]) => routeParams[param] !== undefined)
|
|
640
|
-
.map(([param, config]) => ({
|
|
641
|
-
accessorKey: config.field,
|
|
642
|
-
mode: config.mode || DEFAULT_FILTER_MODE,
|
|
643
|
-
value: routeParams[param] as string,
|
|
644
|
-
}));
|
|
645
|
-
});
|
|
646
|
-
|
|
647
659
|
// Quick filters: dropdowns of the toolbar over a column's values (a select's
|
|
648
660
|
// items, a boolean, the rows a relation points to), writing that column's
|
|
649
661
|
// filter like the filters row does.
|
|
@@ -788,34 +800,15 @@ const archiveQuery = computed(() =>
|
|
|
788
800
|
archiveMode ? { showArchived: showArchived.value } : {},
|
|
789
801
|
);
|
|
790
802
|
|
|
791
|
-
const
|
|
792
|
-
results: [],
|
|
793
|
-
total: 0,
|
|
794
|
-
offset: 0,
|
|
795
|
-
limit: 0,
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
const tableDataKey = buildTableDataKey({
|
|
803
|
+
const { data, status, error, refresh } = await useTableListData<T>({
|
|
799
804
|
componentId,
|
|
800
805
|
pageId,
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
806
|
+
rows: tableRows,
|
|
807
|
+
query: queryRequest,
|
|
808
|
+
archiveQuery,
|
|
809
|
+
isSelfManaged: isActiveDisplaySelfManaged,
|
|
804
810
|
});
|
|
805
811
|
|
|
806
|
-
const { data, status, error, refresh } = await useServerRenderedAsyncData(
|
|
807
|
-
tableDataKey,
|
|
808
|
-
(): Promise<TableViewListResponse<T>> => {
|
|
809
|
-
// Self-managed displays (e.g. kanban) fetch their own data; skip the
|
|
810
|
-
// shared list query for them.
|
|
811
|
-
if (isActiveDisplaySelfManaged.value) {
|
|
812
|
-
return Promise.resolve(EMPTY_LIST_RESULT);
|
|
813
|
-
}
|
|
814
|
-
return tableRows.list({ ...queryRequest.value, ...archiveQuery.value });
|
|
815
|
-
},
|
|
816
|
-
{ watch: [queryRequest, archiveQuery, isActiveDisplaySelfManaged] },
|
|
817
|
-
);
|
|
818
|
-
|
|
819
812
|
// Nothing listed yet (a client navigation paints before the first page
|
|
820
813
|
// arrives): the table draws its skeleton, never the empty state.
|
|
821
814
|
const isFirstPageLoading = computed(() => data.value === null && !error.value);
|
|
@@ -949,19 +942,13 @@ const tabCountsQuery = computed(() => [
|
|
|
949
942
|
// No default: `null` until the counts arrive, so the tabs draw placeholders
|
|
950
943
|
// instead of a count of nothing.
|
|
951
944
|
const { data: tabCountsData, refresh: refreshTabCounts } =
|
|
952
|
-
await
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
query: { ...query, ...archiveQuery.value },
|
|
960
|
-
})),
|
|
961
|
-
);
|
|
962
|
-
},
|
|
963
|
-
{ watch: [tabCountsQuery, archiveQuery] },
|
|
964
|
-
);
|
|
945
|
+
await useTableCountsData<T>({
|
|
946
|
+
componentId,
|
|
947
|
+
pageId,
|
|
948
|
+
rows: tableRows,
|
|
949
|
+
queries: tabCountsQuery,
|
|
950
|
+
archiveQuery,
|
|
951
|
+
});
|
|
965
952
|
|
|
966
953
|
const viewItems = computed<TableViewItem[]>(() =>
|
|
967
954
|
tableViewItems.value.map((view) => ({
|
|
@@ -1131,6 +1118,11 @@ const refreshAll = async () => {
|
|
|
1131
1118
|
]);
|
|
1132
1119
|
};
|
|
1133
1120
|
|
|
1121
|
+
// The rows can move without the table seeing it (a batch, a job, another
|
|
1122
|
+
// block's action): a topic of the backend or a page refresh reads them again,
|
|
1123
|
+
// keeping the current rows on screen until the answer lands.
|
|
1124
|
+
useRefreshTriggers(props.realtimeTopic, () => void refreshAll());
|
|
1125
|
+
|
|
1134
1126
|
// "Preview as role": a link tab to a page the previewed role could not open
|
|
1135
1127
|
// is drawn locked, one to a page it opens partially is drawn partially
|
|
1136
1128
|
// locked. Never outside a preview (`entryState` is null then).
|
|
@@ -1847,7 +1839,6 @@ defineShortcuts(
|
|
|
1847
1839
|
|
|
1848
1840
|
const tableStatePreferences = {
|
|
1849
1841
|
sorting,
|
|
1850
|
-
pagination: paginationState,
|
|
1851
1842
|
columnFilters: columnFilters,
|
|
1852
1843
|
columnVisibility,
|
|
1853
1844
|
filtersOpen: filtersRowOpen,
|
|
@@ -1980,6 +1971,7 @@ onMounted(() => {
|
|
|
1980
1971
|
:quick-filters="resolvedQuickFilters"
|
|
1981
1972
|
:footer="resolvedFooter"
|
|
1982
1973
|
:empty-states="props.emptyStates"
|
|
1974
|
+
:hidden-filters-apply="queryParamHiddenFilters.length > 0"
|
|
1983
1975
|
:default-page-size="props.pageSize"
|
|
1984
1976
|
:archive-toggle="canToggleArchived"
|
|
1985
1977
|
:displays="availableDisplays"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from "vue";
|
|
3
3
|
import type { TopListItem } from "../../../composables/chart/types";
|
|
4
|
+
import { useComposedText } from "../../../../../dms-core/app/composables/translation/useComposedText";
|
|
4
5
|
// Imported rather than resolved from the registry: a registered component is
|
|
5
6
|
// a lazy chunk of its own, fetched only when the data first shows it, so the
|
|
6
7
|
// trend would pop in a beat after the row.
|
|
@@ -31,11 +32,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
31
32
|
|
|
32
33
|
const PERCENT_UNIT = "%";
|
|
33
34
|
|
|
34
|
-
const {
|
|
35
|
+
const { processText } = useComposedText();
|
|
35
36
|
|
|
36
|
-
const titleDisplay = computed(() =>
|
|
37
|
+
const titleDisplay = computed(() => processText(props.item.title));
|
|
37
38
|
const descriptionDisplay = computed(() =>
|
|
38
|
-
props.item.description ?
|
|
39
|
+
props.item.description ? processText(props.item.description) : "",
|
|
39
40
|
);
|
|
40
41
|
|
|
41
42
|
const hasSparklineData = computed(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { BlockText } from "#dms-core/app/types/composed-text";
|
|
2
|
+
|
|
1
3
|
/** Serializable X-axis settings, matching the DMS server contract. */
|
|
2
4
|
export interface ChartXAxis {
|
|
3
5
|
/** Requested intervals; ignored by ApexCharts on datetime axes. */
|
|
@@ -100,8 +102,9 @@ export interface KpiCardResponse {
|
|
|
100
102
|
|
|
101
103
|
export interface TopListItem {
|
|
102
104
|
id: string | number;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
/** A literal, a `$`-prefixed i18n key, or a composed text. */
|
|
106
|
+
title: BlockText;
|
|
107
|
+
description?: BlockText;
|
|
105
108
|
value: number;
|
|
106
109
|
delta?: number | null;
|
|
107
110
|
sparkline?: number[];
|