@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.
- package/dist/admin/components/AssetCard/VideoPreview.js +1 -1
- package/dist/admin/components/AssetCard/VideoPreview.js.map +1 -1
- package/dist/admin/components/AssetCard/VideoPreview.mjs +1 -1
- package/dist/admin/components/AssetCard/VideoPreview.mjs.map +1 -1
- package/dist/admin/components/TableList/TableList.js +1 -1
- package/dist/admin/components/TableList/TableList.js.map +1 -1
- package/dist/admin/components/TableList/TableList.mjs +1 -1
- package/dist/admin/components/TableList/TableList.mjs.map +1 -1
- package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromUrlForm.js +21 -4
- package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromUrlForm.js.map +1 -1
- package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromUrlForm.mjs +21 -4
- package/dist/admin/components/UploadAssetDialog/AddAssetStep/FromUrlForm.mjs.map +1 -1
- package/dist/admin/future/components/Drawer.js +3 -1
- package/dist/admin/future/components/Drawer.js.map +1 -1
- package/dist/admin/future/components/Drawer.mjs +3 -1
- package/dist/admin/future/components/Drawer.mjs.map +1 -1
- package/dist/admin/future/components/UploadProgressDialog.js +200 -27
- package/dist/admin/future/components/UploadProgressDialog.js.map +1 -1
- package/dist/admin/future/components/UploadProgressDialog.mjs +203 -30
- package/dist/admin/future/components/UploadProgressDialog.mjs.map +1 -1
- package/dist/admin/future/hooks/useMediaLibraryPermissions.js +35 -0
- package/dist/admin/future/hooks/useMediaLibraryPermissions.js.map +1 -0
- package/dist/admin/future/hooks/useMediaLibraryPermissions.mjs +33 -0
- package/dist/admin/future/hooks/useMediaLibraryPermissions.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/AssetsPage.js +203 -78
- package/dist/admin/future/pages/Assets/AssetsPage.js.map +1 -1
- package/dist/admin/future/pages/Assets/AssetsPage.mjs +200 -75
- package/dist/admin/future/pages/Assets/AssetsPage.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js +75 -32
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs +75 -32
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js +46 -19
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs +47 -20
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsGrid.js +5 -2
- package/dist/admin/future/pages/Assets/components/AssetsGrid.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs +5 -2
- package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsSearchInput.js +100 -0
- package/dist/admin/future/pages/Assets/components/AssetsSearchInput.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/AssetsSearchInput.mjs +98 -0
- package/dist/admin/future/pages/Assets/components/AssetsSearchInput.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/components/AssetsTable.js +10 -4
- package/dist/admin/future/pages/Assets/components/AssetsTable.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsTable.mjs +10 -4
- package/dist/admin/future/pages/Assets/components/AssetsTable.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.js +140 -22
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs +142 -24
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/DateRangeCalendar.js +193 -0
- package/dist/admin/future/pages/Assets/components/DateRangeCalendar.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/DateRangeCalendar.mjs +191 -0
- package/dist/admin/future/pages/Assets/components/DateRangeCalendar.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js +18 -1
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs +18 -1
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.js +13 -3
- package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.mjs +13 -3
- package/dist/admin/future/pages/Assets/components/DropZone/UploadDropZoneContext.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/EmptyState.js +62 -3
- package/dist/admin/future/pages/Assets/components/EmptyState.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/EmptyState.mjs +62 -4
- package/dist/admin/future/pages/Assets/components/EmptyState.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/FilterBadges.js +408 -0
- package/dist/admin/future/pages/Assets/components/FilterBadges.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/FilterBadges.mjs +406 -0
- package/dist/admin/future/pages/Assets/components/FilterBadges.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/components/FilterMenu.js +273 -0
- package/dist/admin/future/pages/Assets/components/FilterMenu.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/FilterMenu.mjs +267 -0
- package/dist/admin/future/pages/Assets/components/FilterMenu.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.js +76 -19
- package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.mjs +78 -21
- package/dist/admin/future/pages/Assets/components/FolderTree/FolderTree.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.js +33 -0
- package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.mjs +31 -0
- package/dist/admin/future/pages/Assets/components/FolderTree/filterFolderTree.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/components/SortMenu.js +21 -32
- package/dist/admin/future/pages/Assets/components/SortMenu.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/SortMenu.mjs +22 -33
- package/dist/admin/future/pages/Assets/components/SortMenu.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useAssetSearch.js +40 -0
- package/dist/admin/future/pages/Assets/hooks/useAssetSearch.js.map +1 -0
- package/dist/admin/future/pages/Assets/hooks/useAssetSearch.mjs +38 -0
- package/dist/admin/future/pages/Assets/hooks/useAssetSearch.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/hooks/useAssetSelection.js +21 -5
- package/dist/admin/future/pages/Assets/hooks/useAssetSelection.js.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useAssetSelection.mjs +21 -5
- package/dist/admin/future/pages/Assets/hooks/useAssetSelection.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.js +33 -12
- package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.js.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.mjs +33 -12
- package/dist/admin/future/pages/Assets/hooks/useFolderNavigation.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js +40 -4
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs +40 -4
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useListFilters.js +168 -0
- package/dist/admin/future/pages/Assets/hooks/useListFilters.js.map +1 -0
- package/dist/admin/future/pages/Assets/hooks/useListFilters.mjs +162 -0
- package/dist/admin/future/pages/Assets/hooks/useListFilters.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/utils/buildAssetFilters.js +201 -0
- package/dist/admin/future/pages/Assets/utils/buildAssetFilters.js.map +1 -0
- package/dist/admin/future/pages/Assets/utils/buildAssetFilters.mjs +198 -0
- package/dist/admin/future/pages/Assets/utils/buildAssetFilters.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/utils/listQueryKey.js +2 -2
- package/dist/admin/future/pages/Assets/utils/listQueryKey.js.map +1 -1
- package/dist/admin/future/pages/Assets/utils/listQueryKey.mjs +2 -2
- package/dist/admin/future/pages/Assets/utils/listQueryKey.mjs.map +1 -1
- package/dist/admin/future/services/api.js +163 -28
- package/dist/admin/future/services/api.js.map +1 -1
- package/dist/admin/future/services/api.mjs +163 -30
- package/dist/admin/future/services/api.mjs.map +1 -1
- package/dist/admin/future/services/assets.js +29 -17
- package/dist/admin/future/services/assets.js.map +1 -1
- package/dist/admin/future/services/assets.mjs +29 -17
- package/dist/admin/future/services/assets.mjs.map +1 -1
- package/dist/admin/future/services/folders.js +29 -17
- package/dist/admin/future/services/folders.js.map +1 -1
- package/dist/admin/future/services/folders.mjs +29 -17
- package/dist/admin/future/services/folders.mjs.map +1 -1
- package/dist/admin/future/services/settings.js +10 -3
- package/dist/admin/future/services/settings.js.map +1 -1
- package/dist/admin/future/services/settings.mjs +10 -3
- package/dist/admin/future/services/settings.mjs.map +1 -1
- package/dist/admin/future/store/uploadProgress.js +109 -2
- package/dist/admin/future/store/uploadProgress.js.map +1 -1
- package/dist/admin/future/store/uploadProgress.mjs +105 -3
- package/dist/admin/future/store/uploadProgress.mjs.map +1 -1
- package/dist/admin/future/utils/searchQueryParam.js +11 -0
- package/dist/admin/future/utils/searchQueryParam.js.map +1 -0
- package/dist/admin/future/utils/searchQueryParam.mjs +9 -0
- package/dist/admin/future/utils/searchQueryParam.mjs.map +1 -0
- package/dist/admin/hooks/useSelectionState.js +2 -4
- package/dist/admin/hooks/useSelectionState.js.map +1 -1
- package/dist/admin/hooks/useSelectionState.mjs +2 -4
- package/dist/admin/hooks/useSelectionState.mjs.map +1 -1
- package/dist/admin/shared/constants.js +43 -0
- package/dist/admin/shared/constants.js.map +1 -0
- package/dist/admin/shared/constants.mjs +38 -0
- package/dist/admin/shared/constants.mjs.map +1 -0
- package/dist/admin/src/future/hooks/useMediaLibraryPermissions.d.ts +21 -0
- package/dist/admin/src/future/pages/Assets/components/AssetDetails/AssetCropEditor.d.ts +7 -1
- package/dist/admin/src/future/pages/Assets/components/AssetsSearchInput.d.ts +5 -0
- package/dist/admin/src/future/pages/Assets/components/BulkActionsBar.d.ts +14 -1
- package/dist/admin/src/future/pages/Assets/components/DateRangeCalendar.d.ts +8 -0
- package/dist/admin/src/future/pages/Assets/components/DropZone/UploadDropZoneContext.d.ts +8 -1
- package/dist/admin/src/future/pages/Assets/components/EmptyState.d.ts +18 -1
- package/dist/admin/src/future/pages/Assets/components/FilterBadges.d.ts +6 -0
- package/dist/admin/src/future/pages/Assets/components/FilterMenu.d.ts +22 -0
- package/dist/admin/src/future/pages/Assets/components/FolderTree/FolderTree.d.ts +13 -4
- package/dist/admin/src/future/pages/Assets/components/FolderTree/filterFolderTree.d.ts +16 -0
- package/dist/admin/src/future/pages/Assets/components/SortMenu.d.ts +6 -0
- package/dist/admin/src/future/pages/Assets/hooks/useAssetSearch.d.ts +12 -0
- package/dist/admin/src/future/pages/Assets/hooks/useAssetSelection.d.ts +8 -1
- package/dist/admin/src/future/pages/Assets/hooks/useInfiniteAssets.d.ts +13 -3
- package/dist/admin/src/future/pages/Assets/hooks/useListFilters.d.ts +69 -0
- package/dist/admin/src/future/pages/Assets/utils/buildAssetFilters.d.ts +46 -0
- package/dist/admin/src/future/pages/Assets/utils/listQueryKey.d.ts +3 -4
- package/dist/admin/src/future/services/api.d.ts +53 -5
- package/dist/admin/src/future/services/assets.d.ts +9 -1
- package/dist/admin/src/future/services/folders.d.ts +3 -0
- package/dist/admin/src/future/services/settings.d.ts +2 -2
- package/dist/admin/src/future/store/uploadProgress.d.ts +83 -1
- package/dist/admin/src/future/utils/searchQueryParam.d.ts +7 -0
- package/dist/admin/src/hooks/useAiAvailability.d.ts +1 -1
- package/dist/admin/src/hooks/useEditAsset.d.ts +4 -4
- package/dist/admin/src/hooks/useEditFolder.d.ts +4 -4
- package/dist/admin/src/hooks/useSettings.d.ts +1 -0
- package/dist/admin/src/hooks/useTracking.d.ts +1 -1
- package/dist/admin/src/hooks/useUpload.d.ts +1 -1
- package/dist/admin/translations/en.json.js +53 -3
- package/dist/admin/translations/en.json.js.map +1 -1
- package/dist/admin/translations/en.json.mjs +53 -3
- package/dist/admin/translations/en.json.mjs.map +1 -1
- package/dist/admin/utils/urlYupSchema.js +33 -9
- package/dist/admin/utils/urlYupSchema.js.map +1 -1
- package/dist/admin/utils/urlYupSchema.mjs +33 -9
- package/dist/admin/utils/urlYupSchema.mjs.map +1 -1
- package/dist/server/config.js +13 -5
- package/dist/server/config.js.map +1 -1
- package/dist/server/config.mjs +13 -5
- package/dist/server/config.mjs.map +1 -1
- package/dist/server/constants.js.map +1 -1
- package/dist/server/constants.mjs.map +1 -1
- package/dist/server/controllers/admin-file.js +30 -0
- package/dist/server/controllers/admin-file.js.map +1 -1
- package/dist/server/controllers/admin-file.mjs +30 -0
- package/dist/server/controllers/admin-file.mjs.map +1 -1
- package/dist/server/controllers/admin-settings.js +20 -2
- package/dist/server/controllers/admin-settings.js.map +1 -1
- package/dist/server/controllers/admin-settings.mjs +21 -3
- package/dist/server/controllers/admin-settings.mjs.map +1 -1
- package/dist/server/controllers/content-api.js +1 -2
- package/dist/server/controllers/content-api.js.map +1 -1
- package/dist/server/controllers/content-api.mjs +2 -2
- package/dist/server/controllers/content-api.mjs.map +1 -1
- package/dist/server/controllers/validation/admin/ai-metadata.js +12 -0
- package/dist/server/controllers/validation/admin/ai-metadata.js.map +1 -0
- package/dist/server/controllers/validation/admin/ai-metadata.mjs +10 -0
- package/dist/server/controllers/validation/admin/ai-metadata.mjs.map +1 -0
- package/dist/server/controllers/validation/admin/settings.js +4 -1
- package/dist/server/controllers/validation/admin/settings.js.map +1 -1
- package/dist/server/controllers/validation/admin/settings.mjs +1 -1
- package/dist/server/controllers/validation/admin/settings.mjs.map +1 -1
- package/dist/server/routes/admin.js +18 -0
- package/dist/server/routes/admin.js.map +1 -1
- package/dist/server/routes/admin.mjs +18 -0
- package/dist/server/routes/admin.mjs.map +1 -1
- package/dist/server/services/ai-metadata.js +108 -11
- package/dist/server/services/ai-metadata.js.map +1 -1
- package/dist/server/services/ai-metadata.mjs +108 -11
- package/dist/server/services/ai-metadata.mjs.map +1 -1
- package/dist/server/services/file.js +86 -13
- package/dist/server/services/file.js.map +1 -1
- package/dist/server/services/file.mjs +87 -14
- package/dist/server/services/file.mjs.map +1 -1
- package/dist/server/services/image-manipulation.js +4 -4
- package/dist/server/services/image-manipulation.js.map +1 -1
- package/dist/server/services/image-manipulation.mjs +4 -4
- package/dist/server/services/image-manipulation.mjs.map +1 -1
- package/dist/server/shared/constants.js +43 -0
- package/dist/server/shared/constants.js.map +1 -0
- package/dist/server/shared/constants.mjs +38 -0
- package/dist/server/shared/constants.mjs.map +1 -0
- package/dist/server/src/config.d.ts +2 -0
- package/dist/server/src/config.d.ts.map +1 -1
- package/dist/server/src/constants.d.ts +2 -1
- package/dist/server/src/constants.d.ts.map +1 -1
- package/dist/server/src/controllers/admin-file.d.ts +10 -0
- package/dist/server/src/controllers/admin-file.d.ts.map +1 -1
- package/dist/server/src/controllers/admin-settings.d.ts.map +1 -1
- package/dist/server/src/controllers/index.d.ts +1 -0
- package/dist/server/src/controllers/index.d.ts.map +1 -1
- package/dist/server/src/controllers/validation/admin/ai-metadata.d.ts +2 -0
- package/dist/server/src/controllers/validation/admin/ai-metadata.d.ts.map +1 -0
- package/dist/server/src/controllers/validation/admin/settings.d.ts +1 -1
- package/dist/server/src/controllers/validation/admin/settings.d.ts.map +1 -1
- package/dist/server/src/index.d.ts +7 -1
- package/dist/server/src/index.d.ts.map +1 -1
- package/dist/server/src/routes/admin.d.ts.map +1 -1
- package/dist/server/src/services/ai-metadata.d.ts +19 -0
- package/dist/server/src/services/ai-metadata.d.ts.map +1 -1
- package/dist/server/src/services/file.d.ts +12 -2
- package/dist/server/src/services/file.d.ts.map +1 -1
- package/dist/server/src/services/index.d.ts +4 -1
- package/dist/server/src/services/index.d.ts.map +1 -1
- package/dist/server/src/types.d.ts +3 -0
- package/dist/server/src/types.d.ts.map +1 -1
- package/dist/shared/constants.d.ts +35 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/contracts/files.d.ts +25 -0
- package/dist/shared/contracts/files.d.ts.map +1 -1
- package/dist/shared/contracts/settings.d.ts +7 -0
- 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
|
-
|
|
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":";;;;AAkCA,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,mBAAAA,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;QACAE,eAAAA,CAAAA,CAAgBrB,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;AACAmB,QAAAA,mBAAAA,CAAAA,CAAoBtB,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;AACA8B,QAAAA,cAAAA,CAAAA,CAAevB,KAAK,EAAA;AAClBA,YAAAA,KAAAA,CAAMV,WAAW,GAAG,CAACU,KAAAA,CAAMV,WAAW;AACxC,QAAA,CAAA;AACAkC,QAAAA,YAAAA,CAAAA,CAAaxB,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;QACAW,eAAAA,CAAAA,CAAgBzB,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;AACAO,QAAAA,mBAAAA,CAAAA,CAAoB1B,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;AACjB,qBAAA;AACF,gBAAA;gBACA,OAAOI,IAAAA;AACT,YAAA,CAAA,CAAA;AACF,QAAA;AACF;AACF,CAAA,CAAA;AAEA;;;;;IAMO,MAAMa,uBAAAA,GAA0BC,sBAAAA,CACrC,CAAC5B,KAAAA,GAAqBA,KAAAA,CAAM6B,cAAc,CAACrC,KAAK,EAChD,CAACA,KAAAA,GAAAA;AACC,IAAA,IAAIA,KAAAA,CAAMsC,MAAM,KAAK,CAAA,EAAG,OAAO,CAAA;IAE/B,MAAMC,SAAAA,GAAYvC,KAAAA,CAAMwC,MAAM,CAAC,CAACC,KAAKC,CAAAA,GAAMD,GAAAA,GAAMC,CAAAA,CAAE1B,IAAI,EAAE,CAAA,CAAA;AAEzD,IAAA,IAAIuB,cAAc,CAAA,EAAG;AACnB,QAAA,MAAMI,UAAU3C,KAAAA,CAAM4C,MAAM,CAC1B,CAACF,CAAAA,GAAMA,EAAE3B,MAAM,KAAK,UAAA,IAAc2B,CAAAA,CAAE3B,MAAM,KAAK,OAAA,IAAW2B,EAAE3B,MAAM,KAAK,aACvEuB,MAAM;AACR,QAAA,OAAOf,KAAKsB,KAAK,CAAC,OAACF,GAAU3C,KAAAA,CAAMsC,MAAM,GAAI,GAAA,CAAA;AAC/C,IAAA;IAEA,MAAMpB,aAAAA,GAAgBlB,KAAAA,CAAMwC,MAAM,CAAC,CAACC,KAAKC,CAAAA,GAAMD,GAAAA,GAAMC,CAAAA,CAAExB,aAAa,EAAE,CAAA,CAAA;AACtE,IAAA,OAAOK,IAAAA,CAAKsB,KAAK,CAAE3B,gBAAgBqB,SAAAA,GAAa,GAAA,CAAA;AAClD,CAAA;AAGK,MAAM,EACXhC,kBAAkB,EAClBY,gBAAgB,EAChBC,eAAe,EACfK,eAAe,EACfC,YAAY,EACZG,eAAe,EACfC,mBAAmB,EACnBC,cAAc,EACdC,YAAY,EACZC,eAAe,EACfC,mBAAmB,EACpB,GAAG/B,mBAAAA,CAAoB2C;AAEjB,MAAMC,qBAAAA,GAAwB5C,mBAAAA,CAAoB6C;;;;;;;;;;;;;;;;"}
|
|
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
|
-
|
|
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":"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"}
|
|
@@ -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
|
-
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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;;;;;;;"}
|