@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
|
@@ -18,9 +18,19 @@ const DATA_LABEL_FONT_SIZE = "11px";
|
|
|
18
18
|
const DATA_LABEL_FONT_WEIGHT = 600;
|
|
19
19
|
const TOOLTIP_DATETIME_FORMAT = "dd MMM yyyy";
|
|
20
20
|
const HOVER_MARKER_SIZE = 5;
|
|
21
|
+
/** Grows a visible point on hover instead of setting a size below its own. */
|
|
22
|
+
const HOVER_POINT_SIZE_OFFSET = 2;
|
|
23
|
+
/** A scatter is nothing but its points, so it draws them without `pointSize`. */
|
|
24
|
+
const DEFAULT_POINT_SIZE = 6;
|
|
25
|
+
const POINT_MARKER_TYPES: ChartType[] = ["scatter"];
|
|
26
|
+
/** Half a data label's box and a gap: lifts the label off the point it names. */
|
|
27
|
+
const POINT_LABEL_CLEARANCE = 11;
|
|
28
|
+
const HALF = 2;
|
|
21
29
|
const HEAD_MARKER_SIZE = 5;
|
|
22
30
|
const SOLID_GRID = 0;
|
|
23
31
|
const HEAD_MARKER_TYPES: ChartType[] = ["line", "area"];
|
|
32
|
+
/** A radar draws its own polygon grid; the cartesian one would cross the card. */
|
|
33
|
+
const OWN_GRID_TYPES: ChartType[] = ["radar"];
|
|
24
34
|
|
|
25
35
|
/** Theme colors every section of an Apex config reads once per build. */
|
|
26
36
|
export interface ApexThemeColors {
|
|
@@ -83,7 +93,10 @@ export function buildGrid(
|
|
|
83
93
|
theme: ApexThemeColors,
|
|
84
94
|
): Record<string, unknown> {
|
|
85
95
|
return {
|
|
86
|
-
show:
|
|
96
|
+
show:
|
|
97
|
+
!isCircular &&
|
|
98
|
+
!OWN_GRID_TYPES.includes(input.type) &&
|
|
99
|
+
(input.showGrid ?? true),
|
|
87
100
|
borderColor: theme.border,
|
|
88
101
|
strokeDashArray: SOLID_GRID,
|
|
89
102
|
padding: GRID_PADDING,
|
|
@@ -140,6 +153,13 @@ export function buildMarkers(
|
|
|
140
153
|
colors: string[],
|
|
141
154
|
theme: ApexThemeColors,
|
|
142
155
|
): Record<string, unknown> {
|
|
156
|
+
if (POINT_MARKER_TYPES.includes(input.type)) {
|
|
157
|
+
return {
|
|
158
|
+
size: pointSize(input),
|
|
159
|
+
strokeWidth: 0,
|
|
160
|
+
hover: { sizeOffset: HOVER_POINT_SIZE_OFFSET },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
143
163
|
const showsHead = HEAD_MARKER_TYPES.includes(input.type) && !input.stacked;
|
|
144
164
|
return {
|
|
145
165
|
size: 0,
|
|
@@ -151,18 +171,55 @@ export function buildMarkers(
|
|
|
151
171
|
};
|
|
152
172
|
}
|
|
153
173
|
|
|
154
|
-
|
|
174
|
+
function pointSize(input: UseApexChartInput): number {
|
|
175
|
+
return input.pointSize ?? DEFAULT_POINT_SIZE;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* How far a data label moves up: Apex centres it on its point, which hides a
|
|
180
|
+
* scatter's point under its own label.
|
|
181
|
+
*/
|
|
182
|
+
export function dataLabelOffset(input: UseApexChartInput): number {
|
|
183
|
+
if (!POINT_MARKER_TYPES.includes(input.type)) return 0;
|
|
184
|
+
return -(pointSize(input) / HALF + POINT_LABEL_CLEARANCE);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Where Apex is drawing a data label. */
|
|
188
|
+
interface DataLabelContext {
|
|
189
|
+
seriesIndex: number;
|
|
190
|
+
dataPointIndex: number;
|
|
191
|
+
w: { config: { series: ChartSeries[] } };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** A point's own `label` when it has one, else the value Apex would print. */
|
|
195
|
+
function pointLabel(value: unknown, context: DataLabelContext): unknown {
|
|
196
|
+
const point =
|
|
197
|
+
context.w.config.series[context.seriesIndex]?.data[context.dataPointIndex];
|
|
198
|
+
if (typeof point !== "object" || point === null || !("label" in point)) {
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
return point.label || value;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Value labels in mono, without Apex's default drop shadow. A circular chart
|
|
206
|
+
* keeps Apex's own percent formatter: its series hold bare numbers.
|
|
207
|
+
*/
|
|
155
208
|
export function buildDataLabels(
|
|
156
209
|
enabled: boolean,
|
|
157
210
|
theme: ApexThemeColors,
|
|
211
|
+
isCircular = false,
|
|
212
|
+
offsetY = 0,
|
|
158
213
|
): Record<string, unknown> {
|
|
159
214
|
return {
|
|
160
215
|
enabled,
|
|
216
|
+
offsetY,
|
|
161
217
|
dropShadow: { enabled: false },
|
|
162
218
|
style: {
|
|
163
219
|
fontSize: DATA_LABEL_FONT_SIZE,
|
|
164
220
|
fontFamily: theme.monoFont,
|
|
165
221
|
fontWeight: DATA_LABEL_FONT_WEIGHT,
|
|
166
222
|
},
|
|
223
|
+
...(isCircular ? {} : { formatter: pointLabel }),
|
|
167
224
|
};
|
|
168
225
|
}
|
|
@@ -3,7 +3,10 @@ import type {
|
|
|
3
3
|
ConfirmDialogSecondaryAction,
|
|
4
4
|
ConfirmDialogSecondaryTarget,
|
|
5
5
|
} from "#dms-core/app/types/confirm-dialog";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
interpolateRowText,
|
|
8
|
+
interpolateUrl,
|
|
9
|
+
} from "#dms-core/app/utils/url-interpolation";
|
|
7
10
|
import { readTranslationKey } from "#dms-core/app/composables/translation/useTranslation";
|
|
8
11
|
import type {
|
|
9
12
|
ConfirmOptions,
|
|
@@ -11,6 +14,12 @@ import type {
|
|
|
11
14
|
} from "../../../composables/confirm/types";
|
|
12
15
|
import { processFieldI18n } from "../../utils/fieldOptionsI18n";
|
|
13
16
|
import { interpolateLiteral } from "../../utils/literalText";
|
|
17
|
+
import {
|
|
18
|
+
namesPageValue,
|
|
19
|
+
resolveLinkVariables,
|
|
20
|
+
resolveTextVariables,
|
|
21
|
+
} from "../../utils/urlVariables";
|
|
22
|
+
import { currentPageUrlValues } from "../page/pageUrlValues";
|
|
14
23
|
|
|
15
24
|
/** `t` as vue-i18n gives it: named parameters, then the plural count. */
|
|
16
25
|
export type ConfirmTranslate = (
|
|
@@ -41,22 +50,36 @@ export type SecondaryTargetRun = (
|
|
|
41
50
|
target: ConfirmDialogSecondaryTarget,
|
|
42
51
|
) => Promise<void> | void;
|
|
43
52
|
|
|
53
|
+
// The page's `{{params.X}}` / `{{query.X}}` come first, as on an action's own
|
|
54
|
+
// target; the route is only read for a text holding such a token.
|
|
55
|
+
function withPageLink(url: string): string {
|
|
56
|
+
return namesPageValue(url)
|
|
57
|
+
? resolveLinkVariables(url, currentPageUrlValues())
|
|
58
|
+
: url;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function withPageText(text: string): string | undefined {
|
|
62
|
+
return namesPageValue(text)
|
|
63
|
+
? resolveTextVariables(text, currentPageUrlValues())
|
|
64
|
+
: text;
|
|
65
|
+
}
|
|
66
|
+
|
|
44
67
|
/**
|
|
45
68
|
* A dialog's secondary target with its URL (and file name) filled in with
|
|
46
|
-
* the same parameters as its texts.
|
|
69
|
+
* the page's values and, as `{{row.X}}`, the same parameters as its texts.
|
|
47
70
|
*/
|
|
48
71
|
function resolveSecondaryTarget(
|
|
49
72
|
target: ConfirmDialogSecondaryTarget,
|
|
50
73
|
params: Record<string, unknown>,
|
|
51
74
|
): ConfirmDialogSecondaryTarget {
|
|
52
|
-
const url = interpolateUrl(target.url, params);
|
|
75
|
+
const url = interpolateUrl(withPageLink(target.url), params);
|
|
53
76
|
if (target.type !== "download" || target.filename === undefined) {
|
|
54
77
|
return { ...target, url };
|
|
55
78
|
}
|
|
56
79
|
return {
|
|
57
80
|
...target,
|
|
58
81
|
url,
|
|
59
|
-
filename:
|
|
82
|
+
filename: withPageText(interpolateRowText(target.filename, params)),
|
|
60
83
|
};
|
|
61
84
|
}
|
|
62
85
|
|
|
@@ -15,12 +15,17 @@ import {
|
|
|
15
15
|
} from "./confirmDialogTexts";
|
|
16
16
|
import type { DownloadFetch } from "../../utils/fileDownload";
|
|
17
17
|
import { secondaryTargetRunner } from "./secondaryTarget";
|
|
18
|
+
import {
|
|
19
|
+
namesPageValue,
|
|
20
|
+
resolveLinkVariables,
|
|
21
|
+
} from "../../utils/urlVariables";
|
|
22
|
+
import { currentPageUrlValues } from "../page/pageUrlValues";
|
|
18
23
|
|
|
19
24
|
/** What an action asks its confirmation with. */
|
|
20
25
|
export interface ActionConfirmRequest {
|
|
21
26
|
/** The row the action targets: its fields fill the texts and `from` URL. */
|
|
22
27
|
row?: Record<string, unknown>;
|
|
23
|
-
/** Fill-ins of a `from` URL besides the row's fields (`{id}`). */
|
|
28
|
+
/** Fill-ins of a `from` URL besides the row's fields (`{{row.id}}`). */
|
|
24
29
|
urlParams?: Record<string, unknown>;
|
|
25
30
|
/**
|
|
26
31
|
* The look of the action, where the dialog leaves it out (an archive's
|
|
@@ -55,7 +60,10 @@ export function useActionConfirm() {
|
|
|
55
60
|
request: ActionConfirmRequest,
|
|
56
61
|
): Promise<ConfirmDialog | undefined> => {
|
|
57
62
|
if (!isConfirmFrom(declared)) return declared;
|
|
58
|
-
const
|
|
63
|
+
const from = namesPageValue(declared.from)
|
|
64
|
+
? resolveLinkVariables(declared.from, currentPageUrlValues())
|
|
65
|
+
: declared.from;
|
|
66
|
+
const url = interpolateUrl(from, {
|
|
59
67
|
...request.row,
|
|
60
68
|
...request.urlParams,
|
|
61
69
|
});
|
|
@@ -322,13 +322,8 @@ function renderRelation(value: unknown, options: unknown) {
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
function renderCascaderRelation(value: unknown, options: unknown) {
|
|
325
|
-
const { t } = useI18n();
|
|
326
325
|
const opts = options as CascaderRelationOptions | undefined;
|
|
327
326
|
|
|
328
|
-
if (opts?.multiple && Array.isArray(value)) {
|
|
329
|
-
return t("dms.form.cascader.selected_count", { count: value.length });
|
|
330
|
-
}
|
|
331
|
-
|
|
332
327
|
if (!value) return value;
|
|
333
328
|
|
|
334
329
|
return h(CascaderPath, {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FormFieldOrGroup } from "../../../composables/form/types/field";
|
|
2
|
+
import { isFieldGroup } from "../../../composables/form/types/field";
|
|
3
|
+
|
|
4
|
+
function entryOrder(entry: FormFieldOrGroup): number | undefined {
|
|
5
|
+
return isFieldGroup(entry) ? entry.order : undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A form's entries with its groups' `order` applied: the groups giving one
|
|
10
|
+
* trade the places they were declared at, sorted on it (equal orders keep
|
|
11
|
+
* their declaration order); every other entry keeps its place.
|
|
12
|
+
*/
|
|
13
|
+
export function orderFormEntries<T extends FormFieldOrGroup>(
|
|
14
|
+
entries: readonly T[],
|
|
15
|
+
): T[] {
|
|
16
|
+
const ordered = entries
|
|
17
|
+
.filter((entry) => entryOrder(entry) !== undefined)
|
|
18
|
+
.sort((left, right) => entryOrder(left)! - entryOrder(right)!);
|
|
19
|
+
let next = 0;
|
|
20
|
+
return entries.map((entry) =>
|
|
21
|
+
entryOrder(entry) === undefined ? entry : ordered[next++]!,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* it has somewhere to go back to, then "Unsaved changes" with Discard and
|
|
7
7
|
* Save.
|
|
8
8
|
* - `action`: a form that does something each time (send a notification,
|
|
9
|
-
* invite someone, run a job).
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* successful submit.
|
|
9
|
+
* invite someone, run a job). Its own submit label, always ready: what it
|
|
10
|
+
* holds untouched, its defaults included, is a payload too. Beside it,
|
|
11
|
+
* Reset once a value changed, Cancel before that in a drawer or a modal.
|
|
12
|
+
* It empties after a successful submit.
|
|
13
13
|
*/
|
|
14
14
|
export type FormKind = "record" | "action";
|
|
15
15
|
|
|
@@ -83,21 +83,25 @@ function recordSaveBar(context: SaveBarContext): SaveBarState {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
* An action form's bar:
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
86
|
+
* An action form's bar: its submit, never held, so a pre-filled form sends as
|
|
87
|
+
* it is. Beside it, Reset once a value changed; before that, Cancel in a
|
|
88
|
+
* drawer or a modal, its only way out. A form nothing in which can be changed
|
|
89
|
+
* has nothing to reset.
|
|
90
90
|
*/
|
|
91
91
|
function actionSaveBar(context: SaveBarContext): SaveBarState {
|
|
92
|
-
const bar = {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
const bar: SaveBarState = {
|
|
93
|
+
showsStatus: false,
|
|
94
|
+
showsSubmit: true,
|
|
95
|
+
isSubmitHeld: false,
|
|
96
|
+
isHidden: false,
|
|
97
|
+
};
|
|
98
|
+
if (context.dirty && context.resettable) {
|
|
99
|
+
return { ...bar, secondary: "discard" };
|
|
96
100
|
}
|
|
97
|
-
if (!context.cancellable) {
|
|
98
|
-
return { ...
|
|
101
|
+
if (!context.dirty && context.cancellable) {
|
|
102
|
+
return { ...bar, secondary: "cancel" };
|
|
99
103
|
}
|
|
100
|
-
return
|
|
104
|
+
return bar;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
const SAVE_BARS: Record<FormKind, (context: SaveBarContext) => SaveBarState> = {
|
|
@@ -5,9 +5,33 @@ import { HTTP_FORBIDDEN } from "#dms-core/app/utils/http-status";
|
|
|
5
5
|
/**
|
|
6
6
|
* Why a form could not load the record it edits: `forbidden` when the server
|
|
7
7
|
* refused the caller the read (a role that may edit rows but not view them),
|
|
8
|
-
* `
|
|
8
|
+
* `missing` when its address lacks the record's id (a page reached without
|
|
9
|
+
* its `?id=`), `error` for anything else (a network failure, a server error,
|
|
10
|
+
* a row gone).
|
|
9
11
|
*/
|
|
10
|
-
export type FormLoadFailure = "forbidden" | "error";
|
|
12
|
+
export type FormLoadFailure = "forbidden" | "missing" | "error";
|
|
13
|
+
|
|
14
|
+
/** What a form knows of the record it is about, as far as `missing` goes. */
|
|
15
|
+
export interface FormRecordAddress {
|
|
16
|
+
requiresRecord?: boolean;
|
|
17
|
+
fetchUrl?: string;
|
|
18
|
+
/** `fetchUrl` with its route values filled; unset while one is missing. */
|
|
19
|
+
resolvedFetchUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `missing` for a form about one record whose address names no record yet:
|
|
24
|
+
* its fields would only hold their defaults, which read as a record.
|
|
25
|
+
*/
|
|
26
|
+
export function missingRecordFailure(
|
|
27
|
+
address: FormRecordAddress,
|
|
28
|
+
): FormLoadFailure | null {
|
|
29
|
+
const isMissing =
|
|
30
|
+
!!address.requiresRecord &&
|
|
31
|
+
!!address.fetchUrl &&
|
|
32
|
+
!address.resolvedFetchUrl;
|
|
33
|
+
return isMissing ? "missing" : null;
|
|
34
|
+
}
|
|
11
35
|
|
|
12
36
|
/** The cached load of a form's record (`useDmsAsyncData`), as far as it is read. */
|
|
13
37
|
export interface FormLoadSource<T> {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
namesPageValue,
|
|
3
|
+
type ReplaceUrlVariablesContext,
|
|
4
|
+
resolveLinkVariables,
|
|
5
|
+
resolveUrlVariables,
|
|
6
|
+
} from "../../utils/urlVariables";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The values of the page being shown that a `{{params.X}}` / `{{query.X}}`
|
|
10
|
+
* token names: the route parameters of the DMS page matching the URL, and its
|
|
11
|
+
* query. Read when called, so it follows the URL as it changes.
|
|
12
|
+
*/
|
|
13
|
+
export function currentPageUrlValues(): ReplaceUrlVariablesContext {
|
|
14
|
+
const route = useDmsRoute();
|
|
15
|
+
return {
|
|
16
|
+
routeParams: useSiteLayout().findMatchingRoute(route.path)?.params,
|
|
17
|
+
routeQuery: route.query as Record<string, unknown>,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A link declared on a block (`to`) with the page's tokens filled, as a page
|
|
23
|
+
* header button's: a query pair the page has no value for is left out. The
|
|
24
|
+
* route is only read for a link holding a token.
|
|
25
|
+
*/
|
|
26
|
+
export function pageLink(to: string): string;
|
|
27
|
+
export function pageLink(to: string | undefined): string | undefined;
|
|
28
|
+
export function pageLink(to: string | undefined): string | undefined {
|
|
29
|
+
if (!to || !namesPageValue(to)) return to;
|
|
30
|
+
return resolveLinkVariables(to, currentPageUrlValues());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A route a field or block requests (`searchUrl`, `rotateUrl`, …) with the
|
|
35
|
+
* page's tokens filled, or `undefined` while one has no value, as a block's
|
|
36
|
+
* `fetchUrl`.
|
|
37
|
+
*/
|
|
38
|
+
export function pageRequestUrl(url: string | undefined): string | undefined {
|
|
39
|
+
if (!url || !namesPageValue(url)) return url;
|
|
40
|
+
return resolveUrlVariables(url, currentPageUrlValues());
|
|
41
|
+
}
|
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LocationQueryValue } from "#dms/frontend-module";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveLinkVariables } from "../../utils/urlVariables";
|
|
3
3
|
|
|
4
4
|
type RouteQuery = Record<string, LocationQueryValue | LocationQueryValue[]>;
|
|
5
5
|
|
|
@@ -23,7 +23,7 @@ export function resolveFormSuccessTarget(
|
|
|
23
23
|
{ url, preserveQuery }: FormSuccessParams,
|
|
24
24
|
route: FormSuccessRoute,
|
|
25
25
|
): { path: string; query?: RouteQuery } {
|
|
26
|
-
const path =
|
|
26
|
+
const path = resolveLinkVariables(url, {
|
|
27
27
|
routeParams: route.params,
|
|
28
28
|
routeQuery: route.query,
|
|
29
29
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { get } from "@nuxt/ui/runtime/utils/index.js";
|
|
2
2
|
import { computed } from "vue";
|
|
3
|
+
import { interpolateUrl } from "#dms-core/app/utils/url-interpolation";
|
|
3
4
|
import type {
|
|
4
5
|
TableViewColumn,
|
|
5
6
|
TableViewListResponse,
|
|
@@ -54,7 +55,6 @@ const DESCENDING = "desc";
|
|
|
54
55
|
*/
|
|
55
56
|
export const LIVE_HELD_AT_QUERY_KEY = "liveHeldAt";
|
|
56
57
|
const NO_ROUTE_VALUES: ReplaceUrlVariablesContext = { routeQuery: {} };
|
|
57
|
-
const ROW_ID_TOKEN = "{id}";
|
|
58
58
|
|
|
59
59
|
type SourceCapabilities = TableViewSourceConfig["capabilities"];
|
|
60
60
|
|
|
@@ -251,10 +251,8 @@ export function useTableRows<T extends Record<string, unknown>>(
|
|
|
251
251
|
id: string,
|
|
252
252
|
): Promise<T | undefined> => {
|
|
253
253
|
const url = resolveUrlVariables(
|
|
254
|
-
sourceConfig.fetchRowUrl
|
|
255
|
-
|
|
256
|
-
encodeURIComponent(id),
|
|
257
|
-
),
|
|
254
|
+
sourceConfig.fetchRowUrl &&
|
|
255
|
+
interpolateUrl(sourceConfig.fetchRowUrl, { id }),
|
|
258
256
|
options.route?.() ?? NO_ROUTE_VALUES,
|
|
259
257
|
);
|
|
260
258
|
return url ? api<T>(url) : Promise.resolve(undefined);
|
|
@@ -102,20 +102,15 @@ export const useTableViewConfig = <T extends Data>(
|
|
|
102
102
|
.map((col) => ({ ...col, header: processI18n(col.header) })),
|
|
103
103
|
);
|
|
104
104
|
|
|
105
|
-
// Config-derived (SSR-safe; the registry is client-only): the
|
|
106
|
-
//
|
|
105
|
+
// Config-derived (SSR-safe; the registry is client-only): the listed
|
|
106
|
+
// `displays`, else the built-in `table` alone. Built-in defaults
|
|
107
107
|
// (selfManagedData/capabilities) are merged per id.
|
|
108
|
-
const resolvedDisplays: TableViewDisplayConfig[] = (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
for (const display of config.displays ?? []) {
|
|
112
|
-
if (display.id === TABLE_DISPLAY_ID) continue;
|
|
113
|
-
list.push(display);
|
|
114
|
-
}
|
|
115
|
-
return list.map(withBuiltinDefaults);
|
|
116
|
-
})();
|
|
108
|
+
const resolvedDisplays: TableViewDisplayConfig[] = (
|
|
109
|
+
config.displays ?? [{ id: TABLE_DISPLAY_ID }]
|
|
110
|
+
).map(withBuiltinDefaults);
|
|
117
111
|
|
|
118
|
-
const defaultDisplay =
|
|
112
|
+
const defaultDisplay =
|
|
113
|
+
config.defaultDisplay ?? resolvedDisplays[0]?.id ?? TABLE_DISPLAY_ID;
|
|
119
114
|
|
|
120
115
|
const initialVisibility = buildInitialColumnVisibility(config.columns ?? []);
|
|
121
116
|
|
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { namesRowField } from "#dms-core/app/utils/url-interpolation";
|
|
1
2
|
import {
|
|
2
3
|
fillFormPageUrl,
|
|
3
4
|
type FormContainer,
|
|
@@ -91,7 +92,6 @@ interface BulkActionConfig {
|
|
|
91
92
|
/** Runs a confirmed action from inside its dialog (see `onConfirm`). */
|
|
92
93
|
type ConfirmedRun = NonNullable<ConfirmOptions["onConfirm"]>;
|
|
93
94
|
|
|
94
|
-
const ROW_URL_PLACEHOLDER = /\{[^}]+\}/;
|
|
95
95
|
/** Options of a delete: from the archive, it is a permanent one. */
|
|
96
96
|
interface DeleteRowsOptions {
|
|
97
97
|
permanently?: boolean;
|
|
@@ -634,7 +634,7 @@ export const useTableRowActions = <T extends Data>(
|
|
|
634
634
|
* reaches: a `from` dialog is worded for one row, so several rows keep the
|
|
635
635
|
* generic confirmation. Undefined when none applies.
|
|
636
636
|
*/
|
|
637
|
-
// A `from` URL naming a row (`{id}`, `{_id}`…) is worded for that row
|
|
637
|
+
// A `from` URL naming a row (`{{row.id}}`, `{{row._id}}`…) is worded for that row
|
|
638
638
|
// alone; one naming none receives the rows as `?ids=`, one or several, like
|
|
639
639
|
// the `from` of a bulk custom action.
|
|
640
640
|
const declaredBulkConfirm = (
|
|
@@ -643,7 +643,7 @@ export const useTableRowActions = <T extends Data>(
|
|
|
643
643
|
): ActionConfirm | undefined => {
|
|
644
644
|
const declared = normalizeActionConfig(actionConfig).confirm;
|
|
645
645
|
if (!declared || !isConfirmFrom(declared)) return declared;
|
|
646
|
-
if (
|
|
646
|
+
if (namesRowField(declared.from)) {
|
|
647
647
|
return ids.length === 1 ? declared : undefined;
|
|
648
648
|
}
|
|
649
649
|
return {
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// bare `<a href="#…">` (an Inertia visit would reload the page to scroll), an
|
|
4
4
|
// absolute URL opens in a new tab, and everything else is a DMS route.
|
|
5
5
|
|
|
6
|
+
import { pageLink } from "../composables/page/pageUrlValues";
|
|
7
|
+
|
|
6
8
|
export type LinkKind = "internal" | "anchor" | "external";
|
|
7
9
|
|
|
8
10
|
const EXTERNAL_LINK_PATTERN = /^(?:[a-z][a-z\d+.-]*:|\/\/)/i;
|
|
@@ -20,12 +22,14 @@ export const EXTERNAL_LINK_ATTRIBUTES = {
|
|
|
20
22
|
} as const;
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
|
-
* Props for a `UButton` (or `ULink`) following `to
|
|
24
|
-
*
|
|
25
|
+
* Props for a `UButton` (or `ULink`) following `to`, its page tokens
|
|
26
|
+
* (`{{params.X}}`, `{{query.X}}`) filled: an external target opens in a new
|
|
27
|
+
* tab, the others navigate in place.
|
|
25
28
|
*/
|
|
26
29
|
export function buttonLinkProps(
|
|
27
|
-
|
|
30
|
+
declared: string | undefined,
|
|
28
31
|
): Record<string, string> {
|
|
32
|
+
const to = pageLink(declared);
|
|
29
33
|
if (!to) return {};
|
|
30
34
|
return linkKind(to) === "external"
|
|
31
35
|
? { to, ...EXTERNAL_LINK_ATTRIBUTES }
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
import { get } from "@nuxt/ui/runtime/utils/index.js";
|
|
2
|
+
|
|
3
|
+
// Every token is `{{<scope>.<key>}}`, spaces inside the braces allowed, as the
|
|
4
|
+
// server's filter tokens (`{{ user.id }}`) are.
|
|
5
|
+
//
|
|
1
6
|
// `{{params.id}}` takes the bare name, which on a route repeating a placeholder
|
|
2
7
|
// is its last occurrence — the row id of a form page. The `:<n>` suffix reaches
|
|
3
8
|
// a specific occurrence, `{{params.id:1}}` being the id of the page carrying the
|
|
4
9
|
// table view (see extractRouteParams).
|
|
5
|
-
const
|
|
10
|
+
const PARAM_KEY = String.raw`params\.(\w+(?::\d+)?)`;
|
|
6
11
|
// A query key may carry dots and dashes: a period selector writes `<id>.period`.
|
|
7
|
-
const
|
|
8
|
-
|
|
12
|
+
const QUERY_KEY = String.raw`query\.([\w.-]+)`;
|
|
13
|
+
// A response key is a path into the answered body: `{{response.data.id}}`.
|
|
14
|
+
const RESPONSE_KEY = String.raw`response\.([\w.-]+)`;
|
|
15
|
+
|
|
16
|
+
const tokenPattern = (key: string, flags = "g"): RegExp =>
|
|
17
|
+
new RegExp(String.raw`\{\{\s*${key}\s*\}\}`, flags);
|
|
18
|
+
|
|
19
|
+
const PARAM_TOKEN = tokenPattern(PARAM_KEY);
|
|
20
|
+
const QUERY_TOKEN = tokenPattern(QUERY_KEY);
|
|
21
|
+
const RESPONSE_TOKEN = tokenPattern(RESPONSE_KEY);
|
|
9
22
|
const TOKEN_OPENING = "{{";
|
|
10
23
|
|
|
11
24
|
export interface ReplaceUrlVariablesContext {
|
|
@@ -63,8 +76,8 @@ function fillResponseTokens(
|
|
|
63
76
|
response: Record<string, unknown> | undefined,
|
|
64
77
|
): string {
|
|
65
78
|
if (!response) return text;
|
|
66
|
-
return text.replace(RESPONSE_TOKEN, (match, key) => {
|
|
67
|
-
const value = response
|
|
79
|
+
return text.replace(RESPONSE_TOKEN, (match, key: string) => {
|
|
80
|
+
const value: unknown = get(response, key);
|
|
68
81
|
return typeof value === "string" || typeof value === "number"
|
|
69
82
|
? String(value)
|
|
70
83
|
: match;
|
|
@@ -122,10 +135,20 @@ export function resolveTextVariables(
|
|
|
122
135
|
|
|
123
136
|
// A route token left without a value: `{{params.id}}`, `{{query.workspace}}`,
|
|
124
137
|
// `{{query.sales.period}}` (a query key may carry dots and dashes, as above).
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
const UNRESOLVED_ROUTE_VALUE =
|
|
128
|
-
|
|
138
|
+
const ROUTE_KEY = `(?:${PARAM_KEY}|${QUERY_KEY})`;
|
|
139
|
+
const UNRESOLVED_ROUTE_TOKEN = tokenPattern(ROUTE_KEY);
|
|
140
|
+
const UNRESOLVED_ROUTE_VALUE = new RegExp(
|
|
141
|
+
`^${tokenPattern(ROUTE_KEY, "").source}$`,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Whether a text names a value of the page (`{{params.X}}`, `{{query.X}}`):
|
|
146
|
+
* the route is only read for one that does.
|
|
147
|
+
*/
|
|
148
|
+
export function namesPageValue(text: string): boolean {
|
|
149
|
+
return new RegExp(UNRESOLVED_ROUTE_TOKEN.source).test(text);
|
|
150
|
+
}
|
|
151
|
+
|
|
129
152
|
const QUERY_SEPARATOR = "?";
|
|
130
153
|
const FRAGMENT_SEPARATOR = "#";
|
|
131
154
|
const PAIR_SEPARATOR = "&";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { pageLink } from "../../build/composables/page/pageUrlValues";
|
|
2
3
|
import { computed, ref, resolveComponent } from "vue";
|
|
3
4
|
import DmsActivityItem from "../activity/ActivityItem.vue";
|
|
4
5
|
import DmsBlockActions, {
|
|
@@ -236,6 +237,11 @@ const scrollArea = computed(() =>
|
|
|
236
237
|
const dayHeadClass = computed(
|
|
237
238
|
() => props.fillHeight && "sticky top-0 z-[1] bg-(--dms-surface-card)",
|
|
238
239
|
);
|
|
240
|
+
// …and the "Show N more" button at its foot, where entries taller than the
|
|
241
|
+
// row would otherwise push it out of sight.
|
|
242
|
+
const foldClass = computed(
|
|
243
|
+
() => props.fillHeight && "sticky bottom-0 z-[1] bg-(--dms-surface-card)",
|
|
244
|
+
);
|
|
239
245
|
|
|
240
246
|
const Wrapper = props.card ? resolveComponent("DmsCard") : "div";
|
|
241
247
|
const wrapperProps = computed(() =>
|
|
@@ -365,13 +371,14 @@ const wrapperProps = computed(() =>
|
|
|
365
371
|
:unread="item.unread"
|
|
366
372
|
:mono="props.mono"
|
|
367
373
|
:wrap="props.wrap"
|
|
368
|
-
:to="item.to"
|
|
374
|
+
:to="pageLink(item.to)"
|
|
369
375
|
:interactive="!!item.to"
|
|
370
376
|
/>
|
|
371
377
|
</section>
|
|
372
378
|
<div
|
|
373
379
|
v-if="foldedCount > 0"
|
|
374
380
|
class="dms-activity-feed__fold border-t border-(--ui-border-muted) px-[18px] pt-2 pb-1"
|
|
381
|
+
:class="foldClass"
|
|
375
382
|
>
|
|
376
383
|
<UButton
|
|
377
384
|
:label="foldLabel"
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from "vue";
|
|
3
3
|
import DmsIconWell, { type IconWellSize } from "../icon-well/IconWell.vue";
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
BANNER_TONE_ICONS,
|
|
6
|
+
type BannerTone,
|
|
7
|
+
} from "../../utils/bannerToneIcons";
|
|
5
8
|
|
|
6
9
|
// DMS-styled notice banner (design .banner): a left-to-right tinted wash over
|
|
7
10
|
// the card surface with a boxed leading icon, used for module beta / info
|
|
8
11
|
// notices. Wraps UAlert so we keep its a11y + slots while pinning the exact
|
|
9
12
|
// reference look. The `cli` slot hosts the optional inset command box
|
|
10
13
|
// (compose with DmsCopyButton), `actions` the right-aligned buttons.
|
|
11
|
-
type BannerTone = Exclude<SemanticColor, "neutral">;
|
|
12
14
|
type BannerSize = "sm" | "md";
|
|
13
15
|
|
|
14
16
|
interface BannerProps {
|
|
15
17
|
title?: string;
|
|
16
18
|
description?: string;
|
|
19
|
+
/** Defaults to the tone's icon. */
|
|
17
20
|
icon?: string;
|
|
18
21
|
/** Colour of the wash and the icon well. */
|
|
19
22
|
tone?: BannerTone;
|
|
@@ -39,7 +42,7 @@ interface BannerSlots {
|
|
|
39
42
|
const props = withDefaults(defineProps<BannerProps>(), {
|
|
40
43
|
title: undefined,
|
|
41
44
|
description: undefined,
|
|
42
|
-
icon:
|
|
45
|
+
icon: undefined,
|
|
43
46
|
tone: "warning",
|
|
44
47
|
size: "md",
|
|
45
48
|
dismissible: false,
|
|
@@ -99,6 +102,8 @@ const alertUi = computed(() => ({
|
|
|
99
102
|
actions: `items-center gap-2 mt-0 ${slots.actions ? "max-sm:basis-full max-sm:flex-wrap" : ""}`,
|
|
100
103
|
}));
|
|
101
104
|
|
|
105
|
+
const leadingIcon = computed(() => props.icon ?? BANNER_TONE_ICONS[props.tone]);
|
|
106
|
+
|
|
102
107
|
function onOpenChange(open: boolean) {
|
|
103
108
|
if (!open) emit("dismiss");
|
|
104
109
|
}
|
|
@@ -116,7 +121,7 @@ function onOpenChange(open: boolean) {
|
|
|
116
121
|
@update:open="onOpenChange"
|
|
117
122
|
>
|
|
118
123
|
<template #leading>
|
|
119
|
-
<DmsIconWell :icon="
|
|
124
|
+
<DmsIconWell :icon="leadingIcon" :tone="tone" :size="SIZE_WELL[size]" />
|
|
120
125
|
</template>
|
|
121
126
|
|
|
122
127
|
<template v-if="slots.description || slots.cli" #description>
|