diff --git a/lib/load.php b/lib/load.php index 7076be29f63127..ce27d53d092091 100644 --- a/lib/load.php +++ b/lib/load.php @@ -64,3 +64,4 @@ function gutenberg_is_experiment_enabled( $name ) { require dirname( __FILE__ ) . '/experiments-page.php'; require dirname( __FILE__ ) . '/customizer.php'; require dirname( __FILE__ ) . '/edit-site-page.php'; +require dirname( __FILE__ ) . '/offline-editing.php'; diff --git a/lib/offline-editing.js b/lib/offline-editing.js new file mode 100644 index 00000000000000..1340c83047b079 --- /dev/null +++ b/lib/offline-editing.js @@ -0,0 +1,43 @@ +/* @todo This file should be moved to a better location. */ +/* global wp, fetch, caches, ERROR_OFFLINE_URL, ERROR_MESSAGES, ERROR_500_URL */ + +// IIFE is used for lexical scoping instead of just a braces block due to bug with const in Safari. +( () => { + const queue = new wp.serviceWorker.backgroundSync.Queue( 'gutenbergPendingEdits' ); + + const editPostHandler = ( { url, event } ) => { + const clone = event.request.clone(); + return fetch( event.request ) + .then( ( response ) => { + return response; + } ) + .catch( () => { + const bodyPromise = clone.blob(); + bodyPromise.then( + function( body ) { + const request = event.request; + const req = new Request( request.url, { + method: request.method, + headers: request.headers, + mode: 'same-origin', + credentials: request.credentials, + referrer: request.referrer, + redirect: 'manual', + body, + } ); + + // Add request to queue. + queue.pushRequest( { + request: req, + } ); + } + ); + } ); + }; + + wp.serviceWorker.routing.registerRoute( + /\/index\.php/, + editPostHandler, + 'POST' + ); +} )(); diff --git a/lib/offline-editing.php b/lib/offline-editing.php new file mode 100644 index 00000000000000..fb8d17ab66665b --- /dev/null +++ b/lib/offline-editing.php @@ -0,0 +1,17 @@ +register( + 'gutenberg-offline-editing', + array( + 'src' => plugin_dir_url( __FILE__ ) . 'offline-editing.js', + 'deps' => array( 'wp-base-config' ), + ) + ); +} + +add_action( 'wp_admin_service_worker', 'gutenberg_register_offline_editing_service_worker_script' ); diff --git a/packages/api-fetch/src/index.js b/packages/api-fetch/src/index.js index 535adb8d88a76a..66252ef843dd65 100644 --- a/packages/api-fetch/src/index.js +++ b/packages/api-fetch/src/index.js @@ -94,8 +94,8 @@ const defaultFetchHandler = ( nextOptions ) => { .then( ( response ) => parseResponseAndNormalizeError( response, parse ) ), () => { throw { - code: 'fetch_error', - message: __( 'You are probably offline.' ), + type: 'offline', + message: __( 'You do not have an internet connection, however, your changes will be synced once back online. Previewing and some editing is not available when offline.' ), }; } ); diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 66fbd292ec3403..d9cea321f0edf0 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -366,7 +366,7 @@ export function* saveEntityRecord( // If we got to the point in the try block where we made an optimistic update, // we need to roll it back here. - if ( persistedEntity && currentEdits ) { + if ( 'offline' !== error.type && persistedEntity && currentEdits ) { yield receiveEntityRecords( kind, name, persistedEntity, undefined, true ); yield editEntityRecord( kind, diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 82a91c3617a4d2..391b20b0af7bbe 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -22,6 +22,7 @@ import { getNotificationArgumentsForSaveSuccess, getNotificationArgumentsForSaveFail, getNotificationArgumentsForTrashFail, + getNotificationArgumentsForOfflineSync, } from './utils/notice-builder'; import serializeBlocks from './utils/serialize-blocks'; @@ -261,7 +262,20 @@ export function* savePost( options = {} ) { previousRecord.type, previousRecord.id ); - if ( error ) { + + if ( error && 'offline' === error.type ) { + const args = getNotificationArgumentsForOfflineSync( { + post: previousRecord, + edits, + error, + } ); + if ( args.length ) { + yield dispatch( 'core/notices', 'createWarningNotice', ...args ); + } + if ( ! options.isAutosave ) { + yield dispatch( 'core/block-editor', '__unstableMarkLastChangeAsPersistent' ); + } + } else if ( error ) { const args = getNotificationArgumentsForSaveFail( { post: previousRecord, edits, diff --git a/packages/editor/src/store/utils/notice-builder.js b/packages/editor/src/store/utils/notice-builder.js index 32ba7dacca5c25..382d3be9319764 100644 --- a/packages/editor/src/store/utils/notice-builder.js +++ b/packages/editor/src/store/utils/notice-builder.js @@ -13,6 +13,20 @@ import { SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID } from '../constants'; */ import { get, includes } from 'lodash'; +export function getNotificationArgumentsForOfflineSync( data ) { + const { error } = data; + let noticeMessage = ''; + + // Check if message string contains HTML. Notice text is currently only + // supported as plaintext, and stripping the tags may muddle the meaning. + if ( error.message && ! ( /<\/?[^>]*>/.test( error.message ) ) ) { + noticeMessage = [ error.message ].join( ' ' ); + } + return [ noticeMessage, { + id: SAVE_POST_NOTICE_ID, + } ]; +} + /** * Builds the arguments for a success notification dispatch. *