@antelopejs/dms 0.7.8 → 0.7.10

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 (184) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/config.d.ts +2 -0
  3. package/dist/config.js +9 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/implementations/dms/api-base-url.d.ts +1 -0
  6. package/dist/implementations/dms/api-base-url.js +6 -0
  7. package/dist/implementations/dms/api-base-url.js.map +1 -0
  8. package/dist/implementations/dms/frontend-modules.js +7 -6
  9. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  10. package/dist/implementations/dms/page.js +8 -2
  11. package/dist/implementations/dms/page.js.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/test/attachment-host/capabilities.d.ts +25 -0
  15. package/dist/test/attachment-host/capabilities.js +53 -1
  16. package/dist/test/attachment-host/capabilities.js.map +1 -1
  17. package/dist/test/integration/table-view-capabilities.test.js +13 -0
  18. package/dist/test/integration/table-view-capabilities.test.js.map +1 -1
  19. package/dist/test/unit/api-base-url.test.d.ts +1 -0
  20. package/dist/test/unit/api-base-url.test.js +31 -0
  21. package/dist/test/unit/api-base-url.test.js.map +1 -0
  22. package/dist/test/unit/interfaces/dms/page-layout-query.test.d.ts +1 -0
  23. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +106 -0
  24. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -0
  25. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -0
  26. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  27. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.d.ts +1 -0
  28. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js +46 -0
  29. package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js.map +1 -0
  30. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.d.ts +1 -0
  31. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +26 -0
  32. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -0
  33. package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +10 -0
  34. package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
  35. package/dist/test/unit/interfaces/dms-base/guard.test.d.ts +1 -0
  36. package/dist/test/unit/interfaces/dms-base/guard.test.js +53 -0
  37. package/dist/test/unit/interfaces/dms-base/guard.test.js.map +1 -0
  38. package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js +23 -0
  39. package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js.map +1 -1
  40. package/dist/test/unit/interfaces/dms-base/page-live.test.js +55 -0
  41. package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -1
  42. package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js +17 -0
  43. package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js.map +1 -1
  44. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +8 -1
  45. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
  46. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +28 -0
  47. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  48. package/frontend-vue/layers/dms-core/app/build/composables/user/preferencesCookie.ts +149 -0
  49. package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +321 -0
  50. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +4 -3
  51. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +4 -3
  52. package/frontend-vue/layers/dms-core/app/composables/period/types.ts +3 -1
  53. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +2 -2
  54. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +9 -2
  55. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +48 -16
  56. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +21 -4
  57. package/frontend-vue/layers/dms-core/app/composables/user/usePreferences.ts +81 -49
  58. package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +35 -0
  59. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
  60. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
  61. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +10 -1
  62. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +32 -1
  63. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +13 -3
  64. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +27 -3
  65. package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +16 -0
  66. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +75 -0
  67. package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +8 -1
  68. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +13 -2
  69. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +7 -0
  70. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +7 -0
  71. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -4
  72. package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +59 -0
  73. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
  74. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
  75. package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +2 -0
  76. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockInlineStatus.vue +67 -0
  77. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +60 -20
  78. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +15 -8
  79. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +14 -0
  80. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +29 -4
  81. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +76 -28
  82. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +26 -2
  83. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useGuard.ts +115 -0
  84. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +1 -1
  85. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +58 -3
  86. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/secondaryTarget.ts +86 -0
  87. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +5 -0
  88. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +6 -0
  89. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveCadence.ts +42 -0
  90. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveHolds.ts +66 -0
  91. package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +42 -0
  92. package/frontend-vue/layers/dms-ui/app/build/composables/live/liveStorage.ts +42 -0
  93. package/frontend-vue/layers/dms-ui/app/build/composables/live/missedTally.ts +65 -0
  94. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +247 -88
  95. package/frontend-vue/layers/dms-ui/app/build/composables/live/useLivePeriodSpan.ts +33 -0
  96. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +79 -5
  97. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparisonControl.ts +70 -0
  98. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +55 -0
  99. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +41 -5
  100. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +22 -9
  101. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +13 -6
  102. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +1 -0
  103. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +27 -2
  104. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +18 -3
  105. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +19 -26
  106. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +66 -0
  107. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +7 -1
  108. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +65 -11
  109. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTablePreferences.ts +149 -0
  110. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +39 -8
  111. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
  112. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +16 -11
  113. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +39 -16
  114. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +29 -0
  115. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +18 -5
  116. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/sortableColumns.ts +17 -0
  117. package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +79 -0
  118. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +46 -1
  119. package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +5 -4
  120. package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +4 -0
  121. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +40 -2
  122. package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +11 -1
  123. package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +12 -2
  124. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +129 -4
  125. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +6 -0
  126. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +6 -1
  127. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +33 -0
  128. package/frontend-vue/layers/dms-ui/app/components/form/components/ChoiceCards.vue +22 -4
  129. package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +74 -0
  130. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -17
  131. package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +15 -0
  132. package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +12 -15
  133. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +104 -16
  134. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +113 -99
  135. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +11 -1
  136. package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +2 -0
  137. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +11 -1
  138. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +15 -1
  139. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +86 -10
  140. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewShiftF.ts +20 -0
  141. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +12 -0
  142. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -0
  143. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +8 -1
  144. package/frontend-vue/layers/dms-ui/app/composables/table-view/useTableViewEmptyState.ts +55 -0
  145. package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
  146. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +2 -0
  147. package/frontend-vue/layers/dms-ui/app/utils/componentId.ts +21 -0
  148. package/frontend-vue/layers/dms-ui/app/utils/rowClickAction.ts +12 -0
  149. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +13 -4
  150. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +12 -3
  151. package/frontend-vue/tests/activity-feed-fill-height.test.ts +1 -0
  152. package/frontend-vue/tests/activity-feed-head.test.ts +140 -0
  153. package/frontend-vue/tests/block-not-found.test.ts +220 -0
  154. package/frontend-vue/tests/choice-cards-columns.test.ts +60 -0
  155. package/frontend-vue/tests/component-id.test.ts +18 -0
  156. package/frontend-vue/tests/confirm-secondary-action.test.ts +269 -0
  157. package/frontend-vue/tests/guard.test.ts +242 -0
  158. package/frontend-vue/tests/header-action-page-query.test.ts +286 -0
  159. package/frontend-vue/tests/live-toggle.test.ts +87 -1
  160. package/frontend-vue/tests/page-layout-registration.test.ts +1 -0
  161. package/frontend-vue/tests/page-live-parity.test.ts +367 -0
  162. package/frontend-vue/tests/page-realtime-remount.test.ts +185 -0
  163. package/frontend-vue/tests/period-selector-parity.test.ts +138 -0
  164. package/frontend-vue/tests/period-selector-url.test.ts +84 -6
  165. package/frontend-vue/tests/period-url-query.test.ts +52 -0
  166. package/frontend-vue/tests/preferences-cookie-size.test.ts +311 -0
  167. package/frontend-vue/tests/row-side-panel.test.ts +109 -10
  168. package/frontend-vue/tests/table-actions-search-label.test.ts +57 -1
  169. package/frontend-vue/tests/table-clear-filters.test.ts +99 -1
  170. package/frontend-vue/tests/table-density-menu.test.ts +4 -3
  171. package/frontend-vue/tests/table-live-hold.test.ts +95 -5
  172. package/frontend-vue/tests/table-row-action-labels.test.ts +181 -0
  173. package/frontend-vue/tests/table-row-hover.test.ts +32 -0
  174. package/frontend-vue/tests/table-source-period-scope.test.ts +31 -3
  175. package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +14 -17
  176. package/frontend-vue/tests/table-view-pinned-rows.test.ts +143 -0
  177. package/frontend-vue/tests/table-view-sortable-columns.test.ts +45 -0
  178. package/frontend-vue/tests/table-view-views.test.ts +56 -15
  179. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +107 -0
  180. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  181. package/package.json +2 -2
  182. package/skills/dms/SKILL.md +9 -3
  183. package/skills/dms-module/SKILL.md +5 -0
  184. package/skills/dms-pages/REFERENCE.md +23 -2
