@antelopejs/dms 0.7.21 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/implementations/dms-auth/account-emails.d.ts +4 -0
  3. package/dist/implementations/dms-auth/account-emails.js +62 -0
  4. package/dist/implementations/dms-auth/account-emails.js.map +1 -0
  5. package/dist/implementations/dms-auth/index.d.ts +1 -3
  6. package/dist/implementations/dms-auth/index.js +15 -61
  7. package/dist/implementations/dms-auth/index.js.map +1 -1
  8. package/dist/pages/settings/users/email-change.js +3 -4
  9. package/dist/pages/settings/users/email-change.js.map +1 -1
  10. package/dist/pages/settings/users/invites.js +1 -1
  11. package/dist/pages/settings/users/invites.js.map +1 -1
  12. package/dist/pages/settings/users/members.js +1 -1
  13. package/dist/pages/settings/users/members.js.map +1 -1
  14. package/dist/pages/settings/users/notifications.js +1 -0
  15. package/dist/pages/settings/users/notifications.js.map +1 -1
  16. package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.d.ts +1 -0
  17. package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js +34 -0
  18. package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js.map +1 -0
  19. package/dist/test/unit/interfaces/dms/hook-handler-results.test.d.ts +1 -0
  20. package/dist/test/unit/interfaces/dms/hook-handler-results.test.js +49 -0
  21. package/dist/test/unit/interfaces/dms/hook-handler-results.test.js.map +1 -0
  22. package/dist/test/unit/interfaces/dms/page-topics.test.js +17 -0
  23. package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
  24. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +2 -2
  25. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -1
  26. package/dist/test/unit/interfaces/dms-base/query-response-delta.test.d.ts +1 -0
  27. package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js +29 -0
  28. package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js.map +1 -0
  29. package/dist/test/unit/interfaces/dms-base/resource-form.test.js +5 -0
  30. package/dist/test/unit/interfaces/dms-base/resource-form.test.js.map +1 -1
  31. package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js +12 -2
  32. package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js.map +1 -1
  33. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +29 -4
  34. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
  35. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +14 -11
  36. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  37. package/dist/test/unit/interfaces/dms-base/table-view-views.test.js +8 -5
  38. package/dist/test/unit/interfaces/dms-base/table-view-views.test.js.map +1 -1
  39. package/dist/test/unit/utils/email-subjects.test.d.ts +1 -0
  40. package/dist/test/unit/utils/email-subjects.test.js +27 -0
  41. package/dist/test/unit/utils/email-subjects.test.js.map +1 -0
  42. package/dist/utils/admin-invite-email.js +2 -4
  43. package/dist/utils/admin-invite-email.js.map +1 -1
  44. package/dist/utils/email-change-mail.d.ts +1 -1
  45. package/dist/utils/email-change-mail.js +11 -6
  46. package/dist/utils/email-change-mail.js.map +1 -1
  47. package/dist/utils/email-subjects.d.ts +3 -0
  48. package/dist/utils/email-subjects.js +32 -0
  49. package/dist/utils/email-subjects.js.map +1 -0
  50. package/dist/utils/export-jobs.js +15 -10
  51. package/dist/utils/export-jobs.js.map +1 -1
  52. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +29 -11
  53. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +17 -0
  54. package/frontend-vue/layers/dms-layout/app/app.config.ts +5 -0
  55. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +18 -2
  56. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +16 -1
  57. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useHeaderActionControls.ts +14 -0
  58. package/frontend-vue/layers/dms-layout/app/components/layout/HStack.vue +16 -0
  59. package/frontend-vue/layers/dms-layout/app/components/layout/VStack.vue +2 -0
  60. package/frontend-vue/layers/dms-layout/app/composables/useFooterLinks.ts +11 -1
  61. package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +1 -1
  62. package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +1 -1
  63. package/frontend-vue/layers/dms-layout/app/emails/EmailChangeNotice.vue +13 -7
  64. package/frontend-vue/layers/dms-layout/app/emails/EmailChangeVerification.vue +18 -7
  65. package/frontend-vue/layers/dms-layout/app/emails/EmailExportReady.vue +23 -12
  66. package/frontend-vue/layers/dms-layout/app/emails/EmailResetPassword.vue +18 -7
  67. package/frontend-vue/layers/dms-layout/app/emails/EmailTwoFactor.vue +23 -4
  68. package/frontend-vue/layers/dms-layout/app/emails/EmailUserValidation.vue +18 -6
  69. package/frontend-vue/layers/dms-layout/app/emails/components/EmailOTP.vue +8 -2
  70. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +40 -0
  71. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +40 -0
  72. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +2 -1
  73. package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +11 -3
  74. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +27 -36
  75. package/frontend-vue/layers/dms-ui/app/build/composables/chart/apexTheme.ts +59 -2
  76. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +27 -4
  77. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +10 -2
  78. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +0 -5
  79. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryOrder.ts +23 -0
  80. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +19 -15
  81. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +26 -2
  82. package/frontend-vue/layers/dms-ui/app/build/composables/page/pageUrlValues.ts +41 -0
  83. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +2 -2
  84. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +3 -5
  85. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +7 -12
  86. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +3 -3
  87. package/frontend-vue/layers/dms-ui/app/build/utils/link.ts +7 -3
  88. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +32 -9
  89. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +8 -1
  90. package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +9 -4
  91. package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +5 -11
  92. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +2 -1
  93. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +32 -1
  94. package/frontend-vue/layers/dms-ui/app/components/check-list/CheckList.vue +1 -1
  95. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +12 -2
  96. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +7 -3
  97. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +7 -0
  98. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +4 -1
  99. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +4 -2
  100. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +3 -2
  101. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -1
  102. package/frontend-vue/layers/dms-ui/app/components/grid/columns.ts +15 -3
  103. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +2 -1
  104. package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +9 -2
  105. package/frontend-vue/layers/dms-ui/app/components/save-bar/InstantSaveBadge.vue +13 -5
  106. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +1 -1
  107. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +3 -2
  108. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +53 -31
  109. package/frontend-vue/layers/dms-ui/app/composables/chart/apexPlotOptions.ts +15 -4
  110. package/frontend-vue/layers/dms-ui/app/composables/chart/apexYRange.ts +123 -0
  111. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +49 -49
  112. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartTheme.ts +20 -1
  113. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +1 -1
  114. package/frontend-vue/layers/dms-ui/app/composables/form/types/props.ts +2 -0
  115. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +8 -6
  116. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +1 -1
  117. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +3 -5
  118. package/frontend-vue/layers/dms-ui/app/utils/bannerToneIcons.ts +13 -0
  119. package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +26 -0
  120. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -0
  121. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -0
  122. package/frontend-vue/tests/activity-feed-fill-height.test.ts +7 -0
  123. package/frontend-vue/tests/banner-tone-icon.test.ts +55 -0
  124. package/frontend-vue/tests/builtin-emails.test.ts +141 -0
  125. package/frontend-vue/tests/cascader.test.ts +27 -0
  126. package/frontend-vue/tests/chart-auto-y-range.test.ts +25 -9
  127. package/frontend-vue/tests/chart-colors.test.ts +8 -0
  128. package/frontend-vue/tests/chart-resize-after-animation.test.ts +100 -0
  129. package/frontend-vue/tests/chart-type-defaults.test.ts +128 -0
  130. package/frontend-vue/tests/check-list-marker.test.ts +58 -0
  131. package/frontend-vue/tests/confirm-from.test.ts +1 -1
  132. package/frontend-vue/tests/confirm-secondary-action.test.ts +2 -2
  133. package/frontend-vue/tests/context-links.test.ts +2 -2
  134. package/frontend-vue/tests/custom-action-confirm-from.test.ts +2 -2
  135. package/frontend-vue/tests/download-action-target.test.ts +2 -2
  136. package/frontend-vue/tests/file-drop-prompt.test.ts +72 -0
  137. package/frontend-vue/tests/footer-config.test.ts +135 -0
  138. package/frontend-vue/tests/form-actions.test.ts +7 -0
  139. package/frontend-vue/tests/form-entry-order.test.ts +45 -0
  140. package/frontend-vue/tests/form-footer-kind.test.ts +11 -6
  141. package/frontend-vue/tests/form-load-error.test.ts +38 -1
  142. package/frontend-vue/tests/grid-responsive-columns.test.ts +9 -0
  143. package/frontend-vue/tests/header-actions.test.ts +55 -9
  144. package/frontend-vue/tests/instant-save-badge.test.ts +11 -4
  145. package/frontend-vue/tests/list-row-truncate.test.ts +46 -0
  146. package/frontend-vue/tests/roles-name-errors.test.ts +2 -3
  147. package/frontend-vue/tests/save-bar.test.ts +15 -3
  148. package/frontend-vue/tests/save-status-error.test.ts +2 -2
  149. package/frontend-vue/tests/table-row-action-errors.test.ts +5 -5
  150. package/frontend-vue/tests/table-row-hover.test.ts +2 -2
  151. package/frontend-vue/tests/table-view-sources.test.ts +1 -1
  152. package/frontend-vue/tests/template-tokens.test.ts +235 -0
  153. package/frontend-vue/tests/url-interpolation.test.ts +6 -6
  154. package/package.json +2 -2
  155. package/skills/dms/SKILL.md +2 -1
  156. package/skills/dms-pages/REFERENCE.md +2 -2
  157. package/skills/dms-project/SKILL.md +0 -1
