@strapi/upload 5.51.1 → 5.51.2

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 (262) hide show
  1. package/dist/admin/components/AssetCard/VideoPreview.js +1 -1
  2. package/dist/admin/components/AssetCard/VideoPreview.js.map +1 -1
  3. package/dist/admin/components/AssetCard/VideoPreview.mjs +1 -1
  4. package/dist/admin/components/AssetCard/VideoPreview.mjs.map +1 -1
  5. package/dist/admin/components/TableList/TableList.js +1 -1
  6. package/dist/admin/components/TableList/TableList.js.map +1 -1
  7. package/dist/admin/components/TableList/TableList.mjs +1 -1
  8. package/dist/admin/components/TableList/TableList.mjs.map +1 -1
  9. package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromUrlForm.js +21 -4
  10. package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromUrlForm.js.map +1 -1
  11. package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromUrlForm.mjs +21 -4
  12. package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromUrlForm.mjs.map +1 -1
  13. package/dist/admin/future/components/Drawer.js +3 -1
  14. package/dist/admin/future/components/Drawer.js.map +1 -1
  15. package/dist/admin/future/components/Drawer.mjs +3 -1
  16. package/dist/admin/future/components/Drawer.mjs.map +1 -1
  17. package/dist/admin/future/components/UploadProgressDialog.js +200 -27
  18. package/dist/admin/future/components/UploadProgressDialog.js.map +1 -1
  19. package/dist/admin/future/components/UploadProgressDialog.mjs +203 -30
  20. package/dist/admin/future/components/UploadProgressDialog.mjs.map +1 -1
  21. package/dist/admin/future/hooks/useMediaLibraryPermissions.js +35 -0
  22. package/dist/admin/future/hooks/useMediaLibraryPermissions.js.map +1 -0
  23. package/dist/admin/future/hooks/useMediaLibraryPermissions.mjs +33 -0
  24. package/dist/admin/future/hooks/useMediaLibraryPermissions.mjs.map +1 -0
  25. package/dist/admin/future/pages/Assets/AssetsPage.js +203 -78
  26. package/dist/admin/future/pages/Assets/AssetsPage.js.map +1 -1
  27. package/dist/admin/future/pages/Assets/AssetsPage.mjs +200 -75
  28. package/dist/admin/future/pages/Assets/AssetsPage.mjs.map +1 -1
  29. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js +75 -32
  30. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js.map +1 -1
  31. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs +75 -32
  32. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs.map +1 -1
  33. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js +46 -19
  34. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js.map +1 -1
  35. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs +47 -20
  36. package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs.map +1 -1
  37. package/dist/admin/future/pages/Assets/components/AssetsGrid.js +5 -2
  38. package/dist/admin/future/pages/Assets/components/AssetsGrid.js.map +1 -1
  39. package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs +5 -2
  40. package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs.map +1 -1
  41. package/dist/admin/future/pages/Assets/components/AssetsSearchInput.js +100 -0
  42. package/dist/admin/future/pages/Assets/components/AssetsSearchInput.js.map +1 -0
  43. package/dist/admin/future/pages/Assets/components/AssetsSearchInput.mjs +98 -0
  44. package/dist/admin/future/pages/Assets/components/AssetsSearchInput.mjs.map +1 -0
  45. package/dist/admin/future/pages/Assets/components/AssetsTable.js +10 -4
  46. package/dist/admin/future/pages/Assets/components/AssetsTable.js.map +1 -1
  47. package/dist/admin/future/pages/Assets/components/AssetsTable.mjs +10 -4
  48. package/dist/admin/future/pages/Assets/components/AssetsTable.mjs.map +1 -1
  49. package/dist/admin/future/pages/Assets/components/BulkActionsBar.js +140 -22
  50. package/dist/admin/future/pages/Assets/components/BulkActionsBar.js.map +1 -1
  51. package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs +142 -24
  52. package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs.map +1 -1
  53. package/dist/admin/future/pages/Assets/components/DateRangeCalendar.js +193 -0
  54. package/dist/admin/future/pages/Assets/components/DateRangeCalendar.js.map +1 -0
  55. package/dist/admin/future/pages/Assets/components/DateRangeCalendar.mjs +191 -0
  56. package/dist/admin/future/pages/Assets/components/DateRangeCalendar.mjs.map +1 -0
  57. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js +18 -1
  58. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js.map +1 -1
  59. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs +18 -1
  60. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs.map +1 -1
  61. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.js +13 -3
  62. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.js.map +1 -1
  63. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.mjs +13 -3
  64. package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.mjs.map +1 -1
  65. package/dist/admin/future/pages/Assets/components/EmptyState.js +62 -3
  66. package/dist/admin/future/pages/Assets/components/EmptyState.js.map +1 -1
  67. package/dist/admin/future/pages/Assets/components/EmptyState.mjs +62 -4
  68. package/dist/admin/future/pages/Assets/components/EmptyState.mjs.map +1 -1
  69. package/dist/admin/future/pages/Assets/components/FilterBadges.js +408 -0
  70. package/dist/admin/future/pages/Assets/components/FilterBadges.js.map +1 -0
  71. package/dist/admin/future/pages/Assets/components/FilterBadges.mjs +406 -0
  72. package/dist/admin/future/pages/Assets/components/FilterBadges.mjs.map +1 -0
  73. package/dist/admin/future/pages/Assets/components/FilterMenu.js +273 -0
  74. package/dist/admin/future/pages/Assets/components/FilterMenu.js.map +1 -0
  75. package/dist/admin/future/pages/Assets/components/FilterMenu.mjs +267 -0
  76. package/dist/admin/future/pages/Assets/components/FilterMenu.mjs.map +1 -0
  77. package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.js +76 -19
  78. package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.js.map +1 -1
  79. package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.mjs +78 -21
  80. package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.mjs.map +1 -1
  81. package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.js +33 -0
  82. package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.js.map +1 -0
  83. package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.mjs +31 -0
  84. package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.mjs.map +1 -0
  85. package/dist/admin/future/pages/Assets/components/SortMenu.js +21 -32
  86. package/dist/admin/future/pages/Assets/components/SortMenu.js.map +1 -1
  87. package/dist/admin/future/pages/Assets/components/SortMenu.mjs +22 -33
  88. package/dist/admin/future/pages/Assets/components/SortMenu.mjs.map +1 -1
  89. package/dist/admin/future/pages/Assets/hooks/useAssetSearch.js +40 -0
  90. package/dist/admin/future/pages/Assets/hooks/useAssetSearch.js.map +1 -0
  91. package/dist/admin/future/pages/Assets/hooks/useAssetSearch.mjs +38 -0
  92. package/dist/admin/future/pages/Assets/hooks/useAssetSearch.mjs.map +1 -0
  93. package/dist/admin/future/pages/Assets/hooks/useAssetSelection.js +21 -5
  94. package/dist/admin/future/pages/Assets/hooks/useAssetSelection.js.map +1 -1
  95. package/dist/admin/future/pages/Assets/hooks/useAssetSelection.mjs +21 -5
  96. package/dist/admin/future/pages/Assets/hooks/useAssetSelection.mjs.map +1 -1
  97. package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.js +33 -12
  98. package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.js.map +1 -1
  99. package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.mjs +33 -12
  100. package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.mjs.map +1 -1
  101. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js +40 -4
  102. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js.map +1 -1
  103. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs +40 -4
  104. package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs.map +1 -1
  105. package/dist/admin/future/pages/Assets/hooks/useListFilters.js +168 -0
  106. package/dist/admin/future/pages/Assets/hooks/useListFilters.js.map +1 -0
  107. package/dist/admin/future/pages/Assets/hooks/useListFilters.mjs +162 -0
  108. package/dist/admin/future/pages/Assets/hooks/useListFilters.mjs.map +1 -0
  109. package/dist/admin/future/pages/Assets/utils/buildAssetFilters.js +201 -0
  110. package/dist/admin/future/pages/Assets/utils/buildAssetFilters.js.map +1 -0
  111. package/dist/admin/future/pages/Assets/utils/buildAssetFilters.mjs +198 -0
  112. package/dist/admin/future/pages/Assets/utils/buildAssetFilters.mjs.map +1 -0
  113. package/dist/admin/future/pages/Assets/utils/listQueryKey.js +2 -2
  114. package/dist/admin/future/pages/Assets/utils/listQueryKey.js.map +1 -1
  115. package/dist/admin/future/pages/Assets/utils/listQueryKey.mjs +2 -2
  116. package/dist/admin/future/pages/Assets/utils/listQueryKey.mjs.map +1 -1
  117. package/dist/admin/future/services/api.js +163 -28
  118. package/dist/admin/future/services/api.js.map +1 -1
  119. package/dist/admin/future/services/api.mjs +163 -30
  120. package/dist/admin/future/services/api.mjs.map +1 -1
  121. package/dist/admin/future/services/assets.js +29 -17
  122. package/dist/admin/future/services/assets.js.map +1 -1
  123. package/dist/admin/future/services/assets.mjs +29 -17
  124. package/dist/admin/future/services/assets.mjs.map +1 -1
  125. package/dist/admin/future/services/folders.js +29 -17
  126. package/dist/admin/future/services/folders.js.map +1 -1
  127. package/dist/admin/future/services/folders.mjs +29 -17
  128. package/dist/admin/future/services/folders.mjs.map +1 -1
  129. package/dist/admin/future/services/settings.js +10 -3
  130. package/dist/admin/future/services/settings.js.map +1 -1
  131. package/dist/admin/future/services/settings.mjs +10 -3
  132. package/dist/admin/future/services/settings.mjs.map +1 -1
  133. package/dist/admin/future/store/uploadProgress.js +109 -2
  134. package/dist/admin/future/store/uploadProgress.js.map +1 -1
  135. package/dist/admin/future/store/uploadProgress.mjs +105 -3
  136. package/dist/admin/future/store/uploadProgress.mjs.map +1 -1
  137. package/dist/admin/future/utils/searchQueryParam.js +11 -0
  138. package/dist/admin/future/utils/searchQueryParam.js.map +1 -0
  139. package/dist/admin/future/utils/searchQueryParam.mjs +9 -0
  140. package/dist/admin/future/utils/searchQueryParam.mjs.map +1 -0
  141. package/dist/admin/hooks/useSelectionState.js +2 -4
  142. package/dist/admin/hooks/useSelectionState.js.map +1 -1
  143. package/dist/admin/hooks/useSelectionState.mjs +2 -4
  144. package/dist/admin/hooks/useSelectionState.mjs.map +1 -1
  145. package/dist/admin/shared/constants.js +43 -0
  146. package/dist/admin/shared/constants.js.map +1 -0
  147. package/dist/admin/shared/constants.mjs +38 -0
  148. package/dist/admin/shared/constants.mjs.map +1 -0
  149. package/dist/admin/src/future/hooks/useMediaLibraryPermissions.d.ts +21 -0
  150. package/dist/admin/src/future/pages/Assets/components/AssetDetails/AssetCropEditor.d.ts +7 -1
  151. package/dist/admin/src/future/pages/Assets/components/AssetsSearchInput.d.ts +5 -0
  152. package/dist/admin/src/future/pages/Assets/components/BulkActionsBar.d.ts +14 -1
  153. package/dist/admin/src/future/pages/Assets/components/DateRangeCalendar.d.ts +8 -0
  154. package/dist/admin/src/future/pages/Assets/components/DropZone/UploadDropZoneContext.d.ts +8 -1
  155. package/dist/admin/src/future/pages/Assets/components/EmptyState.d.ts +18 -1
  156. package/dist/admin/src/future/pages/Assets/components/FilterBadges.d.ts +6 -0
  157. package/dist/admin/src/future/pages/Assets/components/FilterMenu.d.ts +22 -0
  158. package/dist/admin/src/future/pages/Assets/components/FolderTree/FolderTree.d.ts +13 -4
  159. package/dist/admin/src/future/pages/Assets/components/FolderTree/filterFolderTree.d.ts +16 -0
  160. package/dist/admin/src/future/pages/Assets/components/SortMenu.d.ts +6 -0
  161. package/dist/admin/src/future/pages/Assets/hooks/useAssetSearch.d.ts +12 -0
  162. package/dist/admin/src/future/pages/Assets/hooks/useAssetSelection.d.ts +8 -1
  163. package/dist/admin/src/future/pages/Assets/hooks/useInfiniteAssets.d.ts +13 -3
  164. package/dist/admin/src/future/pages/Assets/hooks/useListFilters.d.ts +69 -0
  165. package/dist/admin/src/future/pages/Assets/utils/buildAssetFilters.d.ts +46 -0
  166. package/dist/admin/src/future/pages/Assets/utils/listQueryKey.d.ts +3 -4
  167. package/dist/admin/src/future/services/api.d.ts +53 -5
  168. package/dist/admin/src/future/services/assets.d.ts +9 -1
  169. package/dist/admin/src/future/services/folders.d.ts +3 -0
  170. package/dist/admin/src/future/services/settings.d.ts +2 -2
  171. package/dist/admin/src/future/store/uploadProgress.d.ts +83 -1
  172. package/dist/admin/src/future/utils/searchQueryParam.d.ts +7 -0
  173. package/dist/admin/src/hooks/useAiAvailability.d.ts +1 -1
  174. package/dist/admin/src/hooks/useEditAsset.d.ts +4 -4
  175. package/dist/admin/src/hooks/useEditFolder.d.ts +4 -4
  176. package/dist/admin/src/hooks/useSettings.d.ts +1 -0
  177. package/dist/admin/src/hooks/useTracking.d.ts +1 -1
  178. package/dist/admin/src/hooks/useUpload.d.ts +1 -1
  179. package/dist/admin/translations/en.json.js +53 -3
  180. package/dist/admin/translations/en.json.js.map +1 -1
  181. package/dist/admin/translations/en.json.mjs +53 -3
  182. package/dist/admin/translations/en.json.mjs.map +1 -1
  183. package/dist/admin/utils/urlYupSchema.js +33 -9
  184. package/dist/admin/utils/urlYupSchema.js.map +1 -1
  185. package/dist/admin/utils/urlYupSchema.mjs +33 -9
  186. package/dist/admin/utils/urlYupSchema.mjs.map +1 -1
  187. package/dist/server/config.js +13 -5
  188. package/dist/server/config.js.map +1 -1
  189. package/dist/server/config.mjs +13 -5
  190. package/dist/server/config.mjs.map +1 -1
  191. package/dist/server/constants.js.map +1 -1
  192. package/dist/server/constants.mjs.map +1 -1
  193. package/dist/server/controllers/admin-file.js +30 -0
  194. package/dist/server/controllers/admin-file.js.map +1 -1
  195. package/dist/server/controllers/admin-file.mjs +30 -0
  196. package/dist/server/controllers/admin-file.mjs.map +1 -1
  197. package/dist/server/controllers/admin-settings.js +20 -2
  198. package/dist/server/controllers/admin-settings.js.map +1 -1
  199. package/dist/server/controllers/admin-settings.mjs +21 -3
  200. package/dist/server/controllers/admin-settings.mjs.map +1 -1
  201. package/dist/server/controllers/content-api.js +1 -2
  202. package/dist/server/controllers/content-api.js.map +1 -1
  203. package/dist/server/controllers/content-api.mjs +2 -2
  204. package/dist/server/controllers/content-api.mjs.map +1 -1
  205. package/dist/server/controllers/validation/admin/ai-metadata.js +12 -0
  206. package/dist/server/controllers/validation/admin/ai-metadata.js.map +1 -0
  207. package/dist/server/controllers/validation/admin/ai-metadata.mjs +10 -0
  208. package/dist/server/controllers/validation/admin/ai-metadata.mjs.map +1 -0
  209. package/dist/server/controllers/validation/admin/settings.js +4 -1
  210. package/dist/server/controllers/validation/admin/settings.js.map +1 -1
  211. package/dist/server/controllers/validation/admin/settings.mjs +1 -1
  212. package/dist/server/controllers/validation/admin/settings.mjs.map +1 -1
  213. package/dist/server/routes/admin.js +18 -0
  214. package/dist/server/routes/admin.js.map +1 -1
  215. package/dist/server/routes/admin.mjs +18 -0
  216. package/dist/server/routes/admin.mjs.map +1 -1
  217. package/dist/server/services/ai-metadata.js +108 -11
  218. package/dist/server/services/ai-metadata.js.map +1 -1
  219. package/dist/server/services/ai-metadata.mjs +108 -11
  220. package/dist/server/services/ai-metadata.mjs.map +1 -1
  221. package/dist/server/services/file.js +86 -13
  222. package/dist/server/services/file.js.map +1 -1
  223. package/dist/server/services/file.mjs +87 -14
  224. package/dist/server/services/file.mjs.map +1 -1
  225. package/dist/server/services/image-manipulation.js +4 -4
  226. package/dist/server/services/image-manipulation.js.map +1 -1
  227. package/dist/server/services/image-manipulation.mjs +4 -4
  228. package/dist/server/services/image-manipulation.mjs.map +1 -1
  229. package/dist/server/shared/constants.js +43 -0
  230. package/dist/server/shared/constants.js.map +1 -0
  231. package/dist/server/shared/constants.mjs +38 -0
  232. package/dist/server/shared/constants.mjs.map +1 -0
  233. package/dist/server/src/config.d.ts +2 -0
  234. package/dist/server/src/config.d.ts.map +1 -1
  235. package/dist/server/src/constants.d.ts +2 -1
  236. package/dist/server/src/constants.d.ts.map +1 -1
  237. package/dist/server/src/controllers/admin-file.d.ts +10 -0
  238. package/dist/server/src/controllers/admin-file.d.ts.map +1 -1
  239. package/dist/server/src/controllers/admin-settings.d.ts.map +1 -1
  240. package/dist/server/src/controllers/index.d.ts +1 -0
  241. package/dist/server/src/controllers/index.d.ts.map +1 -1
  242. package/dist/server/src/controllers/validation/admin/ai-metadata.d.ts +2 -0
  243. package/dist/server/src/controllers/validation/admin/ai-metadata.d.ts.map +1 -0
  244. package/dist/server/src/controllers/validation/admin/settings.d.ts +1 -1
  245. package/dist/server/src/controllers/validation/admin/settings.d.ts.map +1 -1
  246. package/dist/server/src/index.d.ts +7 -1
  247. package/dist/server/src/index.d.ts.map +1 -1
  248. package/dist/server/src/routes/admin.d.ts.map +1 -1
  249. package/dist/server/src/services/ai-metadata.d.ts +19 -0
  250. package/dist/server/src/services/ai-metadata.d.ts.map +1 -1
  251. package/dist/server/src/services/file.d.ts +12 -2
  252. package/dist/server/src/services/file.d.ts.map +1 -1
  253. package/dist/server/src/services/index.d.ts +4 -1
  254. package/dist/server/src/services/index.d.ts.map +1 -1
  255. package/dist/server/src/types.d.ts +3 -0
  256. package/dist/server/src/types.d.ts.map +1 -1
  257. package/dist/shared/constants.d.ts +35 -0
  258. package/dist/shared/constants.d.ts.map +1 -0
  259. package/dist/shared/contracts/files.d.ts +25 -0
  260. package/dist/shared/contracts/files.d.ts.map +1 -1
  261. package/dist/shared/contracts/settings.d.ts +7 -0
  262. package/package.json +7 -7
