@wordpress/preferences-persistence 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE.md +788 -0
  3. package/README.md +59 -0
  4. package/build/create/debounce-async.js +80 -0
  5. package/build/create/debounce-async.js.map +1 -0
  6. package/build/create/index.js +115 -0
  7. package/build/create/index.js.map +1 -0
  8. package/build/index.js +61 -0
  9. package/build/index.js.map +1 -0
  10. package/build/migrations/legacy-local-storage-data/convert-edit-post-panels.js +60 -0
  11. package/build/migrations/legacy-local-storage-data/convert-edit-post-panels.js.map +1 -0
  12. package/build/migrations/legacy-local-storage-data/index.js +111 -0
  13. package/build/migrations/legacy-local-storage-data/index.js.map +1 -0
  14. package/build/migrations/legacy-local-storage-data/move-feature-preferences.js +135 -0
  15. package/build/migrations/legacy-local-storage-data/move-feature-preferences.js.map +1 -0
  16. package/build/migrations/legacy-local-storage-data/move-individual-preference.js +91 -0
  17. package/build/migrations/legacy-local-storage-data/move-individual-preference.js.map +1 -0
  18. package/build/migrations/legacy-local-storage-data/move-interface-enable-items.js +114 -0
  19. package/build/migrations/legacy-local-storage-data/move-interface-enable-items.js.map +1 -0
  20. package/build/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js +99 -0
  21. package/build/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js.map +1 -0
  22. package/build-module/create/debounce-async.js +73 -0
  23. package/build-module/create/debounce-async.js.map +1 -0
  24. package/build-module/create/index.js +104 -0
  25. package/build-module/create/index.js.map +1 -0
  26. package/build-module/index.js +45 -0
  27. package/build-module/index.js.map +1 -0
  28. package/build-module/migrations/legacy-local-storage-data/convert-edit-post-panels.js +53 -0
  29. package/build-module/migrations/legacy-local-storage-data/convert-edit-post-panels.js.map +1 -0
  30. package/build-module/migrations/legacy-local-storage-data/index.js +95 -0
  31. package/build-module/migrations/legacy-local-storage-data/index.js.map +1 -0
  32. package/build-module/migrations/legacy-local-storage-data/move-feature-preferences.js +128 -0
  33. package/build-module/migrations/legacy-local-storage-data/move-feature-preferences.js.map +1 -0
  34. package/build-module/migrations/legacy-local-storage-data/move-individual-preference.js +84 -0
  35. package/build-module/migrations/legacy-local-storage-data/move-individual-preference.js.map +1 -0
  36. package/build-module/migrations/legacy-local-storage-data/move-interface-enable-items.js +107 -0
  37. package/build-module/migrations/legacy-local-storage-data/move-interface-enable-items.js.map +1 -0
  38. package/build-module/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js +92 -0
  39. package/build-module/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js.map +1 -0
  40. package/package.json +37 -0
  41. package/src/create/debounce-async.js +75 -0
  42. package/src/create/index.js +112 -0
  43. package/src/create/test/debounce-async.js +129 -0
  44. package/src/create/test/index.js +178 -0
  45. package/src/index.js +49 -0
  46. package/src/migrations/legacy-local-storage-data/README.md +42 -0
  47. package/src/migrations/legacy-local-storage-data/convert-edit-post-panels.js +50 -0
  48. package/src/migrations/legacy-local-storage-data/index.js +102 -0
  49. package/src/migrations/legacy-local-storage-data/move-feature-preferences.js +135 -0
  50. package/src/migrations/legacy-local-storage-data/move-individual-preference.js +90 -0
  51. package/src/migrations/legacy-local-storage-data/move-interface-enable-items.js +120 -0
  52. package/src/migrations/legacy-local-storage-data/move-third-party-feature-preferences.js +98 -0
  53. package/src/migrations/legacy-local-storage-data/test/convert-edit-post-panels.js +47 -0
  54. package/src/migrations/legacy-local-storage-data/test/index.js +229 -0
  55. package/src/migrations/legacy-local-storage-data/test/move-feature-preferences.js +260 -0
  56. package/src/migrations/legacy-local-storage-data/test/move-individual-preference.js +188 -0
  57. package/src/migrations/legacy-local-storage-data/test/move-interface-enable-items.js +118 -0
  58. package/src/migrations/legacy-local-storage-data/test/move-third-party-feature-preferences.js +107 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import apiFetch from '@wordpress/api-fetch';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import create from '..';