@@ -140,7 +140,7 @@ describe("Table row actions: refusals", () => {
140
140
  label: "Resend",
141
141
  target: {
142
142
  type: "api",
143
- url: "/api/invites/{_id}/resend",
143
+ url: "/api/invites/{{row._id}}/resend",
144
144
  successMessage: "Sent",
145
145
  },
146
146
  confirm: { title: "Resend?", description: "" },
@@ -170,7 +170,7 @@ describe("Table row actions: refusals", () => {
170
170
  target: {
171
171
  type: "api",
172
172
  method: "GET",
173
- url: "/api/invites/{_id}/link",
173
+ url: "/api/invites/{{row._id}}/link",
174
174
  successMessage: "Copied",
175
175
  copy: "url",
176
176
  },
@@ -267,7 +267,7 @@ describe("Table row actions: refusals", () => {
267
267
  label: "Ban",
268
268
  target: {
269
269
  type: "api",
270
- url: "/api/users/{_id}/ban",
270
+ url: "/api/users/{{row._id}}/ban",
271
271
  body: { notify: true, scope: "all" },
272
272
  successMessage: "Banned",
273
273
  },
@@ -293,7 +293,7 @@ describe("Table row actions: refusals", () => {
293
293
  const { deleteRows } = await rowActions();
294
294
 
295
295
  const done = await deleteRows(["t1"], {
296
- confirm: { from: "/api/task/{id}/delete-confirm" },
296
+ confirm: { from: "/api/task/{{row.id}}/delete-confirm" },
297
297
  });
298
298
 
299
299
  expect(done).toBe(true);
@@ -329,7 +329,7 @@ describe("Table row actions: refusals", () => {
329
329
  const { deleteRows } = await rowActions();
330
330
 
331
331
  await deleteRows(["t1", "t2"], {
332
- confirm: { from: "/api/task/{id}/delete-confirm" },
332
+ confirm: { from: "/api/task/{{row.id}}/delete-confirm" },
333
333
  });
334
334
 
335
335
  expect(api).not.toHaveBeenCalledWith("/api/task/t1/delete-confirm");
@@ -164,13 +164,13 @@ describe("rowCustomActions", () => {
164
164
  const stopSelling: CustomRowAction = {
165
165
  id: "stop_selling",
166
166
  label: "Stop selling",
167
- target: { type: "api", url: "/plans/{_id}/stop", method: "POST" },
167
+ target: { type: "api", url: "/plans/{{row._id}}/stop", method: "POST" },
168
168
  rule: { field: "isActive", equals: true },
169
169
  };
170
170
  const remove: CustomRowAction = {
171
171
  id: "delete",
172
172
  label: "Delete",
173
- target: { type: "api", url: "/plans/{_id}", method: "DELETE" },
173
+ target: { type: "api", url: "/plans/{{row._id}}", method: "DELETE" },
174
174
  rule: { field: "workspaces", equals: 0 },
175
175
  };
176
176
 
@@ -182,7 +182,7 @@ describe("source tables", () => {
182
182
 
183
183
  describe("reading one row (fetchRowUrl)", () => {
184
184
  const FETCH_URL = "/api/ws/{{params.ws}}/requests";
185
- const ROW_URL = "/api/ws/{{params.ws}}/requests/{id}";
185
+ const ROW_URL = "/api/ws/{{params.ws}}/requests/{{row.id}}";
186
186
 
187
187
  function rowsWith(
188
188
  api: (url: string) => Promise<unknown>,
@@ -0,0 +1,235 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Every option naming a value with a token writes it `{{<scope>.<key>}}`, and
4
+ * every URL or link option fills the page's `{{params.X}}` / `{{query.X}}`
5
+ * the same way: an `api` or `exportJob` target as a `page` one, a
6
+ * confirmation's `from`, a block's links, a form's `backTo`.
7
+ */
8
+ import { createApp, defineComponent, h, reactive, type App } from "vue";
9
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10
+ import { useActionTargets } from "../layers/dms-ui/app/build/composables/actions/useActionTargets";
11
+ import {
12
+ interpolateRowText,
13
+ interpolateUrl,
14
+ } from "../layers/dms-core/app/utils/url-interpolation";
15
+ import { HttpMethod } from "../layers/dms-core/app/types/http";
16
+ import {
17
+ replaceUrlVariables,
18
+ resolveLinkVariables,
19
+ } from "../layers/dms-ui/app/build/utils/urlVariables";
20
+ import {
21
+ pageLink,
22
+ pageRequestUrl,
23
+ } from "../layers/dms-ui/app/build/composables/page/pageUrlValues";
24
+ import { buttonLinkProps } from "../layers/dms-ui/app/build/utils/link";
25
+ import { resolveConfirmDialog } from "../layers/dms-ui/app/build/composables/confirm/confirmDialogTexts";
26
+ import type { CustomButton } from "../layers/dms-ui/app/composables/table-view/types/custom-button";
27
+ import type { CustomRowAction } from "../layers/dms-ui/app/types/row-action";
28
+
29
+ const route = reactive<{ path: string; query: Record<string, unknown> }>({
30
+ path: "/modules/shop/stores/s1",
31
+ query: {},
32
+ });
33
+ const api = vi.fn();
34
+ const runJob = vi.fn();
35
+ let app: App | undefined;
36
+
37
+ function mountTargets() {
38
+ let targets: ReturnType<typeof useActionTargets> | undefined;
39
+ app = createApp(
40
+ defineComponent({
41
+ setup() {
42
+ targets = useActionTargets({
43
+ api: api as never,
44
+ pageId: "store-page",
45
+ componentId: "orders",
46
+ handleApiError: vi.fn(),
47
+ });
48
+ return () => h("div");
49
+ },
50
+ }),
51
+ );
52
+ app.mount(document.createElement("div"));
53
+ return targets!;
54
+ }
55
+
56
+ beforeEach(() => {
57
+ route.query = { store: "north", period: "7d" };
58
+ api.mockReset().mockResolvedValue(undefined);
59
+ runJob.mockReset().mockResolvedValue(undefined);
60
+ vi.stubGlobal("useToast", () => ({ add: vi.fn() }));
61
+ vi.stubGlobal("useTranslation", () => ({
62
+ processI18n: (text: string) => text,
63
+ processApiMessage: (text: string) => text,
64
+ }));
65
+ vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
66
+ vi.stubGlobal("useModal", () => ({ open: vi.fn() }));
67
+ vi.stubGlobal("useDrawer", () => ({ open: vi.fn() }));
68
+ vi.stubGlobal("useExportJob", () => ({ runJob }));
69
+ vi.stubGlobal("useConfirm", () => ({ confirm: vi.fn(async () => true) }));
70
+ vi.stubGlobal("useAuthFetch", () => ({
71
+ $authFetch: Object.assign(vi.fn(), { raw: vi.fn() }),
72
+ }));
73
+ vi.stubGlobal("interpolateUrl", interpolateUrl);
74
+ vi.stubGlobal("HttpMethod", HttpMethod);
75
+ vi.stubGlobal("useDmsRoute", () => route);
76
+ vi.stubGlobal("useSiteLayout", () => ({
77
+ findMatchingRoute: () => ({ params: { id: "s1" } }),
78
+ }));
79
+ });
80
+
81
+ afterEach(() => {
82
+ app?.unmount();
83
+ app = undefined;
84
+ vi.restoreAllMocks();
85
+ vi.unstubAllGlobals();
86
+ });
87
+
88
+ describe("token syntax", () => {
89
+ const context = {
90
+ routeParams: { id: "s 1" },
91
+ routeQuery: { store: "north" },
92
+ response: { data: { id: "o-9" } },
93
+ };
94
+
95
+ it("allows spaces inside the braces, as the server's filter tokens do", () => {
96
+ expect(
97
+ replaceUrlVariables(
98
+ "/s/{{ params.id }}?store={{ query.store }}",
99
+ context,
100
+ ),
101
+ ).toBe("/s/s%201?store=north");
102
+ });
103
+
104
+ it("reaches into a response with a dotted path", () => {
105
+ expect(replaceUrlVariables("/orders/{{response.data.id}}", context)).toBe(
106
+ "/orders/o-9",
107
+ );
108
+ });
109
+
110
+ it("resolves a redirect's response and page tokens, dropping an empty pair", () => {
111
+ expect(
112
+ resolveLinkVariables(
113
+ "/orders/{{response.data.id}}?store={{query.store}}&tab={{query.tab}}",
114
+ context,
115
+ ),
116
+ ).toBe("/orders/o-9?store=north");
117
+ });
118
+
119
+ it("names a row field with {{row.X}}, a dotted path reaching into it", () => {
120
+ const row = { _id: "a 1", customer: { name: "Ada" } };
121
+ expect(interpolateUrl("/o/{{row._id}}/{{ row.customer.name }}", row)).toBe(
122
+ "/o/a%201/Ada",
123
+ );
124
+ expect(interpolateRowText("{{row._id}}.csv", row)).toBe("a 1.csv");
125
+ });
126
+
127
+ it("does not take a single-brace name for a row field", () => {
128
+ expect(interpolateUrl("/o/{_id}", { _id: "a" })).toBe("/o/{_id}");
129
+ });
130
+
131
+ it("leaves another scope's token whole when filling a row", () => {
132
+ expect(
133
+ interpolateUrl("/s/{{params.id}}/o/{{row._id}}", { _id: "a" }),
134
+ ).toBe("/s/{{params.id}}/o/a");
135
+ });
136
+ });
137
+
138
+ describe("page tokens in links and requested routes", () => {
139
+ it("fills a block link, leaving out a pair the page has no value for", () => {
140
+ expect(pageLink("/stores/{{params.id}}?store={{query.store}}")).toBe(
141
+ "/stores/s1?store=north",
142
+ );
143
+ expect(pageLink("/orders?tab={{query.tab}}")).toBe("/orders");
144
+ expect(buttonLinkProps("/stores/{{params.id}}/orders")).toEqual({
145
+ to: "/stores/s1/orders",
146
+ });
147
+ });
148
+
149
+ it("does not request a route while one of its tokens has no value", () => {
150
+ expect(pageRequestUrl("/api/stores/{{params.id}}/rotate")).toBe(
151
+ "/api/stores/s1/rotate",
152
+ );
153
+ expect(pageRequestUrl("/api/x?tab={{query.tab}}")).toBeUndefined();
154
+ });
155
+
156
+ it("fills a confirmation's secondary target with the page, then the row", async () => {
157
+ const runTarget = vi.fn();
158
+ const dialog = resolveConfirmDialog(
159
+ {
160
+ title: "Remove",
161
+ secondaryAction: {
162
+ label: "Download",
163
+ target: {
164
+ type: "download",
165
+ url: "/api/stores/{{params.id}}/orders/{{row._id}}",
166
+ filename: "{{query.store}}-{{row.name}}.json",
167
+ },
168
+ },
169
+ },
170
+ { _id: "o1", name: "order" },
171
+ (key) => key,
172
+ runTarget,
173
+ );
174
+ await dialog.secondaryAction!.run();
175
+ expect(runTarget).toHaveBeenCalledWith({
176
+ type: "download",
177
+ url: "/api/stores/s1/orders/o1",
178
+ filename: "north-order.json",
179
+ });
180
+ });
181
+ });
182
+
183
+ describe("an action target's URL", () => {
184
+ it("fills an api target on a row with the page's values, then the row's", async () => {
185
+ const action: CustomRowAction = {
186
+ label: "Refund",
187
+ target: {
188
+ type: "api",
189
+ url: "/api/stores/{{params.id}}/orders/{{row._id}}/refund?store={{query.store}}",
190
+ successMessage: "Refunded",
191
+ },
192
+ };
193
+ mountTargets().handleCustomRowAction(action, { _id: "o 1" });
194
+ await vi.waitFor(() => expect(api).toHaveBeenCalled());
195
+ expect(api.mock.calls[0]![0]).toBe(
196
+ "/api/stores/s1/orders/o%201/refund?store=north",
197
+ );
198
+ });
199
+
200
+ it("fills an api target on a toolbar button, without a row", async () => {
201
+ const button: CustomButton = {
202
+ id: "sync",
203
+ label: "Sync",
204
+ target: {
205
+ type: "api",
206
+ url: "/api/stores/{{params.id}}/sync?period={{query.period}}",
207
+ successMessage: "Synced",
208
+ },
209
+ };
210
+ mountTargets().handleCustomButton(button);
211
+ await vi.waitFor(() => expect(api).toHaveBeenCalled());
212
+ expect(api.mock.calls[0]![0]).toBe("/api/stores/s1/sync?period=7d");
213
+ });
214
+
215
+ it("fills an export job's start, status and download URLs", async () => {
216
+ const button: CustomButton = {
217
+ id: "export",
218
+ label: "Export",
219
+ target: {
220
+ type: "exportJob",
221
+ url: "/api/stores/{{params.id}}/export?store={{query.store}}",
222
+ statusUrl: "/api/stores/{{params.id}}/export/{{job.id}}",
223
+ downloadUrl: "/api/stores/{{params.id}}/export/{{ job.id }}/file",
224
+ },
225
+ };
226
+ mountTargets().handleCustomButton(button);
227
+ await vi.waitFor(() => expect(runJob).toHaveBeenCalled());
228
+ const job = runJob.mock.calls[0]![0];
229
+ expect(job.startUrl).toBe("/api/stores/s1/export?store=north");
230
+ expect(job.statusUrl({ jobId: "j1" })).toBe("/api/stores/s1/export/j1");
231
+ expect(job.downloadUrl({ jobId: "j1" })).toBe(
232
+ "/api/stores/s1/export/j1/file",
233
+ );
234
+ });
235
+ });
@@ -4,25 +4,25 @@ import { interpolateUrl } from "../layers/dms-core/app/utils/url-interpolation";
4
4
  describe("interpolateUrl", () => {
5
5
  it("fills row fields into the URL, encoded", () => {
6
6
  expect(
7
- interpolateUrl("/items/{_id}/tag/{name}", { _id: "a1", name: "a b/c" }),
7
+ interpolateUrl("/items/{{row._id}}/tag/{{row.name}}", { _id: "a1", name: "a b/c" }),
8
8
  ).toBe("/items/a1/tag/a%20b%2Fc");
9
9
  });
10
10
 
11
11
  it("keeps the colon of a tenant-scoped id readable", () => {
12
12
  expect(
13
- interpolateUrl("/members/{_id}/owner", { _id: "default:29fd-11" }),
13
+ interpolateUrl("/members/{{row._id}}/owner", { _id: "default:29fd-11" }),
14
14
  ).toBe("/members/default:29fd-11/owner");
15
15
  });
16
16
 
17
17
  it("leaves a missing field empty", () => {
18
- expect(interpolateUrl("/items/{missing}", {})).toBe("/items/");
18
+ expect(interpolateUrl("/items/{{row.missing}}", {})).toBe("/items/");
19
19
  });
20
20
 
21
21
  it("keeps web and mail links", () => {
22
- expect(interpolateUrl("https://acme.dev/{slug}", { slug: "a" })).toBe(
22
+ expect(interpolateUrl("https://acme.dev/{{row.slug}}", { slug: "a" })).toBe(
23
23
  "https://acme.dev/a",
24
24
  );
25
- expect(interpolateUrl("mailto:{email}", { email: "a@b.c" })).toBe(
25
+ expect(interpolateUrl("mailto:{{row.email}}", { email: "a@b.c" })).toBe(
26
26
  "mailto:a%40b.c",
27
27
  );
28
28
  });
@@ -34,7 +34,7 @@ describe("interpolateUrl", () => {
34
34
  "data:text/html,x",
35
35
  "vbscript:x",
36
36
  ]) {
37
- expect(interpolateUrl("{site}", { site })).toBe("");
37
+ expect(interpolateUrl("{{row.site}}", { site })).toBe("");
38
38
  }
39
39
  });
40
40
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms",
3
- "version": "0.7.21",
3
+ "version": "0.8.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/AntelopeJS/dms.git"
@@ -37,7 +37,7 @@
37
37
  "@antelopejs/interface-data-api": ">=0.2.0 <1.0.0",
38
38
  "@antelopejs/interface-database": ">=0.1.8 <1.0.0",
39
39
  "@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
40
- "@antelopejs/interface-dms": ">=0.5.13 <0.6.0",
40
+ "@antelopejs/interface-dms": ">=0.6.0 <0.7.0",
41
41
  "@antelopejs/interface-email": ">=0.0.5 <1.0.0",
42
42
  "@antelopejs/interface-file-storage": ">=0.1.3 <1.0.0",
43
43
  "archiver": "^7.0.1",
@@ -33,7 +33,8 @@ For the framework underneath (the core, `ajs` CLI, interfaces as versioned contr
33
33
  frontend renders it. **Components fetch their own data** via URLs (`fetchUrl`, `submitUrl`, …) —
34
34
  routes often declared `@Get`/`@Post` on the same page class; the backend stays the source of truth.
35
35
  A URL names the record of its page with `{{params.id}}` / `{{query.X}}` (filled from the page URL, percent-encoded;
36
- nothing is fetched while one is unresolved), and `.watch(FormEvents.SUBMIT_SUCCESS,
36
+ nothing is fetched while one is unresolved) — every token is `{{<scope>.<key>}}`: `params`, `query`, `row` (an
37
+ action's row), `response` (a form's redirect), and the same page tokens work in every URL and link option, and `.watch(FormEvents.SUBMIT_SUCCESS,
37
38
  BlockFunctions.REFRESH_PAGE)` makes the page's data blocks (table views included) re-fetch after a form saves;
38
39
  `periodScope` binds a card, chart, list block, `Meter` or `TableView.fromSource` to a `PeriodSelector`
39
40
  (which keeps its period in the page URL, `?<id>.period=`), and
@@ -54,10 +54,10 @@ own drawer in a `CustomComponent`.
54
54
  To read rows one after the other next to the table (a request log), use a `sidePanel`
55
55
  target instead: a non-modal panel docked on the right whose header the DMS draws (title,
56
56
  `rowTitle` naming the row from its fields, `positionLabel` "x of y", previous / next, J / K, close), `?record=` kept by default (`deepLink: false` opts out), a "not found" state on a 404 (`notFound: { title, description }`) and the table's
57
- `rowClick: "single"` (a `TableView.fromSource` table reads a deep-linked row beyond its first page through `fetchRowUrl: "/api/x/{id}"`) — never bridge the table's DOM to build one —
57
+ `rowClick: "single"` (a `TableView.fromSource` table reads a deep-linked row beyond its first page through `fetchRowUrl: "/api/x/{{row.id}}"`) — never bridge the table's DOM to build one —
58
58
  `docs/04.components/06.tables/8.row-drawers.md#side-panel`.
59
59
  A button downloading a file the user's session may read (`/api/...`) takes a `download`
60
- target, `{ url, filename? }`, with the same `{field}` / `{{query.X}}` tokens — never a Vue
60
+ target, `{ url, filename? }`, with the same `{{row.field}}` / `{{query.X}}` tokens — never a Vue
61
61
  header button or an `api` target for it — `docs/07.interfaces/3.base/2.types.md`.
62
62
 
63
63
  A row that must stay on top of a table under any sort (a base language, a default
@@ -82,7 +82,6 @@ export function construct(config: Config): void {
82
82
  registerAllPermissions(); // RegisterPermission(...) — see dms-auth
83
83
  RegisterHook(Hook.DATABASE_INITIALIZED, async () => {
84
84
  await initDatabase();
85
- return undefined;
86
85
  });
87
86
  }
88
87
  export async function start(): Promise<void> {