@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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { APEX_TYPE_CONFIGS } from "./apexTypeConfigs";
|
|
2
|
+
import type { ChartSeries } from "./types";
|
|
3
|
+
import type { UseApexChartInput } from "./useApexChart.types";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_Y_RANGE_PADDING = 0.08;
|
|
6
|
+
const ZERO_BOUND_MAGNITUDE = 1;
|
|
7
|
+
const STEP_DIVISIONS = 10;
|
|
8
|
+
const NICE_STEP_FACTORS = [1, 2, 5, 10];
|
|
9
|
+
const DECIMAL_BASE = 10;
|
|
10
|
+
/** Enough digits to keep every real value while dropping binary float noise. */
|
|
11
|
+
const SIGNIFICANT_DIGITS = 12;
|
|
12
|
+
|
|
13
|
+
const DOWNWARD = -1;
|
|
14
|
+
const UPWARD = 1;
|
|
15
|
+
|
|
16
|
+
type Rounding = (value: number) => number;
|
|
17
|
+
|
|
18
|
+
/** Bounds Apex resolves from the extent it measured on the drawn series. */
|
|
19
|
+
interface AutoYBounds {
|
|
20
|
+
min?: (nativeMin: number) => number;
|
|
21
|
+
max?: (nativeMax: number) => number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
25
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function withoutFloatNoise(value: number): number {
|
|
29
|
+
return Number(value.toPrecision(SIGNIFICANT_DIGITS));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function pointValues(point: ChartSeries["data"][number]): unknown[] {
|
|
33
|
+
if (typeof point === "number") return [point];
|
|
34
|
+
if (!point || typeof point !== "object") return [];
|
|
35
|
+
return Array.isArray(point.y) ? point.y : [point.y];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function seriesValues(series: ChartSeries[]): number[] {
|
|
39
|
+
return series.flatMap((entry) =>
|
|
40
|
+
entry.data.flatMap(pointValues).filter(isFiniteNumber),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function annotationValues(input: UseApexChartInput): number[] {
|
|
45
|
+
return (input.annotations ?? [])
|
|
46
|
+
.flatMap((annotation) => [annotation.y, annotation.y2])
|
|
47
|
+
.filter(isFiniteNumber);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolvePadding(input: UseApexChartInput): number {
|
|
51
|
+
const requested = input.autoYRange?.padding ?? DEFAULT_Y_RANGE_PADDING;
|
|
52
|
+
return Number.isFinite(requested) && requested >= 0
|
|
53
|
+
? requested
|
|
54
|
+
: DEFAULT_Y_RANGE_PADDING;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function padOutward(bound: number, padding: number, direction: number): number {
|
|
58
|
+
return bound + direction * (Math.abs(bound) || ZERO_BOUND_MAGNITUDE) * padding;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The smallest 1, 2 or 5 times a power of ten that covers `span`. */
|
|
62
|
+
export function niceStep(span: number): number {
|
|
63
|
+
const magnitude = DECIMAL_BASE ** Math.floor(Math.log10(span));
|
|
64
|
+
const factor =
|
|
65
|
+
NICE_STEP_FACTORS.find((candidate) => candidate * magnitude >= span) ??
|
|
66
|
+
DECIMAL_BASE;
|
|
67
|
+
return withoutFloatNoise(factor * magnitude);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function snapToStep(value: number, step: number, round: Rounding): number {
|
|
71
|
+
return withoutFloatNoise(round(withoutFloatNoise(value / step)) * step);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The tick step for the padded extent of what is known up front: the series
|
|
76
|
+
* and the annotations. Apex only lays round ticks between bounds that are
|
|
77
|
+
* multiples of a round step; padded raw bounds give 603.6, 437.2…
|
|
78
|
+
*/
|
|
79
|
+
function estimateStep(values: number[], padding: number): number | undefined {
|
|
80
|
+
if (values.length === 0) return undefined;
|
|
81
|
+
const lower = padOutward(Math.min(...values), padding, DOWNWARD);
|
|
82
|
+
const upper = padOutward(Math.max(...values), padding, UPWARD);
|
|
83
|
+
const span = upper - lower;
|
|
84
|
+
return span > 0 ? niceStep(span / STEP_DIVISIONS) : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stepForBound(bound: number, knownStep: number | undefined): number {
|
|
88
|
+
if (knownStep !== undefined) return knownStep;
|
|
89
|
+
return niceStep((Math.abs(bound) || ZERO_BOUND_MAGNITUDE) / STEP_DIVISIONS);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Y bounds that take in the annotations, padded outward and rounded to a round
|
|
94
|
+
* step so the axis ticks read 100, 200… rather than the raw padded extent.
|
|
95
|
+
*/
|
|
96
|
+
export function buildAutoYRange(input: UseApexChartInput): AutoYBounds {
|
|
97
|
+
if (!input.autoYRange || APEX_TYPE_CONFIGS[input.type].isCircular) return {};
|
|
98
|
+
const padding = resolvePadding(input);
|
|
99
|
+
const annotations = annotationValues(input);
|
|
100
|
+
const knownStep = estimateStep(
|
|
101
|
+
[...seriesValues(input.series), ...annotations],
|
|
102
|
+
padding,
|
|
103
|
+
);
|
|
104
|
+
const bound = (native: number, direction: number, round: Rounding) => {
|
|
105
|
+
const extent = [native, ...annotations];
|
|
106
|
+
const raw = direction === DOWNWARD ? Math.min(...extent) : Math.max(...extent);
|
|
107
|
+
const padded = padOutward(raw, padding, direction);
|
|
108
|
+
return snapToStep(padded, stepForBound(padded, knownStep), round);
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
min: (nativeMin) => bound(nativeMin, DOWNWARD, Math.floor),
|
|
112
|
+
max: (nativeMax) => bound(nativeMax, UPWARD, Math.ceil),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Axis labels without a format of their own. Apex writes them with as many
|
|
118
|
+
* decimals as the most precise data point, so 160 reads "160.00000000000000"
|
|
119
|
+
* once one price carries float noise.
|
|
120
|
+
*/
|
|
121
|
+
export function formatPlainAxisValue(value: number): string {
|
|
122
|
+
return isFiniteNumber(value) ? String(withoutFloatNoise(value)) : String(value);
|
|
123
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { APEX_TYPE_CONFIGS } from "./apexTypeConfigs";
|
|
3
3
|
import { buildAnnotations } from "./apexAnnotations";
|
|
4
|
-
import { buildPlotOptions } from "./apexPlotOptions";
|
|
4
|
+
import { buildPlotOptions, ringFitsSliceLabels } from "./apexPlotOptions";
|
|
5
|
+
import { buildAutoYRange, formatPlainAxisValue } from "./apexYRange";
|
|
5
6
|
import { isReducedMotionActive } from "../../build/utils/accessibilityPreferences";
|
|
6
7
|
import {
|
|
7
8
|
axisLabelStyle,
|
|
8
9
|
buildDataLabels,
|
|
10
|
+
dataLabelOffset,
|
|
9
11
|
buildGrid,
|
|
10
12
|
buildLegend,
|
|
11
13
|
buildMarkers,
|
|
@@ -286,36 +288,6 @@ function applySeriesStroke(
|
|
|
286
288
|
|
|
287
289
|
const HIGH_CONTRAST_YAXIS_TYPES: ChartType[] = ["radar"];
|
|
288
290
|
|
|
289
|
-
const DEFAULT_Y_RANGE_PADDING = 0.08;
|
|
290
|
-
const ZERO_BOUND_MAGNITUDE = 1;
|
|
291
|
-
|
|
292
|
-
function buildAutoYRange(input: UseApexChartInput): Record<string, unknown> {
|
|
293
|
-
if (!input.autoYRange || APEX_TYPE_CONFIGS[input.type].isCircular) return {};
|
|
294
|
-
const requested = input.autoYRange.padding ?? DEFAULT_Y_RANGE_PADDING;
|
|
295
|
-
const padding =
|
|
296
|
-
Number.isFinite(requested) && requested >= 0
|
|
297
|
-
? requested
|
|
298
|
-
: DEFAULT_Y_RANGE_PADDING;
|
|
299
|
-
const bounds = (input.annotations ?? [])
|
|
300
|
-
.flatMap((annotation) => [annotation.y, annotation.y2])
|
|
301
|
-
.filter(
|
|
302
|
-
(value): value is number =>
|
|
303
|
-
typeof value === "number" && Number.isFinite(value),
|
|
304
|
-
);
|
|
305
|
-
const lower = bounds.reduce((min, value) => Math.min(min, value), Infinity);
|
|
306
|
-
const upper = bounds.reduce((max, value) => Math.max(max, value), -Infinity);
|
|
307
|
-
return {
|
|
308
|
-
min: (nativeMin: number) => {
|
|
309
|
-
const min = Math.min(nativeMin, lower);
|
|
310
|
-
return min - (Math.abs(min) || ZERO_BOUND_MAGNITUDE) * padding;
|
|
311
|
-
},
|
|
312
|
-
max: (nativeMax: number) => {
|
|
313
|
-
const max = Math.max(nativeMax, upper);
|
|
314
|
-
return max + (Math.abs(max) || ZERO_BOUND_MAGNITUDE) * padding;
|
|
315
|
-
},
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
291
|
function buildYAxis(
|
|
320
292
|
input: UseApexChartInput,
|
|
321
293
|
theme: ApexThemeColors,
|
|
@@ -331,9 +303,8 @@ function buildYAxis(
|
|
|
331
303
|
yaxis.min = input.yRange.min;
|
|
332
304
|
yaxis.max = input.yRange.max;
|
|
333
305
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
306
|
+
(yaxis.labels as Record<string, unknown>).formatter =
|
|
307
|
+
input.yAxisFormatter ?? formatPlainAxisValue;
|
|
337
308
|
return yaxis;
|
|
338
309
|
}
|
|
339
310
|
|
|
@@ -387,20 +358,50 @@ function applyMixedSeriesDefs(
|
|
|
387
358
|
});
|
|
388
359
|
}
|
|
389
360
|
|
|
361
|
+
const RAW_OPTION_PATH_SEPARATOR = ".";
|
|
362
|
+
|
|
363
|
+
function parseRawOptionValue(value: string): unknown {
|
|
364
|
+
try {
|
|
365
|
+
return JSON.parse(value);
|
|
366
|
+
} catch {
|
|
367
|
+
return value;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function isOptionSection(value: unknown): value is Record<string, unknown> {
|
|
372
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Set `value` at a dotted path ("markers.size"), copying each section on the
|
|
377
|
+
* way so the generated options are not mutated. A key without a dot replaces
|
|
378
|
+
* its whole section, as it always has.
|
|
379
|
+
*/
|
|
380
|
+
function setOptionPath(
|
|
381
|
+
options: Record<string, unknown>,
|
|
382
|
+
path: string[],
|
|
383
|
+
value: unknown,
|
|
384
|
+
): Record<string, unknown> {
|
|
385
|
+
const [head, ...rest] = path;
|
|
386
|
+
if (head === undefined) return options;
|
|
387
|
+
if (rest.length === 0) return { ...options, [head]: value };
|
|
388
|
+
const section = isOptionSection(options[head]) ? options[head] : {};
|
|
389
|
+
return { ...options, [head]: setOptionPath(section, rest, value) };
|
|
390
|
+
}
|
|
391
|
+
|
|
390
392
|
function injectRawOptions(
|
|
391
393
|
options: Record<string, unknown>,
|
|
392
394
|
rawOptions: Array<{ key: string; value: string }> | undefined,
|
|
393
395
|
): Record<string, unknown> {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
return merged;
|
|
396
|
+
return (rawOptions ?? []).reduce(
|
|
397
|
+
(merged, entry) =>
|
|
398
|
+
setOptionPath(
|
|
399
|
+
merged,
|
|
400
|
+
entry.key.split(RAW_OPTION_PATH_SEPARATOR),
|
|
401
|
+
parseRawOptionValue(entry.value),
|
|
402
|
+
),
|
|
403
|
+
options,
|
|
404
|
+
);
|
|
404
405
|
}
|
|
405
406
|
|
|
406
407
|
const RANGE_AREA_APEX_TYPE = "rangeArea";
|
|
@@ -465,8 +466,11 @@ function buildBaseOptions(
|
|
|
465
466
|
chart: buildChartConfig(input, theme.muted),
|
|
466
467
|
colors: seriesColors,
|
|
467
468
|
dataLabels: buildDataLabels(
|
|
468
|
-
config.supportsDataLabels || input.showLabels === true
|
|
469
|
+
(config.supportsDataLabels || input.showLabels === true) &&
|
|
470
|
+
ringFitsSliceLabels(input),
|
|
469
471
|
theme,
|
|
472
|
+
config.isCircular,
|
|
473
|
+
dataLabelOffset(input),
|
|
470
474
|
),
|
|
471
475
|
stroke: buildStroke(
|
|
472
476
|
input,
|
|
@@ -600,10 +604,6 @@ function applyOptionalSections(
|
|
|
600
604
|
if (isCircular) {
|
|
601
605
|
baseOptions.labels = buildCircularLabels(input.donutData).labels;
|
|
602
606
|
}
|
|
603
|
-
|
|
604
|
-
if (input.type === "scatter" && input.pointSize !== undefined) {
|
|
605
|
-
baseOptions.markers = { size: input.pointSize };
|
|
606
|
-
}
|
|
607
607
|
}
|
|
608
608
|
|
|
609
609
|
function projectSeries(
|
|
@@ -11,6 +11,11 @@ const SHADE_TOKEN =
|
|
|
11
11
|
const CSS_VARIABLE = /^--[\w-]+$/;
|
|
12
12
|
const CSS_VARIABLE_FUNCTION = /^var\((--[\w-]+)\)$/;
|
|
13
13
|
const COLOR_CHANNEL_MAX = 255;
|
|
14
|
+
const PERCENT_SCALE = 100;
|
|
15
|
+
const PERCENT_SUFFIX = "%";
|
|
16
|
+
/** CSS Color 4 `rgb(R G B / A)`, which Apex cannot parse: it splits on commas. */
|
|
17
|
+
const SPACE_SEPARATED_RGB =
|
|
18
|
+
/^rgba?\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*(?:\/\s*([\d.]+%?)\s*)?\)$/i;
|
|
14
19
|
const DEFAULT_PALETTE = [
|
|
15
20
|
"--dms-chart-1",
|
|
16
21
|
"--dms-chart-2",
|
|
@@ -34,6 +39,20 @@ function isHexLike(value: string): boolean {
|
|
|
34
39
|
return HEX_PREFIXES.some((prefix) => value.startsWith(prefix));
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
function alphaValue(alpha: string): number {
|
|
43
|
+
if (!alpha.endsWith(PERCENT_SUFFIX)) return Number(alpha);
|
|
44
|
+
return Number(alpha.slice(0, -PERCENT_SUFFIX.length)) / PERCENT_SCALE;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Rewrite a space-separated `rgb()` in the comma form Apex reads. */
|
|
48
|
+
function toCommaSeparatedRgb(color: string): string {
|
|
49
|
+
const match = SPACE_SEPARATED_RGB.exec(color);
|
|
50
|
+
if (!match) return color;
|
|
51
|
+
const [, red, green, blue, alpha] = match;
|
|
52
|
+
if (alpha === undefined) return `rgb(${red}, ${green}, ${blue})`;
|
|
53
|
+
return `rgba(${red}, ${green}, ${blue}, ${alphaValue(alpha)})`;
|
|
54
|
+
}
|
|
55
|
+
|
|
37
56
|
const colorProbeCache = new Map<string, string>();
|
|
38
57
|
|
|
39
58
|
/** Invalidate resolved colors before consumers recompute for a theme change. */
|
|
@@ -105,7 +124,7 @@ function colorVariable(value: string): string | undefined {
|
|
|
105
124
|
/** Resolve semantic colors, shade tokens and global CSS variables for Apex. */
|
|
106
125
|
export function resolveChartColor(value: string | undefined): string {
|
|
107
126
|
const colorName = value ?? DEFAULT_COLOR_NAME;
|
|
108
|
-
if (isHexLike(colorName)) return colorName;
|
|
127
|
+
if (isHexLike(colorName)) return toCommaSeparatedRgb(colorName);
|
|
109
128
|
const variable = colorVariable(colorName);
|
|
110
129
|
if (!variable) return colorName;
|
|
111
130
|
return readCssVariable(variable, SAFE_HEX_FALLBACK);
|
|
@@ -76,7 +76,7 @@ export interface ConfirmOptions
|
|
|
76
76
|
*/
|
|
77
77
|
export interface ConfirmFromOptions {
|
|
78
78
|
/**
|
|
79
|
-
* Fill-ins of the URL's `{
|
|
79
|
+
* Fill-ins of the URL's `{{row.X}}` tokens and of the dialog's texts (a
|
|
80
80
|
* record's fields); the dialog's own `params` win in its texts.
|
|
81
81
|
*/
|
|
82
82
|
params?: Record<string, unknown>;
|
|
@@ -59,4 +59,6 @@ export interface FormProps extends FormComponentProps {
|
|
|
59
59
|
redirectOnSuccess?: string;
|
|
60
60
|
/** Field of the loaded row a page form names in the breadcrumb. */
|
|
61
61
|
labelKey?: string;
|
|
62
|
+
/** Without the record `fetchUrl` names, "No entry selected", no fields. */
|
|
63
|
+
requiresRecord?: boolean;
|
|
62
64
|
}
|
|
@@ -9,10 +9,11 @@ import type { FormField, FormFieldOrGroup } from "./types/field";
|
|
|
9
9
|
import { isFieldGroup } from "./types/field";
|
|
10
10
|
import { resolveResponseToast } from "../../build/utils/responseWarning";
|
|
11
11
|
import { processFieldI18n } from "../../build/utils/fieldOptionsI18n";
|
|
12
|
+
import { orderFormEntries } from "../../build/composables/form/formEntryOrder";
|
|
12
13
|
import {
|
|
13
14
|
type ReplaceUrlVariablesContext,
|
|
14
15
|
hasUrlVariables,
|
|
15
|
-
|
|
16
|
+
resolveLinkVariables,
|
|
16
17
|
resolveTextVariables,
|
|
17
18
|
resolveUrlVariables,
|
|
18
19
|
} from "../../build/utils/urlVariables";
|
|
@@ -90,16 +91,17 @@ export function cloneFormValue(
|
|
|
90
91
|
|
|
91
92
|
/**
|
|
92
93
|
* Whether a form offers to save: once it has an address and a field someone
|
|
93
|
-
* can fill in, unless its `saveMode` is `none`.
|
|
94
|
+
* can fill in, unless its `saveMode` is `none`. An `action` form does
|
|
95
|
+
* something even with nothing to fill in, so it needs only the address.
|
|
94
96
|
*/
|
|
95
97
|
export function formShowsActions(
|
|
96
|
-
options: Pick<FormProps, "saveMode" | "submitUrl">,
|
|
98
|
+
options: Pick<FormProps, "saveMode" | "submitUrl" | "kind">,
|
|
97
99
|
fields: ReadonlyArray<{ disabled?: boolean }>,
|
|
98
100
|
): boolean {
|
|
99
101
|
return (
|
|
100
102
|
options.saveMode !== "none" &&
|
|
101
103
|
!!options.submitUrl &&
|
|
102
|
-
!fields.every((field) => field.disabled)
|
|
104
|
+
(options.kind === "action" || !fields.every((field) => field.disabled))
|
|
103
105
|
);
|
|
104
106
|
}
|
|
105
107
|
|
|
@@ -573,7 +575,7 @@ export const useForm = (props: FormProps, options: UseFormOptions = {}) => {
|
|
|
573
575
|
);
|
|
574
576
|
|
|
575
577
|
const fields = computed<FormFieldOrGroup[]>(() =>
|
|
576
|
-
props.fields.map((item) => {
|
|
578
|
+
orderFormEntries(props.fields).map((item) => {
|
|
577
579
|
if (isFieldGroup(item)) {
|
|
578
580
|
return {
|
|
579
581
|
...item,
|
|
@@ -770,7 +772,7 @@ export const useForm = (props: FormProps, options: UseFormOptions = {}) => {
|
|
|
770
772
|
showSubmitNotice(response);
|
|
771
773
|
props.onSuccessCallback?.(response, plainData);
|
|
772
774
|
if (props.redirectOnSuccess) {
|
|
773
|
-
const target =
|
|
775
|
+
const target = resolveLinkVariables(
|
|
774
776
|
props.redirectOnSuccess,
|
|
775
777
|
buildUrlContext(response as Record<string, unknown> | undefined),
|
|
776
778
|
);
|
|
@@ -31,7 +31,7 @@ export interface SidePanelTargetOptions {
|
|
|
31
31
|
*/
|
|
32
32
|
export interface DownloadActionTarget {
|
|
33
33
|
type: "download";
|
|
34
|
-
/** Takes the `{field}` and `{{params.X}}` / `{{query.X}}` tokens. */
|
|
34
|
+
/** Takes the `{{row.field}}` and `{{params.X}}` / `{{query.X}}` tokens. */
|
|
35
35
|
url: string;
|
|
36
36
|
/** Saved under; the response's `Content-Disposition` name, else the URL's. */
|
|
37
37
|
filename?: string;
|
|
@@ -224,7 +224,7 @@ export interface TableViewSourceCapabilities {
|
|
|
224
224
|
/** Where a `TableView.fromSource` table reads its rows (backend `source`). */
|
|
225
225
|
export interface TableViewSourceConfig {
|
|
226
226
|
fetchUrl: string;
|
|
227
|
-
/** Route answering one row, `{id}` its id (backend `fetchRowUrl`). */
|
|
227
|
+
/** Route answering one row, `{{row.id}}` its id (backend `fetchRowUrl`). */
|
|
228
228
|
fetchRowUrl?: string;
|
|
229
229
|
capabilities: TableViewSourceCapabilities;
|
|
230
230
|
/** Id of the `PeriodSelector` whose period is sent to `fetchUrl`. */
|
|
@@ -294,11 +294,9 @@ export interface TableViewConfig<T extends Data>
|
|
|
294
294
|
routeParamFilters?: RouteParamFilters;
|
|
295
295
|
routeParams?: Record<string, string>;
|
|
296
296
|
tabs?: TableViewTab[];
|
|
297
|
-
/** Displays this table view offers,
|
|
297
|
+
/** Displays this table view offers, in order; the built-in `table` alone when left out. */
|
|
298
298
|
displays?: TableViewDisplayConfig[];
|
|
299
|
-
/**
|
|
300
|
-
tableDisplay?: boolean;
|
|
301
|
-
/** Display shown by default when the user has no saved preference. Defaults to "table". */
|
|
299
|
+
/** Display shown by default when the user has no saved preference. Defaults to the first display. */
|
|
302
300
|
defaultDisplay?: string;
|
|
303
301
|
/** Expandable rows: caret column + detail band. */
|
|
304
302
|
expandable?: TableViewExpandableConfig;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SemanticColor } from "./semanticTint";
|
|
2
|
+
|
|
3
|
+
/** The tones a banner washes with. */
|
|
4
|
+
export type BannerTone = Exclude<SemanticColor, "neutral">;
|
|
5
|
+
|
|
6
|
+
/** The icon a banner shows for its tone when it is given none. */
|
|
7
|
+
export const BANNER_TONE_ICONS: Record<BannerTone, string> = {
|
|
8
|
+
info: "i-ph-info",
|
|
9
|
+
success: "i-ph-check-circle",
|
|
10
|
+
warning: "i-ph-warning",
|
|
11
|
+
error: "i-ph-warning-circle",
|
|
12
|
+
primary: "i-ph-sparkle",
|
|
13
|
+
};
|
|
@@ -201,3 +201,29 @@ export function buildCascaderPathLabels(
|
|
|
201
201
|
}
|
|
202
202
|
return labels;
|
|
203
203
|
}
|
|
204
|
+
|
|
205
|
+
function asPickedRow(item: unknown): CascaderRow | undefined {
|
|
206
|
+
return typeof item === "object" && item !== null
|
|
207
|
+
? (item as CascaderRow)
|
|
208
|
+
: undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The label path of a picked value — an id, or a row carrying its id and its
|
|
213
|
+
* own label — resolved against the flat list of rows once it is there; its
|
|
214
|
+
* own label, or its id, until then or when the list does not hold it.
|
|
215
|
+
*/
|
|
216
|
+
export function cascaderValuePath(
|
|
217
|
+
item: unknown,
|
|
218
|
+
keyMapping: CascaderKeyMapping,
|
|
219
|
+
rows?: CascaderRow[],
|
|
220
|
+
): string[] {
|
|
221
|
+
const row = asPickedRow(item);
|
|
222
|
+
const ownLabel = row ? rowValue(row, keyMapping.label) : undefined;
|
|
223
|
+
const id = row
|
|
224
|
+
? rowValue(row, keyMapping.value)
|
|
225
|
+
: rowValue({ id: item }, "id");
|
|
226
|
+
if (!id) return ownLabel ? [ownLabel] : [];
|
|
227
|
+
const labels = rows ? buildCascaderPathLabels(rows, keyMapping, id) : [];
|
|
228
|
+
return labels.length > 0 ? labels : [ownLabel ?? id];
|
|
229
|
+
}
|
|
@@ -284,6 +284,8 @@
|
|
|
284
284
|
"error_description": "Check your connection, then try again. Nothing can be saved until the entry loads.",
|
|
285
285
|
"forbidden_title": "You can't view this entry",
|
|
286
286
|
"forbidden_description": "Your role doesn't let you view this entry, so the form can't open on its current values. Ask an administrator for access.",
|
|
287
|
+
"missing_title": "No entry selected",
|
|
288
|
+
"missing_description": "This form shows one entry, and the address names none. Open it from an entry to see its values.",
|
|
287
289
|
"retry": "Try again"
|
|
288
290
|
},
|
|
289
291
|
"select_date": "Select date",
|
|
@@ -335,6 +337,8 @@
|
|
|
335
337
|
},
|
|
336
338
|
"file": {
|
|
337
339
|
"count": "{count} file(s)",
|
|
340
|
+
"drop_prompt": "Drop a file or browse",
|
|
341
|
+
"drop_prompt_multiple": "Drop files or browse",
|
|
338
342
|
"max_size": "Max size: {size}",
|
|
339
343
|
"uploaded_files": "Uploaded files",
|
|
340
344
|
"rejected_title": "File not added",
|
|
@@ -285,6 +285,8 @@
|
|
|
285
285
|
"error_description": "Vérifiez votre connexion, puis réessayez. Rien ne peut être enregistré tant que l'élément n'est pas chargé.",
|
|
286
286
|
"forbidden_title": "Vous ne pouvez pas voir cet élément",
|
|
287
287
|
"forbidden_description": "Votre rôle ne vous permet pas de voir cet élément, le formulaire ne peut donc pas s'ouvrir sur ses valeurs actuelles. Demandez l'accès à un administrateur.",
|
|
288
|
+
"missing_title": "Aucun élément sélectionné",
|
|
289
|
+
"missing_description": "Ce formulaire affiche un élément, et l'adresse n'en désigne aucun. Ouvrez-le depuis un élément pour voir ses valeurs.",
|
|
288
290
|
"retry": "Réessayer"
|
|
289
291
|
},
|
|
290
292
|
"select_date": "Sélectionner une date",
|
|
@@ -336,6 +338,8 @@
|
|
|
336
338
|
},
|
|
337
339
|
"file": {
|
|
338
340
|
"count": "{count} fichier(s)",
|
|
341
|
+
"drop_prompt": "Glisser un fichier ou parcourir",
|
|
342
|
+
"drop_prompt_multiple": "Glisser des fichiers ou parcourir",
|
|
339
343
|
"max_size": "Taille max : {size}",
|
|
340
344
|
"uploaded_files": "Fichiers téléversés",
|
|
341
345
|
"rejected_title": "Fichier non ajouté",
|
|
@@ -87,6 +87,13 @@ it("keeps each day's separator on top of the scrolled entries", async () => {
|
|
|
87
87
|
expect(html).toMatch(/<div[^>]*class="[^"]*\bsticky top-0\b[^"]*"[^>]*><h3/);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it("keeps the fold button in sight at the foot of the scrolled entries", async () => {
|
|
91
|
+
const html = await render({ fillHeight: true, visibleItems: 3 });
|
|
92
|
+
expect(html).toMatch(
|
|
93
|
+
/class="dms-activity-feed__fold[^"]*\bsticky bottom-0\b[^"]*"/,
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
90
97
|
it("names the region after the feed when it has no title", async () => {
|
|
91
98
|
const area = scrollArea(await render({ fillHeight: true, title: undefined }));
|
|
92
99
|
expect(area).toContain('aria-label="dms.activity_feed.list_label"');
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* A DmsBanner given no icon shows its tone's, as the Banner block does: a
|
|
4
|
+
* check on success, an "i" on info. An icon given wins.
|
|
5
|
+
*/
|
|
6
|
+
import { createApp, defineComponent, h, type App } from "vue";
|
|
7
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
8
|
+
|
|
9
|
+
vi.mock("../layers/dms-ui/app/components/icon-well/IconWell.vue", () => ({
|
|
10
|
+
default: defineComponent({
|
|
11
|
+
props: { icon: { type: String, default: undefined } },
|
|
12
|
+
setup: (props) => () => h("i", { "data-icon": props.icon }),
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const { default: DmsBanner } = await import(
|
|
17
|
+
"../layers/dms-ui/app/components/banner/Banner.vue"
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const UAlert = defineComponent({
|
|
21
|
+
setup:
|
|
22
|
+
(_props, { slots }) =>
|
|
23
|
+
() =>
|
|
24
|
+
h("div", [slots.leading?.()]),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
let app: App | undefined;
|
|
28
|
+
|
|
29
|
+
afterEach(() => app?.unmount());
|
|
30
|
+
|
|
31
|
+
function iconOf(props: Record<string, unknown>): string | null {
|
|
32
|
+
const host = document.createElement("div");
|
|
33
|
+
app = createApp({ render: () => h(DmsBanner, { title: "Notice", ...props }) });
|
|
34
|
+
app.component("UAlert", UAlert);
|
|
35
|
+
app.mount(host);
|
|
36
|
+
return host.querySelector("[data-icon]")?.getAttribute("data-icon") ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
it.each([
|
|
40
|
+
["warning", "i-ph-warning"],
|
|
41
|
+
["success", "i-ph-check-circle"],
|
|
42
|
+
["info", "i-ph-info"],
|
|
43
|
+
["error", "i-ph-warning-circle"],
|
|
44
|
+
["primary", "i-ph-sparkle"],
|
|
45
|
+
])("shows the %s tone's icon when given none", (tone, icon) => {
|
|
46
|
+
expect(iconOf({ tone })).toBe(icon);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("keeps the warning icon of its default tone", () => {
|
|
50
|
+
expect(iconOf({})).toBe("i-ph-warning");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("shows the icon it is given whatever the tone", () => {
|
|
54
|
+
expect(iconOf({ tone: "success", icon: "i-ph-rocket" })).toBe("i-ph-rocket");
|
|
55
|
+
});
|