@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.
- package/CHANGELOG.md +39 -0
- package/dist/implementations/dms-auth/account-emails.d.ts +4 -0
- package/dist/implementations/dms-auth/account-emails.js +62 -0
- package/dist/implementations/dms-auth/account-emails.js.map +1 -0
- package/dist/implementations/dms-auth/index.d.ts +1 -3
- package/dist/implementations/dms-auth/index.js +15 -61
- package/dist/implementations/dms-auth/index.js.map +1 -1
- package/dist/pages/settings/users/email-change.js +3 -4
- package/dist/pages/settings/users/email-change.js.map +1 -1
- package/dist/pages/settings/users/invites.js +1 -1
- package/dist/pages/settings/users/invites.js.map +1 -1
- package/dist/pages/settings/users/members.js +1 -1
- package/dist/pages/settings/users/members.js.map +1 -1
- package/dist/pages/settings/users/notifications.js +1 -0
- package/dist/pages/settings/users/notifications.js.map +1 -1
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js +34 -0
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.js +49 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js +17 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +2 -2
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js +29 -0
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/resource-form.test.js +5 -0
- package/dist/test/unit/interfaces/dms-base/resource-form.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js +12 -2
- package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +29 -4
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +14 -11
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-views.test.js +8 -5
- package/dist/test/unit/interfaces/dms-base/table-view-views.test.js.map +1 -1
- package/dist/test/unit/utils/email-subjects.test.d.ts +1 -0
- package/dist/test/unit/utils/email-subjects.test.js +27 -0
- package/dist/test/unit/utils/email-subjects.test.js.map +1 -0
- package/dist/utils/admin-invite-email.js +2 -4
- package/dist/utils/admin-invite-email.js.map +1 -1
- package/dist/utils/email-change-mail.d.ts +1 -1
- package/dist/utils/email-change-mail.js +11 -6
- package/dist/utils/email-change-mail.js.map +1 -1
- package/dist/utils/email-subjects.d.ts +3 -0
- package/dist/utils/email-subjects.js +32 -0
- package/dist/utils/email-subjects.js.map +1 -0
- package/dist/utils/export-jobs.js +15 -10
- package/dist/utils/export-jobs.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +29 -11
- package/frontend-vue/layers/dms-core/shared/types/app-config.ts +17 -0
- package/frontend-vue/layers/dms-layout/app/app.config.ts +5 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +18 -2
- package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +16 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/useHeaderActionControls.ts +14 -0
- package/frontend-vue/layers/dms-layout/app/components/layout/HStack.vue +16 -0
- package/frontend-vue/layers/dms-layout/app/components/layout/VStack.vue +2 -0
- package/frontend-vue/layers/dms-layout/app/composables/useFooterLinks.ts +11 -1
- package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/emails/EmailChangeNotice.vue +13 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailChangeVerification.vue +18 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailExportReady.vue +23 -12
- package/frontend-vue/layers/dms-layout/app/emails/EmailResetPassword.vue +18 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailTwoFactor.vue +23 -4
- package/frontend-vue/layers/dms-layout/app/emails/EmailUserValidation.vue +18 -6
- package/frontend-vue/layers/dms-layout/app/emails/components/EmailOTP.vue +8 -2
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +40 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +40 -0
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +11 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +27 -36
- package/frontend-vue/layers/dms-ui/app/build/composables/chart/apexTheme.ts +59 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +27 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +10 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +0 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryOrder.ts +23 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +19 -15
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +26 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/page/pageUrlValues.ts +41 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +2 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +3 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +7 -12
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +3 -3
- package/frontend-vue/layers/dms-ui/app/build/utils/link.ts +7 -3
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +32 -9
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +8 -1
- package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +9 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +5 -11
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +32 -1
- package/frontend-vue/layers/dms-ui/app/components/check-list/CheckList.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +12 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +7 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +7 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +4 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/grid/columns.ts +15 -3
- package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +9 -2
- package/frontend-vue/layers/dms-ui/app/components/save-bar/InstantSaveBadge.vue +13 -5
- package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +53 -31
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexPlotOptions.ts +15 -4
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexYRange.ts +123 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +49 -49
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartTheme.ts +20 -1
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/composables/form/types/props.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +8 -6
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +3 -5
- package/frontend-vue/layers/dms-ui/app/utils/bannerToneIcons.ts +13 -0
- package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +26 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -0
- package/frontend-vue/tests/activity-feed-fill-height.test.ts +7 -0
- package/frontend-vue/tests/banner-tone-icon.test.ts +55 -0
- package/frontend-vue/tests/builtin-emails.test.ts +141 -0
- package/frontend-vue/tests/cascader.test.ts +27 -0
- package/frontend-vue/tests/chart-auto-y-range.test.ts +25 -9
- package/frontend-vue/tests/chart-colors.test.ts +8 -0
- package/frontend-vue/tests/chart-resize-after-animation.test.ts +100 -0
- package/frontend-vue/tests/chart-type-defaults.test.ts +128 -0
- package/frontend-vue/tests/check-list-marker.test.ts +58 -0
- package/frontend-vue/tests/confirm-from.test.ts +1 -1
- package/frontend-vue/tests/confirm-secondary-action.test.ts +2 -2
- package/frontend-vue/tests/context-links.test.ts +2 -2
- package/frontend-vue/tests/custom-action-confirm-from.test.ts +2 -2
- package/frontend-vue/tests/download-action-target.test.ts +2 -2
- package/frontend-vue/tests/file-drop-prompt.test.ts +72 -0
- package/frontend-vue/tests/footer-config.test.ts +135 -0
- package/frontend-vue/tests/form-actions.test.ts +7 -0
- package/frontend-vue/tests/form-entry-order.test.ts +45 -0
- package/frontend-vue/tests/form-footer-kind.test.ts +11 -6
- package/frontend-vue/tests/form-load-error.test.ts +38 -1
- package/frontend-vue/tests/grid-responsive-columns.test.ts +9 -0
- package/frontend-vue/tests/header-actions.test.ts +55 -9
- package/frontend-vue/tests/instant-save-badge.test.ts +11 -4
- package/frontend-vue/tests/list-row-truncate.test.ts +46 -0
- package/frontend-vue/tests/roles-name-errors.test.ts +2 -3
- package/frontend-vue/tests/save-bar.test.ts +15 -3
- package/frontend-vue/tests/save-status-error.test.ts +2 -2
- package/frontend-vue/tests/table-row-action-errors.test.ts +5 -5
- package/frontend-vue/tests/table-row-hover.test.ts +2 -2
- package/frontend-vue/tests/table-view-sources.test.ts +1 -1
- package/frontend-vue/tests/template-tokens.test.ts +235 -0
- package/frontend-vue/tests/url-interpolation.test.ts +6 -6
- package/package.json +2 -2
- package/skills/dms/SKILL.md +2 -1
- package/skills/dms-pages/REFERENCE.md +2 -2
- 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.
|
|
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.
|
|
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",
|
package/skills/dms/SKILL.md
CHANGED
|
@@ -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)
|
|
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> {
|