@@ -0,0 +1,220 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * A data block whose source answers 404 shows what it reads does not exist:
4
+ * a translated not-found state (the server's message or `emptyState` over
5
+ * it), never "Check your connection" with a retry, and never the values it
6
+ * showed for the record it read before.
7
+ */
8
+ import {
9
+ type Component,
10
+ computed,
11
+ createApp,
12
+ defineComponent,
13
+ h,
14
+ nextTick,
15
+ reactive,
16
+ ref,
17
+ } from "vue";
18
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
19
+ import { readFetchNotFound } from "../layers/dms-ui/app/build/utils/fetchNotFound";
20
+
21
+ interface FetchFailure extends Error {
22
+ statusCode?: number;
23
+ data?: unknown;
24
+ }
25
+
26
+ const route = reactive<{ query: Record<string, unknown> }>({ query: {} });
27
+ const answers = new Map<string, unknown>();
28
+
29
+ function failure(statusCode: number, data?: unknown): FetchFailure {
30
+ return Object.assign(new Error(`${statusCode}`), {
31
+ name: "FetchError",
32
+ statusCode,
33
+ data,
34
+ });
35
+ }
36
+
37
+ vi.stubGlobal("useAuthFetch", () => ({
38
+ $authFetch: async (url: string) => {
39
+ const answer = answers.get(url);
40
+ if (answer instanceof Error) throw answer;
41
+ return answer;
42
+ },
43
+ }));
44
+ vi.stubGlobal("useDmsRoute", () => route);
45
+
46
+ vi.mock("../layers/dms-core/app/composables/watch/useWatch", () => ({
47
+ useWatch: () => ({ state: ref({}) }),
48
+ }));
49
+ vi.mock(
50
+ "../layers/dms-core/app/composables/components/useComponentEvent",
51
+ () => ({ useComponentEvent: () => ({}) }),
52
+ );
53
+
54
+ const { useChartFetch } = await import(
55
+ "../layers/dms-ui/app/composables/chart/useChartFetch"
56
+ );
57
+ const { useBlockItems } = await import(
58
+ "../layers/dms-ui/app/build/composables/blocks/useBlockItems"
59
+ );
60
+ const { default: BlockStatus } = await import(
61
+ "../layers/dms-ui/app/build/components/blocks/BlockStatus.vue"
62
+ );
63
+
64
+ // useChartFetch debounces every refresh past the first by 180ms.
65
+ const SETTLE_MS = 250;
66
+ const settle = () => new Promise((resolve) => setTimeout(resolve, SETTLE_MS));
67
+ const URL = "/api/kpis?workspace={{query.workspace}}";
68
+
69
+ const mounted: Array<{ unmount: () => void }> = [];
70
+
71
+ // Runs a composable inside a component's setup.
72
+ const Probe = defineComponent({
73
+ props: { run: { type: Function, required: true } },
74
+ setup(props) {
75
+ props.run();
76
+ return () => h("div");
77
+ },
78
+ });
79
+
80
+ const probe = (run: () => void) => mount({ render: () => h(Probe, { run }) });
81
+
82
+ function mount(component: Component): HTMLElement {
83
+ const app = createApp(component);
84
+ for (const name of ["UIcon", "UButton", "DmsLink"]) {
85
+ app.component(name, (props: Record<string, unknown>) =>
86
+ h("i", { "data-stub": name, "data-label": props.label }),
87
+ );
88
+ }
89
+ const host = document.createElement("div");
90
+ app.mount(host);
91
+ mounted.push(app);
92
+ return host;
93
+ }
94
+
95
+ beforeEach(() => {
96
+ answers.clear();
97
+ route.query = { workspace: "web" };
98
+ answers.set("/api/kpis?workspace=web", { items: [{ label: "web" }] });
99
+ answers.set(
100
+ "/api/kpis?workspace=gone",
101
+ failure(404, { code: "unknown_workspace", message: "No such workspace" }),
102
+ );
103
+ answers.set("/api/kpis?workspace=down", failure(500));
104
+ vi.stubGlobal("computed", computed);
105
+ vi.stubGlobal("useI18n", () => ({
106
+ t: (key: string) => `t(${key})`,
107
+ locale: ref("en"),
108
+ }));
109
+ vi.stubGlobal("useTranslation", () => ({
110
+ processI18n: (text: string) => (text.startsWith("$") ? `t(${text})` : text),
111
+ }));
112
+ });
113
+
114
+ afterEach(() => {
115
+ mounted.splice(0).forEach((app) => app.unmount());
116
+ });
117
+
118
+ async function switchWorkspace(workspace: string) {
119
+ route.query = { workspace };
120
+ await nextTick();
121
+ await settle();
122
+ }
123
+
124
+ describe("readFetchNotFound", () => {
125
+ it("reads a 404's message and emptyState", () => {
126
+ expect(
127
+ readFetchNotFound(
128
+ failure(404, { message: "Gone", emptyState: { title: "$x.y" } }),
129
+ ),
130
+ ).toEqual({ message: "Gone", emptyState: { title: "$x.y" } });
131
+ });
132
+
133
+ it("is null for any other failure", () => {
134
+ expect(readFetchNotFound(failure(500))).toBeNull();
135
+ expect(readFetchNotFound(new TypeError("Failed to fetch"))).toBeNull();
136
+ expect(readFetchNotFound(null)).toBeNull();
137
+ });
138
+ });
139
+
140
+ describe("useChartFetch on a 404", () => {
141
+ function mountFetch() {
142
+ const state: Partial<ReturnType<typeof useChartFetch<unknown>>> = {};
143
+ probe(() =>
144
+ Object.assign(state, useChartFetch<unknown>({ fetchUrl: URL })),
145
+ );
146
+ return state;
147
+ }
148
+
149
+ it("drops the values of the record it read before", async () => {
150
+ const state = mountFetch();
151
+ await settle();
152
+ expect(state.data?.value).toEqual({ items: [{ label: "web" }] });
153
+ await switchWorkspace("gone");
154
+ expect(state.data?.value).toBeNull();
155
+ expect(readFetchNotFound(state.error?.value)?.message).toBe(
156
+ "No such workspace",
157
+ );
158
+ });
159
+
160
+ it("keeps them on any other failure", async () => {
161
+ const state = mountFetch();
162
+ await settle();
163
+ await switchWorkspace("down");
164
+ expect(state.data?.value).toEqual({ items: [{ label: "web" }] });
165
+ });
166
+ });
167
+
168
+ describe("useBlockItems on a 404", () => {
169
+ it("reports not found, not a load error, and lists nothing", async () => {
170
+ const state: Partial<ReturnType<typeof useBlockItems<unknown>>> = {};
171
+ probe(() =>
172
+ Object.assign(
173
+ state,
174
+ useBlockItems<unknown>({ items: () => [], fetchUrl: URL }),
175
+ ),
176
+ );
177
+ await settle();
178
+ await switchWorkspace("gone");
179
+ expect(state.items?.value).toEqual([]);
180
+ expect(state.hasError?.value).toBe(false);
181
+ expect(state.notFound?.value?.message).toBe("No such workspace");
182
+ // Nothing left on screen to keep: the next failure is a load error.
183
+ await switchWorkspace("down");
184
+ expect(state.notFound?.value).toBeNull();
185
+ expect(state.hasError?.value).toBe(true);
186
+ });
187
+ });
188
+
189
+ describe("BlockStatus not-found", () => {
190
+ const render = (notFound: unknown) =>
191
+ mount({
192
+ render: () => h(BlockStatus, { state: "not-found", notFound }),
193
+ }).textContent;
194
+
195
+ it("says so in the user's language, with no retry", () => {
196
+ const text = render({});
197
+ expect(text).toContain("t(dms.blocks.not_found)");
198
+ expect(text).toContain("t(dms.blocks.not_found_hint)");
199
+ expect(text).not.toContain("load_error");
200
+ });
201
+
202
+ it("describes it with the server's message", () => {
203
+ expect(render({ message: "No such workspace" })).toContain(
204
+ "No such workspace",
205
+ );
206
+ });
207
+
208
+ it("lays the server's emptyState over it", () => {
209
+ const text = render({
210
+ message: "No such workspace",
211
+ emptyState: {
212
+ title: "$lang.unknown_title",
213
+ description: "$lang.unknown_hint",
214
+ },
215
+ });
216
+ expect(text).toContain("t($lang.unknown_title)");
217
+ expect(text).toContain("t($lang.unknown_hint)");
218
+ expect(text).not.toContain("No such workspace");
219
+ });
220
+ });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Choice cards sit two per row once the field is wide enough; `columns` sets
3
+ * how many, so three options fit one row instead of wrapping 2 + 1.
4
+ */
5
+ import { computed, createSSRApp, defineComponent, h } from "vue";
6
+ import { renderToString } from "vue/server-renderer";
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8
+ import ChoiceCards from "../layers/dms-ui/app/components/form/components/ChoiceCards.vue";
9
+
10
+ const Group = defineComponent({
11
+ props: { ui: { type: Object, default: undefined } },
12
+ setup: (props) => () =>
13
+ h("fieldset", { class: (props.ui as { fieldset?: string })?.fieldset }),
14
+ });
15
+
16
+ async function render(props: Record<string, unknown>): Promise<string> {
17
+ const app = createSSRApp({
18
+ render: () =>
19
+ h(ChoiceCards, {
20
+ items: [
21
+ { label: "30 days", value: 30 },
22
+ { label: "90 days", value: 90 },
23
+ { label: "365 days", value: 365 },
24
+ ],
25
+ modelValue: 30,
26
+ ...props,
27
+ }),
28
+ });
29
+ app.component("URadioGroup", Group);
30
+ app.component("UCheckboxGroup", Group);
31
+ return renderToString(app);
32
+ }
33
+
34
+ // A Nuxt auto-import, which ChoiceCards.vue uses unimported.
35
+ beforeEach(() => vi.stubGlobal("computed", computed));
36
+ afterEach(() => vi.unstubAllGlobals());
37
+
38
+ describe("ChoiceCards columns", () => {
39
+ it("lays the cards on a grid whose column count is a custom property", async () => {
40
+ const html = await render({});
41
+ expect(html).toContain(
42
+ "@min-[480px]:grid-cols-[repeat(var(--dms-choice-columns),minmax(0,1fr))]",
43
+ );
44
+ });
45
+
46
+ it("keeps two cards per row by default", async () => {
47
+ expect(await render({})).toContain("--dms-choice-columns:2");
48
+ });
49
+
50
+ it("puts `columns` cards per row", async () => {
51
+ expect(await render({ columns: 3 })).toContain("--dms-choice-columns:3");
52
+ });
53
+
54
+ it("keeps at least one whole column", async () => {
55
+ expect(await render({ columns: 0 })).toContain("--dms-choice-columns:1");
56
+ expect(await render({ columns: 2.6, multiple: true })).toContain(
57
+ "--dms-choice-columns:3",
58
+ );
59
+ });
60
+ });
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ childComponentId,
4
+ siblingComponentId,
5
+ } from "../layers/dms-ui/app/utils/componentId";
6
+
7
+ describe("siblingComponentId", () => {
8
+ it("names a top-level block's neighbour by its key", () => {
9
+ expect(siblingComponentId("preview", "form")).toBe("form");
10
+ });
11
+
12
+ it("names a nested block's neighbour under the same parent", () => {
13
+ const row = childComponentId("editor", "row");
14
+ expect(siblingComponentId(childComponentId(row, "preview"), "form")).toBe(
15
+ "editor-child-row-child-form",
16
+ );
17
+ });
18
+ });
@@ -0,0 +1,269 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import {
4
+ type App,
5
+ computed,
6
+ createApp,
7
+ defineComponent,
8
+ h,
9
+ nextTick,
10
+ ref,
11
+ } from "vue";
12
+ import { createI18n } from "vue-i18n";
13
+ import ConfirmModal from "../layers/dms-ui/app/components/confirm/ConfirmModal.vue";
14
+ import { resolveConfirmDialog } from "../layers/dms-ui/app/build/composables/confirm/confirmDialogTexts";
15
+ import {
16
+ downloadFilename,
17
+ secondaryTargetRunner,
18
+ } from "../layers/dms-ui/app/build/composables/confirm/secondaryTarget";
19
+
20
+ vi.mock("reka-ui", async () => {
21
+ const { defineComponent: define, h: render } = await import("vue");
22
+ const Text = define({
23
+ setup:
24
+ (_, { slots }) =>
25
+ () =>
26
+ render("div", slots.default?.()),
27
+ });
28
+ return { DialogTitle: Text, DialogDescription: Text };
29
+ });
30
+
31
+ const i18n = createI18n({
32
+ legacy: false,
33
+ locale: "en",
34
+ messages: { en: { lang: { download: "Download {file}" } } },
35
+ });
36
+ const t = i18n.global.t as (
37
+ key: string,
38
+ params: Record<string, unknown>,
39
+ plural?: number,
40
+ ) => string;
41
+
42
+ describe("a confirmation's secondary action, resolved", () => {
43
+ it("fills its label, URL and file name with the row and the params", async () => {
44
+ const runTarget = vi.fn();
45
+ const options = resolveConfirmDialog(
46
+ {
47
+ title: "Remove {name}?",
48
+ params: { file: "fr.json" },
49
+ secondaryAction: {
50
+ label: "$lang.download",
51
+ icon: "i-ph-download-simple",
52
+ target: {
53
+ type: "download",
54
+ url: "/legacy/lang/{workspace}/{code}.json",
55
+ filename: "{workspace}-{file}",
56
+ },
57
+ },
58
+ },
59
+ { name: "French", code: "fr", workspace: "site" },
60
+ t,
61
+ runTarget,
62
+ );
63
+ expect(options.secondaryAction).toMatchObject({
64
+ label: "Download fr.json",
65
+ icon: "i-ph-download-simple",
66
+ });
67
+ await options.secondaryAction!.run();
68
+ expect(runTarget).toHaveBeenCalledWith({
69
+ type: "download",
70
+ url: "/legacy/lang/site/fr.json",
71
+ filename: "site-fr.json",
72
+ });
73
+ });
74
+
75
+ it("is left out when nothing can run it", () => {
76
+ const options = resolveConfirmDialog(
77
+ {
78
+ title: "Remove?",
79
+ secondaryAction: {
80
+ label: "Docs",
81
+ target: { type: "external", url: "https://antelopejs.com" },
82
+ },
83
+ },
84
+ undefined,
85
+ t,
86
+ );
87
+ expect(options.secondaryAction).toBeUndefined();
88
+ });
89
+ });
90
+
91
+ describe("a confirmation's secondary target", () => {
92
+ afterEach(() => {
93
+ vi.restoreAllMocks();
94
+ vi.unstubAllGlobals();
95
+ });
96
+
97
+ it("names a download as declared, else as served, else after its URL", () => {
98
+ const target = {
99
+ type: "download" as const,
100
+ url: "/files/fr%20FR.json?v=2",
101
+ };
102
+ expect(downloadFilename({ ...target, filename: "a.json" }, null)).toBe(
103
+ "a.json",
104
+ );
105
+ expect(downloadFilename(target, 'attachment; filename="served.json"')).toBe(
106
+ "served.json",
107
+ );
108
+ expect(downloadFilename(target, null)).toBe("fr FR.json");
109
+ });
110
+
111
+ it("fetches a download with the session and saves it", async () => {
112
+ const blob = new Blob(["{}"]);
113
+ const fetchRaw = vi.fn(async () => ({
114
+ _data: blob,
115
+ headers: new Headers(),
116
+ }));
117
+ const createObjectURL = vi.fn(() => "blob:fr");
118
+ const revokeObjectURL = vi.fn();
119
+ vi.stubGlobal(
120
+ "URL",
121
+ Object.assign(URL, { createObjectURL, revokeObjectURL }),
122
+ );
123
+ const saved: string[] = [];
124
+ vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(function (
125
+ this: HTMLAnchorElement,
126
+ ) {
127
+ saved.push(`${this.href} ${this.download}`);
128
+ });
129
+
130
+ await secondaryTargetRunner(fetchRaw)({
131
+ type: "download",
132
+ url: "/legacy/lang/fr.json",
133
+ });
134
+
135
+ expect(fetchRaw).toHaveBeenCalledWith("/legacy/lang/fr.json", {
136
+ responseType: "blob",
137
+ });
138
+ expect(createObjectURL).toHaveBeenCalledWith(blob);
139
+ expect(saved).toEqual(["blob:fr fr.json"]);
140
+ expect(revokeObjectURL).toHaveBeenCalledWith("blob:fr");
141
+ });
142
+
143
+ it("opens a link in a new tab when asked", async () => {
144
+ const open = vi.spyOn(window, "open").mockReturnValue(null);
145
+ const fetchRaw = vi.fn();
146
+ await secondaryTargetRunner(fetchRaw)({
147
+ type: "external",
148
+ url: "https://antelopejs.com",
149
+ newTab: true,
150
+ });
151
+ expect(open).toHaveBeenCalledWith(
152
+ "https://antelopejs.com",
153
+ "_blank",
154
+ "noopener,noreferrer",
155
+ );
156
+ expect(fetchRaw).not.toHaveBeenCalled();
157
+ });
158
+ });
159
+
160
+ describe("ConfirmModal secondary action", () => {
161
+ let app: App | undefined;
162
+ let host: HTMLDivElement;
163
+ const closed = vi.fn();
164
+
165
+ const Slots = defineComponent({
166
+ setup:
167
+ (_, { slots }) =>
168
+ () =>
169
+ h("div", [slots.header?.(), slots.body?.(), slots.footer?.()]),
170
+ });
171
+ const Button = defineComponent({
172
+ props: { label: String },
173
+ emits: ["click"],
174
+ setup:
175
+ (props, { emit }) =>
176
+ () =>
177
+ h("button", { onClick: () => emit("click") }, props.label),
178
+ });
179
+ const Alert = defineComponent({
180
+ props: { title: String },
181
+ setup: (props) => () => h("p", { "data-alert": "" }, props.title),
182
+ });
183
+ const Empty = defineComponent({ setup: () => () => null });
184
+
185
+ async function flush(): Promise<void> {
186
+ for (let tick = 0; tick < 6; tick++) await nextTick();
187
+ }
188
+
189
+ function mount(props: Record<string, unknown>): void {
190
+ app = createApp(ConfirmModal, { onClose: closed, ...props });
191
+ app.config.globalProperties.$t = (key: string) => key;
192
+ app.component("UModal", Slots);
193
+ app.component("UButton", Button);
194
+ app.component("UAlert", Alert);
195
+ for (const name of ["UIcon", "UKbd", "UInput", "UFormField", "I18nT"]) {
196
+ app.component(name, Empty);
197
+ }
198
+ app.component("DmsIconWell", Empty);
199
+ app.mount(host);
200
+ }
201
+
202
+ const button = (label: string) =>
203
+ [...host.querySelectorAll("button")].find(
204
+ (entry) => entry.textContent === label,
205
+ );
206
+
207
+ beforeEach(() => {
208
+ vi.stubGlobal("ref", ref);
209
+ vi.stubGlobal("computed", computed);
210
+ vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
211
+ vi.stubGlobal("useTranslation", () => ({
212
+ processApiMessage: (message: string) => message,
213
+ }));
214
+ vi.stubGlobal("resolveDmsComponent", () => undefined);
215
+ host = document.createElement("div");
216
+ document.body.append(host);
217
+ closed.mockReset();
218
+ });
219
+
220
+ afterEach(() => {
221
+ app?.unmount();
222
+ app = undefined;
223
+ host.remove();
224
+ vi.unstubAllGlobals();
225
+ });
226
+
227
+ it("draws no third button without one", async () => {
228
+ mount({ title: "Remove French?" });
229
+ await flush();
230
+ expect(host.querySelectorAll("button")).toHaveLength(3);
231
+ });
232
+
233
+ it("runs it and stays open, the action still to confirm", async () => {
234
+ const run = vi.fn().mockResolvedValue(undefined);
235
+ const onConfirm = vi.fn().mockResolvedValue(undefined);
236
+ mount({
237
+ title: "Remove French?",
238
+ secondaryAction: { label: "Download fr.json", run },
239
+ onConfirm,
240
+ });
241
+ await flush();
242
+
243
+ button("Download fr.json")!.click();
244
+ await flush();
245
+
246
+ expect(run).toHaveBeenCalledTimes(1);
247
+ expect(closed).not.toHaveBeenCalled();
248
+ expect(onConfirm).not.toHaveBeenCalled();
249
+
250
+ button("dms.confirm.confirm")!.click();
251
+ await flush();
252
+ expect(closed).toHaveBeenCalledWith(true);
253
+ });
254
+
255
+ it("shows a failure in the dialog's alert, the dialog open", async () => {
256
+ const run = vi.fn().mockRejectedValue(new Error("offline"));
257
+ mount({
258
+ title: "Remove French?",
259
+ secondaryAction: { label: "Download fr.json", run },
260
+ });
261
+ await flush();
262
+
263
+ button("Download fr.json")!.click();
264
+ await flush();
265
+
266
+ expect(host.querySelector("[data-alert]")).not.toBeNull();
267
+ expect(closed).not.toHaveBeenCalled();
268
+ });
269
+ });