@wordpress/media-utils 5.41.1-next.v.202603102151.0 → 5.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/components/media-upload-modal/index.cjs +136 -89
  3. package/build/components/media-upload-modal/index.cjs.map +3 -3
  4. package/build/components/media-upload-modal/upload-status-popover.cjs +156 -0
  5. package/build/components/media-upload-modal/upload-status-popover.cjs.map +7 -0
  6. package/build/components/media-upload-modal/use-invalidate-attachment-resolutions.cjs +45 -0
  7. package/build/components/media-upload-modal/use-invalidate-attachment-resolutions.cjs.map +7 -0
  8. package/build/components/media-upload-modal/use-upload-status.cjs +127 -0
  9. package/build/components/media-upload-modal/use-upload-status.cjs.map +7 -0
  10. package/build-module/components/media-upload-modal/index.mjs +129 -90
  11. package/build-module/components/media-upload-modal/index.mjs.map +2 -2
  12. package/build-module/components/media-upload-modal/upload-status-popover.mjs +131 -0
  13. package/build-module/components/media-upload-modal/upload-status-popover.mjs.map +7 -0
  14. package/build-module/components/media-upload-modal/use-invalidate-attachment-resolutions.mjs +20 -0
  15. package/build-module/components/media-upload-modal/use-invalidate-attachment-resolutions.mjs.map +7 -0
  16. package/build-module/components/media-upload-modal/use-upload-status.mjs +102 -0
  17. package/build-module/components/media-upload-modal/use-upload-status.mjs.map +7 -0
  18. package/build-style/style-rtl.css +73 -3
  19. package/build-style/style.css +73 -3
  20. package/build-types/components/media-upload-modal/index.d.ts.map +1 -1
  21. package/build-types/components/media-upload-modal/upload-status-popover.d.ts +15 -0
  22. package/build-types/components/media-upload-modal/upload-status-popover.d.ts.map +1 -0
  23. package/build-types/components/media-upload-modal/use-invalidate-attachment-resolutions.d.ts +22 -0
  24. package/build-types/components/media-upload-modal/use-invalidate-attachment-resolutions.d.ts.map +1 -0
  25. package/build-types/components/media-upload-modal/use-upload-status.d.ts +41 -0
  26. package/build-types/components/media-upload-modal/use-upload-status.d.ts.map +1 -0
  27. package/package.json +17 -15
  28. package/src/components/media-upload-modal/index.tsx +131 -108
  29. package/src/components/media-upload-modal/style.scss +88 -3
  30. package/src/components/media-upload-modal/test/use-upload-status.test.ts +501 -0
  31. package/src/components/media-upload-modal/upload-status-popover.tsx +155 -0
  32. package/src/components/media-upload-modal/use-invalidate-attachment-resolutions.ts +50 -0
  33. package/src/components/media-upload-modal/use-upload-status.ts +203 -0
