@wordpress/data 6.0.1 → 6.1.3

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 (50) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +31 -2
  3. package/build/components/registry-provider/use-registry.js +1 -1
  4. package/build/components/registry-provider/use-registry.js.map +1 -1
  5. package/build/controls.js +23 -15
  6. package/build/controls.js.map +1 -1
  7. package/build/plugins/persistence/index.js +43 -50
  8. package/build/plugins/persistence/index.js.map +1 -1
  9. package/build/redux-store/index.js +15 -3
  10. package/build/redux-store/index.js.map +1 -1
  11. package/build/registry.js +40 -7
  12. package/build/registry.js.map +1 -1
  13. package/build/types.js +2 -0
  14. package/build/{types.d.js.map → types.js.map} +0 -0
  15. package/build/utils/emitter.js +57 -0
  16. package/build/utils/emitter.js.map +1 -0
  17. package/build-module/components/registry-provider/use-registry.js +1 -1
  18. package/build-module/components/registry-provider/use-registry.js.map +1 -1
  19. package/build-module/controls.js +22 -15
  20. package/build-module/controls.js.map +1 -1
  21. package/build-module/plugins/persistence/index.js +42 -51
  22. package/build-module/plugins/persistence/index.js.map +1 -1
  23. package/build-module/redux-store/index.js +15 -3
  24. package/build-module/redux-store/index.js.map +1 -1
  25. package/build-module/registry.js +40 -8
  26. package/build-module/registry.js.map +1 -1
  27. package/build-module/types.js +2 -0
  28. package/build-module/{types.d.js.map → types.js.map} +0 -0
  29. package/build-module/utils/emitter.js +50 -0
  30. package/build-module/utils/emitter.js.map +1 -0
  31. package/build-types/redux-store/metadata/selectors.d.ts +1 -3
  32. package/build-types/redux-store/metadata/selectors.d.ts.map +1 -1
  33. package/build-types/types.d.ts +35 -0
  34. package/build-types/types.d.ts.map +1 -0
  35. package/build-types/utils/emitter.d.ts +7 -0
  36. package/build-types/utils/emitter.d.ts.map +1 -0
  37. package/package.json +6 -6
  38. package/src/components/registry-provider/use-registry.js +1 -1
  39. package/src/controls.js +43 -15
  40. package/src/plugins/persistence/index.js +56 -62
  41. package/src/plugins/persistence/test/index.js +117 -1
  42. package/src/redux-store/index.js +17 -4
  43. package/src/registry.js +38 -8
  44. package/src/test/registry.js +34 -0
  45. package/src/{types.d.ts → types.ts} +8 -0
  46. package/src/utils/emitter.js +46 -0
  47. package/tsconfig.json +2 -0
  48. package/tsconfig.tsbuildinfo +1 -826
  49. package/build/types.d.js +0 -2
  50. package/build-module/types.d.js +0 -2
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { merge, isPlainObject, get, has } from 'lodash';
4
+ import { merge, isPlainObject } from 'lodash';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
@@ -223,75 +223,69 @@ function persistencePlugin( registry, pluginOptions ) {
223
223
  }
224
224
 
225
225
  /**
226
- * Deprecated: Remove this function and the code in WordPress Core that calls
227
- * it once WordPress 5.4 is released.
226
+ * Move the 'features' object in local storage from the sourceStoreName to the
227
+ * interface store.
228
+ *
229
+ * @param {Object} persistence The persistence interface.
230
+ * @param {string} sourceStoreName The name of the store that has persisted
231
+ * preferences to migrate to the interface
232
+ * package.
228
233
  */
