@antelopejs/dms 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +21 -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/test/attachment-host/native.d.ts +25 -0
  15. package/dist/test/attachment-host/native.js +54 -1
  16. package/dist/test/attachment-host/native.js.map +1 -1
  17. package/dist/test/helpers/attachments.d.ts +1 -1
  18. package/dist/test/helpers/attachments.js +2 -2
  19. package/dist/test/helpers/attachments.js.map +1 -1
  20. package/dist/test/integration/files/field-constraints.test.d.ts +1 -0
  21. package/dist/test/integration/files/field-constraints.test.js +100 -0
  22. package/dist/test/integration/files/field-constraints.test.js.map +1 -0
  23. package/dist/test/integration/files/page-mode-form-files.test.d.ts +1 -0
  24. package/dist/test/integration/files/page-mode-form-files.test.js +114 -0
  25. package/dist/test/integration/files/page-mode-form-files.test.js.map +1 -0
  26. package/dist/test/integration/files/resource-form-files.test.d.ts +1 -0
  27. package/dist/test/integration/files/resource-form-files.test.js +121 -0
  28. package/dist/test/integration/files/resource-form-files.test.js.map +1 -0
  29. package/dist/test/integration/files/shared-controller-files.test.d.ts +1 -0
  30. package/dist/test/integration/files/shared-controller-files.test.js +136 -0
  31. package/dist/test/integration/files/shared-controller-files.test.js.map +1 -0
  32. package/dist/test/unit/implementations/dms-base/table-view-shared-controller.test.d.ts +1 -0
  33. package/dist/test/unit/implementations/dms-base/table-view-shared-controller.test.js +113 -0
  34. package/dist/test/unit/implementations/dms-base/table-view-shared-controller.test.js.map +1 -0
  35. package/dist/test/unit/interfaces/dms-base/resource-form.test.js +19 -0
  36. package/dist/test/unit/interfaces/dms-base/resource-form.test.js.map +1 -1
  37. package/dist/test/unit/interfaces/dms-base/table-view-tenant-gate.test.js +2 -2
  38. package/dist/test/unit/interfaces/dms-base/table-view-tenant-gate.test.js.map +1 -1
  39. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +50 -21
  40. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +54 -21
  41. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -3
  42. package/frontend-vue/layers/dms-ui/app/composables/form/useUploadWithProgress.ts +44 -3
  43. package/frontend-vue/layers/dms-ui/app/utils/metadataRetry.ts +47 -0
  44. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +3 -1
  45. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +3 -1
  46. package/frontend-vue/tests/file-unavailable.test.ts +107 -0
  47. package/frontend-vue/tests/image-hydration-ids.test.ts +87 -0
  48. package/frontend-vue/tests/image-reference-preservation.test.ts +12 -2
  49. package/frontend-vue/tests/metadata-retry.test.ts +187 -0
  50. package/frontend-vue/tests/upload-forbidden-headers.test.ts +52 -0
  51. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"table-view-tenant-gate.test.js","sourceRoot":"","sources":["../../../../../src/test/unit/interfaces/dms-base/table-view-tenant-gate.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"table-view-tenant-gate.test.js","sourceRoot":"","sources":["../../../../../src/test/unit/interfaces/dms-base/table-view-tenant-gate.test.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,10 @@ import {
7
7
  type FileConstraintViolation,
8
8
  type FileFieldConstraints,
9
9
  } from "../../../utils/fileConstraints";
10
+ import {
11
+ isDefinitiveMetadataFailure,
12
+ resolveMetadataRetryDelay,
13
+ } from "../../../utils/metadataRetry";
10
14
 
11
15
  export type FileConstraints = FileFieldConstraints;
12
16
 
@@ -53,12 +57,13 @@ const selectedFiles = ref<File | File[] | null>(null);
53
57
  const uploadingFiles = ref<UploadingFile[]>([]);
54
58
  const uploadedKeys = ref<string[]>([]);
55
59
  const fileMetadata = ref<Map<string, FileMetadataResponse>>(new Map());
60
+ const unavailableKeys = ref<Set<string>>(new Set());
56
61
  const metadataTimers = new Map<string, ReturnType<typeof setTimeout>>();
62
+ const metadataFailures = new Map<string, number>();
57
63
  const pendingMetadata = new Map<string, Promise<void>>();
58
64
  let isDisposed = false;
59
65
  const contentLanguage = inject(FORM_CONTENT_LANGUAGE_KEY, undefined);
60
66
  const URL_REFRESH_LEAD_MS = 5000;
61
- const TRANSIENT_RETRY_MS = 1000;
62
67
 
63
68
  const initializeFromModelValue = (
64
69
  val: string | string[] | null | undefined,
@@ -106,6 +111,33 @@ const fetchMetadata = async (
106
111
  await request;
107
112
  };
108
113
 
114
+ const markUnavailable = (resourceKey: string, error: unknown) => {
115
+ const cached = fileMetadata.value.get(resourceKey);
116
+ const isExpired =
117
+ cached?.expiresAt !== undefined && cached.expiresAt <= Date.now();
118
+ if (cached && !isExpired && !isDefinitiveMetadataFailure(error)) return;
119
+ fileMetadata.value.set(resourceKey, {
120
+ resourceKey,
121
+ filename: cached?.filename ?? resourceKey,
122
+ size: cached?.size ?? 0,
123
+ mimetype: cached?.mimetype ?? "application/octet-stream",
124
+ url: "",
125
+ });
126
+ unavailableKeys.value.add(resourceKey);
127
+ };
128
+
129
+ const handleMetadataFailure = (resourceKey: string, error: unknown) => {
130
+ markUnavailable(resourceKey, error);
131
+ const failures = (metadataFailures.get(resourceKey) ?? 0) + 1;
132
+ metadataFailures.set(resourceKey, failures);
133
+ const delay = resolveMetadataRetryDelay(error, failures);
134
+ if (delay === null || import.meta.env.SSR) return;
135
+ metadataTimers.set(
136
+ resourceKey,
137
+ setTimeout(() => fetchMetadata(resourceKey, true), delay),
138
+ );
139
+ };
140
+
109
141
  const requestMetadata = async (resourceKey: string): Promise<void> => {
110
142
  try {
111
143
  const metadata = await $authFetch<FileMetadataResponse>(
@@ -122,27 +154,12 @@ const requestMetadata = async (resourceKey: string): Promise<void> => {
122
154
  );
123
155
  if (isDisposed || !uploadedKeys.value.includes(resourceKey)) return;
124
156
  fileMetadata.value.set(resourceKey, metadata);
157
+ unavailableKeys.value.delete(resourceKey);
158
+ metadataFailures.delete(resourceKey);
125
159
  scheduleMetadataRefresh(resourceKey, metadata.expiresAt);
126
- } catch (_error) {
160
+ } catch (error) {
127
161
  if (isDisposed || !uploadedKeys.value.includes(resourceKey)) return;
128
- const cached = fileMetadata.value.get(resourceKey);
129
- const isExpired =
130
- cached?.expiresAt !== undefined && cached.expiresAt <= Date.now();
131
- if (!cached || isExpired) {
132
- fileMetadata.value.set(resourceKey, {
133
- resourceKey,
134
- filename: cached?.filename ?? resourceKey,
135
- size: cached?.size ?? 0,
136
- mimetype: cached?.mimetype ?? "application/octet-stream",
137
- url: "",
138
- });
139
- }
140
- if (!import.meta.env.SSR) {
141
- metadataTimers.set(
142
- resourceKey,
143
- setTimeout(() => fetchMetadata(resourceKey, true), TRANSIENT_RETRY_MS),
144
- );
145
- }
162
+ handleMetadataFailure(resourceKey, error);
146
163
  } finally {
147
164
  pendingMetadata.delete(resourceKey);
148
165
  }
@@ -284,6 +301,8 @@ const removeUploadingFile = (id: string) => {
284
301
  const removeUploadedFile = (key: string) => {
285
302
  uploadedKeys.value = uploadedKeys.value.filter((k) => k !== key);
286
303
  fileMetadata.value.delete(key);
304
+ unavailableKeys.value.delete(key);
305
+ metadataFailures.delete(key);
287
306
  const timer = metadataTimers.get(key);
288
307
  if (timer) clearTimeout(timer);
289
308
  emitValue();
@@ -293,6 +312,8 @@ const getMetadata = (key: string): FileMetadataResponse | undefined => {
293
312
  return fileMetadata.value.get(key);
294
313
  };
295
314
 
315
+ const isUnavailable = (key: string): boolean => unavailableKeys.value.has(key);
316
+
296
317
  const isImage = (mimetype: string): boolean => {
297
318
  return mimetype.startsWith("image/");
298
319
  };
@@ -399,6 +420,11 @@ onScopeDispose(() => {
399
420
  :alt="getMetadata(key)!.filename"
400
421
  class="h-8 w-8 rounded object-cover"
401
422
  />
423
+ <UIcon
424
+ v-else-if="isUnavailable(key)"
425
+ name="i-lucide-alert-circle"
426
+ class="text-error h-4 w-4"
427
+ />
402
428
  <UIcon
403
429
  v-else
404
430
  name="i-lucide-file-check"
@@ -412,7 +438,10 @@ onScopeDispose(() => {
412
438
  >
413
439
  {{ getMetadata(key)!.filename }}
414
440
  </ULink>
415
- <span class="text-muted text-xs">
441
+ <span v-if="isUnavailable(key)" class="text-error text-xs">
442
+ {{ $t("dms.form.file.file_unavailable") }}
443
+ </span>
444
+ <span v-else class="text-muted text-xs">
416
445
  {{ formatFileSize(getMetadata(key)!.size) }}
417
446
  </span>
418
447
  </template>
@@ -10,6 +10,10 @@ import {
10
10
  type ImageResizeBounds,
11
11
  resizeImageToBounds,
12
12
  } from "../../../utils/imageResize";
13
+ import {
14
+ isDefinitiveMetadataFailure,
15
+ resolveMetadataRetryDelay,
16
+ } from "../../../utils/metadataRetry";
13
17
 
14
18
  export type ImageConstraints = FileFieldConstraints;
15
19
 
@@ -25,7 +29,11 @@ interface ImageProps {
25
29
  disabled?: boolean;
26
30
  }
27
31
 
28
- type ItemStatus = "uploading" | "done" | "error";
32
+ /**
33
+ * `error` is a failed upload of a picked file; `unavailable` is a stored value
34
+ * whose metadata could not be read (deleted, swept from staging, or denied).
35
+ */
36
+ type ItemStatus = "uploading" | "done" | "error" | "unavailable";
29
37
 
30
38
  interface GalleryItem {
31
39
  id: string;
@@ -67,7 +75,18 @@ const contentLanguage = inject(FORM_CONTENT_LANGUAGE_KEY, undefined);
67
75
 
68
76
  const DEFAULT_ACCEPT = "image/*";
69
77
  const URL_REFRESH_LEAD_MS = 5000;
70
- const TRANSIENT_RETRY_MS = 1000;
78
+ const FAILURE_LABEL_KEYS: Partial<Record<ItemStatus, string>> = {
79
+ error: "dms.form.image.upload_failed",
80
+ unavailable: "dms.form.image.file_unavailable",
81
+ };
82
+
83
+ /**
84
+ * Item ids are random, so they differ between the server render and
85
+ * hydration; DOM ids derive from `useId()`, which both sides agree on.
86
+ */
87
+ const fieldId = useId();
88
+ const singleAltInputId = `${fieldId}-alt`;
89
+ const detailAltInputId = `${fieldId}-alt-detail`;
71
90
 
72
91
  const items = ref<GalleryItem[]>([]);
73
92
  const fileInput = ref<HTMLInputElement | null>(null);
@@ -77,6 +96,7 @@ const dragOverItemId = ref<string | null>(null);
77
96
  const detailItemId = ref<string | null>(null);
78
97
  const detailAltDraft = ref("");
79
98
  const metadataTimers = new Map<string, ReturnType<typeof setTimeout>>();
99
+ const metadataFailures = new Map<string, number>();
80
100
  const pendingMetadata = new Set<string>();
81
101
  let isDisposed = false;
82
102
 
@@ -120,6 +140,21 @@ const scheduleItemExpiryRefresh = (item: GalleryItem) => {
120
140
  scheduleItemMetadataRefresh(item, remaining - refreshLead);
121
141
  };
122
142
 
143
+ const handleItemMetadataFailure = (item: GalleryItem, error: unknown) => {
144
+ if (
145
+ isDefinitiveMetadataFailure(error) ||
146
+ !item.url ||
147
+ (item.expiresAt !== undefined && item.expiresAt <= Date.now())
148
+ ) {
149
+ item.url = "";
150
+ item.status = "unavailable";
151
+ }
152
+ const failures = (metadataFailures.get(item.id) ?? 0) + 1;
153
+ metadataFailures.set(item.id, failures);
154
+ const delay = resolveMetadataRetryDelay(error, failures);
155
+ if (delay !== null) scheduleItemMetadataRefresh(item, delay);
156
+ };
157
+
123
158
  const fetchItemMetadata = async (item: GalleryItem, force = false) => {
124
159
  if (isDisposed) return;
125
160
  if (!item.key || (!force && item.url) || pendingMetadata.has(item.id)) return;
@@ -141,17 +176,11 @@ const fetchItemMetadata = async (item: GalleryItem, force = false) => {
141
176
  item.size = metadata.size;
142
177
  item.expiresAt = metadata.expiresAt;
143
178
  item.status = "done";
179
+ metadataFailures.delete(item.id);
144
180
  scheduleItemExpiryRefresh(item);
145
- } catch (_error) {
181
+ } catch (error) {
146
182
  if (isDisposed || !items.value.some(({ id }) => id === item.id)) return;
147
- if (
148
- !item.url ||
149
- (item.expiresAt !== undefined && item.expiresAt <= Date.now())
150
- ) {
151
- item.url = "";
152
- item.status = "error";
153
- }
154
- scheduleItemMetadataRefresh(item, TRANSIENT_RETRY_MS);
183
+ handleItemMetadataFailure(item, error);
155
184
  } finally {
156
185
  pendingMetadata.delete(item.id);
157
186
  }
@@ -180,6 +209,7 @@ const releaseItem = (item: GalleryItem) => {
180
209
  const timer = metadataTimers.get(item.id);
181
210
  if (timer) clearTimeout(timer);
182
211
  metadataTimers.delete(item.id);
212
+ metadataFailures.delete(item.id);
183
213
  };
184
214
 
185
215
  const initializeFromModelValue = () => {
@@ -524,6 +554,9 @@ const formatFileSize = (bytes: number): string => {
524
554
  return `${size.toFixed(1)} ${units[unitIndex]}`;
525
555
  };
526
556
 
557
+ const failureLabelKey = (item: GalleryItem): string | undefined =>
558
+ FAILURE_LABEL_KEYS[item.status];
559
+
527
560
  const itemMeta = (item: GalleryItem): string => {
528
561
  const parts: string[] = [];
529
562
  if (item.width && item.height) parts.push(`${item.width}×${item.height}`);
@@ -650,13 +683,13 @@ onBeforeUnmount(() => {
650
683
  </div>
651
684
 
652
685
  <div
653
- v-else-if="item.status === 'error'"
686
+ v-else-if="failureLabelKey(item)"
654
687
  class="bg-error/15 absolute inset-0 z-[5] flex flex-col items-center justify-center gap-2 p-2 text-center backdrop-blur-[1px]"
655
688
  >
656
689
  <span
657
690
  class="bg-error rounded-md px-2 py-0.5 text-[11px] font-semibold text-white"
658
691
  >
659
- {{ t("dms.form.image.upload_failed") }}
692
+ {{ t(failureLabelKey(item)!) }}
660
693
  </span>
661
694
  <UButton
662
695
  v-if="item.file"
@@ -794,13 +827,13 @@ onBeforeUnmount(() => {
794
827
  </div>
795
828
 
796
829
  <div
797
- v-else-if="singleItem.status === 'error'"
830
+ v-else-if="failureLabelKey(singleItem)"
798
831
  class="bg-error/15 absolute inset-0 z-[5] flex flex-col items-center justify-center gap-2 p-2 text-center backdrop-blur-[1px]"
799
832
  >
800
833
  <span
801
834
  class="bg-error rounded-md px-2 py-0.5 text-[11px] font-semibold text-white"
802
835
  >
803
- {{ t("dms.form.image.upload_failed") }}
836
+ {{ t(failureLabelKey(singleItem)!) }}
804
837
  </span>
805
838
  <UButton
806
839
  v-if="singleItem.file"
@@ -835,21 +868,21 @@ onBeforeUnmount(() => {
835
868
  t("dms.form.image.uploading", { progress: singleItem.progress })
836
869
  }}
837
870
  </template>
838
- <template v-else-if="singleItem.status === 'error'">
839
- {{ t("dms.form.image.upload_failed") }}
871
+ <template v-else-if="failureLabelKey(singleItem)">
872
+ {{ t(failureLabelKey(singleItem)!) }}
840
873
  </template>
841
874
  <template v-else>{{ itemMeta(singleItem) }}</template>
842
875
  </div>
843
876
 
844
877
  <div class="mt-3">
845
878
  <label
846
- :for="`${singleItem.id}-alt`"
879
+ :for="singleAltInputId"
847
880
  class="text-muted mb-1.5 block text-xs font-semibold"
848
881
  >
849
882
  {{ t("dms.form.image.alt_label") }}
850
883
  </label>
851
884
  <UInput
852
- :id="`${singleItem.id}-alt`"
885
+ :id="singleAltInputId"
853
886
  v-model="singleItem.alt"
854
887
  :placeholder="t('dms.form.image.alt_placeholder')"
855
888
  :disabled="disabled || singleItem.status !== 'done'"
@@ -903,13 +936,13 @@ onBeforeUnmount(() => {
903
936
 
904
937
  <div class="flex flex-col gap-2 p-4">
905
938
  <label
906
- :for="`${detailItem.id}-alt-detail`"
939
+ :for="detailAltInputId"
907
940
  class="text-muted text-xs font-semibold"
908
941
  >
909
942
  {{ t("dms.form.image.alt_label") }}
910
943
  </label>
911
944
  <UTextarea
912
- :id="`${detailItem.id}-alt-detail`"
945
+ :id="detailAltInputId"
913
946
  v-model="detailAltDraft"
914
947
  :placeholder="t('dms.form.image.alt_placeholder')"
915
948
  :rows="3"
@@ -31,7 +31,7 @@ interface RelationProps extends SelectMenuProps<T, "value", M> {
31
31
  disabled?: string;
32
32
  };
33
33
  addForm?: { componentName: string; options?: FormProps };
34
- addPermissionId?: string;
34
+ addPermissionIds?: string[];
35
35
  }
36
36
 
37
37
  const props = withDefaults(defineProps<RelationProps>(), {
@@ -48,7 +48,7 @@ const forwardedProps = computed(() => {
48
48
  deselectable: ____,
49
49
  modelValue: _____,
50
50
  addForm: ______,
51
- addPermissionId: _______,
51
+ addPermissionIds: _______,
52
52
  ...rest
53
53
  } = props;
54
54
  return rest as SelectMenuProps<T, "value", M>;
@@ -110,7 +110,8 @@ const canAdd = computed(
110
110
  () =>
111
111
  !!props.addForm &&
112
112
  !!props.addForm.options?.submitUrl &&
113
- (!props.addPermissionId || hasPermission(props.addPermissionId)),
113
+ (!props.addPermissionIds?.length ||
114
+ props.addPermissionIds.some((id) => hasPermission(id))),
114
115
  );
115
116
 
116
117
  interface RelationResponse {
@@ -7,6 +7,47 @@ export interface PresignResponse {
7
7
  const UPLOAD_SUCCESS_MIN = 200;
8
8
  const UPLOAD_SUCCESS_MAX = 300;
9
9
 
10
+ /**
11
+ * Request headers a browser refuses to let script set (Fetch standard,
12
+ * "forbidden request-header"). A presign may still list them -- the storage
13
+ * signs `Content-Length` -- and the browser sends them itself, so copying them
14
+ * only logs `Refused to set unsafe header` on every upload.
15
+ */
16
+ const FORBIDDEN_REQUEST_HEADERS = new Set([
17
+ "accept-charset",
18
+ "accept-encoding",
19
+ "access-control-request-headers",
20
+ "access-control-request-method",
21
+ "connection",
22
+ "content-length",
23
+ "cookie",
24
+ "cookie2",
25
+ "date",
26
+ "dnt",
27
+ "expect",
28
+ "host",
29
+ "keep-alive",
30
+ "origin",
31
+ "referer",
32
+ "set-cookie",
33
+ "te",
34
+ "trailer",
35
+ "transfer-encoding",
36
+ "upgrade",
37
+ "via",
38
+ ]);
39
+ const FORBIDDEN_REQUEST_HEADER_PREFIXES = ["proxy-", "sec-"];
40
+
41
+ const isForbiddenRequestHeader = (name: string): boolean => {
42
+ const normalized = name.trim().toLowerCase();
43
+ return (
44
+ FORBIDDEN_REQUEST_HEADERS.has(normalized) ||
45
+ FORBIDDEN_REQUEST_HEADER_PREFIXES.some((prefix) =>
46
+ normalized.startsWith(prefix),
47
+ )
48
+ );
49
+ };
50
+
10
51
  /**
11
52
  * Uploads a file to the presigned storage URL with a PUT request, reporting
12
53
  * real upload progress and rejecting on a non-2xx response (a plain `fetch`
@@ -21,9 +62,9 @@ export function useUploadWithProgress() {
21
62
  new Promise((resolve, reject) => {
22
63
  const xhr = new XMLHttpRequest();
23
64
  xhr.open("PUT", presign.uploadUrl);
24
- Object.entries(presign.headers ?? {}).forEach(([name, value]) =>
25
- xhr.setRequestHeader(name, value),
26
- );
65
+ Object.entries(presign.headers ?? {})
66
+ .filter(([name]) => !isForbiddenRequestHeader(name))
67
+ .forEach(([name, value]) => xhr.setRequestHeader(name, value));
27
68
  xhr.upload.onprogress = (event) => {
28
69
  if (!event.lengthComputable) return;
29
70
  onProgress(Math.round((event.loaded / event.total) * 100));
@@ -0,0 +1,47 @@
1
+ // Retry policy for reading a stored file's metadata. A refused or missing file
2
+ // will not come back by asking again, so only failures that can heal on their
3
+ // own (network, 5xx, rate limiting) are retried, and with a capped backoff.
4
+
5
+ interface FetchFailure {
6
+ statusCode?: number;
7
+ status?: number;
8
+ }
9
+
10
+ const RETRY_BASE_DELAY_MS = 1000;
11
+ const RETRY_MAX_DELAY_MS = 30000;
12
+ const CLIENT_ERROR_STATUS_MIN = 400;
13
+ const CLIENT_ERROR_STATUS_MAX = 499;
14
+ const TRANSIENT_CLIENT_STATUSES = new Set([408, 425, 429]);
15
+
16
+ function failureStatus(error: unknown): number | undefined {
17
+ if (!error || typeof error !== "object") return undefined;
18
+ const { statusCode, status } = error as FetchFailure;
19
+ return statusCode ?? status;
20
+ }
21
+
22
+ /**
23
+ * A 4xx (404 gone, 403 denied, 410…) is the server's final answer about this
24
+ * file; request timeouts and rate limiting are the exceptions that can heal.
25
+ */
26
+ export function isDefinitiveMetadataFailure(error: unknown): boolean {
27
+ const status = failureStatus(error);
28
+ return (
29
+ status !== undefined &&
30
+ status >= CLIENT_ERROR_STATUS_MIN &&
31
+ status <= CLIENT_ERROR_STATUS_MAX &&
32
+ !TRANSIENT_CLIENT_STATUSES.has(status)
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Delay before retrying after `failedAttempts` consecutive failures, or `null`
38
+ * when the failure is definitive and the read must not be retried.
39
+ */
40
+ export function resolveMetadataRetryDelay(
41
+ error: unknown,
42
+ failedAttempts: number,
43
+ ): number | null {
44
+ if (isDefinitiveMetadataFailure(error)) return null;
45
+ const exponent = Math.max(failedAttempts - 1, 0);
46
+ return Math.min(RETRY_BASE_DELAY_MS * 2 ** exponent, RETRY_MAX_DELAY_MS);
47
+ }
@@ -223,7 +223,8 @@
223
223
  "uploaded_files": "Uploaded files",
224
224
  "rejected_title": "File not added",
225
225
  "rejected_size": "{name} exceeds the maximum size ({size})",
226
- "rejected_type": "{name} is not an allowed file type"
226
+ "rejected_type": "{name} is not an allowed file type",
227
+ "file_unavailable": "File unavailable"
227
228
  },
228
229
  "image": {
229
230
  "add": "Add",
@@ -233,6 +234,7 @@
233
234
  "single_hint": "Single image",
234
235
  "formats_any": "Any image format",
235
236
  "upload_failed": "Upload failed",
237
+ "file_unavailable": "File unavailable",
236
238
  "retry": "Retry",
237
239
  "replace": "Replace",
238
240
  "delete": "Delete",
@@ -224,7 +224,8 @@
224
224
  "uploaded_files": "Fichiers téléversés",
225
225
  "rejected_title": "Fichier non ajouté",
226
226
  "rejected_size": "{name} dépasse la taille maximale ({size})",
227
- "rejected_type": "{name} n'est pas un type de fichier autorisé"
227
+ "rejected_type": "{name} n'est pas un type de fichier autorisé",
228
+ "file_unavailable": "Fichier indisponible"
228
229
  },
229
230
  "image": {
230
231
  "add": "Ajouter",
@@ -234,6 +235,7 @@
234
235
  "single_hint": "Une seule image",
235
236
  "formats_any": "Tous formats d'image",
236
237
  "upload_failed": "Échec de l'envoi",
238
+ "file_unavailable": "Fichier indisponible",
237
239
  "retry": "Réessayer",
238
240
  "replace": "Remplacer",
239
241
  "delete": "Supprimer",
@@ -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
+ });