@antelopejs/dms 0.7.20 → 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 (172) hide show
  1. package/CHANGELOG.md +62 -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-row-menu-label.test.d.ts +1 -0
  36. package/dist/test/unit/interfaces/dms-base/table-view-row-menu-label.test.js +27 -0
  37. package/dist/test/unit/interfaces/dms-base/table-view-row-menu-label.test.js.map +1 -0
  38. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +18 -4
  39. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  40. package/dist/test/unit/interfaces/dms-base/table-view-views.test.js +8 -5
  41. package/dist/test/unit/interfaces/dms-base/table-view-views.test.js.map +1 -1
  42. package/dist/test/unit/utils/email-subjects.test.d.ts +1 -0
  43. package/dist/test/unit/utils/email-subjects.test.js +27 -0
  44. package/dist/test/unit/utils/email-subjects.test.js.map +1 -0
  45. package/dist/utils/admin-invite-email.js +2 -4
  46. package/dist/utils/admin-invite-email.js.map +1 -1
  47. package/dist/utils/email-change-mail.d.ts +1 -1
  48. package/dist/utils/email-change-mail.js +11 -6
  49. package/dist/utils/email-change-mail.js.map +1 -1
  50. package/dist/utils/email-subjects.d.ts +3 -0
  51. package/dist/utils/email-subjects.js +32 -0
  52. package/dist/utils/email-subjects.js.map +1 -0
  53. package/dist/utils/export-jobs.js +15 -10
  54. package/dist/utils/export-jobs.js.map +1 -1
  55. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +29 -11
  56. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +17 -0
  57. package/frontend-vue/layers/dms-layout/app/app.config.ts +5 -0
  58. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +18 -2
  59. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +16 -1
  60. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useHeaderActionControls.ts +14 -0
  61. package/frontend-vue/layers/dms-layout/app/components/layout/HStack.vue +16 -0
  62. package/frontend-vue/layers/dms-layout/app/components/layout/VStack.vue +2 -0
  63. package/frontend-vue/layers/dms-layout/app/composables/useFooterLinks.ts +11 -1
  64. package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +1 -1
  65. package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +1 -1
  66. package/frontend-vue/layers/dms-layout/app/emails/EmailChangeNotice.vue +13 -7
  67. package/frontend-vue/layers/dms-layout/app/emails/EmailChangeVerification.vue +18 -7
  68. package/frontend-vue/layers/dms-layout/app/emails/EmailExportReady.vue +23 -12
  69. package/frontend-vue/layers/dms-layout/app/emails/EmailResetPassword.vue +18 -7
  70. package/frontend-vue/layers/dms-layout/app/emails/EmailTwoFactor.vue +23 -4
  71. package/frontend-vue/layers/dms-layout/app/emails/EmailUserValidation.vue +18 -6
  72. package/frontend-vue/layers/dms-layout/app/emails/components/EmailOTP.vue +8 -2
  73. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +40 -0
  74. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +40 -0
  75. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +2 -1
  76. package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +11 -3
  77. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +13 -3
  78. package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +1 -1
  79. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +14 -0
  80. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +90 -43
  81. package/frontend-vue/layers/dms-ui/app/build/composables/chart/apexTheme.ts +59 -2
  82. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +27 -4
  83. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +10 -2
  84. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +0 -5
  85. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryOrder.ts +23 -0
  86. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +19 -15
  87. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +26 -2
  88. package/frontend-vue/layers/dms-ui/app/build/composables/page/pageUrlValues.ts +41 -0
  89. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +2 -2
  90. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +3 -5
  91. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableUrlTab.ts +42 -12
  92. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +7 -11
  93. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +3 -3
  94. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +10 -11
  95. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +29 -0
  96. package/frontend-vue/layers/dms-ui/app/build/utils/link.ts +7 -3
  97. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +32 -9
  98. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +8 -1
  99. package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +9 -4
  100. package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +5 -11
  101. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +2 -1
  102. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +32 -1
  103. package/frontend-vue/layers/dms-ui/app/components/check-list/CheckList.vue +1 -1
  104. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +12 -2
  105. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +7 -3
  106. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +7 -0
  107. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +4 -1
  108. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +4 -2
  109. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +3 -2
  110. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -1
  111. package/frontend-vue/layers/dms-ui/app/components/grid/columns.ts +15 -3
  112. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +2 -1
  113. package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +9 -2
  114. package/frontend-vue/layers/dms-ui/app/components/save-bar/InstantSaveBadge.vue +13 -5
  115. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +1 -1
  116. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +3 -2
  117. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +53 -31
  118. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +14 -6
  119. package/frontend-vue/layers/dms-ui/app/composables/chart/apexPlotOptions.ts +15 -4
  120. package/frontend-vue/layers/dms-ui/app/composables/chart/apexYRange.ts +123 -0
  121. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +49 -49
  122. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartTheme.ts +20 -1
  123. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +1 -1
  124. package/frontend-vue/layers/dms-ui/app/composables/form/types/props.ts +2 -0
  125. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +8 -6
  126. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +1 -1
  127. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +3 -3
  128. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +6 -0
  129. package/frontend-vue/layers/dms-ui/app/utils/bannerToneIcons.ts +13 -0
  130. package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +26 -0
  131. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -0
  132. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -0
  133. package/frontend-vue/tests/activity-feed-fill-height.test.ts +7 -0
  134. package/frontend-vue/tests/banner-tone-icon.test.ts +55 -0
  135. package/frontend-vue/tests/builtin-emails.test.ts +141 -0
  136. package/frontend-vue/tests/cascader.test.ts +27 -0
  137. package/frontend-vue/tests/chart-auto-y-range.test.ts +25 -9
  138. package/frontend-vue/tests/chart-colors.test.ts +8 -0
  139. package/frontend-vue/tests/chart-resize-after-animation.test.ts +100 -0
  140. package/frontend-vue/tests/chart-type-defaults.test.ts +128 -0
  141. package/frontend-vue/tests/check-list-marker.test.ts +58 -0
  142. package/frontend-vue/tests/confirm-from.test.ts +1 -1
  143. package/frontend-vue/tests/confirm-secondary-action.test.ts +2 -2
  144. package/frontend-vue/tests/context-links.test.ts +2 -2
  145. package/frontend-vue/tests/custom-action-confirm-from.test.ts +2 -2
  146. package/frontend-vue/tests/download-action-target.test.ts +2 -2
  147. package/frontend-vue/tests/file-drop-prompt.test.ts +72 -0
  148. package/frontend-vue/tests/footer-config.test.ts +135 -0
  149. package/frontend-vue/tests/form-actions.test.ts +7 -0
  150. package/frontend-vue/tests/form-entry-order.test.ts +45 -0
  151. package/frontend-vue/tests/form-footer-kind.test.ts +11 -6
  152. package/frontend-vue/tests/form-load-error.test.ts +38 -1
  153. package/frontend-vue/tests/grid-responsive-columns.test.ts +9 -0
  154. package/frontend-vue/tests/header-actions.test.ts +55 -9
  155. package/frontend-vue/tests/instant-save-badge.test.ts +11 -4
  156. package/frontend-vue/tests/list-row-truncate.test.ts +46 -0
  157. package/frontend-vue/tests/roles-name-errors.test.ts +2 -3
  158. package/frontend-vue/tests/row-side-panel.test.ts +93 -2
  159. package/frontend-vue/tests/save-bar.test.ts +15 -3
  160. package/frontend-vue/tests/save-status-error.test.ts +2 -2
  161. package/frontend-vue/tests/table-row-action-errors.test.ts +5 -5
  162. package/frontend-vue/tests/table-row-hover.test.ts +2 -2
  163. package/frontend-vue/tests/table-view-bulk-drawer.test.ts +114 -0
  164. package/frontend-vue/tests/table-view-cards-reread.test.ts +77 -0
  165. package/frontend-vue/tests/table-view-sources.test.ts +1 -1
  166. package/frontend-vue/tests/table-view-url-tab.test.ts +84 -2
  167. package/frontend-vue/tests/template-tokens.test.ts +235 -0
  168. package/frontend-vue/tests/url-interpolation.test.ts +6 -6
  169. package/package.json +2 -2
  170. package/skills/dms/SKILL.md +2 -1
  171. package/skills/dms-pages/REFERENCE.md +2 -2
  172. package/skills/dms-project/SKILL.md +0 -1