229
-
230
- persistencePlugin.__unstableMigrate = ( pluginOptions ) => {
231
- const persistence = createPersistenceInterface( pluginOptions );
232
-
234
+ export function migrateFeaturePreferencesToInterfaceStore(
235
+ persistence,
236
+ sourceStoreName
237
+ ) {
238
+ const interfaceStoreName = 'core/interface';
233
239
  const state = persistence.get();
240
+ const sourcePreferences = state[ sourceStoreName ]?.preferences;
241
+ const sourceFeatures = sourcePreferences?.features;
242
+
243
+ if ( sourceFeatures ) {
244
+ const targetFeatures =
245
+ state[ interfaceStoreName ]?.preferences?.features;
246
+
247
+ // Avoid migrating features again if they've previously been migrated.
248
+ if ( ! targetFeatures?.[ sourceStoreName ] ) {
249
+ // Set the feature values in the interface store, the features
250
+ // object is keyed by 'scope', which matches the store name for
251
+ // the source.
252
+ persistence.set( interfaceStoreName, {
253
+ preferences: {
254
+ features: {
255
+ ...targetFeatures,
256
+ [ sourceStoreName ]: sourceFeatures,
257
+ },
258
+ },
259
+ } );
234
260
 
235
- // Migrate 'insertUsage' from 'core/editor' to 'core/block-editor'
236
- const editorInsertUsage = state[ 'core/editor' ]?.preferences?.insertUsage;
237
- if ( editorInsertUsage ) {
238
- const blockEditorInsertUsage =
239
- state[ 'core/block-editor' ]?.preferences?.insertUsage;
240
- persistence.set( 'core/block-editor', {
241
- preferences: {
242
- insertUsage: {
243
- ...editorInsertUsage,
244
- ...blockEditorInsertUsage,
261
+ // Remove feature preferences from the source.
262
+ persistence.set( sourceStoreName, {
263
+ preferences: {
264
+ ...sourcePreferences,
265
+ features: undefined,
245
266
  },
246
- },
247
- } );
267
+ } );
268
+ }
248
269
  }
270
+ }
249
271
 
250
- let editPostState = state[ 'core/edit-post' ];
251
-
252
- // Default `fullscreenMode` to `false` if any persisted state had existed
253
- // and the user hadn't made an explicit choice about fullscreen mode. This
254
- // is needed since `fullscreenMode` previously did not have a default value
255
- // and was implicitly false by its absence. It is now `true` by default, but
256
- // this change is not intended to affect upgrades from earlier versions.
257
- const hadPersistedState = Object.keys( state ).length > 0;
258
- const hadFullscreenModePreference = has( state, [
259
- 'core/edit-post',
260
- 'preferences',
261
- 'features',
262
- 'fullscreenMode',
263
- ] );
264
- if ( hadPersistedState && ! hadFullscreenModePreference ) {
265
- editPostState = merge( {}, editPostState, {
266
- preferences: { features: { fullscreenMode: false } },
267
- } );
268
- }
272
+ /**
273
+ * Deprecated: Remove this function and the code in WordPress Core that calls
274
+ * it once WordPress 6.0 is released.
275
+ */
269
276
 
270
- // Migrate 'areTipsEnabled' from 'core/nux' to 'showWelcomeGuide' in 'core/edit-post'
271
- const areTipsEnabled = get( state, [
272
- 'core/nux',
273
- 'preferences',
274
- 'areTipsEnabled',
275
- ] );
276
- const hasWelcomeGuide = has( state, [
277
- 'core/edit-post',
278
- 'preferences',
279
- 'features',
280
- 'welcomeGuide',
281
- ] );
282
- if ( areTipsEnabled !== undefined && ! hasWelcomeGuide ) {
283
- editPostState = merge( {}, editPostState, {
284
- preferences: {
285
- features: {
286
- welcomeGuide: areTipsEnabled,
287
- },
288
- },
289
- } );
290
- }
277
+ persistencePlugin.__unstableMigrate = ( pluginOptions ) => {
278
+ const persistence = createPersistenceInterface( pluginOptions );
291
279
 
292
- if ( editPostState !== state[ 'core/edit-post' ] ) {
293
- persistence.set( 'core/edit-post', editPostState );
294
- }
280
+ migrateFeaturePreferencesToInterfaceStore(
281
+ persistence,
282
+ 'core/edit-widgets'
283
+ );
284
+ migrateFeaturePreferencesToInterfaceStore(
285
+ persistence,
286
+ 'core/customize-widgets'
287
+ );
288
+ migrateFeaturePreferencesToInterfaceStore( persistence, 'core/edit-post' );
295
289
  };
296
290
 
297
291
  export default persistencePlugin;
@@ -6,7 +6,11 @@ import deepFreeze from 'deep-freeze';
6
6
  /**
7
7
  * Internal dependencies
8
8
  */
9
- import plugin, { createPersistenceInterface, withLazySameState } from '../';
9
+ import plugin, {
10
+ createPersistenceInterface,
11
+ withLazySameState,
12
+ migrateFeaturePreferencesToInterfaceStore,
13
+ } from '../';
10
14
  import objectStorage from '../storage/object';
11
15
  import { createRegistry } from '../../../';
12
16
 
@@ -377,3 +381,115 @@ describe( 'persistence', () => {
377
381
  } );
378
382
  } );
379
383
  } );
