@strapi/upload 5.36.0 → 5.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/admin/future/App.js +5 -12
  2. package/dist/admin/future/App.js.map +1 -1
  3. package/dist/admin/future/App.mjs +5 -12
  4. package/dist/admin/future/App.mjs.map +1 -1
  5. package/dist/admin/future/components/UploadProgressDialog.js +494 -0
  6. package/dist/admin/future/components/UploadProgressDialog.js.map +1 -0
  7. package/dist/admin/future/components/UploadProgressDialog.mjs +473 -0
  8. package/dist/admin/future/components/UploadProgressDialog.mjs.map +1 -0
  9. package/dist/admin/future/pages/Assets/AssetsPage.js +183 -181
  10. package/dist/admin/future/pages/Assets/AssetsPage.js.map +1 -1
  11. package/dist/admin/future/pages/Assets/AssetsPage.mjs +190 -188
  12. package/dist/admin/future/pages/Assets/AssetsPage.mjs.map +1 -1
  13. package/dist/admin/future/pages/Assets/components/AssetsGrid.js +95 -13
  14. package/dist/admin/future/pages/Assets/components/AssetsGrid.js.map +1 -1
  15. package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs +97 -15
  16. package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs.map +1 -1
  17. package/dist/admin/future/pages/Assets/components/AssetsTable.js +99 -6
  18. package/dist/admin/future/pages/Assets/components/AssetsTable.js.map +1 -1
  19. package/dist/admin/future/pages/Assets/components/AssetsTable.mjs +100 -7
  20. package/dist/admin/future/pages/Assets/components/AssetsTable.mjs.map +1 -1
  21. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZone.js +127 -0
  22. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZone.js.map +1 -0
  23. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZone.mjs +105 -0
  24. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZone.mjs.map +1 -0
  25. package/dist/admin/future/pages/Assets/hooks/useFolderInfo.js +50 -0
  26. package/dist/admin/future/pages/Assets/hooks/useFolderInfo.js.map +1 -0
  27. package/dist/admin/future/pages/Assets/hooks/useFolderInfo.mjs +48 -0
  28. package/dist/admin/future/pages/Assets/hooks/useFolderInfo.mjs.map +1 -0
  29. package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.js +20 -0
  30. package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.js.map +1 -0
  31. package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.mjs +18 -0
  32. package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.mjs.map +1 -0
  33. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js +77 -0
  34. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js.map +1 -0
  35. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs +74 -0
  36. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs.map +1 -0
  37. package/dist/admin/future/services/api.js +419 -9
  38. package/dist/admin/future/services/api.js.map +1 -1
  39. package/dist/admin/future/services/api.mjs +417 -9
  40. package/dist/admin/future/services/api.mjs.map +1 -1
  41. package/dist/admin/future/services/assets.js +32 -3
  42. package/dist/admin/future/services/assets.js.map +1 -1
  43. package/dist/admin/future/services/assets.mjs +32 -3
  44. package/dist/admin/future/services/assets.mjs.map +1 -1
  45. package/dist/admin/future/services/folders.js +101 -0
  46. package/dist/admin/future/services/folders.js.map +1 -0
  47. package/dist/admin/future/services/folders.mjs +98 -0
  48. package/dist/admin/future/services/folders.mjs.map +1 -0
  49. package/dist/admin/future/store/hooks.js +10 -0
  50. package/dist/admin/future/store/hooks.js.map +1 -0
  51. package/dist/admin/future/store/hooks.mjs +7 -0
  52. package/dist/admin/future/store/hooks.mjs.map +1 -0
  53. package/dist/admin/future/store/uploadProgress.js +156 -0
  54. package/dist/admin/future/store/uploadProgress.js.map +1 -0
  55. package/dist/admin/future/store/uploadProgress.mjs +143 -0
  56. package/dist/admin/future/store/uploadProgress.mjs.map +1 -0
  57. package/dist/admin/index.js +11 -0
  58. package/dist/admin/index.js.map +1 -1
  59. package/dist/admin/index.mjs +11 -0
  60. package/dist/admin/index.mjs.map +1 -1
  61. package/dist/admin/package.json.js +11 -10
  62. package/dist/admin/package.json.js.map +1 -1
  63. package/dist/admin/package.json.mjs +11 -10
  64. package/dist/admin/package.json.mjs.map +1 -1
  65. package/dist/admin/src/future/components/UploadProgressDialog.d.ts +1 -0
  66. package/dist/admin/src/future/pages/Assets/components/AssetsGrid.d.ts +3 -1
  67. package/dist/admin/src/future/pages/Assets/components/AssetsTable.d.ts +3 -1
  68. package/dist/admin/src/future/pages/Assets/components/DropZone/UploadDropZone.d.ts +10 -0
  69. package/dist/admin/src/future/pages/Assets/hooks/useFolderInfo.d.ts +5 -0
  70. package/dist/admin/src/future/pages/Assets/hooks/useFolderNavigation.d.ts +5 -0
  71. package/dist/admin/src/future/pages/Assets/hooks/useInfiniteAssets.d.ts +17 -0
  72. package/dist/admin/src/future/services/api.d.ts +21 -3
  73. package/dist/admin/src/future/services/folders.d.ts +16 -0
  74. package/dist/admin/src/future/store/hooks.d.ts +6 -0
  75. package/dist/admin/src/future/store/uploadProgress.d.ts +46 -0
  76. package/dist/admin/translations/en.json.js +24 -0
  77. package/dist/admin/translations/en.json.js.map +1 -1
  78. package/dist/admin/translations/en.json.mjs +24 -0
  79. package/dist/admin/translations/en.json.mjs.map +1 -1
  80. package/dist/server/controllers/admin-upload.js +151 -2
  81. package/dist/server/controllers/admin-upload.js.map +1 -1
  82. package/dist/server/controllers/admin-upload.mjs +151 -2
  83. package/dist/server/controllers/admin-upload.mjs.map +1 -1
  84. package/dist/server/controllers/content-api.js +14 -6
  85. package/dist/server/controllers/content-api.js.map +1 -1
  86. package/dist/server/controllers/content-api.mjs +15 -7
  87. package/dist/server/controllers/content-api.mjs.map +1 -1
  88. package/dist/server/routes/admin.js +10 -0
  89. package/dist/server/routes/admin.js.map +1 -1
  90. package/dist/server/routes/admin.mjs +10 -0
  91. package/dist/server/routes/admin.mjs.map +1 -1
  92. package/dist/server/src/controllers/admin-upload.d.ts +12 -0
  93. package/dist/server/src/controllers/admin-upload.d.ts.map +1 -1
  94. package/dist/server/src/controllers/content-api.d.ts.map +1 -1
  95. package/dist/server/src/controllers/index.d.ts +1 -0
  96. package/dist/server/src/controllers/index.d.ts.map +1 -1
  97. package/dist/server/src/index.d.ts +1 -0
  98. package/dist/server/src/index.d.ts.map +1 -1
  99. package/dist/server/src/routes/admin.d.ts.map +1 -1
  100. package/dist/server/src/utils/mime-validation.d.ts +5 -0
  101. package/dist/server/src/utils/mime-validation.d.ts.map +1 -1
  102. package/dist/server/utils/mime-validation.js +7 -4
  103. package/dist/server/utils/mime-validation.js.map +1 -1
  104. package/dist/server/utils/mime-validation.mjs +7 -4
  105. package/dist/server/utils/mime-validation.mjs.map +1 -1
  106. package/dist/shared/contracts/files.d.ts +52 -0
  107. package/dist/shared/contracts/files.d.ts.map +1 -0
  108. package/dist/shared/contracts/folders.d.ts +2 -0
  109. package/package.json +11 -10
  110. package/dist/admin/future/pages/AIGenerationPage.js +0 -24
  111. package/dist/admin/future/pages/AIGenerationPage.js.map +0 -1
  112. package/dist/admin/future/pages/AIGenerationPage.mjs +0 -22
  113. package/dist/admin/future/pages/AIGenerationPage.mjs.map +0 -1
  114. package/dist/admin/future/pages/Assets/components/DropZone/DropZoneWithOverlay.js +0 -33
  115. package/dist/admin/future/pages/Assets/components/DropZone/DropZoneWithOverlay.js.map +0 -1
  116. package/dist/admin/future/pages/Assets/components/DropZone/DropZoneWithOverlay.mjs +0 -31
  117. package/dist/admin/future/pages/Assets/components/DropZone/DropZoneWithOverlay.mjs.map +0 -1
  118. package/dist/admin/src/future/pages/AIGenerationPage.d.ts +0 -1
  119. package/dist/admin/src/future/pages/Assets/components/DropZone/DropZoneWithOverlay.d.ts +0 -4
