From 16e98d49ffbd32e231099aa687bdc1bbc933c411 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Thu, 1 Oct 2020 14:41:06 +0200 Subject: [PATCH] Data: Build the basic data controls into every store (#25362) * createRegistrySelector: don't bind to defaultRegistry by default We intend to register some controls created by `createRegistryControl` as built-ins with every store. In order to do that, we need to break a dependency cycle where registry creation depends on store registration which depends on creating controls which depends on default registry. The solution is to remove the `defaultRegistry` binding, which is there only to satisfy a typechecker anyway, and doesn't have any runtime impact. * Data: add built-in controls for select, resolveSelect and dispatch Instead of shipping them in a separate `data-controls` package and require the store author to register them, build them in into every store. * data-controls: Remove implementation of built-in data controls The implementation and the unit test have been moved to the `@wordpress/data` package. The `data-controls` package now exposes only legacy aliases. * Update unit tests that depend on internal representation of data controls Updates unit tests of actions (in `block-directory`, `edit-site` and `core-data`) that inspect the internal properties (like `type`) of controls that the action generator yields. A test that verifies if the action _behaves_ correctly wouldn't need to be changed like that. * Block Editor: replace a home-grown SELECT control with builtin one * Mark the deprecated controls as deprecated * Comment out the deprecations until all Gutenberg packages are updated --- .../block-directory/src/store/test/actions.js | 6 +- packages/block-editor/src/store/actions.js | 49 +- packages/block-editor/src/store/controls.js | 28 - .../block-editor/src/store/test/actions.js | 492 ++++++++++-------- packages/core-data/src/test/actions.js | 16 +- packages/data-controls/CHANGELOG.md | 5 + packages/data-controls/README.md | 76 +-- packages/data-controls/src/index.js | 133 ++--- packages/data-controls/src/test/index.js | 120 +---- packages/data/README.md | 4 + packages/data/src/controls.js | 109 ++++ packages/data/src/factory.js | 16 - packages/data/src/index.js | 1 + packages/data/src/namespace-store/index.js | 18 +- packages/data/src/test/controls.js | 175 +++++++ packages/data/src/test/registry.js | 11 - packages/edit-site/src/store/test/actions.js | 10 +- 17 files changed, 663 insertions(+), 606 deletions(-) create mode 100644 packages/data/src/controls.js create mode 100644 packages/data/src/test/controls.js diff --git a/packages/block-directory/src/store/test/actions.js b/packages/block-directory/src/store/test/actions.js index b74f7f1a65772..516f6deef3fe1 100644 --- a/packages/block-directory/src/store/test/actions.js +++ b/packages/block-directory/src/store/test/actions.js @@ -81,7 +81,7 @@ describe( 'actions', () => { args: [], selectorName: 'getBlockTypes', storeKey: 'core/blocks', - type: 'SELECT', + type: '@@data/RESOLVE_SELECT', } ); expect( generator.next( [ block ] ).value ).toEqual( { @@ -143,7 +143,7 @@ describe( 'actions', () => { args: [], selectorName: 'getBlockTypes', storeKey: 'core/blocks', - type: 'SELECT', + type: '@@data/RESOLVE_SELECT', } ); expect( generator.next( [ inactiveBlock ] ).value ).toEqual( { @@ -274,7 +274,7 @@ describe( 'actions', () => { data: null, }; expect( generator.throw( apiError ).value ).toMatchObject( { - type: 'DISPATCH', + type: '@@data/DISPATCH', actionName: 'createErrorNotice', storeKey: 'core/notices', } ); diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index cd1365f38fd0c..e506e3271028e 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -14,10 +14,7 @@ import { } from '@wordpress/blocks'; import { speak } from '@wordpress/a11y'; import { __ } from '@wordpress/i18n'; -/** - * Internal dependencies - */ -import { select } from './controls'; +import { controls } from '@wordpress/data'; /** * Generator which will yield a default block insert action if there @@ -26,7 +23,7 @@ import { select } from './controls'; * replacement, etc). */ function* ensureDefaultBlock() { - const count = yield select( 'core/block-editor', 'getBlockCount' ); + const count = yield controls.select( 'core/block-editor', 'getBlockCount' ); // To avoid a focus loss when removing the last block, assure there is // always a default block if the last of the blocks have been removed. @@ -156,7 +153,7 @@ export function selectBlock( clientId, initialPosition = null ) { * @param {string} clientId Block client ID. */ export function* selectPreviousBlock( clientId ) { - const previousBlockClientId = yield select( + const previousBlockClientId = yield controls.select( 'core/block-editor', 'getPreviousBlockClientId', clientId @@ -175,7 +172,7 @@ export function* selectPreviousBlock( clientId ) { * @param {string} clientId Block client ID. */ export function* selectNextBlock( clientId ) { - const nextBlockClientId = yield select( + const nextBlockClientId = yield controls.select( 'core/block-editor', 'getNextBlockClientId', clientId @@ -303,9 +300,9 @@ export function* replaceBlocks( clientIds = castArray( clientIds ); blocks = getBlocksWithDefaultStylesApplied( castArray( blocks ), - yield select( 'core/block-editor', 'getSettings' ) + yield controls.select( 'core/block-editor', 'getSettings' ) ); - const rootClientId = yield select( + const rootClientId = yield controls.select( 'core/block-editor', 'getBlockRootClientId', first( clientIds ) @@ -313,7 +310,7 @@ export function* replaceBlocks( // Replace is valid if the new blocks can be inserted in the root block. for ( let index = 0; index < blocks.length; index++ ) { const block = blocks[ index ]; - const canInsertBlock = yield select( + const canInsertBlock = yield controls.select( 'core/block-editor', 'canInsertBlockType', block.name, @@ -386,7 +383,7 @@ export function* moveBlocksToPosition( toRootClientId = '', index ) { - const templateLock = yield select( + const templateLock = yield controls.select( 'core/block-editor', 'getTemplateLock', fromRootClientId @@ -419,7 +416,7 @@ export function* moveBlocksToPosition( return; } - const canInsertBlocks = yield select( + const canInsertBlocks = yield controls.select( 'core/block-editor', 'canInsertBlocks', clientIds, @@ -498,11 +495,11 @@ export function* insertBlocks( ) { blocks = getBlocksWithDefaultStylesApplied( castArray( blocks ), - yield select( 'core/block-editor', 'getSettings' ) + yield controls.select( 'core/block-editor', 'getSettings' ) ); const allowedBlocks = []; for ( const block of blocks ) { - const isValid = yield select( + const isValid = yield controls.select( 'core/block-editor', 'canInsertBlockType', block.name, @@ -608,12 +605,12 @@ export function* removeBlocks( clientIds, selectPrevious = true ) { } clientIds = castArray( clientIds ); - const rootClientId = yield select( + const rootClientId = yield controls.select( 'core/block-editor', 'getBlockRootClientId', clientIds[ 0 ] ); - const isLocked = yield select( + const isLocked = yield controls.select( 'core/block-editor', 'getTemplateLock', rootClientId @@ -626,7 +623,7 @@ export function* removeBlocks( clientIds, selectPrevious = true ) { if ( selectPrevious ) { previousBlockId = yield selectPreviousBlock( clientIds[ 0 ] ); } else { - previousBlockId = yield select( + previousBlockId = yield controls.select( 'core/block-editor', 'getPreviousBlockClientId', clientIds[ 0 ] @@ -951,12 +948,12 @@ export function* duplicateBlocks( clientIds, updateSelection = true ) { if ( ! clientIds && ! clientIds.length ) { return; } - const blocks = yield select( + const blocks = yield controls.select( 'core/block-editor', 'getBlocksByClientId', clientIds ); - const rootClientId = yield select( + const rootClientId = yield controls.select( 'core/block-editor', 'getBlockRootClientId', clientIds[ 0 ] @@ -976,7 +973,7 @@ export function* duplicateBlocks( clientIds, updateSelection = true ) { return; } - const lastSelectedIndex = yield select( + const lastSelectedIndex = yield controls.select( 'core/block-editor', 'getBlockIndex', last( castArray( clientIds ) ), @@ -1007,12 +1004,12 @@ export function* insertBeforeBlock( clientId ) { if ( ! clientId ) { return; } - const rootClientId = yield select( + const rootClientId = yield controls.select( 'core/block-editor', 'getBlockRootClientId', clientId ); - const isLocked = yield select( + const isLocked = yield controls.select( 'core/block-editor', 'getTemplateLock', rootClientId @@ -1021,7 +1018,7 @@ export function* insertBeforeBlock( clientId ) { return; } - const firstSelectedIndex = yield select( + const firstSelectedIndex = yield controls.select( 'core/block-editor', 'getBlockIndex', clientId, @@ -1039,12 +1036,12 @@ export function* insertAfterBlock( clientId ) { if ( ! clientId ) { return; } - const rootClientId = yield select( + const rootClientId = yield controls.select( 'core/block-editor', 'getBlockRootClientId', clientId ); - const isLocked = yield select( + const isLocked = yield controls.select( 'core/block-editor', 'getTemplateLock', rootClientId @@ -1053,7 +1050,7 @@ export function* insertAfterBlock( clientId ) { return; } - const firstSelectedIndex = yield select( + const firstSelectedIndex = yield controls.select( 'core/block-editor', 'getBlockIndex', clientId, diff --git a/packages/block-editor/src/store/controls.js b/packages/block-editor/src/store/controls.js index f0cbed509f313..83b4f45342546 100644 --- a/packages/block-editor/src/store/controls.js +++ b/packages/block-editor/src/store/controls.js @@ -1,32 +1,4 @@ -/** - * WordPress dependencies - */ -import { createRegistryControl } from '@wordpress/data'; - -/** - * Calls a selector using the current state. - * - * @param {string} storeName Store name. - * @param {string} selectorName Selector name. - * @param {Array} args Selector arguments. - * - * @return {Object} control descriptor. - */ -export function select( storeName, selectorName, ...args ) { - return { - type: 'SELECT', - storeName, - selectorName, - args, - }; -} - const controls = { - SELECT: createRegistryControl( - ( registry ) => ( { storeName, selectorName, args } ) => { - return registry.select( storeName )[ selectorName ]( ...args ); - } - ), SLEEP( { duration } ) { return new Promise( ( resolve ) => { setTimeout( resolve, duration ); diff --git a/packages/block-editor/src/store/test/actions.js b/packages/block-editor/src/store/test/actions.js index 772afcefda508..dc7ad96e3261a 100644 --- a/packages/block-editor/src/store/test/actions.js +++ b/packages/block-editor/src/store/test/actions.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { controls } from '@wordpress/data'; + /** * Internal dependencies */ @@ -32,7 +37,6 @@ import { updateBlockAttributes, updateBlockListSettings, } from '../actions'; -import { select } from '../controls'; describe( 'actions', () => { describe( 'resetBlocks', () => { @@ -142,19 +146,22 @@ describe( 'actions', () => { // Skip getSettings select. replaceBlockGenerator.next(); - expect( replaceBlockGenerator.next().value ).toEqual( { - args: [ 'chicken' ], - selectorName: 'getBlockRootClientId', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( replaceBlockGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'getBlockRootClientId', + 'chicken' + ) + ); - expect( replaceBlockGenerator.next().value ).toEqual( { - args: [ 'core/test-block', undefined ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( replaceBlockGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-block', + undefined + ) + ); expect( replaceBlockGenerator.next( true ).value ).toEqual( { type: 'REPLACE_BLOCKS', @@ -163,12 +170,9 @@ describe( 'actions', () => { time: expect.any( Number ), } ); - expect( replaceBlockGenerator.next().value ).toEqual( { - args: [], - selectorName: 'getBlockCount', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( replaceBlockGenerator.next().value ).toEqual( + controls.select( 'core/block-editor', 'getBlockCount' ) + ); expect( replaceBlockGenerator.next( 1 ) ).toEqual( { value: undefined, @@ -195,33 +199,35 @@ describe( 'actions', () => { blocks ); - expect( replaceBlockGenerator.next().value ).toEqual( { - args: [], - selectorName: 'getSettings', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( replaceBlockGenerator.next().value ).toEqual( + controls.select( 'core/block-editor', 'getSettings' ) + ); - expect( replaceBlockGenerator.next().value ).toEqual( { - args: [ 'chicken' ], - selectorName: 'getBlockRootClientId', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( replaceBlockGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'getBlockRootClientId', + 'chicken' + ) + ); - expect( replaceBlockGenerator.next().value ).toEqual( { - args: [ 'core/test-ribs', undefined ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( replaceBlockGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-ribs', + undefined + ) + ); - expect( replaceBlockGenerator.next( true ).value ).toEqual( { - args: [ 'core/test-chicken', undefined ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( replaceBlockGenerator.next( true ).value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-chicken', + undefined + ) + ); expect( replaceBlockGenerator.next( false ) ).toEqual( { value: undefined, @@ -249,26 +255,31 @@ describe( 'actions', () => { // Skip getSettings select. replaceBlockGenerator.next(); - expect( replaceBlockGenerator.next().value ).toEqual( { - args: [ 'chicken' ], - selectorName: 'getBlockRootClientId', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( replaceBlockGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'getBlockRootClientId', + 'chicken' + ) + ); - expect( replaceBlockGenerator.next().value ).toEqual( { - args: [ 'core/test-ribs', undefined ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( replaceBlockGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-ribs', + undefined + ) + ); - expect( replaceBlockGenerator.next( true ).value ).toEqual( { - args: [ 'core/test-chicken', undefined ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( replaceBlockGenerator.next( true ).value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-chicken', + undefined + ) + ); expect( replaceBlockGenerator.next( true ).value ).toEqual( { type: 'REPLACE_BLOCKS', @@ -277,12 +288,9 @@ describe( 'actions', () => { time: expect.any( Number ), } ); - expect( replaceBlockGenerator.next().value ).toEqual( { - args: [], - selectorName: 'getBlockCount', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( replaceBlockGenerator.next().value ).toEqual( + controls.select( 'core/block-editor', 'getBlockCount' ) + ); expect( replaceBlockGenerator.next( 1 ) ).toEqual( { value: undefined, @@ -348,12 +356,14 @@ describe( 'actions', () => { // Skip getSettings select. insertBlockGenerator.next(); - expect( insertBlockGenerator.next().value ).toEqual( { - args: [ 'core/test-block', 'testclientid' ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( insertBlockGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-block', + 'testclientid' + ) + ); expect( insertBlockGenerator.next( true ) ).toEqual( { done: true, @@ -392,12 +402,9 @@ describe( 'actions', () => { false ); - expect( insertBlocksGenerator.next().value ).toEqual( { - args: [], - selectorName: 'getSettings', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( insertBlocksGenerator.next().value ).toEqual( + controls.select( 'core/block-editor', 'getSettings' ) + ); expect( insertBlocksGenerator.next( { @@ -408,26 +415,32 @@ describe( 'actions', () => { }, }, } ).value - ).toEqual( { - args: [ 'core/test-ribs', 'testrootid' ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-ribs', + 'testrootid' + ) + ); - expect( insertBlocksGenerator.next( true ).value ).toEqual( { - args: [ 'core/test-chicken', 'testrootid' ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( insertBlocksGenerator.next( true ).value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-chicken', + 'testrootid' + ) + ); - expect( insertBlocksGenerator.next( true ).value ).toEqual( { - args: [ 'core/test-chicken-ribs', 'testrootid' ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( insertBlocksGenerator.next( true ).value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-chicken-ribs', + 'testrootid' + ) + ); expect( insertBlocksGenerator.next( true ) ).toEqual( { done: true, @@ -469,12 +482,9 @@ describe( 'actions', () => { false ); - expect( insertBlocksGenerator.next().value ).toEqual( { - args: [], - selectorName: 'getSettings', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( insertBlocksGenerator.next().value ).toEqual( + controls.select( 'core/block-editor', 'getSettings' ) + ); expect( insertBlocksGenerator.next( { @@ -484,12 +494,14 @@ describe( 'actions', () => { }, }, } ).value - ).toEqual( { - args: [ 'core/test-ribs', 'testrootid' ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-ribs', + 'testrootid' + ) + ); expect( insertBlocksGenerator.next( true ) ).toEqual( { done: true, @@ -533,26 +545,32 @@ describe( 'actions', () => { // Skip getSettings select. insertBlocksGenerator.next(); - expect( insertBlocksGenerator.next().value ).toEqual( { - args: [ 'core/test-ribs', 'testrootid' ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( insertBlocksGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-ribs', + 'testrootid' + ) + ); - expect( insertBlocksGenerator.next( true ).value ).toEqual( { - args: [ 'core/test-chicken', 'testrootid' ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( insertBlocksGenerator.next( true ).value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-chicken', + 'testrootid' + ) + ); - expect( insertBlocksGenerator.next( false ).value ).toEqual( { - args: [ 'core/test-chicken-ribs', 'testrootid' ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( insertBlocksGenerator.next( false ).value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-chicken-ribs', + 'testrootid' + ) + ); expect( insertBlocksGenerator.next( true ) ).toEqual( { done: true, @@ -588,19 +606,23 @@ describe( 'actions', () => { // Skip getSettings select. insertBlocksGenerator.next(); - expect( insertBlocksGenerator.next().value ).toEqual( { - args: [ 'core/test-ribs', 'testrootid' ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( insertBlocksGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-ribs', + 'testrootid' + ) + ); - expect( insertBlocksGenerator.next( false ).value ).toEqual( { - args: [ 'core/test-chicken', 'testrootid' ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( insertBlocksGenerator.next( false ).value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-chicken', + 'testrootid' + ) + ); expect( insertBlocksGenerator.next( false ) ).toEqual( { done: true, @@ -635,26 +657,32 @@ describe( 'actions', () => { // Skip getSettings select. insertBlocksGenerator.next(); - expect( insertBlocksGenerator.next().value ).toEqual( { - args: [ 'core/test-ribs', 'testrootid' ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( insertBlocksGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-ribs', + 'testrootid' + ) + ); - expect( insertBlocksGenerator.next( true ).value ).toEqual( { - args: [ 'core/test-chicken', 'testrootid' ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( insertBlocksGenerator.next( true ).value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-chicken', + 'testrootid' + ) + ); - expect( insertBlocksGenerator.next( false ).value ).toEqual( { - args: [ 'core/test-chicken-ribs', 'testrootid' ], - selectorName: 'canInsertBlockType', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( insertBlocksGenerator.next( false ).value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlockType', + 'core/test-chicken-ribs', + 'testrootid' + ) + ); expect( insertBlocksGenerator.next( true ) ).toEqual( { done: true, @@ -708,14 +736,22 @@ describe( 'actions', () => { const actions = Array.from( removeBlocks( clientIds ) ); expect( actions ).toEqual( [ - select( 'core/block-editor', 'getBlockRootClientId', clientId ), - select( 'core/block-editor', 'getTemplateLock', undefined ), + controls.select( + 'core/block-editor', + 'getBlockRootClientId', + clientId + ), + controls.select( + 'core/block-editor', + 'getTemplateLock', + undefined + ), selectPreviousBlock( clientId ), { type: 'REMOVE_BLOCKS', clientIds, }, - select( 'core/block-editor', 'getBlockCount' ), + controls.select( 'core/block-editor', 'getBlockCount' ), ] ); } ); } ); @@ -729,12 +765,13 @@ describe( 'actions', () => { 5 ); - expect( moveBlockToPositionGenerator.next().value ).toEqual( { - args: [ 'ribs' ], - selectorName: 'getTemplateLock', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( moveBlockToPositionGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'getTemplateLock', + 'ribs' + ) + ); expect( moveBlockToPositionGenerator.next( 'insert' ).value @@ -757,12 +794,13 @@ describe( 'actions', () => { 5 ); - expect( moveBlockToPositionGenerator.next().value ).toEqual( { - args: [ 'ribs' ], - selectorName: 'getTemplateLock', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( moveBlockToPositionGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'getTemplateLock', + 'ribs' + ) + ); expect( moveBlockToPositionGenerator.next( 'all' ) ).toEqual( { done: true, @@ -778,12 +816,13 @@ describe( 'actions', () => { 5 ); - expect( moveBlockToPositionGenerator.next().value ).toEqual( { - args: [ 'ribs' ], - selectorName: 'getTemplateLock', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( moveBlockToPositionGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'getTemplateLock', + 'ribs' + ) + ); expect( moveBlockToPositionGenerator.next( 'insert' ) ).toEqual( { done: true, @@ -799,19 +838,22 @@ describe( 'actions', () => { 5 ); - expect( moveBlockToPositionGenerator.next().value ).toEqual( { - args: [ 'ribs' ], - selectorName: 'getTemplateLock', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( moveBlockToPositionGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'getTemplateLock', + 'ribs' + ) + ); - expect( moveBlockToPositionGenerator.next().value ).toEqual( { - args: [ [ 'chicken' ], 'chicken-ribs' ], - selectorName: 'canInsertBlocks', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( moveBlockToPositionGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlocks', + [ 'chicken' ], + 'chicken-ribs' + ) + ); expect( moveBlockToPositionGenerator.next( true ).value ).toEqual( { type: 'MOVE_BLOCKS_TO_POSITION', @@ -835,19 +877,22 @@ describe( 'actions', () => { 5 ); - expect( moveBlockToPositionGenerator.next().value ).toEqual( { - args: [ 'ribs' ], - selectorName: 'getTemplateLock', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( moveBlockToPositionGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'getTemplateLock', + 'ribs' + ) + ); - expect( moveBlockToPositionGenerator.next().value ).toEqual( { - args: [ [ 'chicken' ], 'chicken-ribs' ], - selectorName: 'canInsertBlocks', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( moveBlockToPositionGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'canInsertBlocks', + [ 'chicken' ], + 'chicken-ribs' + ) + ); expect( moveBlockToPositionGenerator.next( false ) ).toEqual( { done: true, @@ -865,12 +910,13 @@ describe( 'actions', () => { 5 ); - expect( moveBlockToPositionGenerator.next().value ).toEqual( { - args: [ 'ribs' ], - selectorName: 'getTemplateLock', - storeName: 'core/block-editor', - type: 'SELECT', - } ); + expect( moveBlockToPositionGenerator.next().value ).toEqual( + controls.select( + 'core/block-editor', + 'getTemplateLock', + 'ribs' + ) + ); expect( moveBlockToPositionGenerator.next().value ).toEqual( { type: 'MOVE_BLOCKS_TO_POSITION', @@ -891,14 +937,22 @@ describe( 'actions', () => { const actions = Array.from( removeBlock( clientId ) ); expect( actions ).toEqual( [ - select( 'core/block-editor', 'getBlockRootClientId', clientId ), - select( 'core/block-editor', 'getTemplateLock', undefined ), + controls.select( + 'core/block-editor', + 'getBlockRootClientId', + clientId + ), + controls.select( + 'core/block-editor', + 'getTemplateLock', + undefined + ), selectPreviousBlock( clientId ), { type: 'REMOVE_BLOCKS', clientIds: [ clientId ], }, - select( 'core/block-editor', 'getBlockCount' ), + controls.select( 'core/block-editor', 'getBlockCount' ), ] ); } ); @@ -908,9 +962,17 @@ describe( 'actions', () => { const actions = Array.from( removeBlock( clientId, false ) ); expect( actions ).toEqual( [ - select( 'core/block-editor', 'getBlockRootClientId', clientId ), - select( 'core/block-editor', 'getTemplateLock', undefined ), - select( + controls.select( + 'core/block-editor', + 'getBlockRootClientId', + clientId + ), + controls.select( + 'core/block-editor', + 'getTemplateLock', + undefined + ), + controls.select( 'core/block-editor', 'getPreviousBlockClientId', 'myclientid' @@ -919,7 +981,7 @@ describe( 'actions', () => { type: 'REMOVE_BLOCKS', clientIds: [ clientId ], }, - select( 'core/block-editor', 'getBlockCount' ), + controls.select( 'core/block-editor', 'getBlockCount' ), ] ); } ); } ); diff --git a/packages/core-data/src/test/actions.js b/packages/core-data/src/test/actions.js index b1d1b7023f820..2527c8bbe096e 100644 --- a/packages/core-data/src/test/actions.js +++ b/packages/core-data/src/test/actions.js @@ -87,11 +87,11 @@ describe( 'saveEntityRecord', () => { // Should select __experimentalGetEntityRecordNoResolver selector (as opposed to getEntityRecord) // see https://github.com/WordPress/gutenberg/pull/19752#discussion_r368498318. - expect( fulfillment.next().value.type ).toBe( 'SYNC_SELECT' ); + expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); expect( fulfillment.next().value.selectorName ).toBe( '__experimentalGetEntityRecordNoResolver' ); - expect( fulfillment.next().value.type ).toBe( 'SYNC_SELECT' ); + expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); expect( fulfillment.next().value.type ).toBe( 'RECEIVE_ITEMS' ); const { value: apiFetchAction } = fulfillment.next( {} ); expect( apiFetchAction.request ).toEqual( { @@ -129,9 +129,9 @@ describe( 'saveEntityRecord', () => { expect( fulfillment.next( entities ).value.type ).toBe( 'SAVE_ENTITY_RECORD_START' ); - expect( fulfillment.next().value.type ).toBe( 'SYNC_SELECT' ); - expect( fulfillment.next().value.type ).toBe( 'SYNC_SELECT' ); - expect( fulfillment.next().value.type ).toBe( 'SYNC_SELECT' ); + expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); + expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); + expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); expect( fulfillment.next().value.type ).toBe( 'RECEIVE_ITEMS' ); const { value: apiFetchAction } = fulfillment.next( {} ); expect( apiFetchAction.request ).toEqual( { @@ -166,9 +166,9 @@ describe( 'saveEntityRecord', () => { expect( fulfillment.next( entities ).value.type ).toBe( 'SAVE_ENTITY_RECORD_START' ); - expect( fulfillment.next().value.type ).toBe( 'SYNC_SELECT' ); - expect( fulfillment.next().value.type ).toBe( 'SYNC_SELECT' ); - expect( fulfillment.next().value.type ).toBe( 'SYNC_SELECT' ); + expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); + expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); + expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); expect( fulfillment.next().value.type ).toBe( 'RECEIVE_ITEMS' ); const { value: apiFetchAction } = fulfillment.next( {} ); expect( apiFetchAction.request ).toEqual( { diff --git a/packages/data-controls/CHANGELOG.md b/packages/data-controls/CHANGELOG.md index 7c95215fe630e..2fdf3382911e9 100644 --- a/packages/data-controls/CHANGELOG.md +++ b/packages/data-controls/CHANGELOG.md @@ -6,6 +6,11 @@ - Expose the `syncSelect` control for synchronous calls of registry selectors +### Deprecations + +- Deprecated the `syncSelect`, `select` and `dispatch` controls that are now part of + `@wordpress/data` and built in by default in every data store. + ## 1.4.0 (2019-11-14) ### Documentation diff --git a/packages/data-controls/README.md b/packages/data-controls/README.md index d0091d86a5a62..d9a346956e00c 100644 --- a/packages/data-controls/README.md +++ b/packages/data-controls/README.md @@ -78,88 +78,30 @@ _Returns_ # **dispatch** -Dispatches a control action for triggering a registry dispatch. - -_Usage_ - -```js -import { dispatch } from '@wordpress/data-controls'; - -// Action generator using dispatch -export function* myAction() { - yield dispatch( 'core/edit-post', 'togglePublishSidebar' ); - // do some other things. -} -``` +Control for dispatching an action in a registered data store. +Alias for the `dispatch` control in the `@wordpress/data` package. _Parameters_ -- _storeKey_ `string`: The key for the store the action belongs to -- _actionName_ `string`: The name of the action to dispatch -- _args_ `Array`: Arguments for the dispatch action. - -_Returns_ - -- `Object`: The control descriptor. +- _args_ `Array`: Arguments passed without change to the `@wordpress/data` control. # **select** -Dispatches a control action for triggering and resolving a registry select. - -Note: when this control action is handled, it automatically considers -selectors that may have a resolver. In such case, it will return a `Promise` that resolves -after the selector finishes resolving, with the final result value. - -_Usage_ - -```js -import { select } from '@wordpress/data-controls'; - -// Action generator using select -export function* myAction() { - const isSidebarOpened = yield select( 'core/edit-post', 'isEditorSideBarOpened' ); - // do stuff with the result from the select. -} -``` +Control for resolving a selector in a registered data store. +Alias for the `resolveSelect` built-in control in the `@wordpress/data` package. _Parameters_ -- _storeKey_ `string`: The key for the store the selector belongs to -- _selectorName_ `string`: The name of the selector -- _args_ `Array`: Arguments for the selector. - -_Returns_ - -- `Object`: The control descriptor. +- _args_ `Array`: Arguments passed without change to the `@wordpress/data` control. # **syncSelect** -Dispatches a control action for triggering a synchronous registry select. - -Note: This control synchronously returns the current selector value, triggering the -resolution, but not waiting for it. - -_Usage_ - -```js -import { syncSelect } from '@wordpress/data-controls'; - -// Action generator using `syncSelect`. -export function* myAction() { - const isEditorSideBarOpened = yield syncSelect( 'core/edit-post', 'isEditorSideBarOpened' ); - // Do stuff with the result from the `syncSelect`. -} -``` +Control for calling a selector in a registered data store. +Alias for the `select` built-in control in the `@wordpress/data` package. _Parameters_ -- _storeKey_ `string`: The key for the store the selector belongs to. -- _selectorName_ `string`: The name of the selector. -- _args_ `Array`: Arguments for the selector. - -_Returns_ - -- `Object`: The control descriptor. +- _args_ `Array`: Arguments passed without change to the `@wordpress/data` control. diff --git a/packages/data-controls/src/index.js b/packages/data-controls/src/index.js index 19eca67cf56b8..4c618e086d885 100644 --- a/packages/data-controls/src/index.js +++ b/packages/data-controls/src/index.js @@ -2,7 +2,9 @@ * WordPress dependencies */ import triggerFetch from '@wordpress/api-fetch'; -import { createRegistryControl } from '@wordpress/data'; +import { controls as dataControls } from '@wordpress/data'; +// TODO: mark the deprecated controls after all Gutenberg usages are removed +// import deprecated from '@wordpress/deprecated'; /** * Dispatches a control action for triggering an api fetch call. @@ -23,105 +25,53 @@ import { createRegistryControl } from '@wordpress/data'; * * @return {Object} The control descriptor. */ -export const apiFetch = ( request ) => { +export function apiFetch( request ) { return { type: 'API_FETCH', request, }; -}; +} /** - * Dispatches a control action for triggering and resolving a registry select. - * - * Note: when this control action is handled, it automatically considers - * selectors that may have a resolver. In such case, it will return a `Promise` that resolves - * after the selector finishes resolving, with the final result value. - * - * @param {string} storeKey The key for the store the selector belongs to - * @param {string} selectorName The name of the selector - * @param {Array} args Arguments for the selector. - * - * @example - * ```js - * import { select } from '@wordpress/data-controls'; - * - * // Action generator using select - * export function* myAction() { - * const isSidebarOpened = yield select( 'core/edit-post', 'isEditorSideBarOpened' ); - * // do stuff with the result from the select. - * } - * ``` + * Control for resolving a selector in a registered data store. + * Alias for the `resolveSelect` built-in control in the `@wordpress/data` package. * - * @return {Object} The control descriptor. + * @param {Array} args Arguments passed without change to the `@wordpress/data` control. */ -export function select( storeKey, selectorName, ...args ) { - return { - type: 'SELECT', - storeKey, - selectorName, - args, - }; +export function select( ...args ) { + // deprecated( '`select` control in `@wordpress/data-controls`', { + // alternative: 'built-in `resolveSelect` control in `@wordpress/data`', + // } ); + + return dataControls.resolveSelect( ...args ); } /** - * Dispatches a control action for triggering a synchronous registry select. - * - * Note: This control synchronously returns the current selector value, triggering the - * resolution, but not waiting for it. - * - * @param {string} storeKey The key for the store the selector belongs to. - * @param {string} selectorName The name of the selector. - * @param {Array} args Arguments for the selector. - * - * @example - * ```js - * import { syncSelect } from '@wordpress/data-controls'; + * Control for calling a selector in a registered data store. + * Alias for the `select` built-in control in the `@wordpress/data` package. * - * // Action generator using `syncSelect`. - * export function* myAction() { - * const isEditorSideBarOpened = yield syncSelect( 'core/edit-post', 'isEditorSideBarOpened' ); - * // Do stuff with the result from the `syncSelect`. - * } - * ``` - * - * @return {Object} The control descriptor. + * @param {Array} args Arguments passed without change to the `@wordpress/data` control. */ -export function syncSelect( storeKey, selectorName, ...args ) { - return { - type: 'SYNC_SELECT', - storeKey, - selectorName, - args, - }; +export function syncSelect( ...args ) { + // deprecated( '`syncSelect` control in `@wordpress/data-controls`', { + // alternative: 'built-in `select` control in `@wordpress/data`', + // } ); + + return dataControls.select( ...args ); } /** - * Dispatches a control action for triggering a registry dispatch. + * Control for dispatching an action in a registered data store. + * Alias for the `dispatch` control in the `@wordpress/data` package. * - * @param {string} storeKey The key for the store the action belongs to - * @param {string} actionName The name of the action to dispatch - * @param {Array} args Arguments for the dispatch action. - * - * @example - * ```js - * import { dispatch } from '@wordpress/data-controls'; - * - * // Action generator using dispatch - * export function* myAction() { - * yield dispatch( 'core/edit-post', 'togglePublishSidebar' ); - * // do some other things. - * } - * ``` - * - * @return {Object} The control descriptor. + * @param {Array} args Arguments passed without change to the `@wordpress/data` control. */ -export function dispatch( storeKey, actionName, ...args ) { - return { - type: 'DISPATCH', - storeKey, - actionName, - args, - }; +export function dispatch( ...args ) { + // deprecated( '`dispatch` control in `@wordpress/data-controls`', { + // alternative: 'built-in `dispatch` control in `@wordpress/data`', + // } ); + + return dataControls.dispatch( ...args ); } /** @@ -156,23 +106,4 @@ export const controls = { API_FETCH( { request } ) { return triggerFetch( request ); }, - SELECT: createRegistryControl( - ( registry ) => ( { storeKey, selectorName, args } ) => { - return registry[ - registry.select( storeKey )[ selectorName ].hasResolver - ? '__experimentalResolveSelect' - : 'select' - ]( storeKey )[ selectorName ]( ...args ); - } - ), - SYNC_SELECT: createRegistryControl( - ( registry ) => ( { storeKey, selectorName, args } ) => { - return registry.select( storeKey )[ selectorName ]( ...args ); - } - ), - DISPATCH: createRegistryControl( - ( registry ) => ( { storeKey, actionName, args } ) => { - return registry.dispatch( storeKey )[ actionName ]( ...args ); - } - ), }; diff --git a/packages/data-controls/src/test/index.js b/packages/data-controls/src/test/index.js index f7c1fe297487d..a9fedde4e289c 100644 --- a/packages/data-controls/src/test/index.js +++ b/packages/data-controls/src/test/index.js @@ -19,127 +19,9 @@ describe( 'controls', () => { controls.API_FETCH( { request: '' } ); expect( triggerFetch ).toHaveBeenCalledTimes( 1 ); } ); - it( 'invokes the triggerFetch funcion with the passed in request', () => { + it( 'invokes the triggerFetch function with the passed in request', () => { controls.API_FETCH( { request: 'foo' } ); expect( triggerFetch ).toHaveBeenCalledWith( 'foo' ); } ); } ); - describe( 'SELECT', () => { - const selectorWithUndefinedResolver = jest.fn(); - const selectorWithResolver = jest.fn(); - selectorWithResolver.hasResolver = true; - const selectorWithFalseResolver = jest.fn(); - selectorWithFalseResolver.hasResolver = false; - const hasFinishedResolution = jest.fn(); - - const mockStore = { - selectorWithResolver, - selectorWithUndefinedResolver, - selectorWithFalseResolver, - }; - - const registryMock = { - __experimentalResolveSelect: ( storeKey ) => { - const stores = { - mockStore, - }; - return stores[ storeKey ]; - }, - select: ( storeKey ) => { - const stores = { - mockStore, - 'core/data': { - hasFinishedResolution, - }, - }; - return stores[ storeKey ]; - }, - }; - const getSelectorArgs = ( storeKey, selectorName, ...args ) => ( { - storeKey, - selectorName, - args, - } ); - beforeEach( () => { - selectorWithUndefinedResolver.mockReturnValue( 'foo' ); - selectorWithFalseResolver.mockReturnValue( 'bar' ); - hasFinishedResolution.mockReturnValue( false ); - selectorWithResolver.mockResolvedValue( 'resolved' ); - } ); - afterEach( () => { - selectorWithUndefinedResolver.mockClear(); - selectorWithResolver.mockClear(); - selectorWithFalseResolver.mockClear(); - hasFinishedResolution.mockClear(); - } ); - it( 'invokes selector with undefined resolver', () => { - const testControl = controls.SELECT( registryMock ); - const value = testControl( - getSelectorArgs( 'mockStore', 'selectorWithUndefinedResolver' ) - ); - expect( value ).toBe( 'foo' ); - expect( selectorWithUndefinedResolver ).toHaveBeenCalled(); - expect( hasFinishedResolution ).not.toHaveBeenCalled(); - } ); - it( 'invokes selector with resolver set to false', () => { - const testControl = controls.SELECT( registryMock ); - const value = testControl( - getSelectorArgs( 'mockStore', 'selectorWithFalseResolver' ) - ); - expect( value ).toBe( 'bar' ); - expect( selectorWithFalseResolver ).toHaveBeenCalled(); - expect( hasFinishedResolution ).not.toHaveBeenCalled(); - } ); - describe( 'invokes selector with resolver set to true', () => { - const testControl = controls.SELECT( registryMock ); - it( 'returns a promise', async () => { - const value = testControl( - getSelectorArgs( 'mockStore', 'selectorWithResolver' ) - ); - await expect( value ).resolves.toBe( 'resolved' ); - expect( selectorWithResolver ).toHaveBeenCalled(); - } ); - it( - 'selector with resolver resolves to expected result when ' + - 'finished', - async () => { - const value = testControl( - getSelectorArgs( 'mockStore', 'selectorWithResolver' ) - ); - hasFinishedResolution.mockReturnValue( true ); - expect( selectorWithResolver ).toHaveBeenCalled(); - await expect( value ).resolves.toBe( 'resolved' ); - } - ); - } ); - } ); - describe( 'DISPATCH', () => { - const mockDispatch = jest.fn(); - const registryMock = { - dispatch: ( storeKey ) => { - const stores = { - mockStore: { - mockDispatch, - }, - }; - return stores[ storeKey ]; - }, - }; - beforeEach( () => { - mockDispatch.mockReturnValue( 'foo' ); - } ); - afterEach( () => { - mockDispatch.mockClear(); - } ); - it( 'invokes dispatch action', () => { - const testControl = controls.DISPATCH( registryMock ); - const value = testControl( { - storeKey: 'mockStore', - actionName: 'mockDispatch', - args: [], - } ); - expect( value ).toBe( 'foo' ); - expect( mockDispatch ).toHaveBeenCalled(); - } ); - } ); } ); diff --git a/packages/data/README.md b/packages/data/README.md index fca2408a8141c..8574f850247d4 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -332,6 +332,10 @@ _Returns_ - `Function`: A reducer that invokes every reducer inside the reducers object, and constructs a state object with the same shape. +# **controls** + +Undocumented declaration. + # **createRegistry** Creates a new store registry, given an optional object of initial store diff --git a/packages/data/src/controls.js b/packages/data/src/controls.js new file mode 100644 index 0000000000000..3fb0a6a4e607a --- /dev/null +++ b/packages/data/src/controls.js @@ -0,0 +1,109 @@ +/** + * Internal dependencies + */ +import { createRegistryControl } from './factory'; + +const SELECT = '@@data/SELECT'; +const RESOLVE_SELECT = '@@data/RESOLVE_SELECT'; +const DISPATCH = '@@data/DISPATCH'; + +/** + * Dispatches a control action for triggering a synchronous registry select. + * + * Note: This control synchronously returns the current selector value, triggering the + * resolution, but not waiting for it. + * + * @param {string} storeKey The key for the store the selector belongs to. + * @param {string} selectorName The name of the selector. + * @param {Array} args Arguments for the selector. + * + * @example + * ```js + * import { controls } from '@wordpress/data'; + * + * // Action generator using `select`. + * export function* myAction() { + * const isEditorSideBarOpened = yield controls.select( 'core/edit-post', 'isEditorSideBarOpened' ); + * // Do stuff with the result from the `select`. + * } + * ``` + * + * @return {Object} The control descriptor. + */ +function select( storeKey, selectorName, ...args ) { + return { type: SELECT, storeKey, selectorName, args }; +} + +/** + * Dispatches a control action for triggering and resolving a registry select. + * + * Note: when this control action is handled, it automatically considers + * selectors that may have a resolver. In such case, it will return a `Promise` that resolves + * after the selector finishes resolving, with the final result value. + * + * @param {string} storeKey The key for the store the selector belongs to + * @param {string} selectorName The name of the selector + * @param {Array} args Arguments for the selector. + * + * @example + * ```js + * import { controls } from '@wordpress/data'; + * + * // Action generator using resolveSelect + * export function* myAction() { + * const isSidebarOpened = yield controls.resolveSelect( 'core/edit-post', 'isEditorSideBarOpened' ); + * // do stuff with the result from the select. + * } + * ``` + * + * @return {Object} The control descriptor. + */ +function resolveSelect( storeKey, selectorName, ...args ) { + return { type: RESOLVE_SELECT, storeKey, selectorName, args }; +} + +/** + * Dispatches a control action for triggering a registry dispatch. + * + * @param {string} storeKey The key for the store the action belongs to + * @param {string} actionName The name of the action to dispatch + * @param {Array} args Arguments for the dispatch action. + * + * @example + * ```js + * import { controls } from '@wordpress/data-controls'; + * + * // Action generator using dispatch + * export function* myAction() { + * yield controls.dispatch( 'core/edit-post', 'togglePublishSidebar' ); + * // do some other things. + * } + * ``` + * + * @return {Object} The control descriptor. + */ +function dispatch( storeKey, actionName, ...args ) { + return { type: DISPATCH, storeKey, actionName, args }; +} + +export const controls = { select, resolveSelect, dispatch }; + +export const builtinControls = { + [ SELECT ]: createRegistryControl( + ( registry ) => ( { storeKey, selectorName, args } ) => + registry.select( storeKey )[ selectorName ]( ...args ) + ), + [ RESOLVE_SELECT ]: createRegistryControl( + ( registry ) => ( { storeKey, selectorName, args } ) => { + const method = registry.select( storeKey )[ selectorName ] + .hasResolver + ? '__experimentalResolveSelect' + : 'select'; + return registry[ method ]( storeKey )[ selectorName ]( ...args ); + } + ), + [ DISPATCH ]: createRegistryControl( + ( registry ) => ( { storeKey, actionName, args } ) => + registry.dispatch( storeKey )[ actionName ]( ...args ) + ), +}; diff --git a/packages/data/src/factory.js b/packages/data/src/factory.js index 5dab0f9bf2c1d..400f4ffe15c0a 100644 --- a/packages/data/src/factory.js +++ b/packages/data/src/factory.js @@ -1,10 +1,3 @@ -/** - * Internal dependencies - */ -import defaultRegistry from './default-registry'; - -/** @typedef {import('./registry').WPDataRegistry} WPDataRegistry */ - /** * Creates a selector function that takes additional curried argument with the * registry `select` function. While a regular selector has signature @@ -58,15 +51,6 @@ export function createRegistrySelector( registrySelector ) { */ selector.isRegistrySelector = true; - /** - * Registry on which to call `select`. The `defaultRegistry` value is overwritten with - * the actual registry when the selector is registered with a store. Therefore, it's never - * used and is assigned here only to satisfy the type constraint of the `registry` field. - * - * @type {WPDataRegistry} - */ - selector.registry = defaultRegistry; - return selector; } diff --git a/packages/data/src/index.js b/packages/data/src/index.js index b0f1156db81d9..911b6e0298379 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -22,6 +22,7 @@ export { useDispatch } from './components/use-dispatch'; export { AsyncModeProvider } from './components/async-mode-provider'; export { createRegistry } from './registry'; export { createRegistrySelector, createRegistryControl } from './factory'; +export { controls } from './controls'; /** * Object of available plugins to use with a registry. diff --git a/packages/data/src/namespace-store/index.js b/packages/data/src/namespace-store/index.js index 3cf0919db018b..8ce882f0e6be8 100644 --- a/packages/data/src/namespace-store/index.js +++ b/packages/data/src/namespace-store/index.js @@ -14,6 +14,7 @@ import createReduxRoutineMiddleware from '@wordpress/redux-routine'; /** * Internal dependencies */ +import { builtinControls } from '../controls'; import promise from '../promise-middleware'; import createResolversCacheMiddleware from '../resolvers-cache-middleware'; import metadataReducer from './metadata/reducer'; @@ -156,18 +157,21 @@ export default function createNamespace( key, options, registry ) { * @return {Object} Newly created redux store. */ function createReduxStore( key, options, registry ) { + const controls = { + ...options.controls, + ...builtinControls, + }; + + const normalizedControls = mapValues( controls, ( control ) => + control.isRegistryControl ? control( registry ) : control + ); + const middlewares = [ createResolversCacheMiddleware( registry, key ), promise, + createReduxRoutineMiddleware( normalizedControls ), ]; - if ( options.controls ) { - const normalizedControls = mapValues( options.controls, ( control ) => { - return control.isRegistryControl ? control( registry ) : control; - } ); - middlewares.push( createReduxRoutineMiddleware( normalizedControls ) ); - } - const enhancers = [ applyMiddleware( ...middlewares ) ]; if ( typeof window !== 'undefined' && diff --git a/packages/data/src/test/controls.js b/packages/data/src/test/controls.js new file mode 100644 index 0000000000000..dfe8ff9798ee4 --- /dev/null +++ b/packages/data/src/test/controls.js @@ -0,0 +1,175 @@ +/** + * Internal dependencies + */ +import { createRegistry, controls } from '..'; + +jest.useRealTimers(); + +describe( 'controls', () => { + // create a registry with store to test select controls + function createSelectTestRegistry() { + const registry = createRegistry(); + + // State is initially null and can receive data. + // Typical for fetching data from remote locations. + const reducer = ( state = null, action ) => { + switch ( action.type ) { + case 'RECEIVE': + return action.data; + default: + return state; + } + }; + + // Select state both without and with a resolver + const selectors = { + selectorWithoutResolver: ( state ) => state, + selectorWithResolver: ( state ) => state, + }; + + // The resolver receives data after a little delay + const resolvers = { + *selectorWithResolver() { + yield new Promise( ( r ) => setTimeout( r, 10 ) ); + return { type: 'RECEIVE', data: 'resolved-data' }; + }, + }; + + // actions that call the tested controls and return the selected value + const actions = { + *resolveWithoutResolver() { + const value = yield controls.resolveSelect( + 'test/select', + 'selectorWithoutResolver' + ); + return value; + }, + *resolveWithResolver() { + const value = yield controls.resolveSelect( + 'test/select', + 'selectorWithResolver' + ); + return value; + }, + *selectWithoutResolver() { + const value = yield controls.select( + 'test/select', + 'selectorWithoutResolver' + ); + return value; + }, + *selectWithResolver() { + const value = yield controls.select( + 'test/select', + 'selectorWithResolver' + ); + return value; + }, + }; + + registry.registerStore( 'test/select', { + reducer, + actions, + selectors, + resolvers, + } ); + + return registry; + } + + describe( 'resolveSelect', () => { + it( 'invokes selector without a resolver', async () => { + const registry = createSelectTestRegistry(); + const value = await registry + .dispatch( 'test/select' ) + .resolveWithoutResolver(); + // Returns the state value without waiting for any resolver. + expect( value ).toBe( null ); + } ); + + it( 'resolves selector with a resolver', async () => { + const registry = createSelectTestRegistry(); + const value = await registry + .dispatch( 'test/select' ) + .resolveWithResolver(); + // Waits for the resolver to resolve and returns the resolved data. + // Never returns the initial `null` state. + expect( value ).toBe( 'resolved-data' ); + } ); + } ); + + describe( 'select', () => { + it( 'invokes selector without a resolver', async () => { + const registry = createSelectTestRegistry(); + const value = await registry + .dispatch( 'test/select' ) + .selectWithoutResolver(); + // Returns the state value without waiting for any resolver. + expect( value ).toBe( null ); + } ); + + it( 'invokes selector with a resolver', async () => { + const registry = createSelectTestRegistry(); + // Check that the action with a control returns the initial state + // without waiting for any resolver. + const value = await registry + .dispatch( 'test/select' ) + .selectWithResolver(); + expect( value ).toBe( null ); + + // Check that re-running the action immediately still returns + // the initial state, as the resolution is still running. + const value2 = await registry + .dispatch( 'test/select' ) + .selectWithResolver(); + expect( value2 ).toBe( null ); + } ); + } ); + + describe( 'dispatch', () => { + function createDispatchTestRegistry() { + const registry = createRegistry(); + + // store stores a counter that can be incremented + const reducer = ( state = 0, action ) => { + switch ( action.type ) { + case 'INC': + return state + 1; + default: + return state; + } + }; + + const actions = { + // increment the counter + inc() { + return { type: 'INC' }; + }, + // increment the counter twice in an async routine with controls + *doubleInc() { + yield controls.dispatch( 'test/dispatch', 'inc' ); + yield controls.dispatch( 'test/dispatch', 'inc' ); + }, + }; + + const selectors = { + get: ( state ) => state, + }; + + registry.registerStore( 'test/dispatch', { + reducer, + actions, + selectors, + } ); + + return registry; + } + + it( 'invokes dispatch action', async () => { + const registry = createDispatchTestRegistry(); + expect( registry.select( 'test/dispatch' ).get() ).toBe( 0 ); + await registry.dispatch( 'test/dispatch' ).doubleInc(); + expect( registry.select( 'test/dispatch' ).get() ).toBe( 2 ); + } ); + } ); +} ); diff --git a/packages/data/src/test/registry.js b/packages/data/src/test/registry.js index 1d78e8dae1c5e..ad4b47d308144 100644 --- a/packages/data/src/test/registry.js +++ b/packages/data/src/test/registry.js @@ -575,17 +575,6 @@ describe( 'createRegistry', () => { 'result1' ); } ); - - it( 'gracefully stubs select on selector calls', () => { - const selector = createRegistrySelector( ( select ) => () => - select - ); - - const maybeSelect = selector(); - - expect( maybeSelect ).toEqual( expect.any( Function ) ); - expect( maybeSelect() ).toEqual( expect.any( Object ) ); - } ); } ); describe( 'subscribe', () => { diff --git a/packages/edit-site/src/store/test/actions.js b/packages/edit-site/src/store/test/actions.js index 4f687e2d45214..5dd56d9e091ef 100644 --- a/packages/edit-site/src/store/test/actions.js +++ b/packages/edit-site/src/store/test/actions.js @@ -40,7 +40,7 @@ describe( 'actions', () => { const it = addTemplate( template ); expect( it.next().value ).toEqual( { - type: 'DISPATCH', + type: '@@data/DISPATCH', storeKey: 'core', actionName: 'saveEntityRecord', args: [ 'postType', 'wp_template', template ], @@ -69,13 +69,13 @@ describe( 'actions', () => { }, } ); expect( it.next().value ).toEqual( { - type: 'SELECT', + type: '@@data/RESOLVE_SELECT', storeKey: 'core/edit-site', selectorName: 'getPage', args: [], } ); expect( it.next( page ).value ).toEqual( { - type: 'DISPATCH', + type: '@@data/DISPATCH', storeKey: 'core/edit-site', actionName: 'setPage', args: [ page ], @@ -123,7 +123,7 @@ describe( 'actions', () => { args: [ 'root', 'site' ], selectorName: 'getEntityRecord', storeKey: 'core', - type: 'SELECT', + type: '@@data/RESOLVE_SELECT', } ); const page = { @@ -156,7 +156,7 @@ describe( 'actions', () => { args: [ 'root', 'site' ], selectorName: 'getEntityRecord', storeKey: 'core', - type: 'SELECT', + type: '@@data/RESOLVE_SELECT', } ); const page = {