384
+
385
+ describe( 'migrateFeaturePreferencesToInterfaceStore', () => {
386
+ it( 'migrates preferences from the source to the interface store', () => {
387
+ const persistenceInterface = createPersistenceInterface( {
388
+ storageKey: 'test-username',
389
+ } );
390
+
391
+ const initialState = {
392
+ preferences: {
393
+ features: {
394
+ featureA: true,
395
+ featureB: false,
396
+ featureC: true,
397
+ },
398
+ },
399
+ };
400
+
401
+ persistenceInterface.set( 'core/test', initialState );
402
+
403
+ migrateFeaturePreferencesToInterfaceStore(
404
+ persistenceInterface,
405
+ 'core/test'
406
+ );
407
+
408
+ expect( persistenceInterface.get() ).toEqual( {
409
+ 'core/interface': {
410
+ preferences: {
411
+ features: {
412
+ 'core/test': {
413
+ featureA: true,
414
+ featureB: false,
415
+ featureC: true,
416
+ },
417
+ },
418
+ },
419
+ },
420
+ 'core/test': {
421
+ preferences: {
422
+ features: undefined,
423
+ },
424
+ },
425
+ } );
426
+ } );
427
+
428
+ it( 'handles multiple preferences from different stores to be migrated', () => {
429
+ const persistenceInterface = createPersistenceInterface( {
430
+ storageKey: 'test-username',
431
+ } );
432
+
433
+ const initialStateA = {
434
+ preferences: {
435
+ features: {
436
+ featureA: true,
437
+ featureB: false,
438
+ featureC: true,
439
+ },
440
+ },
441
+ };
442
+
443
+ const initialStateB = {
444
+ preferences: {
445
+ features: {
446
+ featureD: true,
447
+ featureE: false,
448
+ featureF: true,
449
+ },
450
+ },
451
+ };
452
+
453
+ persistenceInterface.set( 'core/test-a', initialStateA );
454
+ persistenceInterface.set( 'core/test-b', initialStateB );
455
+
456
+ migrateFeaturePreferencesToInterfaceStore(
457
+ persistenceInterface,
458
+ 'core/test-a'
459
+ );
460
+
461
+ migrateFeaturePreferencesToInterfaceStore(
462
+ persistenceInterface,
463
+ 'core/test-b'
464
+ );
465
+
466
+ expect( persistenceInterface.get() ).toEqual( {
467
+ 'core/interface': {
468
+ preferences: {
469
+ features: {
470
+ 'core/test-a': {
471
+ featureA: true,
472
+ featureB: false,
473
+ featureC: true,
474
+ },
475
+ 'core/test-b': {
476
+ featureD: true,
477
+ featureE: false,
478
+ featureF: true,
479
+ },
480
+ },
481
+ },
482
+ },
483
+ 'core/test-a': {
484
+ preferences: {
485
+ features: undefined,
486
+ },
487
+ },
488
+ 'core/test-b': {
489
+ preferences: {
490
+ features: undefined,
491
+ },
492
+ },
493
+ } );
494
+ } );
495
+ } );
@@ -26,6 +26,16 @@ import * as metadataActions from './metadata/actions';
26
26
  /** @typedef {import('../types').WPDataStore} WPDataStore */
27
27
  /** @typedef {import('../types').WPDataReduxStoreConfig} WPDataReduxStoreConfig */
28
28
 
