@antelopejs/dms 0.7.1 → 0.7.3

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 (110) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/hooks/tenant-instances.d.ts +1 -0
  3. package/dist/hooks/tenant-instances.js +13 -0
  4. package/dist/hooks/tenant-instances.js.map +1 -0
  5. package/dist/index.js +3 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/test/unit/interfaces/dms/page-topics.test.js +67 -0
  8. package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
  9. package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js +1 -0
  10. package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js.map +1 -1
  11. package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.d.ts +1 -0
  12. package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js +46 -0
  13. package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js.map +1 -0
  14. package/dist/test/unit/interfaces/dms-base/column-display.test.js +11 -0
  15. package/dist/test/unit/interfaces/dms-base/column-display.test.js.map +1 -1
  16. package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js +31 -0
  17. package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js.map +1 -1
  18. package/dist/test/unit/utils/tenant-instances.test.d.ts +1 -0
  19. package/dist/test/unit/utils/tenant-instances.test.js +53 -0
  20. package/dist/test/unit/utils/tenant-instances.test.js.map +1 -0
  21. package/dist/utils/index.d.ts +1 -0
  22. package/dist/utils/index.js +1 -0
  23. package/dist/utils/index.js.map +1 -1
  24. package/dist/utils/tenant-instances.d.ts +2 -0
  25. package/dist/utils/tenant-instances.js +38 -0
  26. package/dist/utils/tenant-instances.js.map +1 -0
  27. package/dist/validation/auth.schema.d.ts +2 -2
  28. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +4 -0
  29. package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +35 -23
  30. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockActions.vue +34 -6
  31. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +26 -8
  32. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +3 -1
  33. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +1 -1
  34. package/frontend-vue/layers/dms-ui/app/build/components/table/MonoCell.vue +3 -2
  35. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +24 -13
  36. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +1 -1
  37. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +12 -5
  38. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useRefreshTriggers.ts +40 -0
  39. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +15 -4
  40. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +85 -22
  41. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +10 -2
  42. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +79 -0
  43. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +94 -0
  44. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +41 -5
  45. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +3 -0
  46. package/frontend-vue/layers/dms-ui/app/build/utils/codeEditor.ts +2 -13
  47. package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +59 -0
  48. package/frontend-vue/layers/dms-ui/app/build/utils/codeLanguages.ts +36 -0
  49. package/frontend-vue/layers/dms-ui/app/build/utils/codePalette.ts +32 -0
  50. package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +55 -0
  51. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +77 -20
  52. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +15 -4
  53. package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +111 -25
  54. package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +95 -0
  55. package/frontend-vue/layers/dms-ui/app/components/blocks/EmptyStateBlock.vue +18 -0
  56. package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +8 -0
  57. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +21 -9
  58. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +8 -0
  59. package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +97 -0
  60. package/frontend-vue/layers/dms-ui/app/components/empty-state/EmptyState.vue +13 -4
  61. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +14 -3
  62. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +2 -8
  63. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +19 -0
  64. package/frontend-vue/layers/dms-ui/app/components/master-detail/MasterDetail.vue +54 -48
  65. package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +24 -9
  66. package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +4 -1
  67. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +66 -74
  68. package/frontend-vue/layers/dms-ui/app/components/top-list/internal/TopListRow.vue +4 -3
  69. package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +5 -2
  70. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +2 -29
  71. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +2 -1
  72. package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +2 -0
  73. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +4 -0
  74. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +2 -2
  75. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +1 -0
  76. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +1 -0
  77. package/frontend-vue/package.json +4 -3
  78. package/frontend-vue/pnpm-lock.yaml +8 -5
  79. package/frontend-vue/tests/activity-feed-days.test.ts +26 -12
  80. package/frontend-vue/tests/banner-block-fetch.test.ts +162 -0
  81. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +100 -0
  82. package/frontend-vue/tests/block-items-period-realtime.test.ts +147 -0
  83. package/frontend-vue/tests/block-route-params.test.ts +48 -0
  84. package/frontend-vue/tests/code-block.test.ts +155 -0
  85. package/frontend-vue/tests/code-highlight.test.ts +72 -0
  86. package/frontend-vue/tests/composed-text-blocks.test.ts +5 -0
  87. package/frontend-vue/tests/composed-text-more-blocks.test.ts +391 -0
  88. package/frontend-vue/tests/drawer-target-direction.test.ts +205 -0
  89. package/frontend-vue/tests/empty-state-late-slots.test.ts +53 -0
  90. package/frontend-vue/tests/flow-canvas-delete-badge.test.ts +88 -0
  91. package/frontend-vue/tests/form-redirect-placeholders.test.ts +11 -0
  92. package/frontend-vue/tests/key-value-list-copy.test.ts +114 -0
  93. package/frontend-vue/tests/master-detail-layout.test.ts +53 -0
  94. package/frontend-vue/tests/mono-cell-truncate.test.ts +50 -0
  95. package/frontend-vue/tests/nav-card-grid-empty-category.test.ts +1 -0
  96. package/frontend-vue/tests/page-fill-height.test.ts +20 -3
  97. package/frontend-vue/tests/route-param-occurrences.test.ts +9 -0
  98. package/frontend-vue/tests/table-actions-search-label.test.ts +147 -0
  99. package/frontend-vue/tests/table-clear-filters.test.ts +22 -0
  100. package/frontend-vue/tests/table-empty-body-viewport.test.ts +25 -0
  101. package/frontend-vue/tests/table-metric-cells.test.ts +22 -0
  102. package/frontend-vue/tests/table-pills-cell.test.ts +126 -0
  103. package/frontend-vue/tests/table-source-navigation.test.ts +123 -0
  104. package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +137 -0
  105. package/frontend-vue/tests/table-view-refresh.test.ts +84 -0
  106. package/frontend-vue/tests/table-view-sources.test.ts +67 -0
  107. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  108. package/package.json +2 -2
  109. package/skills/dms/SKILL.md +14 -3
  110. package/skills/dms-pages/REFERENCE.md +17 -1
