@antelopejs/dms 0.5.2 → 0.5.4

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 (70) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/attachments/access.d.ts +1 -0
  3. package/dist/attachments/access.js +7 -3
  4. package/dist/attachments/access.js.map +1 -1
  5. package/dist/attachments/constraints.d.ts +4 -0
  6. package/dist/attachments/constraints.js +28 -0
  7. package/dist/attachments/constraints.js.map +1 -0
  8. package/dist/attachments/save.js +23 -12
  9. package/dist/attachments/save.js.map +1 -1
  10. package/dist/attachments/uploads.js +2 -0
  11. package/dist/attachments/uploads.js.map +1 -1
  12. package/dist/implementations/dms-base/search-route.js +3 -4
  13. package/dist/implementations/dms-base/search-route.js.map +1 -1
  14. package/dist/pages/settings/users/invite-email-outcome.d.ts +6 -0
  15. package/dist/pages/settings/users/invite-email-outcome.js +9 -0
  16. package/dist/pages/settings/users/invite-email-outcome.js.map +1 -0
  17. package/dist/pages/settings/users/invites.d.ts +4 -1
  18. package/dist/pages/settings/users/invites.js +33 -28
  19. package/dist/pages/settings/users/invites.js.map +1 -1
  20. package/dist/pages/settings/users/members.d.ts +8 -3
  21. package/dist/pages/settings/users/members.js +16 -4
  22. package/dist/pages/settings/users/members.js.map +1 -1
  23. package/dist/test/attachment-host/native.d.ts +25 -0
  24. package/dist/test/attachment-host/native.js +54 -1
  25. package/dist/test/attachment-host/native.js.map +1 -1
  26. package/dist/test/helpers/attachments.d.ts +1 -1
  27. package/dist/test/helpers/attachments.js +2 -2
  28. package/dist/test/helpers/attachments.js.map +1 -1
  29. package/dist/test/integration/files/field-constraints.test.d.ts +1 -0
  30. package/dist/test/integration/files/field-constraints.test.js +100 -0
  31. package/dist/test/integration/files/field-constraints.test.js.map +1 -0
  32. package/dist/test/integration/files/page-mode-form-files.test.d.ts +1 -0
  33. package/dist/test/integration/files/page-mode-form-files.test.js +114 -0
  34. package/dist/test/integration/files/page-mode-form-files.test.js.map +1 -0
  35. package/dist/test/integration/files/resource-form-files.test.d.ts +1 -0
  36. package/dist/test/integration/files/resource-form-files.test.js +121 -0
  37. package/dist/test/integration/files/resource-form-files.test.js.map +1 -0
  38. package/dist/test/integration/files/shared-controller-files.test.d.ts +1 -0
  39. package/dist/test/integration/files/shared-controller-files.test.js +136 -0
  40. package/dist/test/integration/files/shared-controller-files.test.js.map +1 -0
  41. package/dist/test/unit/implementations/dms-base/table-view-shared-controller.test.d.ts +1 -0
  42. package/dist/test/unit/implementations/dms-base/table-view-shared-controller.test.js +113 -0
  43. package/dist/test/unit/implementations/dms-base/table-view-shared-controller.test.js.map +1 -0
  44. package/dist/test/unit/interfaces/dms-base/resource-form.test.js +19 -0
  45. package/dist/test/unit/interfaces/dms-base/resource-form.test.js.map +1 -1
  46. package/dist/test/unit/interfaces/dms-base/table-view-tenant-gate.test.js +2 -2
  47. package/dist/test/unit/interfaces/dms-base/table-view-tenant-gate.test.js.map +1 -1
  48. package/dist/test/unit/pages/settings/invite-email-delivery.test.d.ts +1 -0
  49. package/dist/test/unit/pages/settings/invite-email-delivery.test.js +141 -0
  50. package/dist/test/unit/pages/settings/invite-email-delivery.test.js.map +1 -0
  51. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +2 -0
  52. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +2 -0
  53. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +9 -5
  54. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +50 -21
  55. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +54 -21
  56. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -3
  57. package/frontend-vue/layers/dms-ui/app/composables/form/types/api.ts +2 -1
  58. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +15 -8
  59. package/frontend-vue/layers/dms-ui/app/composables/form/useUploadWithProgress.ts +44 -3
  60. package/frontend-vue/layers/dms-ui/app/utils/metadataRetry.ts +47 -0
  61. package/frontend-vue/layers/dms-ui/app/utils/responseWarning.ts +46 -0
  62. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -1
  63. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -1
  64. package/frontend-vue/tests/file-unavailable.test.ts +107 -0
  65. package/frontend-vue/tests/image-hydration-ids.test.ts +87 -0
  66. package/frontend-vue/tests/image-reference-preservation.test.ts +12 -2
  67. package/frontend-vue/tests/metadata-retry.test.ts +187 -0
  68. package/frontend-vue/tests/response-warning-toast.test.ts +57 -0
  69. package/frontend-vue/tests/upload-forbidden-headers.test.ts +52 -0
  70. package/package.json +2 -2
