@antelopejs/dms 0.7.21 → 0.8.0
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 +39 -0
- package/dist/implementations/dms-auth/account-emails.d.ts +4 -0
- package/dist/implementations/dms-auth/account-emails.js +62 -0
- package/dist/implementations/dms-auth/account-emails.js.map +1 -0
- package/dist/implementations/dms-auth/index.d.ts +1 -3
- package/dist/implementations/dms-auth/index.js +15 -61
- package/dist/implementations/dms-auth/index.js.map +1 -1
- package/dist/pages/settings/users/email-change.js +3 -4
- package/dist/pages/settings/users/email-change.js.map +1 -1
- package/dist/pages/settings/users/invites.js +1 -1
- package/dist/pages/settings/users/invites.js.map +1 -1
- package/dist/pages/settings/users/members.js +1 -1
- package/dist/pages/settings/users/members.js.map +1 -1
- package/dist/pages/settings/users/notifications.js +1 -0
- package/dist/pages/settings/users/notifications.js.map +1 -1
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js +34 -0
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.js +49 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js +17 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +2 -2
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js +29 -0
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/resource-form.test.js +5 -0
- package/dist/test/unit/interfaces/dms-base/resource-form.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js +12 -2
- package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +29 -4
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +14 -11
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-views.test.js +8 -5
- package/dist/test/unit/interfaces/dms-base/table-view-views.test.js.map +1 -1
- package/dist/test/unit/utils/email-subjects.test.d.ts +1 -0
- package/dist/test/unit/utils/email-subjects.test.js +27 -0
- package/dist/test/unit/utils/email-subjects.test.js.map +1 -0
- package/dist/utils/admin-invite-email.js +2 -4
- package/dist/utils/admin-invite-email.js.map +1 -1
- package/dist/utils/email-change-mail.d.ts +1 -1
- package/dist/utils/email-change-mail.js +11 -6
- package/dist/utils/email-change-mail.js.map +1 -1
- package/dist/utils/email-subjects.d.ts +3 -0
- package/dist/utils/email-subjects.js +32 -0
- package/dist/utils/email-subjects.js.map +1 -0
- package/dist/utils/export-jobs.js +15 -10
- package/dist/utils/export-jobs.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +29 -11
- package/frontend-vue/layers/dms-core/shared/types/app-config.ts +17 -0
- package/frontend-vue/layers/dms-layout/app/app.config.ts +5 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +18 -2
- package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +16 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/useHeaderActionControls.ts +14 -0
- package/frontend-vue/layers/dms-layout/app/components/layout/HStack.vue +16 -0
- package/frontend-vue/layers/dms-layout/app/components/layout/VStack.vue +2 -0
- package/frontend-vue/layers/dms-layout/app/composables/useFooterLinks.ts +11 -1
- package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/emails/EmailChangeNotice.vue +13 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailChangeVerification.vue +18 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailExportReady.vue +23 -12
- package/frontend-vue/layers/dms-layout/app/emails/EmailResetPassword.vue +18 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailTwoFactor.vue +23 -4
- package/frontend-vue/layers/dms-layout/app/emails/EmailUserValidation.vue +18 -6
- package/frontend-vue/layers/dms-layout/app/emails/components/EmailOTP.vue +8 -2
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +40 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +40 -0
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +11 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +27 -36
- package/frontend-vue/layers/dms-ui/app/build/composables/chart/apexTheme.ts +59 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +27 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +10 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +0 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryOrder.ts +23 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +19 -15
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +26 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/page/pageUrlValues.ts +41 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +2 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +3 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +7 -12
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +3 -3
- package/frontend-vue/layers/dms-ui/app/build/utils/link.ts +7 -3
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +32 -9
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +8 -1
- package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +9 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +5 -11
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +32 -1
- package/frontend-vue/layers/dms-ui/app/components/check-list/CheckList.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +12 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +7 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +7 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +4 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/grid/columns.ts +15 -3
- package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +9 -2
- package/frontend-vue/layers/dms-ui/app/components/save-bar/InstantSaveBadge.vue +13 -5
- package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +53 -31
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexPlotOptions.ts +15 -4
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexYRange.ts +123 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +49 -49
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartTheme.ts +20 -1
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/composables/form/types/props.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +8 -6
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +3 -5
- package/frontend-vue/layers/dms-ui/app/utils/bannerToneIcons.ts +13 -0
- package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +26 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -0
- package/frontend-vue/tests/activity-feed-fill-height.test.ts +7 -0
- package/frontend-vue/tests/banner-tone-icon.test.ts +55 -0
- package/frontend-vue/tests/builtin-emails.test.ts +141 -0
- package/frontend-vue/tests/cascader.test.ts +27 -0
- package/frontend-vue/tests/chart-auto-y-range.test.ts +25 -9
- package/frontend-vue/tests/chart-colors.test.ts +8 -0
- package/frontend-vue/tests/chart-resize-after-animation.test.ts +100 -0
- package/frontend-vue/tests/chart-type-defaults.test.ts +128 -0
- package/frontend-vue/tests/check-list-marker.test.ts +58 -0
- package/frontend-vue/tests/confirm-from.test.ts +1 -1
- package/frontend-vue/tests/confirm-secondary-action.test.ts +2 -2
- package/frontend-vue/tests/context-links.test.ts +2 -2
- package/frontend-vue/tests/custom-action-confirm-from.test.ts +2 -2
- package/frontend-vue/tests/download-action-target.test.ts +2 -2
- package/frontend-vue/tests/file-drop-prompt.test.ts +72 -0
- package/frontend-vue/tests/footer-config.test.ts +135 -0
- package/frontend-vue/tests/form-actions.test.ts +7 -0
- package/frontend-vue/tests/form-entry-order.test.ts +45 -0
- package/frontend-vue/tests/form-footer-kind.test.ts +11 -6
- package/frontend-vue/tests/form-load-error.test.ts +38 -1
- package/frontend-vue/tests/grid-responsive-columns.test.ts +9 -0
- package/frontend-vue/tests/header-actions.test.ts +55 -9
- package/frontend-vue/tests/instant-save-badge.test.ts +11 -4
- package/frontend-vue/tests/list-row-truncate.test.ts +46 -0
- package/frontend-vue/tests/roles-name-errors.test.ts +2 -3
- package/frontend-vue/tests/save-bar.test.ts +15 -3
- package/frontend-vue/tests/save-status-error.test.ts +2 -2
- package/frontend-vue/tests/table-row-action-errors.test.ts +5 -5
- package/frontend-vue/tests/table-row-hover.test.ts +2 -2
- package/frontend-vue/tests/table-view-sources.test.ts +1 -1
- package/frontend-vue/tests/template-tokens.test.ts +235 -0
- package/frontend-vue/tests/url-interpolation.test.ts +6 -6
- package/package.json +2 -2
- package/skills/dms/SKILL.md +2 -1
- package/skills/dms-pages/REFERENCE.md +2 -2
- package/skills/dms-project/SKILL.md +0 -1
|
@@ -8,14 +8,16 @@ import DmsBlockActions, {
|
|
|
8
8
|
import { useActionTargets } from "../../build/composables/actions/useActionTargets";
|
|
9
9
|
import { useChartFetch } from "../../composables/chart/useChartFetch";
|
|
10
10
|
import { refreshPageBlocks } from "../../utils/blockRefresh";
|
|
11
|
+
import {
|
|
12
|
+
BANNER_TONE_ICONS,
|
|
13
|
+
type BannerTone,
|
|
14
|
+
} from "../../utils/bannerToneIcons";
|
|
11
15
|
import type { CustomButton } from "../../composables/table-view/types/custom-button";
|
|
12
16
|
import { useWatch } from "../../../../dms-core/app/composables/watch/useWatch";
|
|
13
17
|
import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
|
|
14
18
|
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
15
19
|
import type { BlockText } from "../../../../dms-core/app/types/composed-text";
|
|
16
20
|
|
|
17
|
-
type BannerTone = "info" | "success" | "warning" | "error" | "primary";
|
|
18
|
-
|
|
19
21
|
/** What a banner shows (interface-dms `BannerContent`). */
|
|
20
22
|
interface BannerContent {
|
|
21
23
|
/** A string (`$` for an i18n key) or a composed text. */
|
|
@@ -83,14 +85,6 @@ const DISMISSED_MAX_KEYS = 40;
|
|
|
83
85
|
const FALLBACK_COMPONENT_ID = "banner";
|
|
84
86
|
const FALLBACK_PAGE_ID = "page";
|
|
85
87
|
|
|
86
|
-
const TONE_ICONS: Record<BannerTone, string> = {
|
|
87
|
-
info: "i-ph-info",
|
|
88
|
-
success: "i-ph-check-circle",
|
|
89
|
-
warning: "i-ph-warning",
|
|
90
|
-
error: "i-ph-warning-circle",
|
|
91
|
-
primary: "i-ph-sparkle",
|
|
92
|
-
};
|
|
93
|
-
|
|
94
88
|
const { state: watchState } = useWatch(
|
|
95
89
|
props.watchActions || [],
|
|
96
90
|
props.componentId,
|
|
@@ -183,7 +177,7 @@ function press(action: BlockButtonAction): void {
|
|
|
183
177
|
:description="
|
|
184
178
|
banner.description ? processText(banner.description) : undefined
|
|
185
179
|
"
|
|
186
|
-
:icon="banner.icon ??
|
|
180
|
+
:icon="banner.icon ?? BANNER_TONE_ICONS[banner.tone]"
|
|
187
181
|
:tone="banner.tone"
|
|
188
182
|
:size="banner.size"
|
|
189
183
|
:dismissible="banner.dismissible"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageLink } from "../../build/composables/page/pageUrlValues";
|
|
2
3
|
import { gridColumnsTemplate } from "../grid/columns";
|
|
3
4
|
import {
|
|
4
5
|
useCategoryNavCards,
|
|
@@ -217,7 +218,7 @@ const { t } = useI18n();
|
|
|
217
218
|
>
|
|
218
219
|
<DmsNavCard
|
|
219
220
|
class="h-full"
|
|
220
|
-
:to="card.to"
|
|
221
|
+
:to="pageLink(card.to)"
|
|
221
222
|
:icon="card.icon"
|
|
222
223
|
:icon-tone="card.iconTone"
|
|
223
224
|
:title="card.title"
|
|
@@ -24,7 +24,14 @@ interface Emits {
|
|
|
24
24
|
(event: "drawn"): void;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** The ApexCharts instance the wrapper exposes as `chart`. */
|
|
28
|
+
interface ApexCore {
|
|
29
|
+
/** Apex's own (debounced) window resize handler: redraws at the container width. */
|
|
30
|
+
windowResizeHandler: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
interface ApexChartInstance {
|
|
34
|
+
chart: ApexCore | null;
|
|
28
35
|
updateOptions: (
|
|
29
36
|
options: Record<string, unknown>,
|
|
30
37
|
redrawPaths?: boolean,
|
|
@@ -39,6 +46,7 @@ const emit = defineEmits<Emits>();
|
|
|
39
46
|
const REDRAW_PATHS = false;
|
|
40
47
|
const ANIMATE = true;
|
|
41
48
|
const UPDATE_SYNCED_CHARTS = false;
|
|
49
|
+
const WIDTH_TOLERANCE_PX = 1;
|
|
42
50
|
|
|
43
51
|
const { locale } = useI18n();
|
|
44
52
|
|
|
@@ -57,6 +65,7 @@ onMounted(() => {
|
|
|
57
65
|
void ensureApexLocale(locale.value).catch(() => undefined);
|
|
58
66
|
});
|
|
59
67
|
|
|
68
|
+
const hostRef = ref<HTMLElement | null>(null);
|
|
60
69
|
const chartRef = ref<ApexChartInstance | null>(null);
|
|
61
70
|
|
|
62
71
|
/**
|
|
@@ -123,10 +132,31 @@ watch(
|
|
|
123
132
|
function onDrawn() {
|
|
124
133
|
emit("drawn");
|
|
125
134
|
}
|
|
135
|
+
|
|
136
|
+
function isDrawnAtStaleWidth(host: HTMLElement): boolean {
|
|
137
|
+
const svg = host.querySelector("svg.apexcharts-svg");
|
|
138
|
+
if (!svg) return false;
|
|
139
|
+
const drawnWidth = svg.getBoundingClientRect().width;
|
|
140
|
+
const hostWidth = host.getBoundingClientRect().width;
|
|
141
|
+
return Math.abs(drawnWidth - hostWidth) > WIDTH_TOLERANCE_PX;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Apex drops its container's resizes while an animation runs, so a sidebar
|
|
146
|
+
* collapsed or expanded during one leaves the chart at its old width, past
|
|
147
|
+
* its card when the container shrank, until the next window resize. Once the
|
|
148
|
+
* animation is over, a chart drawn at another width than its host redraws.
|
|
149
|
+
*/
|
|
150
|
+
function redrawIfResizedWhileAnimating() {
|
|
151
|
+
const host = hostRef.value;
|
|
152
|
+
const chart = chartRef.value?.chart;
|
|
153
|
+
if (!host || !chart || !isDrawnAtStaleWidth(host)) return;
|
|
154
|
+
chart.windowResizeHandler();
|
|
155
|
+
}
|
|
126
156
|
</script>
|
|
127
157
|
|
|
128
158
|
<template>
|
|
129
|
-
<div class="dms-apex">
|
|
159
|
+
<div ref="hostRef" class="dms-apex">
|
|
130
160
|
<ApexChart
|
|
131
161
|
ref="chartRef"
|
|
132
162
|
:type="apexType"
|
|
@@ -135,6 +165,7 @@ function onDrawn() {
|
|
|
135
165
|
:series="apexSeries"
|
|
136
166
|
@mounted="onDrawn"
|
|
137
167
|
@updated="onDrawn"
|
|
168
|
+
@animation-end="redrawIfResizedWhileAnimating"
|
|
138
169
|
/>
|
|
139
170
|
</div>
|
|
140
171
|
</template>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageLink } from "../../build/composables/page/pageUrlValues";
|
|
2
3
|
import type { FormProps } from "../../composables/form/types/props";
|
|
3
4
|
import type {
|
|
4
5
|
FormData,
|
|
@@ -64,6 +65,7 @@ import DmsFormErrorSummary from "../../build/components/form/FormErrorSummary.vu
|
|
|
64
65
|
import DmsFormLoadError from "../../build/components/form/FormLoadError.vue";
|
|
65
66
|
import {
|
|
66
67
|
type FormRecordLoad,
|
|
68
|
+
missingRecordFailure,
|
|
67
69
|
trackFormRecordLoad,
|
|
68
70
|
} from "../../build/composables/form/formRecordLoad";
|
|
69
71
|
import DmsFormRequiredLegend from "./FormRequiredLegend.vue";
|
|
@@ -492,7 +494,7 @@ function cancelForm(): void {
|
|
|
492
494
|
window.history.back();
|
|
493
495
|
return;
|
|
494
496
|
}
|
|
495
|
-
if (props.backTo) void router.push(props.backTo);
|
|
497
|
+
if (props.backTo) void router.push(pageLink(props.backTo));
|
|
496
498
|
}
|
|
497
499
|
|
|
498
500
|
const validators = new Set<FormValidator>();
|
|
@@ -573,7 +575,15 @@ if (props.fetchUrl) {
|
|
|
573
575
|
});
|
|
574
576
|
}
|
|
575
577
|
|
|
576
|
-
const loadFailure = computed(
|
|
578
|
+
const loadFailure = computed(
|
|
579
|
+
() =>
|
|
580
|
+
recordLoad?.failure.value ??
|
|
581
|
+
missingRecordFailure({
|
|
582
|
+
requiresRecord: props.requiresRecord,
|
|
583
|
+
fetchUrl: props.fetchUrl,
|
|
584
|
+
resolvedFetchUrl: resolvedFetchUrl.value,
|
|
585
|
+
}),
|
|
586
|
+
);
|
|
577
587
|
const isRetryingLoad = computed(() => recordLoad?.retrying.value ?? false);
|
|
578
588
|
|
|
579
589
|
/** Load the record again after a failure; it is what the form starts from. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
|
|
2
3
|
import {
|
|
3
4
|
buildCascaderTree,
|
|
4
5
|
CASCADER_FETCH_LIMIT,
|
|
@@ -68,8 +69,10 @@ interface CascaderResponse {
|
|
|
68
69
|
|
|
69
70
|
const { data, status, execute } = await useDmsAsyncData(
|
|
70
71
|
`cascader-${instanceId}`,
|
|
71
|
-
() =>
|
|
72
|
-
|
|
72
|
+
() => {
|
|
73
|
+
const url = pageRequestUrl(props.searchUrl);
|
|
74
|
+
if (!url) return Promise.resolve({ results: [], total: 0 });
|
|
75
|
+
return $authFetch<CascaderResponse>(url, {
|
|
73
76
|
params: { limit: CASCADER_FETCH_LIMIT },
|
|
74
77
|
onRequestError: ({ error }: { error: Error }) => {
|
|
75
78
|
toast.add({
|
|
@@ -78,7 +81,8 @@ const { data, status, execute } = await useDmsAsyncData(
|
|
|
78
81
|
description: error.message,
|
|
79
82
|
});
|
|
80
83
|
},
|
|
81
|
-
})
|
|
84
|
+
});
|
|
85
|
+
},
|
|
82
86
|
{ lazy: true, immediate: false },
|
|
83
87
|
);
|
|
84
88
|
|
|
@@ -349,6 +349,12 @@ const acceptString = computed(() =>
|
|
|
349
349
|
props.constraints?.allowedMimetypes?.join(","),
|
|
350
350
|
);
|
|
351
351
|
|
|
352
|
+
const dropPromptKey = computed(() =>
|
|
353
|
+
props.multiple
|
|
354
|
+
? "dms.form.file.drop_prompt_multiple"
|
|
355
|
+
: "dms.form.file.drop_prompt",
|
|
356
|
+
);
|
|
357
|
+
|
|
352
358
|
const maxSizeFormatted = computed(() => {
|
|
353
359
|
if (!props.constraints?.maxSize) return null;
|
|
354
360
|
return formatFileSize(props.constraints.maxSize);
|
|
@@ -373,6 +379,7 @@ onScopeDispose(() => {
|
|
|
373
379
|
:disabled="disabled || isUploading"
|
|
374
380
|
:color="color"
|
|
375
381
|
:highlight="highlight"
|
|
382
|
+
:label="$t(dropPromptKey)"
|
|
376
383
|
v-bind="dropZoneAria"
|
|
377
384
|
>
|
|
378
385
|
<template v-if="maxSizeFormatted" #description>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
|
|
2
3
|
import type { SelectMenuItem } from "@nuxt/ui";
|
|
3
4
|
import { refDebounced } from "@vueuse/core";
|
|
4
5
|
import {
|
|
@@ -225,7 +226,9 @@ interface CountryOptions {
|
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
async function fetchCountryOptions(): Promise<CountryOptions> {
|
|
228
|
-
const
|
|
229
|
+
const url = pageRequestUrl(props.searchUrl);
|
|
230
|
+
if (!url) return { results: [], total: 0 };
|
|
231
|
+
const data = await $authFetch<CountryApiResponse>(url, {
|
|
229
232
|
params: {
|
|
230
233
|
q: countrySearchTermDebounced.value,
|
|
231
234
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
|
|
2
3
|
import { getCurrentInstance, h, render } from "vue";
|
|
3
4
|
import { useFormField } from "@nuxt/ui/composables/useFormField";
|
|
4
5
|
import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
|
|
@@ -84,10 +85,11 @@ function checkSyntax(text: string): void {
|
|
|
84
85
|
|
|
85
86
|
async function loadCompletions(): Promise<CodeEditorCompletion[]> {
|
|
86
87
|
const fixed = props.completions ?? [];
|
|
87
|
-
|
|
88
|
+
const url = pageRequestUrl(props.completionsUrl);
|
|
89
|
+
if (!url) return fixed;
|
|
88
90
|
const response = await $authFetch<
|
|
89
91
|
CodeEditorCompletion[] | CompletionsResponse
|
|
90
|
-
>(
|
|
92
|
+
>(url).catch(() => []);
|
|
91
93
|
const fetched = Array.isArray(response) ? response : (response.items ?? []);
|
|
92
94
|
return [...fixed, ...fetched];
|
|
93
95
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
|
|
2
3
|
import { useConfirm } from "../../../composables/confirm/useConfirm";
|
|
3
4
|
import DmsCopyButton from "../../copy/CopyButton.vue";
|
|
4
5
|
|
|
@@ -43,7 +44,7 @@ const masked = computed(() => {
|
|
|
43
44
|
});
|
|
44
45
|
|
|
45
46
|
async function rotate(): Promise<void> {
|
|
46
|
-
const url = props.rotateUrl;
|
|
47
|
+
const url = pageRequestUrl(props.rotateUrl);
|
|
47
48
|
if (!url) return;
|
|
48
49
|
const confirmed = await confirm({
|
|
49
50
|
title: t("dms.form.secret.rotate_title"),
|
|
@@ -95,7 +96,7 @@ async function rotate(): Promise<void> {
|
|
|
95
96
|
</UInput>
|
|
96
97
|
<DmsCopyButton v-if="props.copyable && value" :value="value" />
|
|
97
98
|
<UButton
|
|
98
|
-
v-if="props.rotateUrl"
|
|
99
|
+
v-if="pageRequestUrl(props.rotateUrl)"
|
|
99
100
|
icon="i-ph-arrows-clockwise"
|
|
100
101
|
:label="t('dms.form.secret.rotate')"
|
|
101
102
|
color="neutral"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
lang="ts"
|
|
4
4
|
generic="T extends ArrayOrNested<SelectMenuItem>, M extends boolean = false"
|
|
5
5
|
>
|
|
6
|
+
import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
|
|
6
7
|
import { useForwardPropsEmits } from "reka-ui";
|
|
7
8
|
import type {
|
|
8
9
|
SelectMenuProps,
|
|
@@ -193,7 +194,9 @@ interface RelationItems {
|
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
async function fetchRelationItems(): Promise<RelationItems> {
|
|
196
|
-
const
|
|
197
|
+
const url = pageRequestUrl(props.searchUrl);
|
|
198
|
+
if (!url) return { results: [], total: 0 };
|
|
199
|
+
const data = await $authFetch<RelationResponse>(url, {
|
|
197
200
|
params: {
|
|
198
201
|
search: searchTermDebounced.value || undefined,
|
|
199
202
|
},
|
|
@@ -5,6 +5,18 @@ const SINGLE_COLUMN = 1;
|
|
|
5
5
|
/** Track floor below which a column stops being usable, as a CSS length. */
|
|
6
6
|
export const GRID_DEFAULT_MIN_COLUMN_WIDTH = "240px";
|
|
7
7
|
|
|
8
|
+
const UNITLESS_NUMBER = /^[+-]?(\d+\.?\d*|\.\d+)$/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A length usable inside `calc()`. A bare `0` is a valid `gap` but not a
|
|
12
|
+
* length in a calculation, where it would void the whole template and leave
|
|
13
|
+
* the grid on a single column; a unitless number reads as pixels.
|
|
14
|
+
*/
|
|
15
|
+
function calcLength(value: string): string {
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
return UNITLESS_NUMBER.test(trimmed) ? `${trimmed}px` : trimmed;
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
/**
|
|
9
21
|
* The column track template a `Grid` and every `GridRow` inside it share.
|
|
10
22
|
*
|
|
@@ -26,8 +38,8 @@ export function gridColumnsTemplate(
|
|
|
26
38
|
minColumnWidth: string,
|
|
27
39
|
): string {
|
|
28
40
|
const columns = Math.max(maxColumns, SINGLE_COLUMN);
|
|
29
|
-
const columnShare = `(100% - ${columns - SINGLE_COLUMN} * ${gap}) / ${columns}`;
|
|
30
|
-
return `repeat(auto-fill, minmax(min(100%, max(${minColumnWidth}, ${columnShare})), 1fr))`;
|
|
41
|
+
const columnShare = `(100% - ${columns - SINGLE_COLUMN} * ${calcLength(gap)}) / ${columns}`;
|
|
42
|
+
return `repeat(auto-fill, minmax(min(100%, max(${calcLength(minColumnWidth)}, ${columnShare})), 1fr))`;
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
/** Custom property a `Grid` / `GridRow` hands its cells: the tracks left. */
|
|
@@ -43,7 +55,7 @@ function trackBreakpoint(
|
|
|
43
55
|
gap: string,
|
|
44
56
|
minColumnWidth: string,
|
|
45
57
|
): string {
|
|
46
|
-
return `calc(${columns} * ${minColumnWidth} + ${columns - SINGLE_COLUMN} * ${gap})`;
|
|
58
|
+
return `calc(${columns} * ${calcLength(minColumnWidth)} + ${columns - SINGLE_COLUMN} * ${calcLength(gap)})`;
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
/** A container name / attribute value built from any id. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageLink } from "../../build/composables/page/pageUrlValues";
|
|
2
3
|
import { computed } from "vue";
|
|
3
4
|
import { regionalDateTimeFormat } from "#dms-core/app/utils/regional";
|
|
4
5
|
import DmsStatusPill from "../status-pill/StatusPill.vue";
|
|
@@ -216,7 +217,7 @@ function keyOf(item: KeyValueItem, index: number): string {
|
|
|
216
217
|
<template v-else>
|
|
217
218
|
<component
|
|
218
219
|
:is="item.to ? DmsAutoLink : 'span'"
|
|
219
|
-
:to="item.to"
|
|
220
|
+
:to="pageLink(item.to)"
|
|
220
221
|
class="min-w-0"
|
|
221
222
|
:class="item.to && item.type !== 'status' && 'hover:underline'"
|
|
222
223
|
>
|
|
@@ -125,6 +125,7 @@ const theme = tv({
|
|
|
125
125
|
dot: "size-2 rounded-full bg-(--dms-accent-fill) shadow-[0_0_0_3px_var(--dms-accent-tint)]",
|
|
126
126
|
body: "min-w-0 flex-1",
|
|
127
127
|
title: "flex items-center gap-2",
|
|
128
|
+
titleText: "",
|
|
128
129
|
description: "text-muted mt-0.5 text-[12.5px]",
|
|
129
130
|
// The "·" closes each entry followed by another (rather than opening it),
|
|
130
131
|
// so a wrapped line never starts with a separator.
|
|
@@ -188,7 +189,11 @@ const theme = tv({
|
|
|
188
189
|
// end, so the title and meta wrap there instead.
|
|
189
190
|
truncate: {
|
|
190
191
|
true: {
|
|
191
|
-
title:
|
|
192
|
+
title:
|
|
193
|
+
"truncate flex-nowrap max-sm:flex-wrap max-sm:whitespace-normal",
|
|
194
|
+
// The text box, not the flex title, carries the ellipsis: a flex
|
|
195
|
+
// container only clips its overflowing children.
|
|
196
|
+
titleText: "truncate max-sm:whitespace-normal",
|
|
192
197
|
meta: "flex-nowrap overflow-hidden whitespace-nowrap max-sm:flex-wrap max-sm:whitespace-normal",
|
|
193
198
|
detail: "truncate max-sm:whitespace-normal",
|
|
194
199
|
},
|
|
@@ -281,7 +286,9 @@ const unreadText = computed(
|
|
|
281
286
|
:class="ui.title()"
|
|
282
287
|
:title="props.truncate ? props.title : undefined"
|
|
283
288
|
>
|
|
284
|
-
<slot>
|
|
289
|
+
<slot>
|
|
290
|
+
<span :class="ui.titleText()">{{ props.title }}</span>
|
|
291
|
+
</slot>
|
|
285
292
|
</div>
|
|
286
293
|
<p v-if="hasDescription" :class="ui.description()">
|
|
287
294
|
<slot name="description">{{ props.description }}</slot>
|
|
@@ -3,10 +3,11 @@ import type { SaveStatusState } from "./SaveStatus.vue";
|
|
|
3
3
|
|
|
4
4
|
interface InstantSaveBadgeProps {
|
|
5
5
|
/**
|
|
6
|
-
* Save activity of the page, for a subtle "just saved" flash: the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Save activity of the page, for a subtle "just saved" flash: the pill
|
|
7
|
+
* reads "Saving" with a spinning icon while a change is being written,
|
|
8
|
+
* then "Saved instantly" with a tick once it is, and turns red with "Not
|
|
9
|
+
* saved" while a save failed (the control says which). Defaults to `idle`
|
|
10
|
+
* (a lightning bolt and "Saved instantly").
|
|
10
11
|
*/
|
|
11
12
|
state?: SaveStatusState;
|
|
12
13
|
}
|
|
@@ -23,6 +24,13 @@ const ICONS: Record<SaveStatusState, string> = {
|
|
|
23
24
|
error: "i-ph-warning-circle",
|
|
24
25
|
};
|
|
25
26
|
|
|
27
|
+
const LABELS: Record<SaveStatusState, string> = {
|
|
28
|
+
idle: "dms.save_bar.instant",
|
|
29
|
+
saving: "dms.save_bar.saving",
|
|
30
|
+
saved: "dms.save_bar.instant",
|
|
31
|
+
error: "dms.save_bar.not_saved",
|
|
32
|
+
};
|
|
33
|
+
|
|
26
34
|
const TONE_CLASSES = {
|
|
27
35
|
ok: "border-success/40 bg-success/10 text-success",
|
|
28
36
|
error: "border-error/40 bg-error/10 text-error",
|
|
@@ -49,6 +57,6 @@ const TONE_CLASSES = {
|
|
|
49
57
|
class="size-3.5 shrink-0"
|
|
50
58
|
:class="{ 'animate-spin': props.state === 'saving' }"
|
|
51
59
|
/>
|
|
52
|
-
{{ t(
|
|
60
|
+
{{ t(LABELS[props.state]) }}
|
|
53
61
|
</span>
|
|
54
62
|
</template>
|
|
@@ -40,7 +40,7 @@ interface SaveBarProps {
|
|
|
40
40
|
kind?: FormKind;
|
|
41
41
|
/**
|
|
42
42
|
* Whether anything in the form can be changed: an `action` form nothing in
|
|
43
|
-
* which can be changed
|
|
43
|
+
* which can be changed never offers Reset.
|
|
44
44
|
*/
|
|
45
45
|
resettable?: boolean;
|
|
46
46
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageLink } from "../../build/composables/page/pageUrlValues";
|
|
2
3
|
import { computed } from "vue";
|
|
3
4
|
import type { IconWellTone } from "../icon-well/IconWell.vue";
|
|
4
5
|
import DmsStatCell from "../../build/components/stat/StatCell.vue";
|
|
@@ -146,7 +147,7 @@ function wellTone(item: StatGroupItem): IconWellTone {
|
|
|
146
147
|
:icon="item.icon"
|
|
147
148
|
:detail="item.detail"
|
|
148
149
|
:detail-tone="item.detailTone"
|
|
149
|
-
:to="item.to"
|
|
150
|
+
:to="pageLink(item.to)"
|
|
150
151
|
size="sm"
|
|
151
152
|
:tone="wellTone(item)"
|
|
152
153
|
:loading="props.loading"
|
|
@@ -175,7 +176,7 @@ function wellTone(item: StatGroupItem): IconWellTone {
|
|
|
175
176
|
:icon="item.icon"
|
|
176
177
|
:detail="item.detail"
|
|
177
178
|
:detail-tone="item.detailTone"
|
|
178
|
-
:to="item.to"
|
|
179
|
+
:to="pageLink(item.to)"
|
|
179
180
|
:tone="wellTone(item)"
|
|
180
181
|
:loading="props.loading"
|
|
181
182
|
stack
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
|
-
buildCascaderPathLabels,
|
|
4
3
|
CASCADER_FETCH_LIMIT,
|
|
5
4
|
CASCADER_PATH_SEPARATOR,
|
|
5
|
+
cascaderValuePath,
|
|
6
6
|
resolveCascaderKeyMapping,
|
|
7
7
|
type CascaderKeyMapping,
|
|
8
8
|
} from "../../utils/cascader";
|
|
9
|
+
import { MAX_RELATION_BADGES } from "../../build/composables/data-types/relationBadges";
|
|
9
10
|
|
|
10
11
|
const DEFAULT_VALUE_KEY = "_id";
|
|
11
12
|
|
|
12
13
|
interface CascaderPathProps {
|
|
14
|
+
/** One picked value, or the list a multiple cascader holds. */
|
|
13
15
|
value: unknown;
|
|
14
16
|
searchUrl?: string;
|
|
15
17
|
keyMapping?: Partial<CascaderKeyMapping>;
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
/** A picked value of a multiple cascader: its name, its path on hover. */
|
|
21
|
+
interface CascaderBadge {
|
|
22
|
+
label: string;
|
|
23
|
+
title: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
const props = defineProps<CascaderPathProps>();
|
|
19
27
|
|
|
20
28
|
const { $authFetch } = useAuthFetch();
|
|
@@ -31,22 +39,6 @@ const keyMapping = computed(() =>
|
|
|
31
39
|
}),
|
|
32
40
|
);
|
|
33
41
|
|
|
34
|
-
const valueObject = computed(() =>
|
|
35
|
-
isObject(props.value) ? (props.value as Record<string, unknown>) : undefined,
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const valueId = computed(() => {
|
|
39
|
-
const raw = valueObject.value
|
|
40
|
-
? valueObject.value[keyMapping.value.value]
|
|
41
|
-
: props.value;
|
|
42
|
-
return raw === undefined || raw === null ? undefined : String(raw);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const ownLabel = computed(() => {
|
|
46
|
-
const label = valueObject.value?.[keyMapping.value.label];
|
|
47
|
-
return label === undefined || label === null ? undefined : String(label);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
42
|
const { data } = await useDmsAsyncData(
|
|
51
43
|
`cascader-path-${props.searchUrl}`,
|
|
52
44
|
() =>
|
|
@@ -58,21 +50,51 @@ const { data } = await useDmsAsyncData(
|
|
|
58
50
|
{ lazy: true },
|
|
59
51
|
);
|
|
60
52
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
53
|
+
const pathOf = (item: unknown): string[] =>
|
|
54
|
+
cascaderValuePath(item, keyMapping.value, data.value?.results);
|
|
55
|
+
|
|
56
|
+
const pathLabel = computed(() =>
|
|
57
|
+
pathOf(props.value).join(CASCADER_PATH_SEPARATOR),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const badges = computed<CascaderBadge[]>(() =>
|
|
61
|
+
(Array.isArray(props.value) ? props.value : [])
|
|
62
|
+
.map(pathOf)
|
|
63
|
+
.filter((path) => path.length > 0)
|
|
64
|
+
.map((path) => ({
|
|
65
|
+
label: path.at(-1) ?? "",
|
|
66
|
+
title: path.join(CASCADER_PATH_SEPARATOR),
|
|
67
|
+
})),
|
|
68
|
+
);
|
|
69
|
+
const visibleBadges = computed(() =>
|
|
70
|
+
badges.value.slice(0, MAX_RELATION_BADGES),
|
|
71
|
+
);
|
|
72
|
+
const hiddenBadges = computed(() => badges.value.slice(MAX_RELATION_BADGES));
|
|
74
73
|
</script>
|
|
75
74
|
|
|
76
75
|
<template>
|
|
77
|
-
<span
|
|
76
|
+
<span
|
|
77
|
+
v-if="Array.isArray(props.value)"
|
|
78
|
+
class="inline-flex flex-wrap items-center gap-1"
|
|
79
|
+
>
|
|
80
|
+
<UBadge
|
|
81
|
+
v-for="badge in visibleBadges"
|
|
82
|
+
:key="badge.title"
|
|
83
|
+
:label="badge.label"
|
|
84
|
+
:title="badge.title"
|
|
85
|
+
color="neutral"
|
|
86
|
+
variant="subtle"
|
|
87
|
+
size="sm"
|
|
88
|
+
class="max-w-40"
|
|
89
|
+
/>
|
|
90
|
+
<UBadge
|
|
91
|
+
v-if="hiddenBadges.length"
|
|
92
|
+
:label="`+${hiddenBadges.length}`"
|
|
93
|
+
:title="hiddenBadges.map((badge) => badge.title).join(', ')"
|
|
94
|
+
color="neutral"
|
|
95
|
+
variant="subtle"
|
|
96
|
+
size="sm"
|
|
97
|
+
/>
|
|
98
|
+
</span>
|
|
99
|
+
<span v-else class="truncate" :title="pathLabel">{{ pathLabel }}</span>
|
|
78
100
|
</template>
|
|
@@ -9,7 +9,14 @@ import {
|
|
|
9
9
|
|
|
10
10
|
const DEFAULT_BAR_RADIUS = 4;
|
|
11
11
|
const DEFAULT_BAR_WIDTH_PCT = "60%";
|
|
12
|
-
const
|
|
12
|
+
const HUNDRED_PERCENT = 100;
|
|
13
|
+
/** Ring thickness, in percent of the radius, of a donut without `arcWidth`. */
|
|
14
|
+
const DEFAULT_ARC_WIDTH = 24;
|
|
15
|
+
/**
|
|
16
|
+
* The thinnest ring, in percent of the radius, a slice label ("33.0%") fits
|
|
17
|
+
* in: on a thinner one its white text runs past the ring onto the card.
|
|
18
|
+
*/
|
|
19
|
+
const MIN_LABELLED_ARC_WIDTH = 35;
|
|
13
20
|
const DEFAULT_RADIAL_HOLE = "60%";
|
|
14
21
|
const CENTER_NAME_FONT_SIZE = "10.5px";
|
|
15
22
|
const CENTER_NAME_FONT_WEIGHT = 600;
|
|
@@ -17,7 +24,6 @@ const CENTER_VALUE_FONT_SIZE = "20px";
|
|
|
17
24
|
const CENTER_VALUE_FONT_WEIGHT = 650;
|
|
18
25
|
const HEATMAP_RADIUS = 4;
|
|
19
26
|
const HEATMAP_DEFAULT_INTENSITY = 0.6;
|
|
20
|
-
const HUNDRED_PERCENT = 100;
|
|
21
27
|
|
|
22
28
|
type PlotBuilder = (props: UseApexChartInput) => Record<string, unknown>;
|
|
23
29
|
|
|
@@ -47,8 +53,13 @@ function buildBarPlotOptions(
|
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
function donutSizeFor(arcWidth: number | undefined): string {
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
return `${HUNDRED_PERCENT - (arcWidth ?? DEFAULT_ARC_WIDTH)}%`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Whether a slice label fits inside the chart's ring; always true off donuts. */
|
|
60
|
+
export function ringFitsSliceLabels(input: UseApexChartInput): boolean {
|
|
61
|
+
if (input.type !== "donut") return true;
|
|
62
|
+
return (input.arcWidth ?? DEFAULT_ARC_WIDTH) >= MIN_LABELLED_ARC_WIDTH;
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
/** v2 donut centre: mono eyebrow over a 20px value. */
|