From 74b5e25454dea8ab5e221bf54d19ee7aee9dd1e2 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 28 Mar 2022 15:33:53 +0800 Subject: [PATCH 01/74] Implement basic API --- packages/preferences/README.md | 8 +++++ packages/preferences/src/store/actions.js | 14 ++++++++ packages/preferences/src/store/reducer.js | 39 +++++++++++++++++++++-- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/packages/preferences/README.md b/packages/preferences/README.md index 72cc2fb1d9e884..333d04af6d7e9b 100644 --- a/packages/preferences/README.md +++ b/packages/preferences/README.md @@ -130,6 +130,14 @@ _Returns_ - `Object`: Action object. +#### setPersistenceLayer + +Sets the persistence layer. + +_Parameters_ + +- _persistenceLayer_ `Object`: Sets the persistence layer. + #### toggle Returns an action object used in signalling that a preference should be diff --git a/packages/preferences/src/store/actions.js b/packages/preferences/src/store/actions.js index 820a762c5d51e7..2b709e10b2c780 100644 --- a/packages/preferences/src/store/actions.js +++ b/packages/preferences/src/store/actions.js @@ -47,3 +47,17 @@ export function setDefaults( scope, defaults ) { defaults, }; } + +/** + * Sets the persistence layer. + * + * @param {Object} persistenceLayer Sets the persistence layer. + */ +export async function setPersistenceLayer( persistenceLayer ) { + const persistedData = await persistenceLayer.get(); + return { + type: 'SET_PERSISTENCE_LAYER', + persistenceLayer, + persistedData, + }; +} diff --git a/packages/preferences/src/store/reducer.js b/packages/preferences/src/store/reducer.js index 7e4752a22ef249..b12de6c4d37717 100644 --- a/packages/preferences/src/store/reducer.js +++ b/packages/preferences/src/store/reducer.js @@ -29,6 +29,41 @@ export function defaults( state = {}, action ) { return state; } +/** + * Higher order reducer that does the following: + * - Merges any data from the persistence layer into the state when the + * `SET_PERSISTENCE_LAYER` action is received. + * - Passes any preferences changes to the persistence layer. + * + * @param {Function} reducer The preferences reducer. + * + * @return {Function} The enhanced reducer. + */ +function withPersistenceLayer( reducer ) { + let persistenceLayer; + + return ( state, action ) => { + const nextState = reducer( state, action ); + + if ( action.type === 'SET_PERSISTENCE_LAYER' ) { + const { persistenceLayer: persistence, persistedData } = action; + + persistenceLayer = persistence; + + // TODO - is this the best strategy? + // Prioritize any user changes to state. + return { + ...persistedData, + ...nextState, + }; + } + + persistenceLayer.set( nextState ); + + return nextState; + }; +} + /** * Reducer returning the user preferences. * @@ -37,7 +72,7 @@ export function defaults( state = {}, action ) { * * @return {Object} Updated state. */ -export function preferences( state = {}, action ) { +const preferences = withPersistenceLayer( ( state = {}, action ) => { if ( action.type === 'SET_PREFERENCE_VALUE' ) { const { scope, name, value } = action; return { @@ -50,7 +85,7 @@ export function preferences( state = {}, action ) { } return state; -} +} ); export default combineReducers( { defaults, From 48ea1d906c6bad405f71c3756a4f8701932a1275 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 28 Mar 2022 16:09:54 +0800 Subject: [PATCH 02/74] Add new local storage persistence package --- docs/manifest.json | 6 +++ packages/persistence-local-storage/.npmrc | 1 + .../persistence-local-storage/CHANGELOG.md | 7 +++ packages/persistence-local-storage/README.md | 3 ++ .../persistence-local-storage/package.json | 36 +++++++++++++ .../persistence-local-storage/src/index.js | 54 +++++++++++++++++++ 6 files changed, 107 insertions(+) create mode 100644 packages/persistence-local-storage/.npmrc create mode 100644 packages/persistence-local-storage/CHANGELOG.md create mode 100644 packages/persistence-local-storage/README.md create mode 100644 packages/persistence-local-storage/package.json create mode 100644 packages/persistence-local-storage/src/index.js diff --git a/docs/manifest.json b/docs/manifest.json index 91bebab753a3bd..af8099c89f63b3 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1691,6 +1691,12 @@ "markdown_source": "../packages/nux/README.md", "parent": "packages" }, + { + "title": "@wordpress/persistence-local-storage", + "slug": "packages-persistence-local-storage", + "markdown_source": "../packages/persistence-local-storage/README.md", + "parent": "packages" + }, { "title": "@wordpress/plugins", "slug": "packages-plugins", diff --git a/packages/persistence-local-storage/.npmrc b/packages/persistence-local-storage/.npmrc new file mode 100644 index 00000000000000..43c97e719a5a82 --- /dev/null +++ b/packages/persistence-local-storage/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/persistence-local-storage/CHANGELOG.md b/packages/persistence-local-storage/CHANGELOG.md new file mode 100644 index 00000000000000..9b07b3bbe4d1d2 --- /dev/null +++ b/packages/persistence-local-storage/CHANGELOG.md @@ -0,0 +1,7 @@ + + +## Unreleased + +## 1.0.0 (2022-03-11) + +- Initial version of the package. diff --git a/packages/persistence-local-storage/README.md b/packages/persistence-local-storage/README.md new file mode 100644 index 00000000000000..740b2396064b30 --- /dev/null +++ b/packages/persistence-local-storage/README.md @@ -0,0 +1,3 @@ +# Persistence - Local Storage + +A persistence layer for storing `@wordpress/preferences` data in local storage. diff --git a/packages/persistence-local-storage/package.json b/packages/persistence-local-storage/package.json new file mode 100644 index 00000000000000..68c167a74548ca --- /dev/null +++ b/packages/persistence-local-storage/package.json @@ -0,0 +1,36 @@ +{ + "name": "@wordpress/persistence-local-storage", + "version": "1.0.0", + "description": "A local storage persistence layer for preferences.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "gutenberg", + "persistence", + "localStorage" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/persistence-local-storage/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/persistence-local-storage" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "engines": { + "node": ">=12" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "types": "build-types", + "sideEffects": false, + "dependencies": { + "@babel/runtime": "^7.16.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/persistence-local-storage/src/index.js b/packages/persistence-local-storage/src/index.js new file mode 100644 index 00000000000000..2751a51b6f681d --- /dev/null +++ b/packages/persistence-local-storage/src/index.js @@ -0,0 +1,54 @@ +/** + * Default plugin storage key. + * + * @type {string} + */ +const DEFAULT_STORAGE_KEY = 'PERSISTENCE_LOCAL_STORAGE_DATA'; +const EMPTY_OBJECT = {}; + +export function createPersistenceLayer( options ) { + const { storageKey = DEFAULT_STORAGE_KEY } = options; + + const storage = window.localStorage; + let data; + + /** + * Returns the persisted data as an object, defaulting to an empty object. + * + * @return {Object} Persisted data. + */ + function get() { + // If unset, getItem is expected to return null. Fall back to + // empty object. + const persisted = storage.getItem( storageKey ); + + if ( persisted === null ) { + return EMPTY_OBJECT; + } + + try { + data = JSON.parse( persisted ); + } catch ( error ) { + // Similarly, should any error be thrown during parse of + // the string (malformed JSON), fall back to empty object. + data = EMPTY_OBJECT; + } + + return data; + } + + /** + * Merges an updated reducer state into the persisted data. + * + * @param {Object} newData The data to persist. + */ + function set( newData ) { + data = { ...newData }; + storage.setItem( storageKey, JSON.stringify( data ) ); + } + + return { + get, + set, + }; +} From f4a04e0dc652252df434ccdc8330e577923e63a5 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 28 Mar 2022 16:25:03 +0800 Subject: [PATCH 03/74] Only call persistenceLayer.set if there is a persistence layer --- packages/preferences/src/store/reducer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preferences/src/store/reducer.js b/packages/preferences/src/store/reducer.js index b12de6c4d37717..fb0a98e4ea2c8e 100644 --- a/packages/preferences/src/store/reducer.js +++ b/packages/preferences/src/store/reducer.js @@ -58,7 +58,7 @@ function withPersistenceLayer( reducer ) { }; } - persistenceLayer.set( nextState ); + persistenceLayer?.set( nextState ); return nextState; }; From 0061caea1cbfb3a7c39b5f025eabb3053fbb6db7 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 28 Mar 2022 16:37:39 +0800 Subject: [PATCH 04/74] Wire up dependencies --- package-lock.json | 7 +++++++ package.json | 1 + packages/edit-post/package.json | 1 + 3 files changed, 9 insertions(+) diff --git a/package-lock.json b/package-lock.json index 9d236ac987e5f6..8655a10750ec35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17577,6 +17577,7 @@ "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", + "@wordpress/persistence-local-storage": "file:packages/persistence-local-storage", "@wordpress/plugins": "file:packages/plugins", "@wordpress/preferences": "file:packages/preferences", "@wordpress/url": "file:packages/url", @@ -18047,6 +18048,12 @@ "rememo": "^3.0.0" } }, + "@wordpress/persistence-local-storage": { + "version": "file:packages/persistence-local-storage", + "requires": { + "@babel/runtime": "^7.16.0" + } + }, "@wordpress/plugins": { "version": "file:packages/plugins", "requires": { diff --git a/package.json b/package.json index 5daf6096d8115d..6bb862166bab22 100755 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", "@wordpress/nux": "file:packages/nux", + "@wordpress/persistence-local-storage": "file:packages/persistence-local-storage", "@wordpress/plugins": "file:packages/plugins", "@wordpress/preferences": "file:packages/preferences", "@wordpress/primitives": "file:packages/primitives", diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index a48a13a92af580..524258fb6168c8 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -47,6 +47,7 @@ "@wordpress/keycodes": "file:../keycodes", "@wordpress/media-utils": "file:../media-utils", "@wordpress/notices": "file:../notices", + "@wordpress/persistence-local-storage": "file:../persistence-local-storage", "@wordpress/plugins": "file:../plugins", "@wordpress/preferences": "file:../preferences", "@wordpress/url": "file:../url", From bce05a514953e1e42af8575226df2d657b84ce5c Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 28 Mar 2022 16:38:40 +0800 Subject: [PATCH 05/74] Fix exporting create as default and get not being async --- packages/persistence-local-storage/src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/persistence-local-storage/src/index.js b/packages/persistence-local-storage/src/index.js index 2751a51b6f681d..6928556e75907a 100644 --- a/packages/persistence-local-storage/src/index.js +++ b/packages/persistence-local-storage/src/index.js @@ -6,7 +6,7 @@ const DEFAULT_STORAGE_KEY = 'PERSISTENCE_LOCAL_STORAGE_DATA'; const EMPTY_OBJECT = {}; -export function createPersistenceLayer( options ) { +export default function createPersistenceLayer( options ) { const { storageKey = DEFAULT_STORAGE_KEY } = options; const storage = window.localStorage; @@ -17,7 +17,7 @@ export function createPersistenceLayer( options ) { * * @return {Object} Persisted data. */ - function get() { + async function get() { // If unset, getItem is expected to return null. Fall back to // empty object. const persisted = storage.getItem( storageKey ); From d592cf351481648aeadf0eb3488483f3e8ab3a82 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 28 Mar 2022 16:39:10 +0800 Subject: [PATCH 06/74] Remove old persistence interface and configure new persistence layer in post editor --- packages/edit-post/src/index.js | 7 +++++++ packages/preferences/src/store/index.js | 12 ++---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 6ff6a7603d89f0..5b06d57a870346 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -9,6 +9,7 @@ import { import { render, unmountComponentAtNode } from '@wordpress/element'; import { dispatch, select } from '@wordpress/data'; import { addFilter } from '@wordpress/hooks'; +import createPersistenceLayer from '@wordpress/persistence-local-storage'; import { store as preferencesStore } from '@wordpress/preferences'; /** @@ -106,6 +107,12 @@ export function initializeEditor( initialEdits ); + const localStoragePersistenceLayer = createPersistenceLayer( { + storageKey: 'WP_DATA_TEST', + } ); + dispatch( preferencesStore ).setPersistenceLayer( + localStoragePersistenceLayer + ); dispatch( preferencesStore ).setDefaults( 'core/edit-post', { editorMode: 'visual', fixedToolbar: false, diff --git a/packages/preferences/src/store/index.js b/packages/preferences/src/store/index.js index 6c7b560f0720cb..25e979422b2e7f 100644 --- a/packages/preferences/src/store/index.js +++ b/packages/preferences/src/store/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createReduxStore, registerStore } from '@wordpress/data'; +import { createReduxStore, register } from '@wordpress/data'; /** * Internal dependencies @@ -25,14 +25,6 @@ export const store = createReduxStore( STORE_NAME, { reducer, actions, selectors, - persist: [ 'preferences' ], } ); -// Once we build a more generic persistence plugin that works across types of stores -// we'd be able to replace this with a register call. -registerStore( STORE_NAME, { - reducer, - actions, - selectors, - persist: [ 'preferences' ], -} ); +register( store ); From 62a5e473f23b94dee929c92abd636178a55bdaf2 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 29 Mar 2022 11:41:18 +0800 Subject: [PATCH 07/74] Use correct user id and storage key --- packages/edit-post/src/index.js | 13 +++--------- packages/edit-post/src/index.native.js | 5 ++--- .../src/utils/configure-preferences.js | 20 +++++++++++++++++++ .../persistence-local-storage/src/index.js | 6 +++--- 4 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 packages/edit-post/src/utils/configure-preferences.js diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 5b06d57a870346..156f9b4812170e 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -9,8 +9,6 @@ import { import { render, unmountComponentAtNode } from '@wordpress/element'; import { dispatch, select } from '@wordpress/data'; import { addFilter } from '@wordpress/hooks'; -import createPersistenceLayer from '@wordpress/persistence-local-storage'; -import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -19,6 +17,7 @@ import './hooks'; import './plugins'; import Editor from './editor'; import { store as editPostStore } from './store'; +import configurePreferences from './utils/configure-preferences'; /** * Reinitializes the editor after the user chooses to reboot the editor after @@ -74,7 +73,7 @@ export function reinitializeEditor( * considered as non-user-initiated (bypass for * unsaved changes prompt). */ -export function initializeEditor( +export async function initializeEditor( id, postType, postId, @@ -107,13 +106,7 @@ export function initializeEditor( initialEdits ); - const localStoragePersistenceLayer = createPersistenceLayer( { - storageKey: 'WP_DATA_TEST', - } ); - dispatch( preferencesStore ).setPersistenceLayer( - localStoragePersistenceLayer - ); - dispatch( preferencesStore ).setDefaults( 'core/edit-post', { + await configurePreferences( { editorMode: 'visual', fixedToolbar: false, fullscreenMode: true, diff --git a/packages/edit-post/src/index.native.js b/packages/edit-post/src/index.native.js index b825d50ace0c01..48bf4185ba2855 100644 --- a/packages/edit-post/src/index.native.js +++ b/packages/edit-post/src/index.native.js @@ -3,14 +3,13 @@ */ import '@wordpress/core-data'; import '@wordpress/format-library'; -import { dispatch } from '@wordpress/data'; -import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies */ export { store } from './store'; import Editor from './editor'; +import configurePreferences from './utils/configure-preferences'; /** * Initializes the Editor and returns a componentProvider @@ -21,7 +20,7 @@ import Editor from './editor'; * @param {Object} postId ID of the post to edit (unused right now) */ export function initializeEditor( id, postType, postId ) { - dispatch( preferencesStore ).setDefaults( 'core/edit-post', { + configurePreferences( { editorMode: 'visual', fixedToolbar: false, fullscreenMode: true, diff --git a/packages/edit-post/src/utils/configure-preferences.js b/packages/edit-post/src/utils/configure-preferences.js new file mode 100644 index 00000000000000..62924ee58cbd0a --- /dev/null +++ b/packages/edit-post/src/utils/configure-preferences.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { store as coreStore } from '@wordpress/core-data'; +import { dispatch, resolveSelect } from '@wordpress/data'; +import createPersistenceLayer from '@wordpress/persistence-local-storage'; +import { store as preferencesStore } from '@wordpress/preferences'; + +export default async function configurePreferences( defaults ) { + const currentUser = await resolveSelect( coreStore ).getCurrentUser(); + const localStoragePersistenceLayer = createPersistenceLayer( { + storageKey: currentUser + ? `WP_DATA_USER_${ currentUser?.id }` + : 'WP_DATA', + } ); + await dispatch( preferencesStore ).setPersistenceLayer( + localStoragePersistenceLayer + ); + dispatch( preferencesStore ).setDefaults( 'core/edit-post', defaults ); +} diff --git a/packages/persistence-local-storage/src/index.js b/packages/persistence-local-storage/src/index.js index 6928556e75907a..76fcd2d01271f0 100644 --- a/packages/persistence-local-storage/src/index.js +++ b/packages/persistence-local-storage/src/index.js @@ -6,9 +6,9 @@ const DEFAULT_STORAGE_KEY = 'PERSISTENCE_LOCAL_STORAGE_DATA'; const EMPTY_OBJECT = {}; -export default function createPersistenceLayer( options ) { - const { storageKey = DEFAULT_STORAGE_KEY } = options; - +export default function createPersistenceLayer( { + storageKey = DEFAULT_STORAGE_KEY, +} = {} ) { const storage = window.localStorage; let data; From b7a87863ad1cda3056f4b50cd1023993881bde06 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 29 Mar 2022 13:58:33 +0800 Subject: [PATCH 08/74] Move local storage persistence layer into the preferences package --- package-lock.json | 1 - packages/edit-post/package.json | 1 - .../src/utils/configure-preferences.js | 8 +-- packages/preferences/src/index.js | 1 + .../src/local-storage-persistence/index.js | 54 +++++++++++++++++++ 5 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 packages/preferences/src/local-storage-persistence/index.js diff --git a/package-lock.json b/package-lock.json index 8655a10750ec35..736b49300f2120 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17577,7 +17577,6 @@ "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", - "@wordpress/persistence-local-storage": "file:packages/persistence-local-storage", "@wordpress/plugins": "file:packages/plugins", "@wordpress/preferences": "file:packages/preferences", "@wordpress/url": "file:packages/url", diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 524258fb6168c8..a48a13a92af580 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -47,7 +47,6 @@ "@wordpress/keycodes": "file:../keycodes", "@wordpress/media-utils": "file:../media-utils", "@wordpress/notices": "file:../notices", - "@wordpress/persistence-local-storage": "file:../persistence-local-storage", "@wordpress/plugins": "file:../plugins", "@wordpress/preferences": "file:../preferences", "@wordpress/url": "file:../url", diff --git a/packages/edit-post/src/utils/configure-preferences.js b/packages/edit-post/src/utils/configure-preferences.js index 62924ee58cbd0a..0370c40f128713 100644 --- a/packages/edit-post/src/utils/configure-preferences.js +++ b/packages/edit-post/src/utils/configure-preferences.js @@ -3,12 +3,14 @@ */ import { store as coreStore } from '@wordpress/core-data'; import { dispatch, resolveSelect } from '@wordpress/data'; -import createPersistenceLayer from '@wordpress/persistence-local-storage'; -import { store as preferencesStore } from '@wordpress/preferences'; +import { + createLocalStoragePersistenceLayer, + store as preferencesStore, +} from '@wordpress/preferences'; export default async function configurePreferences( defaults ) { const currentUser = await resolveSelect( coreStore ).getCurrentUser(); - const localStoragePersistenceLayer = createPersistenceLayer( { + const localStoragePersistenceLayer = createLocalStoragePersistenceLayer( { storageKey: currentUser ? `WP_DATA_USER_${ currentUser?.id }` : 'WP_DATA', diff --git a/packages/preferences/src/index.js b/packages/preferences/src/index.js index 72531a0824c178..c8e119e73c8f60 100644 --- a/packages/preferences/src/index.js +++ b/packages/preferences/src/index.js @@ -1,2 +1,3 @@ export * from './components'; export { store } from './store'; +export { default as createLocalStoragePersistenceLayer } from './local-storage-persistence'; diff --git a/packages/preferences/src/local-storage-persistence/index.js b/packages/preferences/src/local-storage-persistence/index.js new file mode 100644 index 00000000000000..425155859747fe --- /dev/null +++ b/packages/preferences/src/local-storage-persistence/index.js @@ -0,0 +1,54 @@ +/** + * Default plugin storage key. + * + * @type {string} + */ +const DEFAULT_STORAGE_KEY = 'PREFERENCES_DATA'; +const EMPTY_OBJECT = {}; + +export default function createLocalStoragePersistenceLayer( { + storageKey = DEFAULT_STORAGE_KEY, +} = {} ) { + const storage = window.localStorage; + let data; + + /** + * Returns the persisted data as an object, defaulting to an empty object. + * + * @return {Object} Persisted data. + */ + async function get() { + // If unset, getItem is expected to return null. Fall back to + // empty object. + const persisted = storage.getItem( storageKey ); + + if ( persisted === null ) { + return EMPTY_OBJECT; + } + + try { + data = JSON.parse( persisted ); + } catch ( error ) { + // Similarly, should any error be thrown during parse of + // the string (malformed JSON), fall back to empty object. + data = EMPTY_OBJECT; + } + + return data; + } + + /** + * Merges an updated reducer state into the persisted data. + * + * @param {Object} newData The data to persist. + */ + function set( newData ) { + data = { ...newData }; + storage.setItem( storageKey, JSON.stringify( data ) ); + } + + return { + get, + set, + }; +} From 2d6e1131d23c6334f6b49de1ce53e92482deca7a Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 29 Mar 2022 14:55:56 +0800 Subject: [PATCH 09/74] Try a wp-preferences package --- docs/manifest.json | 12 ++-- package-lock.json | 25 ++++---- package.json | 2 +- packages/persistence-local-storage/README.md | 3 - .../persistence-local-storage/src/index.js | 54 ---------------- packages/preferences/package.json | 7 +-- .../.npmrc | 0 .../CHANGELOG.md | 0 packages/wp-preferences/README.md | 3 + .../package.json | 20 +++--- .../wp-preferences/src/components/index.js | 1 + .../preference-toggle-menu-item/README.md | 58 +++++++++++++++++ .../preference-toggle-menu-item/index.js | 62 +++++++++++++++++++ .../src/database-persistence-layer/index.js | 0 packages/wp-preferences/src/index.js | 26 ++++++++ .../wp-preferences/src/migrations/index.js | 0 16 files changed, 184 insertions(+), 89 deletions(-) delete mode 100644 packages/persistence-local-storage/README.md delete mode 100644 packages/persistence-local-storage/src/index.js rename packages/{persistence-local-storage => wp-preferences}/.npmrc (100%) rename packages/{persistence-local-storage => wp-preferences}/CHANGELOG.md (100%) create mode 100644 packages/wp-preferences/README.md rename packages/{persistence-local-storage => wp-preferences}/package.json (55%) create mode 100644 packages/wp-preferences/src/components/index.js create mode 100644 packages/wp-preferences/src/components/preference-toggle-menu-item/README.md create mode 100644 packages/wp-preferences/src/components/preference-toggle-menu-item/index.js create mode 100644 packages/wp-preferences/src/database-persistence-layer/index.js create mode 100644 packages/wp-preferences/src/index.js create mode 100644 packages/wp-preferences/src/migrations/index.js diff --git a/docs/manifest.json b/docs/manifest.json index af8099c89f63b3..939318182a6b28 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1691,12 +1691,6 @@ "markdown_source": "../packages/nux/README.md", "parent": "packages" }, - { - "title": "@wordpress/persistence-local-storage", - "slug": "packages-persistence-local-storage", - "markdown_source": "../packages/persistence-local-storage/README.md", - "parent": "packages" - }, { "title": "@wordpress/plugins", "slug": "packages-plugins", @@ -1841,6 +1835,12 @@ "markdown_source": "../packages/wordcount/README.md", "parent": "packages" }, + { + "title": "@wordpress/wp-preferences", + "slug": "packages-wp-preferences", + "markdown_source": "../packages/wp-preferences/README.md", + "parent": "packages" + }, { "title": "Data Module Reference", "slug": "data", diff --git a/package-lock.json b/package-lock.json index 736b49300f2120..64149121de3869 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18047,12 +18047,6 @@ "rememo": "^3.0.0" } }, - "@wordpress/persistence-local-storage": { - "version": "file:packages/persistence-local-storage", - "requires": { - "@babel/runtime": "^7.16.0" - } - }, "@wordpress/plugins": { "version": "file:packages/plugins", "requires": { @@ -18081,12 +18075,7 @@ "version": "file:packages/preferences", "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/a11y": "file:packages/a11y", - "@wordpress/components": "file:packages/components", - "@wordpress/data": "file:packages/data", - "@wordpress/i18n": "file:packages/i18n", - "@wordpress/icons": "file:packages/icons", - "classnames": "^2.3.1" + "@wordpress/data": "file:packages/data" } }, "@wordpress/prettier-config": { @@ -18394,6 +18383,18 @@ "lodash": "^4.17.21" } }, + "@wordpress/wp-preferences": { + "version": "file:packages/wp-preferences", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/a11y": "file:packages/a11y", + "@wordpress/components": "file:packages/components", + "@wordpress/data": "file:packages/data", + "@wordpress/i18n": "file:packages/i18n", + "@wordpress/icons": "file:packages/icons", + "@wordpress/preferences": "file:packages/preferences" + } + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", diff --git a/package.json b/package.json index 6bb862166bab22..b28f11c78fd21d 100755 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", "@wordpress/nux": "file:packages/nux", - "@wordpress/persistence-local-storage": "file:packages/persistence-local-storage", + "@wordpress/wp-preferences": "file:packages/wp-preferences", "@wordpress/plugins": "file:packages/plugins", "@wordpress/preferences": "file:packages/preferences", "@wordpress/primitives": "file:packages/primitives", diff --git a/packages/persistence-local-storage/README.md b/packages/persistence-local-storage/README.md deleted file mode 100644 index 740b2396064b30..00000000000000 --- a/packages/persistence-local-storage/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Persistence - Local Storage - -A persistence layer for storing `@wordpress/preferences` data in local storage. diff --git a/packages/persistence-local-storage/src/index.js b/packages/persistence-local-storage/src/index.js deleted file mode 100644 index 76fcd2d01271f0..00000000000000 --- a/packages/persistence-local-storage/src/index.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Default plugin storage key. - * - * @type {string} - */ -const DEFAULT_STORAGE_KEY = 'PERSISTENCE_LOCAL_STORAGE_DATA'; -const EMPTY_OBJECT = {}; - -export default function createPersistenceLayer( { - storageKey = DEFAULT_STORAGE_KEY, -} = {} ) { - const storage = window.localStorage; - let data; - - /** - * Returns the persisted data as an object, defaulting to an empty object. - * - * @return {Object} Persisted data. - */ - async function get() { - // If unset, getItem is expected to return null. Fall back to - // empty object. - const persisted = storage.getItem( storageKey ); - - if ( persisted === null ) { - return EMPTY_OBJECT; - } - - try { - data = JSON.parse( persisted ); - } catch ( error ) { - // Similarly, should any error be thrown during parse of - // the string (malformed JSON), fall back to empty object. - data = EMPTY_OBJECT; - } - - return data; - } - - /** - * Merges an updated reducer state into the persisted data. - * - * @param {Object} newData The data to persist. - */ - function set( newData ) { - data = { ...newData }; - storage.setItem( storageKey, JSON.stringify( data ) ); - } - - return { - get, - set, - }; -} diff --git a/packages/preferences/package.json b/packages/preferences/package.json index f620b89a5148be..4f6bf606dc9614 100644 --- a/packages/preferences/package.json +++ b/packages/preferences/package.json @@ -30,12 +30,7 @@ "sideEffects": false, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/a11y": "file:../a11y", - "@wordpress/components": "file:../components", - "@wordpress/data": "file:../data", - "@wordpress/i18n": "file:../i18n", - "@wordpress/icons": "file:../icons", - "classnames": "^2.3.1" + "@wordpress/data": "file:../data" }, "peerDependencies": { "react": "^17.0.0", diff --git a/packages/persistence-local-storage/.npmrc b/packages/wp-preferences/.npmrc similarity index 100% rename from packages/persistence-local-storage/.npmrc rename to packages/wp-preferences/.npmrc diff --git a/packages/persistence-local-storage/CHANGELOG.md b/packages/wp-preferences/CHANGELOG.md similarity index 100% rename from packages/persistence-local-storage/CHANGELOG.md rename to packages/wp-preferences/CHANGELOG.md diff --git a/packages/wp-preferences/README.md b/packages/wp-preferences/README.md new file mode 100644 index 00000000000000..2d6bd56c4ff17c --- /dev/null +++ b/packages/wp-preferences/README.md @@ -0,0 +1,3 @@ +# WP Preferences + +WordPress specific utilities for preferences. diff --git a/packages/persistence-local-storage/package.json b/packages/wp-preferences/package.json similarity index 55% rename from packages/persistence-local-storage/package.json rename to packages/wp-preferences/package.json index 68c167a74548ca..1ecd45c06115e5 100644 --- a/packages/persistence-local-storage/package.json +++ b/packages/wp-preferences/package.json @@ -1,20 +1,20 @@ { - "name": "@wordpress/persistence-local-storage", + "name": "@wordpress/wp-preferences", "version": "1.0.0", - "description": "A local storage persistence layer for preferences.", + "description": "Wordpress-specific utilities for preferences.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", "keywords": [ "wordpress", "gutenberg", - "persistence", - "localStorage" + "preferences", + "settings" ], - "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/persistence-local-storage/README.md", + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/wp-preferences/README.md", "repository": { "type": "git", "url": "https://github.com/WordPress/gutenberg.git", - "directory": "packages/persistence-local-storage" + "directory": "packages/wp-preferences" }, "bugs": { "url": "https://github.com/WordPress/gutenberg/issues" @@ -28,7 +28,13 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.16.0" + "@babel/runtime": "^7.16.0", + "@wordpress/a11y": "file:../a11y", + "@wordpress/components": "file:../components", + "@wordpress/data": "file:../data", + "@wordpress/i18n": "file:../i18n", + "@wordpress/icons": "file:../icons", + "@wordpress/preferences": "file:../preferences" }, "publishConfig": { "access": "public" diff --git a/packages/wp-preferences/src/components/index.js b/packages/wp-preferences/src/components/index.js new file mode 100644 index 00000000000000..7bd32262d3abe4 --- /dev/null +++ b/packages/wp-preferences/src/components/index.js @@ -0,0 +1 @@ +export { default as PreferenceToggleMenuItem } from './preference-toggle-menu-item'; diff --git a/packages/wp-preferences/src/components/preference-toggle-menu-item/README.md b/packages/wp-preferences/src/components/preference-toggle-menu-item/README.md new file mode 100644 index 00000000000000..0bd270f16804c7 --- /dev/null +++ b/packages/wp-preferences/src/components/preference-toggle-menu-item/README.md @@ -0,0 +1,58 @@ +# PreferenceToggleMenuItem + +`PreferenceToggleMenuItem` renders a menu item that is connected to the preference package's store, and will toggle the value of a 'preference' between true and false. + +This component implements a `MenuItem` component from the `@wordpress/components` package. + +## Props + +### scope + +The 'scope' of the feature. This is usually a namespaced string that represents the name of the editor (e.g. 'core/edit-post'), and often matches the name of the store for the editor. + +- Type: `String` +- Required: Yes + +### name + +The name of the preference to toggle (e.g. 'fixedToolbar'). + +- Type: `String` +- Required: Yes + +### label + +A human readable label for the feature. + +- Type: `String` +- Required: Yes + +### info + +A human readable description of what this toggle does. + +- Type: `Object` +- Required: No + +### messageActivated + +A message read by a screen reader when the feature is activated. (e.g. 'Fixed toolbar activated') + +- Type: `String` +- Required: No + +### messageDeactivated + +A message read by a screen reader when the feature is deactivated. (e.g. 'Fixed toolbar deactivated') + +- Type: `String` +- Required: No + +### shortcut + +A keyboard shortcut for the feature. This is just used for display purposes and the implementation of the shortcut should be handled separately. + +Consider using the `displayShortcut` helper from the `@wordpress/keycodes` package for this prop. + +- Type: `Array` +- Required: No diff --git a/packages/wp-preferences/src/components/preference-toggle-menu-item/index.js b/packages/wp-preferences/src/components/preference-toggle-menu-item/index.js new file mode 100644 index 00000000000000..53eef4a115faf4 --- /dev/null +++ b/packages/wp-preferences/src/components/preference-toggle-menu-item/index.js @@ -0,0 +1,62 @@ +/** + * WordPress dependencies + */ +import { speak } from '@wordpress/a11y'; +import { MenuItem } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { __, sprintf } from '@wordpress/i18n'; +import { check } from '@wordpress/icons'; +import { store as preferencesStore } from '@wordpress/preferences'; + +export default function PreferenceToggleMenuItem( { + scope, + name, + label, + info, + messageActivated, + messageDeactivated, + shortcut, +} ) { + const isActive = useSelect( + ( select ) => !! select( preferencesStore ).get( scope, name ), + [ name ] + ); + const { toggle } = useDispatch( preferencesStore ); + const speakMessage = () => { + if ( isActive ) { + const message = + messageDeactivated || + sprintf( + /* translators: %s: preference name, e.g. 'Fullscreen mode' */ + __( 'Preference deactivated - %s' ), + label + ); + speak( message ); + } else { + const message = + messageActivated || + sprintf( + /* translators: %s: preference name, e.g. 'Fullscreen mode' */ + __( 'Preference activated - %s' ), + label + ); + speak( message ); + } + }; + + return ( + { + toggle( scope, name ); + speakMessage(); + } } + role="menuitemcheckbox" + info={ info } + shortcut={ shortcut } + > + { label } + + ); +} diff --git a/packages/wp-preferences/src/database-persistence-layer/index.js b/packages/wp-preferences/src/database-persistence-layer/index.js new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/wp-preferences/src/index.js b/packages/wp-preferences/src/index.js new file mode 100644 index 00000000000000..43deb08716e1ab --- /dev/null +++ b/packages/wp-preferences/src/index.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { dispatch } from '@wordpress/data'; +import { store as preferencesStore } from '@wordpress/preferences'; + +/** + * Internal dependencies + */ +import createDatabasePersistenceLayer from './database-persistence-layer'; +import migrate from './migrations'; + +export * from './components'; + +export async function configurePreferences( { defaults, storageKey } ) { + const databasePersistenceLayer = createDatabasePersistenceLayer( { + storageKey, + __unstableMigrate: migrate, + } ); + + await dispatch( preferencesStore ).setPersistenceLayer( + databasePersistenceLayer + ); + + dispatch( preferencesStore ).setDefaults( 'core/edit-post', defaults ); +} diff --git a/packages/wp-preferences/src/migrations/index.js b/packages/wp-preferences/src/migrations/index.js new file mode 100644 index 00000000000000..e69de29bb2d1d6 From 0cb6615b5b8e8ac203f0474baa37eaf684f8b8f1 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 29 Mar 2022 16:27:19 +0800 Subject: [PATCH 10/74] Use database persistence --- lib/experimental/persisted-preferences.php | 32 +++++++++++++++ lib/load.php | 1 + package-lock.json | 2 + packages/edit-post/package.json | 1 + .../src/utils/configure-preferences.js | 18 +++----- .../src/local-storage-persistence/index.js | 5 ++- packages/wp-preferences/package.json | 1 + .../src/database-persistence-layer/index.js | 41 +++++++++++++++++++ packages/wp-preferences/src/index.js | 26 +----------- 9 files changed, 87 insertions(+), 40 deletions(-) create mode 100644 lib/experimental/persisted-preferences.php diff --git a/lib/experimental/persisted-preferences.php b/lib/experimental/persisted-preferences.php new file mode 100644 index 00000000000000..00da8ed4512dc1 --- /dev/null +++ b/lib/experimental/persisted-preferences.php @@ -0,0 +1,32 @@ +get_blog_prefix() . 'persisted_preferences', + array( + 'type' => 'object', + 'single' => true, + 'show_in_rest' => array( + 'name' => 'persisted_preferences', + 'type' => 'object', + 'schema' => array( + 'type' => 'object', + 'properties' => array(), + 'additionalProperties' => true, + ), + ), + ) + ); +} + +add_action( 'init', 'gutenberg_register_persisted_preferences_user_meta' ); diff --git a/lib/load.php b/lib/load.php index 5d153d72b791b8..c607508265f117 100644 --- a/lib/load.php +++ b/lib/load.php @@ -134,6 +134,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/class-wp-webfonts-provider-local.php'; require __DIR__ . '/experimental/webfonts.php'; require __DIR__ . '/experimental/blocks.php'; +require __DIR__ . '/experimental/persisted-preferences.php'; require __DIR__ . '/experimental/navigation-theme-opt-in.php'; require __DIR__ . '/experimental/navigation-page.php'; diff --git a/package-lock.json b/package-lock.json index 64149121de3869..93510266b4f70e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17582,6 +17582,7 @@ "@wordpress/url": "file:packages/url", "@wordpress/viewport": "file:packages/viewport", "@wordpress/warning": "file:packages/warning", + "@wordpress/wp-preferences": "file:packages/wp-preferences", "classnames": "^2.3.1", "lodash": "^4.17.21", "memize": "^1.1.0", @@ -18388,6 +18389,7 @@ "requires": { "@babel/runtime": "^7.16.0", "@wordpress/a11y": "file:packages/a11y", + "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/components": "file:packages/components", "@wordpress/data": "file:packages/data", "@wordpress/i18n": "file:packages/i18n", diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index a48a13a92af580..e304d53cf31ac2 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -52,6 +52,7 @@ "@wordpress/url": "file:../url", "@wordpress/viewport": "file:../viewport", "@wordpress/warning": "file:../warning", + "@wordpress/wp-preferences": "file:../wp-preferences", "classnames": "^2.3.1", "lodash": "^4.17.21", "memize": "^1.1.0", diff --git a/packages/edit-post/src/utils/configure-preferences.js b/packages/edit-post/src/utils/configure-preferences.js index 0370c40f128713..ac0585df795b42 100644 --- a/packages/edit-post/src/utils/configure-preferences.js +++ b/packages/edit-post/src/utils/configure-preferences.js @@ -1,22 +1,14 @@ /** * WordPress dependencies */ -import { store as coreStore } from '@wordpress/core-data'; -import { dispatch, resolveSelect } from '@wordpress/data'; -import { - createLocalStoragePersistenceLayer, - store as preferencesStore, -} from '@wordpress/preferences'; +import { dispatch } from '@wordpress/data'; +import { store as preferencesStore } from '@wordpress/preferences'; +import { createDatabasePersistenceLayer } from '@wordpress/wp-preferences'; export default async function configurePreferences( defaults ) { - const currentUser = await resolveSelect( coreStore ).getCurrentUser(); - const localStoragePersistenceLayer = createLocalStoragePersistenceLayer( { - storageKey: currentUser - ? `WP_DATA_USER_${ currentUser?.id }` - : 'WP_DATA', - } ); + const databasePersistenceLayer = createDatabasePersistenceLayer(); await dispatch( preferencesStore ).setPersistenceLayer( - localStoragePersistenceLayer + databasePersistenceLayer ); dispatch( preferencesStore ).setDefaults( 'core/edit-post', defaults ); } diff --git a/packages/preferences/src/local-storage-persistence/index.js b/packages/preferences/src/local-storage-persistence/index.js index 425155859747fe..91b07583fb5b8c 100644 --- a/packages/preferences/src/local-storage-persistence/index.js +++ b/packages/preferences/src/local-storage-persistence/index.js @@ -10,7 +10,6 @@ export default function createLocalStoragePersistenceLayer( { storageKey = DEFAULT_STORAGE_KEY, } = {} ) { const storage = window.localStorage; - let data; /** * Returns the persisted data as an object, defaulting to an empty object. @@ -26,6 +25,8 @@ export default function createLocalStoragePersistenceLayer( { return EMPTY_OBJECT; } + let data; + try { data = JSON.parse( persisted ); } catch ( error ) { @@ -43,7 +44,7 @@ export default function createLocalStoragePersistenceLayer( { * @param {Object} newData The data to persist. */ function set( newData ) { - data = { ...newData }; + const data = { ...newData }; storage.setItem( storageKey, JSON.stringify( data ) ); } diff --git a/packages/wp-preferences/package.json b/packages/wp-preferences/package.json index 1ecd45c06115e5..1d54e05c3df9f0 100644 --- a/packages/wp-preferences/package.json +++ b/packages/wp-preferences/package.json @@ -30,6 +30,7 @@ "dependencies": { "@babel/runtime": "^7.16.0", "@wordpress/a11y": "file:../a11y", + "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/components": "file:../components", "@wordpress/data": "file:../data", "@wordpress/i18n": "file:../i18n", diff --git a/packages/wp-preferences/src/database-persistence-layer/index.js b/packages/wp-preferences/src/database-persistence-layer/index.js index e69de29bb2d1d6..22590fbd6ea0bc 100644 --- a/packages/wp-preferences/src/database-persistence-layer/index.js +++ b/packages/wp-preferences/src/database-persistence-layer/index.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +export default function createDatabasePersistenceLayer() { + let data; + + async function get() { + if ( data ) { + return data; + } + + const userData = await apiFetch( { + path: '/wp/v2/users/me', + } ); + + data = userData?.meta?.persisted_preferences; + + return data; + } + + async function set( newData ) { + data = { ...newData }; + + return apiFetch( { + path: '/wp/v2/users/me', + method: 'PUT', + data: { + meta: { + persisted_preferences: newData, + }, + }, + } ); + } + + return { + get, + set, + }; +} diff --git a/packages/wp-preferences/src/index.js b/packages/wp-preferences/src/index.js index 43deb08716e1ab..ea866c3a234676 100644 --- a/packages/wp-preferences/src/index.js +++ b/packages/wp-preferences/src/index.js @@ -1,26 +1,2 @@ -/** - * WordPress dependencies - */ -import { dispatch } from '@wordpress/data'; -import { store as preferencesStore } from '@wordpress/preferences'; - -/** - * Internal dependencies - */ -import createDatabasePersistenceLayer from './database-persistence-layer'; -import migrate from './migrations'; - +export { default as createDatabasePersistenceLayer } from './database-persistence-layer'; export * from './components'; - -export async function configurePreferences( { defaults, storageKey } ) { - const databasePersistenceLayer = createDatabasePersistenceLayer( { - storageKey, - __unstableMigrate: migrate, - } ); - - await dispatch( preferencesStore ).setPersistenceLayer( - databasePersistenceLayer - ); - - dispatch( preferencesStore ).setDefaults( 'core/edit-post', defaults ); -} From 5be7bd5594b4bef6ccb1ab5451a03a53fdd83990 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 29 Mar 2022 16:27:56 +0800 Subject: [PATCH 11/74] Only set preference values with particular actions --- packages/preferences/src/store/reducer.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/preferences/src/store/reducer.js b/packages/preferences/src/store/reducer.js index fb0a98e4ea2c8e..666f8b84ef7dd4 100644 --- a/packages/preferences/src/store/reducer.js +++ b/packages/preferences/src/store/reducer.js @@ -51,14 +51,20 @@ function withPersistenceLayer( reducer ) { persistenceLayer = persistence; // TODO - is this the best strategy? - // Prioritize any user changes to state. - return { + // Handle any changes to state that may have ocurred before + // preferences were loaded. + const mergedState = { ...persistedData, ...nextState, }; + persistenceLayer?.set( mergedState ); + + return mergedState; } - persistenceLayer?.set( nextState ); + if ( action.type === 'SET_PREFERENCE_VALUE' ) { + persistenceLayer?.set( nextState ); + } return nextState; }; From 7d9a302e5a3679f64f6febcf41feb859402f8079 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 29 Mar 2022 16:29:29 +0800 Subject: [PATCH 12/74] Remove component for now --- .../wp-preferences/src/components/index.js | 1 - .../preference-toggle-menu-item/README.md | 58 ----------------- .../preference-toggle-menu-item/index.js | 62 ------------------- packages/wp-preferences/src/index.js | 1 - 4 files changed, 122 deletions(-) delete mode 100644 packages/wp-preferences/src/components/index.js delete mode 100644 packages/wp-preferences/src/components/preference-toggle-menu-item/README.md delete mode 100644 packages/wp-preferences/src/components/preference-toggle-menu-item/index.js diff --git a/packages/wp-preferences/src/components/index.js b/packages/wp-preferences/src/components/index.js deleted file mode 100644 index 7bd32262d3abe4..00000000000000 --- a/packages/wp-preferences/src/components/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as PreferenceToggleMenuItem } from './preference-toggle-menu-item'; diff --git a/packages/wp-preferences/src/components/preference-toggle-menu-item/README.md b/packages/wp-preferences/src/components/preference-toggle-menu-item/README.md deleted file mode 100644 index 0bd270f16804c7..00000000000000 --- a/packages/wp-preferences/src/components/preference-toggle-menu-item/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# PreferenceToggleMenuItem - -`PreferenceToggleMenuItem` renders a menu item that is connected to the preference package's store, and will toggle the value of a 'preference' between true and false. - -This component implements a `MenuItem` component from the `@wordpress/components` package. - -## Props - -### scope - -The 'scope' of the feature. This is usually a namespaced string that represents the name of the editor (e.g. 'core/edit-post'), and often matches the name of the store for the editor. - -- Type: `String` -- Required: Yes - -### name - -The name of the preference to toggle (e.g. 'fixedToolbar'). - -- Type: `String` -- Required: Yes - -### label - -A human readable label for the feature. - -- Type: `String` -- Required: Yes - -### info - -A human readable description of what this toggle does. - -- Type: `Object` -- Required: No - -### messageActivated - -A message read by a screen reader when the feature is activated. (e.g. 'Fixed toolbar activated') - -- Type: `String` -- Required: No - -### messageDeactivated - -A message read by a screen reader when the feature is deactivated. (e.g. 'Fixed toolbar deactivated') - -- Type: `String` -- Required: No - -### shortcut - -A keyboard shortcut for the feature. This is just used for display purposes and the implementation of the shortcut should be handled separately. - -Consider using the `displayShortcut` helper from the `@wordpress/keycodes` package for this prop. - -- Type: `Array` -- Required: No diff --git a/packages/wp-preferences/src/components/preference-toggle-menu-item/index.js b/packages/wp-preferences/src/components/preference-toggle-menu-item/index.js deleted file mode 100644 index 53eef4a115faf4..00000000000000 --- a/packages/wp-preferences/src/components/preference-toggle-menu-item/index.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * WordPress dependencies - */ -import { speak } from '@wordpress/a11y'; -import { MenuItem } from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { __, sprintf } from '@wordpress/i18n'; -import { check } from '@wordpress/icons'; -import { store as preferencesStore } from '@wordpress/preferences'; - -export default function PreferenceToggleMenuItem( { - scope, - name, - label, - info, - messageActivated, - messageDeactivated, - shortcut, -} ) { - const isActive = useSelect( - ( select ) => !! select( preferencesStore ).get( scope, name ), - [ name ] - ); - const { toggle } = useDispatch( preferencesStore ); - const speakMessage = () => { - if ( isActive ) { - const message = - messageDeactivated || - sprintf( - /* translators: %s: preference name, e.g. 'Fullscreen mode' */ - __( 'Preference deactivated - %s' ), - label - ); - speak( message ); - } else { - const message = - messageActivated || - sprintf( - /* translators: %s: preference name, e.g. 'Fullscreen mode' */ - __( 'Preference activated - %s' ), - label - ); - speak( message ); - } - }; - - return ( - { - toggle( scope, name ); - speakMessage(); - } } - role="menuitemcheckbox" - info={ info } - shortcut={ shortcut } - > - { label } - - ); -} diff --git a/packages/wp-preferences/src/index.js b/packages/wp-preferences/src/index.js index ea866c3a234676..5735893b4221f3 100644 --- a/packages/wp-preferences/src/index.js +++ b/packages/wp-preferences/src/index.js @@ -1,2 +1 @@ export { default as createDatabasePersistenceLayer } from './database-persistence-layer'; -export * from './components'; From 4477ca5eefe3d47e0dd1ef1edc7c3790d142ded0 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 29 Mar 2022 16:42:22 +0800 Subject: [PATCH 13/74] Make native initialize function async --- packages/edit-post/src/index.native.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-post/src/index.native.js b/packages/edit-post/src/index.native.js index 48bf4185ba2855..90b77937ae54b4 100644 --- a/packages/edit-post/src/index.native.js +++ b/packages/edit-post/src/index.native.js @@ -19,8 +19,8 @@ import configurePreferences from './utils/configure-preferences'; * @param {Object} postType Post type of the post to edit. * @param {Object} postId ID of the post to edit (unused right now) */ -export function initializeEditor( id, postType, postId ) { - configurePreferences( { +export async function initializeEditor( id, postType, postId ) { + await configurePreferences( { editorMode: 'visual', fixedToolbar: false, fullscreenMode: true, From a4007a88227ded9bd298fce595af4089c2ffd25b Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 29 Mar 2022 17:03:11 +0800 Subject: [PATCH 14/74] Revert changes to preferences package.json --- package-lock.json | 7 ++++++- packages/preferences/package.json | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 93510266b4f70e..c51513882a4993 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18076,7 +18076,12 @@ "version": "file:packages/preferences", "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/data": "file:packages/data" + "@wordpress/a11y": "file:packages/a11y", + "@wordpress/components": "file:packages/components", + "@wordpress/data": "file:packages/data", + "@wordpress/i18n": "file:packages/i18n", + "@wordpress/icons": "file:packages/icons", + "classnames": "^2.3.1" } }, "@wordpress/prettier-config": { diff --git a/packages/preferences/package.json b/packages/preferences/package.json index 4f6bf606dc9614..f620b89a5148be 100644 --- a/packages/preferences/package.json +++ b/packages/preferences/package.json @@ -30,7 +30,12 @@ "sideEffects": false, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/data": "file:../data" + "@wordpress/a11y": "file:../a11y", + "@wordpress/components": "file:../components", + "@wordpress/data": "file:../data", + "@wordpress/i18n": "file:../i18n", + "@wordpress/icons": "file:../icons", + "classnames": "^2.3.1" }, "peerDependencies": { "react": "^17.0.0", From 2af0bd0ed0c44db24d3cf7af3779f2b9ae460620 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 30 Mar 2022 12:57:48 +0800 Subject: [PATCH 15/74] Try using an inline script to configure preferences --- docs/manifest.json | 12 ++--- lib/experimental/persisted-preferences.php | 25 ++++++++-- package-lock.json | 24 ++++----- package.json | 2 +- .../.npmrc | 0 .../CHANGELOG.md | 0 packages/database-persistence-layer/README.md | 3 ++ .../package.json | 12 ++--- .../src/create/index.js | 49 +++++++++++++++++++ .../database-persistence-layer/src/index.js | 17 +++++++ .../src/migrations/index.js | 0 packages/edit-post/package.json | 2 +- packages/edit-post/src/index.js | 7 +-- packages/edit-post/src/index.native.js | 7 +-- .../src/utils/configure-preferences.js | 14 ------ packages/preferences/src/store/index.js | 3 -- packages/wp-preferences/README.md | 3 -- .../src/database-persistence-layer/index.js | 41 ---------------- packages/wp-preferences/src/index.js | 1 - 19 files changed, 121 insertions(+), 101 deletions(-) rename packages/{wp-preferences => database-persistence-layer}/.npmrc (100%) rename packages/{wp-preferences => database-persistence-layer}/CHANGELOG.md (100%) create mode 100644 packages/database-persistence-layer/README.md rename packages/{wp-preferences => database-persistence-layer}/package.json (70%) create mode 100644 packages/database-persistence-layer/src/create/index.js create mode 100644 packages/database-persistence-layer/src/index.js rename packages/{wp-preferences => database-persistence-layer}/src/migrations/index.js (100%) delete mode 100644 packages/edit-post/src/utils/configure-preferences.js delete mode 100644 packages/wp-preferences/README.md delete mode 100644 packages/wp-preferences/src/database-persistence-layer/index.js delete mode 100644 packages/wp-preferences/src/index.js diff --git a/docs/manifest.json b/docs/manifest.json index 939318182a6b28..b921bf4a869c1f 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1481,6 +1481,12 @@ "markdown_source": "../packages/data/README.md", "parent": "packages" }, + { + "title": "@wordpress/database-persistence-layer", + "slug": "packages-database-persistence-layer", + "markdown_source": "../packages/database-persistence-layer/README.md", + "parent": "packages" + }, { "title": "@wordpress/date", "slug": "packages-date", @@ -1835,12 +1841,6 @@ "markdown_source": "../packages/wordcount/README.md", "parent": "packages" }, - { - "title": "@wordpress/wp-preferences", - "slug": "packages-wp-preferences", - "markdown_source": "../packages/wp-preferences/README.md", - "parent": "packages" - }, { "title": "Data Module Reference", "slug": "data", diff --git a/lib/experimental/persisted-preferences.php b/lib/experimental/persisted-preferences.php index 00da8ed4512dc1..2daf0f251781c3 100644 --- a/lib/experimental/persisted-preferences.php +++ b/lib/experimental/persisted-preferences.php @@ -8,11 +8,18 @@ /** * Register the user meta for persisted preferences. */ -function gutenberg_register_persisted_preferences_user_meta() { +function gutenberg_configure_persisted_preferences() { + $user_id = get_current_user_id(); + if ( empty( $user_id ) ) { + return; + } + global $wpdb; + + $meta_key = $wpdb->get_blog_prefix() . 'persisted_preferences'; register_meta( 'user', - $wpdb->get_blog_prefix() . 'persisted_preferences', + $meta_key, array( 'type' => 'object', 'single' => true, @@ -27,6 +34,18 @@ function gutenberg_register_persisted_preferences_user_meta() { ), ) ); + + $preload_data = get_user_meta( $user_id, $meta_key, true ); + + wp_add_inline_script( + 'wp-database-persistence-layer', + sprintf( + 'wp.databasePersistenceLayer.__experimentalConfigureDatabasePersistenceLayer( { preloadedData: %s } );', + wp_json_encode( $preload_data ) + ), + 'after' + ); } -add_action( 'init', 'gutenberg_register_persisted_preferences_user_meta' ); +add_action( 'init', 'gutenberg_configure_persisted_preferences' ); + diff --git a/package-lock.json b/package-lock.json index c51513882a4993..23093a5fdc4d1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17430,6 +17430,15 @@ "@wordpress/deprecated": "file:packages/deprecated" } }, + "@wordpress/database-persistence-layer": { + "version": "file:packages/database-persistence-layer", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/api-fetch": "file:packages/api-fetch", + "@wordpress/data": "file:packages/data", + "@wordpress/preferences": "file:packages/preferences" + } + }, "@wordpress/date": { "version": "file:packages/date", "requires": { @@ -17566,6 +17575,7 @@ "@wordpress/compose": "file:packages/compose", "@wordpress/core-data": "file:packages/core-data", "@wordpress/data": "file:packages/data", + "@wordpress/database-persistence-layer": "file:packages/database-persistence-layer", "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/editor": "file:packages/editor", "@wordpress/element": "file:packages/element", @@ -17582,7 +17592,6 @@ "@wordpress/url": "file:packages/url", "@wordpress/viewport": "file:packages/viewport", "@wordpress/warning": "file:packages/warning", - "@wordpress/wp-preferences": "file:packages/wp-preferences", "classnames": "^2.3.1", "lodash": "^4.17.21", "memize": "^1.1.0", @@ -18389,19 +18398,6 @@ "lodash": "^4.17.21" } }, - "@wordpress/wp-preferences": { - "version": "file:packages/wp-preferences", - "requires": { - "@babel/runtime": "^7.16.0", - "@wordpress/a11y": "file:packages/a11y", - "@wordpress/api-fetch": "file:packages/api-fetch", - "@wordpress/components": "file:packages/components", - "@wordpress/data": "file:packages/data", - "@wordpress/i18n": "file:packages/i18n", - "@wordpress/icons": "file:packages/icons", - "@wordpress/preferences": "file:packages/preferences" - } - }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", diff --git a/package.json b/package.json index b28f11c78fd21d..54ed98046ae5aa 100755 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@wordpress/customize-widgets": "file:packages/customize-widgets", "@wordpress/data": "file:packages/data", "@wordpress/data-controls": "file:packages/data-controls", + "@wordpress/database-persistence-layer": "file:packages/database-persistence-layer", "@wordpress/date": "file:packages/date", "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/dom": "file:packages/dom", @@ -63,7 +64,6 @@ "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", "@wordpress/nux": "file:packages/nux", - "@wordpress/wp-preferences": "file:packages/wp-preferences", "@wordpress/plugins": "file:packages/plugins", "@wordpress/preferences": "file:packages/preferences", "@wordpress/primitives": "file:packages/primitives", diff --git a/packages/wp-preferences/.npmrc b/packages/database-persistence-layer/.npmrc similarity index 100% rename from packages/wp-preferences/.npmrc rename to packages/database-persistence-layer/.npmrc diff --git a/packages/wp-preferences/CHANGELOG.md b/packages/database-persistence-layer/CHANGELOG.md similarity index 100% rename from packages/wp-preferences/CHANGELOG.md rename to packages/database-persistence-layer/CHANGELOG.md diff --git a/packages/database-persistence-layer/README.md b/packages/database-persistence-layer/README.md new file mode 100644 index 00000000000000..e6510baf769952 --- /dev/null +++ b/packages/database-persistence-layer/README.md @@ -0,0 +1,3 @@ +# Database persistence layer + +A persistence layer for `@wordpress/preferences` that stores data in the WordPress user meta. diff --git a/packages/wp-preferences/package.json b/packages/database-persistence-layer/package.json similarity index 70% rename from packages/wp-preferences/package.json rename to packages/database-persistence-layer/package.json index 1d54e05c3df9f0..8862445c7a6e63 100644 --- a/packages/wp-preferences/package.json +++ b/packages/database-persistence-layer/package.json @@ -1,7 +1,7 @@ { - "name": "@wordpress/wp-preferences", + "name": "@wordpress/database-persistence-layer", "version": "1.0.0", - "description": "Wordpress-specific utilities for preferences.", + "description": "A persistence layer that stores data in the WordPress database.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", "keywords": [ @@ -10,11 +10,11 @@ "preferences", "settings" ], - "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/wp-preferences/README.md", + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/database-persistence-layer/README.md", "repository": { "type": "git", "url": "https://github.com/WordPress/gutenberg.git", - "directory": "packages/wp-preferences" + "directory": "packages/database-persistence-layer" }, "bugs": { "url": "https://github.com/WordPress/gutenberg/issues" @@ -29,12 +29,8 @@ "sideEffects": false, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", - "@wordpress/components": "file:../components", "@wordpress/data": "file:../data", - "@wordpress/i18n": "file:../i18n", - "@wordpress/icons": "file:../icons", "@wordpress/preferences": "file:../preferences" }, "publishConfig": { diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js new file mode 100644 index 00000000000000..476eb99871a234 --- /dev/null +++ b/packages/database-persistence-layer/src/create/index.js @@ -0,0 +1,49 @@ +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +/** + * Creates a database persistence layer, storing data in the user meta. + * + * @param {Object} options + * @param {Object} options.preloadedData Any persisted data that should be preloaded. + * + * @return {Object} A database persistence layer. + */ +export default function create( { preloadedData } ) { + let cache = preloadedData; + + async function get() { + if ( cache ) { + return cache; + } + + const user = await apiFetch( { + path: '/wp/v2/users/me', + } ); + + cache = user?.meta?.persisted_preferences; + + return cache; + } + + async function set( newData ) { + cache = { ...newData }; + + return apiFetch( { + path: '/wp/v2/users/me', + method: 'PUT', + data: { + meta: { + persisted_preferences: newData, + }, + }, + } ); + } + + return { + get, + set, + }; +} diff --git a/packages/database-persistence-layer/src/index.js b/packages/database-persistence-layer/src/index.js new file mode 100644 index 00000000000000..526ecfa53324ef --- /dev/null +++ b/packages/database-persistence-layer/src/index.js @@ -0,0 +1,17 @@ +/** + * WordPress dependencies + */ +import { dispatch } from '@wordpress/data'; +import { store as preferencesStore } from '@wordpress/preferences'; + +/** + * Internal dependencies + */ +import create from './create'; + +export { create }; + +export function __experimentalConfigureDatabasePersistenceLayer( options ) { + const persistenceLayer = create( { options } ); + dispatch( preferencesStore ).setPersistenceLayer( persistenceLayer ); +} diff --git a/packages/wp-preferences/src/migrations/index.js b/packages/database-persistence-layer/src/migrations/index.js similarity index 100% rename from packages/wp-preferences/src/migrations/index.js rename to packages/database-persistence-layer/src/migrations/index.js diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index e304d53cf31ac2..13fb528faa07b3 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -36,6 +36,7 @@ "@wordpress/compose": "file:../compose", "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", + "@wordpress/database-persistence-layer": "file:../database-persistence-layer", "@wordpress/deprecated": "file:../deprecated", "@wordpress/editor": "file:../editor", "@wordpress/element": "file:../element", @@ -52,7 +53,6 @@ "@wordpress/url": "file:../url", "@wordpress/viewport": "file:../viewport", "@wordpress/warning": "file:../warning", - "@wordpress/wp-preferences": "file:../wp-preferences", "classnames": "^2.3.1", "lodash": "^4.17.21", "memize": "^1.1.0", diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 156f9b4812170e..a956ae1b0740c3 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -9,6 +9,8 @@ import { import { render, unmountComponentAtNode } from '@wordpress/element'; import { dispatch, select } from '@wordpress/data'; import { addFilter } from '@wordpress/hooks'; +import { store as preferencesStore } from '@wordpress/preferences'; +import '@wordpress/database-persistence-layer'; /** * Internal dependencies @@ -17,7 +19,6 @@ import './hooks'; import './plugins'; import Editor from './editor'; import { store as editPostStore } from './store'; -import configurePreferences from './utils/configure-preferences'; /** * Reinitializes the editor after the user chooses to reboot the editor after @@ -73,7 +74,7 @@ export function reinitializeEditor( * considered as non-user-initiated (bypass for * unsaved changes prompt). */ -export async function initializeEditor( +export function initializeEditor( id, postType, postId, @@ -106,7 +107,7 @@ export async function initializeEditor( initialEdits ); - await configurePreferences( { + dispatch( preferencesStore ).setDefaults( 'core/edit-post', { editorMode: 'visual', fixedToolbar: false, fullscreenMode: true, diff --git a/packages/edit-post/src/index.native.js b/packages/edit-post/src/index.native.js index 90b77937ae54b4..b825d50ace0c01 100644 --- a/packages/edit-post/src/index.native.js +++ b/packages/edit-post/src/index.native.js @@ -3,13 +3,14 @@ */ import '@wordpress/core-data'; import '@wordpress/format-library'; +import { dispatch } from '@wordpress/data'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies */ export { store } from './store'; import Editor from './editor'; -import configurePreferences from './utils/configure-preferences'; /** * Initializes the Editor and returns a componentProvider @@ -19,8 +20,8 @@ import configurePreferences from './utils/configure-preferences'; * @param {Object} postType Post type of the post to edit. * @param {Object} postId ID of the post to edit (unused right now) */ -export async function initializeEditor( id, postType, postId ) { - await configurePreferences( { +export function initializeEditor( id, postType, postId ) { + dispatch( preferencesStore ).setDefaults( 'core/edit-post', { editorMode: 'visual', fixedToolbar: false, fullscreenMode: true, diff --git a/packages/edit-post/src/utils/configure-preferences.js b/packages/edit-post/src/utils/configure-preferences.js deleted file mode 100644 index ac0585df795b42..00000000000000 --- a/packages/edit-post/src/utils/configure-preferences.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * WordPress dependencies - */ -import { dispatch } from '@wordpress/data'; -import { store as preferencesStore } from '@wordpress/preferences'; -import { createDatabasePersistenceLayer } from '@wordpress/wp-preferences'; - -export default async function configurePreferences( defaults ) { - const databasePersistenceLayer = createDatabasePersistenceLayer(); - await dispatch( preferencesStore ).setPersistenceLayer( - databasePersistenceLayer - ); - dispatch( preferencesStore ).setDefaults( 'core/edit-post', defaults ); -} diff --git a/packages/preferences/src/store/index.js b/packages/preferences/src/store/index.js index 25e979422b2e7f..0c2421966a0d79 100644 --- a/packages/preferences/src/store/index.js +++ b/packages/preferences/src/store/index.js @@ -3,9 +3,6 @@ */ import { createReduxStore, register } from '@wordpress/data'; -/** - * Internal dependencies - */ /** * Internal dependencies */ diff --git a/packages/wp-preferences/README.md b/packages/wp-preferences/README.md deleted file mode 100644 index 2d6bd56c4ff17c..00000000000000 --- a/packages/wp-preferences/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# WP Preferences - -WordPress specific utilities for preferences. diff --git a/packages/wp-preferences/src/database-persistence-layer/index.js b/packages/wp-preferences/src/database-persistence-layer/index.js deleted file mode 100644 index 22590fbd6ea0bc..00000000000000 --- a/packages/wp-preferences/src/database-persistence-layer/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * WordPress dependencies - */ -import apiFetch from '@wordpress/api-fetch'; - -export default function createDatabasePersistenceLayer() { - let data; - - async function get() { - if ( data ) { - return data; - } - - const userData = await apiFetch( { - path: '/wp/v2/users/me', - } ); - - data = userData?.meta?.persisted_preferences; - - return data; - } - - async function set( newData ) { - data = { ...newData }; - - return apiFetch( { - path: '/wp/v2/users/me', - method: 'PUT', - data: { - meta: { - persisted_preferences: newData, - }, - }, - } ); - } - - return { - get, - set, - }; -} diff --git a/packages/wp-preferences/src/index.js b/packages/wp-preferences/src/index.js deleted file mode 100644 index 5735893b4221f3..00000000000000 --- a/packages/wp-preferences/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as createDatabasePersistenceLayer } from './database-persistence-layer'; From 3f68f83908348e9209ad449aab9135cd2f86135e Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 31 Mar 2022 10:36:36 +0800 Subject: [PATCH 16/74] Use a filter to add the necessary script dependencies --- lib/experimental/persisted-preferences.php | 30 ++++++++++++++++++- package-lock.json | 4 +-- .../database-persistence-layer/package.json | 4 +-- .../database-persistence-layer/src/index.js | 18 +---------- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/lib/experimental/persisted-preferences.php b/lib/experimental/persisted-preferences.php index 2daf0f251781c3..641f40905bee98 100644 --- a/lib/experimental/persisted-preferences.php +++ b/lib/experimental/persisted-preferences.php @@ -40,12 +40,40 @@ function gutenberg_configure_persisted_preferences() { wp_add_inline_script( 'wp-database-persistence-layer', sprintf( - 'wp.databasePersistenceLayer.__experimentalConfigureDatabasePersistenceLayer( { preloadedData: %s } );', + 'const { create } = wp.databasePersistenceLayer; + const persistenceLayer = create( { preloadedData: %s } ); + const { store: preferencesStore } = wp.preferences; + wp.data.dispatch( "core/preferences" ).setPersistenceLayer( persistenceLayer );', wp_json_encode( $preload_data ) ), 'after' ); + } add_action( 'init', 'gutenberg_configure_persisted_preferences' ); +/** + * Register dependencies for the inline script that configures the persistence layer. + * + * Note: When porting this to core add an extra case to the switch here: + * https://github.com/WordPress/wordpress-develop/blob/d2ab3d183740c3d1252cb921b18005495007e022/src/wp-includes/script-loader.php#L251-L258 + * + * And the same to the gutenberg client assets file here: + * https://github.com/WordPress/gutenberg/blob/3f3c8df23c70a37b7ac4dddebc82030362133593/lib/client-assets.php#L242-L254 + * ``` + * case 'wp-database-persistence-layer': + * array_push( $dependencies, 'wp-data', 'wp-preferences' ); + * break; + * ``` + * + * @param WP_Scripts $scripts An instance of WP_Scripts. + */ +function gutenberg_update_database_persistence_layer_deps( $scripts ) { + $script = $scripts->query( 'wp-database-persistence-layer', 'registered' ); + if ( isset( $script->deps ) ) { + array_push( $script->deps, 'wp-data', 'wp-preferences' ); + } +} + +add_action( 'wp_default_scripts', 'gutenberg_update_database_persistence_layer_deps', 11 ); diff --git a/package-lock.json b/package-lock.json index 23093a5fdc4d1f..0b68c51ec362c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17434,9 +17434,7 @@ "version": "file:packages/database-persistence-layer", "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/api-fetch": "file:packages/api-fetch", - "@wordpress/data": "file:packages/data", - "@wordpress/preferences": "file:packages/preferences" + "@wordpress/api-fetch": "file:packages/api-fetch" } }, "@wordpress/date": { diff --git a/packages/database-persistence-layer/package.json b/packages/database-persistence-layer/package.json index 8862445c7a6e63..59342b258f7905 100644 --- a/packages/database-persistence-layer/package.json +++ b/packages/database-persistence-layer/package.json @@ -29,9 +29,7 @@ "sideEffects": false, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/api-fetch": "file:../api-fetch", - "@wordpress/data": "file:../data", - "@wordpress/preferences": "file:../preferences" + "@wordpress/api-fetch": "file:../api-fetch" }, "publishConfig": { "access": "public" diff --git a/packages/database-persistence-layer/src/index.js b/packages/database-persistence-layer/src/index.js index 526ecfa53324ef..c02d86cd8cb441 100644 --- a/packages/database-persistence-layer/src/index.js +++ b/packages/database-persistence-layer/src/index.js @@ -1,17 +1 @@ -/** - * WordPress dependencies - */ -import { dispatch } from '@wordpress/data'; -import { store as preferencesStore } from '@wordpress/preferences'; - -/** - * Internal dependencies - */ -import create from './create'; - -export { create }; - -export function __experimentalConfigureDatabasePersistenceLayer( options ) { - const persistenceLayer = create( { options } ); - dispatch( preferencesStore ).setPersistenceLayer( persistenceLayer ); -} +export { default as create } from './create'; From ee97cd78eb019248b112c31d7b8b99f1e54881e5 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 31 Mar 2022 12:48:05 +0800 Subject: [PATCH 17/74] Remove import and package.json dependency from edit-post in favor of wiring up scripts in PHP --- lib/experimental/persisted-preferences.php | 44 +++++++++++++++++++--- package-lock.json | 1 - packages/edit-post/package.json | 1 - packages/edit-post/src/index.js | 1 - 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/lib/experimental/persisted-preferences.php b/lib/experimental/persisted-preferences.php index 641f40905bee98..7b334377b0e440 100644 --- a/lib/experimental/persisted-preferences.php +++ b/lib/experimental/persisted-preferences.php @@ -56,23 +56,57 @@ function gutenberg_configure_persisted_preferences() { /** * Register dependencies for the inline script that configures the persistence layer. * - * Note: When porting this to core add an extra case to the switch here: + * Note: When porting this to core update the code here: * https://github.com/WordPress/wordpress-develop/blob/d2ab3d183740c3d1252cb921b18005495007e022/src/wp-includes/script-loader.php#L251-L258 * - * And the same to the gutenberg client assets file here: + * And make the same update to the gutenberg client assets file here: * https://github.com/WordPress/gutenberg/blob/3f3c8df23c70a37b7ac4dddebc82030362133593/lib/client-assets.php#L242-L254 + * + * The update should be something like this: * ``` * case 'wp-database-persistence-layer': * array_push( $dependencies, 'wp-data', 'wp-preferences' ); * break; + * case 'wp-edit-post': + * array_push( $dependencies, // ... other deps, 'wp-database-persistence-layer' ); + * break; + * case 'wp-edit-site': + * array_push( $dependencies, // ... other deps, 'wp-database-persistence-layer' ); + * break; + * case 'wp-edit-widgets': + * array_push( $dependencies, // ... other deps, 'wp-database-persistence-layer' ); + * break; + * case 'wp-customize-widgets': + * array_push( $dependencies, // ... other deps, 'wp-database-persistence-layer' ); + * break; * ``` * * @param WP_Scripts $scripts An instance of WP_Scripts. */ function gutenberg_update_database_persistence_layer_deps( $scripts ) { - $script = $scripts->query( 'wp-database-persistence-layer', 'registered' ); - if ( isset( $script->deps ) ) { - array_push( $script->deps, 'wp-data', 'wp-preferences' ); + $persistence_script = $scripts->query( 'wp-database-persistence-layer', 'registered' ); + if ( isset( $persistence_script->deps ) ) { + array_push( $persistence_script->deps, 'wp-data', 'wp-preferences' ); + } + + $edit_post_script = $scripts->query( 'wp-edit-post', 'registered' ); + if ( isset( $edit_post_script->deps ) ) { + array_push( $edit_post_script->deps, 'wp-database-persistence-layer' ); + } + + $edit_site_script = $scripts->query( 'wp-edit-site', 'registered' ); + if ( isset( $edit_site_script->deps ) ) { + array_push( $edit_site_script->deps, 'wp-database-persistence-layer' ); + } + + $edit_widgets_script = $scripts->query( 'wp-edit-widgets', 'registered' ); + if ( isset( $edit_widgets_script->deps ) ) { + array_push( $edit_widgets_script->deps, 'wp-database-persistence-layer' ); + } + + $customize_widgets_script = $scripts->query( 'wp-customize-widgets', 'registered' ); + if ( isset( $customize_widgets_script->deps ) ) { + array_push( $customize_widgets_script->deps, 'wp-database-persistence-layer' ); } } diff --git a/package-lock.json b/package-lock.json index 0b68c51ec362c4..cbda08b89d4455 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17573,7 +17573,6 @@ "@wordpress/compose": "file:packages/compose", "@wordpress/core-data": "file:packages/core-data", "@wordpress/data": "file:packages/data", - "@wordpress/database-persistence-layer": "file:packages/database-persistence-layer", "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/editor": "file:packages/editor", "@wordpress/element": "file:packages/element", diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 13fb528faa07b3..a48a13a92af580 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -36,7 +36,6 @@ "@wordpress/compose": "file:../compose", "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", - "@wordpress/database-persistence-layer": "file:../database-persistence-layer", "@wordpress/deprecated": "file:../deprecated", "@wordpress/editor": "file:../editor", "@wordpress/element": "file:../element", diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index a956ae1b0740c3..6ff6a7603d89f0 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -10,7 +10,6 @@ import { render, unmountComponentAtNode } from '@wordpress/element'; import { dispatch, select } from '@wordpress/data'; import { addFilter } from '@wordpress/hooks'; import { store as preferencesStore } from '@wordpress/preferences'; -import '@wordpress/database-persistence-layer'; /** * Internal dependencies From 145c79dfd1f0a040d132bc3d2590255be847a664 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 31 Mar 2022 12:54:32 +0800 Subject: [PATCH 18/74] Remove merging of state --- packages/preferences/src/store/reducer.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/preferences/src/store/reducer.js b/packages/preferences/src/store/reducer.js index 666f8b84ef7dd4..ae108cbbab6c5f 100644 --- a/packages/preferences/src/store/reducer.js +++ b/packages/preferences/src/store/reducer.js @@ -43,25 +43,15 @@ function withPersistenceLayer( reducer ) { let persistenceLayer; return ( state, action ) => { - const nextState = reducer( state, action ); - + // Setup the persistence layer, and return the persisted data + // as the state. if ( action.type === 'SET_PERSISTENCE_LAYER' ) { const { persistenceLayer: persistence, persistedData } = action; - persistenceLayer = persistence; - - // TODO - is this the best strategy? - // Handle any changes to state that may have ocurred before - // preferences were loaded. - const mergedState = { - ...persistedData, - ...nextState, - }; - persistenceLayer?.set( mergedState ); - - return mergedState; + return persistedData; } + const nextState = reducer( state, action ); if ( action.type === 'SET_PREFERENCE_VALUE' ) { persistenceLayer?.set( nextState ); } From 4e7a13a5296d162b7e66ee5bd61be3d8168f827f Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 31 Mar 2022 14:27:28 +0800 Subject: [PATCH 19/74] Try abort controller for cancelling multiple in flight requests --- .../src/create/index.js | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index 476eb99871a234..93417de54a9bfc 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -13,6 +13,7 @@ import apiFetch from '@wordpress/api-fetch'; */ export default function create( { preloadedData } ) { let cache = preloadedData; + let abortController = null; async function get() { if ( cache ) { @@ -31,7 +32,14 @@ export default function create( { preloadedData } ) { async function set( newData ) { cache = { ...newData }; - return apiFetch( { + abortController?.abort(); + + abortController = + typeof AbortController === 'undefined' + ? undefined + : new AbortController(); + + const promise = apiFetch( { path: '/wp/v2/users/me', method: 'PUT', data: { @@ -39,7 +47,18 @@ export default function create( { preloadedData } ) { persisted_preferences: newData, }, }, - } ); + signal: abortController?.signal, + } ) + .catch( ( error ) => { + if ( error.code !== error.ABORT_ERR ) { + throw error; + } + } ) + .finally( () => { + abortController = null; + } ); + + return promise; } return { From 7d5ed020224ffa0445dee70b318e197364494698 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 31 Mar 2022 14:36:14 +0800 Subject: [PATCH 20/74] Revert "Try abort controller for cancelling multiple in flight requests" This reverts commit 06345b57253f80c3619be3e34c6c5b0e2d6aa0b9. --- .../src/create/index.js | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index 93417de54a9bfc..476eb99871a234 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -13,7 +13,6 @@ import apiFetch from '@wordpress/api-fetch'; */ export default function create( { preloadedData } ) { let cache = preloadedData; - let abortController = null; async function get() { if ( cache ) { @@ -32,14 +31,7 @@ export default function create( { preloadedData } ) { async function set( newData ) { cache = { ...newData }; - abortController?.abort(); - - abortController = - typeof AbortController === 'undefined' - ? undefined - : new AbortController(); - - const promise = apiFetch( { + return apiFetch( { path: '/wp/v2/users/me', method: 'PUT', data: { @@ -47,18 +39,7 @@ export default function create( { preloadedData } ) { persisted_preferences: newData, }, }, - signal: abortController?.signal, - } ) - .catch( ( error ) => { - if ( error.code !== error.ABORT_ERR ) { - throw error; - } - } ) - .finally( () => { - abortController = null; - } ); - - return promise; + } ); } return { From 37c0c6d3cde875e00d178e0b959f7cd098e8d806 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 31 Mar 2022 15:21:47 +0800 Subject: [PATCH 21/74] Try throttling API requests --- .../src/create/create-async-throttle.js | 52 +++++++++++++++++++ .../src/create/index.js | 30 +++++++---- 2 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 packages/database-persistence-layer/src/create/create-async-throttle.js diff --git a/packages/database-persistence-layer/src/create/create-async-throttle.js b/packages/database-persistence-layer/src/create/create-async-throttle.js new file mode 100644 index 00000000000000..63936760dbdfd1 --- /dev/null +++ b/packages/database-persistence-layer/src/create/create-async-throttle.js @@ -0,0 +1,52 @@ +/** + * Performs a leading edge throttle of async functions. + * + * If three functions are throttled at the same time: + * - The first happens immediately. + * - The second is never called. + * - The third happens `delayMS` milliseconds after the first has resolved. + * + * @param {number} delayMS A delay in milliseconds. + * @return {Function} A function that throttle whatever function is passed + * to it. + */ +export default function createAsyncThrottle( delayMS ) { + let timeoutId; + let activePromise; + + return async function throttle( func ) { + // This is a leading edge throttle. If there's no promise or timeout + // in progress, + if ( ! activePromise && ! timeoutId ) { + // Keep a reference to the promise. + activePromise = func().finally( () => { + // As soon this promise is complete, clear the way for the + // next one to happen immediately. + activePromise = null; + } ); + return; + } + + if ( activePromise ) { + // Let any active promises finish before queuing the next request. + await activePromise; + } + + // Clear any active timeouts, abandoning any requests that have + // been queued but not been made. + if ( timeoutId ) { + window.clearTimeout( timeoutId ); + timeoutId = null; + } + + // Schedule the next request but with a delay. + timeoutId = setTimeout( () => { + activePromise = func().finally( () => { + // As soon this promise is complete, clear the way for the + // next one to happen immediately. + activePromise = null; + timeoutId = null; + } ); + }, delayMS ); + }; +} diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index 476eb99871a234..044f051b8839fc 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -3,16 +3,24 @@ */ import apiFetch from '@wordpress/api-fetch'; +/** + * Internal dependencies + */ +import createAsyncThrottle from './create-async-throttle'; + /** * Creates a database persistence layer, storing data in the user meta. * * @param {Object} options - * @param {Object} options.preloadedData Any persisted data that should be preloaded. + * @param {Object} options.preloadedData Any persisted data that should be preloaded. + * @param {number} options.requestThrottleMS Throttle requests to the API so that they only + * happen every n milliseconds. * * @return {Object} A database persistence layer. */ -export default function create( { preloadedData } ) { +export default function create( { preloadedData, requestThrottleMS = 2500 } ) { let cache = preloadedData; + const throttle = createAsyncThrottle( requestThrottleMS ); async function get() { if ( cache ) { @@ -31,15 +39,17 @@ export default function create( { preloadedData } ) { async function set( newData ) { cache = { ...newData }; - return apiFetch( { - path: '/wp/v2/users/me', - method: 'PUT', - data: { - meta: { - persisted_preferences: newData, + throttle( () => + apiFetch( { + path: '/wp/v2/users/me', + method: 'PUT', + data: { + meta: { + persisted_preferences: newData, + }, }, - }, - } ); + } ) + ); } return { From 8835a596321d80ba1bfb6a8ec8392da01061fa85 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 31 Mar 2022 15:43:00 +0800 Subject: [PATCH 22/74] Simplify dependencies --- lib/experimental/persisted-preferences.php | 44 +++------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/lib/experimental/persisted-preferences.php b/lib/experimental/persisted-preferences.php index 7b334377b0e440..adfefa7f964c20 100644 --- a/lib/experimental/persisted-preferences.php +++ b/lib/experimental/persisted-preferences.php @@ -38,7 +38,7 @@ function gutenberg_configure_persisted_preferences() { $preload_data = get_user_meta( $user_id, $meta_key, true ); wp_add_inline_script( - 'wp-database-persistence-layer', + 'wp-preferences', sprintf( 'const { create } = wp.databasePersistenceLayer; const persistenceLayer = create( { preloadedData: %s } ); @@ -62,51 +62,19 @@ function gutenberg_configure_persisted_preferences() { * And make the same update to the gutenberg client assets file here: * https://github.com/WordPress/gutenberg/blob/3f3c8df23c70a37b7ac4dddebc82030362133593/lib/client-assets.php#L242-L254 * - * The update should be something like this: + * The update should be adding a new case like this like this: * ``` - * case 'wp-database-persistence-layer': - * array_push( $dependencies, 'wp-data', 'wp-preferences' ); - * break; - * case 'wp-edit-post': - * array_push( $dependencies, // ... other deps, 'wp-database-persistence-layer' ); - * break; - * case 'wp-edit-site': - * array_push( $dependencies, // ... other deps, 'wp-database-persistence-layer' ); - * break; - * case 'wp-edit-widgets': - * array_push( $dependencies, // ... other deps, 'wp-database-persistence-layer' ); - * break; - * case 'wp-customize-widgets': - * array_push( $dependencies, // ... other deps, 'wp-database-persistence-layer' ); + * case 'wp-preferences': + * array_push( $dependencies, 'wp-database-persistence-layer' ); * break; * ``` * * @param WP_Scripts $scripts An instance of WP_Scripts. */ function gutenberg_update_database_persistence_layer_deps( $scripts ) { - $persistence_script = $scripts->query( 'wp-database-persistence-layer', 'registered' ); + $persistence_script = $scripts->query( 'wp-preferences', 'registered' ); if ( isset( $persistence_script->deps ) ) { - array_push( $persistence_script->deps, 'wp-data', 'wp-preferences' ); - } - - $edit_post_script = $scripts->query( 'wp-edit-post', 'registered' ); - if ( isset( $edit_post_script->deps ) ) { - array_push( $edit_post_script->deps, 'wp-database-persistence-layer' ); - } - - $edit_site_script = $scripts->query( 'wp-edit-site', 'registered' ); - if ( isset( $edit_site_script->deps ) ) { - array_push( $edit_site_script->deps, 'wp-database-persistence-layer' ); - } - - $edit_widgets_script = $scripts->query( 'wp-edit-widgets', 'registered' ); - if ( isset( $edit_widgets_script->deps ) ) { - array_push( $edit_widgets_script->deps, 'wp-database-persistence-layer' ); - } - - $customize_widgets_script = $scripts->query( 'wp-customize-widgets', 'registered' ); - if ( isset( $customize_widgets_script->deps ) ) { - array_push( $customize_widgets_script->deps, 'wp-database-persistence-layer' ); + array_push( $persistence_script->deps, 'wp-database-persistence-layer' ); } } From 8a65181088a07d3577cfbe05ccd259858534de9e Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 1 Apr 2022 15:00:14 +0800 Subject: [PATCH 23/74] Test that preferences are not persisted in tests due to reload --- packages/e2e-test-utils/src/create-new-post.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/e2e-test-utils/src/create-new-post.js b/packages/e2e-test-utils/src/create-new-post.js index 193468e5afacc1..a8117dfd5e73a0 100644 --- a/packages/e2e-test-utils/src/create-new-post.js +++ b/packages/e2e-test-utils/src/create-new-post.js @@ -48,8 +48,7 @@ export async function createNewPost( { wp.data.dispatch( 'core/edit-post' ).toggleFeature( 'welcomeGuide' ) ); - await page.reload(); - await page.waitForSelector( '.edit-post-layout' ); + await page.waitForSelector( '.components-guide', { hidden: true } ); } if ( isFullscreenMode ) { From bbcf4d3e6b50548188ffeefa125b9ab6d968e0a2 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 4 Apr 2022 13:29:53 +0800 Subject: [PATCH 24/74] Reset all preferences in e2e test setup --- packages/e2e-test-utils/README.md | 4 ++++ packages/e2e-test-utils/src/index.js | 1 + packages/e2e-test-utils/src/preferences.js | 19 +++++++++++++++++++ .../e2e-tests/config/setup-test-framework.js | 2 ++ 4 files changed, 26 insertions(+) create mode 100644 packages/e2e-test-utils/src/preferences.js diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md index db839113e77d23..47a914b2d816f0 100644 --- a/packages/e2e-test-utils/README.md +++ b/packages/e2e-test-utils/README.md @@ -687,6 +687,10 @@ _Returns_ - `Promise`: Promise resolving when publish is complete. +### resetPreferences + +Clears all user meta preferences. + ### saveDraft Saves the post as a draft, resolving once the request is complete (once the diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js index 0ae7d379516696..3543fd3dfebb5b 100644 --- a/packages/e2e-test-utils/src/index.js +++ b/packages/e2e-test-utils/src/index.js @@ -67,6 +67,7 @@ export { openDocumentSettingsSidebar } from './open-document-settings-sidebar'; export { openPublishPanel } from './open-publish-panel'; export { openTypographyToolsPanelMenu } from './open-typography-tools-panel-menu'; export { trashAllPosts } from './posts'; +export { resetPreferences } from './preferences'; export { pressKeyTimes } from './press-key-times'; export { pressKeyWithModifier, diff --git a/packages/e2e-test-utils/src/preferences.js b/packages/e2e-test-utils/src/preferences.js new file mode 100644 index 00000000000000..ce43b35834a637 --- /dev/null +++ b/packages/e2e-test-utils/src/preferences.js @@ -0,0 +1,19 @@ +/** + * Internal dependencies + */ +import { rest } from './rest-api'; + +/** + * Clears all user meta preferences. + */ +export async function resetPreferences() { + await rest( { + path: '/wp/v2/users/me', + method: 'PUT', + data: { + meta: { + persisted_preferences: {}, + }, + }, + } ); +} diff --git a/packages/e2e-tests/config/setup-test-framework.js b/packages/e2e-tests/config/setup-test-framework.js index fcf63e0bda002f..95d3cd0fb297ab 100644 --- a/packages/e2e-tests/config/setup-test-framework.js +++ b/packages/e2e-tests/config/setup-test-framework.js @@ -13,6 +13,7 @@ import { clearLocalStorage, enablePageDialogAccept, isOfflineMode, + resetPreferences, setBrowserViewport, trashAllPosts, } from '@wordpress/e2e-test-utils'; @@ -242,6 +243,7 @@ beforeAll( async () => { enablePageDialogAccept(); observeConsoleLogging(); await simulateAdverseConditions(); + await resetPreferences(); await activateTheme( 'twentytwentyone' ); await trashAllPosts(); await trashAllPosts( 'wp_block' ); From 528540b78263e939347148276b07fb624479d82e Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 4 Apr 2022 13:53:11 +0800 Subject: [PATCH 25/74] Try `keepalive` --- packages/database-persistence-layer/src/create/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index 044f051b8839fc..b10d9fec60a3bc 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -43,6 +43,9 @@ export default function create( { preloadedData, requestThrottleMS = 2500 } ) { apiFetch( { path: '/wp/v2/users/me', method: 'PUT', + // `keepalive` will still send the request in the background, + // even when a browser unload event might interrupt it. + keepalive: true, data: { meta: { persisted_preferences: newData, From 20f8b4e3f183af0a03676acd46cee9985e77a275 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 4 Apr 2022 14:01:07 +0800 Subject: [PATCH 26/74] Oh, this is actually a debounce, not a throttle --- ...e-async-throttle.js => create-async-debounce.js} | 13 ++++++++----- .../database-persistence-layer/src/create/index.js | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) rename packages/database-persistence-layer/src/create/{create-async-throttle.js => create-async-debounce.js} (77%) diff --git a/packages/database-persistence-layer/src/create/create-async-throttle.js b/packages/database-persistence-layer/src/create/create-async-debounce.js similarity index 77% rename from packages/database-persistence-layer/src/create/create-async-throttle.js rename to packages/database-persistence-layer/src/create/create-async-debounce.js index 63936760dbdfd1..202f5c2417155f 100644 --- a/packages/database-persistence-layer/src/create/create-async-throttle.js +++ b/packages/database-persistence-layer/src/create/create-async-debounce.js @@ -1,21 +1,24 @@ /** - * Performs a leading edge throttle of async functions. + * Performs a leading edge debounce of async functions. * * If three functions are throttled at the same time: * - The first happens immediately. * - The second is never called. * - The third happens `delayMS` milliseconds after the first has resolved. * + * This is distinct from `lodash.debounce` in that it waits for promise + * resolution. + * * @param {number} delayMS A delay in milliseconds. - * @return {Function} A function that throttle whatever function is passed + * @return {Function} A function that debounce whatever function is passed * to it. */ -export default function createAsyncThrottle( delayMS ) { +export default function createAsyncDebounce( delayMS ) { let timeoutId; let activePromise; - return async function throttle( func ) { - // This is a leading edge throttle. If there's no promise or timeout + return async function debounce( func ) { + // This is a leading edge debounce. If there's no promise or timeout // in progress, if ( ! activePromise && ! timeoutId ) { // Keep a reference to the promise. diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index b10d9fec60a3bc..053c7d51100184 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -6,21 +6,21 @@ import apiFetch from '@wordpress/api-fetch'; /** * Internal dependencies */ -import createAsyncThrottle from './create-async-throttle'; +import createAsyncDebounce from './create-async-debounce'; /** * Creates a database persistence layer, storing data in the user meta. * * @param {Object} options * @param {Object} options.preloadedData Any persisted data that should be preloaded. - * @param {number} options.requestThrottleMS Throttle requests to the API so that they only + * @param {number} options.requestDebounceMS Throttle requests to the API so that they only * happen every n milliseconds. * * @return {Object} A database persistence layer. */ -export default function create( { preloadedData, requestThrottleMS = 2500 } ) { +export default function create( { preloadedData, requestDebounceMS = 2500 } ) { let cache = preloadedData; - const throttle = createAsyncThrottle( requestThrottleMS ); + const debounce = createAsyncDebounce( requestDebounceMS ); async function get() { if ( cache ) { @@ -39,7 +39,10 @@ export default function create( { preloadedData, requestThrottleMS = 2500 } ) { async function set( newData ) { cache = { ...newData }; - throttle( () => + // The user meta endpoint seems susceptible to errors when consecutive + // requests are made in quick succession. Ensure there's a gap between + // any consecutive requests. + debounce( () => apiFetch( { path: '/wp/v2/users/me', method: 'PUT', From 628a81032d98d367da0b9716ffa1a8a1bc6f2e88 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 5 Apr 2022 12:24:12 +0800 Subject: [PATCH 27/74] Try a localstorage fallback --- lib/experimental/persisted-preferences.php | 16 +++++++-- .../src/create/index.js | 35 +++++++++++++++---- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/lib/experimental/persisted-preferences.php b/lib/experimental/persisted-preferences.php index adfefa7f964c20..82085233438570 100644 --- a/lib/experimental/persisted-preferences.php +++ b/lib/experimental/persisted-preferences.php @@ -40,11 +40,21 @@ function gutenberg_configure_persisted_preferences() { wp_add_inline_script( 'wp-preferences', sprintf( - 'const { create } = wp.databasePersistenceLayer; - const persistenceLayer = create( { preloadedData: %s } ); + 'const serverData = %s; + const fallbackLocalStorageKey = "WP_PREFERENCES_USER_%s"; + const localData = JSON.parse( + localStorage.getItem( fallbackLocalStorageKey ) + ); + const serverTimestamp = serverData?.__timestamp ?? 0; + const localTimestamp = localData?.__timestamp ?? 0; + const preloadedData = serverTimestamp > localTimestamp ? serverData : localData; + + const { create } = wp.databasePersistenceLayer; + const persistenceLayer = create( { preloadedData, fallbackLocalStorageKey } ); const { store: preferencesStore } = wp.preferences; wp.data.dispatch( "core/preferences" ).setPersistenceLayer( persistenceLayer );', - wp_json_encode( $preload_data ) + wp_json_encode( $preload_data ), + $user_id ), 'after' ); diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index 053c7d51100184..d9a075bb1f29db 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -12,15 +12,20 @@ import createAsyncDebounce from './create-async-debounce'; * Creates a database persistence layer, storing data in the user meta. * * @param {Object} options - * @param {Object} options.preloadedData Any persisted data that should be preloaded. - * @param {number} options.requestDebounceMS Throttle requests to the API so that they only - * happen every n milliseconds. + * @param {Object} options.preloadedData Any persisted data that should be preloaded. + * @param {number} options.requestDebounceMS Throttle requests to the API so that they only + * @param {string} options.fallbackLocalStorageKey The key to use for restoring the localStorage backup. * * @return {Object} A database persistence layer. */ -export default function create( { preloadedData, requestDebounceMS = 2500 } ) { +export default function create( { + fallbackLocalStorageKey, + preloadedData, + requestDebounceMS = 2500, +} ) { let cache = preloadedData; const debounce = createAsyncDebounce( requestDebounceMS ); + const localStorage = window.localStorage; async function get() { if ( cache ) { @@ -31,13 +36,29 @@ export default function create( { preloadedData, requestDebounceMS = 2500 } ) { path: '/wp/v2/users/me', } ); - cache = user?.meta?.persisted_preferences; + // Restore data from local storage if it's more recent. + const serverData = user?.meta?.persisted_preferences; + const localData = JSON.parse( + localStorage.getItem( fallbackLocalStorageKey ) + ); + const serverTimestamp = serverData?.__timestamp ?? 0; + const localTimestamp = localData?.__timestamp ?? 0; + cache = serverTimestamp > localTimestamp ? serverData : localData; return cache; } async function set( newData ) { - cache = { ...newData }; + const dataWithTimestamp = { ...newData, __timestamp: Date.now() }; + cache = dataWithTimestamp; + + // Store data in local storage as a fallback. If for some reason the + // api request does not complete, this data can be used to restore + // preferences. + localStorage.setItem( + fallbackLocalStorageKey, + JSON.stringify( dataWithTimestamp ) + ); // The user meta endpoint seems susceptible to errors when consecutive // requests are made in quick succession. Ensure there's a gap between @@ -51,7 +72,7 @@ export default function create( { preloadedData, requestDebounceMS = 2500 } ) { keepalive: true, data: { meta: { - persisted_preferences: newData, + persisted_preferences: dataWithTimestamp, }, }, } ) From 997fba608f3d7f9b1701a0a4f5ac474060f83451 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 5 Apr 2022 13:28:44 +0800 Subject: [PATCH 28/74] Avoid `null` value becoming preloaded state --- lib/experimental/persisted-preferences.php | 10 +++++++++- .../database-persistence-layer/src/create/index.js | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/experimental/persisted-preferences.php b/lib/experimental/persisted-preferences.php index 82085233438570..0414247d02f4ea 100644 --- a/lib/experimental/persisted-preferences.php +++ b/lib/experimental/persisted-preferences.php @@ -47,7 +47,15 @@ function gutenberg_configure_persisted_preferences() { ); const serverTimestamp = serverData?.__timestamp ?? 0; const localTimestamp = localData?.__timestamp ?? 0; - const preloadedData = serverTimestamp > localTimestamp ? serverData : localData; + + let preloadedData; + if ( serverData && serverTimestamp > localTimestamp ) { + preloadedData = serverData; + } else if ( localData ) { + preloadedData = localData; + } else { + preloadedData = {}; + } const { create } = wp.databasePersistenceLayer; const persistenceLayer = create( { preloadedData, fallbackLocalStorageKey } ); diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index d9a075bb1f29db..7f035bef0428f8 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -43,7 +43,14 @@ export default function create( { ); const serverTimestamp = serverData?.__timestamp ?? 0; const localTimestamp = localData?.__timestamp ?? 0; - cache = serverTimestamp > localTimestamp ? serverData : localData; + + if ( serverData && serverTimestamp > localTimestamp ) { + cache = serverData; + } else if ( localData ) { + cache = localData; + } else { + cache = {}; + } return cache; } From 97c8640c5774e937df9bb391e34c6cdac16b6c39 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 5 Apr 2022 13:52:57 +0800 Subject: [PATCH 29/74] Improve naming and docs --- lib/experimental/persisted-preferences.php | 6 +++--- .../src/create/index.js | 21 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/experimental/persisted-preferences.php b/lib/experimental/persisted-preferences.php index 0414247d02f4ea..b30c773ed37a0d 100644 --- a/lib/experimental/persisted-preferences.php +++ b/lib/experimental/persisted-preferences.php @@ -41,9 +41,9 @@ function gutenberg_configure_persisted_preferences() { 'wp-preferences', sprintf( 'const serverData = %s; - const fallbackLocalStorageKey = "WP_PREFERENCES_USER_%s"; + const localStorageRestoreKey = "WP_PREFERENCES_USER_%s"; const localData = JSON.parse( - localStorage.getItem( fallbackLocalStorageKey ) + localStorage.getItem( localStorageRestoreKey ) ); const serverTimestamp = serverData?.__timestamp ?? 0; const localTimestamp = localData?.__timestamp ?? 0; @@ -58,7 +58,7 @@ function gutenberg_configure_persisted_preferences() { } const { create } = wp.databasePersistenceLayer; - const persistenceLayer = create( { preloadedData, fallbackLocalStorageKey } ); + const persistenceLayer = create( { preloadedData, localStorageRestoreKey } ); const { store: preferencesStore } = wp.preferences; wp.data.dispatch( "core/preferences" ).setPersistenceLayer( persistenceLayer );', wp_json_encode( $preload_data ), diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index 7f035bef0428f8..b4e3d5e314ff6d 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -11,15 +11,18 @@ import createAsyncDebounce from './create-async-debounce'; /** * Creates a database persistence layer, storing data in the user meta. * + * + * * @param {Object} options - * @param {Object} options.preloadedData Any persisted data that should be preloaded. - * @param {number} options.requestDebounceMS Throttle requests to the API so that they only - * @param {string} options.fallbackLocalStorageKey The key to use for restoring the localStorage backup. + * @param {Object} options.preloadedData Any persisted data that should be preloaded. + * @param {number} options.requestDebounceMS Debounce requests to the API so that they only occur + * don't swamp the server. + * @param {string} options.localStorageRestoreKey The key to use for restoring the localStorage backup. * * @return {Object} A database persistence layer. */ export default function create( { - fallbackLocalStorageKey, + localStorageRestoreKey, preloadedData, requestDebounceMS = 2500, } ) { @@ -36,14 +39,15 @@ export default function create( { path: '/wp/v2/users/me', } ); - // Restore data from local storage if it's more recent. const serverData = user?.meta?.persisted_preferences; const localData = JSON.parse( - localStorage.getItem( fallbackLocalStorageKey ) + localStorage.getItem( localStorageRestoreKey ) ); const serverTimestamp = serverData?.__timestamp ?? 0; const localTimestamp = localData?.__timestamp ?? 0; + // Prefer server data if it exists and is more recent. + // Otherwise fallback to localStorage data. if ( serverData && serverTimestamp > localTimestamp ) { cache = serverData; } else if ( localData ) { @@ -63,7 +67,7 @@ export default function create( { // api request does not complete, this data can be used to restore // preferences. localStorage.setItem( - fallbackLocalStorageKey, + localStorageRestoreKey, JSON.stringify( dataWithTimestamp ) ); @@ -76,6 +80,9 @@ export default function create( { method: 'PUT', // `keepalive` will still send the request in the background, // even when a browser unload event might interrupt it. + // This should hopefully make things more resilient. + // This does have a size limit of 64kb, but the data is usually + // much less. keepalive: true, data: { meta: { From 7f3ca74fd4d500c9f2e446614a41c00cc824975c Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 5 Apr 2022 13:54:23 +0800 Subject: [PATCH 30/74] Revert "Test that preferences are not persisted in tests due to reload" This reverts commit 3865610409ae2ce960ade361f1e75e213263da11. --- packages/e2e-test-utils/src/create-new-post.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/e2e-test-utils/src/create-new-post.js b/packages/e2e-test-utils/src/create-new-post.js index a8117dfd5e73a0..193468e5afacc1 100644 --- a/packages/e2e-test-utils/src/create-new-post.js +++ b/packages/e2e-test-utils/src/create-new-post.js @@ -48,7 +48,8 @@ export async function createNewPost( { wp.data.dispatch( 'core/edit-post' ).toggleFeature( 'welcomeGuide' ) ); - await page.waitForSelector( '.components-guide', { hidden: true } ); + await page.reload(); + await page.waitForSelector( '.edit-post-layout' ); } if ( isFullscreenMode ) { From fe8944c62020d185ba04fc81a3bdb88baf250fa0 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 5 Apr 2022 14:20:17 +0800 Subject: [PATCH 31/74] Use a stable reference when storage is empty --- packages/database-persistence-layer/src/create/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index b4e3d5e314ff6d..b43c8c291ddc21 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -8,6 +8,9 @@ import apiFetch from '@wordpress/api-fetch'; */ import createAsyncDebounce from './create-async-debounce'; +const EMPTY_OBJECT = {}; +const localStorage = window.localStorage; + /** * Creates a database persistence layer, storing data in the user meta. * @@ -25,10 +28,9 @@ export default function create( { localStorageRestoreKey, preloadedData, requestDebounceMS = 2500, -} ) { +} = {} ) { let cache = preloadedData; const debounce = createAsyncDebounce( requestDebounceMS ); - const localStorage = window.localStorage; async function get() { if ( cache ) { @@ -53,7 +55,7 @@ export default function create( { } else if ( localData ) { cache = localData; } else { - cache = {}; + cache = EMPTY_OBJECT; } return cache; From 269589f4e3dc2dc7478148c03e8ecae4349e9709 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 22 Mar 2022 14:26:46 +0800 Subject: [PATCH 32/74] Migrate block editor insert usage to preferences store Update tests Make updateInsertUsage a proper action that can be unit tested Fix reusable block tests --- .../data/data-core-block-editor.md | 8 + package-lock.json | 1 + packages/block-editor/package.json | 1 + packages/block-editor/src/store/actions.js | 51 +++- packages/block-editor/src/store/defaults.js | 4 - .../block-editor/src/store/defaults.native.js | 7 +- packages/block-editor/src/store/index.js | 9 +- packages/block-editor/src/store/reducer.js | 55 +--- packages/block-editor/src/store/selectors.js | 46 ++- .../block-editor/src/store/test/actions.js | 279 ++++++++++++++++-- .../block-editor/src/store/test/reducer.js | 149 ---------- .../block-editor/src/store/test/selectors.js | 60 +++- .../data/src/plugins/persistence/index.js | 6 + .../reusable-blocks/src/store/test/actions.js | 2 + 14 files changed, 414 insertions(+), 264 deletions(-) diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 69b12c4507b8be..b40dd9071730b3 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -1632,6 +1632,14 @@ _Returns_ - `Object`: Action object +### updateInsertUsage + +Updates the inserter usage statistics in the preferences store. + +_Parameters_ + +- _blocks_ `Array`: The array of blocks that were inserted. + ### updateSettings Action that updates the block editor settings. diff --git a/package-lock.json b/package-lock.json index cbda08b89d4455..1f1e9bd0a1b523 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17123,6 +17123,7 @@ "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/notices": "file:packages/notices", + "@wordpress/preferences": "file:packages/preferences", "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/shortcode": "file:packages/shortcode", "@wordpress/style-engine": "file:packages/style-engine", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index e35f4c071fcd80..6f388a96c37079 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -52,6 +52,7 @@ "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes", "@wordpress/notices": "file:../notices", + "@wordpress/preferences": "file:../preferences", "@wordpress/rich-text": "file:../rich-text", "@wordpress/shortcode": "file:../shortcode", "@wordpress/style-engine": "file:../style-engine", diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index c9170d64801813..5525626260129e 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -16,11 +16,13 @@ import { hasBlockSupport, switchToBlockType, synchronizeBlocksWithTemplate, + store as blocksStore, } from '@wordpress/blocks'; import { speak } from '@wordpress/a11y'; import { __, _n, sprintf } from '@wordpress/i18n'; import { create, insert, remove, toHTMLString } from '@wordpress/rich-text'; import deprecated from '@wordpress/deprecated'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -402,11 +404,12 @@ export const replaceBlocks = ( return; } } + + dispatch.updateInsertUsage( blocks ); dispatch( { type: 'REPLACE_BLOCKS', clientIds, blocks, - time: Date.now(), indexToSelect, initialPosition, meta, @@ -554,6 +557,49 @@ export function insertBlock( ); } +/** + * Updates the inserter usage statistics in the preferences store. + * + * @param {Array} blocks The array of blocks that were inserted. + */ +export const updateInsertUsage = ( blocks ) => ( { registry } ) => { + const previousInsertUsage = + registry.select( preferencesStore ).get( 'core', 'insertUsage' ) ?? {}; + + const time = Date.now(); + + const updatedInsertUsage = blocks.reduce( ( previousState, block ) => { + const { attributes, name: blockName } = block; + const match = registry + .select( blocksStore ) + .getActiveBlockVariation( blockName, attributes ); + + // If a block variation match is found change the name to be the same with the + // one that is used for block variations in the Inserter (`getItemFromVariation`). + let id = match?.name ? `${ blockName }/${ match.name }` : blockName; + const _insert = { name: id }; + if ( blockName === 'core/block' ) { + _insert.ref = attributes.ref; + id += '/' + attributes.ref; + } + + const previousCount = previousState?.[ id ]?.count ?? 0; + + return { + ...previousState, + [ id ]: { + time, + count: previousCount + 1, + insert: _insert, + }, + }; + }, previousInsertUsage ); + + registry + .dispatch( preferencesStore ) + .set( 'core', 'insertUsage', updatedInsertUsage ); +}; + /* eslint-disable jsdoc/valid-types */ /** * Action that inserts an array of blocks, optionally at a specific index respective a root block list. @@ -596,12 +642,12 @@ export const insertBlocks = ( } } if ( allowedBlocks.length ) { + dispatch.updateInsertUsage( blocks ); dispatch( { type: 'INSERT_BLOCKS', blocks: allowedBlocks, index, rootClientId, - time: Date.now(), updateSelection, initialPosition: updateSelection ? initialPosition : null, meta, @@ -1192,7 +1238,6 @@ export function replaceInnerBlocks( blocks, updateSelection, initialPosition: updateSelection ? initialPosition : null, - time: Date.now(), }; } diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js index 551f65146b4c98..ef7b486df01e4d 100644 --- a/packages/block-editor/src/store/defaults.js +++ b/packages/block-editor/src/store/defaults.js @@ -3,10 +3,6 @@ */ import { __, _x } from '@wordpress/i18n'; -export const PREFERENCES_DEFAULTS = { - insertUsage: {}, -}; - /** * The default editor settings * diff --git a/packages/block-editor/src/store/defaults.native.js b/packages/block-editor/src/store/defaults.native.js index fddf7cdb647b97..91527d2a33c99f 100644 --- a/packages/block-editor/src/store/defaults.native.js +++ b/packages/block-editor/src/store/defaults.native.js @@ -1,10 +1,7 @@ /** * Internal dependencies */ -import { - PREFERENCES_DEFAULTS, - SETTINGS_DEFAULTS as SETTINGS, -} from './defaults.js'; +import { SETTINGS_DEFAULTS as SETTINGS } from './defaults.js'; const SETTINGS_DEFAULTS = { ...SETTINGS, @@ -23,4 +20,4 @@ const SETTINGS_DEFAULTS = { }, }; -export { PREFERENCES_DEFAULTS, SETTINGS_DEFAULTS }; +export { SETTINGS_DEFAULTS }; diff --git a/packages/block-editor/src/store/index.js b/packages/block-editor/src/store/index.js index 2538a469eaf187..51d35c61eb52f9 100644 --- a/packages/block-editor/src/store/index.js +++ b/packages/block-editor/src/store/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createReduxStore, registerStore } from '@wordpress/data'; +import { createReduxStore, register } from '@wordpress/data'; /** * Internal dependencies @@ -33,11 +33,6 @@ export const storeConfig = { */ export const store = createReduxStore( STORE_NAME, { ...storeConfig, - persist: [ 'preferences' ], } ); -// Ideally we'd use register instead of register stores. -registerStore( STORE_NAME, { - ...storeConfig, - persist: [ 'preferences' ], -} ); +register( store ); diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 658918b8f3a09c..8744037fe40a7a 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -19,12 +19,12 @@ import { /** * WordPress dependencies */ -import { combineReducers, select } from '@wordpress/data'; -import { store as blocksStore } from '@wordpress/blocks'; +import { combineReducers } from '@wordpress/data'; + /** * Internal dependencies */ -import { PREFERENCES_DEFAULTS, SETTINGS_DEFAULTS } from './defaults'; +import { SETTINGS_DEFAULTS } from './defaults'; import { insertAt, moveTo } from './array'; /** @@ -1485,54 +1485,6 @@ export function settings( state = SETTINGS_DEFAULTS, action ) { return state; } -/** - * Reducer returning the user preferences. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {string} Updated state. - */ -export function preferences( state = PREFERENCES_DEFAULTS, action ) { - switch ( action.type ) { - case 'INSERT_BLOCKS': - case 'REPLACE_BLOCKS': - return action.blocks.reduce( ( prevState, block ) => { - const { attributes, name: blockName } = block; - const match = select( blocksStore ).getActiveBlockVariation( - blockName, - attributes - ); - // If a block variation match is found change the name to be the same with the - // one that is used for block variations in the Inserter (`getItemFromVariation`). - let id = match?.name - ? `${ blockName }/${ match.name }` - : blockName; - const insert = { name: id }; - if ( blockName === 'core/block' ) { - insert.ref = attributes.ref; - id += '/' + attributes.ref; - } - - return { - ...prevState, - insertUsage: { - ...prevState.insertUsage, - [ id ]: { - time: action.time, - count: prevState.insertUsage[ id ] - ? prevState.insertUsage[ id ].count + 1 - : 1, - insert, - }, - }, - }; - }, state ); - } - - return state; -} - /** * Reducer returning an object where each key is a block client ID, its value * representing the settings for its nested blocks. @@ -1754,7 +1706,6 @@ export default combineReducers( { insertionPoint, template, settings, - preferences, lastBlockAttributesChange, isNavigationMode, hasBlockMovingClientId, diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index adc79a355258f5..0c1549cfa82fd4 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -29,10 +29,12 @@ import { parse, switchToBlockType, } from '@wordpress/blocks'; +import { createRegistrySelector } from '@wordpress/data'; import { Platform } from '@wordpress/element'; import { applyFilters } from '@wordpress/hooks'; import { symbol } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; +import { store as preferencesStore } from '@wordpress/preferences'; import { create, remove, toHTMLString } from '@wordpress/rich-text'; import deprecated from '@wordpress/deprecated'; @@ -67,6 +69,7 @@ const MILLISECONDS_PER_WEEK = 7 * 24 * 3600 * 1000; * @type {Array} */ const EMPTY_ARRAY = []; +const EMPTY_OBJECT = {}; /** * Returns a block's name given its client ID, or null if no block exists with @@ -1741,9 +1744,27 @@ export function canLockBlockType( state, nameOrType ) { return !! state.settings?.canLockBlocks; } +/** + * Return all insert usage stats. + * + * This is only exported since registry selectors need to be exported. It's marked + * as unstable so that it's not considered part of the public API. + * + * @return {Object} An object with an `id` key representing the type + * of block and an object value that contains + * block insertion statistics. + */ +export const __unstableGetInsertUsage = createRegistrySelector( + ( select ) => () => + select( preferencesStore ).get( 'core', 'insertUsage' ) ?? EMPTY_OBJECT +); + /** * Returns information about how recently and frequently a block has been inserted. * + * This is only exported since registry selectors need to be exported. It's marked + * as unstable so that it's not considered part of the public API. + * * @param {Object} state Global application state. * @param {string} id A string which identifies the insert, e.g. 'core/block/12' * @@ -1751,9 +1772,15 @@ export function canLockBlockType( state, nameOrType ) { * insert occurred as a UNIX epoch, and `count` which is * the number of inserts that have occurred. */ -function getInsertUsage( state, id ) { - return state.preferences.insertUsage?.[ id ] ?? null; -} +export const __unstableGetInsertUsageForBlock = createRegistrySelector( + ( select ) => ( state, id ) => { + const insertUsage = select( preferencesStore ).get( + 'core', + 'insertUsage' + ); + return insertUsage?.[ id ] ?? null; + } +); /** * Returns whether we can show a block type in the inserter @@ -1781,7 +1808,8 @@ const canIncludeBlockTypeInInserter = ( state, blockType, rootClientId ) => { */ const getItemFromVariation = ( state, item ) => ( variation ) => { const variationId = `${ item.id }/${ variation.name }`; - const { time, count = 0 } = getInsertUsage( state, variationId ) || {}; + const { time, count = 0 } = + __unstableGetInsertUsageForBlock( state, variationId ) || {}; return { ...item, id: variationId, @@ -1856,7 +1884,8 @@ const buildBlockTypeItem = ( state, { buildScope = 'inserter' } ) => ( ); } - const { time, count = 0 } = getInsertUsage( state, id ) || {}; + const { time, count = 0 } = + __unstableGetInsertUsageForBlock( state, id ) || {}; const blockItemBase = { id, name: blockType.name, @@ -1964,7 +1993,8 @@ export const getInserterItems = createSelector( } const id = `core/block/${ reusableBlock.id }`; - const { time, count = 0 } = getInsertUsage( state, id ) || {}; + const { time, count = 0 } = + __unstableGetInsertUsageForBlock( state, id ) || {}; const frecency = calculateFrecency( time, count ); return { @@ -2031,7 +2061,7 @@ export const getInserterItems = createSelector( state.blockListSettings[ rootClientId ], state.blocks.byClientId, state.blocks.order, - state.preferences.insertUsage, + __unstableGetInsertUsage(), state.settings.allowedBlockTypes, state.settings.templateLock, getReusableBlocks( state ), @@ -2110,7 +2140,7 @@ export const getBlockTransformItems = createSelector( ( state, rootClientId ) => [ state.blockListSettings[ rootClientId ], state.blocks.byClientId, - state.preferences.insertUsage, + __unstableGetInsertUsage(), state.settings.allowedBlockTypes, state.settings.templateLock, getBlockTypes(), diff --git a/packages/block-editor/src/store/test/actions.js b/packages/block-editor/src/store/test/actions.js index 8cd09d7a7e1d0a..6b7f6b80ab1d1c 100644 --- a/packages/block-editor/src/store/test/actions.js +++ b/packages/block-editor/src/store/test/actions.js @@ -51,6 +51,7 @@ const { updateBlock, updateBlockAttributes, updateBlockListSettings, + updateInsertUsage, updateSettings, validateBlocksToTemplate, } = actions; @@ -214,7 +215,9 @@ describe( 'actions', () => { canInsertBlockType: () => true, getBlockCount: () => 1, }; - const dispatch = jest.fn(); + const dispatch = Object.assign( jest.fn(), { + updateInsertUsage: () => {}, + } ); replaceBlock( 'chicken', block )( { select, dispatch } ); @@ -222,7 +225,6 @@ describe( 'actions', () => { type: 'REPLACE_BLOCKS', clientIds: [ 'chicken' ], blocks: [ block ], - time: expect.any( Number ), initialPosition: 0, } ); } ); @@ -254,7 +256,9 @@ describe( 'actions', () => { } }, }; - const dispatch = jest.fn(); + const dispatch = Object.assign( jest.fn(), { + updateInsertUsage: () => {}, + } ); replaceBlocks( [ 'chicken' ], blocks )( { select, dispatch } ); @@ -279,7 +283,9 @@ describe( 'actions', () => { canInsertBlockType: () => true, getBlockCount: () => 1, }; - const dispatch = jest.fn(); + const dispatch = Object.assign( jest.fn(), { + updateInsertUsage: () => {}, + } ); replaceBlocks( [ 'chicken' ], blocks )( { select, dispatch } ); @@ -287,7 +293,6 @@ describe( 'actions', () => { type: 'REPLACE_BLOCKS', clientIds: [ 'chicken' ], blocks, - time: expect.any( Number ), initialPosition: 0, } ); } ); @@ -312,7 +317,9 @@ describe( 'actions', () => { canInsertBlockType: () => true, getBlockCount: () => 1, }; - const dispatch = jest.fn(); + const dispatch = Object.assign( jest.fn(), { + updateInsertUsage: () => {}, + } ); replaceBlocks( [ 'chicken' ], @@ -326,12 +333,44 @@ describe( 'actions', () => { type: 'REPLACE_BLOCKS', clientIds: [ 'chicken' ], blocks, - time: expect.any( Number ), indexToSelect: null, initialPosition: null, meta: { patternName: 'core/chicken-ribs-pattern' }, } ); } ); + + it( 'should set insertUsage in the preferences store', () => { + const blocks = [ + { + clientId: 'ribs', + name: 'core/test-ribs', + }, + { + clientId: 'chicken', + name: 'core/test-chicken', + }, + ]; + + const select = { + getSettings: () => null, + getBlockRootClientId: () => null, + canInsertBlockType: () => true, + getBlockCount: () => 1, + }; + const updateInsertUsageSpy = jest.fn(); + const dispatch = Object.assign( jest.fn(), { + updateInsertUsage: updateInsertUsageSpy, + } ); + + replaceBlocks( + [ 'pineapple' ], + blocks, + null, + null + )( { select, dispatch } ); + + expect( updateInsertUsageSpy ).toHaveBeenCalledWith( blocks ); + } ); } ); describe( 'insertBlock', () => { @@ -346,7 +385,9 @@ describe( 'actions', () => { getSettings: () => null, canInsertBlockType: () => true, }; - const dispatch = jest.fn(); + const dispatch = Object.assign( jest.fn(), { + updateInsertUsage: () => {}, + } ); insertBlock( block, @@ -360,7 +401,6 @@ describe( 'actions', () => { blocks: [ block ], index, rootClientId: 'testclientid', - time: expect.any( Number ), updateSelection: true, initialPosition: 0, } ); @@ -394,7 +434,9 @@ describe( 'actions', () => { } ), canInsertBlockType: () => true, }; - const dispatch = jest.fn(); + const dispatch = Object.assign( jest.fn(), { + updateInsertUsage: () => {}, + } ); insertBlocks( blocks, @@ -418,7 +460,6 @@ describe( 'actions', () => { ], index: 5, rootClientId: 'testrootid', - time: expect.any( Number ), updateSelection: false, initialPosition: null, } ); @@ -444,7 +485,9 @@ describe( 'actions', () => { } ), canInsertBlockType: () => true, }; - const dispatch = jest.fn(); + const dispatch = Object.assign( jest.fn(), { + updateInsertUsage: () => {}, + } ); insertBlocks( blocks, @@ -463,7 +506,6 @@ describe( 'actions', () => { ], index: 5, rootClientId: 'testrootid', - time: expect.any( Number ), updateSelection: false, initialPosition: null, } ); @@ -499,7 +541,9 @@ describe( 'actions', () => { } }, }; - const dispatch = jest.fn(); + const dispatch = Object.assign( jest.fn(), { + updateInsertUsage: () => {}, + } ); insertBlocks( blocks, @@ -513,7 +557,6 @@ describe( 'actions', () => { blocks: [ ribsBlock, chickenRibsBlock ], index: 5, rootClientId: 'testrootid', - time: expect.any( Number ), updateSelection: false, initialPosition: null, } ); @@ -534,7 +577,9 @@ describe( 'actions', () => { getSettings: () => null, canInsertBlockType: () => false, }; - const dispatch = jest.fn(); + const dispatch = Object.assign( jest.fn(), { + updateInsertUsage: () => {}, + } ); insertBlocks( blocks, @@ -577,7 +622,9 @@ describe( 'actions', () => { } }, }; - const dispatch = jest.fn(); + const dispatch = Object.assign( jest.fn(), { + updateInsertUsage: () => {}, + } ); insertBlocks( blocks, @@ -593,12 +640,206 @@ describe( 'actions', () => { blocks: [ ribsBlock, chickenRibsBlock ], index: 5, rootClientId: 'testrootid', - time: expect.any( Number ), updateSelection: false, initialPosition: null, meta: { patternName: 'core/chicken-ribs-pattern' }, } ); } ); + + it( 'should set insertUsage in the preferences store', () => { + const blocks = [ + { + clientId: 'ribs', + name: 'core/test-ribs', + }, + { + clientId: 'chicken', + name: 'core/test-chicken', + }, + ]; + + const select = { + getSettings: () => null, + canInsertBlockType: () => true, + }; + const updateInsertUsageSpy = jest.fn(); + const dispatch = Object.assign( jest.fn(), { + updateInsertUsage: updateInsertUsageSpy, + } ); + + insertBlocks( + blocks, + 5, + 'testrootid', + false, + 0 + )( { select, dispatch } ); + + expect( updateInsertUsageSpy ).toHaveBeenCalledWith( blocks ); + } ); + } ); + + describe( 'updateInsertUsage', () => { + it( 'should record recently used blocks', () => { + const setPreference = jest.fn(); + const registry = { + dispatch: () => ( { + set: setPreference, + } ), + select: () => ( { + get: () => {}, + getActiveBlockVariation: () => {}, + } ), + }; + + updateInsertUsage( [ + { + clientId: 'bacon', + name: 'core/embed', + }, + ] )( { registry } ); + + expect( setPreference ).toHaveBeenCalledWith( + 'core', + 'insertUsage', + { + 'core/embed': { + time: expect.any( Number ), + count: 1, + insert: { name: 'core/embed' }, + }, + } + ); + } ); + + it( 'merges insert usage if more blocks are added of the same type', () => { + const setPreference = jest.fn(); + const registry = { + dispatch: () => ( { + set: setPreference, + } ), + select: () => ( { + // simulate an existing embed block. + get: () => ( { + 'core/embed': { + time: 123456, + count: 1, + insert: { name: 'core/embed' }, + }, + } ), + getActiveBlockVariation: () => {}, + } ), + }; + + updateInsertUsage( [ + { + clientId: 'eggs', + name: 'core/embed', + }, + { + clientId: 'bacon', + name: 'core/block', + attributes: { ref: 123 }, + }, + ] )( { registry } ); + + expect( setPreference ).toHaveBeenCalledWith( + 'core', + 'insertUsage', + { + // The reusable block has a special case where each ref is + // stored as though an individual block, and the ref is + // also recorded in the `insert` object. + 'core/block/123': { + time: expect.any( Number ), + count: 1, + insert: { name: 'core/block', ref: 123 }, + }, + 'core/embed': { + time: expect.any( Number ), + count: 2, + insert: { name: 'core/embed' }, + }, + } + ); + } ); + + describe( 'block variations handling', () => { + const blockWithVariations = 'core/test-block-with-variations'; + + it( 'should return proper results with both found or not found block variation matches', () => { + const setPreference = jest.fn(); + const registry = { + dispatch: () => ( { + set: setPreference, + } ), + select: () => ( { + get: () => {}, + // simulate an active block variation: + // - 'apple' when the fruit attribute is 'apple'. + // - 'orange' when the fruit attribute is 'orange'. + getActiveBlockVariation: ( + blockName, + { fruit } = {} + ) => { + if ( blockName === blockWithVariations ) { + if ( fruit === 'orange' ) + return { name: 'orange' }; + if ( fruit === 'apple' ) + return { name: 'apple' }; + } + }, + } ), + }; + + updateInsertUsage( [ + { + clientId: 'no match', + name: blockWithVariations, + }, + { + clientId: 'not a variation match', + name: blockWithVariations, + attributes: { fruit: 'not in a variation' }, + }, + { + clientId: 'orange', + name: blockWithVariations, + attributes: { fruit: 'orange' }, + }, + { + clientId: 'apple', + name: blockWithVariations, + attributes: { fruit: 'apple' }, + }, + ] )( { registry } ); + + const orangeVariationName = `${ blockWithVariations }/orange`; + const appleVariationName = `${ blockWithVariations }/apple`; + + expect( setPreference ).toHaveBeenCalledWith( + 'core', + 'insertUsage', + { + [ orangeVariationName ]: { + time: expect.any( Number ), + count: 1, + insert: { name: orangeVariationName }, + }, + [ appleVariationName ]: { + time: expect.any( Number ), + count: 1, + insert: { name: appleVariationName }, + }, + [ blockWithVariations ]: { + time: expect.any( Number ), + count: 2, + insert: { name: blockWithVariations }, + }, + } + ); + } ); + } ); } ); describe( 'showInsertionPoint', () => { @@ -870,7 +1111,6 @@ describe( 'actions', () => { type: 'REPLACE_INNER_BLOCKS', blocks: [ block ], rootClientId: 'root', - time: expect.any( Number ), updateSelection: false, initialPosition: null, } ); @@ -881,7 +1121,6 @@ describe( 'actions', () => { type: 'REPLACE_INNER_BLOCKS', blocks: [ block ], rootClientId: 'root', - time: expect.any( Number ), updateSelection: true, initialPosition: 0, } ); diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index faca5d4f86117f..3ccb38f0427e4c 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -25,7 +25,6 @@ import { selection, initialPosition, isMultiSelecting, - preferences, blocksMode, insertionPoint, template, @@ -2558,154 +2557,6 @@ describe( 'state', () => { } ); } ); - describe( 'preferences()', () => { - it( 'should apply all defaults', () => { - const state = preferences( undefined, {} ); - - expect( state ).toEqual( { - insertUsage: {}, - } ); - } ); - it( 'should record recently used blocks', () => { - const state = preferences( deepFreeze( { insertUsage: {} } ), { - type: 'INSERT_BLOCKS', - blocks: [ - { - clientId: 'bacon', - name: 'core/embed', - }, - ], - time: 123456, - } ); - - expect( state ).toEqual( { - insertUsage: { - 'core/embed': { - time: 123456, - count: 1, - insert: { name: 'core/embed' }, - }, - }, - } ); - - const twoRecentBlocks = preferences( - deepFreeze( { - insertUsage: { - 'core/embed': { - time: 123456, - count: 1, - insert: { name: 'core/embed' }, - }, - }, - } ), - { - type: 'INSERT_BLOCKS', - blocks: [ - { - clientId: 'eggs', - name: 'core/embed', - }, - { - clientId: 'bacon', - name: 'core/block', - attributes: { ref: 123 }, - }, - ], - time: 123457, - } - ); - - expect( twoRecentBlocks ).toEqual( { - insertUsage: { - 'core/embed': { - time: 123457, - count: 2, - insert: { name: 'core/embed' }, - }, - 'core/block/123': { - time: 123457, - count: 1, - insert: { name: 'core/block', ref: 123 }, - }, - }, - } ); - } ); - describe( 'block variations handling', () => { - const blockWithVariations = 'core/test-block-with-variations'; - beforeAll( () => { - const variations = [ - { - name: 'apple', - attributes: { fruit: 'apple' }, - }, - { name: 'orange', attributes: { fruit: 'orange' } }, - ].map( ( variation ) => ( { - ...variation, - isActive: ( blockAttributes, variationAttributes ) => - blockAttributes?.fruit === variationAttributes.fruit, - } ) ); - registerBlockType( blockWithVariations, { - save: noop, - edit: noop, - title: 'Fruit with variations', - variations, - } ); - } ); - afterAll( () => { - unregisterBlockType( blockWithVariations ); - } ); - it( 'should return proper results with both found or not found block variation matches', () => { - const state = preferences( deepFreeze( { insertUsage: {} } ), { - type: 'INSERT_BLOCKS', - blocks: [ - { - clientId: 'no match', - name: blockWithVariations, - }, - { - clientId: 'not a variation match', - name: blockWithVariations, - attributes: { fruit: 'not in a variation' }, - }, - { - clientId: 'orange', - name: blockWithVariations, - attributes: { fruit: 'orange' }, - }, - { - clientId: 'apple', - name: blockWithVariations, - attributes: { fruit: 'apple' }, - }, - ], - time: 123456, - } ); - - const orangeVariationName = `${ blockWithVariations }/orange`; - const appleVariationName = `${ blockWithVariations }/apple`; - expect( state ).toEqual( { - insertUsage: expect.objectContaining( { - [ orangeVariationName ]: { - time: 123456, - count: 1, - insert: { name: orangeVariationName }, - }, - [ appleVariationName ]: { - time: 123456, - count: 1, - insert: { name: appleVariationName }, - }, - [ blockWithVariations ]: { - time: 123456, - count: 2, - insert: { name: blockWithVariations }, - }, - } ), - } ); - } ); - } ); - } ); - describe( 'blocksMode', () => { it( 'should set mode to html if not set', () => { const action = { diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index a152b7da564c52..772c514169d767 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -77,6 +77,8 @@ const { __experimentalGetPatternTransformItems, wasBlockJustInserted, __experimentalGetGlobalBlocksByName, + __unstableGetInsertUsage, + __unstableGetInsertUsageForBlock, } = selectors; describe( 'selectors', () => { @@ -2744,6 +2746,12 @@ describe( 'selectors', () => { describe( 'getInserterItems', () => { it( 'should properly list block type and reusable block items', () => { + const registry = { + select: () => ( { get: () => ( {} ) } ), + }; + __unstableGetInsertUsage.registry = registry; + __unstableGetInsertUsageForBlock.registry = registry; + const state = { blocks: { byClientId: {}, @@ -2767,11 +2775,6 @@ describe( 'selectors', () => { }, ], }, - // Intentionally include a test case which considers - // `insertUsage` as not present within preferences. - // - // See: https://github.com/WordPress/gutenberg/issues/14580 - preferences: {}, blockListSettings: {}, }; const items = getInserterItems( state ); @@ -2813,6 +2816,15 @@ describe( 'selectors', () => { } ); it( 'should correctly cache the return values', () => { + // Define the empty object here to simulate that the preferences + // store won't return a new object every time. + const EMPTY_OBJECT = {}; + const registry = { + select: () => ( { get: () => EMPTY_OBJECT } ), + }; + __unstableGetInsertUsage.registry = registry; + __unstableGetInsertUsageForBlock.registry = registry; + const state = { blocks: { byClientId: { @@ -2864,9 +2876,6 @@ describe( 'selectors', () => { }, ], }, - preferences: { - insertUsage: {}, - }, blockListSettings: { block3: {}, block4: {}, @@ -2920,6 +2929,12 @@ describe( 'selectors', () => { } ); it( 'should set isDisabled when a block with `multiple: false` has been used', () => { + const registry = { + select: () => ( { get: () => ( {} ) } ), + }; + __unstableGetInsertUsage.registry = registry; + __unstableGetInsertUsageForBlock.registry = registry; + const state = { blocks: { byClientId: { @@ -2945,9 +2960,6 @@ describe( 'selectors', () => { controlledInnerBlocks: {}, parents: {}, }, - preferences: { - insertUsage: {}, - }, blockListSettings: {}, settings: {}, }; @@ -2959,6 +2971,16 @@ describe( 'selectors', () => { } ); it( 'should set a frecency', () => { + // Simulate returning block insertUsage from the preferences store. + const registry = { + select: () => ( { + get: () => ( { + 'core/test-block-b': { count: 10, time: 1000 }, + } ), + } ), + }; + __unstableGetInsertUsage.registry = registry; + __unstableGetInsertUsageForBlock.registry = registry; const state = { blocks: { byClientId: {}, @@ -2967,11 +2989,6 @@ describe( 'selectors', () => { parents: {}, cache: {}, }, - preferences: { - insertUsage: { - 'core/test-block-b': { count: 10, time: 1000 }, - }, - }, blockListSettings: {}, settings: {}, }; @@ -3177,6 +3194,17 @@ describe( 'selectors', () => { ); } ); it( 'should set frecency', () => { + // Simulate returning block insertUsage from the preferences store. + const registry = { + select: () => ( { + get: () => ( { + 'core/with-tranforms-a': { count: 10, time: 1000 }, + } ), + } ), + }; + __unstableGetInsertUsage.registry = registry; + __unstableGetInsertUsageForBlock.registry = registry; + const state = { blocks: { byClientId: {}, diff --git a/packages/data/src/plugins/persistence/index.js b/packages/data/src/plugins/persistence/index.js index f00f8a8fc2b185..653d291970ad34 100644 --- a/packages/data/src/plugins/persistence/index.js +++ b/packages/data/src/plugins/persistence/index.js @@ -639,6 +639,12 @@ persistencePlugin.__unstableMigrate = ( pluginOptions ) => { { from: 'core/edit-site', scope: 'core/edit-site' }, 'editorMode' ); + migrateIndividualPreferenceToPreferencesStore( + persistence, + { from: 'core/block-editor', scope: 'core' }, + 'insertUsage' + ); + migrateInterfaceEnableItemsToPreferencesStore( persistence ); }; diff --git a/packages/reusable-blocks/src/store/test/actions.js b/packages/reusable-blocks/src/store/test/actions.js index ffa907b4b18a39..f5cb46716f8b9b 100644 --- a/packages/reusable-blocks/src/store/test/actions.js +++ b/packages/reusable-blocks/src/store/test/actions.js @@ -11,6 +11,7 @@ import { } from '@wordpress/blocks'; import { store as coreStore } from '@wordpress/core-data'; +import { store as preferencesStore } from '@wordpress/preferences'; import apiFetch from '@wordpress/api-fetch'; /** @@ -31,6 +32,7 @@ function createRegistryWithStores() { registry.register( blockEditorStore ); registry.register( reusableBlocksStore ); registry.register( blocksStore ); + registry.register( preferencesStore ); // Register entity here instead of mocking API handlers for loadPostTypeEntities() registry.dispatch( coreStore ).addEntities( [ From b6c847e4160c90a916a6c8c9fd25e41ef2ab651a Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 5 Apr 2022 15:02:34 +0800 Subject: [PATCH 33/74] Also reset preferences after each test --- packages/e2e-tests/config/setup-test-framework.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/e2e-tests/config/setup-test-framework.js b/packages/e2e-tests/config/setup-test-framework.js index 95d3cd0fb297ab..ed1d92f67bf197 100644 --- a/packages/e2e-tests/config/setup-test-framework.js +++ b/packages/e2e-tests/config/setup-test-framework.js @@ -255,6 +255,7 @@ beforeAll( async () => { } ); afterEach( async () => { + await resetPreferences(); await setupBrowser(); } ); From bcab787ca00d5739317dbdea1771428d1b272da5 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 5 Apr 2022 15:32:32 +0800 Subject: [PATCH 34/74] Use beforeEach for clearing e2e test browser state --- packages/e2e-tests/config/setup-test-framework.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/e2e-tests/config/setup-test-framework.js b/packages/e2e-tests/config/setup-test-framework.js index ed1d92f67bf197..803e332b668d94 100644 --- a/packages/e2e-tests/config/setup-test-framework.js +++ b/packages/e2e-tests/config/setup-test-framework.js @@ -243,18 +243,16 @@ beforeAll( async () => { enablePageDialogAccept(); observeConsoleLogging(); await simulateAdverseConditions(); - await resetPreferences(); await activateTheme( 'twentytwentyone' ); await trashAllPosts(); await trashAllPosts( 'wp_block' ); - await setupBrowser(); await activatePlugin( 'gutenberg-test-plugin-disables-the-css-animations' ); await page.emulateMediaFeatures( [ { name: 'prefers-reduced-motion', value: 'reduce' }, ] ); } ); -afterEach( async () => { +beforeEach( async () => { await resetPreferences(); await setupBrowser(); } ); From 48e3d56409a0f0219177dceb7c9c4cce84788e2f Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 5 Apr 2022 16:18:52 +0800 Subject: [PATCH 35/74] Try avoiding beforeAll and afterAll in nonce test --- .../e2e-tests/specs/editor/plugins/nonce.test.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/e2e-tests/specs/editor/plugins/nonce.test.js b/packages/e2e-tests/specs/editor/plugins/nonce.test.js index f2be63aa9beaad..38b81672f89fa7 100644 --- a/packages/e2e-tests/specs/editor/plugins/nonce.test.js +++ b/packages/e2e-tests/specs/editor/plugins/nonce.test.js @@ -9,19 +9,12 @@ import { } from '@wordpress/e2e-test-utils'; describe( 'Nonce', () => { - beforeAll( async () => { + it( 'should refresh when expired', async () => { + // This test avoids using `beforeAll` and `afterAll` as that + // interferes with the `rest` and `batch` e2e test utils. await activatePlugin( 'gutenberg-test-plugin-nonce' ); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-plugin-nonce' ); - } ); - - beforeEach( async () => { await createNewPost(); - } ); - it( 'should refresh when expired', async () => { await page.keyboard.press( 'Enter' ); // eslint-disable-next-line no-restricted-syntax await page.waitForTimeout( 5000 ); @@ -31,5 +24,6 @@ describe( 'Nonce', () => { await saveDraft(); // We expect a 403 status once. expect( console ).toHaveErrored(); + await deactivatePlugin( 'gutenberg-test-plugin-nonce' ); } ); } ); From b09ac178803be8f5a0c05b3da3b55ecab1face95 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 6 Apr 2022 13:50:58 +0800 Subject: [PATCH 36/74] Revert "Use beforeEach for clearing e2e test browser state" This reverts commit e51a00a08bdfba21fcc2d2598d428ef7740d28f2. --- packages/e2e-tests/config/setup-test-framework.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/e2e-tests/config/setup-test-framework.js b/packages/e2e-tests/config/setup-test-framework.js index 803e332b668d94..ed1d92f67bf197 100644 --- a/packages/e2e-tests/config/setup-test-framework.js +++ b/packages/e2e-tests/config/setup-test-framework.js @@ -243,16 +243,18 @@ beforeAll( async () => { enablePageDialogAccept(); observeConsoleLogging(); await simulateAdverseConditions(); + await resetPreferences(); await activateTheme( 'twentytwentyone' ); await trashAllPosts(); await trashAllPosts( 'wp_block' ); + await setupBrowser(); await activatePlugin( 'gutenberg-test-plugin-disables-the-css-animations' ); await page.emulateMediaFeatures( [ { name: 'prefers-reduced-motion', value: 'reduce' }, ] ); } ); -beforeEach( async () => { +afterEach( async () => { await resetPreferences(); await setupBrowser(); } ); From 0c611cef3c530aceb98e22717b19bcb60e7dfd82 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 6 Apr 2022 14:06:24 +0800 Subject: [PATCH 37/74] Switch to using beforeEach and afterEach, as they also ensure correct order of operation --- .../e2e-tests/specs/editor/plugins/nonce.test.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/e2e-tests/specs/editor/plugins/nonce.test.js b/packages/e2e-tests/specs/editor/plugins/nonce.test.js index 38b81672f89fa7..669ebf041a9e57 100644 --- a/packages/e2e-tests/specs/editor/plugins/nonce.test.js +++ b/packages/e2e-tests/specs/editor/plugins/nonce.test.js @@ -9,12 +9,18 @@ import { } from '@wordpress/e2e-test-utils'; describe( 'Nonce', () => { - it( 'should refresh when expired', async () => { - // This test avoids using `beforeAll` and `afterAll` as that - // interferes with the `rest` and `batch` e2e test utils. + // While using beforeEach/afterEach is suboptimal for multiple tests, they + // are used here to ensure that the nonce plugin doesn't interfere with API + // calls made in global before/after calls, which may perform API requests. + beforeEach( async () => { await activatePlugin( 'gutenberg-test-plugin-nonce' ); - await createNewPost(); + } ); + afterEach( async () => { + await deactivatePlugin( 'gutenberg-test-plugin-nonce' ); + } ); + it( 'should refresh when expired', async () => { + await createNewPost(); await page.keyboard.press( 'Enter' ); // eslint-disable-next-line no-restricted-syntax await page.waitForTimeout( 5000 ); @@ -24,6 +30,5 @@ describe( 'Nonce', () => { await saveDraft(); // We expect a 403 status once. expect( console ).toHaveErrored(); - await deactivatePlugin( 'gutenberg-test-plugin-nonce' ); } ); } ); From 1cd7d4328337d77ee12b3968df0ce4abbf2c26ae Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 6 Apr 2022 14:35:32 +0800 Subject: [PATCH 38/74] Revert "Migrate block editor insert usage to preferences store" This reverts commit da21c6793cc7d2beeb084c23260004131c8624bc. --- .../data/data-core-block-editor.md | 8 - package-lock.json | 1 - packages/block-editor/package.json | 1 - packages/block-editor/src/store/actions.js | 51 +--- packages/block-editor/src/store/defaults.js | 4 + .../block-editor/src/store/defaults.native.js | 7 +- packages/block-editor/src/store/index.js | 9 +- packages/block-editor/src/store/reducer.js | 55 +++- packages/block-editor/src/store/selectors.js | 46 +-- .../block-editor/src/store/test/actions.js | 279 ++---------------- .../block-editor/src/store/test/reducer.js | 149 ++++++++++ .../block-editor/src/store/test/selectors.js | 60 +--- .../data/src/plugins/persistence/index.js | 6 - .../reusable-blocks/src/store/test/actions.js | 2 - 14 files changed, 264 insertions(+), 414 deletions(-) diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index b40dd9071730b3..69b12c4507b8be 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -1632,14 +1632,6 @@ _Returns_ - `Object`: Action object -### updateInsertUsage - -Updates the inserter usage statistics in the preferences store. - -_Parameters_ - -- _blocks_ `Array`: The array of blocks that were inserted. - ### updateSettings Action that updates the block editor settings. diff --git a/package-lock.json b/package-lock.json index 1f1e9bd0a1b523..cbda08b89d4455 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17123,7 +17123,6 @@ "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/notices": "file:packages/notices", - "@wordpress/preferences": "file:packages/preferences", "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/shortcode": "file:packages/shortcode", "@wordpress/style-engine": "file:packages/style-engine", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 6f388a96c37079..e35f4c071fcd80 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -52,7 +52,6 @@ "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes", "@wordpress/notices": "file:../notices", - "@wordpress/preferences": "file:../preferences", "@wordpress/rich-text": "file:../rich-text", "@wordpress/shortcode": "file:../shortcode", "@wordpress/style-engine": "file:../style-engine", diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 5525626260129e..c9170d64801813 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -16,13 +16,11 @@ import { hasBlockSupport, switchToBlockType, synchronizeBlocksWithTemplate, - store as blocksStore, } from '@wordpress/blocks'; import { speak } from '@wordpress/a11y'; import { __, _n, sprintf } from '@wordpress/i18n'; import { create, insert, remove, toHTMLString } from '@wordpress/rich-text'; import deprecated from '@wordpress/deprecated'; -import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -404,12 +402,11 @@ export const replaceBlocks = ( return; } } - - dispatch.updateInsertUsage( blocks ); dispatch( { type: 'REPLACE_BLOCKS', clientIds, blocks, + time: Date.now(), indexToSelect, initialPosition, meta, @@ -557,49 +554,6 @@ export function insertBlock( ); } -/** - * Updates the inserter usage statistics in the preferences store. - * - * @param {Array} blocks The array of blocks that were inserted. - */ -export const updateInsertUsage = ( blocks ) => ( { registry } ) => { - const previousInsertUsage = - registry.select( preferencesStore ).get( 'core', 'insertUsage' ) ?? {}; - - const time = Date.now(); - - const updatedInsertUsage = blocks.reduce( ( previousState, block ) => { - const { attributes, name: blockName } = block; - const match = registry - .select( blocksStore ) - .getActiveBlockVariation( blockName, attributes ); - - // If a block variation match is found change the name to be the same with the - // one that is used for block variations in the Inserter (`getItemFromVariation`). - let id = match?.name ? `${ blockName }/${ match.name }` : blockName; - const _insert = { name: id }; - if ( blockName === 'core/block' ) { - _insert.ref = attributes.ref; - id += '/' + attributes.ref; - } - - const previousCount = previousState?.[ id ]?.count ?? 0; - - return { - ...previousState, - [ id ]: { - time, - count: previousCount + 1, - insert: _insert, - }, - }; - }, previousInsertUsage ); - - registry - .dispatch( preferencesStore ) - .set( 'core', 'insertUsage', updatedInsertUsage ); -}; - /* eslint-disable jsdoc/valid-types */ /** * Action that inserts an array of blocks, optionally at a specific index respective a root block list. @@ -642,12 +596,12 @@ export const insertBlocks = ( } } if ( allowedBlocks.length ) { - dispatch.updateInsertUsage( blocks ); dispatch( { type: 'INSERT_BLOCKS', blocks: allowedBlocks, index, rootClientId, + time: Date.now(), updateSelection, initialPosition: updateSelection ? initialPosition : null, meta, @@ -1238,6 +1192,7 @@ export function replaceInnerBlocks( blocks, updateSelection, initialPosition: updateSelection ? initialPosition : null, + time: Date.now(), }; } diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js index ef7b486df01e4d..551f65146b4c98 100644 --- a/packages/block-editor/src/store/defaults.js +++ b/packages/block-editor/src/store/defaults.js @@ -3,6 +3,10 @@ */ import { __, _x } from '@wordpress/i18n'; +export const PREFERENCES_DEFAULTS = { + insertUsage: {}, +}; + /** * The default editor settings * diff --git a/packages/block-editor/src/store/defaults.native.js b/packages/block-editor/src/store/defaults.native.js index 91527d2a33c99f..fddf7cdb647b97 100644 --- a/packages/block-editor/src/store/defaults.native.js +++ b/packages/block-editor/src/store/defaults.native.js @@ -1,7 +1,10 @@ /** * Internal dependencies */ -import { SETTINGS_DEFAULTS as SETTINGS } from './defaults.js'; +import { + PREFERENCES_DEFAULTS, + SETTINGS_DEFAULTS as SETTINGS, +} from './defaults.js'; const SETTINGS_DEFAULTS = { ...SETTINGS, @@ -20,4 +23,4 @@ const SETTINGS_DEFAULTS = { }, }; -export { SETTINGS_DEFAULTS }; +export { PREFERENCES_DEFAULTS, SETTINGS_DEFAULTS }; diff --git a/packages/block-editor/src/store/index.js b/packages/block-editor/src/store/index.js index 51d35c61eb52f9..2538a469eaf187 100644 --- a/packages/block-editor/src/store/index.js +++ b/packages/block-editor/src/store/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createReduxStore, register } from '@wordpress/data'; +import { createReduxStore, registerStore } from '@wordpress/data'; /** * Internal dependencies @@ -33,6 +33,11 @@ export const storeConfig = { */ export const store = createReduxStore( STORE_NAME, { ...storeConfig, + persist: [ 'preferences' ], } ); -register( store ); +// Ideally we'd use register instead of register stores. +registerStore( STORE_NAME, { + ...storeConfig, + persist: [ 'preferences' ], +} ); diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 8744037fe40a7a..658918b8f3a09c 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -19,12 +19,12 @@ import { /** * WordPress dependencies */ -import { combineReducers } from '@wordpress/data'; - +import { combineReducers, select } from '@wordpress/data'; +import { store as blocksStore } from '@wordpress/blocks'; /** * Internal dependencies */ -import { SETTINGS_DEFAULTS } from './defaults'; +import { PREFERENCES_DEFAULTS, SETTINGS_DEFAULTS } from './defaults'; import { insertAt, moveTo } from './array'; /** @@ -1485,6 +1485,54 @@ export function settings( state = SETTINGS_DEFAULTS, action ) { return state; } +/** + * Reducer returning the user preferences. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {string} Updated state. + */ +export function preferences( state = PREFERENCES_DEFAULTS, action ) { + switch ( action.type ) { + case 'INSERT_BLOCKS': + case 'REPLACE_BLOCKS': + return action.blocks.reduce( ( prevState, block ) => { + const { attributes, name: blockName } = block; + const match = select( blocksStore ).getActiveBlockVariation( + blockName, + attributes + ); + // If a block variation match is found change the name to be the same with the + // one that is used for block variations in the Inserter (`getItemFromVariation`). + let id = match?.name + ? `${ blockName }/${ match.name }` + : blockName; + const insert = { name: id }; + if ( blockName === 'core/block' ) { + insert.ref = attributes.ref; + id += '/' + attributes.ref; + } + + return { + ...prevState, + insertUsage: { + ...prevState.insertUsage, + [ id ]: { + time: action.time, + count: prevState.insertUsage[ id ] + ? prevState.insertUsage[ id ].count + 1 + : 1, + insert, + }, + }, + }; + }, state ); + } + + return state; +} + /** * Reducer returning an object where each key is a block client ID, its value * representing the settings for its nested blocks. @@ -1706,6 +1754,7 @@ export default combineReducers( { insertionPoint, template, settings, + preferences, lastBlockAttributesChange, isNavigationMode, hasBlockMovingClientId, diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 0c1549cfa82fd4..adc79a355258f5 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -29,12 +29,10 @@ import { parse, switchToBlockType, } from '@wordpress/blocks'; -import { createRegistrySelector } from '@wordpress/data'; import { Platform } from '@wordpress/element'; import { applyFilters } from '@wordpress/hooks'; import { symbol } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; -import { store as preferencesStore } from '@wordpress/preferences'; import { create, remove, toHTMLString } from '@wordpress/rich-text'; import deprecated from '@wordpress/deprecated'; @@ -69,7 +67,6 @@ const MILLISECONDS_PER_WEEK = 7 * 24 * 3600 * 1000; * @type {Array} */ const EMPTY_ARRAY = []; -const EMPTY_OBJECT = {}; /** * Returns a block's name given its client ID, or null if no block exists with @@ -1744,27 +1741,9 @@ export function canLockBlockType( state, nameOrType ) { return !! state.settings?.canLockBlocks; } -/** - * Return all insert usage stats. - * - * This is only exported since registry selectors need to be exported. It's marked - * as unstable so that it's not considered part of the public API. - * - * @return {Object} An object with an `id` key representing the type - * of block and an object value that contains - * block insertion statistics. - */ -export const __unstableGetInsertUsage = createRegistrySelector( - ( select ) => () => - select( preferencesStore ).get( 'core', 'insertUsage' ) ?? EMPTY_OBJECT -); - /** * Returns information about how recently and frequently a block has been inserted. * - * This is only exported since registry selectors need to be exported. It's marked - * as unstable so that it's not considered part of the public API. - * * @param {Object} state Global application state. * @param {string} id A string which identifies the insert, e.g. 'core/block/12' * @@ -1772,15 +1751,9 @@ export const __unstableGetInsertUsage = createRegistrySelector( * insert occurred as a UNIX epoch, and `count` which is * the number of inserts that have occurred. */ -export const __unstableGetInsertUsageForBlock = createRegistrySelector( - ( select ) => ( state, id ) => { - const insertUsage = select( preferencesStore ).get( - 'core', - 'insertUsage' - ); - return insertUsage?.[ id ] ?? null; - } -); +function getInsertUsage( state, id ) { + return state.preferences.insertUsage?.[ id ] ?? null; +} /** * Returns whether we can show a block type in the inserter @@ -1808,8 +1781,7 @@ const canIncludeBlockTypeInInserter = ( state, blockType, rootClientId ) => { */ const getItemFromVariation = ( state, item ) => ( variation ) => { const variationId = `${ item.id }/${ variation.name }`; - const { time, count = 0 } = - __unstableGetInsertUsageForBlock( state, variationId ) || {}; + const { time, count = 0 } = getInsertUsage( state, variationId ) || {}; return { ...item, id: variationId, @@ -1884,8 +1856,7 @@ const buildBlockTypeItem = ( state, { buildScope = 'inserter' } ) => ( ); } - const { time, count = 0 } = - __unstableGetInsertUsageForBlock( state, id ) || {}; + const { time, count = 0 } = getInsertUsage( state, id ) || {}; const blockItemBase = { id, name: blockType.name, @@ -1993,8 +1964,7 @@ export const getInserterItems = createSelector( } const id = `core/block/${ reusableBlock.id }`; - const { time, count = 0 } = - __unstableGetInsertUsageForBlock( state, id ) || {}; + const { time, count = 0 } = getInsertUsage( state, id ) || {}; const frecency = calculateFrecency( time, count ); return { @@ -2061,7 +2031,7 @@ export const getInserterItems = createSelector( state.blockListSettings[ rootClientId ], state.blocks.byClientId, state.blocks.order, - __unstableGetInsertUsage(), + state.preferences.insertUsage, state.settings.allowedBlockTypes, state.settings.templateLock, getReusableBlocks( state ), @@ -2140,7 +2110,7 @@ export const getBlockTransformItems = createSelector( ( state, rootClientId ) => [ state.blockListSettings[ rootClientId ], state.blocks.byClientId, - __unstableGetInsertUsage(), + state.preferences.insertUsage, state.settings.allowedBlockTypes, state.settings.templateLock, getBlockTypes(), diff --git a/packages/block-editor/src/store/test/actions.js b/packages/block-editor/src/store/test/actions.js index 6b7f6b80ab1d1c..8cd09d7a7e1d0a 100644 --- a/packages/block-editor/src/store/test/actions.js +++ b/packages/block-editor/src/store/test/actions.js @@ -51,7 +51,6 @@ const { updateBlock, updateBlockAttributes, updateBlockListSettings, - updateInsertUsage, updateSettings, validateBlocksToTemplate, } = actions; @@ -215,9 +214,7 @@ describe( 'actions', () => { canInsertBlockType: () => true, getBlockCount: () => 1, }; - const dispatch = Object.assign( jest.fn(), { - updateInsertUsage: () => {}, - } ); + const dispatch = jest.fn(); replaceBlock( 'chicken', block )( { select, dispatch } ); @@ -225,6 +222,7 @@ describe( 'actions', () => { type: 'REPLACE_BLOCKS', clientIds: [ 'chicken' ], blocks: [ block ], + time: expect.any( Number ), initialPosition: 0, } ); } ); @@ -256,9 +254,7 @@ describe( 'actions', () => { } }, }; - const dispatch = Object.assign( jest.fn(), { - updateInsertUsage: () => {}, - } ); + const dispatch = jest.fn(); replaceBlocks( [ 'chicken' ], blocks )( { select, dispatch } ); @@ -283,9 +279,7 @@ describe( 'actions', () => { canInsertBlockType: () => true, getBlockCount: () => 1, }; - const dispatch = Object.assign( jest.fn(), { - updateInsertUsage: () => {}, - } ); + const dispatch = jest.fn(); replaceBlocks( [ 'chicken' ], blocks )( { select, dispatch } ); @@ -293,6 +287,7 @@ describe( 'actions', () => { type: 'REPLACE_BLOCKS', clientIds: [ 'chicken' ], blocks, + time: expect.any( Number ), initialPosition: 0, } ); } ); @@ -317,9 +312,7 @@ describe( 'actions', () => { canInsertBlockType: () => true, getBlockCount: () => 1, }; - const dispatch = Object.assign( jest.fn(), { - updateInsertUsage: () => {}, - } ); + const dispatch = jest.fn(); replaceBlocks( [ 'chicken' ], @@ -333,44 +326,12 @@ describe( 'actions', () => { type: 'REPLACE_BLOCKS', clientIds: [ 'chicken' ], blocks, + time: expect.any( Number ), indexToSelect: null, initialPosition: null, meta: { patternName: 'core/chicken-ribs-pattern' }, } ); } ); - - it( 'should set insertUsage in the preferences store', () => { - const blocks = [ - { - clientId: 'ribs', - name: 'core/test-ribs', - }, - { - clientId: 'chicken', - name: 'core/test-chicken', - }, - ]; - - const select = { - getSettings: () => null, - getBlockRootClientId: () => null, - canInsertBlockType: () => true, - getBlockCount: () => 1, - }; - const updateInsertUsageSpy = jest.fn(); - const dispatch = Object.assign( jest.fn(), { - updateInsertUsage: updateInsertUsageSpy, - } ); - - replaceBlocks( - [ 'pineapple' ], - blocks, - null, - null - )( { select, dispatch } ); - - expect( updateInsertUsageSpy ).toHaveBeenCalledWith( blocks ); - } ); } ); describe( 'insertBlock', () => { @@ -385,9 +346,7 @@ describe( 'actions', () => { getSettings: () => null, canInsertBlockType: () => true, }; - const dispatch = Object.assign( jest.fn(), { - updateInsertUsage: () => {}, - } ); + const dispatch = jest.fn(); insertBlock( block, @@ -401,6 +360,7 @@ describe( 'actions', () => { blocks: [ block ], index, rootClientId: 'testclientid', + time: expect.any( Number ), updateSelection: true, initialPosition: 0, } ); @@ -434,9 +394,7 @@ describe( 'actions', () => { } ), canInsertBlockType: () => true, }; - const dispatch = Object.assign( jest.fn(), { - updateInsertUsage: () => {}, - } ); + const dispatch = jest.fn(); insertBlocks( blocks, @@ -460,6 +418,7 @@ describe( 'actions', () => { ], index: 5, rootClientId: 'testrootid', + time: expect.any( Number ), updateSelection: false, initialPosition: null, } ); @@ -485,9 +444,7 @@ describe( 'actions', () => { } ), canInsertBlockType: () => true, }; - const dispatch = Object.assign( jest.fn(), { - updateInsertUsage: () => {}, - } ); + const dispatch = jest.fn(); insertBlocks( blocks, @@ -506,6 +463,7 @@ describe( 'actions', () => { ], index: 5, rootClientId: 'testrootid', + time: expect.any( Number ), updateSelection: false, initialPosition: null, } ); @@ -541,9 +499,7 @@ describe( 'actions', () => { } }, }; - const dispatch = Object.assign( jest.fn(), { - updateInsertUsage: () => {}, - } ); + const dispatch = jest.fn(); insertBlocks( blocks, @@ -557,6 +513,7 @@ describe( 'actions', () => { blocks: [ ribsBlock, chickenRibsBlock ], index: 5, rootClientId: 'testrootid', + time: expect.any( Number ), updateSelection: false, initialPosition: null, } ); @@ -577,9 +534,7 @@ describe( 'actions', () => { getSettings: () => null, canInsertBlockType: () => false, }; - const dispatch = Object.assign( jest.fn(), { - updateInsertUsage: () => {}, - } ); + const dispatch = jest.fn(); insertBlocks( blocks, @@ -622,9 +577,7 @@ describe( 'actions', () => { } }, }; - const dispatch = Object.assign( jest.fn(), { - updateInsertUsage: () => {}, - } ); + const dispatch = jest.fn(); insertBlocks( blocks, @@ -640,206 +593,12 @@ describe( 'actions', () => { blocks: [ ribsBlock, chickenRibsBlock ], index: 5, rootClientId: 'testrootid', + time: expect.any( Number ), updateSelection: false, initialPosition: null, meta: { patternName: 'core/chicken-ribs-pattern' }, } ); } ); - - it( 'should set insertUsage in the preferences store', () => { - const blocks = [ - { - clientId: 'ribs', - name: 'core/test-ribs', - }, - { - clientId: 'chicken', - name: 'core/test-chicken', - }, - ]; - - const select = { - getSettings: () => null, - canInsertBlockType: () => true, - }; - const updateInsertUsageSpy = jest.fn(); - const dispatch = Object.assign( jest.fn(), { - updateInsertUsage: updateInsertUsageSpy, - } ); - - insertBlocks( - blocks, - 5, - 'testrootid', - false, - 0 - )( { select, dispatch } ); - - expect( updateInsertUsageSpy ).toHaveBeenCalledWith( blocks ); - } ); - } ); - - describe( 'updateInsertUsage', () => { - it( 'should record recently used blocks', () => { - const setPreference = jest.fn(); - const registry = { - dispatch: () => ( { - set: setPreference, - } ), - select: () => ( { - get: () => {}, - getActiveBlockVariation: () => {}, - } ), - }; - - updateInsertUsage( [ - { - clientId: 'bacon', - name: 'core/embed', - }, - ] )( { registry } ); - - expect( setPreference ).toHaveBeenCalledWith( - 'core', - 'insertUsage', - { - 'core/embed': { - time: expect.any( Number ), - count: 1, - insert: { name: 'core/embed' }, - }, - } - ); - } ); - - it( 'merges insert usage if more blocks are added of the same type', () => { - const setPreference = jest.fn(); - const registry = { - dispatch: () => ( { - set: setPreference, - } ), - select: () => ( { - // simulate an existing embed block. - get: () => ( { - 'core/embed': { - time: 123456, - count: 1, - insert: { name: 'core/embed' }, - }, - } ), - getActiveBlockVariation: () => {}, - } ), - }; - - updateInsertUsage( [ - { - clientId: 'eggs', - name: 'core/embed', - }, - { - clientId: 'bacon', - name: 'core/block', - attributes: { ref: 123 }, - }, - ] )( { registry } ); - - expect( setPreference ).toHaveBeenCalledWith( - 'core', - 'insertUsage', - { - // The reusable block has a special case where each ref is - // stored as though an individual block, and the ref is - // also recorded in the `insert` object. - 'core/block/123': { - time: expect.any( Number ), - count: 1, - insert: { name: 'core/block', ref: 123 }, - }, - 'core/embed': { - time: expect.any( Number ), - count: 2, - insert: { name: 'core/embed' }, - }, - } - ); - } ); - - describe( 'block variations handling', () => { - const blockWithVariations = 'core/test-block-with-variations'; - - it( 'should return proper results with both found or not found block variation matches', () => { - const setPreference = jest.fn(); - const registry = { - dispatch: () => ( { - set: setPreference, - } ), - select: () => ( { - get: () => {}, - // simulate an active block variation: - // - 'apple' when the fruit attribute is 'apple'. - // - 'orange' when the fruit attribute is 'orange'. - getActiveBlockVariation: ( - blockName, - { fruit } = {} - ) => { - if ( blockName === blockWithVariations ) { - if ( fruit === 'orange' ) - return { name: 'orange' }; - if ( fruit === 'apple' ) - return { name: 'apple' }; - } - }, - } ), - }; - - updateInsertUsage( [ - { - clientId: 'no match', - name: blockWithVariations, - }, - { - clientId: 'not a variation match', - name: blockWithVariations, - attributes: { fruit: 'not in a variation' }, - }, - { - clientId: 'orange', - name: blockWithVariations, - attributes: { fruit: 'orange' }, - }, - { - clientId: 'apple', - name: blockWithVariations, - attributes: { fruit: 'apple' }, - }, - ] )( { registry } ); - - const orangeVariationName = `${ blockWithVariations }/orange`; - const appleVariationName = `${ blockWithVariations }/apple`; - - expect( setPreference ).toHaveBeenCalledWith( - 'core', - 'insertUsage', - { - [ orangeVariationName ]: { - time: expect.any( Number ), - count: 1, - insert: { name: orangeVariationName }, - }, - [ appleVariationName ]: { - time: expect.any( Number ), - count: 1, - insert: { name: appleVariationName }, - }, - [ blockWithVariations ]: { - time: expect.any( Number ), - count: 2, - insert: { name: blockWithVariations }, - }, - } - ); - } ); - } ); } ); describe( 'showInsertionPoint', () => { @@ -1111,6 +870,7 @@ describe( 'actions', () => { type: 'REPLACE_INNER_BLOCKS', blocks: [ block ], rootClientId: 'root', + time: expect.any( Number ), updateSelection: false, initialPosition: null, } ); @@ -1121,6 +881,7 @@ describe( 'actions', () => { type: 'REPLACE_INNER_BLOCKS', blocks: [ block ], rootClientId: 'root', + time: expect.any( Number ), updateSelection: true, initialPosition: 0, } ); diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index 3ccb38f0427e4c..faca5d4f86117f 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -25,6 +25,7 @@ import { selection, initialPosition, isMultiSelecting, + preferences, blocksMode, insertionPoint, template, @@ -2557,6 +2558,154 @@ describe( 'state', () => { } ); } ); + describe( 'preferences()', () => { + it( 'should apply all defaults', () => { + const state = preferences( undefined, {} ); + + expect( state ).toEqual( { + insertUsage: {}, + } ); + } ); + it( 'should record recently used blocks', () => { + const state = preferences( deepFreeze( { insertUsage: {} } ), { + type: 'INSERT_BLOCKS', + blocks: [ + { + clientId: 'bacon', + name: 'core/embed', + }, + ], + time: 123456, + } ); + + expect( state ).toEqual( { + insertUsage: { + 'core/embed': { + time: 123456, + count: 1, + insert: { name: 'core/embed' }, + }, + }, + } ); + + const twoRecentBlocks = preferences( + deepFreeze( { + insertUsage: { + 'core/embed': { + time: 123456, + count: 1, + insert: { name: 'core/embed' }, + }, + }, + } ), + { + type: 'INSERT_BLOCKS', + blocks: [ + { + clientId: 'eggs', + name: 'core/embed', + }, + { + clientId: 'bacon', + name: 'core/block', + attributes: { ref: 123 }, + }, + ], + time: 123457, + } + ); + + expect( twoRecentBlocks ).toEqual( { + insertUsage: { + 'core/embed': { + time: 123457, + count: 2, + insert: { name: 'core/embed' }, + }, + 'core/block/123': { + time: 123457, + count: 1, + insert: { name: 'core/block', ref: 123 }, + }, + }, + } ); + } ); + describe( 'block variations handling', () => { + const blockWithVariations = 'core/test-block-with-variations'; + beforeAll( () => { + const variations = [ + { + name: 'apple', + attributes: { fruit: 'apple' }, + }, + { name: 'orange', attributes: { fruit: 'orange' } }, + ].map( ( variation ) => ( { + ...variation, + isActive: ( blockAttributes, variationAttributes ) => + blockAttributes?.fruit === variationAttributes.fruit, + } ) ); + registerBlockType( blockWithVariations, { + save: noop, + edit: noop, + title: 'Fruit with variations', + variations, + } ); + } ); + afterAll( () => { + unregisterBlockType( blockWithVariations ); + } ); + it( 'should return proper results with both found or not found block variation matches', () => { + const state = preferences( deepFreeze( { insertUsage: {} } ), { + type: 'INSERT_BLOCKS', + blocks: [ + { + clientId: 'no match', + name: blockWithVariations, + }, + { + clientId: 'not a variation match', + name: blockWithVariations, + attributes: { fruit: 'not in a variation' }, + }, + { + clientId: 'orange', + name: blockWithVariations, + attributes: { fruit: 'orange' }, + }, + { + clientId: 'apple', + name: blockWithVariations, + attributes: { fruit: 'apple' }, + }, + ], + time: 123456, + } ); + + const orangeVariationName = `${ blockWithVariations }/orange`; + const appleVariationName = `${ blockWithVariations }/apple`; + expect( state ).toEqual( { + insertUsage: expect.objectContaining( { + [ orangeVariationName ]: { + time: 123456, + count: 1, + insert: { name: orangeVariationName }, + }, + [ appleVariationName ]: { + time: 123456, + count: 1, + insert: { name: appleVariationName }, + }, + [ blockWithVariations ]: { + time: 123456, + count: 2, + insert: { name: blockWithVariations }, + }, + } ), + } ); + } ); + } ); + } ); + describe( 'blocksMode', () => { it( 'should set mode to html if not set', () => { const action = { diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 772c514169d767..a152b7da564c52 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -77,8 +77,6 @@ const { __experimentalGetPatternTransformItems, wasBlockJustInserted, __experimentalGetGlobalBlocksByName, - __unstableGetInsertUsage, - __unstableGetInsertUsageForBlock, } = selectors; describe( 'selectors', () => { @@ -2746,12 +2744,6 @@ describe( 'selectors', () => { describe( 'getInserterItems', () => { it( 'should properly list block type and reusable block items', () => { - const registry = { - select: () => ( { get: () => ( {} ) } ), - }; - __unstableGetInsertUsage.registry = registry; - __unstableGetInsertUsageForBlock.registry = registry; - const state = { blocks: { byClientId: {}, @@ -2775,6 +2767,11 @@ describe( 'selectors', () => { }, ], }, + // Intentionally include a test case which considers + // `insertUsage` as not present within preferences. + // + // See: https://github.com/WordPress/gutenberg/issues/14580 + preferences: {}, blockListSettings: {}, }; const items = getInserterItems( state ); @@ -2816,15 +2813,6 @@ describe( 'selectors', () => { } ); it( 'should correctly cache the return values', () => { - // Define the empty object here to simulate that the preferences - // store won't return a new object every time. - const EMPTY_OBJECT = {}; - const registry = { - select: () => ( { get: () => EMPTY_OBJECT } ), - }; - __unstableGetInsertUsage.registry = registry; - __unstableGetInsertUsageForBlock.registry = registry; - const state = { blocks: { byClientId: { @@ -2876,6 +2864,9 @@ describe( 'selectors', () => { }, ], }, + preferences: { + insertUsage: {}, + }, blockListSettings: { block3: {}, block4: {}, @@ -2929,12 +2920,6 @@ describe( 'selectors', () => { } ); it( 'should set isDisabled when a block with `multiple: false` has been used', () => { - const registry = { - select: () => ( { get: () => ( {} ) } ), - }; - __unstableGetInsertUsage.registry = registry; - __unstableGetInsertUsageForBlock.registry = registry; - const state = { blocks: { byClientId: { @@ -2960,6 +2945,9 @@ describe( 'selectors', () => { controlledInnerBlocks: {}, parents: {}, }, + preferences: { + insertUsage: {}, + }, blockListSettings: {}, settings: {}, }; @@ -2971,16 +2959,6 @@ describe( 'selectors', () => { } ); it( 'should set a frecency', () => { - // Simulate returning block insertUsage from the preferences store. - const registry = { - select: () => ( { - get: () => ( { - 'core/test-block-b': { count: 10, time: 1000 }, - } ), - } ), - }; - __unstableGetInsertUsage.registry = registry; - __unstableGetInsertUsageForBlock.registry = registry; const state = { blocks: { byClientId: {}, @@ -2989,6 +2967,11 @@ describe( 'selectors', () => { parents: {}, cache: {}, }, + preferences: { + insertUsage: { + 'core/test-block-b': { count: 10, time: 1000 }, + }, + }, blockListSettings: {}, settings: {}, }; @@ -3194,17 +3177,6 @@ describe( 'selectors', () => { ); } ); it( 'should set frecency', () => { - // Simulate returning block insertUsage from the preferences store. - const registry = { - select: () => ( { - get: () => ( { - 'core/with-tranforms-a': { count: 10, time: 1000 }, - } ), - } ), - }; - __unstableGetInsertUsage.registry = registry; - __unstableGetInsertUsageForBlock.registry = registry; - const state = { blocks: { byClientId: {}, diff --git a/packages/data/src/plugins/persistence/index.js b/packages/data/src/plugins/persistence/index.js index 653d291970ad34..f00f8a8fc2b185 100644 --- a/packages/data/src/plugins/persistence/index.js +++ b/packages/data/src/plugins/persistence/index.js @@ -639,12 +639,6 @@ persistencePlugin.__unstableMigrate = ( pluginOptions ) => { { from: 'core/edit-site', scope: 'core/edit-site' }, 'editorMode' ); - migrateIndividualPreferenceToPreferencesStore( - persistence, - { from: 'core/block-editor', scope: 'core' }, - 'insertUsage' - ); - migrateInterfaceEnableItemsToPreferencesStore( persistence ); }; diff --git a/packages/reusable-blocks/src/store/test/actions.js b/packages/reusable-blocks/src/store/test/actions.js index f5cb46716f8b9b..ffa907b4b18a39 100644 --- a/packages/reusable-blocks/src/store/test/actions.js +++ b/packages/reusable-blocks/src/store/test/actions.js @@ -11,7 +11,6 @@ import { } from '@wordpress/blocks'; import { store as coreStore } from '@wordpress/core-data'; -import { store as preferencesStore } from '@wordpress/preferences'; import apiFetch from '@wordpress/api-fetch'; /** @@ -32,7 +31,6 @@ function createRegistryWithStores() { registry.register( blockEditorStore ); registry.register( reusableBlocksStore ); registry.register( blocksStore ); - registry.register( preferencesStore ); // Register entity here instead of mocking API handlers for loadPostTypeEntities() registry.dispatch( coreStore ).addEntities( [ From 08413dd96425592c0975c19d238b9244f13bbc78 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 6 Apr 2022 16:16:17 +0800 Subject: [PATCH 39/74] Migrate basic preferences in from localstorage --- lib/experimental/persisted-preferences.php | 7 +++++-- packages/database-persistence-layer/src/index.js | 1 + .../src/migrations/convert-from-local-storage.js | 13 +++++++++++++ .../src/migrations/index.js | 0 4 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 packages/database-persistence-layer/src/migrations/convert-from-local-storage.js delete mode 100644 packages/database-persistence-layer/src/migrations/index.js diff --git a/lib/experimental/persisted-preferences.php b/lib/experimental/persisted-preferences.php index b30c773ed37a0d..d55f0b9cd38e76 100644 --- a/lib/experimental/persisted-preferences.php +++ b/lib/experimental/persisted-preferences.php @@ -41,7 +41,8 @@ function gutenberg_configure_persisted_preferences() { 'wp-preferences', sprintf( 'const serverData = %s; - const localStorageRestoreKey = "WP_PREFERENCES_USER_%s"; + const userId = "%s"; + const localStorageRestoreKey = `WP_PREFERENCES_USER_${ userId }`; const localData = JSON.parse( localStorage.getItem( localStorageRestoreKey ) ); @@ -54,7 +55,9 @@ function gutenberg_configure_persisted_preferences() { } else if ( localData ) { preloadedData = localData; } else { - preloadedData = {}; + // Check if there is data in the old format. + const { __unstableConvertFromLocalStorage } = wp.databasePersistenceLayer; + preloadedData = __unstableConvertFromLocalStorage( userId ); } const { create } = wp.databasePersistenceLayer; diff --git a/packages/database-persistence-layer/src/index.js b/packages/database-persistence-layer/src/index.js index c02d86cd8cb441..610c0ce2625788 100644 --- a/packages/database-persistence-layer/src/index.js +++ b/packages/database-persistence-layer/src/index.js @@ -1 +1,2 @@ export { default as create } from './create'; +export { default as __unstableConvertFromLocalStorage } from './migrations/convert-from-local-storage'; diff --git a/packages/database-persistence-layer/src/migrations/convert-from-local-storage.js b/packages/database-persistence-layer/src/migrations/convert-from-local-storage.js new file mode 100644 index 00000000000000..04042ff9053baf --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/convert-from-local-storage.js @@ -0,0 +1,13 @@ +/** + * Converts data from the old `@wordpress/data` package format. + * + * @param {number | string} userId + * + * @return {Object | undefined} The converted data or `undefined` if there was nothing to convert. + */ +export default function convertFromLocalStorage( userId ) { + const key = `WP_DATA_USER_${ userId }`; + const unparsedData = window.localStorage.getItem( key ); + const data = JSON.parse( unparsedData ); + return data?.[ 'core/preferences' ]; +} diff --git a/packages/database-persistence-layer/src/migrations/index.js b/packages/database-persistence-layer/src/migrations/index.js deleted file mode 100644 index e69de29bb2d1d6..00000000000000 From 4192f7101fe7a13ab8870f7d6b86238a543ab6f2 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 6 Apr 2022 16:49:05 +0800 Subject: [PATCH 40/74] Port the migrate-individual-preference function --- ...ividual-preference-to-preferences-store.js | 65 ++++++ ...ividual-preference-to-preferences-store.js | 188 ++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 packages/database-persistence-layer/src/migrations/migrate-individual-preference-to-preferences-store.js create mode 100644 packages/database-persistence-layer/src/migrations/test/migrate-individual-preference-to-preferences-store.js diff --git a/packages/database-persistence-layer/src/migrations/migrate-individual-preference-to-preferences-store.js b/packages/database-persistence-layer/src/migrations/migrate-individual-preference-to-preferences-store.js new file mode 100644 index 00000000000000..350d22c7d91c15 --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/migrate-individual-preference-to-preferences-store.js @@ -0,0 +1,65 @@ +const identity = ( arg ) => arg; + +/** + * Migrates an individual item inside the `preferences` object for a store. + * + * @param {Object} state The original state. + * @param {Object} migrate An options object that contains details of the migration. + * @param {string} migrate.from The name of the store to migrate from. + * @param {string} migrate.scope The scope in the preferences store to migrate to. + * @param {string} key The key in the preferences object to migrate. + * @param {?Function} convert A function that converts preferences from one format to another. + */ +export default function migrateIndividualPreferenceToPreferencesStore( + state, + { from: sourceStoreName, scope }, + key, + convert = identity +) { + const preferencesStoreName = 'core/preferences'; + const sourcePreference = state[ sourceStoreName ]?.preferences?.[ key ]; + + // There's nothing to migrate, exit early. + if ( sourcePreference === undefined ) { + return state; + } + + const targetPreference = + state[ preferencesStoreName ]?.preferences?.[ scope ]?.[ key ]; + + // There's existing data at the target, so don't overwrite it, exit early. + if ( targetPreference ) { + return state; + } + + const otherScopes = state[ preferencesStoreName ]?.preferences; + const otherPreferences = + state[ preferencesStoreName ]?.preferences?.[ scope ]; + + const otherSourceState = state[ sourceStoreName ]; + const allSourcePreferences = state[ sourceStoreName ]?.preferences; + + // Pass an object with the key and value as this allows the convert + // function to convert to a data structure that has different keys. + const convertedPreferences = convert( { [ key ]: sourcePreference } ); + + return { + ...state, + [ preferencesStoreName ]: { + preferences: { + ...otherScopes, + [ scope ]: { + ...otherPreferences, + ...convertedPreferences, + }, + }, + }, + [ sourceStoreName ]: { + ...otherSourceState, + preferences: { + ...allSourcePreferences, + [ key ]: undefined, + }, + }, + }; +} diff --git a/packages/database-persistence-layer/src/migrations/test/migrate-individual-preference-to-preferences-store.js b/packages/database-persistence-layer/src/migrations/test/migrate-individual-preference-to-preferences-store.js new file mode 100644 index 00000000000000..08b4381e5334e2 --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/test/migrate-individual-preference-to-preferences-store.js @@ -0,0 +1,188 @@ +/** + * Internal dependencies + */ +import migrateIndividualPreferenceToPreferencesStore from '../migrate-individual-preference-to-preferences-store'; + +describe( 'migrateIndividualPreferenceToPreferencesStore', () => { + it( 'migrates an individual preference from the source to the preferences store', () => { + const initialState = { + 'core/test': { + preferences: { + myPreference: '123', + }, + }, + }; + + const convertedData = migrateIndividualPreferenceToPreferencesStore( + initialState, + { from: 'core/test', scope: 'core/test' }, + 'myPreference' + ); + + expect( convertedData ).toEqual( { + 'core/preferences': { + preferences: { + 'core/test': { + myPreference: '123', + }, + }, + }, + 'core/test': { + preferences: { + myPreference: undefined, + }, + }, + } ); + } ); + + it( 'does not overwrite other preferences in the preferences store', () => { + const initialState = { + 'core/test': { + otherData: { + test: 1, + }, + preferences: { + myPreference: '123', + }, + }, + 'core/preferences': { + preferences: { + 'core/other-store': { + preferenceA: 1, + preferenceB: 2, + }, + 'core/test': { + unrelatedPreference: 'unrelated-value', + }, + }, + }, + }; + + const convertedData = migrateIndividualPreferenceToPreferencesStore( + initialState, + { from: 'core/test', scope: 'core/test' }, + 'myPreference' + ); + + expect( convertedData ).toEqual( { + 'core/preferences': { + preferences: { + 'core/other-store': { + preferenceA: 1, + preferenceB: 2, + }, + 'core/test': { + unrelatedPreference: 'unrelated-value', + myPreference: '123', + }, + }, + }, + 'core/test': { + otherData: { + test: 1, + }, + preferences: { + myPreference: undefined, + }, + }, + } ); + } ); + + it( 'supports moving data to a scope that is differently named to the source store', () => { + const initialState = { + 'core/source': { + preferences: { + myPreference: '123', + }, + }, + }; + + const convertedData = migrateIndividualPreferenceToPreferencesStore( + initialState, + { from: 'core/source', scope: 'core/destination' }, + 'myPreference' + ); + + expect( convertedData ).toEqual( { + 'core/preferences': { + preferences: { + 'core/destination': { + myPreference: '123', + }, + }, + }, + 'core/source': { + preferences: { + myPreference: undefined, + }, + }, + } ); + } ); + + it( 'does not migrate data if there is already a matching preference key at the target', () => { + const initialState = { + 'core/test': { + preferences: { + myPreference: '123', + }, + }, + 'core/preferences': { + preferences: { + 'core/test': { + myPreference: 'already-set', + }, + }, + }, + }; + + const convertedData = migrateIndividualPreferenceToPreferencesStore( + initialState, + { from: 'core/test', scope: 'core/test' }, + 'myPreference' + ); + + expect( convertedData ).toEqual( { + 'core/preferences': { + preferences: { + 'core/test': { + myPreference: 'already-set', + }, + }, + }, + 'core/test': { + preferences: { + myPreference: '123', + }, + }, + } ); + } ); + + it( 'migrates preferences that have a `false` value', () => { + const initialState = { + 'core/test': { + preferences: { + myFalsePreference: false, + }, + }, + }; + + const convertedData = migrateIndividualPreferenceToPreferencesStore( + initialState, + { from: 'core/test', scope: 'core/test' }, + 'myFalsePreference' + ); + + expect( convertedData ).toEqual( { + 'core/preferences': { + preferences: { + 'core/test': { + myFalsePreference: false, + }, + }, + }, + 'core/test': { + preferences: {}, + }, + } ); + } ); +} ); From ca3fabc8bd7836a641c17038550d97e28a535c63 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 6 Apr 2022 17:17:37 +0800 Subject: [PATCH 41/74] Port the migrate-feature-preference function --- ...eature-preferences-to-preferences-store.js | 89 ++++++ ...eature-preferences-to-preferences-store.js | 272 ++++++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 packages/database-persistence-layer/src/migrations/migrate-feature-preferences-to-preferences-store.js create mode 100644 packages/database-persistence-layer/src/migrations/test/migrate-feature-preferences-to-preferences-store.js diff --git a/packages/database-persistence-layer/src/migrations/migrate-feature-preferences-to-preferences-store.js b/packages/database-persistence-layer/src/migrations/migrate-feature-preferences-to-preferences-store.js new file mode 100644 index 00000000000000..60d6c5c81f3282 --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/migrate-feature-preferences-to-preferences-store.js @@ -0,0 +1,89 @@ +/** + * Move the 'features' object in local storage from the sourceStoreName to the + * preferences store. + * + * @param {Object} state The state before migration. + * @param {string} sourceStoreName The name of the store that has persisted + * preferences to migrate to the preferences + * package. + * @return {Object} The migrated state + */ +export default function migrateFeaturePreferencesToPreferencesStore( + state, + sourceStoreName +) { + const preferencesStoreName = 'core/preferences'; + const interfaceStoreName = 'core/interface'; + + // Features most recently (and briefly) lived in the interface package. + // If data exists there, prioritize using that for the migration. If not + // also check the original package as the user may have updated from an + // older block editor version. + const interfaceFeatures = + state[ interfaceStoreName ]?.preferences?.features?.[ sourceStoreName ]; + const sourceFeatures = state[ sourceStoreName ]?.preferences?.features; + const featuresToMigrate = interfaceFeatures + ? interfaceFeatures + : sourceFeatures; + + if ( ! featuresToMigrate ) { + return state; + } + + const existingPreferences = state[ preferencesStoreName ]?.preferences; + + // Avoid migrating features again if they've previously been migrated. + if ( existingPreferences?.[ sourceStoreName ] ) { + return state; + } + + let updatedInterfaceState; + if ( interfaceFeatures ) { + const otherInterfaceState = state[ interfaceStoreName ]; + const otherInterfaceScopes = + state[ interfaceStoreName ]?.preferences?.features; + + updatedInterfaceState = { + [ interfaceStoreName ]: { + ...otherInterfaceState, + preferences: { + features: { + ...otherInterfaceScopes, + [ sourceStoreName ]: undefined, + }, + }, + }, + }; + } + + let updatedSourceState; + if ( sourceFeatures ) { + const otherSourceState = state[ sourceStoreName ]; + const sourcePreferences = state[ sourceStoreName ]?.preferences; + + updatedSourceState = { + [ sourceStoreName ]: { + ...otherSourceState, + preferences: { + ...sourcePreferences, + features: undefined, + }, + }, + }; + } + + // Set the feature values in the interface store, the features + // object is keyed by 'scope', which matches the store name for + // the source. + return { + ...state, + [ preferencesStoreName ]: { + preferences: { + ...existingPreferences, + [ sourceStoreName ]: featuresToMigrate, + }, + }, + ...updatedInterfaceState, + ...updatedSourceState, + }; +} diff --git a/packages/database-persistence-layer/src/migrations/test/migrate-feature-preferences-to-preferences-store.js b/packages/database-persistence-layer/src/migrations/test/migrate-feature-preferences-to-preferences-store.js new file mode 100644 index 00000000000000..872134e6df24d9 --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/test/migrate-feature-preferences-to-preferences-store.js @@ -0,0 +1,272 @@ +/** + * Internal dependencies + */ +import migrateFeaturePreferencesToPreferencesStore from '../migrate-feature-preferences-to-preferences-store'; + +describe( 'migrateFeaturePreferencesToPreferencesStore', () => { + it( 'migrates multiple preferences from persisted source stores to preferences without overwriting data', () => { + const state = { + 'core/test-a': { + preferences: { + features: { + featureA: true, + featureB: false, + featureC: true, + }, + }, + }, + 'core/test-b': { + preferences: { + features: { + featureD: true, + featureE: false, + featureF: true, + }, + }, + }, + }; + + let convertedState = migrateFeaturePreferencesToPreferencesStore( + state, + 'core/test-a' + ); + + convertedState = migrateFeaturePreferencesToPreferencesStore( + convertedState, + 'core/test-b' + ); + + expect( convertedState ).toEqual( { + 'core/preferences': { + preferences: { + 'core/test-a': { + featureA: true, + featureB: false, + featureC: true, + }, + 'core/test-b': { + featureD: true, + featureE: false, + featureF: true, + }, + }, + }, + 'core/test-a': { + preferences: { + features: undefined, + }, + }, + 'core/test-b': { + preferences: { + features: undefined, + }, + }, + } ); + } ); + + it( 'migrates multiple preferences from the persisted interface store to preferences, with interface state taking precedence over source stores', () => { + const state = { + 'core/test-a': { + preferences: { + features: { + featureA: true, + featureB: false, + featureC: true, + }, + }, + }, + 'core/test-b': { + preferences: { + features: { + featureD: true, + featureE: false, + featureF: true, + }, + }, + }, + 'core/interface': { + otherData: { + test: 1, + }, + preferences: { + features: { + 'core/test-a': { + featureG: true, + featureH: false, + featureI: true, + }, + 'core/test-b': { + featureJ: true, + featureK: false, + featureL: true, + }, + }, + }, + }, + }; + + let convertedState = migrateFeaturePreferencesToPreferencesStore( + state, + 'core/test-a' + ); + + convertedState = migrateFeaturePreferencesToPreferencesStore( + convertedState, + 'core/test-b' + ); + + expect( convertedState ).toEqual( { + 'core/preferences': { + preferences: { + 'core/test-a': { + featureG: true, + featureH: false, + featureI: true, + }, + 'core/test-b': { + featureJ: true, + featureK: false, + featureL: true, + }, + }, + }, + 'core/interface': { + otherData: { + test: 1, + }, + preferences: { + features: { + 'core/test-a': undefined, + 'core/test-b': undefined, + }, + }, + }, + 'core/test-a': { + preferences: { + features: undefined, + }, + }, + 'core/test-b': { + preferences: { + features: undefined, + }, + }, + } ); + } ); + + it( 'only migrates persisted preference data for the source name from source stores', () => { + const state = { + 'core/test-a': { + otherData: { + test: 1, + }, + preferences: { + features: { + featureA: true, + featureB: false, + featureC: true, + }, + }, + }, + 'core/test-b': { + otherData: { + test: 2, + }, + preferences: { + features: { + featureD: true, + featureE: false, + featureF: true, + }, + }, + }, + }; + + const convertedState = migrateFeaturePreferencesToPreferencesStore( + state, + 'core/test-a' + ); + + expect( convertedState ).toEqual( { + 'core/preferences': { + preferences: { + 'core/test-a': { + featureA: true, + featureB: false, + featureC: true, + }, + }, + }, + 'core/test-a': { + otherData: { + test: 1, + }, + preferences: { + features: undefined, + }, + }, + 'core/test-b': { + otherData: { + test: 2, + }, + preferences: { + features: { + featureD: true, + featureE: false, + featureF: true, + }, + }, + }, + } ); + } ); + + it( 'only migrates persisted data for the source name from interface', () => { + const state = { + 'core/interface': { + preferences: { + features: { + 'core/test-a': { + featureG: true, + featureH: false, + featureI: true, + }, + 'core/test-b': { + featureJ: true, + featureK: false, + featureL: true, + }, + }, + }, + }, + }; + + const convertedState = migrateFeaturePreferencesToPreferencesStore( + state, + 'core/test-a' + ); + + expect( convertedState ).toEqual( { + 'core/preferences': { + preferences: { + 'core/test-a': { + featureG: true, + featureH: false, + featureI: true, + }, + }, + }, + 'core/interface': { + preferences: { + features: { + 'core/test-a': undefined, + 'core/test-b': { + featureJ: true, + featureK: false, + featureL: true, + }, + }, + }, + }, + } ); + } ); +} ); From 8289807c94696f774463c1ed254c595afd891541 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 7 Apr 2022 11:00:37 +0800 Subject: [PATCH 42/74] Copy convert-edit-post-panels function from persistence plugin --- .../migrations/convert-edit-post-panels.js | 50 +++++++++++++++++++ .../test/convert-edit-post-panels.js | 47 +++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 packages/database-persistence-layer/src/migrations/convert-edit-post-panels.js create mode 100644 packages/database-persistence-layer/src/migrations/test/convert-edit-post-panels.js diff --git a/packages/database-persistence-layer/src/migrations/convert-edit-post-panels.js b/packages/database-persistence-layer/src/migrations/convert-edit-post-panels.js new file mode 100644 index 00000000000000..09362e3f369d31 --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/convert-edit-post-panels.js @@ -0,0 +1,50 @@ +/** + * Convert from: + * ``` + * { + * panels: { + * tags: { + * enabled: true, + * opened: true, + * }, + * permalinks: { + * enabled: false, + * opened: false, + * }, + * }, + * } + * ``` + * + * to: + * { + * inactivePanels: [ + * 'permalinks', + * ], + * openPanels: [ + * 'tags', + * ], + * } + * + * @param {Object} preferences A preferences object. + * + * @return {Object} The converted data. + */ +export default function convertEditPostPanels( preferences ) { + const panels = preferences?.panels ?? {}; + return Object.keys( panels ).reduce( + ( convertedData, panelName ) => { + const panel = panels[ panelName ]; + + if ( panel?.enabled === false ) { + convertedData.inactivePanels.push( panelName ); + } + + if ( panel?.opened === true ) { + convertedData.openPanels.push( panelName ); + } + + return convertedData; + }, + { inactivePanels: [], openPanels: [] } + ); +} diff --git a/packages/database-persistence-layer/src/migrations/test/convert-edit-post-panels.js b/packages/database-persistence-layer/src/migrations/test/convert-edit-post-panels.js new file mode 100644 index 00000000000000..579ed661a673ce --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/test/convert-edit-post-panels.js @@ -0,0 +1,47 @@ +/** + * Internal dependencies + */ +import convertEditPostPanels from '../convert-edit-post-panels'; + +describe( 'convertEditPostPanels', () => { + it( 'converts from one format to another', () => { + expect( + convertEditPostPanels( { + panels: { + tags: { + enabled: true, + opened: true, + }, + permalinks: { + enabled: false, + opened: false, + }, + categories: { + enabled: true, + opened: false, + }, + excerpt: { + enabled: false, + opened: true, + }, + discussion: { + enabled: false, + }, + template: { + opened: true, + }, + }, + } ) + ).toEqual( { + inactivePanels: [ 'permalinks', 'excerpt', 'discussion' ], + openPanels: [ 'tags', 'excerpt', 'template' ], + } ); + } ); + + it( 'returns empty arrays when there is no data to convert', () => { + expect( convertEditPostPanels( {} ) ).toEqual( { + inactivePanels: [], + openPanels: [], + } ); + } ); +} ); From 32ba8c01f80dad34f60ec2086b32428a23e5cf49 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 7 Apr 2022 12:12:00 +0800 Subject: [PATCH 43/74] Add moveInterfaceEnableItems function and improve folder structure --- .../convert-edit-post-panels.js | 0 .../index.js} | 2 +- ...ove-feature-preferences-to-preferences.js} | 2 +- ...e-individual-preference-to-preferences.js} | 2 +- ...e-interface-enable-items-to-preferences.js | 118 ++++++++++++++++++ ...arty-feature-preferences-to-preferences.js | 54 ++++++++ .../test/convert-edit-post-panels.js | 0 ...ove-feature-preferences-to-preferences.js} | 16 +-- ...e-individual-preference-to-preferences.js} | 14 +-- ...e-interface-enable-items-to-preferences.js | 118 ++++++++++++++++++ ...arty-feature-preferences-to-preferences.js | 111 ++++++++++++++++ 11 files changed, 419 insertions(+), 18 deletions(-) rename packages/database-persistence-layer/src/migrations/{ => from-legacy-local-storage-persistence}/convert-edit-post-panels.js (100%) rename packages/database-persistence-layer/src/migrations/{convert-from-local-storage.js => from-legacy-local-storage-persistence/index.js} (83%) rename packages/database-persistence-layer/src/migrations/{migrate-feature-preferences-to-preferences-store.js => from-legacy-local-storage-persistence/move-feature-preferences-to-preferences.js} (97%) rename packages/database-persistence-layer/src/migrations/{migrate-individual-preference-to-preferences-store.js => from-legacy-local-storage-persistence/move-individual-preference-to-preferences.js} (96%) create mode 100644 packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-interface-enable-items-to-preferences.js create mode 100644 packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-third-party-feature-preferences-to-preferences.js rename packages/database-persistence-layer/src/migrations/{ => from-legacy-local-storage-persistence}/test/convert-edit-post-panels.js (100%) rename packages/database-persistence-layer/src/migrations/{test/migrate-feature-preferences-to-preferences-store.js => from-legacy-local-storage-persistence/test/move-feature-preferences-to-preferences.js} (88%) rename packages/database-persistence-layer/src/migrations/{test/migrate-individual-preference-to-preferences-store.js => from-legacy-local-storage-persistence/test/move-individual-preference-to-preferences.js} (85%) create mode 100644 packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-interface-enable-items-to-preferences.js create mode 100644 packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-third-party-feature-preferences-to-preferences.js diff --git a/packages/database-persistence-layer/src/migrations/convert-edit-post-panels.js b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/convert-edit-post-panels.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/convert-edit-post-panels.js rename to packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/convert-edit-post-panels.js diff --git a/packages/database-persistence-layer/src/migrations/convert-from-local-storage.js b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/index.js similarity index 83% rename from packages/database-persistence-layer/src/migrations/convert-from-local-storage.js rename to packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/index.js index 04042ff9053baf..11faa442523bf5 100644 --- a/packages/database-persistence-layer/src/migrations/convert-from-local-storage.js +++ b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/index.js @@ -5,7 +5,7 @@ * * @return {Object | undefined} The converted data or `undefined` if there was nothing to convert. */ -export default function convertFromLocalStorage( userId ) { +export default function migrateFromLegacyLocalStoragePersistence( userId ) { const key = `WP_DATA_USER_${ userId }`; const unparsedData = window.localStorage.getItem( key ); const data = JSON.parse( unparsedData ); diff --git a/packages/database-persistence-layer/src/migrations/migrate-feature-preferences-to-preferences-store.js b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-feature-preferences-to-preferences.js similarity index 97% rename from packages/database-persistence-layer/src/migrations/migrate-feature-preferences-to-preferences-store.js rename to packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-feature-preferences-to-preferences.js index 60d6c5c81f3282..1715579ba4a4f9 100644 --- a/packages/database-persistence-layer/src/migrations/migrate-feature-preferences-to-preferences-store.js +++ b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-feature-preferences-to-preferences.js @@ -8,7 +8,7 @@ * package. * @return {Object} The migrated state */ -export default function migrateFeaturePreferencesToPreferencesStore( +export default function moveFeaturePreferencesToPreferences( state, sourceStoreName ) { diff --git a/packages/database-persistence-layer/src/migrations/migrate-individual-preference-to-preferences-store.js b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-individual-preference-to-preferences.js similarity index 96% rename from packages/database-persistence-layer/src/migrations/migrate-individual-preference-to-preferences-store.js rename to packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-individual-preference-to-preferences.js index 350d22c7d91c15..044f10754be734 100644 --- a/packages/database-persistence-layer/src/migrations/migrate-individual-preference-to-preferences-store.js +++ b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-individual-preference-to-preferences.js @@ -10,7 +10,7 @@ const identity = ( arg ) => arg; * @param {string} key The key in the preferences object to migrate. * @param {?Function} convert A function that converts preferences from one format to another. */ -export default function migrateIndividualPreferenceToPreferencesStore( +export default function moveIndividualPreferenceToPreferences( state, { from: sourceStoreName, scope }, key, diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-interface-enable-items-to-preferences.js b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-interface-enable-items-to-preferences.js new file mode 100644 index 00000000000000..d69bc66e8f878a --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-interface-enable-items-to-preferences.js @@ -0,0 +1,118 @@ +/** + * Migrates interface 'enableItems' data to the preferences store. + * + * The interface package stores this data in this format: + * ```js + * { + * enableItems: { + * singleEnableItems: { + * complementaryArea: { + * 'core/edit-post': 'edit-post/document', + * 'core/edit-site': 'edit-site/global-styles', + * } + * }, + * multipleEnableItems: { + * pinnedItems: { + * 'core/edit-post': { + * 'plugin-1': true, + * }, + * 'core/edit-site': { + * 'plugin-2': true, + * }, + * }, + * } + * } + * } + * ``` + * and it should be converted it to: + * ```js + * { + * 'core/edit-post': { + * complementaryArea: 'edit-post/document', + * pinnedItems: { + * 'plugin-1': true, + * }, + * }, + * 'core/edit-site': { + * complementaryArea: 'edit-site/global-styles', + * pinnedItems: { + * 'plugin-2': true, + * }, + * }, + * } + * ``` + * + * @param {Object} state The local storage state. + */ +export default function moveInterfaceEnableItemsToPreferences( state ) { + const interfaceStoreName = 'core/interface'; + const preferencesStoreName = 'core/preferences'; + const sourceEnableItems = state[ interfaceStoreName ]?.enableItems; + + // There's nothing to migrate, exit early. + if ( ! sourceEnableItems ) { + return; + } + + const allPreferences = state[ preferencesStoreName ]?.preferences ?? {}; + + // First convert complementaryAreas into the right format. + // Use the existing preferences as the accumulator so that the data is + // merged. + const sourceComplementaryAreas = + sourceEnableItems?.singleEnableItems?.complementaryArea ?? {}; + + const preferencesWithConvertedComplementaryAreas = Object.keys( + sourceComplementaryAreas + ).reduce( ( accumulator, scope ) => { + const data = sourceComplementaryAreas[ scope ]; + + // Don't overwrite any existing data in the preferences store. + if ( accumulator[ scope ]?.complementaryArea ) { + return accumulator; + } + + return { + ...accumulator, + [ scope ]: { + ...accumulator[ scope ], + complementaryArea: data, + }, + }; + }, allPreferences ); + + // Next feed the converted complementary areas back into a reducer that + // converts the pinned items, resulting in the fully migrated data. + const sourcePinnedItems = + sourceEnableItems?.multipleEnableItems?.pinnedItems ?? {}; + const allConvertedData = Object.keys( sourcePinnedItems ).reduce( + ( accumulator, scope ) => { + const data = sourcePinnedItems[ scope ]; + // Don't overwrite any existing data in the preferences store. + if ( accumulator[ scope ]?.pinnedItems ) { + return accumulator; + } + + return { + ...accumulator, + [ scope ]: { + ...accumulator[ scope ], + pinnedItems: data, + }, + }; + }, + preferencesWithConvertedComplementaryAreas + ); + + const otherInterfaceItems = state[ interfaceStoreName ]; + + return { + [ preferencesStoreName ]: { + preferences: allConvertedData, + }, + [ interfaceStoreName ]: { + ...otherInterfaceItems, + enableItems: undefined, + }, + }; +} diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-third-party-feature-preferences-to-preferences.js b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-third-party-feature-preferences-to-preferences.js new file mode 100644 index 00000000000000..bc883c263df59d --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-third-party-feature-preferences-to-preferences.js @@ -0,0 +1,54 @@ +export default function moveThirdPartyFeaturePreferencesToPreferences( state ) { + const interfaceStoreName = 'core/interface'; + const preferencesStoreName = 'core/preferences'; + + const interfaceScopes = state[ interfaceStoreName ]?.preferences?.features; + const interfaceScopeKeys = Object.keys( interfaceScopes ); + + if ( ! interfaceScopeKeys?.length ) { + return state; + } + + return interfaceScopeKeys.reduce( function ( convertedState, scope ) { + if ( scope.startsWith( 'core' ) ) { + return convertedState; + } + + const featuresToMigrate = interfaceScopes[ scope ]; + if ( ! featuresToMigrate ) { + return convertedState; + } + + const existingMigratedData = + convertedState[ preferencesStoreName ]?.preferences?.[ scope ]; + + if ( existingMigratedData ) { + return convertedState; + } + + const otherPreferencesScopes = + convertedState[ preferencesStoreName ]?.preferences; + const otherInterfaceState = convertedState[ interfaceStoreName ]; + const otherInterfaceScopes = + convertedState[ interfaceStoreName ]?.preferences?.features; + + return { + ...convertedState, + [ preferencesStoreName ]: { + preferences: { + ...otherPreferencesScopes, + [ scope ]: featuresToMigrate, + }, + }, + [ interfaceStoreName ]: { + ...otherInterfaceState, + preferences: { + features: { + ...otherInterfaceScopes, + [ scope ]: undefined, + }, + }, + }, + }; + }, state ); +} diff --git a/packages/database-persistence-layer/src/migrations/test/convert-edit-post-panels.js b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/convert-edit-post-panels.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/test/convert-edit-post-panels.js rename to packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/convert-edit-post-panels.js diff --git a/packages/database-persistence-layer/src/migrations/test/migrate-feature-preferences-to-preferences-store.js b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-feature-preferences-to-preferences.js similarity index 88% rename from packages/database-persistence-layer/src/migrations/test/migrate-feature-preferences-to-preferences-store.js rename to packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-feature-preferences-to-preferences.js index 872134e6df24d9..f2c9ce4c853dca 100644 --- a/packages/database-persistence-layer/src/migrations/test/migrate-feature-preferences-to-preferences-store.js +++ b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-feature-preferences-to-preferences.js @@ -1,9 +1,9 @@ /** * Internal dependencies */ -import migrateFeaturePreferencesToPreferencesStore from '../migrate-feature-preferences-to-preferences-store'; +import moveFeaturePreferencesToPreferences from '../move-feature-preferences-to-preferences'; -describe( 'migrateFeaturePreferencesToPreferencesStore', () => { +describe( 'moveFeaturePreferencesToPreferences', () => { it( 'migrates multiple preferences from persisted source stores to preferences without overwriting data', () => { const state = { 'core/test-a': { @@ -26,12 +26,12 @@ describe( 'migrateFeaturePreferencesToPreferencesStore', () => { }, }; - let convertedState = migrateFeaturePreferencesToPreferencesStore( + let convertedState = moveFeaturePreferencesToPreferences( state, 'core/test-a' ); - convertedState = migrateFeaturePreferencesToPreferencesStore( + convertedState = moveFeaturePreferencesToPreferences( convertedState, 'core/test-b' ); @@ -105,12 +105,12 @@ describe( 'migrateFeaturePreferencesToPreferencesStore', () => { }, }; - let convertedState = migrateFeaturePreferencesToPreferencesStore( + let convertedState = moveFeaturePreferencesToPreferences( state, 'core/test-a' ); - convertedState = migrateFeaturePreferencesToPreferencesStore( + convertedState = moveFeaturePreferencesToPreferences( convertedState, 'core/test-b' ); @@ -182,7 +182,7 @@ describe( 'migrateFeaturePreferencesToPreferencesStore', () => { }, }; - const convertedState = migrateFeaturePreferencesToPreferencesStore( + const convertedState = moveFeaturePreferencesToPreferences( state, 'core/test-a' ); @@ -240,7 +240,7 @@ describe( 'migrateFeaturePreferencesToPreferencesStore', () => { }, }; - const convertedState = migrateFeaturePreferencesToPreferencesStore( + const convertedState = moveFeaturePreferencesToPreferences( state, 'core/test-a' ); diff --git a/packages/database-persistence-layer/src/migrations/test/migrate-individual-preference-to-preferences-store.js b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-individual-preference-to-preferences.js similarity index 85% rename from packages/database-persistence-layer/src/migrations/test/migrate-individual-preference-to-preferences-store.js rename to packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-individual-preference-to-preferences.js index 08b4381e5334e2..7c49bb0ce13a56 100644 --- a/packages/database-persistence-layer/src/migrations/test/migrate-individual-preference-to-preferences-store.js +++ b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-individual-preference-to-preferences.js @@ -1,9 +1,9 @@ /** * Internal dependencies */ -import migrateIndividualPreferenceToPreferencesStore from '../migrate-individual-preference-to-preferences-store'; +import moveIndividualPreferenceToPreferences from '../move-individual-preference-to-preferences'; -describe( 'migrateIndividualPreferenceToPreferencesStore', () => { +describe( 'moveIndividualPreferenceToPreferences', () => { it( 'migrates an individual preference from the source to the preferences store', () => { const initialState = { 'core/test': { @@ -13,7 +13,7 @@ describe( 'migrateIndividualPreferenceToPreferencesStore', () => { }, }; - const convertedData = migrateIndividualPreferenceToPreferencesStore( + const convertedData = moveIndividualPreferenceToPreferences( initialState, { from: 'core/test', scope: 'core/test' }, 'myPreference' @@ -58,7 +58,7 @@ describe( 'migrateIndividualPreferenceToPreferencesStore', () => { }, }; - const convertedData = migrateIndividualPreferenceToPreferencesStore( + const convertedData = moveIndividualPreferenceToPreferences( initialState, { from: 'core/test', scope: 'core/test' }, 'myPreference' @@ -97,7 +97,7 @@ describe( 'migrateIndividualPreferenceToPreferencesStore', () => { }, }; - const convertedData = migrateIndividualPreferenceToPreferencesStore( + const convertedData = moveIndividualPreferenceToPreferences( initialState, { from: 'core/source', scope: 'core/destination' }, 'myPreference' @@ -135,7 +135,7 @@ describe( 'migrateIndividualPreferenceToPreferencesStore', () => { }, }; - const convertedData = migrateIndividualPreferenceToPreferencesStore( + const convertedData = moveIndividualPreferenceToPreferences( initialState, { from: 'core/test', scope: 'core/test' }, 'myPreference' @@ -166,7 +166,7 @@ describe( 'migrateIndividualPreferenceToPreferencesStore', () => { }, }; - const convertedData = migrateIndividualPreferenceToPreferencesStore( + const convertedData = moveIndividualPreferenceToPreferences( initialState, { from: 'core/test', scope: 'core/test' }, 'myFalsePreference' diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-interface-enable-items-to-preferences.js b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-interface-enable-items-to-preferences.js new file mode 100644 index 00000000000000..4a812e5b30c0f5 --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-interface-enable-items-to-preferences.js @@ -0,0 +1,118 @@ +/** + * Internal dependencies + */ +import moveInterfaceEnableItemsToPreferences from '../move-interface-enable-items-to-preferences'; + +describe( 'moveInterfaceEnableItemsToPreferences', () => { + it( 'migrates enableItems to the preferences store', () => { + const state = { + 'core/interface': { + enableItems: { + singleEnableItems: { + complementaryArea: { + 'core/edit-post': 'edit-post/document', + 'core/edit-site': 'edit-site/global-styles', + }, + }, + multipleEnableItems: { + pinnedItems: { + 'core/edit-post': { + 'plugin-1': true, + }, + 'core/edit-site': { + 'plugin-2': true, + }, + }, + }, + }, + }, + }; + + const convertedState = moveInterfaceEnableItemsToPreferences( state ); + + expect( convertedState ).toEqual( { + 'core/preferences': { + preferences: { + 'core/edit-post': { + complementaryArea: 'edit-post/document', + pinnedItems: { + 'plugin-1': true, + }, + }, + 'core/edit-site': { + complementaryArea: 'edit-site/global-styles', + pinnedItems: { + 'plugin-2': true, + }, + }, + }, + }, + 'core/interface': { + enableItems: undefined, + }, + } ); + } ); + + it( 'merges pinnedItems and complementaryAreas with existing preferences store data', () => { + const state = { + 'core/interface': { + enableItems: { + singleEnableItems: { + complementaryArea: { + 'core/edit-post': 'edit-post/document', + 'core/edit-site': 'edit-site/global-styles', + }, + }, + multipleEnableItems: { + pinnedItems: { + 'core/edit-post': { + 'plugin-1': true, + }, + 'core/edit-site': { + 'plugin-2': true, + }, + }, + }, + }, + }, + 'core/preferences': { + preferences: { + 'core/edit-post': { + preferenceA: 1, + preferenceB: 2, + }, + 'core/edit-site': { + preferenceC: true, + }, + }, + }, + }; + + const convertedState = moveInterfaceEnableItemsToPreferences( state ); + + expect( convertedState ).toEqual( { + 'core/preferences': { + preferences: { + 'core/edit-post': { + preferenceA: 1, + preferenceB: 2, + complementaryArea: 'edit-post/document', + pinnedItems: { + 'plugin-1': true, + }, + }, + 'core/edit-site': { + preferenceC: true, + complementaryArea: 'edit-site/global-styles', + pinnedItems: { + 'plugin-2': true, + }, + }, + }, + }, + 'core/interface': { + enableItems: undefined, + }, + } ); + } ); +} ); diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-third-party-feature-preferences-to-preferences.js b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-third-party-feature-preferences-to-preferences.js new file mode 100644 index 00000000000000..a512871713f455 --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-third-party-feature-preferences-to-preferences.js @@ -0,0 +1,111 @@ +/** + * Internal dependencies + */ +import moveThirdPartyFeaturePreferencesToPreferences from '../move-third-party-feature-preferences-to-preferences'; + +describe( 'moveThirdPartyFeaturePreferencesToPreferences', () => { + it( 'migrates multiple scopes from the interface package to the preferences package', () => { + const state = { + 'core/interface': { + otherData: { + test: 1, + }, + preferences: { + features: { + 'plugin-a': { + featureA: true, + featureB: false, + featureC: true, + }, + 'plugin-b': { + featureD: true, + featureE: false, + featureF: true, + }, + }, + }, + }, + }; + + const convertedState = moveThirdPartyFeaturePreferencesToPreferences( + state + ); + + expect( convertedState ).toEqual( { + 'core/preferences': { + preferences: { + 'plugin-a': { + featureA: true, + featureB: false, + featureC: true, + }, + 'plugin-b': { + featureD: true, + featureE: false, + featureF: true, + }, + }, + }, + 'core/interface': { + otherData: { + test: 1, + }, + preferences: { + features: { + 'plugin-a': undefined, + 'plugin-b': undefined, + }, + }, + }, + } ); + } ); + + it( 'ignores any core scopes', () => { + const state = { + 'core/interface': { + preferences: { + features: { + 'plugin-a': { + featureA: true, + featureB: false, + featureC: true, + }, + 'core/edit-post': { + featureD: true, + featureE: false, + featureF: true, + }, + }, + }, + }, + }; + + const convertedState = moveThirdPartyFeaturePreferencesToPreferences( + state + ); + + expect( convertedState ).toEqual( { + 'core/preferences': { + preferences: { + 'plugin-a': { + featureA: true, + featureB: false, + featureC: true, + }, + }, + }, + 'core/interface': { + preferences: { + features: { + 'plugin-a': undefined, + 'core/edit-post': { + featureD: true, + featureE: false, + featureF: true, + }, + }, + }, + }, + } ); + } ); +} ); From 5fb2a6f9388bc26c4841409525ce062014899abc Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 7 Apr 2022 15:38:46 +0800 Subject: [PATCH 44/74] Tidy up migration code --- lib/experimental/persisted-preferences.php | 6 +- .../database-persistence-layer/src/index.js | 2 +- .../index.js | 13 --- .../legacy-local-storage-data/README.md | 42 +++++++++ .../convert-edit-post-panels.js | 4 +- .../legacy-local-storage-data/index.js | 93 +++++++++++++++++++ .../move-feature-preferences.js} | 54 ++++++++++- .../move-individual-preference.js} | 41 ++++++-- .../move-interface-enable-items.js} | 1 + .../move-third-party-feature-preferences.js} | 41 ++++++++ .../test/convert-edit-post-panels.js | 0 .../test/move-feature-preferences.js} | 28 ++---- .../test/move-individual-preference.js} | 24 ++--- .../test/move-interface-enable-items.js} | 8 +- .../move-third-party-feature-preferences.js} | 12 +-- 15 files changed, 293 insertions(+), 76 deletions(-) delete mode 100644 packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/index.js create mode 100644 packages/database-persistence-layer/src/migrations/legacy-local-storage-data/README.md rename packages/database-persistence-layer/src/migrations/{from-legacy-local-storage-persistence => legacy-local-storage-data}/convert-edit-post-panels.js (91%) create mode 100644 packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js rename packages/database-persistence-layer/src/migrations/{from-legacy-local-storage-persistence/move-feature-preferences-to-preferences.js => legacy-local-storage-data/move-feature-preferences.js} (65%) rename packages/database-persistence-layer/src/migrations/{from-legacy-local-storage-persistence/move-individual-preference-to-preferences.js => legacy-local-storage-data/move-individual-preference.js} (53%) rename packages/database-persistence-layer/src/migrations/{from-legacy-local-storage-persistence/move-interface-enable-items-to-preferences.js => legacy-local-storage-data/move-interface-enable-items.js} (99%) rename packages/database-persistence-layer/src/migrations/{from-legacy-local-storage-persistence/move-third-party-feature-preferences-to-preferences.js => legacy-local-storage-data/move-third-party-feature-preferences.js} (57%) rename packages/database-persistence-layer/src/migrations/{from-legacy-local-storage-persistence => legacy-local-storage-data}/test/convert-edit-post-panels.js (100%) rename packages/database-persistence-layer/src/migrations/{from-legacy-local-storage-persistence/test/move-feature-preferences-to-preferences.js => legacy-local-storage-data/test/move-feature-preferences.js} (86%) rename packages/database-persistence-layer/src/migrations/{from-legacy-local-storage-persistence/test/move-individual-preference-to-preferences.js => legacy-local-storage-data/test/move-individual-preference.js} (80%) rename packages/database-persistence-layer/src/migrations/{from-legacy-local-storage-persistence/test/move-interface-enable-items-to-preferences.js => legacy-local-storage-data/test/move-interface-enable-items.js} (87%) rename packages/database-persistence-layer/src/migrations/{from-legacy-local-storage-persistence/test/move-third-party-feature-preferences-to-preferences.js => legacy-local-storage-data/test/move-third-party-feature-preferences.js} (82%) diff --git a/lib/experimental/persisted-preferences.php b/lib/experimental/persisted-preferences.php index d55f0b9cd38e76..3d5d2d9c3d9d32 100644 --- a/lib/experimental/persisted-preferences.php +++ b/lib/experimental/persisted-preferences.php @@ -55,9 +55,9 @@ function gutenberg_configure_persisted_preferences() { } else if ( localData ) { preloadedData = localData; } else { - // Check if there is data in the old format. - const { __unstableConvertFromLocalStorage } = wp.databasePersistenceLayer; - preloadedData = __unstableConvertFromLocalStorage( userId ); + // Check if there is data in the legacy format from the old persistence system. + const { __unstableConvertLegacyLocalStorageData } = wp.databasePersistenceLayer; + preloadedData = __unstableConvertLegacyLocalStorageData( userId ); } const { create } = wp.databasePersistenceLayer; diff --git a/packages/database-persistence-layer/src/index.js b/packages/database-persistence-layer/src/index.js index 610c0ce2625788..18da7f9b56b9ab 100644 --- a/packages/database-persistence-layer/src/index.js +++ b/packages/database-persistence-layer/src/index.js @@ -1,2 +1,2 @@ export { default as create } from './create'; -export { default as __unstableConvertFromLocalStorage } from './migrations/convert-from-local-storage'; +export { default as __unstableConvertLegacyLocalStorageData } from './migrations/legacy-local-storage-data'; diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/index.js b/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/index.js deleted file mode 100644 index 11faa442523bf5..00000000000000 --- a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/index.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Converts data from the old `@wordpress/data` package format. - * - * @param {number | string} userId - * - * @return {Object | undefined} The converted data or `undefined` if there was nothing to convert. - */ -export default function migrateFromLegacyLocalStoragePersistence( userId ) { - const key = `WP_DATA_USER_${ userId }`; - const unparsedData = window.localStorage.getItem( key ); - const data = JSON.parse( unparsedData ); - return data?.[ 'core/preferences' ]; -} diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/README.md b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/README.md new file mode 100644 index 00000000000000..29187e4f0f33e8 --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/README.md @@ -0,0 +1,42 @@ +# Legacy local storage migrations + +This folder contains all the migration code for converting from the old `data` package persistence system. + +## History + +Previously, some packages could configure a store to persist particular parts of state. In this example, the post editor is configured to persist the state for its `preferences` reducer to local storage: +```js +registerStore( + 'core/edit-post', { + selectors, + actions, + reducer, + persist: [ 'preferences' ], + }, +); +``` + +This would result in local storage data being saved in this format: +```json +{ + "core/edit-post": { + "preferences": { + // ... preferences state from the post editor. + } + }, + // ... other persisted state from other editors. +} +``` + +And when an editor was reloaded, this would become the initial store state. + +The preferences package was later introduced, and this became a centralized place for managing and persisting preferences for other packages. The job of these migration functions is to migrate data from the old persistence system to the new format for the preferences store: +```json +{ + "preferences": { + "core/edit-post": { + // ... preferences for the post editor. + } + // ... preferences for other editors + } +} diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/convert-edit-post-panels.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/convert-edit-post-panels.js similarity index 91% rename from packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/convert-edit-post-panels.js rename to packages/database-persistence-layer/src/migrations/legacy-local-storage-data/convert-edit-post-panels.js index 09362e3f369d31..8a534b02bbc2db 100644 --- a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/convert-edit-post-panels.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/convert-edit-post-panels.js @@ -1,5 +1,5 @@ /** - * Convert from: + * Convert the post editor's panels state from: * ``` * { * panels: { @@ -15,7 +15,7 @@ * } * ``` * - * to: + * to a new, more concise data structure: * { * inactivePanels: [ * 'permalinks', diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js new file mode 100644 index 00000000000000..0367cd3af9530b --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js @@ -0,0 +1,93 @@ +/** + * Internal dependencies + */ +import moveFeaturePreferences from './move-feature-preferences'; +import moveThirdPartyFeaturePreferences from './move-third-party-feature-preferences'; +import moveIndividualPreference from './move-individual-preference'; +import moveInterfaceEnableItems from './move-interface-enable-items'; +import convertEditPostPanels from './convert-edit-post-panels'; + +/** + * Gets the legacy local storage data for a given user. + * + * @param {string} userId The user id. + * + * @return {Object | null} The local storage data. + */ +function getLegacyData( userId ) { + const key = `WP_DATA_USER_${ userId }`; + const unparsedData = window.localStorage.getItem( key ); + return JSON.parse( unparsedData ); +} + +/** + * Converts data from the old `@wordpress/data` package format. + * + * @param {Object | null | undefined} data The legacy data in its original format. + * + * @return {Object | undefined} The converted data or `undefined` if there was + * nothing to convert. + */ +export function convertLegacyData( data ) { + if ( ! data ) { + return; + } + + // Move boolean feature preferences from each editor into the + // preferences store data structure. + data = moveFeaturePreferences( data, 'core/edit-widgets' ); + data = moveFeaturePreferences( data, 'core/customize-widgets' ); + data = moveFeaturePreferences( data, 'core/edit-post' ); + data = moveFeaturePreferences( data, 'core/edit-site' ); + + // Move third party boolean feature preferences from the interface package + // to the preferences store data structure. + data = moveThirdPartyFeaturePreferences( data ); + + // Move and convert the interface store's `enableItems` data into the + // preferences data structure. + data = moveInterfaceEnableItems( data ); + + // Move individual ad-hoc preferences from various packages into the + // preferences store data structure. + data = moveIndividualPreference( + data, + { from: 'core/edit-post', to: 'core/edit-post' }, + 'hiddenBlockTypes' + ); + data = moveIndividualPreference( + data, + { from: 'core/edit-post', to: 'core/edit-post' }, + 'editorMode' + ); + data = moveIndividualPreference( + data, + { from: 'core/edit-post', to: 'core/edit-post' }, + 'preferredStyleVariations' + ); + data = moveIndividualPreference( + data, + { from: 'core/edit-post', to: 'core/edit-post' }, + 'panels', + convertEditPostPanels + ); + data = moveIndividualPreference( + data, + { from: 'core/editor', to: 'core/edit-post' }, + 'isPublishSidebarEnabled' + ); + data = moveIndividualPreference( + data, + { from: 'core/edit-site', to: 'core/edit-site' }, + 'editorMode' + ); + + // The previous legacy persistence system contained preferences for + // multiple stores + return data?.[ 'core/preferences' ]; +} + +export default function convertLegacyLocalStorageData( userId ) { + const data = getLegacyData( userId ); + return convertLegacyData( data ); +} diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-feature-preferences-to-preferences.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-feature-preferences.js similarity index 65% rename from packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-feature-preferences-to-preferences.js rename to packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-feature-preferences.js index 1715579ba4a4f9..e1b4b51002fdca 100644 --- a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-feature-preferences-to-preferences.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-feature-preferences.js @@ -1,6 +1,53 @@ /** * Move the 'features' object in local storage from the sourceStoreName to the - * preferences store. + * preferences store data structure. + * + * Previously, editors used a data structure like this for feature preferences: + * ```js + * { + * 'core/edit-post': { + * preferences: { + * features; { + * topToolbar: true, + * // ... other boolean 'feature' preferences + * }, + * }, + * }, + * } + * ``` + * + * And for a while these feature preferences lived in the interface package: + * ```js + * { + * 'core/interface': { + * preferences: { + * features: { + * 'core/edit-post': { + * topToolbar: true + * } + * } + * } + * } + * } + * ``` + * + * In the preferences store, 'features' aren't considered special, they're + * merged to the root level of the scope along with other preferences: + * ```js + * { + * 'core/preferences': { + * preferences: { + * 'core/edit-post': { + * topToolbar: true, + * // ... any other preferences. + * } + * } + * } + * } + * ``` + * + * This function handles moving from either the source store or the interface + * store to the preferences data structure. * * @param {Object} state The state before migration. * @param {string} sourceStoreName The name of the store that has persisted @@ -8,10 +55,7 @@ * package. * @return {Object} The migrated state */ -export default function moveFeaturePreferencesToPreferences( - state, - sourceStoreName -) { +export default function moveFeaturePreferences( state, sourceStoreName ) { const preferencesStoreName = 'core/preferences'; const interfaceStoreName = 'core/interface'; diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-individual-preference-to-preferences.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-individual-preference.js similarity index 53% rename from packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-individual-preference-to-preferences.js rename to packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-individual-preference.js index 044f10754be734..e543de5b9e1e03 100644 --- a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-individual-preference-to-preferences.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-individual-preference.js @@ -1,18 +1,43 @@ const identity = ( arg ) => arg; /** - * Migrates an individual item inside the `preferences` object for a store. + * Migrates an individual item inside the `preferences` object for a package's store. * - * @param {Object} state The original state. - * @param {Object} migrate An options object that contains details of the migration. - * @param {string} migrate.from The name of the store to migrate from. - * @param {string} migrate.scope The scope in the preferences store to migrate to. - * @param {string} key The key in the preferences object to migrate. - * @param {?Function} convert A function that converts preferences from one format to another. + * Previously, some packages had individual 'preferences' of any data type, and many used + * complex nested data structures. For example: + * ```js + * { + * 'core/edit-post': { + * preferences: { + * panels: { + * publish: { + * opened: true, + * enabled: true, + * } + * }, + * // ...other preferences. + * }, + * }, + * } + * + * This function supports moving an individual preference like 'panels' above into the + * preferences package data structure. + * + * It supports moving a preference to a particular scope in the preferences store and + * optionally converting the data using a `convert` function. + * + * ``` + * + * @param {Object} state The original state. + * @param {Object} migrate An options object that contains details of the migration. + * @param {string} migrate.from The name of the store to migrate from. + * @param {string} migrate.to The scope in the preferences store to migrate to. + * @param {string} key The key in the preferences object to migrate. + * @param {?Function} convert A function that converts preferences from one format to another. */ export default function moveIndividualPreferenceToPreferences( state, - { from: sourceStoreName, scope }, + { from: sourceStoreName, to: scope }, key, convert = identity ) { diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-interface-enable-items-to-preferences.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-interface-enable-items.js similarity index 99% rename from packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-interface-enable-items-to-preferences.js rename to packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-interface-enable-items.js index d69bc66e8f878a..522618645f02f4 100644 --- a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-interface-enable-items-to-preferences.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-interface-enable-items.js @@ -24,6 +24,7 @@ * } * } * ``` + * * and it should be converted it to: * ```js * { diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-third-party-feature-preferences-to-preferences.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js similarity index 57% rename from packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-third-party-feature-preferences-to-preferences.js rename to packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js index bc883c263df59d..01f0d2993d974e 100644 --- a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/move-third-party-feature-preferences-to-preferences.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js @@ -1,3 +1,44 @@ +/** + * The interface package previously had a public API that could be used by + * plugins to set persisted boolean 'feature' preferences. + * + * While usage was likely non-existent or very small, this function ensures + * those are migrated to the preferences data structure. The interface + * package's APIs have now been deprecated and use the preferences store. + * + * This will convert data that looks like this: + * ```js + * { + * 'core/interface': { + * preferences: { + * features: { + * 'my-plugin': { + * myPluginFeature: true + * } + * } + * } + * } + * } + * ``` + * + * To this: + * ```js + * * { + * 'core/preferences': { + * preferences: { + * 'my-plugin': { + * myPluginFeature: true + * } + * } + * } + * } + * ``` + * + * @param {Object} state The local storage state + * + * @return {Object} The state with third party preferences moved to the + * preferences data structure. + */ export default function moveThirdPartyFeaturePreferencesToPreferences( state ) { const interfaceStoreName = 'core/interface'; const preferencesStoreName = 'core/preferences'; diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/convert-edit-post-panels.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/convert-edit-post-panels.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/convert-edit-post-panels.js rename to packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/convert-edit-post-panels.js diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-feature-preferences-to-preferences.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-feature-preferences.js similarity index 86% rename from packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-feature-preferences-to-preferences.js rename to packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-feature-preferences.js index f2c9ce4c853dca..120f8526d749a1 100644 --- a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-feature-preferences-to-preferences.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-feature-preferences.js @@ -1,9 +1,9 @@ /** * Internal dependencies */ -import moveFeaturePreferencesToPreferences from '../move-feature-preferences-to-preferences'; +import moveFeaturePreferences from '../move-feature-preferences'; -describe( 'moveFeaturePreferencesToPreferences', () => { +describe( 'moveFeaturePreferences', () => { it( 'migrates multiple preferences from persisted source stores to preferences without overwriting data', () => { const state = { 'core/test-a': { @@ -26,12 +26,9 @@ describe( 'moveFeaturePreferencesToPreferences', () => { }, }; - let convertedState = moveFeaturePreferencesToPreferences( - state, - 'core/test-a' - ); + let convertedState = moveFeaturePreferences( state, 'core/test-a' ); - convertedState = moveFeaturePreferencesToPreferences( + convertedState = moveFeaturePreferences( convertedState, 'core/test-b' ); @@ -105,12 +102,9 @@ describe( 'moveFeaturePreferencesToPreferences', () => { }, }; - let convertedState = moveFeaturePreferencesToPreferences( - state, - 'core/test-a' - ); + let convertedState = moveFeaturePreferences( state, 'core/test-a' ); - convertedState = moveFeaturePreferencesToPreferences( + convertedState = moveFeaturePreferences( convertedState, 'core/test-b' ); @@ -182,10 +176,7 @@ describe( 'moveFeaturePreferencesToPreferences', () => { }, }; - const convertedState = moveFeaturePreferencesToPreferences( - state, - 'core/test-a' - ); + const convertedState = moveFeaturePreferences( state, 'core/test-a' ); expect( convertedState ).toEqual( { 'core/preferences': { @@ -240,10 +231,7 @@ describe( 'moveFeaturePreferencesToPreferences', () => { }, }; - const convertedState = moveFeaturePreferencesToPreferences( - state, - 'core/test-a' - ); + const convertedState = moveFeaturePreferences( state, 'core/test-a' ); expect( convertedState ).toEqual( { 'core/preferences': { diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-individual-preference-to-preferences.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-individual-preference.js similarity index 80% rename from packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-individual-preference-to-preferences.js rename to packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-individual-preference.js index 7c49bb0ce13a56..e043c55d7697ef 100644 --- a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-individual-preference-to-preferences.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-individual-preference.js @@ -1,9 +1,9 @@ /** * Internal dependencies */ -import moveIndividualPreferenceToPreferences from '../move-individual-preference-to-preferences'; +import moveIndividualPreference from '../move-individual-preference'; -describe( 'moveIndividualPreferenceToPreferences', () => { +describe( 'moveIndividualPreference', () => { it( 'migrates an individual preference from the source to the preferences store', () => { const initialState = { 'core/test': { @@ -13,9 +13,9 @@ describe( 'moveIndividualPreferenceToPreferences', () => { }, }; - const convertedData = moveIndividualPreferenceToPreferences( + const convertedData = moveIndividualPreference( initialState, - { from: 'core/test', scope: 'core/test' }, + { from: 'core/test', to: 'core/test' }, 'myPreference' ); @@ -58,9 +58,9 @@ describe( 'moveIndividualPreferenceToPreferences', () => { }, }; - const convertedData = moveIndividualPreferenceToPreferences( + const convertedData = moveIndividualPreference( initialState, - { from: 'core/test', scope: 'core/test' }, + { from: 'core/test', to: 'core/test' }, 'myPreference' ); @@ -97,9 +97,9 @@ describe( 'moveIndividualPreferenceToPreferences', () => { }, }; - const convertedData = moveIndividualPreferenceToPreferences( + const convertedData = moveIndividualPreference( initialState, - { from: 'core/source', scope: 'core/destination' }, + { from: 'core/source', to: 'core/destination' }, 'myPreference' ); @@ -135,9 +135,9 @@ describe( 'moveIndividualPreferenceToPreferences', () => { }, }; - const convertedData = moveIndividualPreferenceToPreferences( + const convertedData = moveIndividualPreference( initialState, - { from: 'core/test', scope: 'core/test' }, + { from: 'core/test', to: 'core/test' }, 'myPreference' ); @@ -166,9 +166,9 @@ describe( 'moveIndividualPreferenceToPreferences', () => { }, }; - const convertedData = moveIndividualPreferenceToPreferences( + const convertedData = moveIndividualPreference( initialState, - { from: 'core/test', scope: 'core/test' }, + { from: 'core/test', to: 'core/test' }, 'myFalsePreference' ); diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-interface-enable-items-to-preferences.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-interface-enable-items.js similarity index 87% rename from packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-interface-enable-items-to-preferences.js rename to packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-interface-enable-items.js index 4a812e5b30c0f5..bbfa89970f6ecb 100644 --- a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-interface-enable-items-to-preferences.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-interface-enable-items.js @@ -1,9 +1,9 @@ /** * Internal dependencies */ -import moveInterfaceEnableItemsToPreferences from '../move-interface-enable-items-to-preferences'; +import moveInterfaceEnableItems from '../move-interface-enable-items'; -describe( 'moveInterfaceEnableItemsToPreferences', () => { +describe( 'moveInterfaceEnableItems', () => { it( 'migrates enableItems to the preferences store', () => { const state = { 'core/interface': { @@ -28,7 +28,7 @@ describe( 'moveInterfaceEnableItemsToPreferences', () => { }, }; - const convertedState = moveInterfaceEnableItemsToPreferences( state ); + const convertedState = moveInterfaceEnableItems( state ); expect( convertedState ).toEqual( { 'core/preferences': { @@ -88,7 +88,7 @@ describe( 'moveInterfaceEnableItemsToPreferences', () => { }, }; - const convertedState = moveInterfaceEnableItemsToPreferences( state ); + const convertedState = moveInterfaceEnableItems( state ); expect( convertedState ).toEqual( { 'core/preferences': { diff --git a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-third-party-feature-preferences-to-preferences.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-third-party-feature-preferences.js similarity index 82% rename from packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-third-party-feature-preferences-to-preferences.js rename to packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-third-party-feature-preferences.js index a512871713f455..9f7d6fdc63b47d 100644 --- a/packages/database-persistence-layer/src/migrations/from-legacy-local-storage-persistence/test/move-third-party-feature-preferences-to-preferences.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-third-party-feature-preferences.js @@ -1,9 +1,9 @@ /** * Internal dependencies */ -import moveThirdPartyFeaturePreferencesToPreferences from '../move-third-party-feature-preferences-to-preferences'; +import moveThirdPartyFeaturePreferences from '../move-third-party-feature-preferences'; -describe( 'moveThirdPartyFeaturePreferencesToPreferences', () => { +describe( 'moveThirdPartyFeaturePreferences', () => { it( 'migrates multiple scopes from the interface package to the preferences package', () => { const state = { 'core/interface': { @@ -27,9 +27,7 @@ describe( 'moveThirdPartyFeaturePreferencesToPreferences', () => { }, }; - const convertedState = moveThirdPartyFeaturePreferencesToPreferences( - state - ); + const convertedState = moveThirdPartyFeaturePreferences( state ); expect( convertedState ).toEqual( { 'core/preferences': { @@ -80,9 +78,7 @@ describe( 'moveThirdPartyFeaturePreferencesToPreferences', () => { }, }; - const convertedState = moveThirdPartyFeaturePreferencesToPreferences( - state - ); + const convertedState = moveThirdPartyFeaturePreferences( state ); expect( convertedState ).toEqual( { 'core/preferences': { From 8d10fd3df8dba4f9d8ecc9d96eb3c0f75ab3c1a3 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 7 Apr 2022 16:22:30 +0800 Subject: [PATCH 45/74] Add a test for the whole migration and fix a bug the test found --- .../move-interface-enable-items.js | 1 + .../legacy-local-storage-data/test/index.js | 121 ++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-interface-enable-items.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-interface-enable-items.js index 522618645f02f4..dd7c5e79d4bfe0 100644 --- a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-interface-enable-items.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-interface-enable-items.js @@ -108,6 +108,7 @@ export default function moveInterfaceEnableItemsToPreferences( state ) { const otherInterfaceItems = state[ interfaceStoreName ]; return { + ...state, [ preferencesStoreName ]: { preferences: allConvertedData, }, diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js new file mode 100644 index 00000000000000..c3efc1435cfd1e --- /dev/null +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js @@ -0,0 +1,121 @@ +/** + * Internal dependencies + */ +import { convertLegacyData } from '..'; + +const legacyData = { + 'core/interface': { + enableItems: { + singleEnableItems: { + complementaryArea: { + 'core/edit-post': 'edit-post/document', + 'core/edit-site': 'edit-site/global-styles', + 'core/edit-widgets': 'edit-widgets/block-areas', + }, + }, + multipleEnableItems: { + pinnedItems: { + 'core/edit-post': { + 'my-sidebar-plugin/title-sidebar': false, + }, + }, + }, + }, + preferences: { + features: { + 'core/edit-post': { welcomeGuide: false, fixedToolbar: true }, + 'core/edit-widgets': { + welcomeGuide: false, + fixedToolbar: true, + keepCaretInsideBlock: true, + }, + 'core/customize-widgets': { + welcomeGuide: false, + fixedToolbar: true, + keepCaretInsideBlock: true, + }, + 'third-party-plugin': { + thirdPartyFeature: true, + }, + }, + }, + }, + 'core/edit-post': { + preferences: { + panels: { + 'post-status': { opened: true }, + 'post-excerpt': { enabled: false }, + 'taxonomy-panel-category': { opened: true }, + }, + editorMode: 'text', + hiddenBlockTypes: [ 'core/heading', 'core/list' ], + preferredStyleVariations: { 'core/quote': 'plain' }, + localAutosaveInterval: 15, + }, + }, + 'core/edit-site': { + preferences: { + features: { + welcomeGuide: false, + welcomeGuideStyles: false, + fixedToolbar: true, + focusMode: true, + }, + }, + }, +}; + +describe( 'convertLegacyData', () => { + it( 'converts to the expected format', () => { + expect( convertLegacyData( legacyData ) ).toMatchInlineSnapshot( ` + Object { + "preferences": Object { + "core/customize-widgets": Object { + "fixedToolbar": true, + "keepCaretInsideBlock": true, + "welcomeGuide": false, + }, + "core/edit-post": Object { + "complementaryArea": "edit-post/document", + "editorMode": "text", + "fixedToolbar": true, + "hiddenBlockTypes": Array [ + "core/heading", + "core/list", + ], + "inactivePanels": Array [ + "post-excerpt", + ], + "openPanels": Array [ + "post-status", + "taxonomy-panel-category", + ], + "pinnedItems": Object { + "my-sidebar-plugin/title-sidebar": false, + }, + "preferredStyleVariations": Object { + "core/quote": "plain", + }, + "welcomeGuide": false, + }, + "core/edit-site": Object { + "complementaryArea": "edit-site/global-styles", + "fixedToolbar": true, + "focusMode": true, + "welcomeGuide": false, + "welcomeGuideStyles": false, + }, + "core/edit-widgets": Object { + "complementaryArea": "edit-widgets/block-areas", + "fixedToolbar": true, + "keepCaretInsideBlock": true, + "welcomeGuide": false, + }, + "third-party-plugin": Object { + "thirdPartyFeature": true, + }, + }, + } + ` ); + } ); +} ); From d234b8bb18ee7ae6108b8ab4c0e0383ef2a554d3 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 7 Apr 2022 16:26:25 +0800 Subject: [PATCH 46/74] Improve comment --- .../migrations/legacy-local-storage-data/index.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js index 0367cd3af9530b..4a47dd2a233c95 100644 --- a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js @@ -10,7 +10,7 @@ import convertEditPostPanels from './convert-edit-post-panels'; /** * Gets the legacy local storage data for a given user. * - * @param {string} userId The user id. + * @param {string | number} userId The user id. * * @return {Object | null} The local storage data. */ @@ -82,11 +82,20 @@ export function convertLegacyData( data ) { 'editorMode' ); - // The previous legacy persistence system contained preferences for - // multiple stores + // The new system is only concerned with persisting + // 'core/preferences', so only return that. return data?.[ 'core/preferences' ]; } +/** + * Gets the legacy local storage data for the given user and returns the + * data converted to the new format. + * + * @param {string | number} userId The user id. + * + * @return {Object | undefined} The converted data or undefined if no local + * storage data could be found. + */ export default function convertLegacyLocalStorageData( userId ) { const data = getLegacyData( userId ); return convertLegacyData( data ); From 21b66dec82ab7d418fe2e043ec98c8aac9dac4e6 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 7 Apr 2022 17:08:25 +0800 Subject: [PATCH 47/74] Fix some more migration issues --- .../src/create/index.js | 4 +- .../legacy-local-storage-data/index.js | 4 +- .../move-feature-preferences.js | 16 +- .../move-individual-preference.js | 12 +- .../move-interface-enable-items.js | 12 +- .../move-third-party-feature-preferences.js | 17 +- .../legacy-local-storage-data/test/index.js | 194 ++++++++++++++---- 7 files changed, 185 insertions(+), 74 deletions(-) diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index b43c8c291ddc21..be0de8c1913e72 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -14,8 +14,6 @@ const localStorage = window.localStorage; /** * Creates a database persistence layer, storing data in the user meta. * - * - * * @param {Object} options * @param {Object} options.preloadedData Any persisted data that should be preloaded. * @param {number} options.requestDebounceMS Debounce requests to the API so that they only occur @@ -61,7 +59,7 @@ export default function create( { return cache; } - async function set( newData ) { + function set( newData ) { const dataWithTimestamp = { ...newData, __timestamp: Date.now() }; cache = dataWithTimestamp; diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js index 4a47dd2a233c95..9ac1f354920306 100644 --- a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js @@ -83,8 +83,8 @@ export function convertLegacyData( data ) { ); // The new system is only concerned with persisting - // 'core/preferences', so only return that. - return data?.[ 'core/preferences' ]; + // 'core/preferences' preferences reducer, so only return that. + return data?.[ 'core/preferences' ]?.preferences; } /** diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-feature-preferences.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-feature-preferences.js index e1b4b51002fdca..6f7989a8557bf5 100644 --- a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-feature-preferences.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-feature-preferences.js @@ -64,8 +64,10 @@ export default function moveFeaturePreferences( state, sourceStoreName ) { // also check the original package as the user may have updated from an // older block editor version. const interfaceFeatures = - state[ interfaceStoreName ]?.preferences?.features?.[ sourceStoreName ]; - const sourceFeatures = state[ sourceStoreName ]?.preferences?.features; + state?.[ interfaceStoreName ]?.preferences?.features?.[ + sourceStoreName + ]; + const sourceFeatures = state?.[ sourceStoreName ]?.preferences?.features; const featuresToMigrate = interfaceFeatures ? interfaceFeatures : sourceFeatures; @@ -74,7 +76,7 @@ export default function moveFeaturePreferences( state, sourceStoreName ) { return state; } - const existingPreferences = state[ preferencesStoreName ]?.preferences; + const existingPreferences = state?.[ preferencesStoreName ]?.preferences; // Avoid migrating features again if they've previously been migrated. if ( existingPreferences?.[ sourceStoreName ] ) { @@ -83,9 +85,9 @@ export default function moveFeaturePreferences( state, sourceStoreName ) { let updatedInterfaceState; if ( interfaceFeatures ) { - const otherInterfaceState = state[ interfaceStoreName ]; + const otherInterfaceState = state?.[ interfaceStoreName ]; const otherInterfaceScopes = - state[ interfaceStoreName ]?.preferences?.features; + state?.[ interfaceStoreName ]?.preferences?.features; updatedInterfaceState = { [ interfaceStoreName ]: { @@ -102,8 +104,8 @@ export default function moveFeaturePreferences( state, sourceStoreName ) { let updatedSourceState; if ( sourceFeatures ) { - const otherSourceState = state[ sourceStoreName ]; - const sourcePreferences = state[ sourceStoreName ]?.preferences; + const otherSourceState = state?.[ sourceStoreName ]; + const sourcePreferences = state?.[ sourceStoreName ]?.preferences; updatedSourceState = { [ sourceStoreName ]: { diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-individual-preference.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-individual-preference.js index e543de5b9e1e03..ff688462aa976d 100644 --- a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-individual-preference.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-individual-preference.js @@ -42,7 +42,7 @@ export default function moveIndividualPreferenceToPreferences( convert = identity ) { const preferencesStoreName = 'core/preferences'; - const sourcePreference = state[ sourceStoreName ]?.preferences?.[ key ]; + const sourcePreference = state?.[ sourceStoreName ]?.preferences?.[ key ]; // There's nothing to migrate, exit early. if ( sourcePreference === undefined ) { @@ -50,19 +50,19 @@ export default function moveIndividualPreferenceToPreferences( } const targetPreference = - state[ preferencesStoreName ]?.preferences?.[ scope ]?.[ key ]; + state?.[ preferencesStoreName ]?.preferences?.[ scope ]?.[ key ]; // There's existing data at the target, so don't overwrite it, exit early. if ( targetPreference ) { return state; } - const otherScopes = state[ preferencesStoreName ]?.preferences; + const otherScopes = state?.[ preferencesStoreName ]?.preferences; const otherPreferences = - state[ preferencesStoreName ]?.preferences?.[ scope ]; + state?.[ preferencesStoreName ]?.preferences?.[ scope ]; - const otherSourceState = state[ sourceStoreName ]; - const allSourcePreferences = state[ sourceStoreName ]?.preferences; + const otherSourceState = state?.[ sourceStoreName ]; + const allSourcePreferences = state?.[ sourceStoreName ]?.preferences; // Pass an object with the key and value as this allows the convert // function to convert to a data structure that has different keys. diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-interface-enable-items.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-interface-enable-items.js index dd7c5e79d4bfe0..e1c701e3b138aa 100644 --- a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-interface-enable-items.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-interface-enable-items.js @@ -45,17 +45,17 @@ * * @param {Object} state The local storage state. */ -export default function moveInterfaceEnableItemsToPreferences( state ) { +export default function moveInterfaceEnableItems( state ) { const interfaceStoreName = 'core/interface'; const preferencesStoreName = 'core/preferences'; - const sourceEnableItems = state[ interfaceStoreName ]?.enableItems; + const sourceEnableItems = state?.[ interfaceStoreName ]?.enableItems; // There's nothing to migrate, exit early. if ( ! sourceEnableItems ) { - return; + return state; } - const allPreferences = state[ preferencesStoreName ]?.preferences ?? {}; + const allPreferences = state?.[ preferencesStoreName ]?.preferences ?? {}; // First convert complementaryAreas into the right format. // Use the existing preferences as the accumulator so that the data is @@ -69,7 +69,7 @@ export default function moveInterfaceEnableItemsToPreferences( state ) { const data = sourceComplementaryAreas[ scope ]; // Don't overwrite any existing data in the preferences store. - if ( accumulator[ scope ]?.complementaryArea ) { + if ( accumulator?.[ scope ]?.complementaryArea ) { return accumulator; } @@ -90,7 +90,7 @@ export default function moveInterfaceEnableItemsToPreferences( state ) { ( accumulator, scope ) => { const data = sourcePinnedItems[ scope ]; // Don't overwrite any existing data in the preferences store. - if ( accumulator[ scope ]?.pinnedItems ) { + if ( accumulator?.[ scope ]?.pinnedItems ) { return accumulator; } diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js index 01f0d2993d974e..e0f765fe920443 100644 --- a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js @@ -43,8 +43,11 @@ export default function moveThirdPartyFeaturePreferencesToPreferences( state ) { const interfaceStoreName = 'core/interface'; const preferencesStoreName = 'core/preferences'; - const interfaceScopes = state[ interfaceStoreName ]?.preferences?.features; - const interfaceScopeKeys = Object.keys( interfaceScopes ); + const interfaceScopes = + state?.[ interfaceStoreName ]?.preferences?.features; + const interfaceScopeKeys = interfaceScopes + ? Object.keys( interfaceScopes ) + : []; if ( ! interfaceScopeKeys?.length ) { return state; @@ -55,23 +58,23 @@ export default function moveThirdPartyFeaturePreferencesToPreferences( state ) { return convertedState; } - const featuresToMigrate = interfaceScopes[ scope ]; + const featuresToMigrate = interfaceScopes?.[ scope ]; if ( ! featuresToMigrate ) { return convertedState; } const existingMigratedData = - convertedState[ preferencesStoreName ]?.preferences?.[ scope ]; + convertedState?.[ preferencesStoreName ]?.preferences?.[ scope ]; if ( existingMigratedData ) { return convertedState; } const otherPreferencesScopes = - convertedState[ preferencesStoreName ]?.preferences; - const otherInterfaceState = convertedState[ interfaceStoreName ]; + convertedState?.[ preferencesStoreName ]?.preferences; + const otherInterfaceState = convertedState?.[ interfaceStoreName ]; const otherInterfaceScopes = - convertedState[ interfaceStoreName ]?.preferences?.features; + convertedState?.[ interfaceStoreName ]?.preferences?.features; return { ...convertedState, diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js index c3efc1435cfd1e..ea86cce59bf835 100644 --- a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js @@ -3,7 +3,7 @@ */ import { convertLegacyData } from '..'; -const legacyData = { +const legacyData1 = { 'core/interface': { enableItems: { singleEnableItems: { @@ -65,55 +65,163 @@ const legacyData = { }, }; +const alreadyConvertedData = { + 'core/block-editor': { + preferences: { + insertUsage: { + 'core/paragraph': { + time: 1649320988011, + count: 2, + insert: { + name: 'core/paragraph', + }, + }, + 'core/quote': { + time: 1649320934860, + count: 1, + insert: { + name: 'core/quote', + }, + }, + 'core/image': { + time: 1649321017053, + count: 1, + insert: { + name: 'core/image', + }, + }, + 'core/group': { + time: 1649321017077, + count: 1, + insert: { + name: 'core/group', + }, + }, + }, + }, + }, + 'core/preferences': { + preferences: { + 'core/edit-widgets': { + welcomeGuide: false, + fixedToolbar: true, + showBlockBreadcrumbs: false, + complementaryArea: 'edit-widgets/block-areas', + }, + 'core/edit-post': { + welcomeGuide: false, + fixedToolbar: true, + fullscreenMode: false, + hiddenBlockTypes: [ 'core/audio', 'core/cover' ], + editorMode: 'visual', + preferredStyleVariations: { + 'core/quote': 'large', + }, + inactivePanels: [], + openPanels: [ 'post-status' ], + complementaryArea: 'edit-post/block', + pinnedItems: { + 'my-sidebar-plugin/title-sidebar': false, + }, + }, + 'core/edit-site': { + welcomeGuide: false, + welcomeGuideStyles: false, + fixedToolbar: true, + complementaryArea: 'edit-site/global-styles', + }, + }, + }, +}; + describe( 'convertLegacyData', () => { it( 'converts to the expected format', () => { - expect( convertLegacyData( legacyData ) ).toMatchInlineSnapshot( ` + expect( convertLegacyData( legacyData1 ) ).toMatchInlineSnapshot( ` Object { - "preferences": Object { - "core/customize-widgets": Object { - "fixedToolbar": true, - "keepCaretInsideBlock": true, - "welcomeGuide": false, - }, - "core/edit-post": Object { - "complementaryArea": "edit-post/document", - "editorMode": "text", - "fixedToolbar": true, - "hiddenBlockTypes": Array [ - "core/heading", - "core/list", - ], - "inactivePanels": Array [ - "post-excerpt", - ], - "openPanels": Array [ - "post-status", - "taxonomy-panel-category", - ], - "pinnedItems": Object { - "my-sidebar-plugin/title-sidebar": false, - }, - "preferredStyleVariations": Object { - "core/quote": "plain", - }, - "welcomeGuide": false, + "core/customize-widgets": Object { + "fixedToolbar": true, + "keepCaretInsideBlock": true, + "welcomeGuide": false, + }, + "core/edit-post": Object { + "complementaryArea": "edit-post/document", + "editorMode": "text", + "fixedToolbar": true, + "hiddenBlockTypes": Array [ + "core/heading", + "core/list", + ], + "inactivePanels": Array [ + "post-excerpt", + ], + "openPanels": Array [ + "post-status", + "taxonomy-panel-category", + ], + "pinnedItems": Object { + "my-sidebar-plugin/title-sidebar": false, }, - "core/edit-site": Object { - "complementaryArea": "edit-site/global-styles", - "fixedToolbar": true, - "focusMode": true, - "welcomeGuide": false, - "welcomeGuideStyles": false, + "preferredStyleVariations": Object { + "core/quote": "plain", }, - "core/edit-widgets": Object { - "complementaryArea": "edit-widgets/block-areas", - "fixedToolbar": true, - "keepCaretInsideBlock": true, - "welcomeGuide": false, + "welcomeGuide": false, + }, + "core/edit-site": Object { + "complementaryArea": "edit-site/global-styles", + "fixedToolbar": true, + "focusMode": true, + "welcomeGuide": false, + "welcomeGuideStyles": false, + }, + "core/edit-widgets": Object { + "complementaryArea": "edit-widgets/block-areas", + "fixedToolbar": true, + "keepCaretInsideBlock": true, + "welcomeGuide": false, + }, + "third-party-plugin": Object { + "thirdPartyFeature": true, + }, + } + ` ); + } ); + + it( 'retains already converted data', () => { + expect( convertLegacyData( alreadyConvertedData ) ) + .toMatchInlineSnapshot( ` + Object { + "core/edit-post": Object { + "complementaryArea": "edit-post/block", + "editorMode": "visual", + "fixedToolbar": true, + "fullscreenMode": false, + "hiddenBlockTypes": Array [ + "core/audio", + "core/cover", + ], + "inactivePanels": Array [], + "openPanels": Array [ + "post-status", + ], + "pinnedItems": Object { + "my-sidebar-plugin/title-sidebar": false, }, - "third-party-plugin": Object { - "thirdPartyFeature": true, + "preferredStyleVariations": Object { + "core/quote": "large", }, + "welcomeGuide": false, + }, + "core/edit-site": Object { + "complementaryArea": "edit-site/global-styles", + "fixedToolbar": true, + "welcomeGuide": false, + "welcomeGuideStyles": false, + }, + "core/edit-widgets": Object { + "complementaryArea": "edit-widgets/block-areas", + "fixedToolbar": true, + "showBlockBreadcrumbs": false, + "welcomeGuide": false, }, } ` ); From 315a4fe5c0b35f4bf1e04aecd7fe7654377f144d Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 7 Apr 2022 17:09:12 +0800 Subject: [PATCH 48/74] Remove old migration code --- .../data/src/plugins/persistence/index.js | 422 +--------- .../src/plugins/persistence/test/index.js | 778 +----------------- 2 files changed, 3 insertions(+), 1197 deletions(-) diff --git a/packages/data/src/plugins/persistence/index.js b/packages/data/src/plugins/persistence/index.js index f00f8a8fc2b185..2052c7c58b0fee 100644 --- a/packages/data/src/plugins/persistence/index.js +++ b/packages/data/src/plugins/persistence/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { merge, isPlainObject, identity } from 'lodash'; +import { merge, isPlainObject } from 'lodash'; /** * Internal dependencies @@ -222,424 +222,6 @@ function persistencePlugin( registry, pluginOptions ) { }; } -/** - * Move the 'features' object in local storage from the sourceStoreName to the - * preferences store. - * - * @param {Object} persistence The persistence interface. - * @param {string} sourceStoreName The name of the store that has persisted - * preferences to migrate to the preferences - * package. - */ -export function migrateFeaturePreferencesToPreferencesStore( - persistence, - sourceStoreName -) { - const preferencesStoreName = 'core/preferences'; - const interfaceStoreName = 'core/interface'; - - const state = persistence.get(); - - // Features most recently (and briefly) lived in the interface package. - // If data exists there, prioritize using that for the migration. If not - // also check the original package as the user may have updated from an - // older block editor version. - const interfaceFeatures = - state[ interfaceStoreName ]?.preferences?.features?.[ sourceStoreName ]; - const sourceFeatures = state[ sourceStoreName ]?.preferences?.features; - const featuresToMigrate = interfaceFeatures - ? interfaceFeatures - : sourceFeatures; - - if ( featuresToMigrate ) { - const existingPreferences = state[ preferencesStoreName ]?.preferences; - - // Avoid migrating features again if they've previously been migrated. - if ( ! existingPreferences?.[ sourceStoreName ] ) { - // Set the feature values in the interface store, the features - // object is keyed by 'scope', which matches the store name for - // the source. - persistence.set( preferencesStoreName, { - preferences: { - ...existingPreferences, - [ sourceStoreName ]: featuresToMigrate, - }, - } ); - - // Remove migrated feature preferences from `interface`. - if ( interfaceFeatures ) { - const otherInterfaceState = state[ interfaceStoreName ]; - const otherInterfaceScopes = - state[ interfaceStoreName ]?.preferences?.features; - - persistence.set( interfaceStoreName, { - ...otherInterfaceState, - preferences: { - features: { - ...otherInterfaceScopes, - [ sourceStoreName ]: undefined, - }, - }, - } ); - } - - // Remove migrated feature preferences from the source. - if ( sourceFeatures ) { - const otherSourceState = state[ sourceStoreName ]; - const sourcePreferences = state[ sourceStoreName ]?.preferences; - - persistence.set( sourceStoreName, { - ...otherSourceState, - preferences: { - ...sourcePreferences, - features: undefined, - }, - } ); - } - } - } -} - -/** - * Migrates an individual item inside the `preferences` object for a store. - * - * @param {Object} persistence The persistence interface. - * @param {Object} migrate An options object that contains details of the migration. - * @param {string} migrate.from The name of the store to migrate from. - * @param {string} migrate.scope The scope in the preferences store to migrate to. - * @param {string} key The key in the preferences object to migrate. - * @param {?Function} convert A function that converts preferences from one format to another. - */ -export function migrateIndividualPreferenceToPreferencesStore( - persistence, - { from: sourceStoreName, scope }, - key, - convert = identity -) { - const preferencesStoreName = 'core/preferences'; - const state = persistence.get(); - const sourcePreference = state[ sourceStoreName ]?.preferences?.[ key ]; - - // There's nothing to migrate, exit early. - if ( sourcePreference === undefined ) { - return; - } - - const targetPreference = - state[ preferencesStoreName ]?.preferences?.[ scope ]?.[ key ]; - - // There's existing data at the target, so don't overwrite it, exit early. - if ( targetPreference ) { - return; - } - - const otherScopes = state[ preferencesStoreName ]?.preferences; - const otherPreferences = - state[ preferencesStoreName ]?.preferences?.[ scope ]; - - // Pass an object with the key and value as this allows the convert - // function to convert to a data structure that has different keys. - const convertedPreferences = convert( { [ key ]: sourcePreference } ); - - persistence.set( preferencesStoreName, { - preferences: { - ...otherScopes, - [ scope ]: { - ...otherPreferences, - ...convertedPreferences, - }, - }, - } ); - - // Remove migrated feature preferences from the source. - const otherSourceState = state[ sourceStoreName ]; - const allSourcePreferences = state[ sourceStoreName ]?.preferences; - persistence.set( sourceStoreName, { - ...otherSourceState, - preferences: { - ...allSourcePreferences, - [ key ]: undefined, - }, - } ); -} - -/** - * Convert from: - * ``` - * { - * panels: { - * tags: { - * enabled: true, - * opened: true, - * }, - * permalinks: { - * enabled: false, - * opened: false, - * }, - * }, - * } - * ``` - * - * to: - * { - * inactivePanels: [ - * 'permalinks', - * ], - * openPanels: [ - * 'tags', - * ], - * } - * - * @param {Object} preferences A preferences object. - * - * @return {Object} The converted data. - */ -export function convertEditPostPanels( preferences ) { - const panels = preferences?.panels ?? {}; - return Object.keys( panels ).reduce( - ( convertedData, panelName ) => { - const panel = panels[ panelName ]; - - if ( panel?.enabled === false ) { - convertedData.inactivePanels.push( panelName ); - } - - if ( panel?.opened === true ) { - convertedData.openPanels.push( panelName ); - } - - return convertedData; - }, - { inactivePanels: [], openPanels: [] } - ); -} - -export function migrateThirdPartyFeaturePreferencesToPreferencesStore( - persistence -) { - const interfaceStoreName = 'core/interface'; - const preferencesStoreName = 'core/preferences'; - - let state = persistence.get(); - - const interfaceScopes = state[ interfaceStoreName ]?.preferences?.features; - - for ( const scope in interfaceScopes ) { - // Don't migrate any core 'scopes'. - if ( scope.startsWith( 'core' ) ) { - continue; - } - - // Skip this scope if there are no features to migrate. - const featuresToMigrate = interfaceScopes[ scope ]; - if ( ! featuresToMigrate ) { - continue; - } - - const existingPreferences = state[ preferencesStoreName ]?.preferences; - - // Add the data to the preferences store structure. - persistence.set( preferencesStoreName, { - preferences: { - ...existingPreferences, - [ scope ]: featuresToMigrate, - }, - } ); - - // Remove the data from the interface store structure. - // Call `persistence.get` again to make sure `state` is up-to-date with - // any changes from the previous iteration of this loop. - state = persistence.get(); - const otherInterfaceState = state[ interfaceStoreName ]; - const otherInterfaceScopes = - state[ interfaceStoreName ]?.preferences?.features; - - persistence.set( interfaceStoreName, { - ...otherInterfaceState, - preferences: { - features: { - ...otherInterfaceScopes, - [ scope ]: undefined, - }, - }, - } ); - } -} - -/** - * Migrates interface 'enableItems' data to the preferences store. - * - * The interface package stores this data in this format: - * ```js - * { - * enableItems: { - * singleEnableItems: { - * complementaryArea: { - * 'core/edit-post': 'edit-post/document', - * 'core/edit-site': 'edit-site/global-styles', - * } - * }, - * multipleEnableItems: { - * pinnedItems: { - * 'core/edit-post': { - * 'plugin-1': true, - * }, - * 'core/edit-site': { - * 'plugin-2': true, - * }, - * }, - * } - * } - * } - * ``` - * and it should be migrated it to: - * ```js - * { - * 'core/edit-post': { - * complementaryArea: 'edit-post/document', - * pinnedItems: { - * 'plugin-1': true, - * }, - * }, - * 'core/edit-site': { - * complementaryArea: 'edit-site/global-styles', - * pinnedItems: { - * 'plugin-2': true, - * }, - * }, - * } - * ``` - * - * @param {Object} persistence The persistence interface. - */ -export function migrateInterfaceEnableItemsToPreferencesStore( persistence ) { - const interfaceStoreName = 'core/interface'; - const preferencesStoreName = 'core/preferences'; - const state = persistence.get(); - const sourceEnableItems = state[ interfaceStoreName ]?.enableItems; - - // There's nothing to migrate, exit early. - if ( ! sourceEnableItems ) { - return; - } - - const allPreferences = state[ preferencesStoreName ]?.preferences ?? {}; - - // First convert complementaryAreas into the right format. - // Use the existing preferences as the accumulator so that the data is - // merged. - const sourceComplementaryAreas = - sourceEnableItems?.singleEnableItems?.complementaryArea ?? {}; - - const convertedComplementaryAreas = Object.keys( - sourceComplementaryAreas - ).reduce( ( accumulator, scope ) => { - const data = sourceComplementaryAreas[ scope ]; - - // Don't overwrite any existing data in the preferences store. - if ( accumulator[ scope ]?.complementaryArea ) { - return accumulator; - } - - return { - ...accumulator, - [ scope ]: { - ...accumulator[ scope ], - complementaryArea: data, - }, - }; - }, allPreferences ); - - // Next feed the converted complementary areas back into a reducer that - // converts the pinned items, resulting in the fully migrated data. - const sourcePinnedItems = - sourceEnableItems?.multipleEnableItems?.pinnedItems ?? {}; - const allConvertedData = Object.keys( sourcePinnedItems ).reduce( - ( accumulator, scope ) => { - const data = sourcePinnedItems[ scope ]; - // Don't overwrite any existing data in the preferences store. - if ( accumulator[ scope ]?.pinnedItems ) { - return accumulator; - } - - return { - ...accumulator, - [ scope ]: { - ...accumulator[ scope ], - pinnedItems: data, - }, - }; - }, - convertedComplementaryAreas - ); - - persistence.set( preferencesStoreName, { - preferences: allConvertedData, - } ); - - // Remove migrated preferences. - const otherInterfaceItems = state[ interfaceStoreName ]; - persistence.set( interfaceStoreName, { - ...otherInterfaceItems, - enableItems: undefined, - } ); -} - -persistencePlugin.__unstableMigrate = ( pluginOptions ) => { - const persistence = createPersistenceInterface( pluginOptions ); - - // Boolean feature preferences. - migrateFeaturePreferencesToPreferencesStore( - persistence, - 'core/edit-widgets' - ); - migrateFeaturePreferencesToPreferencesStore( - persistence, - 'core/customize-widgets' - ); - migrateFeaturePreferencesToPreferencesStore( - persistence, - 'core/edit-post' - ); - migrateFeaturePreferencesToPreferencesStore( - persistence, - 'core/edit-site' - ); - migrateThirdPartyFeaturePreferencesToPreferencesStore( persistence ); - - // Other ad-hoc preferences. - migrateIndividualPreferenceToPreferencesStore( - persistence, - { from: 'core/edit-post', scope: 'core/edit-post' }, - 'hiddenBlockTypes' - ); - migrateIndividualPreferenceToPreferencesStore( - persistence, - { from: 'core/edit-post', scope: 'core/edit-post' }, - 'editorMode' - ); - migrateIndividualPreferenceToPreferencesStore( - persistence, - { from: 'core/edit-post', scope: 'core/edit-post' }, - 'preferredStyleVariations' - ); - migrateIndividualPreferenceToPreferencesStore( - persistence, - { from: 'core/edit-post', scope: 'core/edit-post' }, - 'panels', - convertEditPostPanels - ); - migrateIndividualPreferenceToPreferencesStore( - persistence, - { from: 'core/editor', scope: 'core/edit-post' }, - 'isPublishSidebarEnabled' - ); - migrateIndividualPreferenceToPreferencesStore( - persistence, - { from: 'core/edit-site', scope: 'core/edit-site' }, - 'editorMode' - ); - migrateInterfaceEnableItemsToPreferencesStore( persistence ); -}; +persistencePlugin.__unstableMigrate = () => {}; export default persistencePlugin; diff --git a/packages/data/src/plugins/persistence/test/index.js b/packages/data/src/plugins/persistence/test/index.js index 9e442a63daa7e9..818f075640ae56 100644 --- a/packages/data/src/plugins/persistence/test/index.js +++ b/packages/data/src/plugins/persistence/test/index.js @@ -6,15 +6,7 @@ import deepFreeze from 'deep-freeze'; /** * Internal dependencies */ -import plugin, { - createPersistenceInterface, - withLazySameState, - migrateFeaturePreferencesToPreferencesStore, - migrateThirdPartyFeaturePreferencesToPreferencesStore, - migrateIndividualPreferenceToPreferencesStore, - convertEditPostPanels, - migrateInterfaceEnableItemsToPreferencesStore, -} from '../'; +import plugin, { createPersistenceInterface, withLazySameState } from '../'; import objectStorage from '../storage/object'; import { createRegistry } from '../../../'; @@ -385,771 +377,3 @@ describe( 'persistence', () => { } ); } ); } ); - -describe( 'migrateFeaturePreferencesToPreferencesStore', () => { - it( 'migrates multiple preferences from persisted source stores to preferences', () => { - const persistenceInterface = createPersistenceInterface( { - storageKey: 'test-username', - } ); - - const sourceStateA = { - preferences: { - features: { - featureA: true, - featureB: false, - featureC: true, - }, - }, - }; - - const sourceStateB = { - preferences: { - features: { - featureD: true, - featureE: false, - featureF: true, - }, - }, - }; - - persistenceInterface.set( 'core/test-a', sourceStateA ); - persistenceInterface.set( 'core/test-b', sourceStateB ); - - migrateFeaturePreferencesToPreferencesStore( - persistenceInterface, - 'core/test-a' - ); - - migrateFeaturePreferencesToPreferencesStore( - persistenceInterface, - 'core/test-b' - ); - - expect( persistenceInterface.get() ).toEqual( { - 'core/preferences': { - preferences: { - 'core/test-a': { - featureA: true, - featureB: false, - featureC: true, - }, - 'core/test-b': { - featureD: true, - featureE: false, - featureF: true, - }, - }, - }, - 'core/test-a': { - preferences: { - features: undefined, - }, - }, - 'core/test-b': { - preferences: { - features: undefined, - }, - }, - } ); - } ); - - it( 'migrates multiple preferences from the persisted interface store to preferences, with interface state taking precedence over source stores', () => { - const persistenceInterface = createPersistenceInterface( { - storageKey: 'test-username', - } ); - - const sourceStateA = { - preferences: { - features: { - featureA: true, - featureB: false, - featureC: true, - }, - }, - }; - - const sourceStateB = { - preferences: { - features: { - featureD: true, - featureE: false, - featureF: true, - }, - }, - }; - - const interfaceState = { - otherData: { - test: 1, - }, - preferences: { - features: { - 'core/test-a': { - featureG: true, - featureH: false, - featureI: true, - }, - 'core/test-b': { - featureJ: true, - featureK: false, - featureL: true, - }, - }, - }, - }; - - persistenceInterface.set( 'core/test-a', sourceStateA ); - persistenceInterface.set( 'core/test-b', sourceStateB ); - persistenceInterface.set( 'core/interface', interfaceState ); - - migrateFeaturePreferencesToPreferencesStore( - persistenceInterface, - 'core/test-a' - ); - - migrateFeaturePreferencesToPreferencesStore( - persistenceInterface, - 'core/test-b' - ); - - expect( persistenceInterface.get() ).toEqual( { - 'core/preferences': { - preferences: { - 'core/test-a': { - featureG: true, - featureH: false, - featureI: true, - }, - 'core/test-b': { - featureJ: true, - featureK: false, - featureL: true, - }, - }, - }, - 'core/interface': { - otherData: { - test: 1, - }, - preferences: { - features: { - 'core/test-a': undefined, - 'core/test-b': undefined, - }, - }, - }, - 'core/test-a': { - preferences: { - features: undefined, - }, - }, - 'core/test-b': { - preferences: { - features: undefined, - }, - }, - } ); - } ); - - it( 'only migrates persisted preference data for the source name from source stores', () => { - const persistenceInterface = createPersistenceInterface( { - storageKey: 'test-username', - } ); - - const sourceStateA = { - otherData: { - test: 1, - }, - preferences: { - features: { - featureA: true, - featureB: false, - featureC: true, - }, - }, - }; - - const sourceStateB = { - otherData: { - test: 2, - }, - preferences: { - features: { - featureD: true, - featureE: false, - featureF: true, - }, - }, - }; - - persistenceInterface.set( 'core/test-a', sourceStateA ); - persistenceInterface.set( 'core/test-b', sourceStateB ); - - migrateFeaturePreferencesToPreferencesStore( - persistenceInterface, - 'core/test-a' - ); - - expect( persistenceInterface.get() ).toEqual( { - 'core/preferences': { - preferences: { - 'core/test-a': { - featureA: true, - featureB: false, - featureC: true, - }, - }, - }, - 'core/test-a': { - otherData: { - test: 1, - }, - preferences: { - features: undefined, - }, - }, - 'core/test-b': { - otherData: { - test: 2, - }, - preferences: { - features: { - featureD: true, - featureE: false, - featureF: true, - }, - }, - }, - } ); - } ); - - it( 'only migrates persisted data for the source name from interface', () => { - const persistenceInterface = createPersistenceInterface( { - storageKey: 'test-username', - } ); - - const interfaceState = { - preferences: { - features: { - 'core/test-a': { - featureG: true, - featureH: false, - featureI: true, - }, - 'core/test-b': { - featureJ: true, - featureK: false, - featureL: true, - }, - }, - }, - }; - - persistenceInterface.set( 'core/interface', interfaceState ); - - migrateFeaturePreferencesToPreferencesStore( - persistenceInterface, - 'core/test-a' - ); - - expect( persistenceInterface.get() ).toEqual( { - 'core/preferences': { - preferences: { - 'core/test-a': { - featureG: true, - featureH: false, - featureI: true, - }, - }, - }, - 'core/interface': { - preferences: { - features: { - 'core/test-a': undefined, - 'core/test-b': { - featureJ: true, - featureK: false, - featureL: true, - }, - }, - }, - }, - } ); - } ); -} ); - -describe( 'migrateIndividualPreferenceToPreferencesStore', () => { - it( 'migrates an individual preference from the source to the preferences store', () => { - const persistenceInterface = createPersistenceInterface( { - storageKey: 'test-username', - } ); - - const initialState = { - preferences: { - myPreference: '123', - }, - }; - - persistenceInterface.set( 'core/test', initialState ); - - migrateIndividualPreferenceToPreferencesStore( - persistenceInterface, - { from: 'core/test', scope: 'core/test' }, - 'myPreference' - ); - - expect( persistenceInterface.get() ).toEqual( { - 'core/preferences': { - preferences: { - 'core/test': { - myPreference: '123', - }, - }, - }, - 'core/test': { - preferences: { - myPreference: undefined, - }, - }, - } ); - } ); - - it( 'does not overwrite other preferences in the preferences store', () => { - const persistenceInterface = createPersistenceInterface( { - storageKey: 'test-username', - } ); - - const initialState = { - otherData: { - test: 1, - }, - preferences: { - myPreference: '123', - }, - }; - - persistenceInterface.set( 'core/test', initialState ); - persistenceInterface.set( 'core/preferences', { - preferences: { - 'core/other-store': { - preferenceA: 1, - preferenceB: 2, - }, - 'core/test': { - unrelatedPreference: 'unrelated-value', - }, - }, - } ); - - migrateIndividualPreferenceToPreferencesStore( - persistenceInterface, - { from: 'core/test', scope: 'core/test' }, - 'myPreference' - ); - - expect( persistenceInterface.get() ).toEqual( { - 'core/preferences': { - preferences: { - 'core/other-store': { - preferenceA: 1, - preferenceB: 2, - }, - 'core/test': { - unrelatedPreference: 'unrelated-value', - myPreference: '123', - }, - }, - }, - 'core/test': { - otherData: { - test: 1, - }, - preferences: { - myPreference: undefined, - }, - }, - } ); - } ); - - it( 'supports moving data to a scope that is differently named to the source store', () => { - const persistenceInterface = createPersistenceInterface( { - storageKey: 'test-username', - } ); - - const initialState = { - preferences: { - myPreference: '123', - }, - }; - - persistenceInterface.set( 'core/source', initialState ); - - migrateIndividualPreferenceToPreferencesStore( - persistenceInterface, - { from: 'core/source', scope: 'core/destination' }, - 'myPreference' - ); - - expect( persistenceInterface.get() ).toEqual( { - 'core/preferences': { - preferences: { - 'core/destination': { - myPreference: '123', - }, - }, - }, - 'core/source': { - preferences: { - myPreference: undefined, - }, - }, - } ); - } ); - - it( 'does not migrate data if there is already a matching preference key at the target', () => { - const persistenceInterface = createPersistenceInterface( { - storageKey: 'test-username', - } ); - - persistenceInterface.set( 'core/test', { - preferences: { - myPreference: '123', - }, - } ); - - persistenceInterface.set( 'core/preferences', { - preferences: { - 'core/test': { - myPreference: 'already-set', - }, - }, - } ); - - migrateIndividualPreferenceToPreferencesStore( - persistenceInterface, - { from: 'core/test', scope: 'core/test' }, - 'myPreference' - ); - - expect( persistenceInterface.get() ).toEqual( { - 'core/preferences': { - preferences: { - 'core/test': { - myPreference: 'already-set', - }, - }, - }, - 'core/test': { - preferences: { - myPreference: '123', - }, - }, - } ); - } ); - - it( 'migrates preferences that have a `false` value', () => { - const persistenceInterface = createPersistenceInterface( { - storageKey: 'test-username', - } ); - - persistenceInterface.set( 'core/test', { - preferences: { - myFalsePreference: false, - }, - } ); - - migrateIndividualPreferenceToPreferencesStore( - persistenceInterface, - { from: 'core/test', scope: 'core/test' }, - 'myFalsePreference' - ); - - expect( persistenceInterface.get() ).toEqual( { - 'core/preferences': { - preferences: { - 'core/test': { - myFalsePreference: false, - }, - }, - }, - 'core/test': { - preferences: {}, - }, - } ); - } ); -} ); - -describe( 'migrateThirdPartyFeaturePreferencesToPreferencesStore', () => { - it( 'migrates multiple scopes from the interface package to the preferences package', () => { - const persistenceInterface = createPersistenceInterface( { - storageKey: 'test-username', - } ); - - const interfaceState = { - otherData: { - test: 1, - }, - preferences: { - features: { - 'plugin-a': { - featureA: true, - featureB: false, - featureC: true, - }, - 'plugin-b': { - featureD: true, - featureE: false, - featureF: true, - }, - }, - }, - }; - persistenceInterface.set( 'core/interface', interfaceState ); - - migrateThirdPartyFeaturePreferencesToPreferencesStore( - persistenceInterface - ); - - expect( persistenceInterface.get() ).toEqual( { - 'core/preferences': { - preferences: { - 'plugin-a': { - featureA: true, - featureB: false, - featureC: true, - }, - 'plugin-b': { - featureD: true, - featureE: false, - featureF: true, - }, - }, - }, - 'core/interface': { - otherData: { - test: 1, - }, - preferences: { - features: { - 'plugin-a': undefined, - 'plugin-b': undefined, - }, - }, - }, - } ); - } ); - - it( 'ignores any core scopes', () => { - const persistenceInterface = createPersistenceInterface( { - storageKey: 'test-username', - } ); - - const interfaceState = { - preferences: { - features: { - 'plugin-a': { - featureA: true, - featureB: false, - featureC: true, - }, - 'core/edit-post': { - featureD: true, - featureE: false, - featureF: true, - }, - }, - }, - }; - persistenceInterface.set( 'core/interface', interfaceState ); - - migrateThirdPartyFeaturePreferencesToPreferencesStore( - persistenceInterface - ); - - expect( persistenceInterface.get() ).toEqual( { - 'core/preferences': { - preferences: { - 'plugin-a': { - featureA: true, - featureB: false, - featureC: true, - }, - }, - }, - 'core/interface': { - preferences: { - features: { - 'plugin-a': undefined, - 'core/edit-post': { - featureD: true, - featureE: false, - featureF: true, - }, - }, - }, - }, - } ); - } ); -} ); - -describe( 'convertEditPostPanels', () => { - it( 'converts from one format to another', () => { - expect( - convertEditPostPanels( { - panels: { - tags: { - enabled: true, - opened: true, - }, - permalinks: { - enabled: false, - opened: false, - }, - categories: { - enabled: true, - opened: false, - }, - excerpt: { - enabled: false, - opened: true, - }, - discussion: { - enabled: false, - }, - template: { - opened: true, - }, - }, - } ) - ).toEqual( { - inactivePanels: [ 'permalinks', 'excerpt', 'discussion' ], - openPanels: [ 'tags', 'excerpt', 'template' ], - } ); - } ); - - it( 'returns empty arrays when there is no data to convert', () => { - expect( convertEditPostPanels( {} ) ).toEqual( { - inactivePanels: [], - openPanels: [], - } ); - } ); -} ); - -describe( 'migrateInterfaceEnableItemsToPreferencesStore', () => { - it( 'migrates enableItems to the preferences store', () => { - const persistenceInterface = createPersistenceInterface( { - storageKey: 'test-username', - } ); - - persistenceInterface.set( 'core/interface', { - enableItems: { - singleEnableItems: { - complementaryArea: { - 'core/edit-post': 'edit-post/document', - 'core/edit-site': 'edit-site/global-styles', - }, - }, - multipleEnableItems: { - pinnedItems: { - 'core/edit-post': { - 'plugin-1': true, - }, - 'core/edit-site': { - 'plugin-2': true, - }, - }, - }, - }, - } ); - - migrateInterfaceEnableItemsToPreferencesStore( persistenceInterface ); - - expect( persistenceInterface.get() ).toEqual( { - 'core/preferences': { - preferences: { - 'core/edit-post': { - complementaryArea: 'edit-post/document', - pinnedItems: { - 'plugin-1': true, - }, - }, - 'core/edit-site': { - complementaryArea: 'edit-site/global-styles', - pinnedItems: { - 'plugin-2': true, - }, - }, - }, - }, - 'core/interface': { - enableItems: undefined, - }, - } ); - } ); - - it( 'merges pinnedItems and complementaryAreas with existing preferences store data', () => { - const persistenceInterface = createPersistenceInterface( { - storageKey: 'test-username', - } ); - - persistenceInterface.set( 'core/interface', { - enableItems: { - singleEnableItems: { - complementaryArea: { - 'core/edit-post': 'edit-post/document', - 'core/edit-site': 'edit-site/global-styles', - }, - }, - multipleEnableItems: { - pinnedItems: { - 'core/edit-post': { - 'plugin-1': true, - }, - 'core/edit-site': { - 'plugin-2': true, - }, - }, - }, - }, - } ); - - persistenceInterface.set( 'core/preferences', { - preferences: { - 'core/edit-post': { - preferenceA: 1, - preferenceB: 2, - }, - 'core/edit-site': { - preferenceC: true, - }, - }, - } ); - - migrateInterfaceEnableItemsToPreferencesStore( persistenceInterface ); - - expect( persistenceInterface.get() ).toEqual( { - 'core/preferences': { - preferences: { - 'core/edit-post': { - preferenceA: 1, - preferenceB: 2, - complementaryArea: 'edit-post/document', - pinnedItems: { - 'plugin-1': true, - }, - }, - 'core/edit-site': { - preferenceC: true, - complementaryArea: 'edit-site/global-styles', - pinnedItems: { - 'plugin-2': true, - }, - }, - }, - }, - 'core/interface': { - enableItems: undefined, - }, - } ); - } ); -} ); From feedc80f22383f5d9cd45cbdd41a304a840375ed Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 8 Apr 2022 14:25:51 +0800 Subject: [PATCH 49/74] Add some docs for setPersistenceLayer --- packages/preferences/README.md | 75 +++++++++++++++++++++-- packages/preferences/src/store/actions.js | 21 ++++++- 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/packages/preferences/README.md b/packages/preferences/README.md index 333d04af6d7e9b..8e7a354b0631ce 100644 --- a/packages/preferences/README.md +++ b/packages/preferences/README.md @@ -1,6 +1,6 @@ # Preferences -Utilities for storing WordPress preferences. +A key/value store for application preferences. ## Installation @@ -16,7 +16,7 @@ _This package assumes that your code will run in an **ES2015+** environment. If ### Data store -Preferences are persisted values of any kind. +Preferences can a value of any JSON serializable type. Set the default preferences for any features on initialization by dispatching an action: @@ -38,7 +38,7 @@ function initialize() { } ``` -Or the `get` selector to get a preference value, and the `set` action to update a preference to any value: +Use the `get` selector to get a preference value, and the `set` action to update a preference: ```js wp.data @@ -66,6 +66,60 @@ wp.data .get( 'namespace/editor-or-plugin-name', 'myPreferenceName' ); // false ``` +#### Setting up a persistence layer + +By default, this package only stores values in-memory. But it can be configured to persist preferences to browser storage or a database via an optional persistence layer. + +Use the `setPersistenceLayer` action to configure how the store persists its preference values. + +```js +wp.data.dispatch( 'core/preferences' ).setPersistenceLayer( { + // `get` is asynchronous to support persisting preferences using a REST API. + // it will immediately be called by `setPersistenceLayer` and the returned + // value used as the initial state of the preferences. + async get() { + return JSON.parse( window.localStorage.getItem( 'MY_PREFERENCES' ) ); + }, + + // `set` is synchronous. It's ok to use asynchronous code, but the + // preferences store won't wait for a promise to resolve, the function is + // 'fire and forget'. + set( preferences ) { + window.localStorage.setItem( + 'MY_PREFERENCES', + JSON.stringify( preferences ) + ); + }, +} ); +``` + +For application that persist data to an asynchronous API, a concern will be that loading preferences can lead to slower application start up. + +A recommendation is to pre-load any persistence layer data and keep it in a local cache particularly if you're using an asynchronous API to persist data. + +While `get` is only called currently when `setPersistenceLayer` is triggered. + +```js +// Preloaded data from the server. +let cache = preloadedData; +wp.data.dispatch( 'core/preferences' ).setPersistenceLayer( { + async get() { + if ( cache ) { + return cache; + } + + // Call to a made-up async API. + return await api.preferences.get(); + }, + set( preferences ) { + cache = preferences; + api.preferences.set( { data: preferences } ); + }, +} ); +``` + +See the `@wordpress/database-persistence-layer` package for a reference implementation. + ### Components The `PreferenceToggleMenuItem` components can be used with a `DropdownMenu` to implement a menu for changing preferences. @@ -134,9 +188,22 @@ _Returns_ Sets the persistence layer. +When a persistence layer is set, the preferences store will: + +- call `get` immediately and update the store state to the value returned. +- call `set` with all preferences whenever a preference changes value. + +`setPersistenceLayer` should ideally be dispatched at the start of an +application's lifecycle, before any other actions have been dispatched to +the preferences store. + _Parameters_ -- _persistenceLayer_ `Object`: Sets the persistence layer. +- _persistenceLayer_ `WPPreferencesPersistenceLayer`: The persistence layer. + +_Returns_ + +- `Object`: Action object. #### toggle diff --git a/packages/preferences/src/store/actions.js b/packages/preferences/src/store/actions.js index 2b709e10b2c780..7fcedd49d35297 100644 --- a/packages/preferences/src/store/actions.js +++ b/packages/preferences/src/store/actions.js @@ -48,10 +48,29 @@ export function setDefaults( scope, defaults ) { }; } +/** @typedef {() => Promise} WPPreferencesPersistenceLayerGet */ +/** @typedef {(*) => void} WPPreferencesPersistenceLayerSet */ +/** + * @typedef WPPreferencesPersistenceLayer + * + * @property {WPPreferencesPersistenceLayerGet} get An async function that gets data from the persistence layer. + * @property {WPPreferencesPersistenceLayerSet} set A function that sets data in the persistence layer. + */ + /** * Sets the persistence layer. * - * @param {Object} persistenceLayer Sets the persistence layer. + * When a persistence layer is set, the preferences store will: + * - call `get` immediately and update the store state to the value returned. + * - call `set` with all preferences whenever a preference changes value. + * + * `setPersistenceLayer` should ideally be dispatched at the start of an + * application's lifecycle, before any other actions have been dispatched to + * the preferences store. + * + * @param {WPPreferencesPersistenceLayer} persistenceLayer The persistence layer. + * + * @return {Object} Action object. */ export async function setPersistenceLayer( persistenceLayer ) { const persistedData = await persistenceLayer.get(); From 65d9de513173bc7f89121e5538acff7fb76d3923 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 8 Apr 2022 15:08:12 +0800 Subject: [PATCH 50/74] Update database persistence layer docs --- packages/database-persistence-layer/README.md | 57 ++++++++++++++++++- .../src/create/index.js | 23 +++++--- 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/packages/database-persistence-layer/README.md b/packages/database-persistence-layer/README.md index e6510baf769952..43cbe91067cba5 100644 --- a/packages/database-persistence-layer/README.md +++ b/packages/database-persistence-layer/README.md @@ -1,3 +1,58 @@ # Database persistence layer -A persistence layer for `@wordpress/preferences` that stores data in the WordPress user meta. +A persistence layer for `@wordpress/preferences` that stores user preferences in a WordPress database as user meta. + +If for any reason data cannot be saved to the database, this persistence layer also uses local storage as a fallback. + +## Installation + +Install the module + +```bash +npm install @wordpress/database-persistence-layer --save +``` + +_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code._ + +## Usage + +Call the `create` function to create a persistence layer. + +```js +const persistenceLayer = create(); +``` + +Next, configure the preferences package to use this persistence layer: + +```js +wp.data( 'core/preferences' ).setPersistenceLayer( persistenceLayer ); +``` + +## Reference + + + +### create + +Creates a database persistence layer, storing data in WordPress user meta. + +_Parameters_ + +- _options_ `Object`: +- _options.preloadedData_ `?Object`: Any persisted preferences data that should be preloaded. When set, the persistence layer will avoid fetching data from the REST API. +- _options.localStorageRestoreKey_ `?string`: The key to use for restoring the localStorage backup, used when the persistence layer calls `localStorage.getItem` or `localStorage.setItem`. +- _options.requestDebounceMS_ `?number`: Debounce requests to the API so that they only occur at minimum every `requestDebounceMS` milliseconds, and don't swamp the server. Defaults to 2500ms. + +_Returns_ + +- `Object`: A database persistence layer. + + + +## Contributing to this package + +This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. + +To find out more about contributing to this package or Gutenberg as a whole, please read the project's main [contributor guide](https://github.com/WordPress/gutenberg/tree/HEAD/CONTRIBUTING.md). + +

Code is Poetry.

diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index be0de8c1913e72..1c1c5eee33ad04 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -12,19 +12,24 @@ const EMPTY_OBJECT = {}; const localStorage = window.localStorage; /** - * Creates a database persistence layer, storing data in the user meta. + * Creates a database persistence layer, storing data in WordPress user meta. * - * @param {Object} options - * @param {Object} options.preloadedData Any persisted data that should be preloaded. - * @param {number} options.requestDebounceMS Debounce requests to the API so that they only occur - * don't swamp the server. - * @param {string} options.localStorageRestoreKey The key to use for restoring the localStorage backup. + * @param {Object} options + * @param {?Object} options.preloadedData Any persisted preferences data that should be preloaded. + * When set, the persistence layer will avoid fetching data + * from the REST API. + * @param {?string} options.localStorageRestoreKey The key to use for restoring the localStorage backup, used + * when the persistence layer calls `localStorage.getItem` or + * `localStorage.setItem`. + * @param {?number} options.requestDebounceMS Debounce requests to the API so that they only occur at + * minimum every `requestDebounceMS` milliseconds, and don't + * swamp the server. Defaults to 2500ms. * * @return {Object} A database persistence layer. */ export default function create( { - localStorageRestoreKey, preloadedData, + localStorageRestoreKey = 'WP_PREFERENCES_RESTORE_DATA', requestDebounceMS = 2500, } = {} ) { let cache = preloadedData; @@ -64,8 +69,8 @@ export default function create( { cache = dataWithTimestamp; // Store data in local storage as a fallback. If for some reason the - // api request does not complete, this data can be used to restore - // preferences. + // api request does not complete or becomes unavailable, this data + // can be used to restore preferences. localStorage.setItem( localStorageRestoreKey, JSON.stringify( dataWithTimestamp ) From 32ffcf2b922acf20d79369f88e5f412760b6c17c Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 11 Apr 2022 13:13:11 +0800 Subject: [PATCH 51/74] Add preferences package changelog entries --- packages/preferences/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/preferences/CHANGELOG.md b/packages/preferences/CHANGELOG.md index 853787086b6818..3e52410949c968 100644 --- a/packages/preferences/CHANGELOG.md +++ b/packages/preferences/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Breaking change + +- The preferences package is no longer compatible with the `@wordpress/data` persistence plugin. Please use the new `setPersistenceLayer` API. ([#39795](https://github.com/WordPress/gutenberg/pull/39795)) + +### Enhancement + +- A new `setPersistenceLayer` action has been introduced. ([#39795](https://github.com/WordPress/gutenberg/pull/39795)) + ## 1.3.0 (2022-04-21) ## 1.2.0 (2022-04-08) From ccff85e85e7e85a1eb1ba73bf43c320eded41a9d Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 11 Apr 2022 13:14:15 +0800 Subject: [PATCH 52/74] Remove unused local storage persistence folder --- packages/preferences/src/index.js | 1 - .../src/local-storage-persistence/index.js | 55 ------------------- 2 files changed, 56 deletions(-) delete mode 100644 packages/preferences/src/local-storage-persistence/index.js diff --git a/packages/preferences/src/index.js b/packages/preferences/src/index.js index c8e119e73c8f60..72531a0824c178 100644 --- a/packages/preferences/src/index.js +++ b/packages/preferences/src/index.js @@ -1,3 +1,2 @@ export * from './components'; export { store } from './store'; -export { default as createLocalStoragePersistenceLayer } from './local-storage-persistence'; diff --git a/packages/preferences/src/local-storage-persistence/index.js b/packages/preferences/src/local-storage-persistence/index.js deleted file mode 100644 index 91b07583fb5b8c..00000000000000 --- a/packages/preferences/src/local-storage-persistence/index.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Default plugin storage key. - * - * @type {string} - */ -const DEFAULT_STORAGE_KEY = 'PREFERENCES_DATA'; -const EMPTY_OBJECT = {}; - -export default function createLocalStoragePersistenceLayer( { - storageKey = DEFAULT_STORAGE_KEY, -} = {} ) { - const storage = window.localStorage; - - /** - * Returns the persisted data as an object, defaulting to an empty object. - * - * @return {Object} Persisted data. - */ - async function get() { - // If unset, getItem is expected to return null. Fall back to - // empty object. - const persisted = storage.getItem( storageKey ); - - if ( persisted === null ) { - return EMPTY_OBJECT; - } - - let data; - - try { - data = JSON.parse( persisted ); - } catch ( error ) { - // Similarly, should any error be thrown during parse of - // the string (malformed JSON), fall back to empty object. - data = EMPTY_OBJECT; - } - - return data; - } - - /** - * Merges an updated reducer state into the persisted data. - * - * @param {Object} newData The data to persist. - */ - function set( newData ) { - const data = { ...newData }; - storage.setItem( storageKey, JSON.stringify( data ) ); - } - - return { - get, - set, - }; -} From b77d4bcf1e519f292efde5edf5e7fe27191c5011 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 11 Apr 2022 14:01:04 +0800 Subject: [PATCH 53/74] Remove unused local storage persistence layer --- .../src/migrations/legacy-local-storage-data/test/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js index ea86cce59bf835..a58da11a94dde0 100644 --- a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js +++ b/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js @@ -3,7 +3,7 @@ */ import { convertLegacyData } from '..'; -const legacyData1 = { +const legacyData = { 'core/interface': { enableItems: { singleEnableItems: { @@ -136,7 +136,7 @@ const alreadyConvertedData = { describe( 'convertLegacyData', () => { it( 'converts to the expected format', () => { - expect( convertLegacyData( legacyData1 ) ).toMatchInlineSnapshot( ` + expect( convertLegacyData( legacyData ) ).toMatchInlineSnapshot( ` Object { "core/customize-widgets": Object { "fixedToolbar": true, From 900090199bde707bfbc090ce7a87130fc6787ff1 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 11 Apr 2022 14:47:38 +0800 Subject: [PATCH 54/74] Add unit tests to preferences store --- packages/preferences/src/store/reducer.js | 2 +- .../preferences/src/store/test/actions.js | 27 +++++++++++ .../preferences/src/store/test/reducer.js | 47 +++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 packages/preferences/src/store/test/actions.js create mode 100644 packages/preferences/src/store/test/reducer.js diff --git a/packages/preferences/src/store/reducer.js b/packages/preferences/src/store/reducer.js index ae108cbbab6c5f..0d2f463ab01de3 100644 --- a/packages/preferences/src/store/reducer.js +++ b/packages/preferences/src/store/reducer.js @@ -68,7 +68,7 @@ function withPersistenceLayer( reducer ) { * * @return {Object} Updated state. */ -const preferences = withPersistenceLayer( ( state = {}, action ) => { +export const preferences = withPersistenceLayer( ( state = {}, action ) => { if ( action.type === 'SET_PREFERENCE_VALUE' ) { const { scope, name, value } = action; return { diff --git a/packages/preferences/src/store/test/actions.js b/packages/preferences/src/store/test/actions.js new file mode 100644 index 00000000000000..493210210a53ef --- /dev/null +++ b/packages/preferences/src/store/test/actions.js @@ -0,0 +1,27 @@ +/** + * Internal dependencies + */ +import { setPersistenceLayer } from '../actions'; + +describe( 'setPersistenceLayer', () => { + it( 'returns an action that contains the persistence layer and the result of calling `persistenceLayer.get`', async () => { + const result = { + testA: 1, + testB: 2, + }; + const testPersistenceLayer = { + async get() { + return result; + }, + set() {}, + }; + + const action = await setPersistenceLayer( testPersistenceLayer ); + + expect( action ).toEqual( { + type: 'SET_PERSISTENCE_LAYER', + persistenceLayer: testPersistenceLayer, + persistedData: result, + } ); + } ); +} ); diff --git a/packages/preferences/src/store/test/reducer.js b/packages/preferences/src/store/test/reducer.js new file mode 100644 index 00000000000000..985959acb2f000 --- /dev/null +++ b/packages/preferences/src/store/test/reducer.js @@ -0,0 +1,47 @@ +/** + * Internal dependencies + */ +import { preferences } from '../reducer'; + +describe( 'withPersistenceLayer( preferences )', () => { + it( 'updates the store state to the persisted data when called with the `SET_PERSISTENCE_LAYER` action', () => { + const persistedData = { + a: 1, + b: 2, + }; + + const action = { + type: 'SET_PERSISTENCE_LAYER', + persistedData, + }; + + expect( preferences( {}, action ) ).toEqual( persistedData ); + } ); + + it( 'calls the persistence layer `set` function with the updated store state whenever the `SET_PREFERENCE_VALUE` action is dispatched', () => { + const set = jest.fn(); + const persistenceLayer = { + set, + }; + + const setPersistenceLayerAction = { + type: 'SET_PERSISTENCE_LAYER', + persistenceLayer, + persistedData: {}, + }; + + // Set the persistence layer. + preferences( {}, setPersistenceLayerAction ); + + // Update a value. + const setPreferenceValueAction = { + type: 'SET_PREFERENCE_VALUE', + name: 'myPreference', + value: 'myValue', + }; + + const state = preferences( {}, setPreferenceValueAction ); + + expect( set ).toHaveBeenCalledWith( state ); + } ); +} ); From 5ff88d5108066c2252236933b2e5e0dc571bbbbe Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 11 Apr 2022 17:29:15 +0800 Subject: [PATCH 55/74] Add tests for persistence layer --- .../src/create/test/index.js | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 packages/database-persistence-layer/src/create/test/index.js diff --git a/packages/database-persistence-layer/src/create/test/index.js b/packages/database-persistence-layer/src/create/test/index.js new file mode 100644 index 00000000000000..c3ca35dccc8786 --- /dev/null +++ b/packages/database-persistence-layer/src/create/test/index.js @@ -0,0 +1,178 @@ +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import create from '..'; + +jest.mock( '@wordpress/api-fetch' ); + +describe( 'create', () => { + afterEach( () => { + apiFetch.mockReset(); + } ); + + describe( 'set', () => { + it( 'stores backup restoration data in localStorage', () => { + apiFetch.mockResolvedValueOnce(); + const spy = jest.spyOn( global.Storage.prototype, 'setItem' ); + + const localStorageRestoreKey = 'test'; + const { set } = create( { localStorageRestoreKey } ); + + const data = { test: 1 }; + set( data ); + + expect( spy ).toHaveBeenCalledWith( + localStorageRestoreKey, + expect.any( String ) + ); + + // The second param of the call to `setItem` has been JSON.stringified. + // Parse it to check it contains the data. + const setItemDataParm = spy.mock.calls[ 0 ][ 1 ]; + expect( JSON.parse( setItemDataParm ) ).toEqual( + expect.objectContaining( data ) + ); + } ); + + it( 'sends data to the `users/me` endpoint', () => { + apiFetch.mockResolvedValueOnce(); + + const { set } = create(); + + const data = { test: 1 }; + set( data ); + + expect( apiFetch ).toHaveBeenCalledWith( { + path: '/wp/v2/users/me', + method: 'PUT', + keepalive: true, + data: { + meta: { + persisted_preferences: expect.objectContaining( data ), + }, + }, + } ); + } ); + } ); + + describe( 'get', () => { + it( 'avoids using the REST API or local storage when data is preloaded', async () => { + const getItemSpy = jest.spyOn( + global.Storage.prototype, + 'getItem' + ); + + const preloadedData = { preloaded: true }; + const { get } = create( { preloadedData } ); + expect( await get() ).toBe( preloadedData ); + expect( getItemSpy ).not.toHaveBeenCalled(); + expect( apiFetch ).not.toHaveBeenCalled(); + } ); + + it( 'returns from a local cache once `set` has been called', async () => { + const getItemSpy = jest.spyOn( + global.Storage.prototype, + 'getItem' + ); + apiFetch.mockResolvedValueOnce(); + + const data = { cached: true }; + const { get, set } = create(); + + // apiFetch was called as a result of calling `set`. + set( data ); + expect( apiFetch ).toHaveBeenCalled(); + apiFetch.mockClear(); + + // Neither localStorage.getItem or apiFetch are called as a result + // of the call to `get`. A local cache is used. + expect( await get() ).toEqual( expect.objectContaining( data ) ); + expect( getItemSpy ).not.toHaveBeenCalled(); + expect( apiFetch ).not.toHaveBeenCalled(); + } ); + + it( 'returns data from the users/me endpoint if there is no data in localStorage', async () => { + const data = { + __timestamp: 0, + test: 2, + }; + apiFetch.mockResolvedValueOnce( { + meta: { persisted_preferences: data }, + } ); + + jest.spyOn( + global.Storage.prototype, + 'getItem' + ).mockReturnValueOnce( 'null' ); + + const { get } = create(); + expect( await get() ).toEqual( data ); + } ); + + it( 'returns data from the REST API if it has a more recent timestamp than localStorage', async () => { + const data = { + __timestamp: 1, + test: 'api', + }; + apiFetch.mockResolvedValueOnce( { + meta: { persisted_preferences: data }, + } ); + + jest.spyOn( + global.Storage.prototype, + 'getItem' + ).mockReturnValueOnce( + JSON.stringify( { + __timestamp: 0, + test: 'localStorage', + } ) + ); + + const { get } = create(); + expect( await get() ).toEqual( data ); + } ); + + it( 'returns data from localStorage if it has a more recent timestamp than data from the REST API', async () => { + apiFetch.mockResolvedValueOnce( { + meta: { + persisted_preferences: { + __timestamp: 0, + test: 'api', + }, + }, + } ); + + const data = { + __timestamp: 1, + test: 'localStorage', + }; + jest.spyOn( + global.Storage.prototype, + 'getItem' + ).mockReturnValueOnce( JSON.stringify( data ) ); + + const { get } = create(); + expect( await get() ).toEqual( data ); + } ); + + it( 'returns an empty object if neither local storage or the REST API return any data', async () => { + apiFetch.mockResolvedValueOnce( { + meta: { + persisted_preferences: null, + }, + } ); + jest.spyOn( + global.Storage.prototype, + 'getItem' + ).mockReturnValueOnce( 'null' ); + + const { get } = create(); + expect( await get() ).toEqual( {} ); + } ); + } ); +} ); From 62e7520e57705d1ba896aac04f43fb24372ba776 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 11 Apr 2022 17:29:32 +0800 Subject: [PATCH 56/74] Handle equality when comparing timestamps --- packages/database-persistence-layer/src/create/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index 1c1c5eee33ad04..154ff302cba557 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -53,7 +53,7 @@ export default function create( { // Prefer server data if it exists and is more recent. // Otherwise fallback to localStorage data. - if ( serverData && serverTimestamp > localTimestamp ) { + if ( serverData && serverTimestamp >= localTimestamp ) { cache = serverData; } else if ( localData ) { cache = localData; From a56ca8372263b1f917b58c47f83070d9f85c2023 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 12 Apr 2022 11:57:39 +0800 Subject: [PATCH 57/74] Improve the preferences README further --- packages/preferences/README.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/preferences/README.md b/packages/preferences/README.md index 8e7a354b0631ce..89fc8f9c2ff6bc 100644 --- a/packages/preferences/README.md +++ b/packages/preferences/README.md @@ -12,12 +12,28 @@ npm install @wordpress/preferences --save _This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code._ +## Key concepts + +### Scope + +Many API calls require a 'scope' parameter that acts like a namespace. If you have multiple parameters with the same key but they apply to different parts of your application, using scopes is the best way to segregate them. + +### Key + +Each preference is set against a key that should be a string. + +### Value + +Values can be of any type, but the types supported may be limited by the persistence layer configure. For example of preferences are saved to browser localStorage in JSON format, only JSON serializable types should be used. + +### Defaults + +Defaults are the value returned when a preference is `undefined`. These are not persisted, they are only kept in memory. They should be during the initialization of an application. + ## Examples ### Data store -Preferences can a value of any JSON serializable type. - Set the default preferences for any features on initialization by dispatching an action: ```js From cde874f9663035688f0a3de96958df1322de415c Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 12 Apr 2022 12:12:10 +0800 Subject: [PATCH 58/74] Improve docs --- packages/preferences/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/preferences/README.md b/packages/preferences/README.md index 89fc8f9c2ff6bc..4cc7fba31488ec 100644 --- a/packages/preferences/README.md +++ b/packages/preferences/README.md @@ -109,11 +109,11 @@ wp.data.dispatch( 'core/preferences' ).setPersistenceLayer( { } ); ``` -For application that persist data to an asynchronous API, a concern will be that loading preferences can lead to slower application start up. +For application that persist data to an asynchronous API, a concern is that loading preferences can lead to slower application start up. A recommendation is to pre-load any persistence layer data and keep it in a local cache particularly if you're using an asynchronous API to persist data. -While `get` is only called currently when `setPersistenceLayer` is triggered. +Note: currently `get` is called only when `setPersistenceLayer` is triggered. This may change in the future, so it's sensible to optimize `get` using a local cache, as shown in the example below. ```js // Preloaded data from the server. @@ -134,8 +134,6 @@ wp.data.dispatch( 'core/preferences' ).setPersistenceLayer( { } ); ``` -See the `@wordpress/database-persistence-layer` package for a reference implementation. - ### Components The `PreferenceToggleMenuItem` components can be used with a `DropdownMenu` to implement a menu for changing preferences. From 95c96e27d73d0953422607d8c627d4a6f9f0aa0e Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 12 Apr 2022 13:21:10 +0800 Subject: [PATCH 59/74] Move php code into wordpress-6.1 folder --- .../wordpress-6.1}/persisted-preferences.php | 0 lib/load.php | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/{experimental => compat/wordpress-6.1}/persisted-preferences.php (100%) diff --git a/lib/experimental/persisted-preferences.php b/lib/compat/wordpress-6.1/persisted-preferences.php similarity index 100% rename from lib/experimental/persisted-preferences.php rename to lib/compat/wordpress-6.1/persisted-preferences.php diff --git a/lib/load.php b/lib/load.php index c607508265f117..3c08f8dc3b46b4 100644 --- a/lib/load.php +++ b/lib/load.php @@ -124,6 +124,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.1 compat. require __DIR__ . '/compat/wordpress-6.1/blocks.php'; +require __DIR__ . '/compat/wordpress-6.1/persisted-preferences.php'; // Experimental features. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; @@ -134,7 +135,6 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/class-wp-webfonts-provider-local.php'; require __DIR__ . '/experimental/webfonts.php'; require __DIR__ . '/experimental/blocks.php'; -require __DIR__ . '/experimental/persisted-preferences.php'; require __DIR__ . '/experimental/navigation-theme-opt-in.php'; require __DIR__ . '/experimental/navigation-page.php'; From f81418adcfad4d60858fd1badf4af64db485e484 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 13 Apr 2022 11:59:23 +0800 Subject: [PATCH 60/74] Reset preferences when running playwright tests --- .../src/request/index.ts | 2 ++ .../src/request/preferences.ts | 21 +++++++++++++++++++ .../e2e-test-utils-playwright/src/test.ts | 1 + 3 files changed, 24 insertions(+) create mode 100644 packages/e2e-test-utils-playwright/src/request/preferences.ts diff --git a/packages/e2e-test-utils-playwright/src/request/index.ts b/packages/e2e-test-utils-playwright/src/request/index.ts index 28d08220b8d25a..31a2df63448bd2 100644 --- a/packages/e2e-test-utils-playwright/src/request/index.ts +++ b/packages/e2e-test-utils-playwright/src/request/index.ts @@ -18,6 +18,7 @@ import { deleteAllTemplates } from './templates'; import { activateTheme } from './themes'; import { deleteAllBlocks } from './blocks'; import { deleteAllPosts } from './posts'; +import { resetPreferences } from './preferences'; import { deleteAllWidgets, addWidgetBlock } from './widgets'; interface StorageState { @@ -120,6 +121,7 @@ class RequestUtils { deleteAllWidgets = deleteAllWidgets; addWidgetBlock = addWidgetBlock; deleteAllTemplates = deleteAllTemplates; + resetPreferences = resetPreferences; } export type { StorageState }; diff --git a/packages/e2e-test-utils-playwright/src/request/preferences.ts b/packages/e2e-test-utils-playwright/src/request/preferences.ts new file mode 100644 index 00000000000000..9696803a6beab5 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/request/preferences.ts @@ -0,0 +1,21 @@ +/** + * Internal dependencies + */ +import type { RequestUtils } from './index'; + +/** + * Reset user preferences + * + * @param {this} this Request utils. + */ +export async function resetPreferences( this: RequestUtils ) { + await this.rest( { + path: '/wp/v2/users/me', + method: 'PUT', + data: { + meta: { + persisted_preferences: {}, + }, + }, + } ); +} diff --git a/packages/e2e-test-utils-playwright/src/test.ts b/packages/e2e-test-utils-playwright/src/test.ts index c01a150602ef13..b6867cad21629f 100644 --- a/packages/e2e-test-utils-playwright/src/test.ts +++ b/packages/e2e-test-utils-playwright/src/test.ts @@ -131,6 +131,7 @@ const test = base.extend< requestUtils.activateTheme( 'twentytwentyone' ), requestUtils.deleteAllPosts(), requestUtils.deleteAllBlocks(), + requestUtils.resetPreferences(), ] ); await use( requestUtils ); From 5d18fa759610f9a0bb4b18fea48753e7912c700c Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 21 Apr 2022 17:13:47 +0800 Subject: [PATCH 61/74] Fix typo Co-authored-by: Robert Anderson --- packages/preferences/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preferences/README.md b/packages/preferences/README.md index 4cc7fba31488ec..74e27f9dc7c8b4 100644 --- a/packages/preferences/README.md +++ b/packages/preferences/README.md @@ -24,7 +24,7 @@ Each preference is set against a key that should be a string. ### Value -Values can be of any type, but the types supported may be limited by the persistence layer configure. For example of preferences are saved to browser localStorage in JSON format, only JSON serializable types should be used. +Values can be of any type, but the types supported may be limited by the persistence layer configure. For example if preferences are saved to browser localStorage in JSON format, only JSON serializable types should be used. ### Defaults From 34c7c95b143b9198742d0c646609c92b1a7fa7d9 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 21 Apr 2022 12:08:55 +0800 Subject: [PATCH 62/74] Always register user meta --- lib/compat/wordpress-6.1/persisted-preferences.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/compat/wordpress-6.1/persisted-preferences.php b/lib/compat/wordpress-6.1/persisted-preferences.php index 3d5d2d9c3d9d32..48e93aabd238e3 100644 --- a/lib/compat/wordpress-6.1/persisted-preferences.php +++ b/lib/compat/wordpress-6.1/persisted-preferences.php @@ -9,11 +9,6 @@ * Register the user meta for persisted preferences. */ function gutenberg_configure_persisted_preferences() { - $user_id = get_current_user_id(); - if ( empty( $user_id ) ) { - return; - } - global $wpdb; $meta_key = $wpdb->get_blog_prefix() . 'persisted_preferences'; @@ -35,6 +30,11 @@ function gutenberg_configure_persisted_preferences() { ) ); + $user_id = get_current_user_id(); + if ( empty( $user_id ) ) { + return; + } + $preload_data = get_user_meta( $user_id, $meta_key, true ); wp_add_inline_script( From b985c1b06fe0d306c6e73a1c43e5dfad22fc1128 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 21 Apr 2022 15:35:05 +0800 Subject: [PATCH 63/74] Use an IIFE for the inline script and and convert to old school JS --- .../wordpress-6.1/persisted-preferences.php | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/compat/wordpress-6.1/persisted-preferences.php b/lib/compat/wordpress-6.1/persisted-preferences.php index 48e93aabd238e3..7ccbe26cdba9c3 100644 --- a/lib/compat/wordpress-6.1/persisted-preferences.php +++ b/lib/compat/wordpress-6.1/persisted-preferences.php @@ -40,30 +40,35 @@ function gutenberg_configure_persisted_preferences() { wp_add_inline_script( 'wp-preferences', sprintf( - 'const serverData = %s; - const userId = "%s"; - const localStorageRestoreKey = `WP_PREFERENCES_USER_${ userId }`; - const localData = JSON.parse( - localStorage.getItem( localStorageRestoreKey ) - ); - const serverTimestamp = serverData?.__timestamp ?? 0; - const localTimestamp = localData?.__timestamp ?? 0; + '( function() { + var serverData = %s; + var userId = "%s"; + var localStorageRestoreKey = "WP_PREFERENCES_USER_" + userId; + var localData = JSON.parse( + localStorage.getItem( localStorageRestoreKey ) + ); + var serverTimestamp = + serverData && serverData.__timestamp ? serverData?.__timestamp : 0; + var localTimestamp = + localData && localData.__timestamp ? localData.__timestamp : 0; - let preloadedData; - if ( serverData && serverTimestamp > localTimestamp ) { - preloadedData = serverData; - } else if ( localData ) { - preloadedData = localData; - } else { - // Check if there is data in the legacy format from the old persistence system. - const { __unstableConvertLegacyLocalStorageData } = wp.databasePersistenceLayer; - preloadedData = __unstableConvertLegacyLocalStorageData( userId ); - } + var preloadedData; + if ( serverData && serverTimestamp > localTimestamp ) { + preloadedData = serverData; + } else if ( localData ) { + preloadedData = localData; + } else { + // Check if there is data in the legacy format from the old persistence system. + const convertLegacyLocalStorageData = + wp.databasePersistenceLayer.__unstableConvertLegacyLocalStorageData; + preloadedData = convertLegacyLocalStorageData( userId ); + } - const { create } = wp.databasePersistenceLayer; - const persistenceLayer = create( { preloadedData, localStorageRestoreKey } ); - const { store: preferencesStore } = wp.preferences; - wp.data.dispatch( "core/preferences" ).setPersistenceLayer( persistenceLayer );', + var create = wp.databasePersistenceLayer.create; + var persistenceLayer = create( { preloadedData, localStorageRestoreKey } ); + var preferencesStore = wp.preferences.store; + wp.data.dispatch( "core/preferences" ).setPersistenceLayer( persistenceLayer ); + } ) ();', wp_json_encode( $preload_data ), $user_id ), From 9d7871b5843e3f8b96b63bc4559ebd4e3a44adc4 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 21 Apr 2022 15:51:13 +0800 Subject: [PATCH 64/74] Use edit context --- lib/compat/wordpress-6.1/persisted-preferences.php | 8 ++++---- packages/database-persistence-layer/src/create/index.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/compat/wordpress-6.1/persisted-preferences.php b/lib/compat/wordpress-6.1/persisted-preferences.php index 7ccbe26cdba9c3..179b08a619442f 100644 --- a/lib/compat/wordpress-6.1/persisted-preferences.php +++ b/lib/compat/wordpress-6.1/persisted-preferences.php @@ -10,7 +10,6 @@ */ function gutenberg_configure_persisted_preferences() { global $wpdb; - $meta_key = $wpdb->get_blog_prefix() . 'persisted_preferences'; register_meta( 'user', @@ -19,9 +18,10 @@ function gutenberg_configure_persisted_preferences() { 'type' => 'object', 'single' => true, 'show_in_rest' => array( - 'name' => 'persisted_preferences', - 'type' => 'object', - 'schema' => array( + 'name' => 'persisted_preferences', + 'type' => 'object', + 'context' => array( 'edit' ), + 'schema' => array( 'type' => 'object', 'properties' => array(), 'additionalProperties' => true, diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index 154ff302cba557..62926753d30ff0 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -41,7 +41,7 @@ export default function create( { } const user = await apiFetch( { - path: '/wp/v2/users/me', + path: '/wp/v2/users/me?context=edit', } ); const serverData = user?.meta?.persisted_preferences; From 01ad8d7ccb3b62f4246e9ff62221b4654cce052f Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 21 Apr 2022 16:46:12 +0800 Subject: [PATCH 65/74] Use ISO date for preferences and rename to modified (not a timestamp any more) --- .../wordpress-6.1/persisted-preferences.php | 23 ++++++++++++++----- .../src/create/index.js | 12 +++++++--- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/lib/compat/wordpress-6.1/persisted-preferences.php b/lib/compat/wordpress-6.1/persisted-preferences.php index 179b08a619442f..ef631f44bfcca0 100644 --- a/lib/compat/wordpress-6.1/persisted-preferences.php +++ b/lib/compat/wordpress-6.1/persisted-preferences.php @@ -23,7 +23,15 @@ function gutenberg_configure_persisted_preferences() { 'context' => array( 'edit' ), 'schema' => array( 'type' => 'object', - 'properties' => array(), + 'properties' => array( + '__modified' => array( + 'description' => __( 'The date and time the preferences were updated.', 'default' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'edit' ), + 'readonly' => true, + ), + ), 'additionalProperties' => true, ), ), @@ -47,13 +55,16 @@ function gutenberg_configure_persisted_preferences() { var localData = JSON.parse( localStorage.getItem( localStorageRestoreKey ) ); - var serverTimestamp = - serverData && serverData.__timestamp ? serverData?.__timestamp : 0; - var localTimestamp = - localData && localData.__timestamp ? localData.__timestamp : 0; + + // Date parse returns NaN for invalid input. Coerce anything invalid + // into a conveniently comparable zero. + var serverModified = + Date.parse( serverData && serverData.__modified ) || 0; + var localModified = + Date.parse( localData && localData.__modified ) || 0; var preloadedData; - if ( serverData && serverTimestamp > localTimestamp ) { + if ( serverData && serverModified >= localModified ) { preloadedData = serverData; } else if ( localData ) { preloadedData = localData; diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index 62926753d30ff0..34ee869244d0fb 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -48,8 +48,11 @@ export default function create( { const localData = JSON.parse( localStorage.getItem( localStorageRestoreKey ) ); - const serverTimestamp = serverData?.__timestamp ?? 0; - const localTimestamp = localData?.__timestamp ?? 0; + + // Date parse returns NaN for invalid input. Coerce anything invalid + // into a conveniently comparable zero. + const serverTimestamp = Date.parse( serverData?.__modified ) || 0; + const localTimestamp = Date.parse( localData?.__modified ) || 0; // Prefer server data if it exists and is more recent. // Otherwise fallback to localStorage data. @@ -65,7 +68,10 @@ export default function create( { } function set( newData ) { - const dataWithTimestamp = { ...newData, __timestamp: Date.now() }; + const dataWithTimestamp = { + ...newData, + __modified: new Date().toISOString(), + }; cache = dataWithTimestamp; // Store data in local storage as a fallback. If for some reason the From 287e6672f498a3d517766cea284c8fb23ad65b8f Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 21 Apr 2022 16:50:40 +0800 Subject: [PATCH 66/74] Add clarifying comment about blog prefix --- lib/compat/wordpress-6.1/persisted-preferences.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/compat/wordpress-6.1/persisted-preferences.php b/lib/compat/wordpress-6.1/persisted-preferences.php index ef631f44bfcca0..b5ccc26f5de15f 100644 --- a/lib/compat/wordpress-6.1/persisted-preferences.php +++ b/lib/compat/wordpress-6.1/persisted-preferences.php @@ -9,6 +9,8 @@ * Register the user meta for persisted preferences. */ function gutenberg_configure_persisted_preferences() { + // Create a meta key that incorporates the blog prefix so that each site + // on a multisite can have distinct user preferences. global $wpdb; $meta_key = $wpdb->get_blog_prefix() . 'persisted_preferences'; register_meta( From 769cbfab6857ab92fd2df4de609e4e6d01abe832 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 21 Apr 2022 17:53:50 +0800 Subject: [PATCH 67/74] Use only one underscore --- lib/compat/wordpress-6.1/persisted-preferences.php | 6 +++--- packages/database-persistence-layer/src/create/index.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/compat/wordpress-6.1/persisted-preferences.php b/lib/compat/wordpress-6.1/persisted-preferences.php index b5ccc26f5de15f..403dacc1328fff 100644 --- a/lib/compat/wordpress-6.1/persisted-preferences.php +++ b/lib/compat/wordpress-6.1/persisted-preferences.php @@ -26,7 +26,7 @@ function gutenberg_configure_persisted_preferences() { 'schema' => array( 'type' => 'object', 'properties' => array( - '__modified' => array( + '_modified' => array( 'description' => __( 'The date and time the preferences were updated.', 'default' ), 'type' => 'string', 'format' => 'date-time', @@ -61,9 +61,9 @@ function gutenberg_configure_persisted_preferences() { // Date parse returns NaN for invalid input. Coerce anything invalid // into a conveniently comparable zero. var serverModified = - Date.parse( serverData && serverData.__modified ) || 0; + Date.parse( serverData && serverData._modified ) || 0; var localModified = - Date.parse( localData && localData.__modified ) || 0; + Date.parse( localData && localData._modified ) || 0; var preloadedData; if ( serverData && serverModified >= localModified ) { diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index 34ee869244d0fb..f8521d91192678 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -51,8 +51,8 @@ export default function create( { // Date parse returns NaN for invalid input. Coerce anything invalid // into a conveniently comparable zero. - const serverTimestamp = Date.parse( serverData?.__modified ) || 0; - const localTimestamp = Date.parse( localData?.__modified ) || 0; + const serverTimestamp = Date.parse( serverData?._modified ) || 0; + const localTimestamp = Date.parse( localData?._modified ) || 0; // Prefer server data if it exists and is more recent. // Otherwise fallback to localStorage data. @@ -70,7 +70,7 @@ export default function create( { function set( newData ) { const dataWithTimestamp = { ...newData, - __modified: new Date().toISOString(), + _modified: new Date().toISOString(), }; cache = dataWithTimestamp; From 46471c2e904365816da39244c9af720dc85a0cb7 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 22 Apr 2022 14:10:14 +0800 Subject: [PATCH 68/74] Make asnync debounce more traditional --- ...te-async-debounce.js => debounce-async.js} | 12 ++++--- .../src/create/index.js | 34 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) rename packages/database-persistence-layer/src/create/{create-async-debounce.js => debounce-async.js} (80%) diff --git a/packages/database-persistence-layer/src/create/create-async-debounce.js b/packages/database-persistence-layer/src/create/debounce-async.js similarity index 80% rename from packages/database-persistence-layer/src/create/create-async-debounce.js rename to packages/database-persistence-layer/src/create/debounce-async.js index 202f5c2417155f..b1450e60381ba8 100644 --- a/packages/database-persistence-layer/src/create/create-async-debounce.js +++ b/packages/database-persistence-layer/src/create/debounce-async.js @@ -9,20 +9,22 @@ * This is distinct from `lodash.debounce` in that it waits for promise * resolution. * - * @param {number} delayMS A delay in milliseconds. + * @param {Function} func A function that returns a promise. + * @param {number} delayMS A delay in milliseconds. + * * @return {Function} A function that debounce whatever function is passed * to it. */ -export default function createAsyncDebounce( delayMS ) { +export default function debounceAsync( func, delayMS ) { let timeoutId; let activePromise; - return async function debounce( func ) { + return async function debounced( ...args ) { // This is a leading edge debounce. If there's no promise or timeout // in progress, if ( ! activePromise && ! timeoutId ) { // Keep a reference to the promise. - activePromise = func().finally( () => { + activePromise = func( ...args ).finally( () => { // As soon this promise is complete, clear the way for the // next one to happen immediately. activePromise = null; @@ -44,7 +46,7 @@ export default function createAsyncDebounce( delayMS ) { // Schedule the next request but with a delay. timeoutId = setTimeout( () => { - activePromise = func().finally( () => { + activePromise = func( ...args ).finally( () => { // As soon this promise is complete, clear the way for the // next one to happen immediately. activePromise = null; diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/database-persistence-layer/src/create/index.js index f8521d91192678..f9234d7e3b3ea1 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/database-persistence-layer/src/create/index.js @@ -6,7 +6,7 @@ import apiFetch from '@wordpress/api-fetch'; /** * Internal dependencies */ -import createAsyncDebounce from './create-async-debounce'; +import debounceAsync from './debounce-async'; const EMPTY_OBJECT = {}; const localStorage = window.localStorage; @@ -33,7 +33,7 @@ export default function create( { requestDebounceMS = 2500, } = {} ) { let cache = preloadedData; - const debounce = createAsyncDebounce( requestDebounceMS ); + const debouncedApiFetch = debounceAsync( apiFetch, requestDebounceMS ); async function get() { if ( cache ) { @@ -85,23 +85,21 @@ export default function create( { // The user meta endpoint seems susceptible to errors when consecutive // requests are made in quick succession. Ensure there's a gap between // any consecutive requests. - debounce( () => - apiFetch( { - path: '/wp/v2/users/me', - method: 'PUT', - // `keepalive` will still send the request in the background, - // even when a browser unload event might interrupt it. - // This should hopefully make things more resilient. - // This does have a size limit of 64kb, but the data is usually - // much less. - keepalive: true, - data: { - meta: { - persisted_preferences: dataWithTimestamp, - }, + debouncedApiFetch( { + path: '/wp/v2/users/me', + method: 'PUT', + // `keepalive` will still send the request in the background, + // even when a browser unload event might interrupt it. + // This should hopefully make things more resilient. + // This does have a size limit of 64kb, but the data is usually + // much less. + keepalive: true, + data: { + meta: { + persisted_preferences: dataWithTimestamp, }, - } ) - ); + }, + } ); } return { From 8fb8b96cc5eff5c1622d95b68d7cf46f9af5bc8f Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 22 Apr 2022 15:06:06 +0800 Subject: [PATCH 69/74] Write tests for debounce async function --- .../src/create/test/debounce-async.js | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 packages/database-persistence-layer/src/create/test/debounce-async.js diff --git a/packages/database-persistence-layer/src/create/test/debounce-async.js b/packages/database-persistence-layer/src/create/test/debounce-async.js new file mode 100644 index 00000000000000..6511cad2d4e1c1 --- /dev/null +++ b/packages/database-persistence-layer/src/create/test/debounce-async.js @@ -0,0 +1,63 @@ +/** + * Internal dependencies + */ +import debounceAsync from '../debounce-async'; + +describe( 'debounceAsync', () => { + function timeout( milliseconds ) { + return new Promise( ( resolve ) => + window.setTimeout( resolve, milliseconds ) + ); + } + + beforeAll( () => { + jest.useRealTimers(); + } ); + + afterAll( () => { + jest.useFakeTimers(); + } ); + + it( 'uses a leading debounce, the first call happens immediately', () => { + const fn = jest.fn( async () => {} ); + const debounced = debounceAsync( fn, 20 ); + debounced(); + expect( fn ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'calls the function on the leading edge and then once on the trailing edge when there are multiple calls', async () => { + const fn = jest.fn( async () => timeout( 10 ) ); + const debounced = debounceAsync( fn, 20 ); + + debounced( 'A' ); + debounced( 'B' ); + debounced( 'C' ); + debounced( 'D' ); + + // We can't wait for `debounced`, so wait a suitable time (resolution time + delay + 10) + // for everything to have resolved. + await timeout( 40 ); + + expect( fn ).toHaveBeenCalledTimes( 2 ); + expect( fn ).toHaveBeenCalledWith( 'A' ); + expect( fn ).toHaveBeenCalledWith( 'D' ); + } ); + + it( 'ensures the delay has elapsed between calls', async () => { + const fn = jest.fn( async () => timeout( 10 ) ); + const debounced = debounceAsync( fn, 20 ); + + // The first call has been triggered, but will take 10ms to resolve. + debounced(); + debounced(); + expect( fn ).toHaveBeenCalledTimes( 1 ); + + // The first call has resolved. The delay period has started but has yet to resolve. + await timeout( 15 ); + expect( fn ).toHaveBeenCalledTimes( 1 ); + + // The second call has now commenced. + await timeout( 20 ); + expect( fn ).toHaveBeenCalledTimes( 2 ); + } ); +} ); From 049da1828f90484084884e5f3eee66beb4d92829 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 22 Apr 2022 15:12:48 +0800 Subject: [PATCH 70/74] Fix timestamps in persistence layer tests --- .../src/create/test/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/database-persistence-layer/src/create/test/index.js b/packages/database-persistence-layer/src/create/test/index.js index c3ca35dccc8786..acf28a9c51ff07 100644 --- a/packages/database-persistence-layer/src/create/test/index.js +++ b/packages/database-persistence-layer/src/create/test/index.js @@ -114,9 +114,9 @@ describe( 'create', () => { expect( await get() ).toEqual( data ); } ); - it( 'returns data from the REST API if it has a more recent timestamp than localStorage', async () => { + it( 'returns data from the REST API if it has a more recent modified date than localStorage', async () => { const data = { - __timestamp: 1, + _modified: '2022-04-22T00:00:00.000Z', test: 'api', }; apiFetch.mockResolvedValueOnce( { @@ -128,7 +128,7 @@ describe( 'create', () => { 'getItem' ).mockReturnValueOnce( JSON.stringify( { - __timestamp: 0, + _modified: '2022-04-21T00:00:00.000Z', test: 'localStorage', } ) ); @@ -137,18 +137,18 @@ describe( 'create', () => { expect( await get() ).toEqual( data ); } ); - it( 'returns data from localStorage if it has a more recent timestamp than data from the REST API', async () => { + it( 'returns data from localStorage if it has a more recent modified date than data from the REST API', async () => { apiFetch.mockResolvedValueOnce( { meta: { persisted_preferences: { - __timestamp: 0, + _modified: '2022-04-21T00:00:00.000Z', test: 'api', }, }, } ); const data = { - __timestamp: 1, + _modified: '2022-04-22T00:00:00.000Z', test: 'localStorage', }; jest.spyOn( From 1febe2588b9fae1a9c361e57078f01984a161f65 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 22 Apr 2022 16:13:32 +0800 Subject: [PATCH 71/74] Use fake timers --- .../src/create/debounce-async.js | 2 +- .../src/create/test/debounce-async.js | 57 ++++++++++++------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/packages/database-persistence-layer/src/create/debounce-async.js b/packages/database-persistence-layer/src/create/debounce-async.js index b1450e60381ba8..445110f082d129 100644 --- a/packages/database-persistence-layer/src/create/debounce-async.js +++ b/packages/database-persistence-layer/src/create/debounce-async.js @@ -40,7 +40,7 @@ export default function debounceAsync( func, delayMS ) { // Clear any active timeouts, abandoning any requests that have // been queued but not been made. if ( timeoutId ) { - window.clearTimeout( timeoutId ); + clearTimeout( timeoutId ); timeoutId = null; } diff --git a/packages/database-persistence-layer/src/create/test/debounce-async.js b/packages/database-persistence-layer/src/create/test/debounce-async.js index 6511cad2d4e1c1..a2520e666f5192 100644 --- a/packages/database-persistence-layer/src/create/test/debounce-async.js +++ b/packages/database-persistence-layer/src/create/test/debounce-async.js @@ -3,21 +3,20 @@ */ import debounceAsync from '../debounce-async'; -describe( 'debounceAsync', () => { - function timeout( milliseconds ) { - return new Promise( ( resolve ) => - window.setTimeout( resolve, milliseconds ) - ); - } - - beforeAll( () => { - jest.useRealTimers(); - } ); +// See https://stackoverflow.com/questions/52177631/jest-timer-and-promise-dont-work-well-settimeout-and-async-function. +// Jest fake timers and async functions don't mix too well, since queued up +// promises can prevent jest from calling timeouts. +// This function flushes promises in the queue. +function flushPromises() { + return new Promise( jest.requireActual( 'timers' ).setImmediate ); +} - afterAll( () => { - jest.useFakeTimers(); - } ); +// Promisify a timeout for use with jest.fn. +function timeout( milliseconds ) { + return new Promise( ( resolve ) => setTimeout( resolve, milliseconds ) ); +} +describe( 'debounceAsync', () => { it( 'uses a leading debounce, the first call happens immediately', () => { const fn = jest.fn( async () => {} ); const debounced = debounceAsync( fn, 20 ); @@ -26,17 +25,20 @@ describe( 'debounceAsync', () => { } ); it( 'calls the function on the leading edge and then once on the trailing edge when there are multiple calls', async () => { - const fn = jest.fn( async () => timeout( 10 ) ); + jest.useFakeTimers(); + const fn = jest.fn( async () => {} ); const debounced = debounceAsync( fn, 20 ); debounced( 'A' ); + + expect( fn ).toHaveBeenCalledTimes( 1 ); + debounced( 'B' ); debounced( 'C' ); debounced( 'D' ); - // We can't wait for `debounced`, so wait a suitable time (resolution time + delay + 10) - // for everything to have resolved. - await timeout( 40 ); + await flushPromises(); + jest.runAllTimers(); expect( fn ).toHaveBeenCalledTimes( 2 ); expect( fn ).toHaveBeenCalledWith( 'A' ); @@ -44,20 +46,35 @@ describe( 'debounceAsync', () => { } ); it( 'ensures the delay has elapsed between calls', async () => { + jest.useFakeTimers(); const fn = jest.fn( async () => timeout( 10 ) ); const debounced = debounceAsync( fn, 20 ); // The first call has been triggered, but will take 10ms to resolve. debounced(); debounced(); + debounced(); + debounced(); expect( fn ).toHaveBeenCalledTimes( 1 ); - // The first call has resolved. The delay period has started but has yet to resolve. - await timeout( 15 ); + // The first call has resolved. The delay period has started but has yet to finish. + await flushPromises(); + jest.advanceTimersByTime( 11 ); + expect( fn ).toHaveBeenCalledTimes( 1 ); + + // The second call is about to commence, but hasn't yet. + await flushPromises(); + jest.advanceTimersByTime( 18 ); expect( fn ).toHaveBeenCalledTimes( 1 ); // The second call has now commenced. - await timeout( 20 ); + await flushPromises(); + jest.advanceTimersByTime( 2 ); + expect( fn ).toHaveBeenCalledTimes( 2 ); + + // No more calls happen. + await flushPromises(); + jest.runAllTimers(); expect( fn ).toHaveBeenCalledTimes( 2 ); } ); } ); From 99b8f521aa36114ad16818d8b6838339a4cbbd08 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 26 Apr 2022 10:58:16 +0800 Subject: [PATCH 72/74] Split the PHP hooks in two --- .../wordpress-6.1/persisted-preferences.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/compat/wordpress-6.1/persisted-preferences.php b/lib/compat/wordpress-6.1/persisted-preferences.php index 403dacc1328fff..4cfeb5eddc4e19 100644 --- a/lib/compat/wordpress-6.1/persisted-preferences.php +++ b/lib/compat/wordpress-6.1/persisted-preferences.php @@ -8,11 +8,12 @@ /** * Register the user meta for persisted preferences. */ -function gutenberg_configure_persisted_preferences() { +function gutenberg_register_persisted_preferences_meta() { // Create a meta key that incorporates the blog prefix so that each site // on a multisite can have distinct user preferences. global $wpdb; $meta_key = $wpdb->get_blog_prefix() . 'persisted_preferences'; + register_meta( 'user', $meta_key, @@ -39,12 +40,22 @@ function gutenberg_configure_persisted_preferences() { ), ) ); +} +add_action( 'init', 'gutenberg_register_persisted_preferences_meta' ); + +/** + * Configures the preferences package to use user meta persistence. + */ +function gutenberg_configure_persisted_preferences() { $user_id = get_current_user_id(); if ( empty( $user_id ) ) { return; } + global $wpdb; + $meta_key = $wpdb->get_blog_prefix() . 'persisted_preferences'; + $preload_data = get_user_meta( $user_id, $meta_key, true ); wp_add_inline_script( @@ -90,7 +101,7 @@ function gutenberg_configure_persisted_preferences() { } -add_action( 'init', 'gutenberg_configure_persisted_preferences' ); +add_action( 'admin_init', 'gutenberg_configure_persisted_preferences' ); /** * Register dependencies for the inline script that configures the persistence layer. From e53a9ddb2f45c93cbe39a198b89c889833195637 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 26 Apr 2022 11:28:24 +0800 Subject: [PATCH 73/74] Minimize code in inline script --- .../wordpress-6.1/persisted-preferences.php | 29 +---------- packages/database-persistence-layer/README.md | 21 -------- .../database-persistence-layer/src/index.js | 49 ++++++++++++++++++- 3 files changed, 49 insertions(+), 50 deletions(-) diff --git a/lib/compat/wordpress-6.1/persisted-preferences.php b/lib/compat/wordpress-6.1/persisted-preferences.php index 4cfeb5eddc4e19..ebbb52369361aa 100644 --- a/lib/compat/wordpress-6.1/persisted-preferences.php +++ b/lib/compat/wordpress-6.1/persisted-preferences.php @@ -64,34 +64,9 @@ function gutenberg_configure_persisted_preferences() { '( function() { var serverData = %s; var userId = "%s"; - var localStorageRestoreKey = "WP_PREFERENCES_USER_" + userId; - var localData = JSON.parse( - localStorage.getItem( localStorageRestoreKey ) - ); - - // Date parse returns NaN for invalid input. Coerce anything invalid - // into a conveniently comparable zero. - var serverModified = - Date.parse( serverData && serverData._modified ) || 0; - var localModified = - Date.parse( localData && localData._modified ) || 0; - - var preloadedData; - if ( serverData && serverModified >= localModified ) { - preloadedData = serverData; - } else if ( localData ) { - preloadedData = localData; - } else { - // Check if there is data in the legacy format from the old persistence system. - const convertLegacyLocalStorageData = - wp.databasePersistenceLayer.__unstableConvertLegacyLocalStorageData; - preloadedData = convertLegacyLocalStorageData( userId ); - } - - var create = wp.databasePersistenceLayer.create; - var persistenceLayer = create( { preloadedData, localStorageRestoreKey } ); + var persistenceLayer = wp.databasePersistenceLayer.__unstableCreatePersistenceLayer( serverData, userId ); var preferencesStore = wp.preferences.store; - wp.data.dispatch( "core/preferences" ).setPersistenceLayer( persistenceLayer ); + wp.data.dispatch( preferencesStore ).setPersistenceLayer( persistenceLayer ); } ) ();', wp_json_encode( $preload_data ), $user_id diff --git a/packages/database-persistence-layer/README.md b/packages/database-persistence-layer/README.md index 43cbe91067cba5..ae577cc4cdb8cd 100644 --- a/packages/database-persistence-layer/README.md +++ b/packages/database-persistence-layer/README.md @@ -28,27 +28,6 @@ Next, configure the preferences package to use this persistence layer: wp.data( 'core/preferences' ).setPersistenceLayer( persistenceLayer ); ``` -## Reference - - - -### create - -Creates a database persistence layer, storing data in WordPress user meta. - -_Parameters_ - -- _options_ `Object`: -- _options.preloadedData_ `?Object`: Any persisted preferences data that should be preloaded. When set, the persistence layer will avoid fetching data from the REST API. -- _options.localStorageRestoreKey_ `?string`: The key to use for restoring the localStorage backup, used when the persistence layer calls `localStorage.getItem` or `localStorage.setItem`. -- _options.requestDebounceMS_ `?number`: Debounce requests to the API so that they only occur at minimum every `requestDebounceMS` milliseconds, and don't swamp the server. Defaults to 2500ms. - -_Returns_ - -- `Object`: A database persistence layer. - - - ## Contributing to this package This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. diff --git a/packages/database-persistence-layer/src/index.js b/packages/database-persistence-layer/src/index.js index 18da7f9b56b9ab..cd3f21461b3a78 100644 --- a/packages/database-persistence-layer/src/index.js +++ b/packages/database-persistence-layer/src/index.js @@ -1,2 +1,47 @@ -export { default as create } from './create'; -export { default as __unstableConvertLegacyLocalStorageData } from './migrations/legacy-local-storage-data'; +/** + * Internal dependencies + */ +import create from './create'; +import convertLegacyLocalStorageData from './migrations/legacy-local-storage-data'; + +/** + * Creates the persistence layer with preloaded data. + * + * It prioritizes any data from the server, but falls back first to localStorage + * restore data, and then to any legacy data. + * + * This function is used internally by WordPress in an inline script, so + * prefixed with `__unstable`. + * + * @param {Object} serverData Preferences data preloaded from the server. + * @param {string} userId The user id. + * + * @return {Object} The persistence layer initialized with the preloaded data. + */ +export function __unstableCreatePersistenceLayer( serverData, userId ) { + const localStorageRestoreKey = `WP_PREFERENCES_USER_${ userId }`; + const localData = JSON.parse( + window.localStorage.getItem( localStorageRestoreKey ) + ); + + // Date parse returns NaN for invalid input. Coerce anything invalid + // into a conveniently comparable zero. + const serverModified = + Date.parse( serverData && serverData._modified ) || 0; + const localModified = Date.parse( localData && localData._modified ) || 0; + + let preloadedData; + if ( serverData && serverModified >= localModified ) { + preloadedData = serverData; + } else if ( localData ) { + preloadedData = localData; + } else { + // Check if there is data in the legacy format from the old persistence system. + preloadedData = convertLegacyLocalStorageData( userId ); + } + + return create( { + preloadedData, + localStorageRestoreKey, + } ); +} From 389abbb8e81772b4cac1206166da50d562ebfb76 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 26 Apr 2022 15:02:25 +0800 Subject: [PATCH 74/74] Rename database-persistence-layer to preferences-persistence --- docs/manifest.json | 12 ++++---- .../wordpress-6.1/persisted-preferences.php | 10 +++---- package-lock.json | 14 ++++----- package.json | 2 +- .../.npmrc | 0 .../CHANGELOG.md | 0 .../README.md | 30 ++++++++++++++++--- .../package.json | 8 ++--- .../src/create/debounce-async.js | 0 .../src/create/index.js | 5 ++-- .../src/create/test/debounce-async.js | 0 .../src/create/test/index.js | 0 .../src/index.js | 2 ++ .../legacy-local-storage-data/README.md | 0 .../convert-edit-post-panels.js | 0 .../legacy-local-storage-data/index.js | 0 .../move-feature-preferences.js | 0 .../move-individual-preference.js | 0 .../move-interface-enable-items.js | 0 .../move-third-party-feature-preferences.js | 0 .../test/convert-edit-post-panels.js | 0 .../legacy-local-storage-data/test/index.js | 0 .../test/move-feature-preferences.js | 0 .../test/move-individual-preference.js | 0 .../test/move-interface-enable-items.js | 0 .../move-third-party-feature-preferences.js | 0 26 files changed, 54 insertions(+), 29 deletions(-) rename packages/{database-persistence-layer => preferences-persistence}/.npmrc (100%) rename packages/{database-persistence-layer => preferences-persistence}/CHANGELOG.md (100%) rename packages/{database-persistence-layer => preferences-persistence}/README.md (53%) rename packages/{database-persistence-layer => preferences-persistence}/package.json (75%) rename packages/{database-persistence-layer => preferences-persistence}/src/create/debounce-async.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/create/index.js (95%) rename packages/{database-persistence-layer => preferences-persistence}/src/create/test/debounce-async.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/create/test/index.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/index.js (98%) rename packages/{database-persistence-layer => preferences-persistence}/src/migrations/legacy-local-storage-data/README.md (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/migrations/legacy-local-storage-data/convert-edit-post-panels.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/migrations/legacy-local-storage-data/index.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/migrations/legacy-local-storage-data/move-feature-preferences.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/migrations/legacy-local-storage-data/move-individual-preference.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/migrations/legacy-local-storage-data/move-interface-enable-items.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/migrations/legacy-local-storage-data/test/convert-edit-post-panels.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/migrations/legacy-local-storage-data/test/index.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/migrations/legacy-local-storage-data/test/move-feature-preferences.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/migrations/legacy-local-storage-data/test/move-individual-preference.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/migrations/legacy-local-storage-data/test/move-interface-enable-items.js (100%) rename packages/{database-persistence-layer => preferences-persistence}/src/migrations/legacy-local-storage-data/test/move-third-party-feature-preferences.js (100%) diff --git a/docs/manifest.json b/docs/manifest.json index b921bf4a869c1f..e39cc0a286217d 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1481,12 +1481,6 @@ "markdown_source": "../packages/data/README.md", "parent": "packages" }, - { - "title": "@wordpress/database-persistence-layer", - "slug": "packages-database-persistence-layer", - "markdown_source": "../packages/database-persistence-layer/README.md", - "parent": "packages" - }, { "title": "@wordpress/date", "slug": "packages-date", @@ -1715,6 +1709,12 @@ "markdown_source": "../packages/postcss-themes/README.md", "parent": "packages" }, + { + "title": "@wordpress/preferences-persistence", + "slug": "packages-preferences-persistence", + "markdown_source": "../packages/preferences-persistence/README.md", + "parent": "packages" + }, { "title": "@wordpress/preferences", "slug": "packages-preferences", diff --git a/lib/compat/wordpress-6.1/persisted-preferences.php b/lib/compat/wordpress-6.1/persisted-preferences.php index ebbb52369361aa..e62c62d3185ac9 100644 --- a/lib/compat/wordpress-6.1/persisted-preferences.php +++ b/lib/compat/wordpress-6.1/persisted-preferences.php @@ -64,7 +64,7 @@ function gutenberg_configure_persisted_preferences() { '( function() { var serverData = %s; var userId = "%s"; - var persistenceLayer = wp.databasePersistenceLayer.__unstableCreatePersistenceLayer( serverData, userId ); + var persistenceLayer = wp.preferencesPersistence.__unstableCreatePersistenceLayer( serverData, userId ); var preferencesStore = wp.preferences.store; wp.data.dispatch( preferencesStore ).setPersistenceLayer( persistenceLayer ); } ) ();', @@ -90,17 +90,17 @@ function gutenberg_configure_persisted_preferences() { * The update should be adding a new case like this like this: * ``` * case 'wp-preferences': - * array_push( $dependencies, 'wp-database-persistence-layer' ); + * array_push( $dependencies, 'wp-preferences-persistence' ); * break; * ``` * * @param WP_Scripts $scripts An instance of WP_Scripts. */ -function gutenberg_update_database_persistence_layer_deps( $scripts ) { +function gutenberg_update_preferences_persistence_deps( $scripts ) { $persistence_script = $scripts->query( 'wp-preferences', 'registered' ); if ( isset( $persistence_script->deps ) ) { - array_push( $persistence_script->deps, 'wp-database-persistence-layer' ); + array_push( $persistence_script->deps, 'wp-preferences-persistence' ); } } -add_action( 'wp_default_scripts', 'gutenberg_update_database_persistence_layer_deps', 11 ); +add_action( 'wp_default_scripts', 'gutenberg_update_preferences_persistence_deps', 11 ); diff --git a/package-lock.json b/package-lock.json index cbda08b89d4455..72330017f4534c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17430,13 +17430,6 @@ "@wordpress/deprecated": "file:packages/deprecated" } }, - "@wordpress/database-persistence-layer": { - "version": "file:packages/database-persistence-layer", - "requires": { - "@babel/runtime": "^7.16.0", - "@wordpress/api-fetch": "file:packages/api-fetch" - } - }, "@wordpress/date": { "version": "file:packages/date", "requires": { @@ -18090,6 +18083,13 @@ "classnames": "^2.3.1" } }, + "@wordpress/preferences-persistence": { + "version": "file:packages/preferences-persistence", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/api-fetch": "file:packages/api-fetch" + } + }, "@wordpress/prettier-config": { "version": "file:packages/prettier-config", "dev": true diff --git a/package.json b/package.json index 54ed98046ae5aa..0000b8d3dd249f 100755 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "@wordpress/customize-widgets": "file:packages/customize-widgets", "@wordpress/data": "file:packages/data", "@wordpress/data-controls": "file:packages/data-controls", - "@wordpress/database-persistence-layer": "file:packages/database-persistence-layer", "@wordpress/date": "file:packages/date", "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/dom": "file:packages/dom", @@ -66,6 +65,7 @@ "@wordpress/nux": "file:packages/nux", "@wordpress/plugins": "file:packages/plugins", "@wordpress/preferences": "file:packages/preferences", + "@wordpress/preferences-persistence": "file:packages/preferences-persistence", "@wordpress/primitives": "file:packages/primitives", "@wordpress/priority-queue": "file:packages/priority-queue", "@wordpress/react-i18n": "file:packages/react-i18n", diff --git a/packages/database-persistence-layer/.npmrc b/packages/preferences-persistence/.npmrc similarity index 100% rename from packages/database-persistence-layer/.npmrc rename to packages/preferences-persistence/.npmrc diff --git a/packages/database-persistence-layer/CHANGELOG.md b/packages/preferences-persistence/CHANGELOG.md similarity index 100% rename from packages/database-persistence-layer/CHANGELOG.md rename to packages/preferences-persistence/CHANGELOG.md diff --git a/packages/database-persistence-layer/README.md b/packages/preferences-persistence/README.md similarity index 53% rename from packages/database-persistence-layer/README.md rename to packages/preferences-persistence/README.md index ae577cc4cdb8cd..3279d73417d2f1 100644 --- a/packages/database-persistence-layer/README.md +++ b/packages/preferences-persistence/README.md @@ -1,15 +1,15 @@ -# Database persistence layer +# Preferences persistence -A persistence layer for `@wordpress/preferences` that stores user preferences in a WordPress database as user meta. +Persistence utilities for `wordpress/preferences`. -If for any reason data cannot be saved to the database, this persistence layer also uses local storage as a fallback. +Includes a persistence layer that saves data to WordPress user meta via the REST API. If for any reason data cannot be saved to the database, this persistence layer also uses local storage as a fallback. ## Installation Install the module ```bash -npm install @wordpress/database-persistence-layer --save +npm install @wordpress/preferences-persistence --save ``` _This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code._ @@ -28,6 +28,28 @@ Next, configure the preferences package to use this persistence layer: wp.data( 'core/preferences' ).setPersistenceLayer( persistenceLayer ); ``` +## Reference + + + +### create + +Creates a persistence layer that stores data in WordPress user meta via the +REST API. + +_Parameters_ + +- _options_ `Object`: +- _options.preloadedData_ `?Object`: Any persisted preferences data that should be preloaded. When set, the persistence layer will avoid fetching data from the REST API. +- _options.localStorageRestoreKey_ `?string`: The key to use for restoring the localStorage backup, used when the persistence layer calls `localStorage.getItem` or `localStorage.setItem`. +- _options.requestDebounceMS_ `?number`: Debounce requests to the API so that they only occur at minimum every `requestDebounceMS` milliseconds, and don't swamp the server. Defaults to 2500ms. + +_Returns_ + +- `Object`: A persistence layer for WordPress user meta. + + + ## Contributing to this package This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. diff --git a/packages/database-persistence-layer/package.json b/packages/preferences-persistence/package.json similarity index 75% rename from packages/database-persistence-layer/package.json rename to packages/preferences-persistence/package.json index 59342b258f7905..0ddfb044c8d72d 100644 --- a/packages/database-persistence-layer/package.json +++ b/packages/preferences-persistence/package.json @@ -1,7 +1,7 @@ { - "name": "@wordpress/database-persistence-layer", + "name": "@wordpress/preferences-persistence", "version": "1.0.0", - "description": "A persistence layer that stores data in the WordPress database.", + "description": "Persistence utilities for `wordpress/preferences`.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", "keywords": [ @@ -10,11 +10,11 @@ "preferences", "settings" ], - "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/database-persistence-layer/README.md", + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/preferences-persistence/README.md", "repository": { "type": "git", "url": "https://github.com/WordPress/gutenberg.git", - "directory": "packages/database-persistence-layer" + "directory": "packages/preferences-persistence" }, "bugs": { "url": "https://github.com/WordPress/gutenberg/issues" diff --git a/packages/database-persistence-layer/src/create/debounce-async.js b/packages/preferences-persistence/src/create/debounce-async.js similarity index 100% rename from packages/database-persistence-layer/src/create/debounce-async.js rename to packages/preferences-persistence/src/create/debounce-async.js diff --git a/packages/database-persistence-layer/src/create/index.js b/packages/preferences-persistence/src/create/index.js similarity index 95% rename from packages/database-persistence-layer/src/create/index.js rename to packages/preferences-persistence/src/create/index.js index f9234d7e3b3ea1..081458e40100c9 100644 --- a/packages/database-persistence-layer/src/create/index.js +++ b/packages/preferences-persistence/src/create/index.js @@ -12,7 +12,8 @@ const EMPTY_OBJECT = {}; const localStorage = window.localStorage; /** - * Creates a database persistence layer, storing data in WordPress user meta. + * Creates a persistence layer that stores data in WordPress user meta via the + * REST API. * * @param {Object} options * @param {?Object} options.preloadedData Any persisted preferences data that should be preloaded. @@ -25,7 +26,7 @@ const localStorage = window.localStorage; * minimum every `requestDebounceMS` milliseconds, and don't * swamp the server. Defaults to 2500ms. * - * @return {Object} A database persistence layer. + * @return {Object} A persistence layer for WordPress user meta. */ export default function create( { preloadedData, diff --git a/packages/database-persistence-layer/src/create/test/debounce-async.js b/packages/preferences-persistence/src/create/test/debounce-async.js similarity index 100% rename from packages/database-persistence-layer/src/create/test/debounce-async.js rename to packages/preferences-persistence/src/create/test/debounce-async.js diff --git a/packages/database-persistence-layer/src/create/test/index.js b/packages/preferences-persistence/src/create/test/index.js similarity index 100% rename from packages/database-persistence-layer/src/create/test/index.js rename to packages/preferences-persistence/src/create/test/index.js diff --git a/packages/database-persistence-layer/src/index.js b/packages/preferences-persistence/src/index.js similarity index 98% rename from packages/database-persistence-layer/src/index.js rename to packages/preferences-persistence/src/index.js index cd3f21461b3a78..a28fc411ece778 100644 --- a/packages/database-persistence-layer/src/index.js +++ b/packages/preferences-persistence/src/index.js @@ -4,6 +4,8 @@ import create from './create'; import convertLegacyLocalStorageData from './migrations/legacy-local-storage-data'; +export { create }; + /** * Creates the persistence layer with preloaded data. * diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/README.md b/packages/preferences-persistence/src/migrations/legacy-local-storage-data/README.md similarity index 100% rename from packages/database-persistence-layer/src/migrations/legacy-local-storage-data/README.md rename to packages/preferences-persistence/src/migrations/legacy-local-storage-data/README.md diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/convert-edit-post-panels.js b/packages/preferences-persistence/src/migrations/legacy-local-storage-data/convert-edit-post-panels.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/legacy-local-storage-data/convert-edit-post-panels.js rename to packages/preferences-persistence/src/migrations/legacy-local-storage-data/convert-edit-post-panels.js diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js b/packages/preferences-persistence/src/migrations/legacy-local-storage-data/index.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/legacy-local-storage-data/index.js rename to packages/preferences-persistence/src/migrations/legacy-local-storage-data/index.js diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-feature-preferences.js b/packages/preferences-persistence/src/migrations/legacy-local-storage-data/move-feature-preferences.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-feature-preferences.js rename to packages/preferences-persistence/src/migrations/legacy-local-storage-data/move-feature-preferences.js diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-individual-preference.js b/packages/preferences-persistence/src/migrations/legacy-local-storage-data/move-individual-preference.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-individual-preference.js rename to packages/preferences-persistence/src/migrations/legacy-local-storage-data/move-individual-preference.js diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-interface-enable-items.js b/packages/preferences-persistence/src/migrations/legacy-local-storage-data/move-interface-enable-items.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-interface-enable-items.js rename to packages/preferences-persistence/src/migrations/legacy-local-storage-data/move-interface-enable-items.js diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js b/packages/preferences-persistence/src/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js rename to packages/preferences-persistence/src/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/convert-edit-post-panels.js b/packages/preferences-persistence/src/migrations/legacy-local-storage-data/test/convert-edit-post-panels.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/convert-edit-post-panels.js rename to packages/preferences-persistence/src/migrations/legacy-local-storage-data/test/convert-edit-post-panels.js diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js b/packages/preferences-persistence/src/migrations/legacy-local-storage-data/test/index.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/index.js rename to packages/preferences-persistence/src/migrations/legacy-local-storage-data/test/index.js diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-feature-preferences.js b/packages/preferences-persistence/src/migrations/legacy-local-storage-data/test/move-feature-preferences.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-feature-preferences.js rename to packages/preferences-persistence/src/migrations/legacy-local-storage-data/test/move-feature-preferences.js diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-individual-preference.js b/packages/preferences-persistence/src/migrations/legacy-local-storage-data/test/move-individual-preference.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-individual-preference.js rename to packages/preferences-persistence/src/migrations/legacy-local-storage-data/test/move-individual-preference.js diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-interface-enable-items.js b/packages/preferences-persistence/src/migrations/legacy-local-storage-data/test/move-interface-enable-items.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-interface-enable-items.js rename to packages/preferences-persistence/src/migrations/legacy-local-storage-data/test/move-interface-enable-items.js diff --git a/packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-third-party-feature-preferences.js b/packages/preferences-persistence/src/migrations/legacy-local-storage-data/test/move-third-party-feature-preferences.js similarity index 100% rename from packages/database-persistence-layer/src/migrations/legacy-local-storage-data/test/move-third-party-feature-preferences.js rename to packages/preferences-persistence/src/migrations/legacy-local-storage-data/test/move-third-party-feature-preferences.js