@@ -0,0 +1,107 @@
1
+ // @vitest-environment jsdom
2
+ import {
3
+ computed,
4
+ createApp,
5
+ defineComponent,
6
+ h,
7
+ inject,
8
+ onScopeDispose,
9
+ ref,
10
+ watch,
11
+ type App,
12
+ } from "vue";
13
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
14
+ import File from "../layers/dms-ui/app/components/form/components/File.vue";
15
+
16
+ const STORED_KEY = "files/profile/avatar.pdf";
17
+ const authFetch = vi.fn();
18
+
19
+ const passthrough = defineComponent({
20
+ inheritAttrs: false,
21
+ setup:
22
+ (_, { slots }) =>
23
+ () =>
24
+ h("span", slots.default?.()),
25
+ });
26
+
27
+ function mountFile(): { app: App; container: HTMLElement } {
28
+ const app = createApp({
29
+ render: () => h(File, { modelValue: STORED_KEY }),
30
+ });
31
+ app.config.globalProperties.$t = (key: string) => key;
32
+ app.component("UFileUpload", passthrough);
33
+ app.component("ULink", passthrough);
34
+ app.component("UButton", defineComponent({ setup: () => () => h("button") }));
35
+ app.component(
36
+ "UIcon",
37
+ defineComponent({
38
+ props: ["name"],
39
+ setup: (props) => () => h("i", { "data-icon": props.name }),
40
+ }),
41
+ );
42
+ const container = document.createElement("div");
43
+ app.mount(container);
44
+ return { app, container };
45
+ }
46
+
47
+ beforeEach(() => {
48
+ vi.useFakeTimers();
49
+ vi.stubGlobal("ref", ref);
50
+ vi.stubGlobal("computed", computed);
51
+ vi.stubGlobal("watch", watch);
52
+ vi.stubGlobal("inject", inject);
53
+ vi.stubGlobal("onScopeDispose", onScopeDispose);
54
+ vi.stubGlobal("FORM_FIELD_LOADING_KEY", Symbol());
55
+ vi.stubGlobal("FORM_CONTENT_LANGUAGE_KEY", Symbol());
56
+ vi.stubGlobal("CONTENT_LANGUAGE_HEADER", "x-content-language");
57
+ vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
58
+ vi.stubGlobal("useUploadWithProgress", () => ({
59
+ uploadWithProgress: vi.fn(),
60
+ }));
61
+ vi.stubGlobal("useFormField", () => ({ emitFormChange: vi.fn() }));
62
+ vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
63
+ vi.stubGlobal("useToast", () => ({ add: vi.fn() }));
64
+ });
65
+
66
+ afterEach(() => {
67
+ vi.useRealTimers();
68
+ vi.unstubAllGlobals();
69
+ vi.resetAllMocks();
70
+ });
71
+
72
+ it("marks a stored file whose metadata cannot be read as unavailable", async () => {
73
+ authFetch.mockRejectedValue(new Error("404"));
74
+ const { app, container } = mountFile();
75
+ await vi.advanceTimersByTimeAsync(0);
76
+
77
+ expect(container.textContent).toContain("dms.form.file.file_unavailable");
78
+ expect(
79
+ container.querySelector('[data-icon="i-lucide-file-check"]'),
80
+ ).toBeNull();
81
+ expect(
82
+ container.querySelector('[data-icon="i-lucide-alert-circle"]'),
83
+ ).not.toBeNull();
84
+ app.unmount();
85
+ });
86
+
87
+ it("clears the unavailable state once a retry reads the file", async () => {
88
+ authFetch.mockRejectedValueOnce(new Error("503")).mockResolvedValueOnce({
89
+ resourceKey: STORED_KEY,
90
+ filename: "avatar.pdf",
91
+ size: 2048,
92
+ mimetype: "application/pdf",
93
+ url: "https://example.test/avatar.pdf",
94
+ });
95
+ const { app, container } = mountFile();
96
+ await vi.advanceTimersByTimeAsync(0);
97
+ expect(container.textContent).toContain("dms.form.file.file_unavailable");
98
+
99
+ await vi.advanceTimersByTimeAsync(1000);
100
+
101
+ expect(container.textContent).not.toContain("dms.form.file.file_unavailable");
102
+ expect(container.textContent).toContain("avatar.pdf");
103
+ expect(
104
+ container.querySelector('[data-icon="i-lucide-file-check"]'),
105
+ ).not.toBeNull();
106
+ app.unmount();
107
+ });
@@ -0,0 +1,87 @@
1
+ // @vitest-environment jsdom
2
+ import {
3
+ computed,
4
+ createSSRApp,
5
+ defineComponent,
6
+ h,
7
+ inject,
8
+ onBeforeUnmount,
9
+ ref,
10
+ useId,
11
+ watch,
12
+ type App,
13
+ } from "vue";
14
+ import { renderToString } from "vue/server-renderer";
15
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
16
+ import Image from "../layers/dms-ui/app/components/form/components/Image.vue";
17
+
18
+ const STORED_IMAGE = { key: "files/profile/cover.png", alt: "Cover" };
19
+
20
+ const passthrough = defineComponent({
21
+ inheritAttrs: false,
22
+ setup:
23
+ (_, { slots }) =>
24
+ () =>
25
+ slots.default?.(),
26
+ });
27
+
28
+ function createImageApp(): App {
29
+ const app = createSSRApp({
30
+ render: () => h(Image, { modelValue: STORED_IMAGE }),
31
+ });
32
+ app.component("UButton", defineComponent({ setup: () => () => h("button") }));
33
+ app.component("UIcon", defineComponent({ setup: () => () => h("span") }));
34
+ app.component("UInput", defineComponent({ setup: () => () => h("input") }));
35
+ app.component("UTooltip", passthrough);
36
+ app.component("UModal", defineComponent({ setup: () => () => null }));
37
+ return app;
38
+ }
39
+
40
+ beforeEach(() => {
41
+ vi.stubGlobal("ref", ref);
42
+ vi.stubGlobal("computed", computed);
43
+ vi.stubGlobal("watch", watch);
44
+ vi.stubGlobal("inject", inject);
45
+ vi.stubGlobal("useId", useId);
46
+ vi.stubGlobal("onBeforeUnmount", onBeforeUnmount);
47
+ vi.stubGlobal("FORM_FIELD_LOADING_KEY", Symbol());
48
+ vi.stubGlobal("FORM_CONTENT_LANGUAGE_KEY", Symbol());
49
+ vi.stubGlobal("CONTENT_LANGUAGE_HEADER", "x-content-language");
50
+ vi.stubGlobal("useAuthFetch", () => ({
51
+ $authFetch: () => new Promise(() => undefined),
52
+ }));
53
+ vi.stubGlobal("useUploadWithProgress", () => ({
54
+ uploadWithProgress: vi.fn(),
55
+ }));
56
+ vi.stubGlobal("useFormField", () => ({ emitFormChange: vi.fn() }));
57
+ vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
58
+ vi.stubGlobal("useToast", () => ({ add: vi.fn() }));
59
+ });
60
+
61
+ afterEach(() => {
62
+ vi.unstubAllGlobals();
63
+ });
64
+
65
+ it("hydrates a stored single image without an attribute mismatch", async () => {
66
+ const html = await renderToString(createImageApp());
67
+ const container = document.createElement("div");
68
+ container.innerHTML = html;
69
+ const warnings: string[] = [];
70
+ const app = createImageApp();
71
+ app.config.warnHandler = (message) => warnings.push(message);
72
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
73
+
74
+ app.mount(container);
75
+
76
+ expect(warnings.filter((message) => message.includes("Hydration"))).toEqual(
77
+ [],
78
+ );
79
+ expect(consoleError).not.toHaveBeenCalledWith(
80
+ expect.stringContaining("Hydration"),
81
+ );
82
+ const label = container.querySelector("label")!;
83
+ expect(label.htmlFor).not.toBe("");
84
+ expect(container.querySelector("input:not([type])")!.id).toBe(label.htmlFor);
85
+ app.unmount();
86
+ consoleError.mockRestore();
87
+ });
@@ -5,6 +5,7 @@ import {
5
5
  createSSRApp,
6
6
  defineComponent,
7
7
  effectScope,
8
+ getCurrentInstance,
8
9
  h,
9
10
  nextTick,
10
11
  onScopeDispose,
@@ -12,12 +13,17 @@ import {
12
13
  reactive,
13
14
  ref,
14
15
  watch,
16
+ useId,
15
17
  type EffectScope,
16
18
  type Ref,
17
19
  } from "vue";
18
20
  import { renderToString } from "vue/server-renderer";
19
21
  import { afterEach, beforeEach, expect, it, vi } from "vitest";
20
22
  import Image from "../layers/dms-ui/app/components/form/components/Image.vue";
23
+ import {
24
+ isDefinitiveMetadataFailure,
25
+ resolveMetadataRetryDelay,
26
+ } from "../layers/dms-ui/app/utils/metadataRetry";
21
27
 
22
28
  interface ImageValue {
23
29
  key: string;
@@ -90,11 +96,14 @@ beforeEach(() => {
90
96
  }),
91
97
  );
92
98
  vi.stubGlobal("defineProps", () => props);
99
+ vi.stubGlobal("isDefinitiveMetadataFailure", isDefinitiveMetadataFailure);
100
+ vi.stubGlobal("resolveMetadataRetryDelay", resolveMetadataRetryDelay);
93
101
  vi.stubGlobal("defineEmits", () => emit);
94
102
  vi.stubGlobal("ref", ref);
95
103
  vi.stubGlobal("computed", computed);
96
104
  vi.stubGlobal("watch", watch);
97
105
  vi.stubGlobal("inject", () => null);
106
+ vi.stubGlobal("useId", () => (getCurrentInstance() ? useId() : "image"));
98
107
  vi.stubGlobal("FORM_FIELD_LOADING_KEY", Symbol());
99
108
  vi.stubGlobal("FORM_CONTENT_LANGUAGE_KEY", Symbol());
100
109
  vi.stubGlobal("CONTENT_LANGUAGE_HEADER", "x-content-language");
@@ -117,7 +126,7 @@ afterEach(() => {
117
126
  it("preserves a denied image when another image is removed", async () => {
118
127
  const image = loadImage();
119
128
  await nextTick();
120
- expect(image.items.value[0]!.status).toBe("error");
129
+ expect(image.items.value[0]!.status).toBe("unavailable");
121
130
  image.removeItem(image.items.value[1]!.id);
122
131
  expect(emit).toHaveBeenLastCalledWith("update:modelValue", [original]);
123
132
  });
@@ -191,7 +200,8 @@ it("keeps the denied image and its explicit deletion control in rendered markup"
191
200
  );
192
201
  app.component("UModal", defineComponent({ setup: () => () => null }));
193
202
  const html = await renderToString(app);
194
- expect(html).toContain("dms.form.image.upload_failed");
203
+ expect(html).toContain("dms.form.image.file_unavailable");
204
+ expect(html).not.toContain("dms.form.image.upload_failed");
195
205
  expect(html).toContain("dms.form.image.delete</button>");
196
206
  expect(emit).not.toHaveBeenCalled();
197
207
  });
@@ -0,0 +1,187 @@
1
+ // @vitest-environment jsdom
2
+ import {
3
+ type App,
4
+ type Component,
5
+ computed,
6
+ createApp,
7
+ type FunctionalComponent,
8
+ h,
9
+ inject,
10
+ onBeforeUnmount,
11
+ onScopeDispose,
12
+ ref,
13
+ useId,
14
+ watch,
15
+ } from "vue";
16
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
17
+ import File from "../layers/dms-ui/app/components/form/components/File.vue";
18
+ import Image from "../layers/dms-ui/app/components/form/components/Image.vue";
19
+ import {
20
+ isDefinitiveMetadataFailure,
21
+ resolveMetadataRetryDelay,
22
+ } from "../layers/dms-ui/app/utils/metadataRetry";
23
+
24
+ const authFetch = vi.fn();
25
+ const LONG_WAIT_MS = 120000;
26
+
27
+ interface FieldCase {
28
+ name: string;
29
+ component: Component;
30
+ modelValue: unknown;
31
+ unavailableLabel: string;
32
+ metadata: Record<string, unknown>;
33
+ }
34
+
35
+ const FIELD_CASES: FieldCase[] = [
36
+ {
37
+ name: "File",
38
+ component: File,
39
+ modelValue: "files/profile/avatar.pdf",
40
+ unavailableLabel: "dms.form.file.file_unavailable",
41
+ metadata: {
42
+ resourceKey: "files/profile/avatar.pdf",
43
+ filename: "avatar.pdf",
44
+ size: 2048,
45
+ mimetype: "application/pdf",
46
+ url: "https://example.test/avatar.pdf",
47
+ },
48
+ },
49
+ {
50
+ name: "Image",
51
+ component: Image,
52
+ modelValue: { key: "files/profile/cover.png", alt: "Cover" },
53
+ unavailableLabel: "dms.form.image.file_unavailable",
54
+ metadata: {
55
+ resourceKey: "files/profile/cover.png",
56
+ filename: "cover.png",
57
+ size: 2048,
58
+ mimetype: "image/png",
59
+ url: "https://example.test/cover.png",
60
+ },
61
+ },
62
+ ];
63
+
64
+ const passthrough: FunctionalComponent = (_, { slots }) =>
65
+ h("span", slots.default?.());
66
+
67
+ const renderTag =
68
+ (tag: string): FunctionalComponent =>
69
+ () =>
70
+ h(tag);
71
+
72
+ function httpError(status: number): Error {
73
+ return Object.assign(new Error(`HTTP ${status}`), {
74
+ status,
75
+ statusCode: status,
76
+ });
77
+ }
78
+
79
+ function mountField(field: FieldCase): { app: App; container: HTMLElement } {
80
+ const app = createApp({
81
+ render: () => h(field.component, { modelValue: field.modelValue }),
82
+ });
83
+ app.config.globalProperties.$t = (key: string) => key;
84
+ for (const name of ["UFileUpload", "ULink", "UTooltip"])
85
+ app.component(name, passthrough);
86
+ app.component("UButton", renderTag("button"));
87
+ app.component("UIcon", renderTag("i"));
88
+ app.component("UInput", renderTag("input"));
89
+ app.component("UModal", () => null);
90
+ const container = document.createElement("div");
91
+ app.mount(container);
92
+ return { app, container };
93
+ }
94
+
95
+ beforeEach(() => {
96
+ vi.useFakeTimers();
97
+ vi.stubGlobal("ref", ref);
98
+ vi.stubGlobal("computed", computed);
99
+ vi.stubGlobal("watch", watch);
100
+ vi.stubGlobal("inject", inject);
101
+ vi.stubGlobal("useId", useId);
102
+ vi.stubGlobal("onBeforeUnmount", onBeforeUnmount);
103
+ vi.stubGlobal("onScopeDispose", onScopeDispose);
104
+ vi.stubGlobal("FORM_FIELD_LOADING_KEY", Symbol());
105
+ vi.stubGlobal("FORM_CONTENT_LANGUAGE_KEY", Symbol());
106
+ vi.stubGlobal("CONTENT_LANGUAGE_HEADER", "x-content-language");
107
+ vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
108
+ vi.stubGlobal("useUploadWithProgress", () => ({
109
+ uploadWithProgress: vi.fn(),
110
+ }));
111
+ vi.stubGlobal("useFormField", () => ({ emitFormChange: vi.fn() }));
112
+ vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
113
+ vi.stubGlobal("useToast", () => ({ add: vi.fn() }));
114
+ });
115
+
116
+ afterEach(() => {
117
+ vi.useRealTimers();
118
+ vi.unstubAllGlobals();
119
+ vi.resetAllMocks();
120
+ });
121
+
122
+ describe.each(FIELD_CASES)("$name stored-value metadata", (field) => {
123
+ it.each([403, 404, 410])(
124
+ "reads a file answered with %i exactly once and shows it unavailable",
125
+ async (status) => {
126
+ authFetch.mockRejectedValue(httpError(status));
127
+ const { app, container } = mountField(field);
128
+
129
+ await vi.advanceTimersByTimeAsync(LONG_WAIT_MS);
130
+
131
+ expect(authFetch).toHaveBeenCalledTimes(1);
132
+ expect(container.textContent).toContain(field.unavailableLabel);
133
+ app.unmount();
134
+ },
135
+ );
136
+
137
+ it("retries transient failures with a growing delay until the read succeeds", async () => {
138
+ authFetch
139
+ .mockRejectedValueOnce(httpError(503))
140
+ .mockRejectedValueOnce(new Error("network down"))
141
+ .mockRejectedValueOnce(httpError(429))
142
+ .mockResolvedValueOnce(field.metadata);
143
+ const { app, container } = mountField(field);
144
+ await vi.advanceTimersByTimeAsync(0);
145
+ expect(authFetch).toHaveBeenCalledTimes(1);
146
+ expect(container.textContent).toContain(field.unavailableLabel);
147
+
148
+ await vi.advanceTimersByTimeAsync(1000);
149
+ expect(authFetch).toHaveBeenCalledTimes(2);
150
+ await vi.advanceTimersByTimeAsync(1999);
151
+ expect(authFetch).toHaveBeenCalledTimes(2);
152
+ await vi.advanceTimersByTimeAsync(1);
153
+ expect(authFetch).toHaveBeenCalledTimes(3);
154
+ await vi.advanceTimersByTimeAsync(4000);
155
+ expect(authFetch).toHaveBeenCalledTimes(4);
156
+
157
+ expect(container.textContent).not.toContain(field.unavailableLabel);
158
+ await vi.advanceTimersByTimeAsync(LONG_WAIT_MS);
159
+ expect(authFetch).toHaveBeenCalledTimes(4);
160
+ app.unmount();
161
+ });
162
+ });
163
+
164
+ describe("metadata retry policy", () => {
165
+ it("treats refusals and missing files as definitive", () => {
166
+ for (const status of [400, 401, 403, 404, 410])
167
+ expect(isDefinitiveMetadataFailure(httpError(status))).toBe(true);
168
+ for (const error of [
169
+ httpError(408),
170
+ httpError(429),
171
+ httpError(500),
172
+ httpError(503),
173
+ new Error("network down"),
174
+ undefined,
175
+ ])
176
+ expect(isDefinitiveMetadataFailure(error)).toBe(false);
177
+ });
178
+
179
+ it("backs off exponentially up to a cap and never retries a definitive failure", () => {
180
+ const transient = httpError(503);
181
+ expect(resolveMetadataRetryDelay(transient, 1)).toBe(1000);
182
+ expect(resolveMetadataRetryDelay(transient, 2)).toBe(2000);
183
+ expect(resolveMetadataRetryDelay(transient, 3)).toBe(4000);
184
+ expect(resolveMetadataRetryDelay(transient, 50)).toBe(30000);
185
+ expect(resolveMetadataRetryDelay(httpError(404), 1)).toBeNull();
186
+ });
187
+ });
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ readResponseWarning,
4
+ resolveResponseToast,
5
+ } from "../layers/dms-ui/app/utils/responseWarning";
6
+ import en from "../layers/dms-layout/i18n/locales/layout-en-GB.json";
7
+ import fr from "../layers/dms-layout/i18n/locales/layout-fr-FR.json";
8
+ import uiEn from "../layers/dms-ui/i18n/locales/ui-en-GB.json";
9
+ import uiFr from "../layers/dms-ui/i18n/locales/ui-fr-FR.json";
10
+
11
+ const INVITE_EMAIL_FAILED = "$page.settings.members.invite.email_failed";
12
+ const SUCCESS = { color: "success", title: "Invitation sent successfully" };
13
+
14
+ const translation = {
15
+ processI18n: (key: string) => `i18n:${key}`,
16
+ processApiMessage: (message: unknown) => `api:${String(message)}`,
17
+ };
18
+
19
+ describe("response warning toast", () => {
20
+ it("tells the warning instead of the success when the response carries one", () => {
21
+ expect(
22
+ resolveResponseToast(
23
+ { redirectPath: "/invites", warning: INVITE_EMAIL_FAILED },
24
+ SUCCESS,
25
+ translation,
26
+ ),
27
+ ).toEqual({
28
+ color: "warning",
29
+ title: "i18n:$dms.form.warning_title",
30
+ description: `api:${INVITE_EMAIL_FAILED}`,
31
+ });
32
+ });
33
+
34
+ it("keeps the success toast for a plain success", () => {
35
+ expect(
36
+ resolveResponseToast({ emailDelivery: "sent" }, SUCCESS, translation),
37
+ ).toBe(SUCCESS);
38
+ expect(resolveResponseToast(undefined, SUCCESS, translation)).toBe(SUCCESS);
39
+ expect(resolveResponseToast("", SUCCESS, translation)).toBe(SUCCESS);
40
+ });
41
+
42
+ it("ignores a warning that is not a message", () => {
43
+ expect(readResponseWarning({ warning: "" })).toBeUndefined();
44
+ expect(readResponseWarning({ warning: 42 })).toBeUndefined();
45
+ });
46
+
47
+ it("has the invitation warnings in every shipped locale", () => {
48
+ for (const locale of [en, fr]) {
49
+ const settings = locale.page.settings;
50
+ expect(settings.members.invite.email_failed).toBeTruthy();
51
+ expect(settings.invites.action.resend_email_failed).toBeTruthy();
52
+ }
53
+ for (const locale of [uiEn, uiFr]) {
54
+ expect(locale.dms.form.warning_title).toBeTruthy();
55
+ }
56
+ });
57
+ });
@@ -0,0 +1,52 @@
1
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
2
+ import { useUploadWithProgress } from "../layers/dms-ui/app/composables/form/useUploadWithProgress";
3
+
4
+ const UPLOAD_URL = "https://storage.example.test/upload/token";
5
+ const setRequestHeader = vi.fn();
6
+
7
+ class FakeXMLHttpRequest {
8
+ status = 200;
9
+ upload = {};
10
+ onload: (() => void) | null = null;
11
+ open = vi.fn();
12
+ setRequestHeader = setRequestHeader;
13
+ send() {
14
+ this.onload?.();
15
+ }
16
+ }
17
+
18
+ beforeEach(() => {
19
+ vi.stubGlobal("XMLHttpRequest", FakeXMLHttpRequest);
20
+ });
21
+
22
+ afterEach(() => {
23
+ vi.unstubAllGlobals();
24
+ vi.resetAllMocks();
25
+ });
26
+
27
+ it("forwards presign headers except the ones a browser refuses to set", async () => {
28
+ const { uploadWithProgress } = useUploadWithProgress();
29
+
30
+ await uploadWithProgress(
31
+ {
32
+ uploadUrl: UPLOAD_URL,
33
+ resourceKey: "__staging__/cover.png",
34
+ headers: {
35
+ "Content-Type": "image/png",
36
+ "Content-Length": "42",
37
+ host: "storage.example.test",
38
+ Connection: "keep-alive",
39
+ "Sec-Fetch-Mode": "cors",
40
+ "Proxy-Authorization": "Basic secret",
41
+ "x-amz-meta-owner": "tenant",
42
+ },
43
+ },
44
+ new File(["bytes"], "cover.png", { type: "image/png" }),
45
+ () => undefined,
46
+ );
47
+
48
+ expect(setRequestHeader.mock.calls).toEqual([
49
+ ["Content-Type", "image/png"],
50
+ ["x-amz-meta-owner", "tenant"],
51
+ ]);
52
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/AntelopeJS/dms.git"
@@ -36,7 +36,7 @@
36
36
  "@antelopejs/interface-data-api": ">=0.1.9 <1.0.0",
37
37
  "@antelopejs/interface-database": ">=0.1.8 <1.0.0",
38
38
  "@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
39
- "@antelopejs/interface-dms": ">=0.3.3 <0.4.0",
39
+ "@antelopejs/interface-dms": ">=0.3.5 <0.4.0",
40
40
  "@antelopejs/interface-email": ">=0.0.5 <1.0.0",
41
41
  "@antelopejs/interface-file-storage": ">=0.1.3 <1.0.0",
42
42
  "archiver": "^7.0.1",