@@ -0,0 +1,15 @@
1
+ export interface UploadingFile {
2
+ id: string;
3
+ batchId: string;
4
+ name: string;
5
+ status: 'uploading' | 'uploaded' | 'error';
6
+ error?: string;
7
+ }
8
+ interface UploadStatusPopoverProps {
9
+ uploadingFiles: UploadingFile[];
10
+ onDismissError?: (fileId: string) => void;
11
+ onOpenChange?: (open: boolean) => void;
12
+ }
13
+ export declare function UploadStatusPopover({ uploadingFiles, onDismissError, onOpenChange, }: UploadStatusPopoverProps): import("react").JSX.Element | null;
14
+ export {};
15
+ //# sourceMappingURL=upload-status-popover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-status-popover.d.ts","sourceRoot":"","sources":["../../../src/components/media-upload-modal/upload-status-popover.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,UAAU,wBAAwB;IACjC,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,cAAc,CAAC,EAAE,CAAE,MAAM,EAAE,MAAM,KAAM,IAAI,CAAC;IAC5C,YAAY,CAAC,EAAE,CAAE,IAAI,EAAE,OAAO,KAAM,IAAI,CAAC;CACzC;AAED,wBAAgB,mBAAmB,CAAE,EACpC,cAAc,EACd,cAAc,EACd,YAAY,GACZ,EAAE,wBAAwB,sCAgI1B"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Hook for invalidating all cached `getEntityRecords` resolutions for the
3
+ * attachment post type.
4
+ *
5
+ * After a file upload completes the media grid needs to refresh, but
6
+ * `invalidateResolution` only clears the exact query that is passed to it.
7
+ * If the user is on page 2, page 1 (where the new upload would appear) stays
8
+ * stale. Using `invalidateResolutionForStoreSelector` would work but is too
9
+ * broad — it clears every `getEntityRecords` resolution, potentially
10
+ * triggering unnecessary refetches for unrelated entity types.
11
+ *
12
+ * This hook provides a middle ground: it iterates over every cached
13
+ * resolution for `getEntityRecords` and invalidates only the entries where
14
+ * the first two arguments match `['postType', 'attachment']`.
15
+ */
16
+ /**
17
+ * Returns a stable callback that invalidates all cached `getEntityRecords`
18
+ * resolutions for `postType / attachment`, leaving every other entity type
19
+ * untouched.
20
+ */
21
+ export declare function useInvalidateAttachmentResolutions(): () => void;
22
+ //# sourceMappingURL=use-invalidate-attachment-resolutions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-invalidate-attachment-resolutions.d.ts","sourceRoot":"","sources":["../../../src/components/media-upload-modal/use-invalidate-attachment-resolutions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH;;;;GAIG;AACH,wBAAgB,kCAAkC,eAqBjD"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Hook for tracking media upload status with batch-scoped callbacks.
3
+ *
4
+ * This is a transitional layer that manually tracks upload progress using
5
+ * local state. The @wordpress/upload-media package provides a Redux-based
6
+ * store with richer capabilities (per-file progress, pause/resume, retry,
7
+ * concurrency control, client-side processing). When the media upload modal
8
+ * adopts @wordpress/upload-media, this hook can be replaced by selectors
9
+ * from that store (getItems, isBatchUploaded, getItemProgress, etc.) while
10
+ * keeping the same return interface.
11
+ */
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import type { Attachment } from '../../utils/types';
16
+ import type { UploadingFile } from './upload-status-popover';
17
+ interface UseUploadStatusOptions {
18
+ onBatchComplete?: (attachments: Partial<Attachment>[]) => void;
19
+ }
20
+ interface RegisterBatchResult {
21
+ onFileChange: (attachments: Partial<Attachment>[]) => void;
22
+ onError: (error: Error) => void;
23
+ }
24
+ interface UseUploadStatusReturn {
25
+ /** Current list of all tracked files. */
26
+ uploadingFiles: UploadingFile[];
27
+ /**
28
+ * Register a new batch of files for tracking.
29
+ * Returns batch-scoped onFileChange and onError callbacks.
30
+ */
31
+ registerBatch: (files: File[]) => RegisterBatchResult;
32
+ /** Remove a single error entry by file id. */
33
+ dismissError: (fileId: string) => void;
34
+ /** Remove all uploaded (completed) entries from the list. */
35
+ clearCompleted: () => void;
36
+ /** True when tracked entries exist but none are still uploading. */
37
+ allComplete: boolean;
38
+ }
39
+ export declare function useUploadStatus({ onBatchComplete, }?: UseUploadStatusOptions): UseUploadStatusReturn;
40
+ export {};
41
+ //# sourceMappingURL=use-upload-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-upload-status.d.ts","sourceRoot":"","sources":["../../../src/components/media-upload-modal/use-upload-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH;;GAEG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAK7D,UAAU,sBAAsB;IAC/B,eAAe,CAAC,EAAE,CAAE,WAAW,EAAE,OAAO,CAAE,UAAU,CAAE,EAAE,KAAM,IAAI,CAAC;CACnE;AAED,UAAU,mBAAmB;IAC5B,YAAY,EAAE,CAAE,WAAW,EAAE,OAAO,CAAE,UAAU,CAAE,EAAE,KAAM,IAAI,CAAC;IAC/D,OAAO,EAAE,CAAE,KAAK,EAAE,KAAK,KAAM,IAAI,CAAC;CAClC;AAED,UAAU,qBAAqB;IAC9B,yCAAyC;IACzC,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC;;;OAGG;IACH,aAAa,EAAE,CAAE,KAAK,EAAE,IAAI,EAAE,KAAM,mBAAmB,CAAC;IACxD,8CAA8C;IAC9C,YAAY,EAAE,CAAE,MAAM,EAAE,MAAM,KAAM,IAAI,CAAC;IACzC,6DAA6D;IAC7D,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,oEAAoE;IACpE,WAAW,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,eAAe,CAAE,EAChC,eAAe,GACf,GAAE,sBAA2B,GAAI,qBAAqB,CAmJtD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/media-utils",
3
- "version": "5.41.1-next.v.202603102151.0+59e17f9ec",
3
+ "version": "5.42.0",
4
4
  "description": "WordPress Media Upload Utils.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -47,19 +47,21 @@