29
+ const trimUndefinedValues = ( array ) => {
30
+ const result = [ ...array ];
31
+ for ( let i = result.length - 1; i >= 0; i-- ) {
32
+ if ( result[ i ] === undefined ) {
33
+ result.splice( i, 1 );
34
+ }
35
+ }
36
+ return result;
37
+ };
38
+
29
39
  /**
30
40
  * Create a cache to track whether resolvers started running or not.
31
41
  *
@@ -35,12 +45,15 @@ function createResolversCache() {
35
45
  const cache = {};
36
46
  return {
37
47
  isRunning( selectorName, args ) {
38
- return cache[ selectorName ] && cache[ selectorName ].get( args );
48
+ return (
49
+ cache[ selectorName ] &&
50
+ cache[ selectorName ].get( trimUndefinedValues( args ) )
51
+ );
39
52
  },
40
53
 
41
54
  clear( selectorName, args ) {
42
55
  if ( cache[ selectorName ] ) {
43
- cache[ selectorName ].delete( args );
56
+ cache[ selectorName ].delete( trimUndefinedValues( args ) );
44
57
  }
45
58
  },
46
59
 
@@ -49,7 +62,7 @@ function createResolversCache() {
49
62
  cache[ selectorName ] = new EquivalentKeyMap();
50
63
  }
51
64
 
52
- cache[ selectorName ].set( args, true );
65
+ cache[ selectorName ].set( trimUndefinedValues( args ), true );
53
66
  },
54
67
  };
55
68
  }
@@ -324,7 +337,6 @@ function mapResolveSelectors( selectors, store ) {
324
337
  const hasFinished = () =>
325
338
  selectors.hasFinishedResolution( selectorName, args );
326
339
  const getResult = () => selector.apply( null, args );
327
-
328
340
  // trigger the selector (to trigger the resolver)
329
341
  const result = getResult();
330
342
  if ( hasFinished() ) {
@@ -376,6 +388,7 @@ function mapResolvers( resolvers, selectors, store, resolversCache ) {
376
388
  const selectorResolver = ( ...args ) => {
377
389
  async function fulfillSelector() {
378
390
  const state = store.getState();
391
+
379
392
  if (
380
393
  resolversCache.isRunning( selectorName, args ) ||
381
394
  ( typeof resolver.isFulfilled === 'function' &&
package/src/registry.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { without, mapValues, isObject } from 'lodash';
4
+ import { mapValues, isObject, forEach } from 'lodash';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
@@ -9,6 +9,7 @@ import { without, mapValues, isObject } from 'lodash';
9
9
  import createReduxStore from './redux-store';
10
10
  import createCoreDataStore from './store';
11
11
  import { STORE_NAME } from './store/name';
12
+ import { createEmitter } from './utils/emitter';
12
13
 
13
14
  /** @typedef {import('./types').WPDataStore} WPDataStore */
14
15
 
@@ -49,14 +50,14 @@ import { STORE_NAME } from './store/name';
49
50
  */