@@ -0,0 +1,18 @@
1
+ import { useQueryParams } from '@strapi/admin/strapi-admin';
2
+
3
+ const useFolderNavigation = ()=>{
4
+ const [{ query }, setQuery] = useQueryParams();
5
+ const currentFolderId = query?.folder ? Number(query.folder) : null;
6
+ const navigateToFolder = (folder)=>{
7
+ setQuery({
8
+ folder: String(folder.id)
9
+ });
10
+ };
11
+ return {
12
+ currentFolderId,
13
+ navigateToFolder
14
+ };
15
+ };
16
+
17
+ export { useFolderNavigation };
18
+ //# sourceMappingURL=useFolderNavigation.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFolderNavigation.mjs","sources":["../../../../../../admin/src/future/pages/Assets/hooks/useFolderNavigation.ts"],"sourcesContent":["import { useQueryParams } from '@strapi/admin/strapi-admin';\n\nimport type { Folder } from '../../../../../../shared/contracts/folders';\n\nexport const useFolderNavigation = () => {\n const [{ query }, setQuery] = useQueryParams<{ folder?: string }>();\n\n const currentFolderId = query?.folder ? Number(query.folder) : null;\n\n const navigateToFolder = (folder: Folder) => {\n setQuery({ folder: String(folder.id) });\n };\n\n return {\n currentFolderId,\n navigateToFolder,\n };\n};\n"],"names":["useFolderNavigation","query","setQuery","useQueryParams","currentFolderId","folder","Number","navigateToFolder","String","id"],"mappings":";;MAIaA,mBAAsB,GAAA,IAAA;AACjC,IAAA,MAAM,CAAC,EAAEC,KAAK,EAAE,EAAEC,SAAS,GAAGC,cAAAA,EAAAA;AAE9B,IAAA,MAAMC,kBAAkBH,KAAOI,EAAAA,MAAAA,GAASC,MAAOL,CAAAA,KAAAA,CAAMI,MAAM,CAAI,GAAA,IAAA;AAE/D,IAAA,MAAME,mBAAmB,CAACF,MAAAA,GAAAA;QACxBH,QAAS,CAAA;YAAEG,MAAQG,EAAAA,MAAAA,CAAOH,OAAOI,EAAE;AAAE,SAAA,CAAA;AACvC,KAAA;IAEA,OAAO;AACLL,QAAAA,eAAAA;AACAG,QAAAA;AACF,KAAA;AACF;;;;"}
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var assets = require('../../../services/assets.js');
5
+
6
+ const PAGE_SIZE = 20;
7
+ const useInfiniteAssets = ({ folder = null, sort } = {})=>{
8
+ const [page, setPage] = React.useState(1);
9
+ const lastResultsRef = React.useRef([]);
10
+ const isMountRef = React.useRef(true);
11
+ const { currentData: data, isLoading, isFetching, error } = assets.useGetAssetsQuery({
12
+ folder,
13
+ page,
14
+ pageSize: PAGE_SIZE,
15
+ sort
16
+ });
17
+ const pagination = data?.pagination;
18
+ // Accumulate pages. When cache is invalidated the current page is refetched
19
+ // detect this and reset to avoid a gap in the results.
20
+ const assets$1 = React.useMemo(()=>{
21
+ if (!data) {
22
+ return lastResultsRef.current;
23
+ }
24
+ const currentPageResults = data.results;
25
+ if (page === 1) {
26
+ lastResultsRef.current = currentPageResults;
27
+ } else {
28
+ // If accumulated length doesn't match expectation, cache was cleared
29
+ const expectedPrior = (page - 1) * PAGE_SIZE;
30
+ if (lastResultsRef.current.length < expectedPrior - PAGE_SIZE) {
31
+ return lastResultsRef.current;
32
+ }
33
+ // Only append if these aren't already accumulated
34
+ if (lastResultsRef.current.length < page * PAGE_SIZE) {
35
+ lastResultsRef.current = [
36
+ ...lastResultsRef.current,
37
+ ...currentPageResults
38
+ ];
39
+ }
40
+ }
41
+ return lastResultsRef.current;
42
+ }, [
43
+ data,
44
+ page
45
+ ]);
46
+ // Reset on filter/sort change — skip the initial mount since the memo
47
+ // already handles page 1 correctly
48
+ React.useEffect(()=>{
49
+ if (isMountRef.current) {
50
+ isMountRef.current = false;
51
+ return;
52
+ }
53
+ setPage(1);
54
+ lastResultsRef.current = [];
55
+ }, [
56
+ folder,
57
+ sort
58
+ ]);
59
+ const hasNextPage = pagination ? page < pagination.pageCount : false;
60
+ const isFetchingMore = isFetching && page > 1;
61
+ const fetchNextPage = React.useCallback(()=>{
62
+ setPage((prev)=>prev + 1);
63
+ }, []);
64
+ return {
65
+ assets: assets$1,
66
+ pagination,
67
+ isLoading,
68
+ isFetchingMore,
69
+ hasNextPage,
70
+ fetchNextPage,
71
+ error
72
+ };
73
+ };
74
+
75
+ exports.PAGE_SIZE = PAGE_SIZE;
76
+ exports.useInfiniteAssets = useInfiniteAssets;
77
+ //# sourceMappingURL=useInfiniteAssets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useInfiniteAssets.js","sources":["../../../../../../admin/src/future/pages/Assets/hooks/useInfiniteAssets.ts"],"sourcesContent":["import { useState, useCallback, useMemo, useEffect, useRef } from 'react';\n\nimport { useGetAssetsQuery } from '../../../services/assets';\n\nimport type { File } from '../../../../../../shared/contracts/files';\n\nconst PAGE_SIZE = 20;\n\ninterface UseInfiniteAssetsOptions {\n folder?: number | null;\n sort?: string;\n}\n\nconst useInfiniteAssets = ({ folder = null, sort }: UseInfiniteAssetsOptions = {}) => {\n const [page, setPage] = useState(1);\n const lastResultsRef = useRef<File[]>([]);\n const isMountRef = useRef(true);\n\n const {\n currentData: data,\n isLoading,\n isFetching,\n error,\n } = useGetAssetsQuery({\n folder,\n page,\n pageSize: PAGE_SIZE,\n sort,\n });\n\n const pagination = data?.pagination;\n\n // Accumulate pages. When cache is invalidated the current page is refetched\n // detect this and reset to avoid a gap in the results.\n const assets = useMemo(() => {\n if (!data) {\n return lastResultsRef.current;\n }\n\n const currentPageResults = data.results;\n\n if (page === 1) {\n lastResultsRef.current = currentPageResults;\n } else {\n // If accumulated length doesn't match expectation, cache was cleared\n const expectedPrior = (page - 1) * PAGE_SIZE;\n if (lastResultsRef.current.length < expectedPrior - PAGE_SIZE) {\n return lastResultsRef.current;\n }\n\n // Only append if these aren't already accumulated\n if (lastResultsRef.current.length < page * PAGE_SIZE) {\n lastResultsRef.current = [...lastResultsRef.current, ...currentPageResults];\n }\n }\n\n return lastResultsRef.current;\n }, [data, page]);\n\n // Reset on filter/sort change — skip the initial mount since the memo\n // already handles page 1 correctly\n useEffect(() => {\n if (isMountRef.current) {\n isMountRef.current = false;\n\n return;\n }\n setPage(1);\n lastResultsRef.current = [];\n }, [folder, sort]);\n\n const hasNextPage = pagination ? page < pagination.pageCount : false;\n const isFetchingMore = isFetching && page > 1;\n\n const fetchNextPage = useCallback(() => {\n setPage((prev) => prev + 1);\n }, []);\n\n return { assets, pagination, isLoading, isFetchingMore, hasNextPage, fetchNextPage, error };\n};\n\nexport { useInfiniteAssets };\nexport { PAGE_SIZE };\n"],"names":["PAGE_SIZE","useInfiniteAssets","folder","sort","page","setPage","useState","lastResultsRef","useRef","isMountRef","currentData","data","isLoading","isFetching","error","useGetAssetsQuery","pageSize","pagination","assets","useMemo","current","currentPageResults","results","expectedPrior","length","useEffect","hasNextPage","pageCount","isFetchingMore","fetchNextPage","useCallback","prev"],"mappings":";;;;;AAMA,MAAMA,SAAY,GAAA;AAOZC,MAAAA,iBAAAA,GAAoB,CAAC,EAAEC,MAAS,GAAA,IAAI,EAAEC,IAAI,EAA4B,GAAG,EAAE,GAAA;AAC/E,IAAA,MAAM,CAACC,IAAAA,EAAMC,OAAQ,CAAA,GAAGC,cAAS,CAAA,CAAA,CAAA;IACjC,MAAMC,cAAAA,GAAiBC,aAAe,EAAE,CAAA;AACxC,IAAA,MAAMC,aAAaD,YAAO,CAAA,IAAA,CAAA;IAE1B,MAAM,EACJE,WAAaC,EAAAA,IAAI,EACjBC,SAAS,EACTC,UAAU,EACVC,KAAK,EACN,GAAGC,wBAAkB,CAAA;AACpBb,QAAAA,MAAAA;AACAE,QAAAA,IAAAA;QACAY,QAAUhB,EAAAA,SAAAA;AACVG,QAAAA;AACF,KAAA,CAAA;AAEA,IAAA,MAAMc,aAAaN,IAAMM,EAAAA,UAAAA;;;AAIzB,IAAA,MAAMC,WAASC,aAAQ,CAAA,IAAA;AACrB,QAAA,IAAI,CAACR,IAAM,EAAA;AACT,YAAA,OAAOJ,eAAea,OAAO;AAC/B;QAEA,MAAMC,kBAAAA,GAAqBV,KAAKW,OAAO;AAEvC,QAAA,IAAIlB,SAAS,CAAG,EAAA;AACdG,YAAAA,cAAAA,CAAea,OAAO,GAAGC,kBAAAA;SACpB,MAAA;;AAEL,YAAA,MAAME,aAAgB,GAACnB,CAAAA,IAAAA,GAAO,CAAA,IAAKJ,SAAAA;AACnC,YAAA,IAAIO,eAAea,OAAO,CAACI,MAAM,GAAGD,gBAAgBvB,SAAW,EAAA;AAC7D,gBAAA,OAAOO,eAAea,OAAO;AAC/B;;AAGA,YAAA,IAAIb,eAAea,OAAO,CAACI,MAAM,GAAGpB,OAAOJ,SAAW,EAAA;AACpDO,gBAAAA,cAAAA,CAAea,OAAO,GAAG;AAAIb,oBAAAA,GAAAA,cAAAA,CAAea,OAAO;AAAKC,oBAAAA,GAAAA;AAAmB,iBAAA;AAC7E;AACF;AAEA,QAAA,OAAOd,eAAea,OAAO;KAC5B,EAAA;AAACT,QAAAA,IAAAA;AAAMP,QAAAA;AAAK,KAAA,CAAA;;;IAIfqB,eAAU,CAAA,IAAA;QACR,IAAIhB,UAAAA,CAAWW,OAAO,EAAE;AACtBX,YAAAA,UAAAA,CAAWW,OAAO,GAAG,KAAA;AAErB,YAAA;AACF;QACAf,OAAQ,CAAA,CAAA,CAAA;QACRE,cAAea,CAAAA,OAAO,GAAG,EAAE;KAC1B,EAAA;AAAClB,QAAAA,MAAAA;AAAQC,QAAAA;AAAK,KAAA,CAAA;AAEjB,IAAA,MAAMuB,WAAcT,GAAAA,UAAAA,GAAab,IAAOa,GAAAA,UAAAA,CAAWU,SAAS,GAAG,KAAA;IAC/D,MAAMC,cAAAA,GAAiBf,cAAcT,IAAO,GAAA,CAAA;AAE5C,IAAA,MAAMyB,gBAAgBC,iBAAY,CAAA,IAAA;QAChCzB,OAAQ,CAAA,CAAC0B,OAASA,IAAO,GAAA,CAAA,CAAA;AAC3B,KAAA,EAAG,EAAE,CAAA;IAEL,OAAO;AAAEb,gBAAAA,QAAAA;AAAQD,QAAAA,UAAAA;AAAYL,QAAAA,SAAAA;AAAWgB,QAAAA,cAAAA;AAAgBF,QAAAA,WAAAA;AAAaG,QAAAA,aAAAA;AAAef,QAAAA;AAAM,KAAA;AAC5F;;;;;"}
@@ -0,0 +1,74 @@
1
+ import { useState, useRef, useMemo, useEffect, useCallback } from 'react';
2
+ import { useGetAssetsQuery } from '../../../services/assets.mjs';
3
+
4
+ const PAGE_SIZE = 20;
5
+ const useInfiniteAssets = ({ folder = null, sort } = {})=>{
6
+ const [page, setPage] = useState(1);
7
+ const lastResultsRef = useRef([]);
8
+ const isMountRef = useRef(true);
9
+ const { currentData: data, isLoading, isFetching, error } = useGetAssetsQuery({
10
+ folder,
11
+ page,
12
+ pageSize: PAGE_SIZE,
13
+ sort
14
+ });
15
+ const pagination = data?.pagination;
16
+ // Accumulate pages. When cache is invalidated the current page is refetched
17
+ // detect this and reset to avoid a gap in the results.
18
+ const assets = useMemo(()=>{
19
+ if (!data) {
20
+ return lastResultsRef.current;
21
+ }
22
+ const currentPageResults = data.results;
23
+ if (page === 1) {
24
+ lastResultsRef.current = currentPageResults;
25
+ } else {
26
+ // If accumulated length doesn't match expectation, cache was cleared
27
+ const expectedPrior = (page - 1) * PAGE_SIZE;
28
+ if (lastResultsRef.current.length < expectedPrior - PAGE_SIZE) {
29
+ return lastResultsRef.current;
30
+ }
31
+ // Only append if these aren't already accumulated
32
+ if (lastResultsRef.current.length < page * PAGE_SIZE) {
33
+ lastResultsRef.current = [
34
+ ...lastResultsRef.current,
35
+ ...currentPageResults
36
+ ];
37
+ }
38
+ }
39
+ return lastResultsRef.current;
40
+ }, [
41
+ data,
42
+ page
43
+ ]);
44
+ // Reset on filter/sort change — skip the initial mount since the memo
45
+ // already handles page 1 correctly
46
+ useEffect(()=>{
47
+ if (isMountRef.current) {
48
+ isMountRef.current = false;
49
+ return;
50
+ }
51
+ setPage(1);
52
+ lastResultsRef.current = [];
53
+ }, [
54
+ folder,
55
+ sort
56
+ ]);
57
+ const hasNextPage = pagination ? page < pagination.pageCount : false;
58
+ const isFetchingMore = isFetching && page > 1;
59
+ const fetchNextPage = useCallback(()=>{
60
+ setPage((prev)=>prev + 1);
61
+ }, []);
62
+ return {
63
+ assets,
64
+ pagination,
65
+ isLoading,
66
+ isFetchingMore,
67
+ hasNextPage,
68
+ fetchNextPage,
69
+ error
70
+ };
71
+ };
72
+
73
+ export { PAGE_SIZE, useInfiniteAssets };
74
+ //# sourceMappingURL=useInfiniteAssets.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useInfiniteAssets.mjs","sources":["../../../../../../admin/src/future/pages/Assets/hooks/useInfiniteAssets.ts"],"sourcesContent":["import { useState, useCallback, useMemo, useEffect, useRef } from 'react';\n\nimport { useGetAssetsQuery } from '../../../services/assets';\n\nimport type { File } from '../../../../../../shared/contracts/files';\n\nconst PAGE_SIZE = 20;\n\ninterface UseInfiniteAssetsOptions {\n folder?: number | null;\n sort?: string;\n}\n\nconst useInfiniteAssets = ({ folder = null, sort }: UseInfiniteAssetsOptions = {}) => {\n const [page, setPage] = useState(1);\n const lastResultsRef = useRef<File[]>([]);\n const isMountRef = useRef(true);\n\n const {\n currentData: data,\n isLoading,\n isFetching,\n error,\n } = useGetAssetsQuery({\n folder,\n page,\n pageSize: PAGE_SIZE,\n sort,\n });\n\n const pagination = data?.pagination;\n\n // Accumulate pages. When cache is invalidated the current page is refetched\n // detect this and reset to avoid a gap in the results.\n const assets = useMemo(() => {\n if (!data) {\n return lastResultsRef.current;\n }\n\n const currentPageResults = data.results;\n\n if (page === 1) {\n lastResultsRef.current = currentPageResults;\n } else {\n // If accumulated length doesn't match expectation, cache was cleared\n const expectedPrior = (page - 1) * PAGE_SIZE;\n if (lastResultsRef.current.length < expectedPrior - PAGE_SIZE) {\n return lastResultsRef.current;\n }\n\n // Only append if these aren't already accumulated\n if (lastResultsRef.current.length < page * PAGE_SIZE) {\n lastResultsRef.current = [...lastResultsRef.current, ...currentPageResults];\n }\n }\n\n return lastResultsRef.current;\n }, [data, page]);\n\n // Reset on filter/sort change — skip the initial mount since the memo\n // already handles page 1 correctly\n useEffect(() => {\n if (isMountRef.current) {\n isMountRef.current = false;\n\n return;\n }\n setPage(1);\n lastResultsRef.current = [];\n }, [folder, sort]);\n\n const hasNextPage = pagination ? page < pagination.pageCount : false;\n const isFetchingMore = isFetching && page > 1;\n\n const fetchNextPage = useCallback(() => {\n setPage((prev) => prev + 1);\n }, []);\n\n return { assets, pagination, isLoading, isFetchingMore, hasNextPage, fetchNextPage, error };\n};\n\nexport { useInfiniteAssets };\nexport { PAGE_SIZE };\n"],"names":["PAGE_SIZE","useInfiniteAssets","folder","sort","page","setPage","useState","lastResultsRef","useRef","isMountRef","currentData","data","isLoading","isFetching","error","useGetAssetsQuery","pageSize","pagination","assets","useMemo","current","currentPageResults","results","expectedPrior","length","useEffect","hasNextPage","pageCount","isFetchingMore","fetchNextPage","useCallback","prev"],"mappings":";;;AAMA,MAAMA,SAAY,GAAA;AAOZC,MAAAA,iBAAAA,GAAoB,CAAC,EAAEC,MAAS,GAAA,IAAI,EAAEC,IAAI,EAA4B,GAAG,EAAE,GAAA;AAC/E,IAAA,MAAM,CAACC,IAAAA,EAAMC,OAAQ,CAAA,GAAGC,QAAS,CAAA,CAAA,CAAA;IACjC,MAAMC,cAAAA,GAAiBC,OAAe,EAAE,CAAA;AACxC,IAAA,MAAMC,aAAaD,MAAO,CAAA,IAAA,CAAA;IAE1B,MAAM,EACJE,WAAaC,EAAAA,IAAI,EACjBC,SAAS,EACTC,UAAU,EACVC,KAAK,EACN,GAAGC,iBAAkB,CAAA;AACpBb,QAAAA,MAAAA;AACAE,QAAAA,IAAAA;QACAY,QAAUhB,EAAAA,SAAAA;AACVG,QAAAA;AACF,KAAA,CAAA;AAEA,IAAA,MAAMc,aAAaN,IAAMM,EAAAA,UAAAA;;;AAIzB,IAAA,MAAMC,SAASC,OAAQ,CAAA,IAAA;AACrB,QAAA,IAAI,CAACR,IAAM,EAAA;AACT,YAAA,OAAOJ,eAAea,OAAO;AAC/B;QAEA,MAAMC,kBAAAA,GAAqBV,KAAKW,OAAO;AAEvC,QAAA,IAAIlB,SAAS,CAAG,EAAA;AACdG,YAAAA,cAAAA,CAAea,OAAO,GAAGC,kBAAAA;SACpB,MAAA;;AAEL,YAAA,MAAME,aAAgB,GAACnB,CAAAA,IAAAA,GAAO,CAAA,IAAKJ,SAAAA;AACnC,YAAA,IAAIO,eAAea,OAAO,CAACI,MAAM,GAAGD,gBAAgBvB,SAAW,EAAA;AAC7D,gBAAA,OAAOO,eAAea,OAAO;AAC/B;;AAGA,YAAA,IAAIb,eAAea,OAAO,CAACI,MAAM,GAAGpB,OAAOJ,SAAW,EAAA;AACpDO,gBAAAA,cAAAA,CAAea,OAAO,GAAG;AAAIb,oBAAAA,GAAAA,cAAAA,CAAea,OAAO;AAAKC,oBAAAA,GAAAA;AAAmB,iBAAA;AAC7E;AACF;AAEA,QAAA,OAAOd,eAAea,OAAO;KAC5B,EAAA;AAACT,QAAAA,IAAAA;AAAMP,QAAAA;AAAK,KAAA,CAAA;;;IAIfqB,SAAU,CAAA,IAAA;QACR,IAAIhB,UAAAA,CAAWW,OAAO,EAAE;AACtBX,YAAAA,UAAAA,CAAWW,OAAO,GAAG,KAAA;AAErB,YAAA;AACF;QACAf,OAAQ,CAAA,CAAA,CAAA;QACRE,cAAea,CAAAA,OAAO,GAAG,EAAE;KAC1B,EAAA;AAAClB,QAAAA,MAAAA;AAAQC,QAAAA;AAAK,KAAA,CAAA;AAEjB,IAAA,MAAMuB,WAAcT,GAAAA,UAAAA,GAAab,IAAOa,GAAAA,UAAAA,CAAWU,SAAS,GAAG,KAAA;IAC/D,MAAMC,cAAAA,GAAiBf,cAAcT,IAAO,GAAA,CAAA;AAE5C,IAAA,MAAMyB,gBAAgBC,WAAY,CAAA,IAAA;QAChCzB,OAAQ,CAAA,CAAC0B,OAASA,IAAO,GAAA,CAAA,CAAA;AAC3B,KAAA,EAAG,EAAE,CAAA;IAEL,OAAO;AAAEb,QAAAA,MAAAA;AAAQD,QAAAA,UAAAA;AAAYL,QAAAA,SAAAA;AAAWgB,QAAAA,cAAAA;AAAgBF,QAAAA,WAAAA;AAAaG,QAAAA,aAAAA;AAAef,QAAAA;AAAM,KAAA;AAC5F;;;;"}
@@ -1,7 +1,189 @@
1
1
  'use strict';
