@antelopejs/dms 0.7.10 → 0.7.12
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 +29 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js +7 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +10 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +7 -29
- package/frontend-vue/layers/dms-core/app/composables/page/usePageRecordLabel.ts +53 -0
- package/frontend-vue/layers/dms-core/app/composables/user/useRegionalFormat.ts +73 -20
- package/frontend-vue/layers/dms-core/app/utils/formatter.ts +37 -1
- package/frontend-vue/layers/dms-core/app/utils/regional.ts +20 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +34 -10
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +69 -17
- package/frontend-vue/layers/dms-ui/app/build/composables/chart/apexTheme.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/form/enclosingForm.ts +38 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/global/searchShortcuts.ts +7 -16
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveNotes.ts +54 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/page/blockAnchors.ts +50 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +78 -35
- package/frontend-vue/layers/dms-ui/app/build/utils/numberAdjustment.ts +60 -0
- package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +11 -0
- package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +17 -9
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +58 -7
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +5 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/EmptyStateBlock.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +7 -20
- package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +3 -0
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -1
- package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +51 -13
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +38 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +58 -3
- package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +19 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +9 -1
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +3 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.types.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/types/validation.ts +10 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useEnclosingForm.ts +61 -0
- package/frontend-vue/layers/dms-ui/app/composables/global/searchShortcuts.ts +24 -0
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +3 -2
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +103 -0
- package/frontend-vue/layers/dms-ui/app/composables/tab/types/composable.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/composables/tab/types/item.ts +8 -1
- package/frontend-vue/layers/dms-ui/app/composables/tab/useTab.ts +35 -23
- package/frontend-vue/layers/dms-ui/app/types/input-number.ts +18 -0
- package/frontend-vue/layers/dms-ui/app/types/live.ts +6 -0
- package/frontend-vue/layers/dms-ui/app/utils/scrollToBlock.ts +23 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +9 -2
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +9 -2
- package/frontend-vue/tests/activity-feed-fold.test.ts +119 -0
- package/frontend-vue/tests/chart-legend.test.ts +133 -0
- package/frontend-vue/tests/code-block.test.ts +28 -0
- package/frontend-vue/tests/field-suffix-count.test.ts +1 -0
- package/frontend-vue/tests/form-enclosing-form.test.ts +141 -0
- package/frontend-vue/tests/header-actions.test.ts +1 -1
- package/frontend-vue/tests/input-number-adjust.test.ts +176 -0
- package/frontend-vue/tests/live-hold-banner.test.ts +66 -8
- package/frontend-vue/tests/module-routing.test.ts +139 -0
- package/frontend-vue/tests/page-live-refresh.test.ts +169 -0
- package/frontend-vue/tests/page-record-label.test.ts +53 -0
- package/frontend-vue/tests/regional-format.test.ts +69 -2
- package/frontend-vue/tests/scroll-to-block.test.ts +142 -0
- package/frontend-vue/tests/settings-search-shortcuts.test.ts +17 -0
- package/frontend-vue/tests/tab-slot-spacing.test.ts +8 -0
- package/frontend-vue/tests/tab-url-slot.test.ts +150 -0
- package/frontend-vue/tests/table-live-hold.test.ts +90 -0
- package/package.json +2 -2
- package/skills/dms/SKILL.md +28 -4
- package/skills/dms-module/SKILL.md +2 -1
|
@@ -5,11 +5,11 @@ import { usePageLive } from "../../composables/live/usePageLive";
|
|
|
5
5
|
import { formatElapsed } from "../../build/composables/live/formatElapsed";
|
|
6
6
|
import { useLiveToggleShortcut } from "../../build/composables/live/useLiveToggleShortcut";
|
|
7
7
|
import { HIDDEN_TAB_NOTE } from "../../build/composables/live/pageLiveController";
|
|
8
|
+
import type { ConfirmTranslate } from "../../build/composables/confirm/confirmDialogTexts";
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "../../build/composables/
|
|
12
|
-
import type { PageLiveNote, PageLiveNoteTone } from "../../types/live";
|
|
10
|
+
missedRowsText,
|
|
11
|
+
showLiveNote,
|
|
12
|
+
} from "../../build/composables/live/liveNotes";
|
|
13
13
|
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
14
14
|
|
|
15
15
|
interface LiveToggleBlockProps extends DefaultComponentProps {
|
|
@@ -40,11 +40,6 @@ const ALIGN_CLASSES: Record<
|
|
|
40
40
|
center: "justify-center",
|
|
41
41
|
right: "justify-end",
|
|
42
42
|
};
|
|
43
|
-
const TONE_CLASSES: Record<PageLiveNoteTone, string> = {
|
|
44
|
-
neutral: "",
|
|
45
|
-
warning: "text-warning",
|
|
46
|
-
error: "text-error",
|
|
47
|
-
};
|
|
48
43
|
const PILL_BUTTON_CLASS =
|
|
49
44
|
"focus-visible:outline-primary flex items-center gap-1.5 rounded-[5px] px-2 py-1 text-[12px] font-medium transition-colors focus-visible:outline-2";
|
|
50
45
|
const PILL_IDLE_CLASS = "text-muted hover:text-default hover:bg-elevated";
|
|
@@ -90,15 +85,10 @@ const updatedLabel = computed(() => {
|
|
|
90
85
|
: updated;
|
|
91
86
|
});
|
|
92
87
|
|
|
93
|
-
const noteText = (note: PageLiveNote) =>
|
|
94
|
-
resolveConfirmText(note.text, note.params ?? {}, translate);
|
|
95
|
-
|
|
96
88
|
const missedText = computed(() => {
|
|
97
89
|
const count = live.missedCount.value;
|
|
98
90
|
if (count === 0) return "";
|
|
99
|
-
return props.missedLabel
|
|
100
|
-
? resolveConfirmText(props.missedLabel, { count }, translate)
|
|
101
|
-
: t("dms.live.new_rows", { count }, count);
|
|
91
|
+
return missedRowsText(count, props.missedLabel, translate);
|
|
102
92
|
});
|
|
103
93
|
|
|
104
94
|
// "Paused · typing in the Tester · 40 new requests", the detail apart: it
|
|
@@ -107,7 +97,7 @@ const pausedText = computed(() => {
|
|
|
107
97
|
const reason = live.pauseReason.value;
|
|
108
98
|
return [
|
|
109
99
|
t("dms.live.paused"),
|
|
110
|
-
reason ?
|
|
100
|
+
reason ? showLiveNote(reason, translate).text : "",
|
|
111
101
|
missedText.value,
|
|
112
102
|
]
|
|
113
103
|
.filter((part) => part !== "")
|
|
@@ -117,10 +107,7 @@ const pausedText = computed(() => {
|
|
|
117
107
|
const detail = computed(() => {
|
|
118
108
|
const note = live.missedDetail.value;
|
|
119
109
|
if (!note || live.missedCount.value === 0) return null;
|
|
120
|
-
return
|
|
121
|
-
text: noteText(note),
|
|
122
|
-
toneClass: TONE_CLASSES[note.tone ?? "neutral"],
|
|
123
|
-
};
|
|
110
|
+
return showLiveNote(note, translate);
|
|
124
111
|
});
|
|
125
112
|
|
|
126
113
|
// A hidden tab resumes by itself when it shows again: no button for it.
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
ChartAnnotation,
|
|
22
22
|
ChartAutoYRange,
|
|
23
23
|
ChartCurve,
|
|
24
|
+
ChartLegendPosition,
|
|
24
25
|
ChartResponse,
|
|
25
26
|
ChartSeries,
|
|
26
27
|
ChartType,
|
|
@@ -48,6 +49,7 @@ interface Props extends DefaultComponentProps {
|
|
|
48
49
|
showGrid?: boolean;
|
|
49
50
|
showTooltip?: boolean;
|
|
50
51
|
showLegend?: boolean;
|
|
52
|
+
legendPosition?: ChartLegendPosition;
|
|
51
53
|
smooth?: boolean;
|
|
52
54
|
curve?: ChartCurve;
|
|
53
55
|
stacked?: boolean;
|
|
@@ -238,6 +240,7 @@ const apex = useApexChart(() => ({
|
|
|
238
240
|
showGrid: props.showGrid,
|
|
239
241
|
showTooltip: props.showTooltip,
|
|
240
242
|
showLegend: effectiveShowLegend.value,
|
|
243
|
+
legendPosition: props.legendPosition,
|
|
241
244
|
smooth: props.smooth,
|
|
242
245
|
curve: props.curve,
|
|
243
246
|
stacked: props.stacked,
|
|
@@ -34,6 +34,8 @@ interface Props extends DefaultComponentProps {
|
|
|
34
34
|
valuePrecision?: ValuePrecision;
|
|
35
35
|
showDelta?: boolean;
|
|
36
36
|
showLegend?: boolean;
|
|
37
|
+
/** The wrapped chart draws its own legend, naming each series. */
|
|
38
|
+
showSeriesLegend?: boolean;
|
|
37
39
|
primaryLabel?: string;
|
|
38
40
|
comparisonLabel?: string;
|
|
39
41
|
}
|
|
@@ -53,6 +55,7 @@ const DEFAULT_COMPARISON_LABEL_KEY = "dms.chart.legend_comparison";
|
|
|
53
55
|
const props = withDefaults(defineProps<Props>(), {
|
|
54
56
|
showDelta: true,
|
|
55
57
|
showLegend: true,
|
|
58
|
+
showSeriesLegend: false,
|
|
56
59
|
valueFormat: "compact",
|
|
57
60
|
currencyCode: "EUR",
|
|
58
61
|
});
|
|
@@ -126,7 +129,7 @@ const nestedContext = reactive<NestedChartContext>({
|
|
|
126
129
|
cardSeries: [],
|
|
127
130
|
cardComparisonSeries: [],
|
|
128
131
|
isLoading: false,
|
|
129
|
-
hideLegend:
|
|
132
|
+
hideLegend: !props.showSeriesLegend,
|
|
130
133
|
valueFormat: props.valueFormat,
|
|
131
134
|
currencyCode: props.currencyCode,
|
|
132
135
|
valuePrecision: props.valuePrecision,
|
|
@@ -138,6 +141,7 @@ watchEffect(() => {
|
|
|
138
141
|
nestedContext.cardSeries = cardSeries.value;
|
|
139
142
|
nestedContext.cardComparisonSeries = cardComparisonSeries.value;
|
|
140
143
|
nestedContext.isLoading = isLoading.value;
|
|
144
|
+
nestedContext.hideLegend = !props.showSeriesLegend;
|
|
141
145
|
nestedContext.valueFormat = props.valueFormat;
|
|
142
146
|
nestedContext.currencyCode = props.currencyCode;
|
|
143
147
|
nestedContext.valuePrecision = props.valuePrecision;
|
|
@@ -10,7 +10,17 @@ import type { CodeToken, highlightCode } from "../../build/utils/codeHighlight";
|
|
|
10
10
|
|
|
11
11
|
// Read-only code in the editor's look: a head with the title, the language
|
|
12
12
|
// and a copy button, then the code highlighted by the editor's languages.
|
|
13
|
-
//
|
|
13
|
+
// Without a title or a language there is no head: the copy button floats in
|
|
14
|
+
// the code's corner. Until the highlighter loads (and on the server) the
|
|
15
|
+
// code shows as is.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How long lines break: `false` scrolls them sideways, `true` wraps them
|
|
19
|
+
* between words, `"anywhere"` also breaks a word too long for the line (a
|
|
20
|
+
* URL, a token, a minified payload).
|
|
21
|
+
*/
|
|
22
|
+
type CodeSnippetWrap = boolean | "anywhere";
|
|
23
|
+
|
|
14
24
|
interface CodeSnippetProps {
|
|
15
25
|
/** The code, verbatim. */
|
|
16
26
|
code: string;
|
|
@@ -25,8 +35,13 @@ interface CodeSnippetProps {
|
|
|
25
35
|
title?: string;
|
|
26
36
|
/** Lines shown before the code scrolls; all of them when unset. */
|
|
27
37
|
maxLines?: number;
|
|
38
|
+
/**
|
|
39
|
+
* A CSS height the code scrolls past (`"20rem"`), when `maxLines` is not
|
|
40
|
+
* set: for a payload whose lines differ in length once wrapped.
|
|
41
|
+
*/
|
|
42
|
+
maxHeight?: string;
|
|
28
43
|
/** Wraps long lines instead of scrolling sideways. */
|
|
29
|
-
wrap?:
|
|
44
|
+
wrap?: CodeSnippetWrap;
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
const props = withDefaults(defineProps<CodeSnippetProps>(), {
|
|
@@ -34,6 +49,7 @@ const props = withDefaults(defineProps<CodeSnippetProps>(), {
|
|
|
34
49
|
copy: true,
|
|
35
50
|
title: undefined,
|
|
36
51
|
maxLines: undefined,
|
|
52
|
+
maxHeight: undefined,
|
|
37
53
|
wrap: false,
|
|
38
54
|
});
|
|
39
55
|
|
|
@@ -41,24 +57,39 @@ const LINE_HEIGHT_PX = 19;
|
|
|
41
57
|
// The code's top and bottom padding (`py-2`), around its lines.
|
|
42
58
|
const CODE_PADDING_PX = 16;
|
|
43
59
|
|
|
60
|
+
const WRAP_CLASSES: Record<"false" | "true" | "anywhere", string> = {
|
|
61
|
+
false: "whitespace-pre",
|
|
62
|
+
true: "break-words whitespace-pre-wrap",
|
|
63
|
+
anywhere: "whitespace-pre-wrap [overflow-wrap:anywhere]",
|
|
64
|
+
};
|
|
65
|
+
|
|
44
66
|
const highlight = shallowRef<typeof highlightCode | null>(null);
|
|
45
67
|
|
|
46
68
|
const language = computed(() => resolveCodeLanguage(props.language));
|
|
69
|
+
// A language given, even `text`, is named in a head; without a title either,
|
|
70
|
+
// a head would only hold the copy button.
|
|
71
|
+
const isHeadless = computed(() => !props.title && props.language === undefined);
|
|
47
72
|
const hasHead = computed(
|
|
48
|
-
() =>
|
|
73
|
+
() =>
|
|
74
|
+
!isHeadless.value &&
|
|
75
|
+
(!!props.title || props.copy || language.value !== PLAIN_CODE_LANGUAGE),
|
|
76
|
+
);
|
|
77
|
+
const hasFloatingCopy = computed(() => isHeadless.value && props.copy);
|
|
78
|
+
const wrapClass = computed(
|
|
79
|
+
() => WRAP_CLASSES[String(props.wrap) as keyof typeof WRAP_CLASSES],
|
|
49
80
|
);
|
|
50
81
|
const tokens = computed<CodeToken[]>(() =>
|
|
51
82
|
highlight.value
|
|
52
83
|
? highlight.value(props.code, language.value)
|
|
53
84
|
: [{ text: props.code }],
|
|
54
85
|
);
|
|
55
|
-
const sizeStyle = computed(() =>
|
|
56
|
-
props.maxLines
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
86
|
+
const sizeStyle = computed(() => {
|
|
87
|
+
if (props.maxLines)
|
|
88
|
+
return {
|
|
89
|
+
maxHeight: `${props.maxLines * LINE_HEIGHT_PX + CODE_PADDING_PX}px`,
|
|
90
|
+
};
|
|
91
|
+
return props.maxHeight ? { maxHeight: props.maxHeight } : undefined;
|
|
92
|
+
});
|
|
62
93
|
|
|
63
94
|
onMounted(async () => {
|
|
64
95
|
const module = await import("../../build/utils/codeHighlight");
|
|
@@ -68,7 +99,7 @@ onMounted(async () => {
|
|
|
68
99
|
|
|
69
100
|
<template>
|
|
70
101
|
<div
|
|
71
|
-
class="border-default min-w-0 overflow-hidden rounded-[10px] border bg-(--ui-bg)"
|
|
102
|
+
class="border-default relative min-w-0 overflow-hidden rounded-[10px] border bg-(--ui-bg)"
|
|
72
103
|
>
|
|
73
104
|
<div
|
|
74
105
|
v-if="hasHead"
|
|
@@ -85,10 +116,17 @@ onMounted(async () => {
|
|
|
85
116
|
</span>
|
|
86
117
|
<DmsCopyButton v-if="props.copy" :value="props.code" />
|
|
87
118
|
</div>
|
|
119
|
+
<!-- Kept clear of the code: the code's end padding leaves it room. -->
|
|
120
|
+
<DmsCopyButton
|
|
121
|
+
v-if="hasFloatingCopy"
|
|
122
|
+
:value="props.code"
|
|
123
|
+
class="absolute end-1 top-1"
|
|
124
|
+
data-floating-copy
|
|
125
|
+
/>
|
|
88
126
|
<!-- Focusable: a keyboard scrolls the code when it overflows. -->
|
|
89
127
|
<pre
|
|
90
|
-
class="text-highlighted m-0 overflow-auto
|
|
91
|
-
:class="
|
|
128
|
+
class="text-highlighted m-0 overflow-auto py-2 ps-3.5 font-mono text-[12.5px] leading-[19px]"
|
|
129
|
+
:class="[wrapClass, hasFloatingCopy ? 'pe-10' : 'pe-3.5']"
|
|
92
130
|
:style="sizeStyle"
|
|
93
131
|
:data-language="language"
|
|
94
132
|
tabindex="0"
|
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
type FormValidator,
|
|
18
18
|
} from "../../composables/form/types/validation";
|
|
19
19
|
import { FORM_FIELD_LOADING_KEY } from "../../composables/form/types/field-loading";
|
|
20
|
+
import {
|
|
21
|
+
FORM_HANDLE_KEY,
|
|
22
|
+
linkedFieldId,
|
|
23
|
+
} from "../../build/composables/form/enclosingForm";
|
|
20
24
|
import { FORM_CONTENT_LANGUAGE_KEY } from "../../composables/form/types/content-language";
|
|
21
25
|
import { DMS_SECTION_SURFACE_KEY } from "../../build/components/section/context";
|
|
22
26
|
import DmsSaveBar from "../save-bar/SaveBar.vue";
|
|
@@ -30,7 +34,7 @@ import { DMS_CONTAINER_KEY } from "../../build/composables/containers/context";
|
|
|
30
34
|
import { useFormDirty } from "../../composables/unsaved-changes/useFormDirty";
|
|
31
35
|
import { useUnsavedChanges } from "../../composables/unsaved-changes/useUnsavedChanges";
|
|
32
36
|
import { formSaveMode } from "../../build/composables/form/formFooter";
|
|
33
|
-
import { usePageRecordLabel } from "#dms-core/app/
|
|
37
|
+
import { usePageRecordLabel } from "#dms-core/app/composables/page/usePageRecordLabel";
|
|
34
38
|
import { formatRecordLabel } from "../../build/composables/table-view/utils/formTexts";
|
|
35
39
|
import {
|
|
36
40
|
FORM_ENTRY_CONTEXT_KEY,
|
|
@@ -168,6 +172,13 @@ async function focusField(name: string | undefined): Promise<void> {
|
|
|
168
172
|
row?.scrollIntoView({ block: "center" });
|
|
169
173
|
}
|
|
170
174
|
|
|
175
|
+
/** Brings a field into view and focuses it: a link's anchor, a field's request. */
|
|
176
|
+
async function revealField(fieldId: string): Promise<void> {
|
|
177
|
+
await formSettled();
|
|
178
|
+
fieldRow(fieldId)?.scrollIntoView({ block: "center" });
|
|
179
|
+
await focusField(fieldId);
|
|
180
|
+
}
|
|
181
|
+
|
|
171
182
|
/**
|
|
172
183
|
* A refused submit: its field errors go under the fields, the first focused.
|
|
173
184
|
*
|
|
@@ -516,6 +527,13 @@ function onSubmit(event: Parameters<typeof handleSubmit>[0]) {
|
|
|
516
527
|
}
|
|
517
528
|
|
|
518
529
|
provide(FORM_VALIDATOR_KEY, registerValidator);
|
|
530
|
+
provide(FORM_HANDLE_KEY, {
|
|
531
|
+
submit: async () => {
|
|
532
|
+
await form.value?.submit();
|
|
533
|
+
},
|
|
534
|
+
registerValidator,
|
|
535
|
+
revealField,
|
|
536
|
+
});
|
|
519
537
|
provide(FORM_FIELD_LOADING_KEY, setFieldLoading);
|
|
520
538
|
provide(FORM_CONTENT_LANGUAGE_KEY, "*");
|
|
521
539
|
|
|
@@ -580,6 +598,25 @@ if (props.labelKey && !inFormContainer) {
|
|
|
580
598
|
});
|
|
581
599
|
}
|
|
582
600
|
|
|
601
|
+
// A page opened on a field's anchor (`#retentionDays`, the id of its
|
|
602
|
+
// control) brings it into view and focuses it once drawn: a link followed
|
|
603
|
+
// inside the app gets no jump from the browser, and the fields of a record
|
|
604
|
+
// exist only once it loaded. A form in a drawer or a modal follows none: the
|
|
605
|
+
// anchor is the page's.
|
|
606
|
+
if (!inFormContainer && !container) {
|
|
607
|
+
const followFieldAnchor = () => {
|
|
608
|
+
const fieldId = linkedFieldId(window.location.hash, allFieldIds.value);
|
|
609
|
+
if (fieldId) void revealField(fieldId);
|
|
610
|
+
};
|
|
611
|
+
onMounted(() => {
|
|
612
|
+
followFieldAnchor();
|
|
613
|
+
window.addEventListener("hashchange", followFieldAnchor);
|
|
614
|
+
});
|
|
615
|
+
onBeforeUnmount(() =>
|
|
616
|
+
window.removeEventListener("hashchange", followFieldAnchor),
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
583
620
|
const isHorizontal = props.fieldsOrientation !== "vertical";
|
|
584
621
|
const surface: FormSurface = inFormContainer
|
|
585
622
|
? "container"
|
|
@@ -4,9 +4,14 @@ import type {
|
|
|
4
4
|
InputNumberEmits,
|
|
5
5
|
InputNumberSlots,
|
|
6
6
|
} from "@nuxt/ui/components/InputNumber.vue";
|
|
7
|
-
import { computed } from "vue";
|
|
7
|
+
import { computed, nextTick } from "vue";
|
|
8
8
|
import { useForwardPropsEmits } from "reka-ui";
|
|
9
9
|
import { useControlError } from "../../../build/composables/form/useControlError";
|
|
10
|
+
import {
|
|
11
|
+
numberAdjustment,
|
|
12
|
+
parseTypedNumber,
|
|
13
|
+
} from "../../../build/utils/numberAdjustment";
|
|
14
|
+
import type { InputNumberAdjustment } from "../../../types/input-number";
|
|
10
15
|
|
|
11
16
|
defineOptions({ inheritAttrs: false });
|
|
12
17
|
|
|
@@ -15,16 +20,27 @@ interface InputNumberFieldProps extends InputNumberProps {
|
|
|
15
20
|
suffix?: string;
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
interface InputNumberAdjustEmits {
|
|
24
|
+
/**
|
|
25
|
+
* The field kept another value than the one typed, on leaving it or on
|
|
26
|
+
* Enter: clamped to `min` / `max`, or put on its `step`.
|
|
27
|
+
*/
|
|
28
|
+
adjust: [adjustment: InputNumberAdjustment];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type InputNumberFieldEmits = InputNumberEmits & InputNumberAdjustEmits;
|
|
32
|
+
|
|
18
33
|
const props = defineProps<InputNumberFieldProps>();
|
|
19
|
-
const emits = defineEmits<
|
|
34
|
+
const emits = defineEmits<InputNumberFieldEmits>();
|
|
20
35
|
defineSlots<InputNumberSlots>();
|
|
21
36
|
|
|
22
37
|
const forwarded = useForwardPropsEmits(props, emits);
|
|
23
38
|
const inputProps = computed(() => {
|
|
24
|
-
const { suffix: _suffix, ...rest } = forwarded.value;
|
|
39
|
+
const { suffix: _suffix, onAdjust: _onAdjust, ...rest } = forwarded.value;
|
|
25
40
|
return rest;
|
|
26
41
|
});
|
|
27
42
|
const { processI18n } = useTranslation();
|
|
43
|
+
const { locale } = useI18n();
|
|
28
44
|
|
|
29
45
|
// v2 number fields keep the value left-aligned with a compact caret stepper.
|
|
30
46
|
const DEFAULT_ORIENTATION = "vertical";
|
|
@@ -41,6 +57,41 @@ function onIncomingText(event: InputEvent): void {
|
|
|
41
57
|
const refusesNegative = props.min !== undefined && props.min >= 0;
|
|
42
58
|
report(refusesNegative && text.includes("-") ? BELOW_MIN : undefined);
|
|
43
59
|
}
|
|
60
|
+
|
|
61
|
+
// What was typed since the field last settled, and the value it settled on.
|
|
62
|
+
let typedText: string | null = null;
|
|
63
|
+
let settledValue: number | null | undefined;
|
|
64
|
+
|
|
65
|
+
function onTyped(event: Event): void {
|
|
66
|
+
typedText = (event.target as HTMLInputElement).value;
|
|
67
|
+
settledValue = undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function onSettledValue(value: number | null | undefined): void {
|
|
71
|
+
settledValue = value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The number field keeps its own copy of the text typed, which it does not
|
|
75
|
+
// always redraw when it settles on the value it already held (1020 typed
|
|
76
|
+
// over 1,000): its `change` handler copies the kept value's text back.
|
|
77
|
+
function redraw(input: HTMLInputElement): void {
|
|
78
|
+
input.dispatchEvent(new Event("change"));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Once the field applied the typed text: on leaving it, or on Enter. */
|
|
82
|
+
async function settle(event: Event): Promise<void> {
|
|
83
|
+
if (typedText === null) return;
|
|
84
|
+
const text = typedText;
|
|
85
|
+
typedText = null;
|
|
86
|
+
const input = event.target as HTMLInputElement;
|
|
87
|
+
await nextTick();
|
|
88
|
+
const typed = parseTypedNumber(text, props.locale ?? locale.value);
|
|
89
|
+
const kept = settledValue === undefined ? props.modelValue : settledValue;
|
|
90
|
+
const adjustment = numberAdjustment(typed, kept, props);
|
|
91
|
+
if (!adjustment) return;
|
|
92
|
+
emits("adjust", adjustment);
|
|
93
|
+
redraw(input);
|
|
94
|
+
}
|
|
44
95
|
</script>
|
|
45
96
|
|
|
46
97
|
<template>
|
|
@@ -49,9 +100,13 @@ function onIncomingText(event: InputEvent): void {
|
|
|
49
100
|
<div
|
|
50
101
|
:class="props.suffix ? 'flex items-center gap-2' : 'contents'"
|
|
51
102
|
@beforeinput.capture="onIncomingText"
|
|
103
|
+
@input="onTyped"
|
|
104
|
+
@keydown.enter="settle"
|
|
52
105
|
>
|
|
53
106
|
<UInputNumber
|
|
54
107
|
v-bind="{ ...inputProps, ...$attrs }"
|
|
108
|
+
@blur="settle"
|
|
109
|
+
@update:model-value="onSettledValue"
|
|
55
110
|
:class="props.suffix && 'min-w-0 flex-1'"
|
|
56
111
|
:orientation="props.orientation ?? DEFAULT_ORIENTATION"
|
|
57
112
|
:increment="{ color: 'neutral' }"
|
|
@@ -13,7 +13,8 @@ import { notFoundTexts } from "../../build/utils/fetchNotFound";
|
|
|
13
13
|
// `Guard` block (interface-dms `base/guard`): renders its children unchanged
|
|
14
14
|
// unless a route says there is nothing for them, then one EmptyState (the
|
|
15
15
|
// `fallback`, with the answer's `emptyState` laid over it) in their place.
|
|
16
|
-
//
|
|
16
|
+
// Several children stack with the room the page leaves between its blocks,
|
|
17
|
+
// as in a tab: the page spaces the guard, not what it holds.
|
|
17
18
|
interface GuardProps extends DefaultComponentProps {
|
|
18
19
|
fetchUrl?: string;
|
|
19
20
|
fetchUrlMethod?: string;
|
|
@@ -67,7 +68,7 @@ const fallback = computed<GuardFallback | null>(() => {
|
|
|
67
68
|
</script>
|
|
68
69
|
|
|
69
70
|
<template>
|
|
70
|
-
<div class="
|
|
71
|
+
<div class="space-y-6" data-dms-guard>
|
|
71
72
|
<DmsEmptyStateBlock v-if="fallback" v-bind="fallback" />
|
|
72
73
|
<slot v-else />
|
|
73
74
|
</div>
|
|
@@ -3,8 +3,11 @@ import type { TabProps } from "../../composables/tab/types";
|
|
|
3
3
|
import { type TabBadges, TabVariant } from "../../composables/tab/types/props";
|
|
4
4
|
import { useChartFetch } from "../../composables/chart/useChartFetch";
|
|
5
5
|
import { confirmLeave } from "../../build/composables/unsaved-changes/registry";
|
|
6
|
+
import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
|
|
7
|
+
import type { BadgeProps } from "@nuxt/ui";
|
|
6
8
|
|
|
7
9
|
const { processI18n } = useTranslation();
|
|
10
|
+
const { processText } = useComposedText();
|
|
8
11
|
|
|
9
12
|
const props = withDefaults(defineProps<TabProps>(), {
|
|
10
13
|
// A tab set with nothing in it yet is a legitimate state — a page being
|
|
@@ -45,9 +48,24 @@ const countFormat = computed(() => new Intl.NumberFormat(locale.value));
|
|
|
45
48
|
const COUNT_BADGE_CLASS =
|
|
46
49
|
"rounded-[4px] bg-elevated px-[5px] py-px font-mono text-[10.5px] font-semibold tabular-nums text-dimmed ring-0 in-data-[state=active]:bg-primary/10 in-data-[state=active]:text-primary";
|
|
47
50
|
|
|
51
|
+
type TabBadge = TabProps["items"][number]["badge"];
|
|
52
|
+
|
|
53
|
+
// A badge written as text is a BlockText: a `$` key or a ComposedText is
|
|
54
|
+
// written in the reader's language, a number as it is.
|
|
55
|
+
function writtenBadge(
|
|
56
|
+
badge: TabBadge,
|
|
57
|
+
): string | number | BadgeProps | undefined {
|
|
58
|
+
if (badge === undefined || typeof badge === "number") return badge;
|
|
59
|
+
if (typeof badge === "string" || "key" in badge) return processText(badge);
|
|
60
|
+
const { label } = badge;
|
|
61
|
+
return typeof label === "string"
|
|
62
|
+
? { ...badge, label: processText(label) }
|
|
63
|
+
: badge;
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
function badgeOf(item: TabProps["items"][number]) {
|
|
49
67
|
const fetched = fetchedBadges.value?.[item.slot];
|
|
50
|
-
if (fetched === undefined) return item.badge;
|
|
68
|
+
if (fetched === undefined) return writtenBadge(item.badge);
|
|
51
69
|
return typeof fetched === "number"
|
|
52
70
|
? countFormat.value.format(fetched)
|
|
53
71
|
: fetched;
|
|
@@ -827,6 +827,8 @@ const liveHold = useTableLiveHold({
|
|
|
827
827
|
gate: () => liveGate,
|
|
828
828
|
signals: {
|
|
829
829
|
isRowOpen: () => openRowId.value !== undefined,
|
|
830
|
+
isSearching: () => !!effectiveGlobalFilter.value,
|
|
831
|
+
isSorted: () => !isSameSorting(effectiveSorting.value, defaultSortState),
|
|
830
832
|
tableElement: () => tableRoot.value?.$el ?? null,
|
|
831
833
|
},
|
|
832
834
|
});
|
|
@@ -2102,9 +2104,15 @@ onMounted(() => {
|
|
|
2102
2104
|
@duplicate="handleRowDuplicate"
|
|
2103
2105
|
@edit="handleRowEdit"
|
|
2104
2106
|
>
|
|
2105
|
-
<template
|
|
2107
|
+
<template
|
|
2108
|
+
v-if="liveHold.newCount.value > 0 || liveHold.reason.value"
|
|
2109
|
+
#notice
|
|
2110
|
+
>
|
|
2106
2111
|
<LiveHoldBanner
|
|
2107
2112
|
:count="liveHold.newCount.value"
|
|
2113
|
+
:reason="liveHold.reason.value"
|
|
2114
|
+
:missed-label="liveHold.missedLabel"
|
|
2115
|
+
:detail="liveHold.missedDetail.value"
|
|
2108
2116
|
@show="liveHold.showNew"
|
|
2109
2117
|
/>
|
|
2110
2118
|
</template>
|
|
@@ -64,6 +64,9 @@ export interface ChartSeries {
|
|
|
64
64
|
|
|
65
65
|
export type ChartCurve = "smooth" | "straight" | "stepline";
|
|
66
66
|
|
|
67
|
+
/** Mirror of `ChartLegendPosition`: where a chart draws its legend. */
|
|
68
|
+
export type ChartLegendPosition = "top" | "bottom";
|
|
69
|
+
|
|
67
70
|
/** Opt-in expansion of native Y bounds to include annotation lines and bands. */
|
|
68
71
|
export interface ChartAutoYRange {
|
|
69
72
|
/** Outward fraction of each bound's magnitude (or 1 at zero). Defaults to 0.08. */
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ChartAnnotation,
|
|
4
4
|
ChartAutoYRange,
|
|
5
5
|
ChartCurve,
|
|
6
|
+
ChartLegendPosition,
|
|
6
7
|
ChartSeries,
|
|
7
8
|
ChartType,
|
|
8
9
|
ChartXAxis,
|
|
@@ -35,6 +36,7 @@ export interface UseApexChartInput {
|
|
|
35
36
|
showGrid?: boolean;
|
|
36
37
|
showTooltip?: boolean;
|
|
37
38
|
showLegend?: boolean;
|
|
39
|
+
legendPosition?: ChartLegendPosition;
|
|
38
40
|
smooth?: boolean;
|
|
39
41
|
curve?: ChartCurve;
|
|
40
42
|
stacked?: boolean;
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { InjectionKey } from "vue";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A check a `Form` runs before each submit: `false` stops the submit before
|
|
5
|
+
* anything is sent. See `useEnclosingForm().addValidator`.
|
|
6
|
+
*/
|
|
3
7
|
export type FormValidator = () => boolean;
|
|
8
|
+
|
|
9
|
+
/** Adds a {@link FormValidator} to a `Form`; returns its removal. */
|
|
4
10
|
export type RegisterValidator = (validator: FormValidator) => () => void;
|
|
5
11
|
|
|
12
|
+
/**
|
|
13
|
+
* What a `Form` provides to register a validator. Kept for the fields that
|
|
14
|
+
* inject it; a module's field calls `useEnclosingForm().addValidator`.
|
|
15
|
+
*/
|
|
6
16
|
export const FORM_VALIDATOR_KEY: InjectionKey<RegisterValidator> =
|
|
7
17
|
Symbol("form-validator");
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getCurrentScope, inject, onScopeDispose } from "vue";
|
|
2
|
+
import { FORM_HANDLE_KEY } from "../../build/composables/form/enclosingForm";
|
|
3
|
+
import type { FormValidator } from "./types/validation";
|
|
4
|
+
|
|
5
|
+
/** The DMS `Form` a custom field is drawn in (see {@link useEnclosingForm}). */
|
|
6
|
+
export interface EnclosingForm {
|
|
7
|
+
/**
|
|
8
|
+
* Submits the form as its save button does: the form validates its fields,
|
|
9
|
+
* runs the validators, then sends what changed. Resolves once the submit
|
|
10
|
+
* was handled; a refused one shows its errors as usual.
|
|
11
|
+
*/
|
|
12
|
+
submit: () => Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Runs `validator` before each submit: answering `false` stops it, before
|
|
15
|
+
* anything is sent (and before the other validators). Removed when the
|
|
16
|
+
* calling component goes, or by calling the returned function.
|
|
17
|
+
*/
|
|
18
|
+
addValidator: (validator: FormValidator) => () => void;
|
|
19
|
+
/**
|
|
20
|
+
* Brings a field of the form into view, by its id, and focuses its control:
|
|
21
|
+
* its first focusable part when the control is a group.
|
|
22
|
+
*/
|
|
23
|
+
focusField: (fieldId: string) => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The DMS `Form` the calling component is drawn in: what a custom field (an
|
|
28
|
+
* `inputComponent` of the module's own) needs of it without reading the DOM.
|
|
29
|
+
* `null` outside a `Form`.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* const form = useEnclosingForm();
|
|
33
|
+
* const { confirm } = useConfirm();
|
|
34
|
+
* // Ask before a save that deletes data; submit again once confirmed.
|
|
35
|
+
* form?.addValidator(() => {
|
|
36
|
+
* if (!isLowered.value || isConfirmed) return true;
|
|
37
|
+
* void confirm({ title: "Delete older logs?" }).then((accepted) => {
|
|
38
|
+
* isConfirmed = accepted;
|
|
39
|
+
* if (accepted) void form.submit();
|
|
40
|
+
* });
|
|
41
|
+
* return false;
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* A page opened on the anchor of a field (`/settings#retentionDays`, the
|
|
46
|
+
* field's id) scrolls to it and focuses it once the form is drawn, without
|
|
47
|
+
* any of this: `focusField` is for a link or a button inside the page.
|
|
48
|
+
*/
|
|
49
|
+
export function useEnclosingForm(): EnclosingForm | null {
|
|
50
|
+
const handle = inject(FORM_HANDLE_KEY, null);
|
|
51
|
+
if (!handle) return null;
|
|
52
|
+
return {
|
|
53
|
+
submit: handle.submit,
|
|
54
|
+
addValidator: (validator) => {
|
|
55
|
+
const remove = handle.registerValidator(validator);
|
|
56
|
+
if (getCurrentScope()) onScopeDispose(remove);
|
|
57
|
+
return remove;
|
|
58
|
+
},
|
|
59
|
+
focusField: handle.revealField,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focuses a page's own search (⌘ / on macOS, Ctrl / elsewhere), so it never
|
|
3
|
+
* competes with "/" for the navigation search. `meta` is read as Ctrl off
|
|
4
|
+
* macOS by `defineShortcuts`. The shifted variant is the same physical keys
|
|
5
|
+
* on layouts where "/" needs Shift (AZERTY: Shift + :), which
|
|
6
|
+
* `defineShortcuts` would otherwise reject as an extra modifier.
|
|
7
|
+
*/
|
|
8
|
+
export const PAGE_SEARCH_SHORTCUTS = ["meta_/", "meta_shift_/"] as const;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `defineShortcuts` map focusing a page's own search on ⌘ / or Ctrl /: the
|
|
12
|
+
* keys a custom page binds its search to, "/" being the navigation
|
|
13
|
+
* search's.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* defineShortcuts(buildPageSearchShortcuts(() => input.value?.focus()));
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function buildPageSearchShortcuts(
|
|
21
|
+
focus: () => void,
|
|
22
|
+
): Record<string, () => void> {
|
|
23
|
+
return Object.fromEntries(PAGE_SEARCH_SHORTCUTS.map((key) => [key, focus]));
|
|
24
|
+
}
|
|
@@ -75,8 +75,9 @@ export interface PageLiveState {
|
|
|
75
75
|
* const { isPaused, missedCount, resume, hold, release } = usePageLive();
|
|
76
76
|
* ```
|
|
77
77
|
*
|
|
78
|
-
* A component that refetches on its own realtime topic need not
|
|
79
|
-
* built-in blocks with a
|
|
78
|
+
* A component that refetches on its own realtime topic need not read it:
|
|
79
|
+
* `usePageLiveRefresh` gates its refresh as the built-in blocks with a
|
|
80
|
+
* `realtimeTopic` are.
|
|
80
81
|
*/
|
|
81
82
|
export function usePageLive(): PageLiveState {
|
|
82
83
|
const isPaused = computed(() => currentPageLive()?.isPaused.value ?? false);
|