47
47
  "build-style/**"
48
48
  ],
49
49
  "dependencies": {
50
- "@wordpress/api-fetch": "^7.41.1-next.v.202603102151.0+59e17f9ec",
51
- "@wordpress/base-styles": "^6.17.1-next.v.202603102151.0+59e17f9ec",
52
- "@wordpress/blob": "^4.41.1-next.v.202603102151.0+59e17f9ec",
53
- "@wordpress/components": "^32.4.1-next.v.202603102151.0+59e17f9ec",
54
- "@wordpress/core-data": "^7.41.2-next.v.202603102151.0+59e17f9ec",
55
- "@wordpress/data": "^10.41.1-next.v.202603102151.0+59e17f9ec",
56
- "@wordpress/dataviews": "^13.1.1-next.v.202603102151.0+59e17f9ec",
57
- "@wordpress/element": "^6.41.1-next.v.202603102151.0+59e17f9ec",
58
- "@wordpress/i18n": "^6.14.1-next.v.202603102151.0+59e17f9ec",
59
- "@wordpress/icons": "^12.0.1-next.v.202603102151.0+59e17f9ec",
60
- "@wordpress/media-fields": "^0.6.1-next.v.202603102151.0+59e17f9ec",
61
- "@wordpress/notices": "^5.41.1-next.v.202603102151.0+59e17f9ec",
62
- "@wordpress/private-apis": "^1.41.1-next.v.202603102151.0+59e17f9ec"
50
+ "@wordpress/api-fetch": "^7.42.0",
51
+ "@wordpress/base-styles": "^6.18.0",
52
+ "@wordpress/blob": "^4.42.0",
53
+ "@wordpress/components": "^32.4.0",
54
+ "@wordpress/core-data": "^7.42.0",
55
+ "@wordpress/data": "^10.42.0",
56
+ "@wordpress/dataviews": "^13.1.0",
57
+ "@wordpress/element": "^6.42.0",
58
+ "@wordpress/i18n": "^6.15.0",
59
+ "@wordpress/icons": "^12.0.0",
60
+ "@wordpress/media-fields": "^0.7.0",
61
+ "@wordpress/notices": "^5.42.0",
62
+ "@wordpress/private-apis": "^1.42.0",
63
+ "@wordpress/ui": "^0.9.0",
64
+ "clsx": "^2.1.1"
63
65
  },
64
66
  "peerDependencies": {
65
67
  "react": "^18.0.0"
@@ -67,5 +69,5 @@
67
69
  "publishConfig": {
68
70
  "access": "public"
69
71
  },
70
- "gitHead": "86db21e727d89e8f0dbba9300d2f97fd22b08693"
72
+ "gitHead": "c20787b1778ae64c2db65643b1c236309d68e6ba"
71
73
  }
@@ -1,3 +1,8 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import clsx from 'clsx';
5
+
1
6
  /**
2
7
  * WordPress dependencies
3
8
  */
@@ -6,6 +11,8 @@ import {
6
11
  useState,
7
12
  useCallback,
8
13
  useMemo,
14
+ useRef,
15
+ useEffect,
9
16
  } from '@wordpress/element';
10
17
  import { __, sprintf, _n } from '@wordpress/i18n';
