@wordpress/media-utils 5.41.1-next.v.202603161435.0 → 5.43.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 +4 -0
  2. package/build/components/media-upload-modal/index.cjs +133 -87
  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 +126 -88
  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 +129 -107
  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.202603161435.0+ab4981c4f",
3
+ "version": "5.43.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.202603161435.0+ab4981c4f",
51
- "@wordpress/base-styles": "^6.17.1-next.v.202603161435.0+ab4981c4f",
52
- "@wordpress/blob": "^4.41.1-next.v.202603161435.0+ab4981c4f",
53
- "@wordpress/components": "^32.4.1-next.v.202603161435.0+ab4981c4f",
54
- "@wordpress/core-data": "^7.41.2-next.v.202603161435.0+ab4981c4f",
55
- "@wordpress/data": "^10.41.1-next.v.202603161435.0+ab4981c4f",
56
- "@wordpress/dataviews": "^13.1.1-next.v.202603161435.0+ab4981c4f",
57
- "@wordpress/element": "^6.41.1-next.v.202603161435.0+ab4981c4f",
58
- "@wordpress/i18n": "^6.14.1-next.v.202603161435.0+ab4981c4f",
59
- "@wordpress/icons": "^12.0.1-next.v.202603161435.0+ab4981c4f",
60
- "@wordpress/media-fields": "^0.6.1-next.v.202603161435.0+ab4981c4f",
61
- "@wordpress/notices": "^5.41.1-next.v.202603161435.0+ab4981c4f",
62
- "@wordpress/private-apis": "^1.41.1-next.v.202603161435.0+ab4981c4f"
50
+ "@wordpress/api-fetch": "^7.43.0",
51
+ "@wordpress/base-styles": "^6.19.0",
52
+ "@wordpress/blob": "^4.43.0",
53
+ "@wordpress/components": "^32.5.0",
54
+ "@wordpress/core-data": "^7.43.0",
55
+ "@wordpress/data": "^10.43.0",
56
+ "@wordpress/dataviews": "^14.0.0",
57
+ "@wordpress/element": "^6.43.0",
58
+ "@wordpress/i18n": "^6.16.0",
59
+ "@wordpress/icons": "^12.1.0",
60
+ "@wordpress/media-fields": "^0.8.0",
61
+ "@wordpress/notices": "^5.43.0",
62
+ "@wordpress/private-apis": "^1.43.0",
63
+ "@wordpress/ui": "^0.10.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": "748f4e4564fcc0e6ae90200d90bb993a3cef5828"
72
+ "gitHead": "2cea90674d11aa521ec3f71652fb3a6a4c383969"
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 >( () => ( {
@@ -244,6 +255,51 @@ export function MediaUploadModal( {
244
255
  };
245
256
  }, [ view, allowedTypes ] );
246
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
+
247
303
  // Fetch all media attachments using WordPress core data with permissions
248
304
  const {
249
305
  records: mediaRecords,
@@ -288,7 +344,7 @@ export function MediaUploadModal( {
288
344
  () => [
289
345
  {
290
346
  id: 'select',
291
- label: multiple ? __( 'Select' ) : __( 'Select' ),
347
+ label: __( 'Select' ),
292
348
  isPrimary: true,
293
349
  supportsBulk: multiple,
294
350
  async callback() {
@@ -318,42 +374,39 @@ export function MediaUploadModal( {
318
374
  ? transformedPosts
319
375
  : transformedPosts?.[ 0 ];
320
376
 
377
+ removeAllNotices( 'snackbar', NOTICES_CONTEXT );
321
378
  onSelect( selectedItems );
322
379
  },
323
380
  },
324
381
  ],
325
- [ multiple, onSelect, selection ]
382
+ [ multiple, onSelect, selection, removeAllNotices ]
326
383
  );
327
384
 
328
385
  const handleModalClose = useCallback( () => {
386
+ removeAllNotices( 'snackbar', NOTICES_CONTEXT );
329
387
  onClose?.();
330
- }, [ onClose ] );
388
+ }, [ removeAllNotices, onClose ] );
331
389
 
332
390
  // Use onUpload if provided, otherwise fall back to uploadMedia
333
391
  const handleUpload = onUpload || uploadMedia;
334
392
 
335
- // Shared upload success handler
336
- const handleUploadComplete = useCallback(
337
- ( attachments: Partial< Attachment >[] ) => {
338
- // Check if all uploads are complete (no blob URLs)
339
- const allComplete = attachments.every(
340
- ( attachment ) =>
341
- attachment.id &&
342
- attachment.url &&
343
- ! isBlobURL( attachment.url )
344
- );
345
-
346
- if ( allComplete && attachments.length > 0 ) {
347
- // 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 ) {
348
401
  createSuccessNotice(
349
402
  sprintf(
350
403
  // translators: %s: number of files
351
404
  _n(
352
405
  'Uploaded %s file',
353
406
  'Uploaded %s files',
354
- attachments.length
407
+ completeCount
355
408
  ),
356
- attachments.length.toLocaleString()
409
+ completeCount.toLocaleString()
357
410
  ),
358
411
  {
359
412
  type: 'snackbar',
@@ -361,84 +414,33 @@ export function MediaUploadModal( {
361
414
  id: NOTICE_ID_UPLOAD_PROGRESS,
362
415
  }
363
416
  );
364
-
365
- // Auto-select the newly uploaded items
366
- const uploadedIds = attachments
367
- .map( ( attachment ) => String( attachment.id ) )
368
- .filter( Boolean );
369
-
370
- if ( multiple ) {
371
- // In multiple mode, add to existing selection
372
- setSelection( ( prev ) => [ ...prev, ...uploadedIds ] );
373
- } else {
374
- // In single mode, replace selection with the first uploaded item
375
- setSelection( uploadedIds.slice( 0, 1 ) );
376
- }
377
-
378
- // Invalidate the entity records resolution to refresh the view
379
- invalidateResolution( 'getEntityRecords', [
380
- 'postType',
381
- 'attachment',
382
- queryArgs,
383
- ] );
384
417
  }
385
- },
386
- [ createSuccessNotice, invalidateResolution, queryArgs, multiple ]
387
- );
388
418
 
389
- // Shared upload error handler
390
- const handleUploadError = useCallback(
391
- ( error: Error ) => {
392
- // Show error notice (replaces progress notice via ID)
393
- createErrorNotice( error.message, {
394
- type: 'snackbar',
395
- context: NOTICES_CONTEXT,
396
- id: NOTICE_ID_UPLOAD_PROGRESS,
397
- } );
398
- },
399
- [ createErrorNotice ]
400
- );
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 ] );
401
427
 
402
428
  const handleFileSelect = useCallback(
403
429
  ( event: React.ChangeEvent< HTMLInputElement > ) => {
404
430
  const files = event.target.files;
405
431
  if ( files && files.length > 0 ) {
406
432
  const filesArray = Array.from( files );
407
-
408
- // Show upload start notice
409
- createInfoNotice(
410
- sprintf(
411
- // translators: %s: number of files
412
- _n(
413
- 'Uploading %s file',
414
- 'Uploading %s files',
415
- filesArray.length
416
- ),
417
- filesArray.length.toLocaleString()
418
- ),
419
- {
420
- type: 'snackbar',
421
- context: NOTICES_CONTEXT,
422
- id: NOTICE_ID_UPLOAD_PROGRESS,
423
- explicitDismiss: true,
424
- }
425
- );
433
+ const { onFileChange, onError } = registerBatch( filesArray );
426
434
 
427
435
  handleUpload( {
428
436
  allowedTypes,
429
437
  filesList: filesArray,
430
- onFileChange: handleUploadComplete,
431
- onError: handleUploadError,
438
+ onFileChange,
439
+ onError,
432
440
  } );
433
441
  }
434
442
  },
435
- [
436
- allowedTypes,
437
- handleUpload,
438
- createInfoNotice,
439
- handleUploadComplete,
440
- handleUploadError,
441
- ]
443
+ [ allowedTypes, handleUpload, registerBatch ]
442
444
  );
443
445
 
444
446
  const paginationInfo = useMemo(
@@ -525,30 +527,14 @@ export function MediaUploadModal( {
525
527
  );
526
528
  }
527
529
  if ( filteredFiles.length > 0 ) {
528
- // Show upload start notice
529
- createInfoNotice(
530
- sprintf(
531
- // translators: %s: number of files
532
- _n(
533
- 'Uploading %s file',
534
- 'Uploading %s files',
535
- filteredFiles.length
536
- ),
537
- filteredFiles.length.toLocaleString()
538
- ),
539
- {
540
- type: 'snackbar',
541
- context: NOTICES_CONTEXT,
542
- id: NOTICE_ID_UPLOAD_PROGRESS,
543
- explicitDismiss: true,
544
- }
545
- );
530
+ const { onFileChange, onError } =
531
+ registerBatch( filteredFiles );
546
532
 
547
533
  handleUpload( {
548
534
  allowedTypes,
549
535
  filesList: filteredFiles,
550
- onFileChange: handleUploadComplete,
551
- onError: handleUploadError,
536
+ onFileChange,
537
+ onError,
552
538
  } );
553
539
  }
554
540
  } }
@@ -566,10 +552,46 @@ export function MediaUploadModal( {
566
552
  paginationInfo={ paginationInfo }
567
553
  defaultLayouts={ defaultLayouts }
568
554
  getItemId={ ( item: RestAttachment ) => String( item.id ) }
569
- search={ search }
570
- searchLabel={ searchLabel }
571
555
  itemListLabel={ __( 'Media items' ) }
572
- />
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>
573
595
  { createPortal(
574
596
  <SnackbarNotices
575
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
+ }