Skip to content

Commit

Permalink
feat: Save data offline (close #7367)
Browse files Browse the repository at this point in the history
  • Loading branch information
tofumatt committed Aug 28, 2018
1 parent 65f6072 commit 0dcdba8
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 0 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@
"@wordpress/plugins": "file:packages/plugins",
"@wordpress/redux-routine": "file:packages/redux-routine",
"@wordpress/shortcode": "file:packages/shortcode",
"@wordpress/storage": "file:packages/storage",
"@wordpress/token-list": "file:packages/token-list",
"@wordpress/url": "file:packages/url",
"@wordpress/viewport": "file:packages/viewport",
"@wordpress/wordcount": "file:packages/wordcount",
"classnames": "2.2.5",
"equivalent-key-map": "0.2.1",
"eslint-plugin-wordpress": "git://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.git#1774343f6226052a46b081e01db3fca8793cc9f1",
"is-online": "7.0.0",
"jquery": "3.3.1",
"lodash": "4.17.10",
"prop-types": "15.5.10",
Expand Down
21 changes: 21 additions & 0 deletions packages/api-fetch/src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* External dependencies
*/
import isOnline from 'is-online';

/**
* WordPress dependencies
*/
Expand Down Expand Up @@ -63,6 +68,22 @@ function apiFetch( options ) {
.then( checkStatus )
.then( parseResponse )
.catch( ( response ) => {
// If the error meets this criteria, that means we couldn't reach the
// server and are likely offline. In that case we'll dispatch an error
// saying we're offline so the UI has the option to display a notice
// or warning instead of an error.
if (
response.name === 'TypeError' &&
response.message === 'NetworkError when attempting to fetch resource.'
) {
console.debug( 'HTTP Status was zero; no internet connection.' );
const noNetworkConnectionError = {
code: 'no_network_connection',
message: __( 'Could not reach server; you may be offline.' ),
};
throw noNetworkConnectionError;
}

if ( ! parse ) {
throw response;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/store/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
requestPostUpdate,
requestPostUpdateSuccess,
requestPostUpdateFailure,
requestPostUpdatePending,
trashPost,
trashPostFailure,
refreshPost,
Expand All @@ -64,6 +65,7 @@ export default {
},
REQUEST_POST_UPDATE_SUCCESS: requestPostUpdateSuccess,
REQUEST_POST_UPDATE_FAILURE: requestPostUpdateFailure,
REQUEST_POST_UPDATE_PENDING: requestPostUpdatePending,
TRASH_POST: ( action, store ) => {
trashPost( action, store );
},
Expand Down
46 changes: 46 additions & 0 deletions packages/editor/src/store/effects/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { pick, includes } from 'lodash';
*/
import apiFetch from '@wordpress/api-fetch';
import { __ } from '@wordpress/i18n';
import storage from '@wordpress/storage';
import { addQueryArgs } from '@wordpress/url';

/**
Expand Down Expand Up @@ -89,6 +90,11 @@ export const requestPostUpdate = async ( action, store ) => {

const postType = await resolveSelector( 'core', 'getPostType', getCurrentPostType( state ) );

const postsDatabase = storage( 'wp_posts' );
console.debug( 'post', post );
console.debug( 'toSend', toSend );
await postsDatabase.setItem( `post-id-${ post.id }`, { ...post, ...toSend } );

dispatch( {
type: 'REQUEST_POST_UPDATE_START',
optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID },
Expand Down Expand Up @@ -155,6 +161,18 @@ export const requestPostUpdate = async ( action, store ) => {
isAutosave,
} );
} catch ( error ) {
if ( error.code === 'no_network_connection' ) {
console.debug( 'Could not save; saving post offline.', error );

dispatch( {
type: 'REQUEST_POST_UPDATE_PENDING',
post,
isAutosave,
} );

return;
}

dispatch( {
type: 'REQUEST_POST_UPDATE_FAILURE',
optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID },
Expand Down Expand Up @@ -280,6 +298,34 @@ export const requestPostUpdateFailure = ( action, store ) => {
dispatch( createErrorNotice( cloudflaredMessage, { id: SAVE_POST_NOTICE_ID } ) );
};

/**
* Request Post Update Pending Effect handler
*
* @param {Object} action action object.
* @param {Object} store Redux Store.
*/
export const requestPostUpdatePending = ( action, store ) => {
const { isAutosave } = action;
const { dispatch } = store;

// Autosaves are neither shown a notice nor redirected.
if ( isAutosave ) {
console.debug( 'Autosave pending' );
return;
}

// Generic fallback notice.
const noticeMessage = __( 'Your connection appears offline, so this post was saved locally.' );

console.debug( 'Save pending' );
if ( noticeMessage ) {
dispatch( createSuccessNotice(
<p>{ noticeMessage }</p>,
{ id: SAVE_POST_NOTICE_ID, spokenMessage: noticeMessage }
) );
}
};

/**
* Trash Post Effect handler
*
Expand Down
7 changes: 7 additions & 0 deletions packages/editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,13 @@ export function saving( state = {}, action ) {
error: null,
};

case 'REQUEST_POST_UPDATE_PENDING':
return {
requesting: false,
successful: false,
error: null,
};

case 'REQUEST_POST_UPDATE_FAILURE':
return {
requesting: false,
Expand Down
13 changes: 13 additions & 0 deletions packages/storage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Storage

Storage module for WordPress; built using localForage.

## Installation

Install the module

```bash
npm install @wordpress/storage --save
```

<br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p>
28 changes: 28 additions & 0 deletions packages/storage/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@wordpress/storage",
"version": "0.0.1",
"description": "Offline storage module for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
"keywords": [
"wordpress",
"shortcode"
],
"homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/storage/README.md",
"repository": {
"type": "git",
"url": "https://github.com/WordPress/gutenberg.git"
},
"bugs": {
"url": "https://github.com/WordPress/gutenberg/issues"
},
"main": "build/index.js",
"module": "build-module/index.js",
"dependencies": {
"@babel/runtime-corejs2": "7.0.0-beta.56",
"localforage": "^1.6.2"
},
"publishConfig": {
"access": "public"
}
}
100 changes: 100 additions & 0 deletions packages/storage/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* External dependencies
*/
import localforage from 'localforage';

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

/**
* Returns a localForage instance given a storeName.
*
* @param {string} storeName alphanumeric+underscore identifier for
* a store. Passed to localForage's `config()`
* method as the `storeName` prop.
*
* @return {localforage} localForage instance
*/
export default function createStorage( storeName ) {
if ( ! storeName || ! storeName.length ) {
throw new Error( 'storeName is required for editor/utils/storage' );
}

return localforage.createInstance( {
name: 'WordPress Editor',
storeName,
} );
}

const DEFAULT_STATE = {};

const actions = {
getItem( storeName, key ) {
return {
type: 'GET_STORAGE_ITEM',
key,
storeName,
};
},

getItemFromStorageBackend( storeName, key ) {
return {
type: 'GET_STORAGE_ITEM_FROM_STORAGE_BACKEND',
key,
storeName,
};
},

setItem( storeName, key, value ) {
return {
type: 'SET_STORAGE_ITEM',
key,
storeName,
value,
};
},
};

registerStore( 'storage', {
reducer( state = DEFAULT_STATE, action ) {
switch ( action.type ) {
case 'RETRIEVE_STORAGE_ITEM':
return {
...state,
[ action.storageName ]: {
[ action.key ]: action.value,
},
};
}

return state;
},

actions,

selectors: {},

controls: {
GET_STORAGE_ITEM_FROM_STORAGE_BACKEND( action ) {
return createStorage( action.storeName ).getItem( action.key );
},
SET_ITEM( action ) {
return createStorage( action.storeName ).setItem( action.key, action.value );
},
},

resolvers: {
* getItem( state, storeName, key ) {
const value = yield actions.getItemFromStorageBackend( storeName, key );

return actions.retrieveStorageItem( storeName, key, value );
},
* setItem( state, storeName, key, value ) {
yield actions.setItemInStorageBackend( storeName, key, value );

return actions.retrieveStorageItem( storeName, key, value );
},
},
} );
10 changes: 10 additions & 0 deletions packages/storage/src/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Internal dependencies
*/
// import { next, replace, attrs } from '../';

describe( 'storage', () => {
it( 'should work', () => {
expect( true ).toEqual( true );
} );
} );

0 comments on commit 0dcdba8

Please sign in to comment.