Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a16c8fc
Add automatic retry logic and missing sizes check for upload-media
adamsilverstein Mar 23, 2026
e25c136
Replace auto-generation with user-initiated missing sizes UI
adamsilverstein Mar 23, 2026
796baa6
Add UI mockup images for PR documentation
adamsilverstein Mar 23, 2026
f030e89
Focus missing sizes UI on pre-publish panel
adamsilverstein Apr 3, 2026
c93d371
Fix lint: remove unused imports and add __next40pxDefaultSize prop
adamsilverstein Apr 4, 2026
713b416
Fix lint: remove extra blank lines in image editor.scss
adamsilverstein Apr 4, 2026
015f32c
Use apiFetch instead of getEntityRecord for missing sizes check
adamsilverstein Apr 6, 2026
290a041
Extract useMissingSizes hook for shared missing sizes detection
adamsilverstein Apr 6, 2026
d63954f
Refactor pre-publish missing sizes panel to use shared hook
adamsilverstein Apr 6, 2026
62ee8bf
Add missing image sizes warning in Post sidebar for published posts
adamsilverstein Apr 6, 2026
11ba991
Remove missing image sizes UI to focus PR on retry + network resilience
adamsilverstein Apr 21, 2026
6a22769
Merge branch 'trunk' into add/upload-media-retry-logic
adamsilverstein Apr 22, 2026
ef81973
Restore ItemStatus import lost in trunk merge
adamsilverstein Apr 22, 2026
951c46c
Expand test coverage for retry edge cases and queue pause/resume
adamsilverstein Apr 22, 2026
6d6f262
Add tests for useNetworkReconnect hook
adamsilverstein Apr 22, 2026
7e1af02
Apply review suggestions: add periods to inline comments
adamsilverstein Apr 22, 2026
259e99c
Merge branch 'trunk' into add/upload-media-retry-logic
adamsilverstein Apr 29, 2026
93a10a7
Skip retry execution while queue is paused
adamsilverstein Apr 29, 2026
af8f264
Upload Media: Add executeRetry to private ActionCreators type
adamsilverstein Apr 29, 2026
d808e6c
Upload Media: Remove unused retry-state selectors
adamsilverstein May 13, 2026
b39c524
Upload Media: Drop dormant ErrorCode/isRetryable scaffolding
adamsilverstein May 13, 2026
cceef41
Upload Media: Lock in non-retry of UploadError without transient message
adamsilverstein May 14, 2026
0f20f78
Upload Media: Enable retry by default in store initial state
adamsilverstein May 14, 2026
fa8fcd2
Merge remote-tracking branch 'origin/trunk' into add/upload-media-ret…
adamsilverstein May 18, 2026
537936a
Merge branch 'trunk' into add/upload-media-retry-logic
adamsilverstein May 20, 2026
c2a7fdb
Fix Prettier formatting on cancelItem retry guard
adamsilverstein May 20, 2026
8c35786
Upload Media: retry uploads that fail with a string error
adamsilverstein May 21, 2026
80c4705
Upload Media: add e2e tests for upload retry
adamsilverstein May 21, 2026
bd8807a
Upload Media: document network resilience in changelogs
adamsilverstein May 21, 2026
a635139
Merge remote-tracking branch 'origin/trunk' into add/upload-media-ret…
adamsilverstein May 27, 2026
07c36f4
Apply suggestion from @adamsilverstein
adamsilverstein Jun 4, 2026
43b7534
Merge remote-tracking branch 'origin/trunk' into add/upload-media-ret…
adamsilverstein Jun 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### Enhancements