@@ -57,6 +57,11 @@ vi.mock(
57
57
  () => ({ useComponentEvent: () => ({}) }),
58
58
  );
59
59
 
60
+ vi.mock(
61
+ "../layers/dms-ui/app/build/composables/actions/useActionTargets",
62
+ () => ({ useActionTargets: () => ({ handleCustomButton: () => {} }) }),
63
+ );
64
+
60
65
  // The generic components only lay out what the block resolved: their props
61
66
  // are what is checked here.
62
67
  const propsProbe = (name: string, keys: string[]) => ({
@@ -0,0 +1,391 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * ActivityFeed, Meter, TopListCard and NavCardGrid draw a composed text
4
+ * wherever they draw a text — from their options or from the JSON their
5
+ * route answers — in the reader's language, and keep drawing plain and `$`
6
+ * strings as before.
7
+ */
8
+ import {
9
+ computed,
10
+ createApp,
11
+ defineComponent,
12
+ h,
13
+ reactive,
14
+ ref,
15
+ type Component,
16
+ } from "vue";
17
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
18
+ import { createI18n } from "vue-i18n";
19
+ import { resolveI18nKey } from "../layers/dms-core/app/composables/translation/useTranslation";
20
+ import { setRegionalPreferencesSource } from "../layers/dms-core/app/utils/regional";
21
+
22
+ const NOW = Date.UTC(2026, 9, 10, 12);
23
+ const THREE_MINUTES_AGO = new Date(NOW - 3 * 60 * 1000).toISOString();
24
+
25
+ const i18n = createI18n({
26
+ legacy: false,
27
+ locale: "en",
28
+ fallbackLocale: "en",
29
+ missingWarn: false,
30
+ fallbackWarn: false,
31
+ messages: {
32
+ en: {
33
+ mail: {
34
+ to_review:
35
+ "no message to review | {count} message to review | {count} messages to review",
36
+ bounced: "{count} bounced on {list}",
37
+ device: "{browser} on {os}",
38
+ signed_in: "Signed in on {device}",
39
+ sent: "Sent {when}",
40
+ },
41
+ media: {
42
+ files: "{count} file | {count} files",
43
+ used: "{size} used",
44
+ quota: "{used} of {total}",
45
+ library: "Library",
46
+ new: "New",
47
+ },
48
+ api: { last_call: "Last call {at}" },
49
+ },
50
+ fr: {
51
+ mail: {
52
+ to_review:
53
+ "aucun message à traiter | {count} message à traiter | {count} messages à traiter",
54
+ bounced: "{count} rejetés sur {list}",
55
+ device: "{browser} sous {os}",
56
+ signed_in: "Connexion sur {device}",
57
+ sent: "Envoyé {when}",
58
+ },
59
+ media: {
60
+ files: "{count} fichier | {count} fichiers",
61
+ used: "{size} utilisés",
62
+ quota: "{used} sur {total}",
63
+ library: "Médiathèque",
64
+ new: "Nouveau",
65
+ },
66
+ api: { last_call: "Dernier appel {at}" },
67
+ },
68
+ },
69
+ });
70
+
71
+ const meterData = ref<Record<string, unknown> | null>(null);
72
+
73
+ vi.stubGlobal("useAuthFetch", () => ({
74
+ $authFetch: async () => ({ items: [] }),
75
+ }));
76
+ vi.stubGlobal("useDmsRoute", () => reactive({ query: {} }));
77
+
78
+ vi.mock("../layers/dms-core/app/composables/watch/useWatch", () => ({
79
+ useWatch: () => ({ state: ref({}) }),
80
+ }));
81
+ vi.mock(
82
+ "../layers/dms-core/app/composables/components/useComponentEvent",
83
+ () => ({ useComponentEvent: () => ({}) }),
84
+ );
85
+ vi.mock("../layers/dms-ui/app/composables/chart/useChartFetch", () => ({
86
+ useChartFetch: () => ({
87
+ data: meterData,
88
+ isLoading: ref(false),
89
+ error: ref(null),
90
+ refresh: async () => {},
91
+ }),
92
+ }));
93
+ vi.mock(
94
+ "#dms-layout/app/build/composables/navigation/useCategoryNavCards",
95
+ () => ({
96
+ useCategoryNavCards: () => ({ cards: computed(() => []) }),
97
+ usePreviewEntryVeil: () => ({
98
+ state: () => null,
99
+ label: () => "",
100
+ detail: () => undefined,
101
+ isActive: ref(false),
102
+ }),
103
+ }),
104
+ );
105
+
106
+ // The generic components only lay out what the block resolved: their props
107
+ // are what is checked here.
108
+ const { propsProbe } = vi.hoisted(() => ({
109
+ propsProbe: (name: string, keys: string[]) => async () => {
110
+ const vue = await import("vue");
111
+ return {
112
+ default: vue.defineComponent({
113
+ props: keys,
114
+ setup:
115
+ (props, { slots }) =>
116
+ () =>
117
+ vue.h("div", [
118
+ vue.h("pre", { "data-probe": name }, JSON.stringify(props)),
119
+ slots.default?.(),
120
+ ]),
121
+ }),
122
+ };
123
+ },
124
+ }));
125
+ vi.mock(
126
+ "../layers/dms-ui/app/components/activity/ActivityItem.vue",
127
+ propsProbe("activity-item", ["title", "subtitle", "trailing"]),
128
+ );
129
+ vi.mock(
130
+ "../layers/dms-ui/app/components/meter/Meter.vue",
131
+ propsProbe("meter", ["label", "hint", "valueLabel", "segments"]),
132
+ );
133
+ vi.mock(
134
+ "../layers/dms-ui/app/components/card/NavCard.vue",
135
+ propsProbe("nav-card", ["title", "description", "state", "tag", "readout"]),
136
+ );
137
+ vi.mock(
138
+ "../layers/dms-ui/app/build/components/permission/PermissionVeil.vue",
139
+ propsProbe("veil", []),
140
+ );
141
+
142
+ const ActivityFeed = (
143
+ await import("../layers/dms-ui/app/components/activity-feed/ActivityFeed.vue")
144
+ ).default;
145
+ const MeterBlock = (
146
+ await import("../layers/dms-ui/app/components/meter/MeterBlock.vue")
147
+ ).default;
148
+ const TopListRow = (
149
+ await import(
150
+ "../layers/dms-ui/app/components/top-list/internal/TopListRow.vue"
151
+ )
152
+ ).default;
153
+ const NavCardGridBlock = (
154
+ await import("../layers/dms-ui/app/components/blocks/NavCardGridBlock.vue")
155
+ ).default;
156
+
157
+ const mounted: Array<{ unmount: () => void }> = [];
158
+ const plainSpaces = (text: string) => text.replace(/[\u00a0\u202f]/g, " ");
159
+
160
+ function mount(component: Component, props: Record<string, unknown>) {
161
+ const root = document.createElement("div");
162
+ const app = createApp(component, props);
163
+ app.component(
164
+ "DmsCard",
165
+ defineComponent({
166
+ setup:
167
+ (_, { slots }) =>
168
+ () =>
169
+ h("div", slots.default?.()),
170
+ }),
171
+ );
172
+ app.mount(root);
173
+ mounted.push(app);
174
+ return root;
175
+ }
176
+
177
+ function probes(root: HTMLElement, name: string): Record<string, unknown>[] {
178
+ return [...root.querySelectorAll(`[data-probe="${name}"]`)].map((probe) =>
179
+ JSON.parse(plainSpaces(probe.textContent ?? "{}")),
180
+ );
181
+ }
182
+
183
+ const count = (value: number) => ({ type: "count", value });
184
+
185
+ beforeEach(() => {
186
+ vi.useFakeTimers({ now: NOW, toFake: ["Date"] });
187
+ i18n.global.locale.value = "en";
188
+ meterData.value = null;
189
+ setRegionalPreferencesSource(() => ({ timeZone: "UTC" }));
190
+ vi.stubGlobal("useI18n", () => ({
191
+ t: i18n.global.t,
192
+ locale: i18n.global.locale,
193
+ }));
194
+ vi.stubGlobal("useTranslation", () => ({
195
+ processI18n: (key: string, params?: Record<string, unknown> | null) =>
196
+ resolveI18nKey(i18n.global.t, key, params),
197
+ }));
198
+ vi.stubGlobal("useDmsCookie", () => ref<string[]>([]));
199
+ });
200
+
201
+ afterEach(() => {
202
+ mounted.splice(0).forEach((app) => app.unmount());
203
+ setRegionalPreferencesSource(() => ({}));
204
+ vi.useRealTimers();
205
+ });
206
+
207
+ describe("ActivityFeed", () => {
208
+ const items = [
209
+ {
210
+ id: "review",
211
+ title: { key: "mail.to_review", params: { count: count(3) } },
212
+ meta: [
213
+ {
214
+ key: "mail.sent",
215
+ params: { when: { type: "relative", value: THREE_MINUTES_AGO } },
216
+ },
217
+ "Inbox",
218
+ ],
219
+ time: { key: "media.new" },
220
+ },
221
+ {
222
+ id: "bounced",
223
+ title: "$mail.bounced",
224
+ params: { count: count(1284), list: "Newsletter" },
225
+ },
226
+ {
227
+ id: "legacy",
228
+ title: "$mail.signed_in",
229
+ meta: ["$mail.device"],
230
+ params: { device: "$mail.device", browser: "Chrome", os: "Windows" },
231
+ },
232
+ ];
233
+
234
+ it("composes titles, details and the trailing text of its entries", () => {
235
+ const root = mount(ActivityFeed, { items, groupByDay: false, card: false });
236
+
237
+ expect(probes(root, "activity-item")).toEqual([
238
+ {
239
+ title: "3 messages to review",
240
+ subtitle: "Sent 3 minutes ago · Inbox",
241
+ trailing: "New",
242
+ },
243
+ { title: "1,284 bounced on Newsletter" },
244
+ {
245
+ title: "Signed in on Chrome on Windows",
246
+ subtitle: "Chrome on Windows",
247
+ },
248
+ ]);
249
+ });
250
+
251
+ it("picks the plural form of a `$` title from its count value, in French", () => {
252
+ i18n.global.locale.value = "fr";
253
+ const root = mount(ActivityFeed, {
254
+ items: [
255
+ { title: "$mail.to_review", params: { count: count(1) } },
256
+ {
257
+ title: "$mail.signed_in",
258
+ params: { device: "$mail.device", browser: "Firefox", os: "Linux" },
259
+ },
260
+ ],
261
+ groupByDay: false,
262
+ card: false,
263
+ });
264
+
265
+ expect(probes(root, "activity-item").map((item) => item.title)).toEqual([
266
+ "1 message à traiter",
267
+ "Connexion sur Firefox sous Linux",
268
+ ]);
269
+ });
270
+ });
271
+
272
+ describe("MeterBlock", () => {
273
+ it("composes the hint, the value text and the segment legends", () => {
274
+ i18n.global.locale.value = "fr";
275
+ const root = mount(MeterBlock, {
276
+ label: "$media.library",
277
+ hint: { key: "media.files", params: { count: count(2) } },
278
+ valueLabel: {
279
+ key: "media.quota",
280
+ params: {
281
+ used: { type: "number", value: 0.42, format: "percent" },
282
+ total: "10 Go",
283
+ },
284
+ },
285
+ segments: [
286
+ {
287
+ value: 3,
288
+ label: { key: "media.files", params: { count: count(1) } },
289
+ },
290
+ { value: 2, label: "Plain" },
291
+ ],
292
+ });
293
+
294
+ expect(probes(root, "meter")).toEqual([
295
+ {
296
+ label: "Médiathèque",
297
+ hint: "2 fichiers",
298
+ valueLabel: "42 % sur 10 Go",
299
+ segments: [
300
+ { value: 3, label: "1 fichier" },
301
+ { value: 2, label: "Plain" },
302
+ ],
303
+ },
304
+ ]);
305
+ });
306
+
307
+ it("composes the texts its route answers", () => {
308
+ meterData.value = {
309
+ value: 4,
310
+ max: 10,
311
+ hint: { key: "media.used", params: { size: "4 GB" } },
312
+ };
313
+ const root = mount(MeterBlock, { fetchUrl: "/api/storage" });
314
+
315
+ expect(probes(root, "meter")[0]?.hint).toBe("4 GB used");
316
+ });
317
+ });
318
+
319
+ describe("TopListRow", () => {
320
+ it("composes the title and the description of a row", () => {
321
+ i18n.global.locale.value = "fr";
322
+ const root = mount(TopListRow, {
323
+ item: {
324
+ id: "orders",
325
+ title: "$media.library",
326
+ description: {
327
+ key: "api.last_call",
328
+ params: { at: { type: "relative", value: THREE_MINUTES_AGO } },
329
+ },
330
+ value: 12,
331
+ },
332
+ showRank: false,
333
+ rankLabel: "1",
334
+ rankClass: "",
335
+ hasAnyIcon: false,
336
+ showSparkline: false,
337
+ sparklineAccent: "primary",
338
+ showDelta: false,
339
+ formattedValue: "12",
340
+ });
341
+
342
+ expect(plainSpaces(root.textContent ?? "")).toContain("Médiathèque");
343
+ expect(plainSpaces(root.textContent ?? "")).toContain(
344
+ "Dernier appel il y a 3 minutes",
345
+ );
346
+ });
347
+
348
+ it("keeps a plain title as written", () => {
349
+ const root = mount(TopListRow, {
350
+ item: { id: 1, title: "GET /orders", value: 1 },
351
+ showRank: false,
352
+ rankLabel: "1",
353
+ rankClass: "",
354
+ hasAnyIcon: false,
355
+ showSparkline: false,
356
+ sparklineAccent: "primary",
357
+ showDelta: false,
358
+ formattedValue: "1",
359
+ });
360
+
361
+ expect(root.textContent).toContain("GET /orders");
362
+ });
363
+ });
364
+
365
+ describe("NavCardGridBlock", () => {
366
+ it("composes the title, description, tag, state and readout of a card", () => {
367
+ const root = mount(NavCardGridBlock, {
368
+ items: [
369
+ {
370
+ icon: "i-ph-images",
371
+ to: "/media",
372
+ title: { key: "media.library" },
373
+ description: "$media.new",
374
+ tag: { key: "media.new" },
375
+ state: { key: "media.files", params: { count: count(1284) } },
376
+ readout: [{ key: "media.used", params: { size: "4 GB" } }, "plain"],
377
+ },
378
+ ],
379
+ });
380
+
381
+ expect(probes(root, "nav-card")).toEqual([
382
+ {
383
+ title: "Library",
384
+ description: "New",
385
+ tag: "New",
386
+ state: "1,284 files",
387
+ readout: ["4 GB used", "plain"],
388
+ },
389
+ ]);
390
+ });
391
+ });
@@ -0,0 +1,205 @@
1
+ // @vitest-environment jsdom
2
+ import {
3
+ computed,
4
+ createApp,
5
+ defineComponent,
6
+ h,
7
+ nextTick,
8
+ provide,
9
+ reactive,
10
+ ref,
11
+ watch,
12
+ type App,
13
+ } from "vue";
14
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
15
+ import { useActionTargets } from "../layers/dms-ui/app/build/composables/actions/useActionTargets";
16
+ import type { ActionTarget } from "../layers/dms-ui/app/composables/table-view/types/action-target";
17
+
18
+ vi.mock("reka-ui", () => {
19
+ const passThrough = defineComponent({
20
+ setup: (_props, { slots }) => () => h("div", slots.default?.()),
21
+ });
22
+ return {
23
+ DialogTitle: passThrough,
24
+ DialogDescription: passThrough,
25
+ VisuallyHidden: passThrough,
26
+ };
27
+ });
28
+
29
+ let app: App | undefined;
30
+ let host: HTMLDivElement;
31
+
32
+ beforeEach(() => {
33
+ host = document.createElement("div");
34
+ document.body.append(host);
35
+ });
36
+
37
+ afterEach(() => {
38
+ app?.unmount();
39
+ app = undefined;
40
+ host.remove();
41
+ vi.unstubAllGlobals();
42
+ });
43
+
44
+ describe("a drawer action target's direction", () => {
45
+ const opened: Array<Record<string, unknown>> = [];
46
+
47
+ const mountTargets = () => {
48
+ let targets: ReturnType<typeof useActionTargets> | undefined;
49
+ app = createApp(
50
+ defineComponent({
51
+ setup() {
52
+ targets = useActionTargets({
53
+ api: vi.fn() as never,
54
+ pageId: "page",
55
+ componentId: "table",
56
+ handleApiError: vi.fn(),
57
+ });
58
+ return () => h("div");
59
+ },
60
+ }),
61
+ );
62
+ app.mount(host);
63
+ return targets!;
64
+ };
65
+
66
+ const drawerTarget = (
67
+ direction?: "right" | "left" | "top" | "bottom",
68
+ ): ActionTarget => ({
69
+ type: "drawer",
70
+ component: { componentName: "Inspector" },
71
+ ...(direction && { direction }),
72
+ });
73
+
74
+ beforeEach(() => {
75
+ opened.length = 0;
76
+ vi.stubGlobal("useToast", () => ({ add: vi.fn() }));
77
+ vi.stubGlobal("useTranslation", () => ({
78
+ processI18n: (text: string) => text,
79
+ processApiMessage: (text: string) => text,
80
+ }));
81
+ vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
82
+ vi.stubGlobal("useModal", () => ({ open: vi.fn() }));
83
+ vi.stubGlobal("useDrawer", () => ({
84
+ open: (options: Record<string, unknown>) => {
85
+ opened.push(options);
86
+ return {
87
+ result: new Promise(() => {}),
88
+ close: vi.fn(),
89
+ patch: vi.fn(),
90
+ };
91
+ },
92
+ }));
93
+ vi.stubGlobal("useExportJob", () => ({ runJob: vi.fn() }));
94
+ vi.stubGlobal("useConfirm", () => ({ confirm: vi.fn(async () => true) }));
95
+ vi.stubGlobal("useAuthFetch", () => ({ $authFetch: vi.fn() }));
96
+ vi.stubGlobal("resolveDmsComponent", () =>
97
+ defineComponent({ render: () => null }),
98
+ );
99
+ });
100
+
101
+ it("opens a toolbar or header button's drawer from the edge it names", async () => {
102
+ const targets = mountTargets();
103
+ targets.handleCustomButton({
104
+ label: "Inspect",
105
+ target: drawerTarget("right"),
106
+ });
107
+ await vi.waitFor(() => expect(opened).toHaveLength(1));
108
+ expect(opened[0]!.direction).toBe("right");
109
+ });
110
+
111
+ it("opens a row action's drawer from the edge it names", async () => {
112
+ const targets = mountTargets();
113
+ targets.handleCustomRowAction(
114
+ { label: "Inspect", target: drawerTarget("left") },
115
+ { _id: "r1" },
116
+ );
117
+ await vi.waitFor(() => expect(opened).toHaveLength(1));
118
+ expect(opened[0]!.direction).toBe("left");
119
+ });
120
+
121
+ it("leaves the direction to the drawer when the target names none", async () => {
122
+ const targets = mountTargets();
123
+ targets.handleCustomButton({ label: "Inspect", target: drawerTarget() });
124
+ await vi.waitFor(() => expect(opened).toHaveLength(1));
125
+ expect(opened[0]!.direction).toBe(undefined);
126
+ });
127
+ });
128
+
129
+ describe("the drawer container", () => {
130
+ interface DrawerStubProps {
131
+ direction?: string;
132
+ ui?: Record<string, string>;
133
+ }
134
+
135
+ const rendered = reactive<DrawerStubProps>({});
136
+
137
+ const mountDrawer = async (props: Record<string, unknown>) => {
138
+ const { default: DynamicDrawer } = await import(
139
+ "../layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue"
140
+ );
141
+ app = createApp({
142
+ setup: () => () =>
143
+ h(DynamicDrawer, {
144
+ title: "Inspector",
145
+ containerId: "drawer-1",
146
+ component: defineComponent({ render: () => null }),
147
+ ...props,
148
+ }),
149
+ });
150
+ app.component(
151
+ "UDrawer",
152
+ defineComponent({
153
+ props: { direction: String, ui: Object },
154
+ setup(stubProps, { slots }) {
155
+ return () => {
156
+ Object.assign(rendered, stubProps);
157
+ return h("div", [slots.header?.(), slots.body?.()]);
158
+ };
159
+ },
160
+ }),
161
+ );
162
+ app.component(
163
+ "UContainer",
164
+ defineComponent({
165
+ setup: (_props, { slots }) => () =>
166
+ h("div", { "data-container": "" }, slots.default?.()),
167
+ }),
168
+ );
169
+ app.component("UButton", () => h("button"));
170
+ app.component("USkeleton", () => h("span"));
171
+ app.mount(host);
172
+ await nextTick();
173
+ };
174
+
175
+ beforeEach(() => {
176
+ rendered.direction = undefined;
177
+ rendered.ui = undefined;
178
+ vi.stubGlobal("ref", ref);
179
+ vi.stubGlobal("computed", computed);
180
+ vi.stubGlobal("watch", watch);
181
+ vi.stubGlobal("provide", provide);
182
+ vi.stubGlobal("useLeaveGuard", () => ({
183
+ executeGuards: async () => true,
184
+ clearGuards: vi.fn(),
185
+ }));
186
+ vi.stubGlobal("useDmsRoute", () => reactive({ path: "/" }));
187
+ });
188
+
189
+ it("slides in from the bottom, full width, by default", async () => {
190
+ await mountDrawer({});
191
+ expect(rendered.direction).toBe("bottom");
192
+ expect(rendered.ui?.content).toBe(undefined);
193
+ const containers = host.querySelectorAll("[data-container]");
194
+ expect(containers[0]!.className).toContain("max-sm:px-0");
195
+ expect(containers[0]!.className).not.toContain("lg:px-0");
196
+ });
197
+
198
+ it("is a side sheet as wide as a side panel from the right", async () => {
199
+ await mountDrawer({ direction: "right" });
200
+ expect(rendered.direction).toBe("right");
201
+ expect(rendered.ui?.content).toBe("w-[440px]");
202
+ const containers = host.querySelectorAll("[data-container]");
203
+ expect(containers[0]!.className).toContain("px-0 sm:px-0 lg:px-0");
204
+ });
205
+ });
@@ -0,0 +1,53 @@
1
+ // @vitest-environment jsdom
2
+ import { createApp, defineComponent, h, nextTick, ref, type App } from "vue";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import EmptyState from "../layers/dms-ui/app/components/empty-state/EmptyState.vue";
5
+
6
+ vi.mock("../layers/dms-ui/app/components/icon-well/IconWell.vue", () => ({
7
+ default: { render: () => null },
8
+ }));
9
+
10
+ let app: App | undefined;
11
+
12
+ function mountWithLateSlots() {
13
+ const shown = ref(false);
14
+ app = createApp({
15
+ render: () =>
16
+ h(
17
+ EmptyState,
18
+ { title: "No tables" },
19
+ shown.value
20
+ ? {
21
+ default: () => "Create one to start.",
22
+ actions: () => h("a", { href: "/new" }, "New table"),
23
+ }
24
+ : {},
25
+ ),
26
+ });
27
+ app.component("UButton", defineComponent({ setup: () => () => h("button") }));
28
+ const container = document.createElement("div");
29
+ app.mount(container);
30
+ return { container, shown };
31
+ }
32
+
33
+ afterEach(() => {
34
+ app?.unmount();
35
+ app = undefined;
36
+ });
37
+
38
+ describe("empty state slots", () => {
39
+ it("shows an actions slot the parent adds after the first render", async () => {
40
+ const { container, shown } = mountWithLateSlots();
41
+ expect(container.querySelector("a")).toBeNull();
42
+
43
+ shown.value = true;
44
+ await nextTick();
45
+ expect(container.querySelector("a")?.textContent).toBe("New table");
46
+ expect(container.textContent).toContain("Create one to start.");
47
+
48
+ shown.value = false;
49
+ await nextTick();
50
+ expect(container.querySelector("a")).toBeNull();
51
+ expect(container.textContent).not.toContain("Create one to start.");
52
+ });
53
+ });