Skip to content

Commit

Permalink
Editor: Add sessionStorage autosave mechanism (#16490)
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth authored and youknowriad committed Sep 16, 2019
1 parent 95ded1f commit e99c212
Show file tree
Hide file tree
Showing 16 changed files with 459 additions and 72 deletions.
21 changes: 0 additions & 21 deletions docs/designers-developers/developers/data/data-core-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,27 +481,6 @@ _Related_

- getPreviousBlockClientId in core/block-editor store.

<a name="getReferenceByDistinctEdits" href="#getReferenceByDistinctEdits">#</a> **getReferenceByDistinctEdits**

Returns a new reference when edited values have changed. This is useful in
inferring where an edit has been made between states by comparison of the
return values using strict equality.

_Usage_

const hasEditOccurred = (
getReferenceByDistinctEdits( beforeState ) !==
getReferenceByDistinctEdits( afterState )
);

_Parameters_

- _state_ `Object`: Editor state.

_Returns_

- `*`: A value whose reference will change only when an edit occurs.

<a name="getSelectedBlock" href="#getSelectedBlock">#</a> **getSelectedBlock**

_Related_
Expand Down
21 changes: 21 additions & 0 deletions docs/designers-developers/developers/data/data-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,27 @@ _Returns_

- `?Object`: The edit.

<a name="getReferenceByDistinctEdits" href="#getReferenceByDistinctEdits">#</a> **getReferenceByDistinctEdits**

Returns a new reference when edited values have changed. This is useful in
inferring where an edit has been made between states by comparison of the
return values using strict equality.

_Usage_

const hasEditOccurred = (
getReferenceByDistinctEdits( beforeState ) !==
getReferenceByDistinctEdits( afterState )
);

_Parameters_

- _state_ `Object`: Editor state.

_Returns_

- `*`: A value whose reference will change only when an edit occurs.

<a name="getThemeSupports" href="#getThemeSupports">#</a> **getThemeSupports**

Return theme supports data in the index.
Expand Down
21 changes: 21 additions & 0 deletions packages/core-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,27 @@ _Returns_

- `?Object`: The edit.

<a name="getReferenceByDistinctEdits" href="#getReferenceByDistinctEdits">#</a> **getReferenceByDistinctEdits**

Returns a new reference when edited values have changed. This is useful in
inferring where an edit has been made between states by comparison of the
return values using strict equality.

_Usage_

const hasEditOccurred = (
getReferenceByDistinctEdits( beforeState ) !==
getReferenceByDistinctEdits( afterState )
);

_Parameters_

- _state_ `Object`: Editor state.

_Returns_

- `*`: A value whose reference will change only when an edit occurs.

<a name="getThemeSupports" href="#getThemeSupports">#</a> **getThemeSupports**

Return theme supports data in the index.
Expand Down
23 changes: 23 additions & 0 deletions packages/core-data/src/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -478,3 +478,26 @@ export function getAutosave( state, postType, postId, authorId ) {
export const hasFetchedAutosaves = createRegistrySelector( ( select ) => ( state, postType, postId ) => {
return select( REDUCER_KEY ).hasFinishedResolution( 'getAutosaves', [ postType, postId ] );
} );

/**
* Returns a new reference when edited values have changed. This is useful in
* inferring where an edit has been made between states by comparison of the
* return values using strict equality.
*
* @example
*
* ```
* const hasEditOccurred = (
* getReferenceByDistinctEdits( beforeState ) !==
* getReferenceByDistinctEdits( afterState )
* );
* ```
*
* @param {Object} state Editor state.
*
* @return {*} A value whose reference will change only when an edit occurs.
*/
export const getReferenceByDistinctEdits = createSelector(
() => [],
( state ) => [ state.undo.length, state.undo.offset ],
);
43 changes: 43 additions & 0 deletions packages/core-data/src/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getAutosave,
getAutosaves,
getCurrentUser,
getReferenceByDistinctEdits,
} from '../selectors';

describe( 'getEntityRecord', () => {
Expand Down Expand Up @@ -275,3 +276,45 @@ describe( 'getCurrentUser', () => {
expect( getCurrentUser( state ) ).toEqual( currentUser );
} );
} );

describe( 'getReferenceByDistinctEdits', () => {
it( 'should return referentially equal values across empty states', () => {
const state = { undo: [] };
expect( getReferenceByDistinctEdits( state ) ).toBe( getReferenceByDistinctEdits( state ) );

const beforeState = { undo: [] };
const afterState = { undo: [] };
expect( getReferenceByDistinctEdits( beforeState ) ).toBe( getReferenceByDistinctEdits( afterState ) );
} );

it( 'should return referentially equal values across unchanging non-empty state', () => {
const undoStates = [ {} ];
const state = { undo: undoStates };
expect( getReferenceByDistinctEdits( state ) ).toBe( getReferenceByDistinctEdits( state ) );

const beforeState = { undo: undoStates };
const afterState = { undo: undoStates };
expect( getReferenceByDistinctEdits( beforeState ) ).toBe( getReferenceByDistinctEdits( afterState ) );
} );

describe( 'when adding edits', () => {
it( 'should return referentially different values across changing states', () => {
const beforeState = { undo: [ {} ] };
beforeState.undo.offset = 0;
const afterState = { undo: [ {}, {} ] };
afterState.undo.offset = 1;
expect( getReferenceByDistinctEdits( beforeState ) ).not.toBe( getReferenceByDistinctEdits( afterState ) );
} );
} );

describe( 'when using undo', () => {
it( 'should return referentially different values across changing states', () => {
const beforeState = { undo: [ {}, {} ] };
beforeState.undo.offset = 1;
const afterState = { undo: [ {}, {} ] };
afterState.undo.offset = 0;
expect( getReferenceByDistinctEdits( beforeState ) ).not.toBe( getReferenceByDistinctEdits( afterState ) );
} );
} );
} );

107 changes: 107 additions & 0 deletions packages/e2e-tests/specs/autosave.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* WordPress dependencies
*/
import {
clickBlockAppender,
createNewPost,
getEditedPostContent,
pressKeyWithModifier,
} from '@wordpress/e2e-test-utils';

// Constant to override editor preference
const AUTOSAVE_INTERVAL_SECONDS = 5;

async function saveDraftWithKeyboard() {
return pressKeyWithModifier( 'primary', 's' );
}

async function sleep( durationInSeconds ) {
return new Promise( ( resolve ) =>
setTimeout( resolve, durationInSeconds * 1000 ) );
}

async function clearSessionStorage() {
await page.evaluate( () => window.sessionStorage.clear() );
}

async function readSessionStorageAutosave( postId ) {
return page.evaluate(
( key ) => window.sessionStorage.getItem( key ),
`wp-autosave-block-editor-post-${ postId }`
);
}

async function getCurrentPostId() {
return page.evaluate(
() => window.wp.data.select( 'core/editor' ).getCurrentPostId()
);
}

async function setLocalAutosaveInterval( value ) {
return page.evaluate( ( _value ) => {
window.wp.data.dispatch( 'core/edit-post' )
.__experimentalUpdateLocalAutosaveInterval( _value );
}, value );
}

function wrapParagraph( text ) {
return `<!-- wp:paragraph -->
<p>${ text }</p>
<!-- /wp:paragraph -->`;
}

describe( 'autosave', () => {
beforeEach( async () => {
await clearSessionStorage();
await createNewPost();
await setLocalAutosaveInterval( AUTOSAVE_INTERVAL_SECONDS );
} );

it( 'should save to sessionStorage', async () => {
await clickBlockAppender();
await page.keyboard.type( 'before save' );
await saveDraftWithKeyboard();
await page.keyboard.type( ' after save' );

// Wait long enough for local autosave to kick in
await sleep( AUTOSAVE_INTERVAL_SECONDS + 1 );

const id = await getCurrentPostId();
const autosave = await readSessionStorageAutosave( id );
const { content } = JSON.parse( autosave );
expect( content ).toBe( wrapParagraph( 'before save after save' ) );

// Test throttling by scattering typing
await page.keyboard.type( ' 1' );
await sleep( AUTOSAVE_INTERVAL_SECONDS - 4 );
await page.keyboard.type( '2' );
await sleep( 2 );
await page.keyboard.type( '3' );
await sleep( 2 );

const newAutosave = await readSessionStorageAutosave( id );
expect( JSON.parse( newAutosave ).content ).toBe( wrapParagraph( 'before save after save 123' ) );
} );

it( 'should recover from sessionStorage', async () => {
await clickBlockAppender();
await page.keyboard.type( 'before save' );
await saveDraftWithKeyboard();
await page.keyboard.type( ' after save' );

// Reload without saving on the server
await sleep( AUTOSAVE_INTERVAL_SECONDS + 1 );
await page.reload();

const notice = await page.$eval( '.components-notice__content', ( element ) => element.innerText );
expect( notice ).toContain( 'The backup of this post in your browser is different from the version below.' );

expect( await getEditedPostContent() ).toEqual( wrapParagraph( 'before save' ) );
await page.click( '.components-notice__action' );
expect( await getEditedPostContent() ).toEqual( wrapParagraph( 'before save after save' ) );
} );

afterAll( async () => {
await clearSessionStorage();
} );
} );
2 changes: 2 additions & 0 deletions packages/edit-post/src/components/layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { __ } from '@wordpress/i18n';
import { PreserveScrollInReorder } from '@wordpress/block-editor';
import {
AutosaveMonitor,
LocalAutosaveMonitor,
UnsavedChangesWarning,
EditorNotices,
PostPublishPanel,
Expand Down Expand Up @@ -77,6 +78,7 @@ function Layout( {
<BrowserURL />
<UnsavedChangesWarning />
<AutosaveMonitor />
<LocalAutosaveMonitor />
<Header />
<div
className="edit-post-layout__content"
Expand Down
7 changes: 6 additions & 1 deletion packages/edit-post/src/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Editor extends Component {
hiddenBlockTypes,
blockTypes,
preferredStyleVariations,
__experimentalLocalAutosaveInterval,
updatePreferredStyleVariations,
) {
settings = {
Expand All @@ -53,6 +54,7 @@ class Editor extends Component {
hasFixedToolbar,
focusMode,
showInserterHelpPanel,
__experimentalLocalAutosaveInterval,
};

// Omit hidden block types if exists and non-empty.
Expand All @@ -77,7 +79,6 @@ class Editor extends Component {

render() {
const {
preferredStyleVariations,
settings,
hasFixedToolbar,
focusMode,
Expand All @@ -87,6 +88,8 @@ class Editor extends Component {
onError,
hiddenBlockTypes,
blockTypes,
preferredStyleVariations,
__experimentalLocalAutosaveInterval,
showInserterHelpPanel,
updatePreferredStyleVariations,
...props
Expand All @@ -104,6 +107,7 @@ class Editor extends Component {
hiddenBlockTypes,
blockTypes,
preferredStyleVariations,
__experimentalLocalAutosaveInterval,
updatePreferredStyleVariations,
);

Expand Down Expand Up @@ -148,6 +152,7 @@ export default compose( [
preferredStyleVariations: getPreference( 'preferredStyleVariations' ),
hiddenBlockTypes: getPreference( 'hiddenBlockTypes' ),
blockTypes: getBlockTypes(),
__experimentalLocalAutosaveInterval: getPreference( 'localAutosaveInterval' ),
};
} ),
withDispatch( ( dispatch ) => {
Expand Down
7 changes: 7 additions & 0 deletions packages/edit-post/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ export function updatePreferredStyleVariations( blockName, blockStyle ) {
};
}

export function __experimentalUpdateLocalAutosaveInterval( interval ) {
return {
type: 'UPDATE_LOCAL_AUTOSAVE_INTERVAL',
interval,
};
}

/**
* Returns an action object used in signalling that block types by the given
* name(s) should be shown.
Expand Down
1 change: 1 addition & 0 deletions packages/edit-post/src/store/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export const PREFERENCES_DEFAULTS = {
pinnedPluginItems: {},
hiddenBlockTypes: [],
preferredStyleVariations: {},
localAutosaveInterval: 15,
};
8 changes: 8 additions & 0 deletions packages/edit-post/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,14 @@ export const preferences = flow( [
}
return state;
},
localAutosaveInterval( state, action ) {
switch ( action.type ) {
case 'UPDATE_LOCAL_AUTOSAVE_INTERVAL':
return action.interval;
}

return state;
},
} );

/**
Expand Down
Loading

0 comments on commit e99c212

Please sign in to comment.