@antelopejs/dms 0.7.14 → 0.7.15

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 (48) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/db/models/userNotifications.model.js +3 -27
  3. package/dist/db/models/userNotifications.model.js.map +1 -1
  4. package/dist/implementations/dms/frontend-modules.d.ts +4 -2
  5. package/dist/implementations/dms/frontend-modules.js +4 -0
  6. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  7. package/dist/implementations/dms/frontend-public-endpoints.d.ts +3 -0
  8. package/dist/implementations/dms/frontend-public-endpoints.js +70 -0
  9. package/dist/implementations/dms/frontend-public-endpoints.js.map +1 -0
  10. package/dist/implementations/dms/manifest.d.ts +1 -0
  11. package/dist/implementations/dms/manifest.js +3 -1
  12. package/dist/implementations/dms/manifest.js.map +1 -1
  13. package/dist/test/integration/notifications.test.js +6 -3
  14. package/dist/test/integration/notifications.test.js.map +1 -1
  15. package/dist/test/unit/implementations/dms/frontend-public-endpoints.test.d.ts +1 -0
  16. package/dist/test/unit/implementations/dms/frontend-public-endpoints.test.js +74 -0
  17. package/dist/test/unit/implementations/dms/frontend-public-endpoints.test.js.map +1 -0
  18. package/dist/test/unit/implementations/dms/manifest-strip.test.js +5 -0
  19. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  20. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +35 -12
  21. package/frontend-vue/layers/dms-layout/app/emails/components/email-logo.ts +54 -0
  22. package/frontend-vue/layers/dms-layout/app/emails/components/index.ts +2 -0
  23. package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +39 -2
  24. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +18 -1
  25. package/frontend-vue/layers/dms-ui/app/build/composables/form/formControlErrors.ts +10 -0
  26. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +8 -32
  27. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +34 -3
  28. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/urlFilters.ts +29 -0
  29. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +1 -1
  30. package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +1 -1
  31. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +1 -1
  32. package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +1 -1
  33. package/frontend-vue/layers/dms-ui/app/components/form/components/InputTags.vue +1 -1
  34. package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +1 -1
  35. package/frontend-vue/layers/dms-ui/app/components/form/components/KeyValue.vue +1 -1
  36. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +61 -2
  37. package/frontend-vue/layers/dms-ui/app/composables/form/useControlError.ts +30 -0
  38. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  39. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +6 -0
  40. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +6 -0
  41. package/frontend-vue/tests/control-error-path.test.ts +11 -0
  42. package/frontend-vue/tests/email-layout.test.ts +124 -0
  43. package/frontend-vue/tests/input-number-range.test.ts +1 -1
  44. package/frontend-vue/tests/input-tags.test.ts +1 -1
  45. package/frontend-vue/tests/row-side-panel.test.ts +2 -2
  46. package/frontend-vue/tests/table-url-filters.test.ts +158 -0
  47. package/frontend-vue/tests/table-view-bulk-drawer.test.ts +23 -0
  48. package/package.json +2 -2
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as legacy from "../layers/dms-ui/app/build/composables/form/useControlError";
3
+ import { useControlError } from "../layers/dms-ui/app/composables/form/useControlError";
4
+ import { FORM_CONTROL_ERRORS_KEY } from "../layers/dms-ui/app/build/composables/form/formControlErrors";
5
+
6
+ describe("useControlError location", () => {
7
+ it("stays importable from its former build/ path", () => {
8
+ expect(legacy.useControlError).toBe(useControlError);
9
+ expect(legacy.FORM_CONTROL_ERRORS_KEY).toBe(FORM_CONTROL_ERRORS_KEY);
10
+ });
11
+ });
@@ -0,0 +1,124 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createSSRApp, defineComponent, h } from "vue";
3
+ import { renderToString } from "vue/server-renderer";
4
+ import EmailLayout from "../layers/dms-layout/app/emails/components/EmailLayout.vue";
5
+ import { emailLogoUrl } from "../layers/dms-layout/app/emails/components/index";
6
+
7
+ const PUBLIC_BASE = "https://admin.example.com";
8
+ const ELEMENTS: Record<string, string> = {
9
+ EHtml: "html",
10
+ EHead: "head",
11
+ EBody: "body",
12
+ EContainer: "div",
13
+ ESection: "section",
14
+ EHeading: "h1",
15
+ EImg: "img",
16
+ EHr: "hr",
17
+ EPreview: "span",
18
+ };
19
+
20
+ function element(tag: string) {
21
+ return defineComponent({
22
+ inheritAttrs: false,
23
+ setup:
24
+ (_, { attrs, slots }) =>
25
+ () =>
26
+ h(tag, { src: attrs.src, alt: attrs.alt }, slots.default?.()),
27
+ });
28
+ }
29
+
30
+ async function renderLayout(
31
+ props: Record<string, unknown>,
32
+ logo = "/images/logo.png",
33
+ clientBaseUrl = PUBLIC_BASE,
34
+ ): Promise<string> {
35
+ vi.stubGlobal("useDmsAppConfig", () => ({
36
+ branding: { logo: { default: { light: logo } } },
37
+ }));
38
+ vi.stubGlobal("useDmsRuntimeConfig", () => ({
39
+ public: { dms: { clientBaseUrl } },
40
+ }));
41
+ const app = createSSRApp({
42
+ render: () =>
43
+ h(EmailLayout, props, {
44
+ default: () => h("p", "Body"),
45
+ footer: () => h("small", "Footer"),
46
+ }),
47
+ });
48
+ Object.entries(ELEMENTS).forEach(([name, tag]) =>
49
+ app.component(name, element(tag)),
50
+ );
51
+ const html = await renderToString(app);
52
+ vi.unstubAllGlobals();
53
+ return html;
54
+ }
55
+
56
+ describe("EmailLayout", () => {
57
+ it("keeps the heading, the separator and the logo by default", async () => {
58
+ const html = await renderLayout({ title: "Welcome" });
59
+ expect(html).toContain("<h1>Welcome</h1>");
60
+ expect(html).toContain("<hr>");
61
+ expect(html).toContain(
62
+ `<img src="${PUBLIC_BASE}/images/logo.png" alt="Logo">`,
63
+ );
64
+ });
65
+
66
+ it("drops the heading without a title and the separator on request", async () => {
67
+ const html = await renderLayout({ divider: false, preheader: "Preview" });
68
+ expect(html).not.toContain("<h1>");
69
+ expect(html).not.toContain("<hr>");
70
+ expect(html).toContain("<span>Preview</span>");
71
+ expect(html).toContain("<p>Body</p>");
72
+ expect(html).toContain("<small>Footer</small>");
73
+ });
74
+
75
+ it("keeps any logo by default", async () => {
76
+ expect(
77
+ await renderLayout({ title: "Hi" }, "/images/logo.svg", ""),
78
+ ).toContain('<img src="/images/logo.svg" alt="Logo">');
79
+ });
80
+
81
+ it("leaves out a logo a mail client cannot load with publicLogoOnly", async () => {
82
+ const guarded = { title: "Hi", publicLogoOnly: true };
83
+ expect(await renderLayout(guarded, "/images/logo.svg")).not.toContain(
84
+ "<img",
85
+ );
86
+ expect(
87
+ await renderLayout(guarded, "/logo.png", "http://localhost:3001"),
88
+ ).not.toContain("<img");
89
+ expect(await renderLayout(guarded)).toContain(
90
+ `<img src="${PUBLIC_BASE}/images/logo.png" alt="Logo">`,
91
+ );
92
+ });
93
+ });
94
+
95
+ describe("emailLogoUrl", () => {
96
+ it("resolves a root path against the public base URL", () => {
97
+ expect(emailLogoUrl("/logo.png", PUBLIC_BASE)).toBe(
98
+ `${PUBLIC_BASE}/logo.png`,
99
+ );
100
+ expect(emailLogoUrl("https://cdn.example.com/logo.png", "")).toBe(
101
+ "https://cdn.example.com/logo.png",
102
+ );
103
+ });
104
+
105
+ it.each([
106
+ [undefined, PUBLIC_BASE],
107
+ ["/logo.svg", PUBLIC_BASE],
108
+ ["https://cdn.example.com/logo.SVG?v=2", ""],
109
+ ["data:image/png;base64,AAAA", ""],
110
+ ["/logo.png", ""],
111
+ ["/logo.png", "http://localhost:3001"],
112
+ ["/logo.png", "http://dms.localhost"],
113
+ ["/logo.png", "http://127.0.0.1:3001"],
114
+ ["/logo.png", "http://10.0.0.5"],
115
+ ["/logo.png", "http://172.20.1.1"],
116
+ ["/logo.png", "http://192.168.1.10"],
117
+ ["/logo.png", "http://169.254.169.254"],
118
+ ["/logo.png", "http://[::1]:3001"],
119
+ ["/logo.png", "http://[fd00::1]"],
120
+ ["/logo.png", "http://intranet.internal"],
121
+ ])("leaves out %s on %s", (logo, base) => {
122
+ expect(emailLogoUrl(logo, base)).toBe("");
123
+ });
124
+ });
@@ -17,7 +17,7 @@ import {
17
17
  nextTick,
18
18
  ref,
19
19
  } from "vue";