@@ -34,20 +34,20 @@ describe("annotation-aware auto Y range", () => {
34
34
 
35
35
  it("expands both ends for annotations above and below native data bounds", () => {
36
36
  const bounds = boundsFor({ annotations: [{ y: 200 }, { y: -50 }] });
37
- expect(bounds.min(10)).toBe(-54);
38
- expect(bounds.max(30)).toBe(216);
37
+ expect(bounds.min(10)).toBe(-100);
38
+ expect(bounds.max(30)).toBe(250);
39
39
  });
40
40
 
41
41
  it("includes both ends of a reversed band and all native series extrema", () => {
42
42
  const bounds = boundsFor({ annotations: [{ y: 200, y2: -50 }] });
43
- expect(bounds.min(-100)).toBe(-108);
44
- expect(bounds.max(300)).toBe(324);
43
+ expect(bounds.min(-100)).toBe(-150);
44
+ expect(bounds.max(300)).toBe(350);
45
45
  });
46
46
 
47
47
  it("pads negative bounds outwards rather than clipping negative data", () => {
48
48
  const bounds = boundsFor();
49
- expect(bounds.min(-100)).toBe(-108);
50
- expect(bounds.max(-10)).toBe(-9.2);
49
+ expect(bounds.min(-100)).toBe(-120);
50
+ expect(bounds.max(-10)).toBe(-9);
51
51
  });
52
52
 
53
53
  it("gives zero-only data a nonzero range without changing the series", () => {
@@ -75,15 +75,15 @@ describe("annotation-aware auto Y range", () => {
75
75
  });
76
76
 
77
77
  it.each([-1, NaN, Infinity])("defaults invalid padding %s", (padding) => {
78
- expect(boundsFor({ autoYRange: { padding } }).max(100)).toBe(108);
78
+ expect(boundsFor({ autoYRange: { padding } }).max(100)).toBe(120);
79
79
  });
80
80
 
81
81
  it("ignores nonfinite annotation coordinates when determining bounds", () => {
82
82
  const bounds = boundsFor({
83
83
  annotations: [{ y: NaN }, { y: Infinity }, { y: 20 }],
84
84
  });
85
- expect(bounds.min(10)).toBe(9.2);
86
- expect(bounds.max(10)).toBe(21.6);
85
+ expect(bounds.min(10)).toBe(9);
86
+ expect(bounds.max(10)).toBe(22);
87
87
  });
88
88
 
89
89
  it("recomputes when annotations or padding change", () => {
@@ -109,6 +109,22 @@ describe("annotation-aware auto Y range", () => {
109
109
  ).toEqual({ min: 0, max: 5 });
110
110
  });
111
111
 
112
+ it("rounds the padded bounds to a step the axis can tick evenly", () => {
113
+ const series = [{ name: "value", data: [104.4, 420, 770] }];
114
+ const bounds = boundsFor({ series, annotations: [{ y: 300, y2: 500 }] });
115
+ expect(bounds.min(104.4)).toBe(0);
116
+ expect(bounds.max(770)).toBe(900);
117
+ });
118
+
119
+ it("writes unformatted axis labels without float noise", () => {
120
+ const labels = chartFor().options.value.yaxis as {
121
+ labels: { formatter: (value: number) => string };
122
+ };
123
+ expect(labels.labels.formatter(160.00000000000003)).toBe("160");
124
+ expect(labels.labels.formatter(0.1 + 0.2)).toBe("0.3");
125
+ expect(labels.labels.formatter(603.6)).toBe("603.6");
126
+ });
127
+
112
128
  it("does not scale circular charts", () => {
113
129
  expect(boundsFor({ type: "donut" })).not.toHaveProperty("min");
114
130
  });
@@ -71,6 +71,14 @@ describe("chart color tokens", () => {
71
71
  expect(resolveChartColor(color)).toBe(color);
72
72
  });
73
73
 
74
+ it.each([
75
+ ["rgb(14 165 233)", "rgb(14, 165, 233)"],
76
+ ["rgb(14 165 233 / 0.5)", "rgba(14, 165, 233, 0.5)"],
77
+ ["rgba(14 165 233 / 40%)", "rgba(14, 165, 233, 0.4)"],
78
+ ])("rewrites space-separated %s in the comma form Apex reads", (color, expected) => {
79
+ expect(resolveChartColor(color)).toBe(expected);
80
+ });
81
+
74
82
  it.each(["", "transparent", "rgba(0, 0, 0, 0)"])(
75
83
  "falls back for unusable computed color %s",
76
84
  (color) => {
@@ -0,0 +1,100 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Apex drops its container's resizes while an animation runs: a sidebar
4
+ * toggled during the intro animation left the chart at its old width, past
5
+ * its card, until the next window resize. The host redraws such a chart once
6
+ * the animation ends.
7
+ */
8
+ import { createApp, defineComponent, h, nextTick, ref } from "vue";
9
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
10
+ import ApexChartHost from "../layers/dms-ui/app/components/chart/ApexChartHost.vue";
11
+
12
+ const HOST_WIDTH = 1047;
13
+ const STALE_WIDTH = 1227;
14
+
15
+ const windowResizeHandler = vi.fn();
16
+ const drawnWidth = ref(HOST_WIDTH);
17
+ let endAnimation: () => void = () => {};
18
+
19
+ vi.mock("apexcharts", () => ({ default: {} }));
20
+ vi.mock("../layers/dms-ui/app/composables/chart/apexLocales", () => ({
21
+ ensureApexLocale: async () => undefined,
22
+ }));
23
+ vi.mock("vue3-apexcharts", () => ({
24
+ default: defineComponent({
25
+ emits: ["animationEnd"],
26
+ setup(_, { emit, expose }) {
27
+ expose({
28
+ chart: { windowResizeHandler },
29
+ updateOptions: async () => undefined,
30
+ });
31
+ endAnimation = () => emit("animationEnd");
32
+ return () =>
33
+ h("div", [
34
+ h("svg", {
35
+ class: "apexcharts-svg",
36
+ "data-width": drawnWidth.value,
37
+ }),
38
+ ]);
39
+ },
40
+ }),
41
+ }));
42
+
43
+ function widthOf(element: Element): number {
44
+ if (element.classList.contains("dms-apex")) return HOST_WIDTH;
45
+ return Number(element.getAttribute("data-width") ?? 0);
46
+ }
47
+
48
+ let app: ReturnType<typeof createApp> | undefined;
49
+
50
+ async function mountHost(): Promise<HTMLElement> {
51
+ const root = document.createElement("div");
52
+ app = createApp(() =>
53
+ h(ApexChartHost, {
54
+ apexType: "line",
55
+ height: 300,
56
+ options: {},
57
+ series: [],
58
+ }),
59
+ );
60
+ app.mount(root);
61
+ await vi.waitFor(() => {
62
+ expect(root.querySelector("svg.apexcharts-svg")).not.toBeNull();
63
+ });
64
+ return root;
65
+ }
66
+
67
+ beforeEach(() => {
68
+ vi.stubGlobal("useI18n", () => ({ locale: ref("en") }));
69
+ vi.spyOn(Element.prototype, "getBoundingClientRect").mockImplementation(
70
+ function (this: Element) {
71
+ return { width: widthOf(this) } as DOMRect;
72
+ },
73
+ );
74
+ });
75
+
76
+ afterEach(() => {
77
+ app?.unmount();
78
+ windowResizeHandler.mockClear();
79
+ drawnWidth.value = HOST_WIDTH;
80
+ vi.restoreAllMocks();
81
+ vi.unstubAllGlobals();
82
+ });
83
+
84
+ it("redraws a chart left at a stale width when its animation ends", async () => {
85
+ await mountHost();
86
+ drawnWidth.value = STALE_WIDTH;
87
+ await nextTick();
88
+
89
+ endAnimation();
90
+
91
+ expect(windowResizeHandler).toHaveBeenCalledOnce();
92
+ });
93
+
94
+ it("leaves a chart drawn at its host's width alone", async () => {
95
+ await mountHost();
96
+
97
+ endAnimation();
98
+
99
+ expect(windowResizeHandler).not.toHaveBeenCalled();
100
+ });
@@ -0,0 +1,128 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { useApexChart } from "../layers/dms-ui/app/composables/chart/useApexChart";
3
+ import type { UseApexChartInput } from "../layers/dms-ui/app/composables/chart/useApexChart.types";
4
+ import type { ChartSeries } from "../layers/dms-ui/app/composables/chart/types";
5
+
6
+ const DEALS: ChartSeries[] = [
7
+ {
8
+ name: "Deals",
9
+ data: [
10
+ { x: 3, y: 120, label: "Acme" },
11
+ { x: 8, y: 340 },
12
+ ],
13
+ },
14
+ ];
15
+
16
+ type Section = Record<string, unknown>;
17
+ type LabelFormatter = (value: unknown, context: unknown) => unknown;
18
+
19
+ function optionsFor(input: Partial<UseApexChartInput>): Section {
20
+ return useApexChart(() => ({ type: "line", series: [], ...input })).options
21
+ .value;
22
+ }
23
+
24
+ function sectionOf(options: Section, key: string): Section {
25
+ return options[key] as Section;
26
+ }
27
+
28
+ function labelContext(dataPointIndex: number) {
29
+ return { seriesIndex: 0, dataPointIndex, w: { config: { series: DEALS } } };
30
+ }
31
+
32
+ describe("radar grid", () => {
33
+ it("leaves the grid to the radar's own polygons", () => {
34
+ expect(sectionOf(optionsFor({ type: "radar" }), "grid").show).toBe(false);
35
+ });
36
+
37
+ it("keeps the cartesian grid on axis charts", () => {
38
+ expect(sectionOf(optionsFor({ type: "line" }), "grid").show).toBe(true);
39
+ });
40
+ });
41
+
42
+ describe("donut slice labels", () => {
43
+ it("hides them on the default thin ring they overflow", () => {
44
+ const labels = sectionOf(optionsFor({ type: "donut" }), "dataLabels");
45
+ expect(labels.enabled).toBe(false);
46
+ });
47
+
48
+ it("shows them on a ring wide enough to hold them", () => {
49
+ const labels = sectionOf(
50
+ optionsFor({ type: "donut", arcWidth: 45 }),
51
+ "dataLabels",
52
+ );
53
+ expect(labels.enabled).toBe(true);
54
+ });
55
+
56
+ it("keeps them on a pie, and Apex's own percent formatter", () => {
57
+ const labels = sectionOf(optionsFor({ type: "pie" }), "dataLabels");
58
+ expect(labels.enabled).toBe(true);
59
+ expect(labels.formatter).toBeUndefined();
60
+ });
61
+ });
62
+
63
+ describe("scatter points", () => {
64
+ it("draws its points without a pointSize", () => {
65
+ const markers = sectionOf(optionsFor({ type: "scatter" }), "markers");
66
+ expect(markers.size).toBeGreaterThan(0);
67
+ });
68
+
69
+ it("honours an explicit pointSize", () => {
70
+ const markers = sectionOf(
71
+ optionsFor({ type: "scatter", pointSize: 9 }),
72
+ "markers",
73
+ );
74
+ expect(markers.size).toBe(9);
75
+ });
76
+
77
+ it("keeps line markers hidden", () => {
78
+ expect(sectionOf(optionsFor({ type: "line" }), "markers").size).toBe(0);
79
+ });
80
+
81
+ it("labels a point with its own label, else its value", () => {
82
+ const labels = sectionOf(
83
+ optionsFor({ type: "scatter", series: DEALS, showLabels: true }),
84
+ "dataLabels",
85
+ );
86
+ const format = labels.formatter as LabelFormatter;
87
+ expect(format(120, labelContext(0))).toBe("Acme");
88
+ expect(format(340, labelContext(1))).toBe(340);
89
+ });
90
+
91
+ it("lifts a point's label off the point it names", () => {
92
+ const labels = sectionOf(
93
+ optionsFor({ type: "scatter", showLabels: true }),
94
+ "dataLabels",
95
+ );
96
+ expect(labels.offsetY).toBeLessThan(0);
97
+ const line = sectionOf(optionsFor({ type: "line" }), "dataLabels");
98
+ expect(line.offsetY).toBe(0);
99
+ });
100
+ });
101
+
102
+ describe("rawOptions paths", () => {
103
+ it("sets a dotted key inside its section", () => {
104
+ const markers = sectionOf(
105
+ optionsFor({
106
+ type: "scatter",
107
+ rawOptions: [{ key: "markers.size", value: "5" }],
108
+ }),
109
+ "markers",
110
+ );
111
+ expect(markers.size).toBe(5);
112
+ expect(markers.strokeWidth).toBe(0);
113
+ });
114
+
115
+ it("creates the sections a dotted key names", () => {
116
+ const options = optionsFor({
117
+ rawOptions: [{ key: "states.hover.filter", value: '{"type":"none"}' }],
118
+ });
119
+ expect(options.states).toEqual({ hover: { filter: { type: "none" } } });
120
+ });
121
+
122
+ it("still replaces a whole section from a plain key", () => {
123
+ const options = optionsFor({
124
+ rawOptions: [{ key: "markers", value: '{"size":5}' }],
125
+ });
126
+ expect(options.markers).toEqual({ size: 5 });
127
+ });
128
+ });
@@ -0,0 +1,58 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { createApp, defineComponent, h, nextTick, type App } from "vue";
4
+ import CheckList, {
5
+ type CheckListItem,
6
+ type CheckListMarker,
7
+ } from "../layers/dms-ui/app/components/check-list/CheckList.vue";
8
+
9
+ const IconStub = defineComponent({
10
+ props: { name: String },
11
+ setup: (props) => () => h("i", { "data-icon": props.name }),
12
+ });
13
+
14
+ const ITEMS: CheckListItem[] = [
15
+ { label: "Ok", state: "ok" },
16
+ { label: "Failed", state: "error" },
17
+ { label: "Waiting", state: "pending" },
18
+ ];
19
+
20
+ let app: App | undefined;
21
+ let host: HTMLDivElement;
22
+
23
+ async function mount(marker: CheckListMarker) {
24
+ app = createApp({ render: () => h(CheckList, { items: ITEMS, marker }) });
25
+ app.component("UIcon", IconStub);
26
+ app.config.globalProperties.$t = (key: string) => key;
27
+ app.mount(host);
28
+ await nextTick();
29
+ }
30
+
31
+ const iconOf = (label: string) =>
32
+ [...host.querySelectorAll("li")]
33
+ .find((item) => item.textContent?.includes(label))
34
+ ?.querySelector("i")?.dataset.icon;
35
+
36
+ beforeEach(() => {
37
+ vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
38
+ host = document.createElement("div");
39
+ document.body.append(host);
40
+ });
41
+
42
+ afterEach(() => {
43
+ app?.unmount();
44
+ app = undefined;
45
+ host.remove();
46
+ vi.unstubAllGlobals();
47
+ });
48
+
49
+ describe("DmsCheckList markers", () => {
50
+ it.each<CheckListMarker>(["icon", "plain"])(
51
+ "never draws a pending line with the error icon (%s)",
52
+ async (marker) => {
53
+ await mount(marker);
54
+ expect(iconOf("Waiting")).toBeTruthy();
55
+ expect(iconOf("Waiting")).not.toBe(iconOf("Failed"));
56
+ },
57
+ );
58
+ });
@@ -59,7 +59,7 @@ describe("confirmFrom", () => {
59
59
  });
60
60
  const onConfirm = vi.fn(async () => undefined);
61
61
  const confirmed = await mountConfirmFrom().confirmFrom(
62
- "/api/campaigns/{id}/delete-confirm",
62
+ "/api/campaigns/{{row.id}}/delete-confirm",
63
63
  {
64
64
  params: { id: "c1", name: "Spring" },
65
65
  fallback: { icon: "i-ph-trash" },
@@ -49,8 +49,8 @@ describe("a confirmation's secondary action, resolved", () => {
49
49
  icon: "i-ph-download-simple",
50
50
  target: {
51
51
  type: "download",
52
- url: "/legacy/lang/{workspace}/{code}.json",
53
- filename: "{workspace}-{file}",
52
+ url: "/legacy/lang/{{row.workspace}}/{{row.code}}.json",
53
+ filename: "{{row.workspace}}-{{row.file}}",
54
54
  },
55
55
  },
56
56
  },
@@ -198,7 +198,7 @@ describe("a link row action", () => {
198
198
  label: "Report",
199
199
  target: {
200
200
  type: "page",
201
- url: "/modules/marketing/funnel?id={_id}&website={{query.website}}&mk.period={{query.mk.period}}&mk.to={{query.mk.to}}&order={{params.id}}",
201
+ url: "/modules/marketing/funnel?id={{row._id}}&website={{query.website}}&mk.period={{query.mk.period}}&mk.to={{query.mk.to}}&order={{params.id}}",
202
202
  },
203
203
  },
204
204
  { _id: "f 1" },
@@ -211,7 +211,7 @@ describe("a link row action", () => {
211
211
 
212
212
  it("leaves a URL without page tokens as before", async () => {
213
213
  mountTargets().handleCustomRowAction(
214
- { label: "Open", target: { type: "page", url: "/orders/{_id}" } },
214
+ { label: "Open", target: { type: "page", url: "/orders/{{row._id}}" } },
215
215
  { _id: "o2" },
216
216
  );
217
217
  await vi.waitFor(() => expect(navigateDms).toHaveBeenCalledTimes(1));
@@ -32,7 +32,7 @@ function mountTargets(rowIdKey?: string) {
32
32
  const REFUND: CustomRowAction = {
33
33
  label: "Refund",
34
34
  target: { type: "page", url: "/orders" },
35
- confirm: { from: "/api/orders/{id}/refund-confirm?customer={customer}" },
35
+ confirm: { from: "/api/orders/{{row.id}}/refund-confirm?customer={{row.customer}}" },
36
36
  };
37
37
 
38
38
  beforeEach(() => {
@@ -60,7 +60,7 @@ afterEach(() => {
60
60
  });
61
61
 
62
62
  describe("a custom row action's confirmation worded by the server", () => {
63
- it("fills {id} with the row's id, like a built-in action, besides its fields", async () => {
63
+ it("fills {{row.id}} with the row's id, like a built-in action, besides its fields", async () => {
64
64
  mountTargets().handleCustomRowAction(REFUND, {
65
65
  _id: "o1",
66
66
  customer: "Ada",
@@ -146,8 +146,8 @@ describe("a download action target", () => {
146
146
  label: "Download",
147
147
  target: {
148
148
  type: "download",
149
- url: "/api/files/{_id}?workspace={{query.workspace}}",
150
- filename: "{name}",
149
+ url: "/api/files/{{row._id}}?workspace={{query.workspace}}",
150
+ filename: "{{row.name}}",
151
151
  },
152
152
  };
153
153
  mountTargets().handleCustomRowAction(action, {
@@ -0,0 +1,72 @@
1
+ // @vitest-environment jsdom
2
+ import {
3
+ computed,
4
+ createApp,
5
+ defineComponent,
6
+ h,
7
+ inject,
8
+ onScopeDispose,
9
+ ref,
10
+ watch,
11
+ type App,
12
+ } from "vue";
13
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
14
+ import File from "../layers/dms-ui/app/components/form/components/File.vue";
15
+
16
+ const dropZone = defineComponent({
17
+ inheritAttrs: false,
18
+ setup:
19
+ (_, { attrs }) =>
20
+ () =>
21
+ h("span", String(attrs.label ?? "")),
22
+ });
23
+
24
+ function mountFile(multiple: boolean): { app: App; container: HTMLElement } {
25
+ const app = createApp({
26
+ render: () => h(File, { modelValue: null, multiple }),
27
+ });
28
+ app.config.globalProperties.$t = (key: string) => key;
29
+ app.component("UFileUpload", dropZone);
30
+ const container = document.createElement("div");
31
+ app.mount(container);
32
+ return { app, container };
33
+ }
34
+
35
+ beforeEach(() => {
36
+ vi.stubGlobal("ref", ref);
37
+ vi.stubGlobal("computed", computed);
38
+ vi.stubGlobal("watch", watch);
39
+ vi.stubGlobal("inject", inject);
40
+ vi.stubGlobal("onScopeDispose", onScopeDispose);
41
+ vi.stubGlobal("FORM_FIELD_LOADING_KEY", Symbol());
42
+ vi.stubGlobal("FORM_CONTENT_LANGUAGE_KEY", Symbol());
43
+ vi.stubGlobal("CONTENT_LANGUAGE_HEADER", "x-content-language");
44
+ vi.stubGlobal("useAuthFetch", () => ({ $authFetch: vi.fn() }));
45
+ vi.stubGlobal("useUploadWithProgress", () => ({
46
+ uploadWithProgress: vi.fn(),
47
+ }));
48
+ vi.stubGlobal("useFormField", () => ({
49
+ emitFormChange: vi.fn(),
50
+ color: ref(undefined),
51
+ highlight: ref(undefined),
52
+ ariaAttrs: ref(undefined),
53
+ }));
54
+ vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
55
+ vi.stubGlobal("useToast", () => ({ add: vi.fn() }));
56
+ });
57
+
58
+ afterEach(() => {
59
+ vi.unstubAllGlobals();
60
+ });
61
+
62
+ it("asks for a file in its drop zone", () => {
63
+ const { app, container } = mountFile(false);
64
+ expect(container.textContent).toBe("dms.form.file.drop_prompt");
65
+ app.unmount();
66
+ });
67
+
68
+ it("asks for files in the drop zone of a multiple field", () => {
69
+ const { app, container } = mountFile(true);
70
+ expect(container.textContent).toBe("dms.form.file.drop_prompt_multiple");
71
+ app.unmount();
72
+ });
@@ -0,0 +1,135 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import {
4
+ computed,
5
+ createApp,
6
+ defineComponent,
7
+ h,
8
+ nextTick,
9
+ onMounted,
10
+ ref,
11
+ type App,
12
+ } from "vue";
13
+ import type { DmsFooterConfig } from "../layers/dms-core/shared/types/app-config";
14
+
15
+ let app: App | undefined;
16
+ let host: HTMLDivElement;
17
+ let footerConfig: DmsFooterConfig | undefined;
18
+
19
+ const SelectStub = defineComponent({
20
+ setup: () => () => h("select", { "data-locale-switcher": "" }),
21
+ });
22
+
23
+ async function mountFooter(): Promise<void> {
24
+ const { default: Footer } = await import(
25
+ "../layers/dms-layout/app/build/components/layout/Footer.vue"
26
+ );
27
+ host = document.createElement("div");
28
+ document.body.append(host);
29
+ app = createApp(Footer);
30
+ app.component("DmsLink", {
31
+ props: ["to"],
32
+ setup:
33
+ (props: { to: string }, { slots }) =>
34
+ () =>
35
+ h("a", { href: props.to }, slots.default?.()),
36
+ });
37
+ app.component("UIcon", { render: () => null });
38
+ app.component("USkeleton", { render: () => null });
39
+ app.component("USelect", SelectStub);
40
+ app.mount(host);
41
+ await nextTick();
42
+ }
43
+
44
+ function linkTexts(): string[] {
45
+ return [...host.querySelectorAll("a")]
46
+ .map((link) => link.textContent?.trim() ?? "")
47
+ .filter((text) => text.length > 0);
48
+ }
49
+
50
+ beforeEach(async () => {
51
+ vi.resetModules();
52
+ footerConfig = undefined;
53
+ vi.stubGlobal("ref", ref);
54
+ vi.stubGlobal("computed", computed);
55
+ vi.stubGlobal("onMounted", onMounted);
56
+ vi.stubGlobal(
57
+ "fetch",
58
+ vi.fn(async () => ({ ok: false })),
59
+ );
60
+ vi.stubGlobal("useI18n", () => ({
61
+ locale: ref("en"),
62
+ setLocale: vi.fn(),
63
+ t: (key: string) => key,
64
+ }));
65
+ vi.stubGlobal("useUniqueLocales", () => ({ uniqueLocales: ref([]) }));
66
+ vi.stubGlobal("useTranslation", () => ({
67
+ processI18n: (text: string) =>
68
+ text.startsWith("$") ? `t(${text.slice(1)})` : text,
69
+ }));
70
+ vi.stubGlobal("useDmsAppConfig", () => ({
71
+ branding: { footer: footerConfig },
72
+ }));
73
+ const footerLinks = await import(
74
+ "../layers/dms-layout/app/composables/useFooterLinks"
75
+ );
76
+ vi.stubGlobal("useFooterLinks", footerLinks.useFooterLinks);
77
+ vi.stubGlobal("registerFooterLink", footerLinks.registerFooterLink);
78
+ vi.stubGlobal("unregisterFooterLink", footerLinks.unregisterFooterLink);
79
+ });
80
+
81
+ afterEach(() => {
82
+ app?.unmount();
83
+ host?.remove();
84
+ vi.unstubAllGlobals();
85
+ });
86
+
87
+ describe("footer", () => {
88
+ it("shows the copyright, the Antelope links, the star and the language picker by default", async () => {
89
+ await mountFooter();
90
+ expect(host.textContent).toContain("© Antelope");
91
+ expect(linkTexts()).toEqual([
92
+ "empty_layout.footer.documentation",
93
+ "empty_layout.footer.website",
94
+ "empty_layout.footer.github_star",
95
+ ]);
96
+ expect(host.querySelector("[data-locale-switcher]")).not.toBeNull();
97
+ });
98
+
99
+ it("drops a link a layer unregisters, the Antelope ones included", async () => {
100
+ const { registerFooterLink, unregisterFooterLink } = await import(
101
+ "../layers/dms-layout/app/composables/useFooterLinks"
102
+ );
103
+ registerFooterLink({
104
+ id: "acme:status",
105
+ label: "Status",
106
+ to: "/status",
107
+ order: 30,
108
+ });
109
+ unregisterFooterLink("dms:documentation");
110
+ unregisterFooterLink("dms:website");
111
+ unregisterFooterLink("never-registered");
112
+ await mountFooter();
113
+ expect(linkTexts()).toEqual(["Status", "empty_layout.footer.github_star"]);
114
+ });
115
+
116
+ it("names the copyright holder, or drops it, the star and the picker", async () => {
117
+ footerConfig = { copyright: "$acme.name" };
118
+ await mountFooter();
119
+ expect(host.textContent).toContain("© t(acme.name)");
120
+ app?.unmount();
121
+ host.remove();
122
+ vi.mocked(fetch).mockClear();
123
+
124
+ footerConfig = {
125
+ copyright: false,
126
+ githubStar: false,
127
+ localeSwitcher: false,
128
+ };
129
+ await mountFooter();
130
+ expect(host.textContent).not.toContain("©");
131
+ expect(linkTexts()).not.toContain("empty_layout.footer.github_star");
132
+ expect(host.querySelector("[data-locale-switcher]")).toBeNull();
133
+ expect(fetch).not.toHaveBeenCalled();
134
+ });
135
+ });
@@ -13,6 +13,13 @@ describe("formShowsActions", () => {
13
13
  ).toBe(false);
14
14
  });
15
15
 
16
+ it("shows the submit of an action form with nothing to fill in", () => {
17
+ const action = { kind: "action", submitUrl: "/api/jobs/run" } as const;
18
+ expect(formShowsActions(action, [])).toBe(true);
19
+ expect(formShowsActions(action, [{ disabled: true }])).toBe(true);
20
+ expect(formShowsActions({ ...action, saveMode: "none" }, [])).toBe(false);
21
+ });
22
+
16
23
  it("hides them with saveMode none, whatever the form could submit", () => {
17
24
  expect(
18
25
  formShowsActions({ saveMode: "none", submitUrl: "/api/order/new" }, [{}]),