10
+
11
+ jest.mock( '@wordpress/api-fetch' );
12
+
13
+ describe( 'create', () => {
14
+ afterEach( () => {
15
+ apiFetch.mockReset();
16
+ } );
17
+
18
+ describe( 'set', () => {
19
+ it( 'stores backup restoration data in localStorage', () => {
20
+ apiFetch.mockResolvedValueOnce();
21
+ const spy = jest.spyOn( global.Storage.prototype, 'setItem' );
22
+
23
+ const localStorageRestoreKey = 'test';
24
+ const { set } = create( { localStorageRestoreKey } );
25
+
26
+ const data = { test: 1 };
27
+ set( data );
28
+
29
+ expect( spy ).toHaveBeenCalledWith(
30
+ localStorageRestoreKey,
31
+ expect.any( String )
32
+ );
33
+
34
+ // The second param of the call to `setItem` has been JSON.stringified.
35
+ // Parse it to check it contains the data.
36
+ const setItemDataParm = spy.mock.calls[ 0 ][ 1 ];
37
+ expect( JSON.parse( setItemDataParm ) ).toEqual(
38
+ expect.objectContaining( data )
39
+ );
40
+ } );
41
+
42
+ it( 'sends data to the `users/me` endpoint', () => {
43
+ apiFetch.mockResolvedValueOnce();
44
+
45
+ const { set } = create();
46
+
47
+ const data = { test: 1 };
48
+ set( data );
49
+
50
+ expect( apiFetch ).toHaveBeenCalledWith( {
51
+ path: '/wp/v2/users/me',
52
+ method: 'PUT',
53
+ keepalive: true,
54
+ data: {
55
+ meta: {
56
+ persisted_preferences: expect.objectContaining( data ),
57
+ },
58
+ },
59
+ } );
60
+ } );
61
+ } );
62
+
63
+ describe( 'get', () => {
64
+ it( 'avoids using the REST API or local storage when data is preloaded', async () => {
65
+ const getItemSpy = jest.spyOn(
66
+ global.Storage.prototype,
67
+ 'getItem'
68
+ );
69
+
70
+ const preloadedData = { preloaded: true };
71
+ const { get } = create( { preloadedData } );
72
+ expect( await get() ).toBe( preloadedData );
73
+ expect( getItemSpy ).not.toHaveBeenCalled();
74
+ expect( apiFetch ).not.toHaveBeenCalled();
75
+ } );
76
+
77
+ it( 'returns from a local cache once `set` has been called', async () => {
78
+ const getItemSpy = jest.spyOn(
79
+ global.Storage.prototype,
80
+ 'getItem'
81
+ );
82
+ apiFetch.mockResolvedValueOnce();
83
+
84
+ const data = { cached: true };
85
+ const { get, set } = create();
86
+
87
+ // apiFetch was called as a result of calling `set`.
88
+ set( data );
89
+ expect( apiFetch ).toHaveBeenCalled();
90
+ apiFetch.mockClear();
91
+
92
+ // Neither localStorage.getItem or apiFetch are called as a result
93
+ // of the call to `get`. A local cache is used.
94
+ expect( await get() ).toEqual( expect.objectContaining( data ) );
95
+ expect( getItemSpy ).not.toHaveBeenCalled();
96
+ expect( apiFetch ).not.toHaveBeenCalled();
97
+ } );
98
+
99
+ it( 'returns data from the users/me endpoint if there is no data in localStorage', async () => {
100
+ const data = {
101
+ __timestamp: 0,
102
+ test: 2,
103
+ };
104
+ apiFetch.mockResolvedValueOnce( {
105
+ meta: { persisted_preferences: data },
106
+ } );
107
+
108
+ jest.spyOn(
109
+ global.Storage.prototype,
110
+ 'getItem'
111
+ ).mockReturnValueOnce( 'null' );
112
+
113
+ const { get } = create();
114
+ expect( await get() ).toEqual( data );
115
+ } );
116
+
117
+ it( 'returns data from the REST API if it has a more recent modified date than localStorage', async () => {
118
+ const data = {
119
+ _modified: '2022-04-22T00:00:00.000Z',
120
+ test: 'api',
121
+ };
122
+ apiFetch.mockResolvedValueOnce( {
123
+ meta: { persisted_preferences: data },
124
+ } );
125
+
126
+ jest.spyOn(
127
+ global.Storage.prototype,
128
+ 'getItem'
129
+ ).mockReturnValueOnce(
130
+ JSON.stringify( {
131
+ _modified: '2022-04-21T00:00:00.000Z',
132
+ test: 'localStorage',
133
+ } )
134
+ );
135
+
136
+ const { get } = create();
137
+ expect( await get() ).toEqual( data );
138
+ } );
139
+
140
+ it( 'returns data from localStorage if it has a more recent modified date than data from the REST API', async () => {
141
+ apiFetch.mockResolvedValueOnce( {
142
+ meta: {
143
+ persisted_preferences: {
144
+ _modified: '2022-04-21T00:00:00.000Z',
145
+ test: 'api',
146
+ },
147
+ },
148
+ } );
149
+
150
+ const data = {
151
+ _modified: '2022-04-22T00:00:00.000Z',
152
+ test: 'localStorage',
153
+ };
154
+ jest.spyOn(
155
+ global.Storage.prototype,
156
+ 'getItem'
157
+ ).mockReturnValueOnce( JSON.stringify( data ) );
158
+
159
+ const { get } = create();
160
+ expect( await get() ).toEqual( data );
161
+ } );
162
+
163
+ it( 'returns an empty object if neither local storage or the REST API return any data', async () => {
164
+ apiFetch.mockResolvedValueOnce( {
165
+ meta: {
166
+ persisted_preferences: null,
167
+ },
168
+ } );
169
+ jest.spyOn(
170
+ global.Storage.prototype,
171
+ 'getItem'
172
+ ).mockReturnValueOnce( 'null' );
173
+
174
+ const { get } = create();
175
+ expect( await get() ).toEqual( {} );
176
+ } );
177
+ } );
178
+ } );
package/src/index.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import create from './create';
5
+ import convertLegacyLocalStorageData from './migrations/legacy-local-storage-data';
6
+
7
+ export { create };
8
+
9
+ /**
10
+ * Creates the persistence layer with preloaded data.
11
+ *
12
+ * It prioritizes any data from the server, but falls back first to localStorage
13
+ * restore data, and then to any legacy data.
14
+ *
15
+ * This function is used internally by WordPress in an inline script, so
16
+ * prefixed with `__unstable`.
17
+ *
18
+ * @param {Object} serverData Preferences data preloaded from the server.
19
+ * @param {string} userId The user id.
20
+ *
21
+ * @return {Object} The persistence layer initialized with the preloaded data.
22
+ */
23
+ export function __unstableCreatePersistenceLayer( serverData, userId ) {
24
+ const localStorageRestoreKey = `WP_PREFERENCES_USER_${ userId }`;
25
+ const localData = JSON.parse(
26
+ window.localStorage.getItem( localStorageRestoreKey )
27
+ );
28
+
29
+ // Date parse returns NaN for invalid input. Coerce anything invalid
30
+ // into a conveniently comparable zero.
31
+ const serverModified =
32
+ Date.parse( serverData && serverData._modified ) || 0;
33
+ const localModified = Date.parse( localData && localData._modified ) || 0;
34
+
35
+ let preloadedData;
36
+ if ( serverData && serverModified >= localModified ) {
37
+ preloadedData = serverData;
38
+ } else if ( localData ) {
39
+ preloadedData = localData;
40
+ } else {
41
+ // Check if there is data in the legacy format from the old persistence system.
42
+ preloadedData = convertLegacyLocalStorageData( userId );
43
+ }
44
+
45
+ return create( {
46
+ preloadedData,
47
+ localStorageRestoreKey,
48
+ } );
49
+ }
@@ -0,0 +1,42 @@
1
+ # Legacy local storage migrations
2
+
3
+ This folder contains all the migration code for converting from the old `data` package persistence system.
4
+
5
+ ## History
6
+
7
+ 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:
8
+ ```js
9
+ registerStore(
10
+ 'core/edit-post', {
11
+ selectors,
12
+ actions,
13
+ reducer,
14
+ persist: [ 'preferences' ],
15
+ },
16
+ );
17
+ ```
18
+
19
+ This would result in local storage data being saved in this format:
20
+ ```json
21
+ {
22
+ "core/edit-post": {
23
+ "preferences": {
24
+ // ... preferences state from the post editor.
25
+ }
26
+ },
27
+ // ... other persisted state from other editors.
28
+ }
29
+ ```
30
+
31
+ And when an editor was reloaded, this would become the initial store state.
32
+
33
+ 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:
34
+ ```json
35
+ {
36
+ "preferences": {
37
+ "core/edit-post": {
38
+ // ... preferences for the post editor.
39
+ }
40
+ // ... preferences for other editors
41
+ }
42
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Convert the post editor's panels state from:
3
+ * ```
4
+ * {
5
+ * panels: {
6
+ * tags: {
7
+ * enabled: true,
8
+ * opened: true,
9
+ * },
10
+ * permalinks: {
11
+ * enabled: false,
12
+ * opened: false,
13
+ * },
14
+ * },
15
+ * }
16
+ * ```
17
+ *
18
+ * to a new, more concise data structure:
19
+ * {
20
+ * inactivePanels: [
21
+ * 'permalinks',
22
+ * ],
23
+ * openPanels: [
24
+ * 'tags',
25
+ * ],
26
+ * }
27
+ *
28
+ * @param {Object} preferences A preferences object.
29
+ *
30
+ * @return {Object} The converted data.
31
+ */
32
+ export default function convertEditPostPanels( preferences ) {
33
+ const panels = preferences?.panels ?? {};
34
+ return Object.keys( panels ).reduce(
35
+ ( convertedData, panelName ) => {
36
+ const panel = panels[ panelName ];
37
+
38
+ if ( panel?.enabled === false ) {
39
+ convertedData.inactivePanels.push( panelName );
40
+ }
41
+
42
+ if ( panel?.opened === true ) {
43
+ convertedData.openPanels.push( panelName );
44
+ }
45
+
46
+ return convertedData;
47
+ },
48
+ { inactivePanels: [], openPanels: [] }
49
+ );
50
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import moveFeaturePreferences from './move-feature-preferences';
5
+ import moveThirdPartyFeaturePreferences from './move-third-party-feature-preferences';
6
+ import moveIndividualPreference from './move-individual-preference';
7
+ import moveInterfaceEnableItems from './move-interface-enable-items';
8
+ import convertEditPostPanels from './convert-edit-post-panels';
9
+
10
+ /**
11
+ * Gets the legacy local storage data for a given user.
12
+ *
13
+ * @param {string | number} userId The user id.
14
+ *
15
+ * @return {Object | null} The local storage data.
16
+ */
17
+ function getLegacyData( userId ) {
18
+ const key = `WP_DATA_USER_${ userId }`;
19
+ const unparsedData = window.localStorage.getItem( key );
20
+ return JSON.parse( unparsedData );
21
+ }
22
+
23
+ /**
24
+ * Converts data from the old `@wordpress/data` package format.
25
+ *
26
+ * @param {Object | null | undefined} data The legacy data in its original format.
27
+ *
28
+ * @return {Object | undefined} The converted data or `undefined` if there was
29
+ * nothing to convert.
30
+ */
31
+ export function convertLegacyData( data ) {
32
+ if ( ! data ) {
33
+ return;
34
+ }
35
+
36
+ // Move boolean feature preferences from each editor into the
37
+ // preferences store data structure.
38
+ data = moveFeaturePreferences( data, 'core/edit-widgets' );
39
+ data = moveFeaturePreferences( data, 'core/customize-widgets' );
40
+ data = moveFeaturePreferences( data, 'core/edit-post' );
41
+ data = moveFeaturePreferences( data, 'core/edit-site' );
42
+
43
+ // Move third party boolean feature preferences from the interface package
44
+ // to the preferences store data structure.
45
+ data = moveThirdPartyFeaturePreferences( data );
46
+
47
+ // Move and convert the interface store's `enableItems` data into the
48
+ // preferences data structure.
49
+ data = moveInterfaceEnableItems( data );
50
+
51
+ // Move individual ad-hoc preferences from various packages into the
52
+ // preferences store data structure.
53
+ data = moveIndividualPreference(
54
+ data,
55
+ { from: 'core/edit-post', to: 'core/edit-post' },
56
+ 'hiddenBlockTypes'
57
+ );
58
+ data = moveIndividualPreference(
59
+ data,
60
+ { from: 'core/edit-post', to: 'core/edit-post' },
61
+ 'editorMode'
62
+ );
63
+ data = moveIndividualPreference(
64
+ data,
65
+ { from: 'core/edit-post', to: 'core/edit-post' },
66
+ 'preferredStyleVariations'
67
+ );
68
+ data = moveIndividualPreference(
69
+ data,
70
+ { from: 'core/edit-post', to: 'core/edit-post' },
71
+ 'panels',
72
+ convertEditPostPanels
73
+ );
74
+ data = moveIndividualPreference(
75
+ data,
76
+ { from: 'core/editor', to: 'core/edit-post' },
77
+ 'isPublishSidebarEnabled'
78
+ );
79
+ data = moveIndividualPreference(
80
+ data,
81
+ { from: 'core/edit-site', to: 'core/edit-site' },
82
+ 'editorMode'
83
+ );
84
+
85
+ // The new system is only concerned with persisting
86
+ // 'core/preferences' preferences reducer, so only return that.
87
+ return data?.[ 'core/preferences' ]?.preferences;
88
+ }
89
+
90
+ /**
91
+ * Gets the legacy local storage data for the given user and returns the
92
+ * data converted to the new format.
93
+ *
94
+ * @param {string | number} userId The user id.
95
+ *
96
+ * @return {Object | undefined} The converted data or undefined if no local
97
+ * storage data could be found.
98
+ */
99
+ export default function convertLegacyLocalStorageData( userId ) {
100
+ const data = getLegacyData( userId );
101
+ return convertLegacyData( data );
102
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Move the 'features' object in local storage from the sourceStoreName to the
3
+ * preferences store data structure.
4
+ *
5
+ * Previously, editors used a data structure like this for feature preferences:
6
+ * ```js
7
+ * {
8
+ * 'core/edit-post': {
9
+ * preferences: {
10
+ * features; {
11
+ * topToolbar: true,
12
+ * // ... other boolean 'feature' preferences
13
+ * },
14
+ * },
15
+ * },
16
+ * }
17
+ * ```
18
+ *
19
+ * And for a while these feature preferences lived in the interface package:
20
+ * ```js
21
+ * {
22
+ * 'core/interface': {
23
+ * preferences: {
24
+ * features: {
25
+ * 'core/edit-post': {
26
+ * topToolbar: true
27
+ * }
28
+ * }
29
+ * }
30
+ * }
31
+ * }
32
+ * ```
33
+ *
34
+ * In the preferences store, 'features' aren't considered special, they're
35
+ * merged to the root level of the scope along with other preferences:
36
+ * ```js
37
+ * {
38
+ * 'core/preferences': {
39
+ * preferences: {
40
+ * 'core/edit-post': {
41
+ * topToolbar: true,
42
+ * // ... any other preferences.
43
+ * }
44
+ * }
45
+ * }
46
+ * }
47
+ * ```
48
+ *
49
+ * This function handles moving from either the source store or the interface
50
+ * store to the preferences data structure.
51
+ *
52
+ * @param {Object} state The state before migration.
53
+ * @param {string} sourceStoreName The name of the store that has persisted
54
+ * preferences to migrate to the preferences
55
+ * package.
56
+ * @return {Object} The migrated state
57
+ */
58
+ export default function moveFeaturePreferences( state, sourceStoreName ) {
59
+ const preferencesStoreName = 'core/preferences';
60
+ const interfaceStoreName = 'core/interface';
61
+
62
+ // Features most recently (and briefly) lived in the interface package.
63
+ // If data exists there, prioritize using that for the migration. If not
64
+ // also check the original package as the user may have updated from an
65
+ // older block editor version.
66
+ const interfaceFeatures =
67
+ state?.[ interfaceStoreName ]?.preferences?.features?.[
68
+ sourceStoreName
69
+ ];
70
+ const sourceFeatures = state?.[ sourceStoreName ]?.preferences?.features;
71
+ const featuresToMigrate = interfaceFeatures
72
+ ? interfaceFeatures
73
+ : sourceFeatures;
74
+
75
+ if ( ! featuresToMigrate ) {
76
+ return state;
77
+ }
78
+
79
+ const existingPreferences = state?.[ preferencesStoreName ]?.preferences;
80
+
81
+ // Avoid migrating features again if they've previously been migrated.
82
+ if ( existingPreferences?.[ sourceStoreName ] ) {
83
+ return state;
84
+ }
85
+
86
+ let updatedInterfaceState;
87
+ if ( interfaceFeatures ) {
88
+ const otherInterfaceState = state?.[ interfaceStoreName ];
89
+ const otherInterfaceScopes =
90
+ state?.[ interfaceStoreName ]?.preferences?.features;
91
+
92
+ updatedInterfaceState = {
93
+ [ interfaceStoreName ]: {
94
+ ...otherInterfaceState,
95
+ preferences: {
96
+ features: {
97
+ ...otherInterfaceScopes,
98
+ [ sourceStoreName ]: undefined,
99
+ },
100
+ },
101
+ },
102
+ };
103
+ }
104
+
105
+ let updatedSourceState;
106
+ if ( sourceFeatures ) {
107
+ const otherSourceState = state?.[ sourceStoreName ];
108
+ const sourcePreferences = state?.[ sourceStoreName ]?.preferences;
109
+
110
+ updatedSourceState = {
111
+ [ sourceStoreName ]: {
112
+ ...otherSourceState,
113
+ preferences: {
114
+ ...sourcePreferences,
115
+ features: undefined,
116
+ },
117
+ },
118
+ };
119
+ }
120
+
121
+ // Set the feature values in the interface store, the features
122
+ // object is keyed by 'scope', which matches the store name for
123
+ // the source.
124
+ return {
125
+ ...state,
126
+ [ preferencesStoreName ]: {
127
+ preferences: {
128
+ ...existingPreferences,
129
+ [ sourceStoreName ]: featuresToMigrate,
130
+ },
131
+ },
132
+ ...updatedInterfaceState,
133
+ ...updatedSourceState,
134
+ };
135
+ }
@@ -0,0 +1,90 @@
1
+ const identity = ( arg ) => arg;
2
+
3
+ /**
4
+ * Migrates an individual item inside the `preferences` object for a package's store.
5
+ *
6
+ * Previously, some packages had individual 'preferences' of any data type, and many used
7
+ * complex nested data structures. For example:
8
+ * ```js
9
+ * {
10
+ * 'core/edit-post': {
11
+ * preferences: {
12
+ * panels: {
13
+ * publish: {
14
+ * opened: true,
15
+ * enabled: true,
16
+ * }
17
+ * },
18
+ * // ...other preferences.
19
+ * },
20
+ * },
21
+ * }
22
+ *
23
+ * This function supports moving an individual preference like 'panels' above into the
24
+ * preferences package data structure.
25
+ *
26
+ * It supports moving a preference to a particular scope in the preferences store and
27
+ * optionally converting the data using a `convert` function.
28
+ *
29
+ * ```
30
+ *
31
+ * @param {Object} state The original state.
32
+ * @param {Object} migrate An options object that contains details of the migration.
33
+ * @param {string} migrate.from The name of the store to migrate from.
34
+ * @param {string} migrate.to The scope in the preferences store to migrate to.
35
+ * @param {string} key The key in the preferences object to migrate.
36
+ * @param {?Function} convert A function that converts preferences from one format to another.
37
+ */
38
+ export default function moveIndividualPreferenceToPreferences(
39
+ state,
40
+ { from: sourceStoreName, to: scope },
41
+ key,
42
+ convert = identity
43
+ ) {
44
+ const preferencesStoreName = 'core/preferences';
45
+ const sourcePreference = state?.[ sourceStoreName ]?.preferences?.[ key ];
46
+
47
+ // There's nothing to migrate, exit early.
48
+ if ( sourcePreference === undefined ) {
49
+ return state;
50
+ }
51
+
52
+ const targetPreference =
53
+ state?.[ preferencesStoreName ]?.preferences?.[ scope ]?.[ key ];
54
+
55
+ // There's existing data at the target, so don't overwrite it, exit early.
56
+ if ( targetPreference ) {
57
+ return state;
58
+ }
59
+
60
+ const otherScopes = state?.[ preferencesStoreName ]?.preferences;
61
+ const otherPreferences =
62
+ state?.[ preferencesStoreName ]?.preferences?.[ scope ];
63
+
64
+ const otherSourceState = state?.[ sourceStoreName ];
65
+ const allSourcePreferences = state?.[ sourceStoreName ]?.preferences;
66
+
67
+ // Pass an object with the key and value as this allows the convert
68
+ // function to convert to a data structure that has different keys.
69
+ const convertedPreferences = convert( { [ key ]: sourcePreference } );
70
+
71
+ return {
72
+ ...state,
73
+ [ preferencesStoreName ]: {
74
+ preferences: {
75
+ ...otherScopes,
76
+ [ scope ]: {
77
+ ...otherPreferences,
78
+ ...convertedPreferences,
79
+ },
80
+ },
81
+ },
82
+ [ sourceStoreName ]: {
83
+ ...otherSourceState,
84
+ preferences: {
85
+ ...allSourcePreferences,
86
+ [ key ]: undefined,
87
+ },
88
+ },
89
+ };
90
+ }