@@ -68,6 +68,34 @@ const uploadProgressSlice = toolkit.createSlice({
68
68
  }
69
69
  ];
70
70
  },
71
+ /**
72
+ * Metadata generation started for a row.
73
+ *
74
+ * Metadata requests are fired-and-forgotten, so their callbacks can land after
75
+ * a new batch has replaced `files` and reused the same row indices. Every
76
+ * metadata payload therefore carries the `uploadId` it was fired for, and stale
77
+ * ones are dropped instead of writing onto another batch's row.
78
+ */ setFileMetadataGenerating (state, action) {
79
+ const { index, uploadId } = action.payload;
80
+ if (uploadId !== state.uploadId) {
81
+ return;
82
+ }
83
+ if (state.files[index]) {
84
+ state.files[index].metadataStatus = 'generating';
85
+ }
86
+ },
87
+ /**
88
+ * Terminal metadata outcome for a row. Never touches `status`/`error`/`errors`:
89
+ * the upload itself already succeeded regardless of how generation went.
90
+ */ setFileMetadataResult (state, action) {
91
+ const { index, uploadId, status } = action.payload;
92
+ if (uploadId !== state.uploadId) {
93
+ return;
94
+ }
95
+ if (state.files[index]) {
96
+ state.files[index].metadataStatus = status;
97
+ }
98
+ },
71
99
  addUploadErrors (state, action) {
72
100
  state.errors = [
73
101
  ...state.errors,
@@ -123,7 +151,9 @@ const uploadProgressSlice = toolkit.createSlice({
123
151
  return {
124
152
  ...file,
125
153
  status: 'pending',
126
- uploadedBytes: 0
154
+ uploadedBytes: 0,
155
+ // A retried row goes through the metadata phase again from scratch.
156
+ metadataStatus: undefined
127
157
  };
128
158
  }
129
159
  return file;
@@ -146,7 +176,79 @@ const uploadProgressSlice = toolkit.createSlice({
146
176
  const uploadedBytes = files.reduce((sum, f)=>sum + f.uploadedBytes, 0);
147
177
  return Math.round(uploadedBytes / totalSize * 100);
148
178
  });
149
- const { openUploadProgress, setFileUploading, setFileProgress, setFileComplete, setFileError, addUploadErrors, closeUploadProgress, toggleMinimize, cancelUpload, setUploadFailed, retryCancelledFiles } = uploadProgressSlice.actions;
179
+ /**
180
+ * Count-based metadata progress across the whole batch: `settled / expected`.
181
+ *
182
+ * Count-based rather than byte-weighted because the generation endpoint answers in one
183
+ * go — there is no intermediate progress to weight.
184
+ *
185
+ * The denominator is every row *expected* to enter the phase, not just those that
186
+ * already have. Generation is fired per file as each upload finishes, so counting only
187
+ * started rows made the denominator grow mid-batch and the percentage jump backwards
188
+ * (1/1 = 100%, then a second row starts and it drops to 1/2 = 50%), which flickered the
189
+ * header subtitle on and off once per file.
190
+ *
191
+ * "Expected" is derived from upload outcome rather than from the AI-metadata flag:
192
+ * - a row still uploading may yet enter the phase, so it counts toward the total;
193
+ * - an errored or cancelled row never will, so it must not — otherwise a batch with
194
+ * one failure could never reach 100%.
195
+ *
196
+ * Returns `null` when nothing has entered the phase and nothing is pending — i.e. AI
197
+ * metadata is off, or the batch is done — so the header can hide the subtitle.
198
+ */ const selectMetadataProgress = toolkit.createSelector((state)=>state.uploadProgress.files, (files)=>{
199
+ const started = files.filter((f)=>f.metadataStatus !== undefined);
200
+ // Nothing has entered the phase yet: with no started row there is no evidence AI
201
+ // metadata is even enabled, so stay silent rather than showing a premature 0%.
202
+ if (started.length === 0) return null;
203
+ // Rows whose upload is still in flight are counted in the denominator so the
204
+ // percentage only ever climbs as the batch progresses.
205
+ const pending = files.filter((f)=>f.metadataStatus === undefined && (f.status === 'pending' || f.status === 'uploading')).length;
206
+ const expected = started.length + pending;
207
+ const settled = started.filter((f)=>f.metadataStatus !== 'generating').length;
208
+ return Math.round(settled / expected * 100);
209
+ });
210
+ /**
211
+ * Whether the metadata phase is still ongoing for the batch as a whole.
212
+ *
213
+ * The header uses this — not `selectMetadataProgress === 100` — to decide when to drop
214
+ * the subtitle. Two distinct reasons the phase can be unfinished:
215
+ * - a row is generating right now;
216
+ * - no row is generating this instant, but rows are still uploading and will enter the
217
+ * phase when they finish.
218
+ *
219
+ * The second clause is what keeps the subtitle stable. Generation for file N typically
220
+ * finishes before file N+1 finishes uploading, so a naive "is anything generating?"
221
+ * check goes false between every pair of files and blinks the subtitle out and back in.
222
+ */ const selectIsGeneratingMetadata = toolkit.createSelector((state)=>state.uploadProgress.files, (files)=>{
223
+ if (files.some((f)=>f.metadataStatus === 'generating')) return true;
224
+ // Only bridge the gap if some row has already entered the phase — otherwise a batch
225
+ // with AI metadata disabled would report the phase as ongoing for its whole upload.
226
+ const hasStarted = files.some((f)=>f.metadataStatus !== undefined);
227
+ return hasStarted && files.some((f)=>f.status === 'pending' || f.status === 'uploading');
228
+ });
229
+ /**
230
+ * How many rows reached each terminal metadata outcome, for the header's completion
231
+ * message. `null` while the phase is unfinished — or was never entered at all — so the
232
+ * caller has a single check for "there is no settled outcome to report yet".
233
+ *
234
+ * Counts are reported separately rather than collapsed into one total because they are
235
+ * not interchangeable: only `generated` rows actually had metadata written, so only that
236
+ * count can be claimed as a success. A batch of non-images settles entirely on `skipped`,
237
+ * where "generated on 0 files" would be worse than saying nothing.
238
+ */ const selectMetadataOutcome = toolkit.createSelector((state)=>state.uploadProgress.files, (files)=>{
239
+ const started = files.filter((f)=>f.metadataStatus !== undefined);
240
+ if (started.length === 0) return null;
241
+ // Mirrors `selectIsGeneratingMetadata`: the phase isn't over while a row is
242
+ // generating, nor while a row is still uploading and has yet to enter it.
243
+ const isOngoing = started.some((f)=>f.metadataStatus === 'generating') || files.some((f)=>f.status === 'pending' || f.status === 'uploading');
244
+ if (isOngoing) return null;
245
+ return {
246
+ generated: started.filter((f)=>f.metadataStatus === 'generated').length,
247
+ skipped: started.filter((f)=>f.metadataStatus === 'skipped').length,
248
+ failed: started.filter((f)=>f.metadataStatus === 'failed').length
249
+ };
250
+ });
251
+ const { openUploadProgress, setFileUploading, setFileProgress, setFileComplete, setFileError, setFileMetadataGenerating, setFileMetadataResult, addUploadErrors, closeUploadProgress, toggleMinimize, cancelUpload, setUploadFailed, retryCancelledFiles } = uploadProgressSlice.actions;
150
252
  const uploadProgressReducer = uploadProgressSlice.reducer;
151
253
 
152
254
  exports.addUploadErrors = addUploadErrors;
@@ -155,8 +257,13 @@ exports.closeUploadProgress = closeUploadProgress;
155
257
  exports.openUploadProgress = openUploadProgress;
156
258
  exports.retryCancelledFiles = retryCancelledFiles;
157
259
  exports.selectAggregateProgress = selectAggregateProgress;
260
+ exports.selectIsGeneratingMetadata = selectIsGeneratingMetadata;
261
+ exports.selectMetadataOutcome = selectMetadataOutcome;
262
+ exports.selectMetadataProgress = selectMetadataProgress;
158
263
  exports.setFileComplete = setFileComplete;
159
264
  exports.setFileError = setFileError;
265
+ exports.setFileMetadataGenerating = setFileMetadataGenerating;
266
+ exports.setFileMetadataResult = setFileMetadataResult;
160
267
  exports.setFileProgress = setFileProgress;
161
268
  exports.setFileUploading = setFileUploading;
162
269
  exports.setUploadFailed = setUploadFailed;
@@ -1 +1 @@
1
- {"version":3,"file":"uploadProgress.js","sources":["../../../../admin/src/future/store/uploadProgress.ts"],"sourcesContent":["import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';\n\nimport type { File } from '../../../../shared/contracts/files';\n\nexport interface FileUploadError {\n name: string;\n message: string;\n}\n\nexport type FileProgressStatus = 'pending' | 'uploading' | 'complete' | 'error' | 'cancelled';\n\nexport interface FileProgress {\n name: string;\n index: number;\n status: FileProgressStatus;\n size: number;\n uploadedBytes: number;\n file?: File;\n error?: string;\n}\n\nexport interface UploadProgressState {\n isVisible: boolean;\n isMinimized: boolean;\n totalFiles: number;\n files: FileProgress[];\n errors: FileUploadError[];\n uploadId: number;\n}\n\nexport interface RootState {\n uploadProgress: UploadProgressState;\n}\n\nconst initialState: UploadProgressState = {\n isVisible: false,\n isMinimized: false,\n totalFiles: 0,\n files: [],\n errors: [],\n uploadId: 0,\n};\n\nconst uploadProgressSlice = createSlice({\n name: 'uploadProgress',\n initialState,\n reducers: {\n openUploadProgress(\n state,\n action: PayloadAction<{\n totalFiles: number;\n fileNames: string[];\n fileSizes?: number[];\n }>\n ) {\n state.isVisible = true;\n state.isMinimized = false;\n\n // Create pending files for upload\n const pendingFiles: FileProgress[] = action.payload.fileNames.map((name, index) => ({\n name,\n index,\n status: 'pending' as FileProgressStatus,\n size: action.payload.fileSizes?.[index] ?? 0,\n uploadedBytes: 0,\n }));\n\n state.files = pendingFiles;\n state.totalFiles = action.payload.totalFiles;\n state.errors = [];\n state.uploadId += 1;\n },\n setFileUploading(state, action: PayloadAction<{ name: string; index: number; size: number }>) {\n const { index, size } = action.payload;\n if (state.files[index]) {\n state.files[index].status = 'uploading';\n state.files[index].size = size;\n }\n },\n setFileProgress(state, action: PayloadAction<{ index: number; bytes: number }>) {\n const { index, bytes } = action.payload;\n const file = state.files[index];\n if (file) {\n // Clamp to the known file size so the aggregate can never exceed 100%.\n file.uploadedBytes = Math.min(bytes, file.size);\n }\n },\n setFileComplete(state, action: PayloadAction<{ index: number; file: File }>) {\n const { index, file } = action.payload;\n if (state.files[index]) {\n state.files[index].status = 'complete';\n state.files[index].file = file;\n // Reflect completion in the aggregate even if the final progress event was throttled.\n state.files[index].uploadedBytes = state.files[index].size;\n }\n },\n setFileError(state, action: PayloadAction<{ index: number; name: string; message: string }>) {\n const { index, name, message } = action.payload;\n if (state.files[index]) {\n state.files[index].status = 'error';\n state.files[index].error = message;\n }\n state.errors = [...state.errors, { name, message }];\n },\n addUploadErrors(state, action: PayloadAction<FileUploadError[]>) {\n state.errors = [...state.errors, ...action.payload];\n },\n closeUploadProgress(state) {\n state.isVisible = false;\n state.isMinimized = false;\n state.totalFiles = 0;\n state.files = [];\n state.errors = [];\n },\n toggleMinimize(state) {\n state.isMinimized = !state.isMinimized;\n },\n cancelUpload(state) {\n // Mark all pending and uploading files as cancelled\n state.files = state.files.map((file) => {\n if (file.status === 'pending' || file.status === 'uploading') {\n return { ...file, status: 'cancelled' as FileProgressStatus };\n }\n return file;\n });\n },\n setUploadFailed(state, action: PayloadAction<{ message: string }>) {\n // Mark all pending and uploading files as errored when a catastrophic failure occurs\n state.files = state.files.map((file) => {\n if (file.status === 'pending' || file.status === 'uploading') {\n return {\n ...file,\n status: 'error' as FileProgressStatus,\n error: action.payload.message,\n };\n }\n return file;\n });\n state.errors = [...state.errors, { name: 'Upload Error', message: action.payload.message }];\n },\n retryCancelledFiles(state) {\n // Reset all cancelled files back to pending for retry\n state.files = state.files.map((file) => {\n if (file.status === 'cancelled') {\n return {\n ...file,\n status: 'pending' as FileProgressStatus,\n uploadedBytes: 0,\n };\n }\n return file;\n });\n },\n },\n});\n\n/**\n * Byte-weighted aggregate progress across the whole batch: `sum(uploadedBytes) / sum(size)`.\n *\n * Falls back to count-based progress (settled files / total files) when all sizes are\n * zero — e.g. URL-flow rows where the file size is unknown up front.\n */\nexport const selectAggregateProgress = createSelector(\n (state: RootState) => state.uploadProgress.files,\n (files): number => {\n if (files.length === 0) return 0;\n\n const totalSize = files.reduce((sum, f) => sum + f.size, 0);\n\n if (totalSize === 0) {\n const settled = files.filter(\n (f) => f.status === 'complete' || f.status === 'error' || f.status === 'cancelled'\n ).length;\n return Math.round((settled / files.length) * 100);\n }\n\n const uploadedBytes = files.reduce((sum, f) => sum + f.uploadedBytes, 0);\n return Math.round((uploadedBytes / totalSize) * 100);\n }\n);\n\nexport const {\n openUploadProgress,\n setFileUploading,\n setFileProgress,\n setFileComplete,\n setFileError,\n addUploadErrors,\n closeUploadProgress,\n toggleMinimize,\n cancelUpload,\n setUploadFailed,\n retryCancelledFiles,\n} = uploadProgressSlice.actions;\n\nexport const uploadProgressReducer = uploadProgressSlice.reducer;\n"],"names":["initialState","isVisible","isMinimized","totalFiles","files","errors","uploadId","uploadProgressSlice","createSlice","name","reducers","openUploadProgress","state","action","pendingFiles","payload","fileNames","map","index","status","size","fileSizes","uploadedBytes","setFileUploading","setFileProgress","bytes","file","Math","min","setFileComplete","setFileError","message","error","addUploadErrors","closeUploadProgress","toggleMinimize","cancelUpload","setUploadFailed","retryCancelledFiles","selectAggregateProgress","createSelector","uploadProgress","length","totalSize","reduce","sum","f","settled","filter","round","actions","uploadProgressReducer","reducer"],"mappings":"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"}
1
+ {"version":3,"file":"uploadProgress.js","sources":["../../../../admin/src/future/store/uploadProgress.ts"],"sourcesContent":["import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';\n\nimport type { File } from '../../../../shared/contracts/files';\n\nexport interface FileUploadError {\n name: string;\n message: string;\n}\n\nexport type FileProgressStatus = 'pending' | 'uploading' | 'complete' | 'error' | 'cancelled';\n\n/**\n * AI metadata generation phase for a single row, tracked independently from the\n * upload status: a metadata failure never turns a successful upload into an error.\n * `undefined` means the row has no metadata phase at all, i.e. AI metadata is\n * disabled — non-images do get a phase and come back `skipped` from the server.\n */\nexport type FileMetadataStatus = 'generating' | 'generated' | 'skipped' | 'failed';\n\nexport type FileMetadataResultStatus = Exclude<FileMetadataStatus, 'generating'>;\n\nexport interface FileProgress {\n name: string;\n index: number;\n status: FileProgressStatus;\n size: number;\n uploadedBytes: number;\n file?: File;\n error?: string;\n metadataStatus?: FileMetadataStatus;\n}\n\nexport interface UploadProgressState {\n isVisible: boolean;\n isMinimized: boolean;\n totalFiles: number;\n files: FileProgress[];\n errors: FileUploadError[];\n uploadId: number;\n}\n\nexport interface RootState {\n uploadProgress: UploadProgressState;\n}\n\nconst initialState: UploadProgressState = {\n isVisible: false,\n isMinimized: false,\n totalFiles: 0,\n files: [],\n errors: [],\n uploadId: 0,\n};\n\nconst uploadProgressSlice = createSlice({\n name: 'uploadProgress',\n initialState,\n reducers: {\n openUploadProgress(\n state,\n action: PayloadAction<{\n totalFiles: number;\n fileNames: string[];\n fileSizes?: number[];\n }>\n ) {\n state.isVisible = true;\n state.isMinimized = false;\n\n // Create pending files for upload\n const pendingFiles: FileProgress[] = action.payload.fileNames.map((name, index) => ({\n name,\n index,\n status: 'pending' as FileProgressStatus,\n size: action.payload.fileSizes?.[index] ?? 0,\n uploadedBytes: 0,\n }));\n\n state.files = pendingFiles;\n state.totalFiles = action.payload.totalFiles;\n state.errors = [];\n state.uploadId += 1;\n },\n setFileUploading(state, action: PayloadAction<{ name: string; index: number; size: number }>) {\n const { index, size } = action.payload;\n if (state.files[index]) {\n state.files[index].status = 'uploading';\n state.files[index].size = size;\n }\n },\n setFileProgress(state, action: PayloadAction<{ index: number; bytes: number }>) {\n const { index, bytes } = action.payload;\n const file = state.files[index];\n if (file) {\n // Clamp to the known file size so the aggregate can never exceed 100%.\n file.uploadedBytes = Math.min(bytes, file.size);\n }\n },\n setFileComplete(state, action: PayloadAction<{ index: number; file: File }>) {\n const { index, file } = action.payload;\n if (state.files[index]) {\n state.files[index].status = 'complete';\n state.files[index].file = file;\n // Reflect completion in the aggregate even if the final progress event was throttled.\n state.files[index].uploadedBytes = state.files[index].size;\n }\n },\n setFileError(state, action: PayloadAction<{ index: number; name: string; message: string }>) {\n const { index, name, message } = action.payload;\n if (state.files[index]) {\n state.files[index].status = 'error';\n state.files[index].error = message;\n }\n state.errors = [...state.errors, { name, message }];\n },\n /**\n * Metadata generation started for a row.\n *\n * Metadata requests are fired-and-forgotten, so their callbacks can land after\n * a new batch has replaced `files` and reused the same row indices. Every\n * metadata payload therefore carries the `uploadId` it was fired for, and stale\n * ones are dropped instead of writing onto another batch's row.\n */\n setFileMetadataGenerating(state, action: PayloadAction<{ index: number; uploadId: number }>) {\n const { index, uploadId } = action.payload;\n if (uploadId !== state.uploadId) {\n return;\n }\n if (state.files[index]) {\n state.files[index].metadataStatus = 'generating';\n }\n },\n /**\n * Terminal metadata outcome for a row. Never touches `status`/`error`/`errors`:\n * the upload itself already succeeded regardless of how generation went.\n */\n setFileMetadataResult(\n state,\n action: PayloadAction<{\n index: number;\n uploadId: number;\n status: FileMetadataResultStatus;\n }>\n ) {\n const { index, uploadId, status } = action.payload;\n if (uploadId !== state.uploadId) {\n return;\n }\n if (state.files[index]) {\n state.files[index].metadataStatus = status;\n }\n },\n addUploadErrors(state, action: PayloadAction<FileUploadError[]>) {\n state.errors = [...state.errors, ...action.payload];\n },\n closeUploadProgress(state) {\n state.isVisible = false;\n state.isMinimized = false;\n state.totalFiles = 0;\n state.files = [];\n state.errors = [];\n },\n toggleMinimize(state) {\n state.isMinimized = !state.isMinimized;\n },\n cancelUpload(state) {\n // Mark all pending and uploading files as cancelled\n state.files = state.files.map((file) => {\n if (file.status === 'pending' || file.status === 'uploading') {\n return { ...file, status: 'cancelled' as FileProgressStatus };\n }\n return file;\n });\n },\n setUploadFailed(state, action: PayloadAction<{ message: string }>) {\n // Mark all pending and uploading files as errored when a catastrophic failure occurs\n state.files = state.files.map((file) => {\n if (file.status === 'pending' || file.status === 'uploading') {\n return {\n ...file,\n status: 'error' as FileProgressStatus,\n error: action.payload.message,\n };\n }\n return file;\n });\n state.errors = [...state.errors, { name: 'Upload Error', message: action.payload.message }];\n },\n retryCancelledFiles(state) {\n // Reset all cancelled files back to pending for retry\n state.files = state.files.map((file) => {\n if (file.status === 'cancelled') {\n return {\n ...file,\n status: 'pending' as FileProgressStatus,\n uploadedBytes: 0,\n // A retried row goes through the metadata phase again from scratch.\n metadataStatus: undefined,\n };\n }\n return file;\n });\n },\n },\n});\n\n/**\n * Byte-weighted aggregate progress across the whole batch: `sum(uploadedBytes) / sum(size)`.\n *\n * Falls back to count-based progress (settled files / total files) when all sizes are\n * zero — e.g. URL-flow rows where the file size is unknown up front.\n */\nexport const selectAggregateProgress = createSelector(\n (state: RootState) => state.uploadProgress.files,\n (files): number => {\n if (files.length === 0) return 0;\n\n const totalSize = files.reduce((sum, f) => sum + f.size, 0);\n\n if (totalSize === 0) {\n const settled = files.filter(\n (f) => f.status === 'complete' || f.status === 'error' || f.status === 'cancelled'\n ).length;\n return Math.round((settled / files.length) * 100);\n }\n\n const uploadedBytes = files.reduce((sum, f) => sum + f.uploadedBytes, 0);\n return Math.round((uploadedBytes / totalSize) * 100);\n }\n);\n\n/**\n * Count-based metadata progress across the whole batch: `settled / expected`.\n *\n * Count-based rather than byte-weighted because the generation endpoint answers in one\n * go — there is no intermediate progress to weight.\n *\n * The denominator is every row *expected* to enter the phase, not just those that\n * already have. Generation is fired per file as each upload finishes, so counting only\n * started rows made the denominator grow mid-batch and the percentage jump backwards\n * (1/1 = 100%, then a second row starts and it drops to 1/2 = 50%), which flickered the\n * header subtitle on and off once per file.\n *\n * \"Expected\" is derived from upload outcome rather than from the AI-metadata flag:\n * - a row still uploading may yet enter the phase, so it counts toward the total;\n * - an errored or cancelled row never will, so it must not — otherwise a batch with\n * one failure could never reach 100%.\n *\n * Returns `null` when nothing has entered the phase and nothing is pending — i.e. AI\n * metadata is off, or the batch is done — so the header can hide the subtitle.\n */\nexport const selectMetadataProgress = createSelector(\n (state: RootState) => state.uploadProgress.files,\n (files): number | null => {\n const started = files.filter((f) => f.metadataStatus !== undefined);\n\n // Nothing has entered the phase yet: with no started row there is no evidence AI\n // metadata is even enabled, so stay silent rather than showing a premature 0%.\n if (started.length === 0) return null;\n\n // Rows whose upload is still in flight are counted in the denominator so the\n // percentage only ever climbs as the batch progresses.\n const pending = files.filter(\n (f) => f.metadataStatus === undefined && (f.status === 'pending' || f.status === 'uploading')\n ).length;\n\n const expected = started.length + pending;\n const settled = started.filter((f) => f.metadataStatus !== 'generating').length;\n\n return Math.round((settled / expected) * 100);\n }\n);\n\n/**\n * Whether the metadata phase is still ongoing for the batch as a whole.\n *\n * The header uses this — not `selectMetadataProgress === 100` — to decide when to drop\n * the subtitle. Two distinct reasons the phase can be unfinished:\n * - a row is generating right now;\n * - no row is generating this instant, but rows are still uploading and will enter the\n * phase when they finish.\n *\n * The second clause is what keeps the subtitle stable. Generation for file N typically\n * finishes before file N+1 finishes uploading, so a naive \"is anything generating?\"\n * check goes false between every pair of files and blinks the subtitle out and back in.\n */\nexport const selectIsGeneratingMetadata = createSelector(\n (state: RootState) => state.uploadProgress.files,\n (files): boolean => {\n if (files.some((f) => f.metadataStatus === 'generating')) return true;\n\n // Only bridge the gap if some row has already entered the phase — otherwise a batch\n // with AI metadata disabled would report the phase as ongoing for its whole upload.\n const hasStarted = files.some((f) => f.metadataStatus !== undefined);\n\n return hasStarted && files.some((f) => f.status === 'pending' || f.status === 'uploading');\n }\n);\n\n/**\n * How many rows reached each terminal metadata outcome, for the header's completion\n * message. `null` while the phase is unfinished — or was never entered at all — so the\n * caller has a single check for \"there is no settled outcome to report yet\".\n *\n * Counts are reported separately rather than collapsed into one total because they are\n * not interchangeable: only `generated` rows actually had metadata written, so only that\n * count can be claimed as a success. A batch of non-images settles entirely on `skipped`,\n * where \"generated on 0 files\" would be worse than saying nothing.\n */\nexport const selectMetadataOutcome = createSelector(\n (state: RootState) => state.uploadProgress.files,\n (files): { generated: number; skipped: number; failed: number } | null => {\n const started = files.filter((f) => f.metadataStatus !== undefined);\n\n if (started.length === 0) return null;\n\n // Mirrors `selectIsGeneratingMetadata`: the phase isn't over while a row is\n // generating, nor while a row is still uploading and has yet to enter it.\n const isOngoing =\n started.some((f) => f.metadataStatus === 'generating') ||\n files.some((f) => f.status === 'pending' || f.status === 'uploading');\n\n if (isOngoing) return null;\n\n return {\n generated: started.filter((f) => f.metadataStatus === 'generated').length,\n skipped: started.filter((f) => f.metadataStatus === 'skipped').length,\n failed: started.filter((f) => f.metadataStatus === 'failed').length,\n };\n }\n);\n\nexport const {\n openUploadProgress,\n setFileUploading,\n setFileProgress,\n setFileComplete,\n setFileError,\n setFileMetadataGenerating,\n setFileMetadataResult,\n addUploadErrors,\n closeUploadProgress,\n toggleMinimize,\n cancelUpload,\n setUploadFailed,\n retryCancelledFiles,\n} = uploadProgressSlice.actions;\n\nexport const uploadProgressReducer = uploadProgressSlice.reducer;\n"],"names":["initialState","isVisible","isMinimized","totalFiles","files","errors","uploadId","uploadProgressSlice","createSlice","name","reducers","openUploadProgress","state","action","pendingFiles","payload","fileNames","map","index","status","size","fileSizes","uploadedBytes","setFileUploading","setFileProgress","bytes","file","Math","min","setFileComplete","setFileError","message","error","setFileMetadataGenerating","metadataStatus","setFileMetadataResult","addUploadErrors","closeUploadProgress","toggleMinimize","cancelUpload","setUploadFailed","retryCancelledFiles","undefined","selectAggregateProgress","createSelector","uploadProgress","length","totalSize","reduce","sum","f","settled","filter","round","selectMetadataProgress","started","pending","expected","selectIsGeneratingMetadata","some","hasStarted","selectMetadataOutcome","isOngoing","generated","skipped","failed","actions","uploadProgressReducer","reducer"],"mappings":"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"}
@@ -66,6 +66,34 @@ const uploadProgressSlice = createSlice({
66
66
  }
67
67
  ];
68
68
  },
69
+ /**
70
+ * Metadata generation started for a row.
71
+ *
72
+ * Metadata requests are fired-and-forgotten, so their callbacks can land after
73
+ * a new batch has replaced `files` and reused the same row indices. Every
74
+ * metadata payload therefore carries the `uploadId` it was fired for, and stale
75
+ * ones are dropped instead of writing onto another batch's row.
76
+ */ setFileMetadataGenerating (state, action) {
77
+ const { index, uploadId } = action.payload;
78
+ if (uploadId !== state.uploadId) {
79
+ return;
80
+ }
81
+ if (state.files[index]) {
82
+ state.files[index].metadataStatus = 'generating';
83
+ }
84
+ },
85
+ /**
86
+ * Terminal metadata outcome for a row. Never touches `status`/`error`/`errors`:
87
+ * the upload itself already succeeded regardless of how generation went.
88
+ */ setFileMetadataResult (state, action) {
89
+ const { index, uploadId, status } = action.payload;
90
+ if (uploadId !== state.uploadId) {
91
+ return;
92
+ }
93
+ if (state.files[index]) {
94
+ state.files[index].metadataStatus = status;
95
+ }
96
+ },
69
97
  addUploadErrors (state, action) {
70
98
  state.errors = [
71
99
  ...state.errors,
@@ -121,7 +149,9 @@ const uploadProgressSlice = createSlice({
121
149
  return {
122
150
  ...file,
123
151
  status: 'pending',
124
- uploadedBytes: 0
152
+ uploadedBytes: 0,
153
+ // A retried row goes through the metadata phase again from scratch.
154
+ metadataStatus: undefined
125
155
  };
126
156
  }
127
157
  return file;
@@ -144,8 +174,80 @@ const uploadProgressSlice = createSlice({
144
174
  const uploadedBytes = files.reduce((sum, f)=>sum + f.uploadedBytes, 0);
145
175
  return Math.round(uploadedBytes / totalSize * 100);
146
176
  });
147
- const { openUploadProgress, setFileUploading, setFileProgress, setFileComplete, setFileError, addUploadErrors, closeUploadProgress, toggleMinimize, cancelUpload, setUploadFailed, retryCancelledFiles } = uploadProgressSlice.actions;
177
+ /**
178
+ * Count-based metadata progress across the whole batch: `settled / expected`.
179
+ *
180
+ * Count-based rather than byte-weighted because the generation endpoint answers in one
181
+ * go — there is no intermediate progress to weight.
182
+ *
183
+ * The denominator is every row *expected* to enter the phase, not just those that
184
+ * already have. Generation is fired per file as each upload finishes, so counting only
185
+ * started rows made the denominator grow mid-batch and the percentage jump backwards
186
+ * (1/1 = 100%, then a second row starts and it drops to 1/2 = 50%), which flickered the
187
+ * header subtitle on and off once per file.
188
+ *
189
+ * "Expected" is derived from upload outcome rather than from the AI-metadata flag:
190
+ * - a row still uploading may yet enter the phase, so it counts toward the total;
191
+ * - an errored or cancelled row never will, so it must not — otherwise a batch with
192
+ * one failure could never reach 100%.
193
+ *
194
+ * Returns `null` when nothing has entered the phase and nothing is pending — i.e. AI
195
+ * metadata is off, or the batch is done — so the header can hide the subtitle.
196
+ */ const selectMetadataProgress = createSelector((state)=>state.uploadProgress.files, (files)=>{
197
+ const started = files.filter((f)=>f.metadataStatus !== undefined);
198
+ // Nothing has entered the phase yet: with no started row there is no evidence AI
199
+ // metadata is even enabled, so stay silent rather than showing a premature 0%.
200
+ if (started.length === 0) return null;
201
+ // Rows whose upload is still in flight are counted in the denominator so the
202
+ // percentage only ever climbs as the batch progresses.
203
+ const pending = files.filter((f)=>f.metadataStatus === undefined && (f.status === 'pending' || f.status === 'uploading')).length;
204
+ const expected = started.length + pending;
205
+ const settled = started.filter((f)=>f.metadataStatus !== 'generating').length;
206
+ return Math.round(settled / expected * 100);
207
+ });
208
+ /**
209
+ * Whether the metadata phase is still ongoing for the batch as a whole.
210
+ *
211
+ * The header uses this — not `selectMetadataProgress === 100` — to decide when to drop
212
+ * the subtitle. Two distinct reasons the phase can be unfinished:
213
+ * - a row is generating right now;
214
+ * - no row is generating this instant, but rows are still uploading and will enter the
215
+ * phase when they finish.
216
+ *
217
+ * The second clause is what keeps the subtitle stable. Generation for file N typically
218
+ * finishes before file N+1 finishes uploading, so a naive "is anything generating?"
219
+ * check goes false between every pair of files and blinks the subtitle out and back in.
220
+ */ const selectIsGeneratingMetadata = createSelector((state)=>state.uploadProgress.files, (files)=>{
221
+ if (files.some((f)=>f.metadataStatus === 'generating')) return true;
222
+ // Only bridge the gap if some row has already entered the phase — otherwise a batch
223
+ // with AI metadata disabled would report the phase as ongoing for its whole upload.
224
+ const hasStarted = files.some((f)=>f.metadataStatus !== undefined);
225
+ return hasStarted && files.some((f)=>f.status === 'pending' || f.status === 'uploading');
226
+ });
227
+ /**
228
+ * How many rows reached each terminal metadata outcome, for the header's completion
229
+ * message. `null` while the phase is unfinished — or was never entered at all — so the
230
+ * caller has a single check for "there is no settled outcome to report yet".
231
+ *
232
+ * Counts are reported separately rather than collapsed into one total because they are
233
+ * not interchangeable: only `generated` rows actually had metadata written, so only that
234
+ * count can be claimed as a success. A batch of non-images settles entirely on `skipped`,
235
+ * where "generated on 0 files" would be worse than saying nothing.
236
+ */ const selectMetadataOutcome = createSelector((state)=>state.uploadProgress.files, (files)=>{
237
+ const started = files.filter((f)=>f.metadataStatus !== undefined);
238
+ if (started.length === 0) return null;
239
+ // Mirrors `selectIsGeneratingMetadata`: the phase isn't over while a row is
240
+ // generating, nor while a row is still uploading and has yet to enter it.
241
+ const isOngoing = started.some((f)=>f.metadataStatus === 'generating') || files.some((f)=>f.status === 'pending' || f.status === 'uploading');
242
+ if (isOngoing) return null;
243
+ return {
244
+ generated: started.filter((f)=>f.metadataStatus === 'generated').length,
245
+ skipped: started.filter((f)=>f.metadataStatus === 'skipped').length,
246
+ failed: started.filter((f)=>f.metadataStatus === 'failed').length
247
+ };
248
+ });
249
+ const { openUploadProgress, setFileUploading, setFileProgress, setFileComplete, setFileError, setFileMetadataGenerating, setFileMetadataResult, addUploadErrors, closeUploadProgress, toggleMinimize, cancelUpload, setUploadFailed, retryCancelledFiles } = uploadProgressSlice.actions;
148
250
  const uploadProgressReducer = uploadProgressSlice.reducer;
149
251
 
150
- export { addUploadErrors, cancelUpload, closeUploadProgress, openUploadProgress, retryCancelledFiles, selectAggregateProgress, setFileComplete, setFileError, setFileProgress, setFileUploading, setUploadFailed, toggleMinimize, uploadProgressReducer };
252
+ export { addUploadErrors, cancelUpload, closeUploadProgress, openUploadProgress, retryCancelledFiles, selectAggregateProgress, selectIsGeneratingMetadata, selectMetadataOutcome, selectMetadataProgress, setFileComplete, setFileError, setFileMetadataGenerating, setFileMetadataResult, setFileProgress, setFileUploading, setUploadFailed, toggleMinimize, uploadProgressReducer };
151
253
  //# sourceMappingURL=uploadProgress.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"uploadProgress.mjs","sources":["../../../../admin/src/future/store/uploadProgress.ts"],"sourcesContent":["import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';\n\nimport type { File } from '../../../../shared/contracts/files';\n\nexport interface FileUploadError {\n name: string;\n message: string;\n}\n\nexport type FileProgressStatus = 'pending' | 'uploading' | 'complete' | 'error' | 'cancelled';\n\nexport interface FileProgress {\n name: string;\n index: number;\n status: FileProgressStatus;\n size: number;\n uploadedBytes: number;\n file?: File;\n error?: string;\n}\n\nexport interface UploadProgressState {\n isVisible: boolean;\n isMinimized: boolean;\n totalFiles: number;\n files: FileProgress[];\n errors: FileUploadError[];\n uploadId: number;\n}\n\nexport interface RootState {\n uploadProgress: UploadProgressState;\n}\n\nconst initialState: UploadProgressState = {\n isVisible: false,\n isMinimized: false,\n totalFiles: 0,\n files: [],\n errors: [],\n uploadId: 0,\n};\n\nconst uploadProgressSlice = createSlice({\n name: 'uploadProgress',\n initialState,\n reducers: {\n openUploadProgress(\n state,\n action: PayloadAction<{\n totalFiles: number;\n fileNames: string[];\n fileSizes?: number[];\n }>\n ) {\n state.isVisible = true;\n state.isMinimized = false;\n\n // Create pending files for upload\n const pendingFiles: FileProgress[] = action.payload.fileNames.map((name, index) => ({\n name,\n index,\n status: 'pending' as FileProgressStatus,\n size: action.payload.fileSizes?.[index] ?? 0,\n uploadedBytes: 0,\n }));\n\n state.files = pendingFiles;\n state.totalFiles = action.payload.totalFiles;\n state.errors = [];\n state.uploadId += 1;\n },\n setFileUploading(state, action: PayloadAction<{ name: string; index: number; size: number }>) {\n const { index, size } = action.payload;\n if (state.files[index]) {\n state.files[index].status = 'uploading';\n state.files[index].size = size;\n }\n },\n setFileProgress(state, action: PayloadAction<{ index: number; bytes: number }>) {\n const { index, bytes } = action.payload;\n const file = state.files[index];\n if (file) {\n // Clamp to the known file size so the aggregate can never exceed 100%.\n file.uploadedBytes = Math.min(bytes, file.size);\n }\n },\n setFileComplete(state, action: PayloadAction<{ index: number; file: File }>) {\n const { index, file } = action.payload;\n if (state.files[index]) {\n state.files[index].status = 'complete';\n state.files[index].file = file;\n // Reflect completion in the aggregate even if the final progress event was throttled.\n state.files[index].uploadedBytes = state.files[index].size;\n }\n },\n setFileError(state, action: PayloadAction<{ index: number; name: string; message: string }>) {\n const { index, name, message } = action.payload;\n if (state.files[index]) {\n state.files[index].status = 'error';\n state.files[index].error = message;\n }\n state.errors = [...state.errors, { name, message }];\n },\n addUploadErrors(state, action: PayloadAction<FileUploadError[]>) {\n state.errors = [...state.errors, ...action.payload];\n },\n closeUploadProgress(state) {\n state.isVisible = false;\n state.isMinimized = false;\n state.totalFiles = 0;\n state.files = [];\n state.errors = [];\n },\n toggleMinimize(state) {\n state.isMinimized = !state.isMinimized;\n },\n cancelUpload(state) {\n // Mark all pending and uploading files as cancelled\n state.files = state.files.map((file) => {\n if (file.status === 'pending' || file.status === 'uploading') {\n return { ...file, status: 'cancelled' as FileProgressStatus };\n }\n return file;\n });\n },\n setUploadFailed(state, action: PayloadAction<{ message: string }>) {\n // Mark all pending and uploading files as errored when a catastrophic failure occurs\n state.files = state.files.map((file) => {\n if (file.status === 'pending' || file.status === 'uploading') {\n return {\n ...file,\n status: 'error' as FileProgressStatus,\n error: action.payload.message,\n };\n }\n return file;\n });\n state.errors = [...state.errors, { name: 'Upload Error', message: action.payload.message }];\n },\n retryCancelledFiles(state) {\n // Reset all cancelled files back to pending for retry\n state.files = state.files.map((file) => {\n if (file.status === 'cancelled') {\n return {\n ...file,\n status: 'pending' as FileProgressStatus,\n uploadedBytes: 0,\n };\n }\n return file;\n });\n },\n },\n});\n\n/**\n * Byte-weighted aggregate progress across the whole batch: `sum(uploadedBytes) / sum(size)`.\n *\n * Falls back to count-based progress (settled files / total files) when all sizes are\n * zero — e.g. URL-flow rows where the file size is unknown up front.\n */\nexport const selectAggregateProgress = createSelector(\n (state: RootState) => state.uploadProgress.files,\n (files): number => {\n if (files.length === 0) return 0;\n\n const totalSize = files.reduce((sum, f) => sum + f.size, 0);\n\n if (totalSize === 0) {\n const settled = files.filter(\n (f) => f.status === 'complete' || f.status === 'error' || f.status === 'cancelled'\n ).length;\n return Math.round((settled / files.length) * 100);\n }\n\n const uploadedBytes = files.reduce((sum, f) => sum + f.uploadedBytes, 0);\n return Math.round((uploadedBytes / totalSize) * 100);\n }\n);\n\nexport const {\n openUploadProgress,\n setFileUploading,\n setFileProgress,\n setFileComplete,\n setFileError,\n addUploadErrors,\n closeUploadProgress,\n toggleMinimize,\n cancelUpload,\n setUploadFailed,\n retryCancelledFiles,\n} = uploadProgressSlice.actions;\n\nexport const uploadProgressReducer = uploadProgressSlice.reducer;\n"],"names":["initialState","isVisible","isMinimized","totalFiles","files","errors","uploadId","uploadProgressSlice","createSlice","name","reducers","openUploadProgress","state","action","pendingFiles","payload","fileNames","map","index","status","size","fileSizes","uploadedBytes","setFileUploading","setFileProgress","bytes","file","Math","min","setFileComplete","setFileError","message","error","addUploadErrors","closeUploadProgress","toggleMinimize","cancelUpload","setUploadFailed","retryCancelledFiles","selectAggregateProgress","createSelector","uploadProgress","length","totalSize","reduce","sum","f","settled","filter","round","actions","uploadProgressReducer","reducer"],"mappings":"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"}
1
+ {"version":3,"file":"uploadProgress.mjs","sources":["../../../../admin/src/future/store/uploadProgress.ts"],"sourcesContent":["import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';\n\nimport type { File } from '../../../../shared/contracts/files';\n\nexport interface FileUploadError {\n name: string;\n message: string;\n}\n\nexport type FileProgressStatus = 'pending' | 'uploading' | 'complete' | 'error' | 'cancelled';\n\n/**\n * AI metadata generation phase for a single row, tracked independently from the\n * upload status: a metadata failure never turns a successful upload into an error.\n * `undefined` means the row has no metadata phase at all, i.e. AI metadata is\n * disabled — non-images do get a phase and come back `skipped` from the server.\n */\nexport type FileMetadataStatus = 'generating' | 'generated' | 'skipped' | 'failed';\n\nexport type FileMetadataResultStatus = Exclude<FileMetadataStatus, 'generating'>;\n\nexport interface FileProgress {\n name: string;\n index: number;\n status: FileProgressStatus;\n size: number;\n uploadedBytes: number;\n file?: File;\n error?: string;\n metadataStatus?: FileMetadataStatus;\n}\n\nexport interface UploadProgressState {\n isVisible: boolean;\n isMinimized: boolean;\n totalFiles: number;\n files: FileProgress[];\n errors: FileUploadError[];\n uploadId: number;\n}\n\nexport interface RootState {\n uploadProgress: UploadProgressState;\n}\n\nconst initialState: UploadProgressState = {\n isVisible: false,\n isMinimized: false,\n totalFiles: 0,\n files: [],\n errors: [],\n uploadId: 0,\n};\n\nconst uploadProgressSlice = createSlice({\n name: 'uploadProgress',\n initialState,\n reducers: {\n openUploadProgress(\n state,\n action: PayloadAction<{\n totalFiles: number;\n fileNames: string[];\n fileSizes?: number[];\n }>\n ) {\n state.isVisible = true;\n state.isMinimized = false;\n\n // Create pending files for upload\n const pendingFiles: FileProgress[] = action.payload.fileNames.map((name, index) => ({\n name,\n index,\n status: 'pending' as FileProgressStatus,\n size: action.payload.fileSizes?.[index] ?? 0,\n uploadedBytes: 0,\n }));\n\n state.files = pendingFiles;\n state.totalFiles = action.payload.totalFiles;\n state.errors = [];\n state.uploadId += 1;\n },\n setFileUploading(state, action: PayloadAction<{ name: string; index: number; size: number }>) {\n const { index, size } = action.payload;\n if (state.files[index]) {\n state.files[index].status = 'uploading';\n state.files[index].size = size;\n }\n },\n setFileProgress(state, action: PayloadAction<{ index: number; bytes: number }>) {\n const { index, bytes } = action.payload;\n const file = state.files[index];\n if (file) {\n // Clamp to the known file size so the aggregate can never exceed 100%.\n file.uploadedBytes = Math.min(bytes, file.size);\n }\n },\n setFileComplete(state, action: PayloadAction<{ index: number; file: File }>) {\n const { index, file } = action.payload;\n if (state.files[index]) {\n state.files[index].status = 'complete';\n state.files[index].file = file;\n // Reflect completion in the aggregate even if the final progress event was throttled.\n state.files[index].uploadedBytes = state.files[index].size;\n }\n },\n setFileError(state, action: PayloadAction<{ index: number; name: string; message: string }>) {\n const { index, name, message } = action.payload;\n if (state.files[index]) {\n state.files[index].status = 'error';\n state.files[index].error = message;\n }\n state.errors = [...state.errors, { name, message }];\n },\n /**\n * Metadata generation started for a row.\n *\n * Metadata requests are fired-and-forgotten, so their callbacks can land after\n * a new batch has replaced `files` and reused the same row indices. Every\n * metadata payload therefore carries the `uploadId` it was fired for, and stale\n * ones are dropped instead of writing onto another batch's row.\n */\n setFileMetadataGenerating(state, action: PayloadAction<{ index: number; uploadId: number }>) {\n const { index, uploadId } = action.payload;\n if (uploadId !== state.uploadId) {\n return;\n }\n if (state.files[index]) {\n state.files[index].metadataStatus = 'generating';\n }\n },\n /**\n * Terminal metadata outcome for a row. Never touches `status`/`error`/`errors`:\n * the upload itself already succeeded regardless of how generation went.\n */\n setFileMetadataResult(\n state,\n action: PayloadAction<{\n index: number;\n uploadId: number;\n status: FileMetadataResultStatus;\n }>\n ) {\n const { index, uploadId, status } = action.payload;\n if (uploadId !== state.uploadId) {\n return;\n }\n if (state.files[index]) {\n state.files[index].metadataStatus = status;\n }\n },\n addUploadErrors(state, action: PayloadAction<FileUploadError[]>) {\n state.errors = [...state.errors, ...action.payload];\n },\n closeUploadProgress(state) {\n state.isVisible = false;\n state.isMinimized = false;\n state.totalFiles = 0;\n state.files = [];\n state.errors = [];\n },\n toggleMinimize(state) {\n state.isMinimized = !state.isMinimized;\n },\n cancelUpload(state) {\n // Mark all pending and uploading files as cancelled\n state.files = state.files.map((file) => {\n if (file.status === 'pending' || file.status === 'uploading') {\n return { ...file, status: 'cancelled' as FileProgressStatus };\n }\n return file;\n });\n },\n setUploadFailed(state, action: PayloadAction<{ message: string }>) {\n // Mark all pending and uploading files as errored when a catastrophic failure occurs\n state.files = state.files.map((file) => {\n if (file.status === 'pending' || file.status === 'uploading') {\n return {\n ...file,\n status: 'error' as FileProgressStatus,\n error: action.payload.message,\n };\n }\n return file;\n });\n state.errors = [...state.errors, { name: 'Upload Error', message: action.payload.message }];\n },\n retryCancelledFiles(state) {\n // Reset all cancelled files back to pending for retry\n state.files = state.files.map((file) => {\n if (file.status === 'cancelled') {\n return {\n ...file,\n status: 'pending' as FileProgressStatus,\n uploadedBytes: 0,\n // A retried row goes through the metadata phase again from scratch.\n metadataStatus: undefined,\n };\n }\n return file;\n });\n },\n },\n});\n\n/**\n * Byte-weighted aggregate progress across the whole batch: `sum(uploadedBytes) / sum(size)`.\n *\n * Falls back to count-based progress (settled files / total files) when all sizes are\n * zero — e.g. URL-flow rows where the file size is unknown up front.\n */\nexport const selectAggregateProgress = createSelector(\n (state: RootState) => state.uploadProgress.files,\n (files): number => {\n if (files.length === 0) return 0;\n\n const totalSize = files.reduce((sum, f) => sum + f.size, 0);\n\n if (totalSize === 0) {\n const settled = files.filter(\n (f) => f.status === 'complete' || f.status === 'error' || f.status === 'cancelled'\n ).length;\n return Math.round((settled / files.length) * 100);\n }\n\n const uploadedBytes = files.reduce((sum, f) => sum + f.uploadedBytes, 0);\n return Math.round((uploadedBytes / totalSize) * 100);\n }\n);\n\n/**\n * Count-based metadata progress across the whole batch: `settled / expected`.\n *\n * Count-based rather than byte-weighted because the generation endpoint answers in one\n * go — there is no intermediate progress to weight.\n *\n * The denominator is every row *expected* to enter the phase, not just those that\n * already have. Generation is fired per file as each upload finishes, so counting only\n * started rows made the denominator grow mid-batch and the percentage jump backwards\n * (1/1 = 100%, then a second row starts and it drops to 1/2 = 50%), which flickered the\n * header subtitle on and off once per file.\n *\n * \"Expected\" is derived from upload outcome rather than from the AI-metadata flag:\n * - a row still uploading may yet enter the phase, so it counts toward the total;\n * - an errored or cancelled row never will, so it must not — otherwise a batch with\n * one failure could never reach 100%.\n *\n * Returns `null` when nothing has entered the phase and nothing is pending — i.e. AI\n * metadata is off, or the batch is done — so the header can hide the subtitle.\n */\nexport const selectMetadataProgress = createSelector(\n (state: RootState) => state.uploadProgress.files,\n (files): number | null => {\n const started = files.filter((f) => f.metadataStatus !== undefined);\n\n // Nothing has entered the phase yet: with no started row there is no evidence AI\n // metadata is even enabled, so stay silent rather than showing a premature 0%.\n if (started.length === 0) return null;\n\n // Rows whose upload is still in flight are counted in the denominator so the\n // percentage only ever climbs as the batch progresses.\n const pending = files.filter(\n (f) => f.metadataStatus === undefined && (f.status === 'pending' || f.status === 'uploading')\n ).length;\n\n const expected = started.length + pending;\n const settled = started.filter((f) => f.metadataStatus !== 'generating').length;\n\n return Math.round((settled / expected) * 100);\n }\n);\n\n/**\n * Whether the metadata phase is still ongoing for the batch as a whole.\n *\n * The header uses this — not `selectMetadataProgress === 100` — to decide when to drop\n * the subtitle. Two distinct reasons the phase can be unfinished:\n * - a row is generating right now;\n * - no row is generating this instant, but rows are still uploading and will enter the\n * phase when they finish.\n *\n * The second clause is what keeps the subtitle stable. Generation for file N typically\n * finishes before file N+1 finishes uploading, so a naive \"is anything generating?\"\n * check goes false between every pair of files and blinks the subtitle out and back in.\n */\nexport const selectIsGeneratingMetadata = createSelector(\n (state: RootState) => state.uploadProgress.files,\n (files): boolean => {\n if (files.some((f) => f.metadataStatus === 'generating')) return true;\n\n // Only bridge the gap if some row has already entered the phase — otherwise a batch\n // with AI metadata disabled would report the phase as ongoing for its whole upload.\n const hasStarted = files.some((f) => f.metadataStatus !== undefined);\n\n return hasStarted && files.some((f) => f.status === 'pending' || f.status === 'uploading');\n }\n);\n\n/**\n * How many rows reached each terminal metadata outcome, for the header's completion\n * message. `null` while the phase is unfinished — or was never entered at all — so the\n * caller has a single check for \"there is no settled outcome to report yet\".\n *\n * Counts are reported separately rather than collapsed into one total because they are\n * not interchangeable: only `generated` rows actually had metadata written, so only that\n * count can be claimed as a success. A batch of non-images settles entirely on `skipped`,\n * where \"generated on 0 files\" would be worse than saying nothing.\n */\nexport const selectMetadataOutcome = createSelector(\n (state: RootState) => state.uploadProgress.files,\n (files): { generated: number; skipped: number; failed: number } | null => {\n const started = files.filter((f) => f.metadataStatus !== undefined);\n\n if (started.length === 0) return null;\n\n // Mirrors `selectIsGeneratingMetadata`: the phase isn't over while a row is\n // generating, nor while a row is still uploading and has yet to enter it.\n const isOngoing =\n started.some((f) => f.metadataStatus === 'generating') ||\n files.some((f) => f.status === 'pending' || f.status === 'uploading');\n\n if (isOngoing) return null;\n\n return {\n generated: started.filter((f) => f.metadataStatus === 'generated').length,\n skipped: started.filter((f) => f.metadataStatus === 'skipped').length,\n failed: started.filter((f) => f.metadataStatus === 'failed').length,\n };\n }\n);\n\nexport const {\n openUploadProgress,\n setFileUploading,\n setFileProgress,\n setFileComplete,\n setFileError,\n setFileMetadataGenerating,\n setFileMetadataResult,\n addUploadErrors,\n closeUploadProgress,\n toggleMinimize,\n cancelUpload,\n setUploadFailed,\n retryCancelledFiles,\n} = uploadProgressSlice.actions;\n\nexport const uploadProgressReducer = uploadProgressSlice.reducer;\n"],"names":["initialState","isVisible","isMinimized","totalFiles","files","errors","uploadId","uploadProgressSlice","createSlice","name","reducers","openUploadProgress","state","action","pendingFiles","payload","fileNames","map","index","status","size","fileSizes","uploadedBytes","setFileUploading","setFileProgress","bytes","file","Math","min","setFileComplete","setFileError","message","error","setFileMetadataGenerating","metadataStatus","setFileMetadataResult","addUploadErrors","closeUploadProgress","toggleMinimize","cancelUpload","setUploadFailed","retryCancelledFiles","undefined","selectAggregateProgress","createSelector","uploadProgress","length","totalSize","reduce","sum","f","settled","filter","round","selectMetadataProgress","started","pending","expected","selectIsGeneratingMetadata","some","hasStarted","selectMetadataOutcome","isOngoing","generated","skipped","failed","actions","uploadProgressReducer","reducer"],"mappings":";;AA6CA,MAAMA,YAAAA,GAAoC;IACxCC,SAAAA,EAAW,KAAA;IACXC,WAAAA,EAAa,KAAA;IACbC,UAAAA,EAAY,CAAA;AACZC,IAAAA,KAAAA,EAAO,EAAE;AACTC,IAAAA,MAAAA,EAAQ,EAAE;IACVC,QAAAA,EAAU;AACZ,CAAA;AAEA,MAAMC,sBAAsBC,WAAAA,CAAY;IACtCC,IAAAA,EAAM,gBAAA;AACNT,IAAAA,YAAAA;IACAU,QAAAA,EAAU;QACRC,kBAAAA,CAAAA,CACEC,KAAK,EACLC,MAIE,EAAA;AAEFD,YAAAA,KAAAA,CAAMX,SAAS,GAAG,IAAA;AAClBW,YAAAA,KAAAA,CAAMV,WAAW,GAAG,KAAA;;YAGpB,MAAMY,YAAAA,GAA+BD,MAAAA,CAAOE,OAAO,CAACC,SAAS,CAACC,GAAG,CAAC,CAACR,IAAAA,EAAMS,KAAAA,IAAW;AAClFT,oBAAAA,IAAAA;AACAS,oBAAAA,KAAAA;oBACAC,MAAAA,EAAQ,SAAA;AACRC,oBAAAA,IAAAA,EAAMP,OAAOE,OAAO,CAACM,SAAS,GAAGH,MAAM,IAAI,CAAA;oBAC3CI,aAAAA,EAAe;iBACjB,CAAA,CAAA;AAEAV,YAAAA,KAAAA,CAAMR,KAAK,GAAGU,YAAAA;AACdF,YAAAA,KAAAA,CAAMT,UAAU,GAAGU,MAAAA,CAAOE,OAAO,CAACZ,UAAU;YAC5CS,KAAAA,CAAMP,MAAM,GAAG,EAAE;AACjBO,YAAAA,KAAAA,CAAMN,QAAQ,IAAI,CAAA;AACpB,QAAA,CAAA;QACAiB,gBAAAA,CAAAA,CAAiBX,KAAK,EAAEC,MAAoE,EAAA;AAC1F,YAAA,MAAM,EAAEK,KAAK,EAAEE,IAAI,EAAE,GAAGP,OAAOE,OAAO;AACtC,YAAA,IAAIH,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,EAAE;AACtBN,gBAAAA,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,CAACC,MAAM,GAAG,WAAA;AAC5BP,gBAAAA,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,CAACE,IAAI,GAAGA,IAAAA;AAC5B,YAAA;AACF,QAAA,CAAA;QACAI,eAAAA,CAAAA,CAAgBZ,KAAK,EAAEC,MAAuD,EAAA;AAC5E,YAAA,MAAM,EAAEK,KAAK,EAAEO,KAAK,EAAE,GAAGZ,OAAOE,OAAO;AACvC,YAAA,MAAMW,IAAAA,GAAOd,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM;AAC/B,YAAA,IAAIQ,IAAAA,EAAM;;AAERA,gBAAAA,IAAAA,CAAKJ,aAAa,GAAGK,IAAAA,CAAKC,GAAG,CAACH,KAAAA,EAAOC,KAAKN,IAAI,CAAA;AAChD,YAAA;AACF,QAAA,CAAA;QACAS,eAAAA,CAAAA,CAAgBjB,KAAK,EAAEC,MAAoD,EAAA;AACzE,YAAA,MAAM,EAAEK,KAAK,EAAEQ,IAAI,EAAE,GAAGb,OAAOE,OAAO;AACtC,YAAA,IAAIH,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,EAAE;AACtBN,gBAAAA,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,CAACC,MAAM,GAAG,UAAA;AAC5BP,gBAAAA,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,CAACQ,IAAI,GAAGA,IAAAA;;gBAE1Bd,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,CAACI,aAAa,GAAGV,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,CAACE,IAAI;AAC5D,YAAA;AACF,QAAA,CAAA;QACAU,YAAAA,CAAAA,CAAalB,KAAK,EAAEC,MAAuE,EAAA;YACzF,MAAM,EAAEK,KAAK,EAAET,IAAI,EAAEsB,OAAO,EAAE,GAAGlB,MAAAA,CAAOE,OAAO;AAC/C,YAAA,IAAIH,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,EAAE;AACtBN,gBAAAA,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,CAACC,MAAM,GAAG,OAAA;AAC5BP,gBAAAA,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,CAACc,KAAK,GAAGD,OAAAA;AAC7B,YAAA;AACAnB,YAAAA,KAAAA,CAAMP,MAAM,GAAG;AAAIO,gBAAAA,GAAAA,KAAAA,CAAMP,MAAM;AAAE,gBAAA;AAAEI,oBAAAA,IAAAA;AAAMsB,oBAAAA;AAAQ;AAAE,aAAA;AACrD,QAAA,CAAA;AACA;;;;;;;QAQAE,yBAAAA,CAAAA,CAA0BrB,KAAK,EAAEC,MAA0D,EAAA;AACzF,YAAA,MAAM,EAAEK,KAAK,EAAEZ,QAAQ,EAAE,GAAGO,OAAOE,OAAO;YAC1C,IAAIT,QAAAA,KAAaM,KAAAA,CAAMN,QAAQ,EAAE;AAC/B,gBAAA;AACF,YAAA;AACA,YAAA,IAAIM,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,EAAE;AACtBN,gBAAAA,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,CAACgB,cAAc,GAAG,YAAA;AACtC,YAAA;AACF,QAAA,CAAA;AACA;;;QAIAC,qBAAAA,CAAAA,CACEvB,KAAK,EACLC,MAIE,EAAA;YAEF,MAAM,EAAEK,KAAK,EAAEZ,QAAQ,EAAEa,MAAM,EAAE,GAAGN,MAAAA,CAAOE,OAAO;YAClD,IAAIT,QAAAA,KAAaM,KAAAA,CAAMN,QAAQ,EAAE;AAC/B,gBAAA;AACF,YAAA;AACA,YAAA,IAAIM,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,EAAE;AACtBN,gBAAAA,KAAAA,CAAMR,KAAK,CAACc,KAAAA,CAAM,CAACgB,cAAc,GAAGf,MAAAA;AACtC,YAAA;AACF,QAAA,CAAA;QACAiB,eAAAA,CAAAA,CAAgBxB,KAAK,EAAEC,MAAwC,EAAA;AAC7DD,YAAAA,KAAAA,CAAMP,MAAM,GAAG;AAAIO,gBAAAA,GAAAA,KAAAA,CAAMP,MAAM;AAAKQ,gBAAAA,GAAAA,MAAAA,CAAOE;AAAQ,aAAA;AACrD,QAAA,CAAA;AACAsB,QAAAA,mBAAAA,CAAAA,CAAoBzB,KAAK,EAAA;AACvBA,YAAAA,KAAAA,CAAMX,SAAS,GAAG,KAAA;AAClBW,YAAAA,KAAAA,CAAMV,WAAW,GAAG,KAAA;AACpBU,YAAAA,KAAAA,CAAMT,UAAU,GAAG,CAAA;YACnBS,KAAAA,CAAMR,KAAK,GAAG,EAAE;YAChBQ,KAAAA,CAAMP,MAAM,GAAG,EAAE;AACnB,QAAA,CAAA;AACAiC,QAAAA,cAAAA,CAAAA,CAAe1B,KAAK,EAAA;AAClBA,YAAAA,KAAAA,CAAMV,WAAW,GAAG,CAACU,KAAAA,CAAMV,WAAW;AACxC,QAAA,CAAA;AACAqC,QAAAA,YAAAA,CAAAA,CAAa3B,KAAK,EAAA;;AAEhBA,YAAAA,KAAAA,CAAMR,KAAK,GAAGQ,KAAAA,CAAMR,KAAK,CAACa,GAAG,CAAC,CAACS,IAAAA,GAAAA;AAC7B,gBAAA,IAAIA,KAAKP,MAAM,KAAK,aAAaO,IAAAA,CAAKP,MAAM,KAAK,WAAA,EAAa;oBAC5D,OAAO;AAAE,wBAAA,GAAGO,IAAI;wBAAEP,MAAAA,EAAQ;AAAkC,qBAAA;AAC9D,gBAAA;gBACA,OAAOO,IAAAA;AACT,YAAA,CAAA,CAAA;AACF,QAAA,CAAA;QACAc,eAAAA,CAAAA,CAAgB5B,KAAK,EAAEC,MAA0C,EAAA;;AAE/DD,YAAAA,KAAAA,CAAMR,KAAK,GAAGQ,KAAAA,CAAMR,KAAK,CAACa,GAAG,CAAC,CAACS,IAAAA,GAAAA;AAC7B,gBAAA,IAAIA,KAAKP,MAAM,KAAK,aAAaO,IAAAA,CAAKP,MAAM,KAAK,WAAA,EAAa;oBAC5D,OAAO;AACL,wBAAA,GAAGO,IAAI;wBACPP,MAAAA,EAAQ,OAAA;wBACRa,KAAAA,EAAOnB,MAAAA,CAAOE,OAAO,CAACgB;AACxB,qBAAA;AACF,gBAAA;gBACA,OAAOL,IAAAA;AACT,YAAA,CAAA,CAAA;AACAd,YAAAA,KAAAA,CAAMP,MAAM,GAAG;AAAIO,gBAAAA,GAAAA,KAAAA,CAAMP,MAAM;AAAE,gBAAA;oBAAEI,IAAAA,EAAM,cAAA;oBAAgBsB,OAAAA,EAASlB,MAAAA,CAAOE,OAAO,CAACgB;AAAQ;AAAE,aAAA;AAC7F,QAAA,CAAA;AACAU,QAAAA,mBAAAA,CAAAA,CAAoB7B,KAAK,EAAA;;AAEvBA,YAAAA,KAAAA,CAAMR,KAAK,GAAGQ,KAAAA,CAAMR,KAAK,CAACa,GAAG,CAAC,CAACS,IAAAA,GAAAA;gBAC7B,IAAIA,IAAAA,CAAKP,MAAM,KAAK,WAAA,EAAa;oBAC/B,OAAO;AACL,wBAAA,GAAGO,IAAI;wBACPP,MAAAA,EAAQ,SAAA;wBACRG,aAAAA,EAAe,CAAA;;wBAEfY,cAAAA,EAAgBQ;AAClB,qBAAA;AACF,gBAAA;gBACA,OAAOhB,IAAAA;AACT,YAAA,CAAA,CAAA;AACF,QAAA;AACF;AACF,CAAA,CAAA;AAEA;;;;;IAMO,MAAMiB,uBAAAA,GAA0BC,cAAAA,CACrC,CAAChC,KAAAA,GAAqBA,KAAAA,CAAMiC,cAAc,CAACzC,KAAK,EAChD,CAACA,KAAAA,GAAAA;AACC,IAAA,IAAIA,KAAAA,CAAM0C,MAAM,KAAK,CAAA,EAAG,OAAO,CAAA;IAE/B,MAAMC,SAAAA,GAAY3C,KAAAA,CAAM4C,MAAM,CAAC,CAACC,KAAKC,CAAAA,GAAMD,GAAAA,GAAMC,CAAAA,CAAE9B,IAAI,EAAE,CAAA,CAAA;AAEzD,IAAA,IAAI2B,cAAc,CAAA,EAAG;AACnB,QAAA,MAAMI,UAAU/C,KAAAA,CAAMgD,MAAM,CAC1B,CAACF,CAAAA,GAAMA,EAAE/B,MAAM,KAAK,UAAA,IAAc+B,CAAAA,CAAE/B,MAAM,KAAK,OAAA,IAAW+B,EAAE/B,MAAM,KAAK,aACvE2B,MAAM;AACR,QAAA,OAAOnB,KAAK0B,KAAK,CAAC,OAACF,GAAU/C,KAAAA,CAAM0C,MAAM,GAAI,GAAA,CAAA;AAC/C,IAAA;IAEA,MAAMxB,aAAAA,GAAgBlB,KAAAA,CAAM4C,MAAM,CAAC,CAACC,KAAKC,CAAAA,GAAMD,GAAAA,GAAMC,CAAAA,CAAE5B,aAAa,EAAE,CAAA,CAAA;AACtE,IAAA,OAAOK,IAAAA,CAAK0B,KAAK,CAAE/B,gBAAgByB,SAAAA,GAAa,GAAA,CAAA;AAClD,CAAA;AAGF;;;;;;;;;;;;;;;;;;;IAoBO,MAAMO,sBAAAA,GAAyBV,cAAAA,CACpC,CAAChC,KAAAA,GAAqBA,KAAAA,CAAMiC,cAAc,CAACzC,KAAK,EAChD,CAACA,KAAAA,GAAAA;IACC,MAAMmD,OAAAA,GAAUnD,MAAMgD,MAAM,CAAC,CAACF,CAAAA,GAAMA,CAAAA,CAAEhB,cAAc,KAAKQ,SAAAA,CAAAA;;;AAIzD,IAAA,IAAIa,OAAAA,CAAQT,MAAM,KAAK,CAAA,EAAG,OAAO,IAAA;;;IAIjC,MAAMU,OAAAA,GAAUpD,MAAMgD,MAAM,CAC1B,CAACF,CAAAA,GAAMA,CAAAA,CAAEhB,cAAc,KAAKQ,SAAAA,KAAcQ,CAAAA,CAAE/B,MAAM,KAAK,SAAA,IAAa+B,CAAAA,CAAE/B,MAAM,KAAK,WAAU,GAC3F2B,MAAM;IAER,MAAMW,QAAAA,GAAWF,OAAAA,CAAQT,MAAM,GAAGU,OAAAA;IAClC,MAAML,OAAAA,GAAUI,OAAAA,CAAQH,MAAM,CAAC,CAACF,IAAMA,CAAAA,CAAEhB,cAAc,KAAK,YAAA,CAAA,CAAcY,MAAM;AAE/E,IAAA,OAAOnB,IAAAA,CAAK0B,KAAK,CAAEF,UAAUM,QAAAA,GAAY,GAAA,CAAA;AAC3C,CAAA;AAGF;;;;;;;;;;;;IAaO,MAAMC,0BAAAA,GAA6Bd,cAAAA,CACxC,CAAChC,KAAAA,GAAqBA,KAAAA,CAAMiC,cAAc,CAACzC,KAAK,EAChD,CAACA,KAAAA,GAAAA;IACC,IAAIA,KAAAA,CAAMuD,IAAI,CAAC,CAACT,IAAMA,CAAAA,CAAEhB,cAAc,KAAK,YAAA,CAAA,EAAe,OAAO,IAAA;;;IAIjE,MAAM0B,UAAAA,GAAaxD,MAAMuD,IAAI,CAAC,CAACT,CAAAA,GAAMA,CAAAA,CAAEhB,cAAc,KAAKQ,SAAAA,CAAAA;AAE1D,IAAA,OAAOkB,UAAAA,IAAcxD,KAAAA,CAAMuD,IAAI,CAAC,CAACT,CAAAA,GAAMA,CAAAA,CAAE/B,MAAM,KAAK,SAAA,IAAa+B,CAAAA,CAAE/B,MAAM,KAAK,WAAA,CAAA;AAChF,CAAA;AAGF;;;;;;;;;IAUO,MAAM0C,qBAAAA,GAAwBjB,cAAAA,CACnC,CAAChC,KAAAA,GAAqBA,KAAAA,CAAMiC,cAAc,CAACzC,KAAK,EAChD,CAACA,KAAAA,GAAAA;IACC,MAAMmD,OAAAA,GAAUnD,MAAMgD,MAAM,CAAC,CAACF,CAAAA,GAAMA,CAAAA,CAAEhB,cAAc,KAAKQ,SAAAA,CAAAA;AAEzD,IAAA,IAAIa,OAAAA,CAAQT,MAAM,KAAK,CAAA,EAAG,OAAO,IAAA;;;IAIjC,MAAMgB,SAAAA,GACJP,QAAQI,IAAI,CAAC,CAACT,CAAAA,GAAMA,CAAAA,CAAEhB,cAAc,KAAK,YAAA,CAAA,IACzC9B,MAAMuD,IAAI,CAAC,CAACT,CAAAA,GAAMA,CAAAA,CAAE/B,MAAM,KAAK,SAAA,IAAa+B,CAAAA,CAAE/B,MAAM,KAAK,WAAA,CAAA;AAE3D,IAAA,IAAI2C,WAAW,OAAO,IAAA;IAEtB,OAAO;QACLC,SAAAA,EAAWR,OAAAA,CAAQH,MAAM,CAAC,CAACF,IAAMA,CAAAA,CAAEhB,cAAc,KAAK,WAAA,CAAA,CAAaY,MAAM;QACzEkB,OAAAA,EAAST,OAAAA,CAAQH,MAAM,CAAC,CAACF,IAAMA,CAAAA,CAAEhB,cAAc,KAAK,SAAA,CAAA,CAAWY,MAAM;QACrEmB,MAAAA,EAAQV,OAAAA,CAAQH,MAAM,CAAC,CAACF,IAAMA,CAAAA,CAAEhB,cAAc,KAAK,QAAA,CAAA,CAAUY;AAC/D,KAAA;AACF,CAAA;AAGK,MAAM,EACXnC,kBAAkB,EAClBY,gBAAgB,EAChBC,eAAe,EACfK,eAAe,EACfC,YAAY,EACZG,yBAAyB,EACzBE,qBAAqB,EACrBC,eAAe,EACfC,mBAAmB,EACnBC,cAAc,EACdC,YAAY,EACZC,eAAe,EACfC,mBAAmB,EACpB,GAAGlC,mBAAAA,CAAoB2D;AAEjB,MAAMC,qBAAAA,GAAwB5D,mBAAAA,CAAoB6D;;;;"}
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `_q` must be encoded on both hops: `useQueryParams.setQuery` and the fetch
5
+ * client both stringify with `{ encode: false }`, so an unencoded `&` would
6
+ * truncate the query. `qs.parse` decodes `+` as a space, so `+` needs escaping
7
+ * beyond what `encodeURIComponent` does.
8
+ */ const encodeSearchQuery = (value)=>encodeURIComponent(value).replace(/\+/g, '%2B');
9
+
10
+ exports.encodeSearchQuery = encodeSearchQuery;
11
+ //# sourceMappingURL=searchQueryParam.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"searchQueryParam.js","sources":["../../../../admin/src/future/utils/searchQueryParam.ts"],"sourcesContent":["/**\n * `_q` must be encoded on both hops: `useQueryParams.setQuery` and the fetch\n * client both stringify with `{ encode: false }`, so an unencoded `&` would\n * truncate the query. `qs.parse` decodes `+` as a space, so `+` needs escaping\n * beyond what `encodeURIComponent` does.\n */\nexport const encodeSearchQuery = (value: string): string =>\n encodeURIComponent(value).replace(/\\+/g, '%2B');\n"],"names":["encodeSearchQuery","value","encodeURIComponent","replace"],"mappings":";;AAAA;;;;;IAMO,MAAMA,iBAAAA,GAAoB,CAACC,KAAAA,GAChCC,mBAAmBD,KAAAA,CAAAA,CAAOE,OAAO,CAAC,KAAA,EAAO,KAAA;;;;"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `_q` must be encoded on both hops: `useQueryParams.setQuery` and the fetch
3
+ * client both stringify with `{ encode: false }`, so an unencoded `&` would
4
+ * truncate the query. `qs.parse` decodes `+` as a space, so `+` needs escaping
5
+ * beyond what `encodeURIComponent` does.
6
+ */ const encodeSearchQuery = (value)=>encodeURIComponent(value).replace(/\+/g, '%2B');
7
+
8
+ export { encodeSearchQuery };
9
+ //# sourceMappingURL=searchQueryParam.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"searchQueryParam.mjs","sources":["../../../../admin/src/future/utils/searchQueryParam.ts"],"sourcesContent":["/**\n * `_q` must be encoded on both hops: `useQueryParams.setQuery` and the fetch\n * client both stringify with `{ encode: false }`, so an unencoded `&` would\n * truncate the query. `qs.parse` decodes `+` as a space, so `+` needs escaping\n * beyond what `encodeURIComponent` does.\n */\nexport const encodeSearchQuery = (value: string): string =>\n encodeURIComponent(value).replace(/\\+/g, '%2B');\n"],"names":["encodeSearchQuery","value","encodeURIComponent","replace"],"mappings":"AAAA;;;;;IAMO,MAAMA,iBAAAA,GAAoB,CAACC,KAAAA,GAChCC,mBAAmBD,KAAAA,CAAAA,CAAOE,OAAO,CAAC,KAAA,EAAO,KAAA;;;;"}
@@ -64,10 +64,8 @@ const useSelectionState = (keys, initialValue)=>{
64
64
  ]);
65
65
  };
66
66
  const deselectMultiple = (nextSelections)=>{
67
- setSelections((currSelections)=>[
68
- // filter out items in currSelections that are in nextSelections
69
- ...currSelections.filter((currentSelection)=>nextSelections.findIndex((nextSelection)=>keys.every((key)=>currentSelection[key] === nextSelection[key])) === -1)
70
- ]);
67
+ setSelections((currSelections)=>// filter out items in currSelections that are in nextSelections
68
+ currSelections.filter((currentSelection)=>nextSelections.findIndex((nextSelection)=>keys.every((key)=>currentSelection[key] === nextSelection[key])) === -1));
71
69
  };
72
70
  return [
73
71
  selections,
@@ -1 +1 @@
1
- {"version":3,"file":"useSelectionState.js","sources":["../../../admin/src/hooks/useSelectionState.ts"],"sourcesContent":["import * as React from 'react';\n\nexport const useSelectionState = <TValues extends object>(\n keys: Array<keyof TValues>,\n initialValue: TValues[]\n) => {\n const [selections, setSelections] = React.useState(initialValue);\n\n const selectOne = (selection: TValues) => {\n const index = selections.findIndex((currentSelection) =>\n keys.every((key) => currentSelection[key] === selection[key])\n );\n\n if (index > -1) {\n setSelections((prevSelected) => [\n ...prevSelected.slice(0, index),\n ...prevSelected.slice(index + 1),\n ]);\n } else {\n setSelections((prevSelected) => [...prevSelected, selection]);\n }\n };\n\n const selectAll = (nextSelections?: TValues[]) => {\n if (selections.length > 0) {\n setSelections([]);\n } else {\n setSelections(nextSelections!);\n }\n };\n\n const selectOnly = (nextSelection: TValues) => {\n const index = selections.findIndex((currentSelection) =>\n keys.every((key) => currentSelection[key] === nextSelection[key])\n );\n\n if (index > -1) {\n setSelections([]);\n } else {\n setSelections([nextSelection]);\n }\n };\n\n const selectMultiple = (nextSelections: TValues[]) => {\n setSelections((currSelections) => [\n // already selected items\n ...currSelections,\n // filter out already selected items from nextSelections\n ...nextSelections.filter(\n (nextSelection) =>\n currSelections.findIndex((currentSelection) =>\n keys.every((key) => currentSelection[key] === nextSelection[key])\n ) === -1\n ),\n ]);\n };\n\n const deselectMultiple = (nextSelections: TValues[]) => {\n setSelections((currSelections) => [\n // filter out items in currSelections that are in nextSelections\n ...currSelections.filter(\n (currentSelection) =>\n nextSelections.findIndex((nextSelection) =>\n keys.every((key) => currentSelection[key] === nextSelection[key])\n ) === -1\n ),\n ]);\n };\n\n return [\n selections,\n { selectOne, selectAll, selectOnly, selectMultiple, deselectMultiple, setSelections },\n ] as const;\n};\n"],"names":["useSelectionState","keys","initialValue","selections","setSelections","React","useState","selectOne","selection","index","findIndex","currentSelection","every","key","prevSelected","slice","selectAll","nextSelections","length","selectOnly","nextSelection","selectMultiple","currSelections","filter","deselectMultiple"],"mappings":"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"}
1
+ {"version":3,"file":"useSelectionState.js","sources":["../../../admin/src/hooks/useSelectionState.ts"],"sourcesContent":["import * as React from 'react';\n\nexport const useSelectionState = <TValues extends object>(\n keys: Array<keyof TValues>,\n initialValue: TValues[]\n) => {\n const [selections, setSelections] = React.useState(initialValue);\n\n const selectOne = (selection: TValues) => {\n const index = selections.findIndex((currentSelection) =>\n keys.every((key) => currentSelection[key] === selection[key])\n );\n\n if (index > -1) {\n setSelections((prevSelected) => [\n ...prevSelected.slice(0, index),\n ...prevSelected.slice(index + 1),\n ]);\n } else {\n setSelections((prevSelected) => [...prevSelected, selection]);\n }\n };\n\n const selectAll = (nextSelections?: TValues[]) => {\n if (selections.length > 0) {\n setSelections([]);\n } else {\n setSelections(nextSelections!);\n }\n };\n\n const selectOnly = (nextSelection: TValues) => {\n const index = selections.findIndex((currentSelection) =>\n keys.every((key) => currentSelection[key] === nextSelection[key])\n );\n\n if (index > -1) {\n setSelections([]);\n } else {\n setSelections([nextSelection]);\n }\n };\n\n const selectMultiple = (nextSelections: TValues[]) => {\n setSelections((currSelections) => [\n // already selected items\n ...currSelections,\n // filter out already selected items from nextSelections\n ...nextSelections.filter(\n (nextSelection) =>\n currSelections.findIndex((currentSelection) =>\n keys.every((key) => currentSelection[key] === nextSelection[key])\n ) === -1\n ),\n ]);\n };\n\n const deselectMultiple = (nextSelections: TValues[]) => {\n setSelections((currSelections) =>\n // filter out items in currSelections that are in nextSelections\n currSelections.filter(\n (currentSelection) =>\n nextSelections.findIndex((nextSelection) =>\n keys.every((key) => currentSelection[key] === nextSelection[key])\n ) === -1\n )\n );\n };\n\n return [\n selections,\n { selectOne, selectAll, selectOnly, selectMultiple, deselectMultiple, setSelections },\n ] as const;\n};\n"],"names":["useSelectionState","keys","initialValue","selections","setSelections","React","useState","selectOne","selection","index","findIndex","currentSelection","every","key","prevSelected","slice","selectAll","nextSelections","length","selectOnly","nextSelection","selectMultiple","currSelections","filter","deselectMultiple"],"mappings":"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"}
@@ -42,10 +42,8 @@ const useSelectionState = (keys, initialValue)=>{
42
42
  ]);
43
43
  };
44
44
  const deselectMultiple = (nextSelections)=>{
45
- setSelections((currSelections)=>[
46
- // filter out items in currSelections that are in nextSelections
47
- ...currSelections.filter((currentSelection)=>nextSelections.findIndex((nextSelection)=>keys.every((key)=>currentSelection[key] === nextSelection[key])) === -1)
48
- ]);
45
+ setSelections((currSelections)=>// filter out items in currSelections that are in nextSelections
46
+ currSelections.filter((currentSelection)=>nextSelections.findIndex((nextSelection)=>keys.every((key)=>currentSelection[key] === nextSelection[key])) === -1));
49
47
  };
50
48
  return [
51
49
  selections,
@@ -1 +1 @@
1
- {"version":3,"file":"useSelectionState.mjs","sources":["../../../admin/src/hooks/useSelectionState.ts"],"sourcesContent":["import * as React from 'react';\n\nexport const useSelectionState = <TValues extends object>(\n keys: Array<keyof TValues>,\n initialValue: TValues[]\n) => {\n const [selections, setSelections] = React.useState(initialValue);\n\n const selectOne = (selection: TValues) => {\n const index = selections.findIndex((currentSelection) =>\n keys.every((key) => currentSelection[key] === selection[key])\n );\n\n if (index > -1) {\n setSelections((prevSelected) => [\n ...prevSelected.slice(0, index),\n ...prevSelected.slice(index + 1),\n ]);\n } else {\n setSelections((prevSelected) => [...prevSelected, selection]);\n }\n };\n\n const selectAll = (nextSelections?: TValues[]) => {\n if (selections.length > 0) {\n setSelections([]);\n } else {\n setSelections(nextSelections!);\n }\n };\n\n const selectOnly = (nextSelection: TValues) => {\n const index = selections.findIndex((currentSelection) =>\n keys.every((key) => currentSelection[key] === nextSelection[key])\n );\n\n if (index > -1) {\n setSelections([]);\n } else {\n setSelections([nextSelection]);\n }\n };\n\n const selectMultiple = (nextSelections: TValues[]) => {\n setSelections((currSelections) => [\n // already selected items\n ...currSelections,\n // filter out already selected items from nextSelections\n ...nextSelections.filter(\n (nextSelection) =>\n currSelections.findIndex((currentSelection) =>\n keys.every((key) => currentSelection[key] === nextSelection[key])\n ) === -1\n ),\n ]);\n };\n\n const deselectMultiple = (nextSelections: TValues[]) => {\n setSelections((currSelections) => [\n // filter out items in currSelections that are in nextSelections\n ...currSelections.filter(\n (currentSelection) =>\n nextSelections.findIndex((nextSelection) =>\n keys.every((key) => currentSelection[key] === nextSelection[key])\n ) === -1\n ),\n ]);\n };\n\n return [\n selections,\n { selectOne, selectAll, selectOnly, selectMultiple, deselectMultiple, setSelections },\n ] as const;\n};\n"],"names":["useSelectionState","keys","initialValue","selections","setSelections","React","useState","selectOne","selection","index","findIndex","currentSelection","every","key","prevSelected","slice","selectAll","nextSelections","length","selectOnly","nextSelection","selectMultiple","currSelections","filter","deselectMultiple"],"mappings":"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"}
1
+ {"version":3,"file":"useSelectionState.mjs","sources":["../../../admin/src/hooks/useSelectionState.ts"],"sourcesContent":["import * as React from 'react';\n\nexport const useSelectionState = <TValues extends object>(\n keys: Array<keyof TValues>,\n initialValue: TValues[]\n) => {\n const [selections, setSelections] = React.useState(initialValue);\n\n const selectOne = (selection: TValues) => {\n const index = selections.findIndex((currentSelection) =>\n keys.every((key) => currentSelection[key] === selection[key])\n );\n\n if (index > -1) {\n setSelections((prevSelected) => [\n ...prevSelected.slice(0, index),\n ...prevSelected.slice(index + 1),\n ]);\n } else {\n setSelections((prevSelected) => [...prevSelected, selection]);\n }\n };\n\n const selectAll = (nextSelections?: TValues[]) => {\n if (selections.length > 0) {\n setSelections([]);\n } else {\n setSelections(nextSelections!);\n }\n };\n\n const selectOnly = (nextSelection: TValues) => {\n const index = selections.findIndex((currentSelection) =>\n keys.every((key) => currentSelection[key] === nextSelection[key])\n );\n\n if (index > -1) {\n setSelections([]);\n } else {\n setSelections([nextSelection]);\n }\n };\n\n const selectMultiple = (nextSelections: TValues[]) => {\n setSelections((currSelections) => [\n // already selected items\n ...currSelections,\n // filter out already selected items from nextSelections\n ...nextSelections.filter(\n (nextSelection) =>\n currSelections.findIndex((currentSelection) =>\n keys.every((key) => currentSelection[key] === nextSelection[key])\n ) === -1\n ),\n ]);\n };\n\n const deselectMultiple = (nextSelections: TValues[]) => {\n setSelections((currSelections) =>\n // filter out items in currSelections that are in nextSelections\n currSelections.filter(\n (currentSelection) =>\n nextSelections.findIndex((nextSelection) =>\n keys.every((key) => currentSelection[key] === nextSelection[key])\n ) === -1\n )\n );\n };\n\n return [\n selections,\n { selectOne, selectAll, selectOnly, selectMultiple, deselectMultiple, setSelections },\n ] as const;\n};\n"],"names":["useSelectionState","keys","initialValue","selections","setSelections","React","useState","selectOne","selection","index","findIndex","currentSelection","every","key","prevSelected","slice","selectAll","nextSelections","length","selectOnly","nextSelection","selectMultiple","currSelections","filter","deselectMultiple"],"mappings":";;AAEO,MAAMA,iBAAAA,GAAoB,CAC/BC,IAAAA,EACAC,YAAAA,GAAAA;AAEA,IAAA,MAAM,CAACC,UAAAA,EAAYC,aAAAA,CAAc,GAAGC,KAAAA,CAAMC,QAAQ,CAACJ,YAAAA,CAAAA;AAEnD,IAAA,MAAMK,YAAY,CAACC,SAAAA,GAAAA;AACjB,QAAA,MAAMC,QAAQN,UAAAA,CAAWO,SAAS,CAAC,CAACC,mBAClCV,IAAAA,CAAKW,KAAK,CAAC,CAACC,MAAQF,gBAAgB,CAACE,IAAI,KAAKL,SAAS,CAACK,GAAAA,CAAI,CAAA,CAAA;QAG9D,IAAIJ,KAAAA,GAAQ,EAAC,EAAG;AACdL,YAAAA,aAAAA,CAAc,CAACU,YAAAA,GAAiB;uBAC3BA,YAAAA,CAAaC,KAAK,CAAC,CAAA,EAAGN,KAAAA,CAAAA;uBACtBK,YAAAA,CAAaC,KAAK,CAACN,KAAAA,GAAQ,CAAA;AAC/B,iBAAA,CAAA;QACH,CAAA,MAAO;AACLL,YAAAA,aAAAA,CAAc,CAACU,YAAAA,GAAiB;AAAIA,oBAAAA,GAAAA,YAAAA;AAAcN,oBAAAA;AAAU,iBAAA,CAAA;AAC9D,QAAA;AACF,IAAA,CAAA;AAEA,IAAA,MAAMQ,YAAY,CAACC,cAAAA,GAAAA;QACjB,IAAId,UAAAA,CAAWe,MAAM,GAAG,CAAA,EAAG;AACzBd,YAAAA,aAAAA,CAAc,EAAE,CAAA;QAClB,CAAA,MAAO;YACLA,aAAAA,CAAca,cAAAA,CAAAA;AAChB,QAAA;AACF,IAAA,CAAA;AAEA,IAAA,MAAME,aAAa,CAACC,aAAAA,GAAAA;AAClB,QAAA,MAAMX,QAAQN,UAAAA,CAAWO,SAAS,CAAC,CAACC,mBAClCV,IAAAA,CAAKW,KAAK,CAAC,CAACC,MAAQF,gBAAgB,CAACE,IAAI,KAAKO,aAAa,CAACP,GAAAA,CAAI,CAAA,CAAA;QAGlE,IAAIJ,KAAAA,GAAQ,EAAC,EAAG;AACdL,YAAAA,aAAAA,CAAc,EAAE,CAAA;QAClB,CAAA,MAAO;YACLA,aAAAA,CAAc;AAACgB,gBAAAA;AAAc,aAAA,CAAA;AAC/B,QAAA;AACF,IAAA,CAAA;AAEA,IAAA,MAAMC,iBAAiB,CAACJ,cAAAA,GAAAA;AACtBb,QAAAA,aAAAA,CAAc,CAACkB,cAAAA,GAAmB;;AAE7BA,gBAAAA,GAAAA,cAAAA;;mBAEAL,cAAAA,CAAeM,MAAM,CACtB,CAACH,aAAAA,GACCE,eAAeZ,SAAS,CAAC,CAACC,gBAAAA,GACxBV,IAAAA,CAAKW,KAAK,CAAC,CAACC,GAAAA,GAAQF,gBAAgB,CAACE,GAAAA,CAAI,KAAKO,aAAa,CAACP,GAAAA,CAAI,CAAA,CAAA,KAC5D,EAAC;AAEZ,aAAA,CAAA;AACH,IAAA,CAAA;AAEA,IAAA,MAAMW,mBAAmB,CAACP,cAAAA,GAAAA;QACxBb,aAAAA,CAAc,CAACkB;YAEbA,cAAAA,CAAeC,MAAM,CACnB,CAACZ,gBAAAA,GACCM,eAAeP,SAAS,CAAC,CAACU,aAAAA,GACxBnB,IAAAA,CAAKW,KAAK,CAAC,CAACC,GAAAA,GAAQF,gBAAgB,CAACE,GAAAA,CAAI,KAAKO,aAAa,CAACP,GAAAA,CAAI,CAAA,CAAA,KAC5D,EAAC,CAAA,CAAA;AAGf,IAAA,CAAA;IAEA,OAAO;AACLV,QAAAA,UAAAA;AACA,QAAA;AAAEI,YAAAA,SAAAA;AAAWS,YAAAA,SAAAA;AAAWG,YAAAA,UAAAA;AAAYE,YAAAA,cAAAA;AAAgBG,YAAAA,gBAAAA;AAAkBpB,YAAAA;AAAc;AACrF,KAAA;AACH;;;;"}
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Constants shared by the upload server and the admin panel.
5
+ *
6
+ * The server stays authoritative — it validates and rejects — but the admin
7
+ * needs the same values to disable actions up front rather than letting the
8
+ * user discover a limit through a failed request.
9
+ */ /**
10
+ * Image formats the AI metadata provider can read.
11
+ *
12
+ * Anything outside this list (non-images, but also exotic image formats like
13
+ * SVG or TIFF) is reported as `skipped` instead of being sent.
14
+ *
15
+ * @see https://ai.google.dev/gemini-api/docs/image-understanding
16
+ */ const AI_METADATA_SUPPORTED_IMAGE_TYPES = [
17
+ 'image/png',
18
+ 'image/jpeg',
19
+ 'image/webp',
20
+ 'image/heic',
21
+ 'image/heif'
22
+ ];
23
+ /**
24
+ * Whether the AI metadata provider can generate metadata for this mime type.
25
+ */ const isAIMetadataSupportedMime = (mime)=>AI_METADATA_SUPPORTED_IMAGE_TYPES.includes(mime);
26
+ /**
27
+ * Number of images sent to the AI server per request when generating metadata
28
+ * for an explicit selection. Matches the URL cap of the bulk URL upload flow.
29
+ */ const AI_METADATA_CHUNK_SIZE = 20;
30
+ /**
31
+ * Upper bound on a single synchronous AI metadata request.
32
+ *
33
+ * The selection is processed in sequential chunks inside one HTTP request, so
34
+ * an unbounded selection means an unbounded request — long past most proxy and
35
+ * load balancer timeouts, with no way for the client to learn what was written.
36
+ * Rejecting with a 400 lets the UI ask for a smaller selection instead.
37
+ */ const AI_METADATA_MAX_FILES = AI_METADATA_CHUNK_SIZE * 2;
38
+
39
+ exports.AI_METADATA_CHUNK_SIZE = AI_METADATA_CHUNK_SIZE;
40
+ exports.AI_METADATA_MAX_FILES = AI_METADATA_MAX_FILES;
41
+ exports.AI_METADATA_SUPPORTED_IMAGE_TYPES = AI_METADATA_SUPPORTED_IMAGE_TYPES;
42
+ exports.isAIMetadataSupportedMime = isAIMetadataSupportedMime;
43
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sources":["../../../shared/constants.ts"],"sourcesContent":["/**\n * Constants shared by the upload server and the admin panel.\n *\n * The server stays authoritative — it validates and rejects — but the admin\n * needs the same values to disable actions up front rather than letting the\n * user discover a limit through a failed request.\n */\n\n/**\n * Image formats the AI metadata provider can read.\n *\n * Anything outside this list (non-images, but also exotic image formats like\n * SVG or TIFF) is reported as `skipped` instead of being sent.\n *\n * @see https://ai.google.dev/gemini-api/docs/image-understanding\n */\nexport const AI_METADATA_SUPPORTED_IMAGE_TYPES = [\n 'image/png',\n 'image/jpeg',\n 'image/webp',\n 'image/heic',\n 'image/heif',\n] as const;\n\nexport type AIMetadataSupportedImageType = (typeof AI_METADATA_SUPPORTED_IMAGE_TYPES)[number];\n\n/**\n * Whether the AI metadata provider can generate metadata for this mime type.\n */\nexport const isAIMetadataSupportedMime = (mime?: string | null): boolean =>\n AI_METADATA_SUPPORTED_IMAGE_TYPES.includes(mime as AIMetadataSupportedImageType);\n\n/**\n * Number of images sent to the AI server per request when generating metadata\n * for an explicit selection. Matches the URL cap of the bulk URL upload flow.\n */\nexport const AI_METADATA_CHUNK_SIZE = 20;\n\n/**\n * Upper bound on a single synchronous AI metadata request.\n *\n * The selection is processed in sequential chunks inside one HTTP request, so\n * an unbounded selection means an unbounded request — long past most proxy and\n * load balancer timeouts, with no way for the client to learn what was written.\n * Rejecting with a 400 lets the UI ask for a smaller selection instead.\n */\nexport const AI_METADATA_MAX_FILES = AI_METADATA_CHUNK_SIZE * 2;\n"],"names":["AI_METADATA_SUPPORTED_IMAGE_TYPES","isAIMetadataSupportedMime","mime","includes","AI_METADATA_CHUNK_SIZE","AI_METADATA_MAX_FILES"],"mappings":";;AAAA;;;;;;;;;;;;;UAgBaA,iCAAAA,GAAoC;AAC/C,IAAA,WAAA;AACA,IAAA,YAAA;AACA,IAAA,YAAA;AACA,IAAA,YAAA;AACA,IAAA;;AAKF;;UAGaC,yBAAAA,GAA4B,CAACC,OACxCF,iCAAAA,CAAkCG,QAAQ,CAACD,IAAAA;AAE7C;;;IAIO,MAAME,sBAAAA,GAAyB;AAEtC;;;;;;;AAOC,IACM,MAAMC,qBAAAA,GAAwBD,sBAAAA,GAAyB;;;;;;;"}