2
2
 
3
3
  var strapiAdmin = require('@strapi/admin/strapi-admin');
4
+ var uploadProgress = require('../store/uploadProgress.js');
4
5
 
6
+ /**
7
+ * Stores original File objects for retry functionality.
8
+ *
9
+ * Similar to abortControllers, File objects cannot be stored in Redux state
10
+ * (they are not serializable). This Map allows us to retry cancelled uploads
11
+ * by retrieving the original files using the uploadId.
12
+ */ const uploadedFiles = new Map();
13
+ /**
14
+ * Registers files for a specific upload to enable retry.
15
+ */ const registerUploadedFiles = (uploadId, files)=>{
16
+ uploadedFiles.set(uploadId, files);
17
+ };
18
+ /**
19
+ * Retrieves stored files for an upload.
20
+ */ const getUploadedFiles = (uploadId)=>{
21
+ return uploadedFiles.get(uploadId);
22
+ };
23
+ /**
24
+ * Manages abort controllers for in-flight uploads.
25
+ *
26
+ * Design decision: Uses a Map to track uploads by their unique uploadId.
27
+ * This approach is necessary because:
28
+ * 1. Redux state cannot store function references (abort controllers)
29
+ * 2. RTK Query's signal is only accessible within the queryFn
30
+ * 3. The upload is triggered in AssetsPage but cancelled from UploadProgressDialog
31
+ *
32
+ * The uploadId ensures we abort the correct upload even if multiple uploads
33
+ * are queued, though the current UI prevents simultaneous uploads.
34
+ */ const abortControllers = new Map();
35
+ /**
36
+ * Registers an abort controller for a specific upload.
37
+ * Called internally when an upload starts.
38
+ */ const registerAbortController = (uploadId, controller)=>{
39
+ abortControllers.set(uploadId, controller);
40
+ };
41
+ /**
42
+ * Removes an abort controller when an upload completes or is aborted.
43
+ */ const unregisterAbortController = (uploadId)=>{
44
+ abortControllers.delete(uploadId);
45
+ };
46
+ /**
47
+ * Aborts an upload by its uploadId.
48
+ * Called from the UploadProgressDialog when the user clicks cancel or close.
49
+ */ const abortUpload = (uploadId)=>{
50
+ const controller = abortControllers.get(uploadId);
51
+ if (controller) {
52
+ controller.abort();
53
+ unregisterAbortController(uploadId);
54
+ }
55
+ };
56
+ /**
57
+ * Parses a raw SSE text chunk into event/data pairs.
58
+ *
59
+ * SSE format:
60
+ * event: <eventName>\n
61
+ * data: <json>\n
62
+ * \n
63
+ */ const parseSSEEvents = (chunk)=>{
64
+ const events = [];
65
+ const blocks = chunk.split('\n\n').filter(Boolean);
66
+ for (const block of blocks){
67
+ let event = '';
68
+ let data = '';
69
+ for (const line of block.split('\n')){
70
+ if (line.startsWith('event: ')) {
71
+ event = line.slice(7);
72
+ } else if (line.startsWith('data: ')) {
73
+ data = line.slice(6);
74
+ }
75
+ }
76
+ if (event && data) {
77
+ events.push({
78
+ event,
79
+ data
80
+ });
81
+ }
82
+ }
83
+ return events;
84
+ };
85
+ /**
86
+ * Makes a streaming upload request to the server.
87
+ *
88
+ * We use fetch directly instead of RTK Query's fetchBaseQuery because:
89
+ * 1. We need access to the raw Response to read the body as a stream
90
+ * 2. RTK Query's baseQuery awaits the full response and parses it as JSON,
91
+ * which doesn't work for Server-Sent Events (SSE) streaming
92
+ * 3. The stream must be read incrementally via response.body.getReader()
93
+ * to dispatch progress updates as files upload
94
+ */ const fetchUploadStream = async ({ token, formData, signal })=>{
95
+ const backendURL = window.strapi.backendURL;
96
+ const headers = {};
97
+ if (token) {
98
+ headers.Authorization = `Bearer ${token}`;
99
+ }
100
+ return fetch(`${backendURL}/upload/unstable/stream`, {
101
+ method: 'POST',
102
+ headers,
103
+ body: formData,
104
+ signal
105
+ });
106
+ };
107
+ /**
108
+ * Processes an SSE stream from the upload endpoint.
109
+ * Dispatches Redux actions for each file event and returns the final result.
110
+ *
111
+ * @param options.response - The fetch Response object with SSE body
112
+ * @param options.dispatch - Redux dispatch function
113
+ * @param options.indexMapper - Optional function to map server indices to state indices (for retry)
114
+ * @param options.logPrefix - Optional prefix for console logs
115
+ * @returns The stream result or null if no files completed
116
+ */ const processSSEStream = async ({ response, dispatch, indexMapper = (i)=>i })=>{
117
+ const reader = response.body.getReader();
118
+ const decoder = new TextDecoder();
119
+ let streamResult = null;
120
+ let buffer = '';
121
+ while(true){
122
+ const { done, value } = await reader.read();
123
+ if (done) {
124
+ break;
125
+ }
126
+ buffer += decoder.decode(value, {
127
+ stream: true
128
+ });
129
+ // Process complete SSE events from the buffer
130
+ const lastDoubleNewline = buffer.lastIndexOf('\n\n');
131
+ if (lastDoubleNewline === -1) {
132
+ continue;
133
+ }
134
+ const completePart = buffer.slice(0, lastDoubleNewline + 2);
135
+ buffer = buffer.slice(lastDoubleNewline + 2);
136
+ const events = parseSSEEvents(completePart);
137
+ for (const { event, data } of events){
138
+ const parsed = JSON.parse(data);
139
+ const mappedIndex = indexMapper(parsed.index);
140
+ switch(event){
141
+ case 'file:uploading':
142
+ {
143
+ const payload = parsed;
144
+ dispatch(uploadProgress.setFileUploading({
145
+ name: payload.name,
146
+ index: mappedIndex,
147
+ total: payload.total,
148
+ size: payload.size
149
+ }));
150
+ break;
151
+ }
152
+ case 'file:complete':
153
+ {
154
+ const payload = parsed;
155
+ dispatch(uploadProgress.setFileComplete({
156
+ index: mappedIndex,
157
+ file: payload.file
158
+ }));
159
+ break;
160
+ }
161
+ case 'file:error':
162
+ {
163
+ const payload = parsed;
164
+ dispatch(uploadProgress.setFileError({
165
+ index: mappedIndex,
166
+ name: payload.name,
167
+ message: payload.message
168
+ }));
169
+ break;
170
+ }
171
+ case 'stream:complete':
172
+ {
173
+ const payload = parsed;
174
+ streamResult = {
175
+ data: payload.data,
176
+ errors: payload.errors
177
+ };
178
+ break;
179
+ }
180
+ default:
181
+ console.error(`[SSE Upload] unknown event: ${event}`, parsed);
182
+ }
183
+ }
184
+ }
185
+ return streamResult;
186
+ };
5
187
  const uploadApi = strapiAdmin.adminApi.enhanceEndpoints({
6
188
  addTagTypes: [
7
189
  'Asset',
@@ -9,20 +191,248 @@ const uploadApi = strapiAdmin.adminApi.enhanceEndpoints({
9
191
  ]
10
192
  }).injectEndpoints({
11
193
  endpoints: (builder)=>({
12
- uploadFiles: builder.mutation({
13
- query: (formData)=>({
14
- url: '/upload',
15
- method: 'POST',
16
- data: formData
17
- }),
194
+ /**
195
+ * Stream upload files to the /upload/unstable/stream endpoint.
196
+ * Reads SSE stream for per-file progress updates.
197
+ */ uploadFilesStream: builder.mutation({
198
+ queryFn: async ({ formData, totalFiles }, { dispatch, getState })=>{
199
+ const token = getState().admin_app?.token;
200
+ // Extract file names and sizes from FormData
201
+ const files = formData.getAll('files');
202
+ const fileInfoJson = formData.get('fileInfo');
203
+ const fileInfo = JSON.parse(fileInfoJson);
204
+ const fileNames = fileInfo.map((info)=>info.name);
205
+ const fileSizes = files.map((file)=>file.size);
206
+ // Open the progress dialog and get the uploadId
207
+ dispatch(uploadProgress.openUploadProgress({
208
+ totalFiles,
209
+ fileNames,
210
+ fileSizes
211
+ }));
212
+ dispatch(uploadProgress.updateProgress(0));
213
+ // Get the uploadId from state after dispatching
214
+ const uploadId = getState().uploadProgress.uploadId;
215
+ // Store original files for retry functionality
216
+ registerUploadedFiles(uploadId, files);
217
+ // Create abort controller for this upload
218
+ const abortController = new AbortController();
219
+ registerAbortController(uploadId, abortController);
220
+ try {
221
+ const response = await fetchUploadStream({
222
+ token,
223
+ formData,
224
+ signal: abortController.signal
225
+ });
226
+ if (!response.ok || !response.body) {
227
+ unregisterAbortController(uploadId);
228
+ // Try to parse error message from response
229
+ let errorMessage = 'Upload request failed';
230
+ try {
231
+ const errorData = await response.json();
232
+ if (errorData.error?.message) {
233
+ errorMessage = errorData.error.message;
234
+ } else if (errorData.message) {
235
+ errorMessage = errorData.message;
236
+ }
237
+ } catch {
238
+ // If we can't parse the error, use a generic message with status code
239
+ errorMessage = `Upload failed with status ${response.status}`;
240
+ }
241
+ // Mark all files as failed in the UI
242
+ dispatch(uploadProgress.setUploadFailed({
243
+ message: errorMessage
244
+ }));
245
+ return {
246
+ error: {
247
+ name: 'UnknownError',
248
+ message: errorMessage,
249
+ status: response.status
250
+ }
251
+ };
252
+ }
253
+ const streamResult = await processSSEStream({
254
+ response,
255
+ dispatch
256
+ });
257
+ unregisterAbortController(uploadId);
258
+ if (streamResult && streamResult.data.length > 0) {
259
+ return {
260
+ data: streamResult
261
+ };
262
+ }
263
+ // If stream ended without completing any files, mark all as failed
264
+ const errorMessage = 'No files were uploaded successfully';
265
+ dispatch(uploadProgress.setUploadFailed({
266
+ message: errorMessage
267
+ }));
268
+ return {
269
+ error: {
270
+ name: 'UnknownError',
271
+ message: errorMessage
272
+ }
273
+ };
274
+ } catch (err) {
275
+ unregisterAbortController(uploadId);
276
+ if (err instanceof DOMException && err.name === 'AbortError') {
277
+ // Don't mark as failed for user-initiated cancellations
278
+ return {
279
+ error: {
280
+ name: 'UnknownError',
281
+ message: 'Upload cancelled'
282
+ }
283
+ };
284
+ }
285
+ // For network errors or other exceptions, mark all files as failed
286
+ const errorMessage = err instanceof Error ? err.message : 'Network error occurred';
287
+ dispatch(uploadProgress.setUploadFailed({
288
+ message: errorMessage
289
+ }));
290
+ return {
291
+ error: {
292
+ name: 'UnknownError',
293
+ message: errorMessage
294
+ }
295
+ };
296
+ }
297
+ },
18
298
  invalidatesTags: [
19
- 'Asset'
299
+ {
300
+ type: 'Asset',
301
+ id: 'LIST'
302
+ }
303
+ ]
304
+ }),
305
+ /**
306
+ * Retry uploading cancelled files.
307
+ * Retrieves original File objects and re-uploads only the cancelled ones.
308
+ */ retryCancelledFilesStream: builder.mutation({
309
+ queryFn: async (_, { dispatch, getState })=>{
310
+ const token = getState().admin_app?.token;
311
+ const { uploadId, files: stateFiles } = getState().uploadProgress;
312
+ // Get cancelled files with their original indices
313
+ const cancelledFiles = stateFiles.filter((f)=>f.status === 'cancelled');
314
+ if (cancelledFiles.length === 0) {
315
+ return {
316
+ error: {
317
+ name: 'UnknownError',
318
+ message: 'No cancelled files to retry'
319
+ }
320
+ };
321
+ }
322
+ // Get the original File objects
323
+ const originalFiles = getUploadedFiles(uploadId);
324
+ if (!originalFiles) {
325
+ return {
326
+ error: {
327
+ name: 'UnknownError',
328
+ message: 'Original files not found'
329
+ }
330
+ };
331
+ }
332
+ // Build mapping from new index to original index
333
+ const indexMapping = cancelledFiles.map((f)=>f.index);
334
+ const filesToRetry = cancelledFiles.map((f)=>originalFiles[f.index]);
335
+ // Reset cancelled files to pending
336
+ dispatch(uploadProgress.retryCancelledFiles());
337
+ // Build FormData for retry
338
+ const formData = new FormData();
339
+ const fileInfoArray = filesToRetry.map((file)=>({
340
+ name: file.name,
341
+ caption: null,
342
+ alternativeText: null,
343
+ folder: null
344
+ }));
345
+ filesToRetry.forEach((file)=>{
346
+ formData.append('files', file);
347
+ });
348
+ formData.append('fileInfo', JSON.stringify(fileInfoArray));
349
+ // Create abort controller for this retry
350
+ const abortController = new AbortController();
351
+ registerAbortController(uploadId, abortController);
352
+ try {
353
+ const response = await fetchUploadStream({
354
+ token,
355
+ formData,
356
+ signal: abortController.signal
357
+ });
358
+ if (!response.ok || !response.body) {
359
+ unregisterAbortController(uploadId);
360
+ let errorMessage = 'Retry request failed';
361
+ try {
362
+ const errorData = await response.json();
363
+ if (errorData.error?.message) {
364
+ errorMessage = errorData.error.message;
365
+ } else if (errorData.message) {
366
+ errorMessage = errorData.message;
367
+ }
368
+ } catch {
369
+ errorMessage = `Retry failed with status ${response.status}`;
370
+ }
371
+ // Mark retried files as failed
372
+ for (const originalIndex of indexMapping){
373
+ dispatch(uploadProgress.setFileError({
374
+ index: originalIndex,
375
+ name: stateFiles[originalIndex].name,
376
+ message: errorMessage
377
+ }));
378
+ }
379
+ return {
380
+ error: {
381
+ name: 'UnknownError',
382
+ message: errorMessage,
383
+ status: response.status
384
+ }
385
+ };
386
+ }
387
+ const streamResult = await processSSEStream({
388
+ response,
389
+ dispatch,
390
+ indexMapper: (serverIndex)=>indexMapping[serverIndex]
391
+ });
392
+ unregisterAbortController(uploadId);
393
+ if (streamResult && streamResult.data.length > 0) {
394
+ return {
395
+ data: streamResult
396
+ };
397
+ }
398
+ return {
399
+ error: {
400
+ name: 'UnknownError',
401
+ message: 'No files were uploaded successfully'
402
+ }
403
+ };
404
+ } catch (err) {
405
+ unregisterAbortController(uploadId);
406
+ if (err instanceof DOMException && err.name === 'AbortError') {
407
+ return {
408
+ error: {
409
+ name: 'UnknownError',
410
+ message: 'Retry cancelled'
411
+ }
412
+ };
413
+ }
414
+ const errorMessage = err instanceof Error ? err.message : 'Network error occurred';
415
+ return {
416
+ error: {
417
+ name: 'UnknownError',
418
+ message: errorMessage
419
+ }
420
+ };
421
+ }
422
+ },
423
+ invalidatesTags: [
424
+ {
425
+ type: 'Asset',
426
+ id: 'LIST'
427
+ }
20
428
  ]
21
429
  })
22
430
  })
23
431
  });
24
- const { useUploadFilesMutation } = uploadApi;
432
+ const { useUploadFilesStreamMutation, useRetryCancelledFilesStreamMutation } = uploadApi;
25
433
 
434
+ exports.abortUpload = abortUpload;
26
435
  exports.uploadApi = uploadApi;
27
- exports.useUploadFilesMutation = useUploadFilesMutation;
436
+ exports.useRetryCancelledFilesStreamMutation = useRetryCancelledFilesStreamMutation;
437
+ exports.useUploadFilesStreamMutation = useUploadFilesStreamMutation;
28
438
  //# sourceMappingURL=api.js.map