11
18
  import {
@@ -17,6 +24,7 @@ import { Modal, DropZone, FormFileUpload, Button } from '@wordpress/components';
17
24
  import { upload as uploadIcon } from '@wordpress/icons';
18
25
  import { DataViewsPicker } from '@wordpress/dataviews';
19
26
  import type { View, Field, ActionButton } from '@wordpress/dataviews';
27
+ import { Stack } from '@wordpress/ui';
20
28
  import {
21
29
  altTextField,
22
30
  attachedToField,
@@ -32,7 +40,6 @@ import {
32
40
  mimeTypeField,
33
41
  } from '@wordpress/media-fields';
34
42
  import { store as noticesStore, SnackbarNotices } from '@wordpress/notices';
35
- import { isBlobURL } from '@wordpress/blob';
36
43
 
37
44
  /**
38
45
  * Internal dependencies
@@ -41,6 +48,9 @@ import type { Attachment, RestAttachment } from '../../utils/types';
41
48
  import { transformAttachment } from '../../utils/transform-attachment';
42
49
  import { uploadMedia } from '../../utils/upload-media';
43
50
  import { unlock } from '../../lock-unlock';
51
+ import { UploadStatusPopover } from './upload-status-popover';
52
+ import { useInvalidateAttachmentResolutions } from './use-invalidate-attachment-resolutions';
53
+ import { useUploadStatus } from './use-upload-status';
44
54
 
45
55
  const { useEntityRecordsWithPermissions } = unlock( coreDataPrivateApis );
46
56
 
@@ -173,9 +183,10 @@ export function MediaUploadModal( {
173
183
  : [ String( value ) ];
174
184
  } );
175
185
 
176
- const { createSuccessNotice, createErrorNotice, createInfoNotice } =
186
+ const { createSuccessNotice, removeAllNotices } =
177
187
  useDispatch( noticesStore );
178
- const { invalidateResolution } = useDispatch( coreStore );
188
+ const invalidateAttachmentResolutions =
189
+ useInvalidateAttachmentResolutions();
179
190
 
180
191
  // DataViews configuration - allow view updates
181
192
  const [ view, setView ] = useState< View >( () => ( {
@@ -186,10 +197,11 @@ export function MediaUploadModal( {
186
197
  mediaField: 'media_thumbnail',
187
198
  search: '',
188
199
  page: 1,
189
- perPage: 20,
200
+ perPage: 50,
190
201
  filters: [],
191
202
  layout: {
192
203
  previewSize: 170,
204
+ density: 'compact',
193
205
  },
194
206
  } ) );
195
207
 
@@ -243,6 +255,51 @@ export function MediaUploadModal( {
243
255
  };
244
256
  }, [ view, allowedTypes ] );
245
257
 
258
+ // Per-batch completion handler: auto-select uploaded items and refresh the grid.
259
+ const handleBatchComplete = useCallback(
260
+ ( attachments: Partial< Attachment >[] ) => {
261
+ const uploadedIds = attachments
262
+ .map( ( attachment ) => String( attachment.id ) )
263
+ .filter( Boolean );
264
+
265
+ if ( multiple ) {
266
+ setSelection( ( prev ) => {
267
+ const existing = new Set( prev );
268
+ const newIds = uploadedIds.filter(
269
+ ( id ) => ! existing.has( id )
270
+ );
271
+ return [ ...prev, ...newIds ];
272
+ } );
273
+ } else {
274
+ setSelection( uploadedIds.slice( 0, 1 ) );
275
+ }
276
+
277
+ // Invalidate all cached attachment queries so every page of
278
+ // results refreshes — not just the page the user is viewing.
279
+ invalidateAttachmentResolutions();
280
+ },
281
+ [ multiple, invalidateAttachmentResolutions ]
282
+ );
283
+
284
+ const {
285
+ uploadingFiles,
286
+ registerBatch,
287
+ dismissError,
288
+ clearCompleted,
289
+ allComplete,
290
+ } = useUploadStatus( { onBatchComplete: handleBatchComplete } );
291
+
292
+ const isPopoverOpenRef = useRef( false );
293
+ const handlePopoverOpenChange = useCallback(
294
+ ( open: boolean ) => {
295
+ isPopoverOpenRef.current = open;
296
+ if ( ! open ) {
297
+ clearCompleted();
298
+ }
299
+ },
300
+ [ clearCompleted ]
301
+ );
302
+
246
303
  // Fetch all media attachments using WordPress core data with permissions
247
304
  const {
248
305
  records: mediaRecords,
@@ -287,7 +344,7 @@ export function MediaUploadModal( {
287
344
  () => [
288
345
  {
289
346
  id: 'select',
290
- label: multiple ? __( 'Select' ) : __( 'Select' ),
347
+ label: __( 'Select' ),
291
348
  isPrimary: true,
292
349
  supportsBulk: multiple,
293
350
  async callback() {
@@ -317,42 +374,39 @@ export function MediaUploadModal( {
317
374
  ? transformedPosts
318
375
  : transformedPosts?.[ 0 ];
319
376
 
377
+ removeAllNotices( 'snackbar', NOTICES_CONTEXT );
320
378
  onSelect( selectedItems );
321
379
  },
322
380
  },
323
381
  ],
324
- [ multiple, onSelect, selection ]
382
+ [ multiple, onSelect, selection, removeAllNotices ]
325
383
  );
326
384
 
327
385
  const handleModalClose = useCallback( () => {
386
+ removeAllNotices( 'snackbar', NOTICES_CONTEXT );
328
387
  onClose?.();
329
- }, [ onClose ] );
388
+ }, [ removeAllNotices, onClose ] );
330
389
 
331
390
  // Use onUpload if provided, otherwise fall back to uploadMedia
332
391
  const handleUpload = onUpload || uploadMedia;
333
392
 
334
- // Shared upload success handler
335
- const handleUploadComplete = useCallback(
336
- ( attachments: Partial< Attachment >[] ) => {
337
- // Check if all uploads are complete (no blob URLs)
338
- const allComplete = attachments.every(
339
- ( attachment ) =>
340
- attachment.id &&
341
- attachment.url &&
342
- ! isBlobURL( attachment.url )
343
- );
344
-
345
- if ( allComplete && attachments.length > 0 ) {
346
- // Show success notice (replaces progress notice via ID)
393
+ // Show success notice and auto-clear completed entries when all batches finish.
394
+ const prevAllCompleteRef = useRef( false );
395
+ useEffect( () => {
396
+ if ( allComplete && ! prevAllCompleteRef.current ) {
397
+ const completeCount = uploadingFiles.filter(
398
+ ( file ) => file.status === 'uploaded'
399
+ ).length;
400
+ if ( completeCount > 0 ) {
347
401
  createSuccessNotice(
348
402
  sprintf(
349
403
  // translators: %s: number of files
350
404
  _n(
351
405
  'Uploaded %s file',
352
406
  'Uploaded %s files',
353
- attachments.length
407
+ completeCount
354
408
  ),
355
- attachments.length.toLocaleString()
409
+ completeCount.toLocaleString()
356
410
  ),
357
411
  {
358
412
  type: 'snackbar',
@@ -360,84 +414,33 @@ export function MediaUploadModal( {
360
414
  id: NOTICE_ID_UPLOAD_PROGRESS,
361
415
  }
362
416
  );
363
-
364
- // Auto-select the newly uploaded items
365
- const uploadedIds = attachments
366
- .map( ( attachment ) => String( attachment.id ) )
367
- .filter( Boolean );
368
-
369
- if ( multiple ) {
370
- // In multiple mode, add to existing selection
371
- setSelection( ( prev ) => [ ...prev, ...uploadedIds ] );
372
- } else {
373
- // In single mode, replace selection with the first uploaded item
374
- setSelection( uploadedIds.slice( 0, 1 ) );
375
- }
376
-
377
- // Invalidate the entity records resolution to refresh the view
378
- invalidateResolution( 'getEntityRecords', [
379
- 'postType',
380
- 'attachment',
381
- queryArgs,
382
- ] );
383
417
  }
384
- },
385
- [ createSuccessNotice, invalidateResolution, queryArgs, multiple ]
386
- );
387
418
 
388
- // Shared upload error handler
389
- const handleUploadError = useCallback(
390
- ( error: Error ) => {
391
- // Show error notice (replaces progress notice via ID)
392
- createErrorNotice( error.message, {
393
- type: 'snackbar',
394
- context: NOTICES_CONTEXT,
395
- id: NOTICE_ID_UPLOAD_PROGRESS,
396
- } );
397
- },
398
- [ createErrorNotice ]
399
- );
419
+ // Auto-clear completed entries, unless the popover is
420
+ // open in that case, they'll be cleared on close.
421
+ if ( ! isPopoverOpenRef.current ) {
422
+ clearCompleted();
423
+ }
424
+ }
425
+ prevAllCompleteRef.current = allComplete;
426
+ }, [ allComplete, uploadingFiles, createSuccessNotice, clearCompleted ] );
400
427
 
401
428
  const handleFileSelect = useCallback(
402
429
  ( event: React.ChangeEvent< HTMLInputElement > ) => {
403
430
  const files = event.target.files;
404
431
  if ( files && files.length > 0 ) {
405
432
  const filesArray = Array.from( files );
406
-
407
- // Show upload start notice
408
- createInfoNotice(
409
- sprintf(
410
- // translators: %s: number of files
411
- _n(
412
- 'Uploading %s file',
413
- 'Uploading %s files',
414
- filesArray.length
415
- ),
416
- filesArray.length.toLocaleString()
417
- ),
418
- {
419
- type: 'snackbar',
420
- context: NOTICES_CONTEXT,
421
- id: NOTICE_ID_UPLOAD_PROGRESS,
422
- explicitDismiss: true,
423
- }
424
- );
433
+ const { onFileChange, onError } = registerBatch( filesArray );
425
434
 
426
435
  handleUpload( {
427
436
  allowedTypes,
428
437
  filesList: filesArray,
429
- onFileChange: handleUploadComplete,
430
- onError: handleUploadError,
438
+ onFileChange,
439
+ onError,
431
440
  } );
432
441
  }
433
442
  },
434
- [
435
- allowedTypes,
436
- handleUpload,
437
- createInfoNotice,
438
- handleUploadComplete,
439
- handleUploadError,
440
- ]
443
+ [ allowedTypes, handleUpload, registerBatch ]
441
444
  );
442
445
 
443
446
  const paginationInfo = useMemo(
@@ -524,30 +527,14 @@ export function MediaUploadModal( {
524
527
  );
525
528
  }
526
529
  if ( filteredFiles.length > 0 ) {
527
- // Show upload start notice
528
- createInfoNotice(
529
- sprintf(
530
- // translators: %s: number of files
531
- _n(
532
- 'Uploading %s file',
533
- 'Uploading %s files',
534
- filteredFiles.length
535
- ),
536
- filteredFiles.length.toLocaleString()
537
- ),
538
- {
539
- type: 'snackbar',
540
- context: NOTICES_CONTEXT,
541
- id: NOTICE_ID_UPLOAD_PROGRESS,
542
- explicitDismiss: true,
543
- }
544
- );
530
+ const { onFileChange, onError } =
531
+ registerBatch( filteredFiles );
545
532
 
546
533
  handleUpload( {
547
534
  allowedTypes,
548
535
  filesList: filteredFiles,
549
- onFileChange: handleUploadComplete,
550
- onError: handleUploadError,
536
+ onFileChange,
537
+ onError,
551
538
  } );
552
539
  }
553
540
  } }
@@ -565,10 +552,46 @@ export function MediaUploadModal( {
565
552
  paginationInfo={ paginationInfo }
566
553
  defaultLayouts={ defaultLayouts }
567
554
  getItemId={ ( item: RestAttachment ) => String( item.id ) }
568
- search={ search }
569
- searchLabel={ searchLabel }
570
555
  itemListLabel={ __( 'Media items' ) }
571
- />
556
+ >
557
+ <Stack
558
+ direction="row"
559
+ align="top"
560
+ justify="space-between"
561
+ className="dataviews__view-actions"
562
+ gap="xs"
563
+ >
564
+ <Stack
565
+ direction="row"
566
+ gap="sm"
567
+ justify="start"
568
+ className="dataviews__search"
569
+ >
570
+ { search && (
571
+ <DataViewsPicker.Search label={ searchLabel } />
572
+ ) }
573
+ <DataViewsPicker.FiltersToggle />
574
+ </Stack>
575
+ <Stack direction="row" gap="xs" style={ { flexShrink: 0 } }>
576
+ <DataViewsPicker.LayoutSwitcher />
577
+ <DataViewsPicker.ViewConfig />
578
+ </Stack>
579
+ </Stack>
580
+ <DataViewsPicker.FiltersToggled className="dataviews-filters__container" />
581
+ <DataViewsPicker.Layout />
582
+ <div
583
+ className={ clsx( 'media-upload-modal__footer', {
584
+ 'is-uploading': uploadingFiles.length > 0,
585
+ } ) }
586
+ >
587
+ <UploadStatusPopover
588
+ uploadingFiles={ uploadingFiles }
589
+ onDismissError={ dismissError }
590
+ onOpenChange={ handlePopoverOpenChange }
591
+ />
592
+ <DataViewsPicker.BulkActionToolbar />
593
+ </div>
594
+ </DataViewsPicker>
572
595
  { createPortal(
573
596
  <SnackbarNotices
574
597
  className="media-upload-modal__snackbar"
@@ -1,4 +1,5 @@
1
1
  @use "@wordpress/base-styles/variables" as *;
2
+ @use "@wordpress/base-styles/colors" as *;
2
3
  @use "@wordpress/base-styles/mixins" as *;
3
4
 
4
5
  .media-upload-modal {
@@ -7,7 +8,7 @@
7
8
  }
8
9
 
9
10
  .components-modal__frame.is-full-screen .components-modal__content {
10
- margin-bottom: $grid-unit-20;
11
+ margin-bottom: 0;
11
12
  }
12
13
 
13
14
  .components-modal__content {
@@ -18,8 +19,41 @@
18
19
  padding-top: $grid-unit-10;
19
20
  }
20
21
 
21
- .dataviews-footer {
22
- padding-bottom: $grid-unit-10;
22
+ .media-upload-modal__footer {
23
+ position: sticky;
24
+ bottom: 0;
25
+ background-color: inherit;
26
+ // Match the z-index the footer normally has, since we take over sticky.
27
+ z-index: 2;
28
+
29
+ &.is-uploading .dataviews-picker-footer__bulk-selection {
30
+ visibility: hidden;
31
+ }
32
+
33
+ // The inner footer no longer needs sticky/z-index since the wrapper handles it.
34
+ .dataviews-footer {
35
+ position: static;
36
+ z-index: auto;
37
+ }
38
+ }
39
+
40
+ .media-upload-modal__upload-status {
41
+ position: absolute;
42
+ display: flex;
43
+ align-items: center;
44
+ gap: $grid-unit-10;
45
+ background-color: var(--wp-dataviews-color-background, $white);
46
+ // Match the footer's padding so the trigger aligns with the item count.
47
+ left: $grid-unit-30;
48
+ top: 1px;
49
+ bottom: 1px;
50
+ z-index: 1;
51
+
52
+ .components-spinner {
53
+ width: $grid-unit-20;
54
+ height: $grid-unit-20;
55
+ margin: 0;
56
+ }
23
57
  }
24
58
  }
25
59
 
@@ -38,3 +72,54 @@
38
72
  transform-origin: 0 !important;
39
73
  }
40
74
  }
75
+
76
+ .media-upload-modal__upload-status__popover {
77
+ .components-popover__content {
78
+ width: 320px;
79
+ }
80
+
81
+ .media-upload-modal__upload-status__header {
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: space-between;
85
+ padding: $grid-unit-15 $grid-unit-20;
86
+ h3 {
87
+ margin: 0;
88
+ font-size: 13px;
89
+ font-weight: $font-weight-medium;
90
+ }
91
+ }
92
+
93
+ .media-upload-modal__upload-status__list {
94
+ max-height: 200px;
95
+ overflow-y: auto;
96
+ margin: 0;
97
+ padding: 0 0 $grid-unit-05;
98
+ list-style: none;
99
+ }
100
+
101
+ .media-upload-modal__upload-status__item {
102
+ display: flex;
103
+ align-items: flex-start;
104
+ margin-bottom: 0;
105
+ justify-content: space-between;
106
+ gap: $grid-unit-10;
107
+ padding: $grid-unit-10 $grid-unit-20;
108
+
109
+ .components-spinner {
110
+ flex-shrink: 0;
111
+ width: $grid-unit-20;
112
+ height: $grid-unit-20;
113
+ margin: 0;
114
+ }
115
+ }
116
+
117
+ .media-upload-modal__upload-status__filename {
118
+ overflow: hidden;
119
+ text-overflow: ellipsis;
120
+ white-space: nowrap;
121
+ min-width: 0;
122
+ flex: 1;
123
+ font-size: 12px;
124
+ }
125
+ }