50
51
  export function createRegistry( storeConfigs = {}, parent = null ) {
51
52
  const stores = {};
52
- let listeners = [];
53
+ const emitter = createEmitter();
53
54
  const __experimentalListeningStores = new Set();
54
55
 
55
56
  /**
56
57
  * Global listener called for each store's update.
57
58
  */
58
59
  function globalListener() {
59
- listeners.forEach( ( listener ) => listener() );
60
+ emitter.emit();
60
61
  }
61
62
 
62
63
  /**
@@ -67,11 +68,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
67
68
  * @return {Function} Unsubscribe function.
68
69
  */
69
70
  const subscribe = ( listener ) => {
70
- listeners.push( listener );
71
-
72
- return () => {
73
- listeners = without( listeners, listener );
74
- };
71
+ return emitter.subscribe( listener );
75
72
  };
76
73
 
77
74
  /**
@@ -177,6 +174,30 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
177
174
  if ( typeof config.subscribe !== 'function' ) {
178
175
  throw new TypeError( 'config.subscribe must be a function' );
179
176
  }
177
+ // Thi emitter is used to keep track of active listeners when the registry
178
+ // get paused, that way, when resumed we should be able to call all these
179
+ // pending listeners.
180
+ config.emitter = createEmitter();
181
+ const currentSubscribe = config.subscribe;
182
+ config.subscribe = ( listener ) => {
183
+ const unsubscribeFromStoreEmitter = config.emitter.subscribe(
184
+ listener
185
+ );
186
+ const unsubscribeFromRootStore = currentSubscribe( () => {
187
+ if ( config.emitter.isPaused ) {
188
+ config.emitter.emit();
189
+ return;
190
+ }
191
+ listener();
192
+ } );
193
+
194
+ return () => {
195
+ if ( unsubscribeFromRootStore ) {
196
+ unsubscribeFromRootStore();
197
+ }
198
+ unsubscribeFromStoreEmitter();
199
+ };
200
+ };
180
201
  stores[ key ] = config;
181
202
  config.subscribe( globalListener );
182
203
  }
@@ -213,7 +234,16 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
213
234
  return parent.__experimentalSubscribeStore( storeName, handler );
214
235
  }
215
236
 
237
+ function batch( callback ) {
238
+ emitter.pause();
239
+ forEach( stores, ( store ) => store.emitter.pause() );
240
+ callback();
241
+ emitter.resume();
242
+ forEach( stores, ( store ) => store.emitter.resume() );
243
+ }
244
+
216
245
  let registry = {
246
+ batch,
217
247
  registerGenericStore,
218
248
  stores,
219
249
  namespaces: stores, // TODO: Deprecate/remove this.
@@ -722,6 +722,40 @@ describe( 'createRegistry', () => {
722
722
  } );
723
723
  } );
724
724
 
725
+ describe( 'batch', () => {
726
+ it( 'should batch callbacks and only run the subscriber once', () => {
727
+ const store = registry.registerStore( 'myAwesomeReducer', {
728
+ reducer: ( state = 0 ) => state + 1,
729
+ } );
730
+ const listener = jest.fn();
731
+ subscribeWithUnsubscribe( listener );
732
+
733
+ registry.batch( () => {} );
734
+ expect( listener ).not.toHaveBeenCalled();
735
+
736
+ registry.batch( () => {
737
+ store.dispatch( { type: 'dummy' } );
738
+ store.dispatch( { type: 'dummy' } );
739
+ } );
740
+ expect( listener ).toHaveBeenCalledTimes( 1 );
741
+
742
+ const listener2 = jest.fn();
743
+ // useSelect subscribes to the stores differently,
744
+ // This test ensures batching works in this case as well.
745
+ const unsubscribe = registry.__experimentalSubscribeStore(
746
+ 'myAwesomeReducer',
747
+ listener2
748
+ );
749
+ registry.batch( () => {
750
+ store.dispatch( { type: 'dummy' } );
751
+ store.dispatch( { type: 'dummy' } );
752
+ } );
753
+ unsubscribe();
754
+
755
+ expect( listener2 ).toHaveBeenCalledTimes( 1 );
756
+ } );
757
+ } );
758
+
725
759
  describe( 'use', () => {
726
760
  it( 'should pass through options object to plugin', () => {
727
761
  const expectedOptions = {};
@@ -30,3 +30,11 @@ export interface WPDataReduxStoreConfig {
30
30
  export interface WPDataRegistry {
31
31
  register: ( store: WPDataStore ) => void;
32
32
  }
33
+
34
+ export interface WPDataEmitter {
35
+ emit: () => void;
36
+ subscribe: ( listener: () => void ) => () => void;
37
+ pause: () => void;
38
+ resume: () => void;
39
+ isPaused: boolean;
40
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Create an event emitter.
3
+ *
4
+ * @return {import("../types").WPDataEmitter} Emitter.
5
+ */
6
+ export function createEmitter() {
7
+ let isPaused = false;
8
+ let isPending = false;
9
+ const listeners = new Set();
10
+ const notifyListeners = () =>
11
+ // We use Array.from to clone the listeners Set
12
+ // This ensures that we don't run a listener
13
+ // that was added as a response to another listener.
14
+ Array.from( listeners ).forEach( ( listener ) => listener() );
15
+
16
+ return {
17
+ get isPaused() {
18
+ return isPaused;
19
+ },
20
+
21
+ subscribe( listener ) {
22
+ listeners.add( listener );
23
+ return () => listeners.delete( listener );
24
+ },
25
+
26
+ pause() {
27
+ isPaused = true;
28
+ },
29
+
30
+ resume() {
31
+ isPaused = false;
32
+ if ( isPending ) {
33
+ isPending = false;
34
+ notifyListeners();
35
+ }
36
+ },
37
+
38
+ emit() {
39
+ if ( isPaused ) {
40
+ isPending = true;
41
+ return;
42
+ }
43
+ notifyListeners();
44
+ },
45
+ };
46
+ }
package/tsconfig.json CHANGED
@@ -16,5 +16,7 @@
16
16
  "include": [
17
17
  "src/redux-store/metadata/**/*",
18
18
  "src/promise-middleware.js",
19
+ "src/utils",
20
+ "src/*.ts"
19
21
  ]
20
22
  }