- Editor: Add padding around inline notices in the editor content area and distraction-free header.
- Editor: Pause the client-side media upload queue while the browser is offline and resume it automatically when connectivity returns ([#76765](https://github.com/WordPress/gutenberg/pull/76765)).
- The Media Editor modal is now mounted unconditionally and the `openMediaEditorModal` setting is always provided to the block editor. Previously both were gated behind the `gutenberg-media-editor-modal` experiment, which has been removed.

### Bug Fixes
Expand Down
4 changes: 4 additions & 0 deletions packages/editor/src/components/provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { useHideBlocksFromInserter } from './use-hide-blocks-from-inserter';
import { useRevisionBlocks } from './use-revision-blocks';
import useCommands from '../commands';
import useUploadSaveLock from './use-upload-save-lock';
import useNetworkReconnect from './use-network-reconnect';
import BlockRemovalWarnings from '../block-removal-warnings';
import StartPageOptions from '../start-page-options';
import KeyboardShortcutHelpModal from '../keyboard-shortcut-help-modal';
Expand Down Expand Up @@ -403,6 +404,9 @@ export const ExperimentalEditorProvider = withRegistryProvider(
// Lock post saving when media uploads are in progress (experimental feature).
useUploadSaveLock();

// Pause/resume media upload queue on network disconnect/reconnect.
useNetworkReconnect();

if ( ! isReady || ! mode ) {
return null;
}
Expand Down
137 changes: 137 additions & 0 deletions packages/editor/src/components/provider/test/use-network-reconnect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* External dependencies
*/
import { renderHook } from '@testing-library/react';

/**
* WordPress dependencies
*/
import { useRegistry } from '@wordpress/data';

/**
* Internal dependencies
*/
import useNetworkReconnect from '../use-network-reconnect';

const mockPauseQueue = jest.fn();
const mockResumeQueue = jest.fn();

jest.mock( '@wordpress/data', () => ( {
useRegistry: jest.fn(),
} ) );

jest.mock( '@wordpress/upload-media', () => ( {
store: 'core/upload-media',
} ) );

jest.mock( '../../../lock-unlock', () => ( {
unlock: jest.fn( () => ( {
pauseQueue: mockPauseQueue,
resumeQueue: mockResumeQueue,
} ) ),
} ) );

describe( 'useNetworkReconnect', () => {
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
let listeners;

beforeEach( () => {
mockPauseQueue.mockClear();
mockResumeQueue.mockClear();
listeners = {};
window.addEventListener = jest.fn( ( event, cb ) => {
listeners[ event ] = cb;
} );
window.removeEventListener = jest.fn( ( event ) => {
delete listeners[ event ];
} );
useRegistry.mockReturnValue( {
dispatch: jest.fn( () => ( {} ) ),
} );
} );

afterEach( () => {
window.addEventListener = originalAddEventListener;
window.removeEventListener = originalRemoveEventListener;
delete window.__clientSideMediaProcessing;
} );

it( 'does nothing when client-side media processing is disabled', () => {
window.__clientSideMediaProcessing = false;
renderHook( () => useNetworkReconnect() );

expect( window.addEventListener ).not.toHaveBeenCalled();
} );

it( 'does nothing when the flag is undefined', () => {
renderHook( () => useNetworkReconnect() );

expect( window.addEventListener ).not.toHaveBeenCalled();
} );

it( 'registers offline and online listeners when enabled', () => {
window.__clientSideMediaProcessing = true;
renderHook( () => useNetworkReconnect() );

expect( window.addEventListener ).toHaveBeenCalledWith(
'offline',
expect.any( Function )
);
expect( window.addEventListener ).toHaveBeenCalledWith(
'online',
expect.any( Function )
);
} );

it( 'pauses the queue when the offline event fires', () => {
window.__clientSideMediaProcessing = true;
renderHook( () => useNetworkReconnect() );

listeners.offline();

expect( mockPauseQueue ).toHaveBeenCalledTimes( 1 );
expect( mockResumeQueue ).not.toHaveBeenCalled();
} );

it( 'resumes the queue when the online event fires', () => {
window.__clientSideMediaProcessing = true;
renderHook( () => useNetworkReconnect() );

listeners.online();

expect( mockResumeQueue ).toHaveBeenCalledTimes( 1 );
expect( mockPauseQueue ).not.toHaveBeenCalled();
} );

it( 'handles multiple offline/online cycles', () => {
window.__clientSideMediaProcessing = true;
renderHook( () => useNetworkReconnect() );

listeners.offline();
listeners.online();
listeners.offline();
listeners.online();

expect( mockPauseQueue ).toHaveBeenCalledTimes( 2 );
expect( mockResumeQueue ).toHaveBeenCalledTimes( 2 );
} );

it( 'removes the listeners on unmount', () => {
window.__clientSideMediaProcessing = true;
const { unmount } = renderHook( () => useNetworkReconnect() );

unmount();

expect( window.removeEventListener ).toHaveBeenCalledWith(
'offline',
expect.any( Function )
);
expect( window.removeEventListener ).toHaveBeenCalledWith(
'online',
expect.any( Function )
);
expect( listeners.offline ).toBeUndefined();
expect( listeners.online ).toBeUndefined();
} );
} );
44 changes: 44 additions & 0 deletions packages/editor/src/components/provider/use-network-reconnect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* WordPress dependencies
*/
import { useEffect } from '@wordpress/element';
import { useRegistry } from '@wordpress/data';
import { store as uploadStore } from '@wordpress/upload-media';

/**
* Internal dependencies
*/
import { unlock } from '../../lock-unlock';

/**
* A hook that pauses the media upload queue when the browser goes offline
* and resumes it when connectivity is restored.
*
* Only active when client-side media processing is enabled.
*/
export default function useNetworkReconnect() {
const isEnabled = window.__clientSideMediaProcessing;
const registry = useRegistry();

useEffect( () => {
if ( ! isEnabled ) {
return;
}

const handleOffline = () => {
unlock( registry.dispatch( uploadStore ) ).pauseQueue();
};

const handleOnline = () => {
unlock( registry.dispatch( uploadStore ) ).resumeQueue();
};

window.addEventListener( 'offline', handleOffline );
window.addEventListener( 'online', handleOnline );

return () => {
window.removeEventListener( 'offline', handleOffline );
window.removeEventListener( 'online', handleOnline );
};
}, [ isEnabled, registry ] );
}
4 changes: 4 additions & 0 deletions packages/upload-media/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Enhancement

- Automatically retry failed uploads with exponential backoff for transient (network/server) errors. Retry behavior is configurable via the `retry` store setting; non-transient failures and child sideloads are not retried. The upload queue can also be paused and resumed, allowing uploads to halt while the browser is offline and continue on reconnect ([#76765](https://github.com/WordPress/gutenberg/pull/76765)).

## 0.32.0 (2026-05-27)

### Bug Fix
Expand Down
23 changes: 23 additions & 0 deletions packages/upload-media/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,24 @@ _Parameters_

Cancels an item in the queue based on an error.

If the error is retryable and the item hasn't exceeded the maximum retry attempts, it will be scheduled for automatic retry instead of being cancelled.

_Parameters_

- _id_ `QueueItemId`: Item ID.
- _error_ `Error`: Error instance.
- _silent_ Whether to cancel the item silently, without invoking its `onError` callback.

#### executeRetry

Executes a scheduled retry for an item.

This is called by the timer set in scheduleRetry. It verifies the item is still in PendingRetry status before proceeding with the retry.

_Parameters_

- _id_ `QueueItemId`: Item ID.

#### retryItem

Retries a failed item in the queue.
Expand All @@ -75,6 +87,17 @@ _Parameters_

- _id_ `QueueItemId`: Item ID.

#### scheduleRetry

Schedules an automatic retry for a failed item.

Uses exponential backoff with jitter to determine the retry delay. The item will be placed in PendingRetry status and automatically retried after the calculated delay.

_Parameters_

- _id_ `QueueItemId`: Item ID.
- _error_ `Error`: The error that caused the failure.

<!-- END TOKEN(Autogenerated actions|src/store/actions.ts) -->

### Selectors
Expand Down
Loading
Loading