20
- import { FORM_CONTROL_ERRORS_KEY } from "../layers/dms-ui/app/build/composables/form/useControlError";
20
+ import { FORM_CONTROL_ERRORS_KEY } from "../layers/dms-ui/app/build/composables/form/formControlErrors";
21
21
  import {
22
22
  ABOVE_MAX_MESSAGE,
23
23
  BELOW_MIN_MESSAGE,
@@ -16,7 +16,7 @@ import {
16
16
  formErrorsInjectionKey,
17
17
  formFieldInjectionKey,
18
18
  } from "@nuxt/ui/composables/useFormField";
19
- import { FORM_CONTROL_ERRORS_KEY } from "../layers/dms-ui/app/build/composables/form/useControlError";
19
+ import { FORM_CONTROL_ERRORS_KEY } from "../layers/dms-ui/app/build/composables/form/formControlErrors";
20
20
 
21
21
  const formErrors = ref<Array<Record<string, unknown>>>([]);
22
22
  const model = ref<string[]>([]);
@@ -453,13 +453,13 @@ describe("deep link to a row the table does not list", () => {
453
453
  ).toEqual({ isMissing: true });
454
454
  });
455
455
 
456
- it("finds nothing, missing or not, on a refusal or without a row route", async () => {
456
+ it("marks a refused read as failed and finds nothing without a row route", async () => {
457
457
  const refused = async () => {
458
458
  throw Object.assign(new Error("Forbidden"), { status: 403 });
459
459
  };
460
460
  expect(
461
461
  await resolveDeepLinkedRow("r9", { listed: rows, idOf, getRow: refused }),
462
- ).toEqual({ isMissing: false });
462
+ ).toEqual({ isMissing: false, isFailed: true });
463
463
  const noRoute = async () => undefined;
464
464
  expect(
465
465
  await resolveDeepLinkedRow("r9", { listed: rows, idOf, getRow: noRoute }),
@@ -0,0 +1,158 @@
1
+ // @vitest-environment jsdom
2
+ import {
3
+ computed,
4
+ createApp,
5
+ defineComponent,
6
+ h,
7
+ nextTick,
8
+ provide,
9
+ ref,
10
+ shallowRef,
11
+ type App,
12
+ type FunctionalComponent,
13
+ } from "vue";
14
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
15
+ import TableFiltersRow from "../layers/dms-ui/app/build/components/table/FiltersRow.vue";
16
+ import type { TableUrlFilter } from "../layers/dms-ui/app/build/components/table/Table.vue";
17
+ import {
18
+ visibleUrlFilters,
19
+ withoutQueryParam,
20
+ } from "../layers/dms-ui/app/build/composables/table-view/utils/urlFilters";
21
+
22
+ describe("visibleUrlFilters", () => {
23
+ const declared = {
24
+ email: { field: "email", visible: true, label: "$app.email" },
25
+ template: { field: "templateSlug", mode: "is_not", visible: true },
26
+ tenant: { field: "tenantId" },
27
+ };
28
+
29
+ it("lists the visible filters the URL applies, hidden ones left out", () => {
30
+ expect(
31
+ visibleUrlFilters(declared, {
32
+ email: "a@b.test",
33
+ tenant: "t1",
34
+ page: "2",
35
+ }),
36
+ ).toEqual([
37
+ {
38
+ param: "email",
39
+ label: "$app.email",
40
+ accessorKey: "email",
41
+ mode: "is",
42
+ value: "a@b.test",
43
+ },
44
+ ]);
45
+ expect(visibleUrlFilters(declared, { template: "welcome" })).toEqual([
46
+ {
47
+ param: "template",
48
+ label: undefined,
49
+ accessorKey: "templateSlug",
50
+ mode: "is_not",
51
+ value: "welcome",
52
+ },
53
+ ]);
54
+ expect(visibleUrlFilters(undefined, { email: "a@b.test" })).toEqual([]);
55
+ });
56
+
57
+ it("removes one parameter from the query", () => {
58
+ expect(withoutQueryParam({ email: "a", tab: "all" }, "email")).toEqual({
59
+ tab: "all",
60
+ });
61
+ });
62
+ });
63
+
64
+ describe("filters row URL chips", () => {
65
+ let app: App | undefined;
66
+ const emits = vi.fn();
67
+
68
+ const mount = (urlFilters: TableUrlFilter[]) => {
69
+ const Host = defineComponent({
70
+ setup() {
71
+ provide(
72
+ "tableSharedData",
73
+ shallowRef({
74
+ columnFiltersState: ref([]),
75
+ labeledColumns: computed(() => [
76
+ {
77
+ column: { id: "templateSlug", columnDef: {} },
78
+ label: "Template",
79
+ },
80
+ ]),
81
+ clearableFilters: computed(() => ({
82
+ filters: false,
83
+ search: false,
84
+ })),
85
+ urlFilters,
86
+ emits,
87
+ }),
88
+ );
89
+ return () => h(TableFiltersRow);
90
+ },
91
+ });
92
+ app = createApp(Host);
93
+ const passthrough =
94
+ (tag: string): FunctionalComponent =>
95
+ (_props, { slots }) =>
96
+ h(tag, slots.default?.());
97
+ app.component("UPopover", passthrough("div"));
98
+ app.component("UButton", passthrough("button"));
99
+ app.component("UIcon", passthrough("i"));
100
+ app.component("Suspense", passthrough("div"));
101
+ const container = document.createElement("div");
102
+ app.mount(container);
103
+ return container;
104
+ };
105
+
106
+ beforeEach(() => {
107
+ emits.mockClear();
108
+ vi.stubGlobal("computed", computed);
109
+ vi.stubGlobal("ref", ref);
110
+ vi.stubGlobal("useI18n", () => ({
111
+ t: (key: string) => key,
112
+ locale: ref("en"),
113
+ }));
114
+ vi.stubGlobal("useTranslation", () => ({
115
+ processI18n: (text: string) =>
116
+ text.startsWith("$") ? `t:${text.slice(1)}` : text,
117
+ }));
118
+ vi.stubGlobal("useDmsAppConfig", () => ({
119
+ ui: { icons: { close: "i-close", plus: "i-plus" } },
120
+ }));
121
+ vi.stubGlobal("useDataTypes", () => ({ getDataType: () => undefined }));
122
+ vi.stubGlobal("isString", (value: unknown) => typeof value === "string");
123
+ vi.stubGlobal("resolveDmsComponent", () => undefined);
124
+ });
125
+
126
+ afterEach(() => {
127
+ app?.unmount();
128
+ app = undefined;
129
+ vi.unstubAllGlobals();
130
+ });
131
+
132
+ it("draws a chip per URL filter and asks to remove its parameter", async () => {
133
+ const container = mount([
134
+ {
135
+ param: "email",
136
+ label: "$app.email",
137
+ accessorKey: "email",
138
+ mode: "is",
139
+ value: "a@b.test",
140
+ },
141
+ {
142
+ param: "template",
143
+ accessorKey: "templateSlug",
144
+ mode: "is",
145
+ value: "welcome",
146
+ },
147
+ ]);
148
+ const chips = container.querySelectorAll("[data-dms-url-filter]");
149
+ expect(chips).toHaveLength(2);
150
+ expect(chips[0]!.textContent).toContain("t:app.email");
151
+ expect(chips[0]!.textContent).toContain("a@b.test");
152
+ expect(chips[1]!.textContent).toContain("Template");
153
+
154
+ chips[1]!.querySelector("button")!.click();
155
+ await nextTick();
156
+ expect(emits).toHaveBeenCalledWith("removeUrlFilter", "template");
157
+ });
158
+ });
@@ -13,6 +13,7 @@ import {
13
13
  rowNavigation,
14
14
  } from "../layers/dms-ui/app/build/composables/actions/rowNavigation";
15
15
  import {
16
+ deepLinkOutcome,
16
17
  readRecordId,
17
18
  recordUrlKey,
18
19
  writeRecordId,
@@ -130,6 +131,28 @@ describe("record deep links", () => {
130
131
  writeRecordId({ tableId: "runs", isSoleTableView: true });
131
132
  expect(window.location.search).toBe("?tab=open");
132
133
  });
134
+
135
+ it("notifies a link no row answers, unless a side panel shows it missing", () => {
136
+ const panel = {
137
+ target: { type: "sidePanel" },
138
+ } as unknown as CustomRowAction;
139
+ const drawer = {
140
+ target: { type: "drawer" },
141
+ deepLink: true,
142
+ } as unknown as CustomRowAction;
143
+ const row = { id: "r1" };
144
+ expect(deepLinkOutcome({ row, isMissing: false }, drawer)).toBe("open");
145
+ expect(deepLinkOutcome({ isMissing: true }, panel)).toBe("missingPanel");
146
+ expect(deepLinkOutcome({ isMissing: true }, drawer)).toBe("notify");
147
+ expect(deepLinkOutcome({ isMissing: false }, panel)).toBe("notify");
148
+ expect(deepLinkOutcome({ isMissing: false }, drawer)).toBe("notify");
149
+ expect(deepLinkOutcome({ isMissing: false, isFailed: true }, panel)).toBe(
150
+ "notifyFailed",
151
+ );
152
+ expect(deepLinkOutcome({ isMissing: false, isFailed: true }, drawer)).toBe(
153
+ "notifyFailed",
154
+ );
155
+ });
133
156
  });
134
157
 
135
158
  describe("action targets on a selection and on a listed row", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms",
3
- "version": "0.7.14",
3
+ "version": "0.7.15",
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.9 <0.6.0",
40
+ "@antelopejs/interface-dms": ">=0.5.10 <0.6.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",