@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.
Files changed (157) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/implementations/dms-auth/account-emails.d.ts +4 -0
  3. package/dist/implementations/dms-auth/account-emails.js +62 -0
  4. package/dist/implementations/dms-auth/account-emails.js.map +1 -0
  5. package/dist/implementations/dms-auth/index.d.ts +1 -3
  6. package/dist/implementations/dms-auth/index.js +15 -61
  7. package/dist/implementations/dms-auth/index.js.map +1 -1
  8. package/dist/pages/settings/users/email-change.js +3 -4
  9. package/dist/pages/settings/users/email-change.js.map +1 -1
  10. package/dist/pages/settings/users/invites.js +1 -1
  11. package/dist/pages/settings/users/invites.js.map +1 -1
  12. package/dist/pages/settings/users/members.js +1 -1
  13. package/dist/pages/settings/users/members.js.map +1 -1
  14. package/dist/pages/settings/users/notifications.js +1 -0
  15. package/dist/pages/settings/users/notifications.js.map +1 -1
  16. package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.d.ts +1 -0
  17. package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js +34 -0
  18. package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js.map +1 -0
  19. package/dist/test/unit/interfaces/dms/hook-handler-results.test.d.ts +1 -0
  20. package/dist/test/unit/interfaces/dms/hook-handler-results.test.js +49 -0
  21. package/dist/test/unit/interfaces/dms/hook-handler-results.test.js.map +1 -0
  22. package/dist/test/unit/interfaces/dms/page-topics.test.js +17 -0
  23. package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
  24. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +2 -2
  25. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -1
  26. package/dist/test/unit/interfaces/dms-base/query-response-delta.test.d.ts +1 -0
  27. package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js +29 -0
  28. package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js.map +1 -0
  29. package/dist/test/unit/interfaces/dms-base/resource-form.test.js +5 -0
  30. package/dist/test/unit/interfaces/dms-base/resource-form.test.js.map +1 -1
  31. package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js +12 -2
  32. package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js.map +1 -1
  33. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +29 -4
  34. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
  35. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +14 -11
  36. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  37. package/dist/test/unit/interfaces/dms-base/table-view-views.test.js +8 -5
  38. package/dist/test/unit/interfaces/dms-base/table-view-views.test.js.map +1 -1
  39. package/dist/test/unit/utils/email-subjects.test.d.ts +1 -0
  40. package/dist/test/unit/utils/email-subjects.test.js +27 -0
  41. package/dist/test/unit/utils/email-subjects.test.js.map +1 -0
  42. package/dist/utils/admin-invite-email.js +2 -4
  43. package/dist/utils/admin-invite-email.js.map +1 -1
  44. package/dist/utils/email-change-mail.d.ts +1 -1
  45. package/dist/utils/email-change-mail.js +11 -6
  46. package/dist/utils/email-change-mail.js.map +1 -1
  47. package/dist/utils/email-subjects.d.ts +3 -0
  48. package/dist/utils/email-subjects.js +32 -0
  49. package/dist/utils/email-subjects.js.map +1 -0
  50. package/dist/utils/export-jobs.js +15 -10
  51. package/dist/utils/export-jobs.js.map +1 -1
  52. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +29 -11
  53. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +17 -0
  54. package/frontend-vue/layers/dms-layout/app/app.config.ts +5 -0
  55. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +18 -2
  56. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +16 -1
  57. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useHeaderActionControls.ts +14 -0
  58. package/frontend-vue/layers/dms-layout/app/components/layout/HStack.vue +16 -0
  59. package/frontend-vue/layers/dms-layout/app/components/layout/VStack.vue +2 -0
  60. package/frontend-vue/layers/dms-layout/app/composables/useFooterLinks.ts +11 -1
  61. package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +1 -1
  62. package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +1 -1
  63. package/frontend-vue/layers/dms-layout/app/emails/EmailChangeNotice.vue +13 -7
  64. package/frontend-vue/layers/dms-layout/app/emails/EmailChangeVerification.vue +18 -7
  65. package/frontend-vue/layers/dms-layout/app/emails/EmailExportReady.vue +23 -12
  66. package/frontend-vue/layers/dms-layout/app/emails/EmailResetPassword.vue +18 -7
  67. package/frontend-vue/layers/dms-layout/app/emails/EmailTwoFactor.vue +23 -4
  68. package/frontend-vue/layers/dms-layout/app/emails/EmailUserValidation.vue +18 -6
  69. package/frontend-vue/layers/dms-layout/app/emails/components/EmailOTP.vue +8 -2
  70. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +40 -0
  71. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +40 -0
  72. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +2 -1
  73. package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +11 -3
  74. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +27 -36
  75. package/frontend-vue/layers/dms-ui/app/build/composables/chart/apexTheme.ts +59 -2
  76. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +27 -4
  77. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +10 -2
  78. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +0 -5
  79. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryOrder.ts +23 -0
  80. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +19 -15
  81. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +26 -2
  82. package/frontend-vue/layers/dms-ui/app/build/composables/page/pageUrlValues.ts +41 -0
  83. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +2 -2
  84. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +3 -5
  85. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +7 -12
  86. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +3 -3
  87. package/frontend-vue/layers/dms-ui/app/build/utils/link.ts +7 -3
  88. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +32 -9
  89. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +8 -1
  90. package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +9 -4
  91. package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +5 -11
  92. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +2 -1
  93. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +32 -1
  94. package/frontend-vue/layers/dms-ui/app/components/check-list/CheckList.vue +1 -1
  95. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +12 -2
  96. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +7 -3
  97. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +7 -0
  98. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +4 -1
  99. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +4 -2
  100. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +3 -2
  101. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -1
  102. package/frontend-vue/layers/dms-ui/app/components/grid/columns.ts +15 -3
  103. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +2 -1
  104. package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +9 -2
  105. package/frontend-vue/layers/dms-ui/app/components/save-bar/InstantSaveBadge.vue +13 -5
  106. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +1 -1
  107. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +3 -2
  108. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +53 -31
  109. package/frontend-vue/layers/dms-ui/app/composables/chart/apexPlotOptions.ts +15 -4
  110. package/frontend-vue/layers/dms-ui/app/composables/chart/apexYRange.ts +123 -0
  111. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +49 -49
  112. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartTheme.ts +20 -1
  113. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +1 -1
  114. package/frontend-vue/layers/dms-ui/app/composables/form/types/props.ts +2 -0
  115. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +8 -6
  116. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +1 -1
  117. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +3 -5
  118. package/frontend-vue/layers/dms-ui/app/utils/bannerToneIcons.ts +13 -0
  119. package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +26 -0
  120. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -0
  121. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -0
  122. package/frontend-vue/tests/activity-feed-fill-height.test.ts +7 -0
  123. package/frontend-vue/tests/banner-tone-icon.test.ts +55 -0
  124. package/frontend-vue/tests/builtin-emails.test.ts +141 -0
  125. package/frontend-vue/tests/cascader.test.ts +27 -0
  126. package/frontend-vue/tests/chart-auto-y-range.test.ts +25 -9
  127. package/frontend-vue/tests/chart-colors.test.ts +8 -0
  128. package/frontend-vue/tests/chart-resize-after-animation.test.ts +100 -0
  129. package/frontend-vue/tests/chart-type-defaults.test.ts +128 -0
  130. package/frontend-vue/tests/check-list-marker.test.ts +58 -0
  131. package/frontend-vue/tests/confirm-from.test.ts +1 -1
  132. package/frontend-vue/tests/confirm-secondary-action.test.ts +2 -2
  133. package/frontend-vue/tests/context-links.test.ts +2 -2
  134. package/frontend-vue/tests/custom-action-confirm-from.test.ts +2 -2
  135. package/frontend-vue/tests/download-action-target.test.ts +2 -2
  136. package/frontend-vue/tests/file-drop-prompt.test.ts +72 -0
  137. package/frontend-vue/tests/footer-config.test.ts +135 -0
  138. package/frontend-vue/tests/form-actions.test.ts +7 -0
  139. package/frontend-vue/tests/form-entry-order.test.ts +45 -0
  140. package/frontend-vue/tests/form-footer-kind.test.ts +11 -6
  141. package/frontend-vue/tests/form-load-error.test.ts +38 -1
  142. package/frontend-vue/tests/grid-responsive-columns.test.ts +9 -0
  143. package/frontend-vue/tests/header-actions.test.ts +55 -9
  144. package/frontend-vue/tests/instant-save-badge.test.ts +11 -4
  145. package/frontend-vue/tests/list-row-truncate.test.ts +46 -0
  146. package/frontend-vue/tests/roles-name-errors.test.ts +2 -3
  147. package/frontend-vue/tests/save-bar.test.ts +15 -3
  148. package/frontend-vue/tests/save-status-error.test.ts +2 -2
  149. package/frontend-vue/tests/table-row-action-errors.test.ts +5 -5
  150. package/frontend-vue/tests/table-row-hover.test.ts +2 -2
  151. package/frontend-vue/tests/table-view-sources.test.ts +1 -1
  152. package/frontend-vue/tests/template-tokens.test.ts +235 -0
  153. package/frontend-vue/tests/url-interpolation.test.ts +6 -6
  154. package/package.json +2 -2
  155. package/skills/dms/SKILL.md +2 -1
  156. package/skills/dms-pages/REFERENCE.md +2 -2
  157. 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
- if (input.yAxisFormatter) {
335
- (yaxis.labels as Record<string, unknown>).formatter = input.yAxisFormatter;
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
- if (!rawOptions || rawOptions.length === 0) return options;
395
- const merged: Record<string, unknown> = { ...options };
396
- for (const entry of rawOptions) {
397
- try {
398
- merged[entry.key] = JSON.parse(entry.value);
399
- } catch {
400
- merged[entry.key] = entry.value;
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 `{placeholders}` and of the dialog's texts (a
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
- replaceUrlVariables,
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 = replaceUrlVariables(
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, beyond the implicit built-in `table`. */
297
+ /** Displays this table view offers, in order; the built-in `table` alone when left out. */
298
298
  displays?: TableViewDisplayConfig[];
299
- /** Whether the built-in `table` display is offered. Defaults to `true`. */
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
+ });