@antelopejs/dms 0.7.11 → 0.7.13
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 +33 -0
- package/dist/implementations/dms/page.js +14 -1
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/test/unit/implementations/dms/module-address-page.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms/module-address-page.test.js +107 -0
- package/dist/test/unit/implementations/dms/module-address-page.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js +30 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
- 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 +23 -2
- package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +34 -39
- package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +6 -3
- package/frontend-vue/layers/dms-layout/app/utils/moduleLanding.ts +56 -0
- 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 +7 -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/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/components/top-list/TopListCard.vue +2 -0
- 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/block-fetch-options-forwarding.test.ts +5 -0
- package/frontend-vue/tests/breadcrumb-target.test.ts +10 -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 +158 -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 +171 -0
- package/frontend-vue/tests/settings-search-shortcuts.test.ts +17 -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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, resolveComponent } from "vue";
|
|
2
|
+
import { computed, ref, resolveComponent } from "vue";
|
|
3
3
|
import DmsActivityItem from "../activity/ActivityItem.vue";
|
|
4
4
|
import DmsBlockActions, {
|
|
5
5
|
type BlockAction,
|
|
@@ -48,6 +48,16 @@ interface ActivityFeedProps extends Partial<DefaultComponentProps> {
|
|
|
48
48
|
groupByDay?: boolean;
|
|
49
49
|
/** Shows at most this many entries. */
|
|
50
50
|
maxItems?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Shows the first this many entries, and a "Show N more" button under them
|
|
53
|
+
* that unfolds the rest (up to `maxItems`), and folds them back.
|
|
54
|
+
*/
|
|
55
|
+
visibleItems?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Entries wrap their title, details and detail line rather than cutting
|
|
58
|
+
* each to one line with an ellipsis.
|
|
59
|
+
*/
|
|
60
|
+
wrap?: boolean;
|
|
51
61
|
/**
|
|
52
62
|
* Fills its grid cell instead of growing with its entries, which scroll
|
|
53
63
|
* under the fixed head: the cards beside it set the row's height.
|
|
@@ -80,6 +90,8 @@ const props = withDefaults(defineProps<ActivityFeedProps>(), {
|
|
|
80
90
|
realtimeTopic: undefined,
|
|
81
91
|
groupByDay: true,
|
|
82
92
|
maxItems: undefined,
|
|
93
|
+
visibleItems: undefined,
|
|
94
|
+
wrap: false,
|
|
83
95
|
fillHeight: false,
|
|
84
96
|
skeletonCount: undefined,
|
|
85
97
|
actions: () => [],
|
|
@@ -119,13 +131,36 @@ const entries = computed<ActivityFeedItem[]>(() =>
|
|
|
119
131
|
props.maxItems ? list.value.slice(0, props.maxItems) : list.value,
|
|
120
132
|
);
|
|
121
133
|
|
|
134
|
+
// Folded, the feed shows its first `visibleItems` entries; the button under
|
|
135
|
+
// them unfolds the rest and folds them back.
|
|
136
|
+
const isUnfolded = ref(false);
|
|
137
|
+
const foldedCount = computed(() =>
|
|
138
|
+
props.visibleItems
|
|
139
|
+
? Math.max(0, entries.value.length - props.visibleItems)
|
|
140
|
+
: 0,
|
|
141
|
+
);
|
|
142
|
+
const shownEntries = computed<ActivityFeedItem[]>(() =>
|
|
143
|
+
foldedCount.value > 0 && !isUnfolded.value
|
|
144
|
+
? entries.value.slice(0, props.visibleItems)
|
|
145
|
+
: entries.value,
|
|
146
|
+
);
|
|
147
|
+
const foldLabel = computed(() =>
|
|
148
|
+
isUnfolded.value
|
|
149
|
+
? t("dms.activity_feed.show_fewer")
|
|
150
|
+
: t(
|
|
151
|
+
"dms.activity_feed.show_more",
|
|
152
|
+
{ count: foldedCount.value },
|
|
153
|
+
foldedCount.value,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
|
|
122
157
|
const days = computed<ActivityFeedDay[]>(() =>
|
|
123
158
|
props.groupByDay
|
|
124
|
-
? groupActivityByDay(
|
|
159
|
+
? groupActivityByDay(shownEntries.value, locale.value, {
|
|
125
160
|
today: t("dms.activity_feed.today"),
|
|
126
161
|
yesterday: t("dms.activity_feed.yesterday"),
|
|
127
162
|
})
|
|
128
|
-
: [{ key: "all", items:
|
|
163
|
+
: [{ key: "all", items: shownEntries.value }],
|
|
129
164
|
);
|
|
130
165
|
|
|
131
166
|
const trailingOf = (item: ActivityFeedItem): string | undefined => {
|
|
@@ -158,11 +193,12 @@ const metaOf = (item: ActivityFeedItem): string | undefined =>
|
|
|
158
193
|
const isFirstLoad = isPending;
|
|
159
194
|
|
|
160
195
|
// The placeholder takes the feed's loaded shape: as many rows as it will show
|
|
161
|
-
// (`skeletonCount`, else `maxItems`; never more than
|
|
162
|
-
// separators when the entries are grouped by day.
|
|
196
|
+
// (`skeletonCount`, else `visibleItems` or `maxItems`; never more than
|
|
197
|
+
// those), under day separators when the entries are grouped by day.
|
|
163
198
|
const skeletonDays = computed<number[][]>(() => {
|
|
164
|
-
const
|
|
165
|
-
const
|
|
199
|
+
const shown = props.visibleItems ?? props.maxItems;
|
|
200
|
+
const wanted = props.skeletonCount ?? shown ?? SKELETON_ROWS;
|
|
201
|
+
const count = shown ? Math.min(wanted, shown) : wanted;
|
|
166
202
|
const perDay = props.groupByDay ? SKELETON_ROWS_PER_DAY : count;
|
|
167
203
|
const days: number[][] = [];
|
|
168
204
|
for (let start = 0; start < count; start += perDay) {
|
|
@@ -328,10 +364,25 @@ const wrapperProps = computed(() =>
|
|
|
328
364
|
:trailing="trailingOf(item)"
|
|
329
365
|
:unread="item.unread"
|
|
330
366
|
:mono="props.mono"
|
|
367
|
+
:wrap="props.wrap"
|
|
331
368
|
:to="item.to"
|
|
332
369
|
:interactive="!!item.to"
|
|
333
370
|
/>
|
|
334
371
|
</section>
|
|
372
|
+
<div
|
|
373
|
+
v-if="foldedCount > 0"
|
|
374
|
+
class="dms-activity-feed__fold border-t border-(--ui-border-muted) px-[18px] pt-2 pb-1"
|
|
375
|
+
>
|
|
376
|
+
<UButton
|
|
377
|
+
:label="foldLabel"
|
|
378
|
+
:aria-expanded="isUnfolded"
|
|
379
|
+
color="neutral"
|
|
380
|
+
variant="link"
|
|
381
|
+
size="xs"
|
|
382
|
+
class="p-0 font-medium"
|
|
383
|
+
@click="isUnfolded = !isUnfolded"
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
335
386
|
</div>
|
|
336
387
|
</div>
|
|
337
388
|
</component>
|
|
@@ -4,6 +4,7 @@ import DmsCodeSnippet from "../code-snippet/CodeSnippet.vue";
|
|
|
4
4
|
import DmsBlockStatus from "../../build/components/blocks/BlockStatus.vue";
|
|
5
5
|
import { useChartFetch } from "../../composables/chart/useChartFetch";
|
|
6
6
|
import { readFetchNotFound } from "../../build/utils/fetchNotFound";
|
|
7
|
+
import { PLAIN_CODE_LANGUAGE } from "../../build/utils/codeLanguages";
|
|
7
8
|
import { useWatch } from "../../../../dms-core/app/composables/watch/useWatch";
|
|
8
9
|
import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
|
|
9
10
|
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
@@ -58,7 +59,10 @@ const { data, isLoading, error, refresh } = useChartFetch<CodeBlockResponse>({
|
|
|
58
59
|
});
|
|
59
60
|
|
|
60
61
|
const code = computed(() => data.value?.code ?? props.code);
|
|
61
|
-
|
|
62
|
+
// The block always heads its code with its language, plain text included.
|
|
63
|
+
const language = computed(
|
|
64
|
+
() => data.value?.language ?? props.language ?? PLAIN_CODE_LANGUAGE,
|
|
65
|
+
);
|
|
62
66
|
const title = computed(() =>
|
|
63
67
|
props.title ? processText(props.title) : undefined,
|
|
64
68
|
);
|
|
@@ -5,6 +5,7 @@ import DmsEmptyState, {
|
|
|
5
5
|
type EmptyStateVariant,
|
|
6
6
|
} from "../empty-state/EmptyState.vue";
|
|
7
7
|
import DmsCodeSnippet from "../code-snippet/CodeSnippet.vue";
|
|
8
|
+
import { PLAIN_CODE_LANGUAGE } from "../../build/utils/codeLanguages";
|
|
8
9
|
import DmsBlockActions, {
|
|
9
10
|
type BlockAction,
|
|
10
11
|
} from "../../build/components/blocks/BlockActions.vue";
|
|
@@ -75,7 +76,7 @@ const description = computed(() =>
|
|
|
75
76
|
<template v-if="props.code" #content>
|
|
76
77
|
<DmsCodeSnippet
|
|
77
78
|
:code="props.code.content"
|
|
78
|
-
:language="props.code.language"
|
|
79
|
+
:language="props.code.language ?? PLAIN_CODE_LANGUAGE"
|
|
79
80
|
:copy="props.code.copy ?? true"
|
|
80
81
|
/>
|
|
81
82
|
</template>
|
|
@@ -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,
|
|
@@ -29,11 +29,14 @@ interface Props extends DefaultComponentProps {
|
|
|
29
29
|
fetchUrl?: string;
|
|
30
30
|
fetchUrlMethod?: string;
|
|
31
31
|
periodScope?: string;
|
|
32
|
+
realtimeTopic?: string | string[];
|
|
32
33
|
valueFormat?: ValueFormat;
|
|
33
34
|
currencyCode?: string;
|
|
34
35
|
valuePrecision?: ValuePrecision;
|
|
35
36
|
showDelta?: boolean;
|
|
36
37
|
showLegend?: boolean;
|
|
38
|
+
/** The wrapped chart draws its own legend, naming each series. */
|
|
39
|
+
showSeriesLegend?: boolean;
|
|
37
40
|
primaryLabel?: string;
|
|
38
41
|
comparisonLabel?: string;
|
|
39
42
|
}
|
|
@@ -53,6 +56,7 @@ const DEFAULT_COMPARISON_LABEL_KEY = "dms.chart.legend_comparison";
|
|
|
53
56
|
const props = withDefaults(defineProps<Props>(), {
|
|
54
57
|
showDelta: true,
|
|
55
58
|
showLegend: true,
|
|
59
|
+
showSeriesLegend: false,
|
|
56
60
|
valueFormat: "compact",
|
|
57
61
|
currencyCode: "EUR",
|
|
58
62
|
});
|
|
@@ -71,6 +75,7 @@ const { data, isLoading } = useChartFetch<ChartCardResponse>({
|
|
|
71
75
|
fetchUrlMethod: props.fetchUrlMethod,
|
|
72
76
|
routeParams: () => props.routeParams,
|
|
73
77
|
periodScope: props.periodScope,
|
|
78
|
+
realtimeTopic: props.realtimeTopic,
|
|
74
79
|
watchSource: () => watchKey.value,
|
|
75
80
|
});
|
|
76
81
|
|
|
@@ -126,7 +131,7 @@ const nestedContext = reactive<NestedChartContext>({
|
|
|
126
131
|
cardSeries: [],
|
|
127
132
|
cardComparisonSeries: [],
|
|
128
133
|
isLoading: false,
|
|
129
|
-
hideLegend:
|
|
134
|
+
hideLegend: !props.showSeriesLegend,
|
|
130
135
|
valueFormat: props.valueFormat,
|
|
131
136
|
currencyCode: props.currencyCode,
|
|
132
137
|
valuePrecision: props.valuePrecision,
|
|
@@ -138,6 +143,7 @@ watchEffect(() => {
|
|
|
138
143
|
nestedContext.cardSeries = cardSeries.value;
|
|
139
144
|
nestedContext.cardComparisonSeries = cardComparisonSeries.value;
|
|
140
145
|
nestedContext.isLoading = isLoading.value;
|
|
146
|
+
nestedContext.hideLegend = !props.showSeriesLegend;
|
|
141
147
|
nestedContext.valueFormat = props.valueFormat;
|
|
142
148
|
nestedContext.currencyCode = props.currencyCode;
|
|
143
149
|
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' }"
|
|
@@ -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>
|
|
@@ -30,6 +30,7 @@ interface Props extends DefaultComponentProps {
|
|
|
30
30
|
fetchUrl?: string;
|
|
31
31
|
fetchUrlMethod?: string;
|
|
32
32
|
periodScope?: string;
|
|
33
|
+
realtimeTopic?: string | string[];
|
|
33
34
|
valueFormat?: ValueFormat;
|
|
34
35
|
currencyCode?: string;
|
|
35
36
|
valuePrecision?: ValuePrecision;
|
|
@@ -120,6 +121,7 @@ const { data, isLoading } = useChartFetch<TopListCardResponse>({
|
|
|
120
121
|
fetchUrlMethod: props.fetchUrlMethod,
|
|
121
122
|
routeParams: () => props.routeParams,
|
|
122
123
|
periodScope: props.periodScope,
|
|
124
|
+
realtimeTopic: props.realtimeTopic,
|
|
123
125
|
staticData: () => staticData.value,
|
|
124
126
|
watchSource: () => watchKey.value,
|
|
125
127
|
});
|
|
@@ -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. */
|