diff --git a/packages/editor/src/hooks/default-autocompleters.js b/packages/editor/src/hooks/default-autocompleters.js index 6ab43ed47d6ab..b80bbc485857e 100644 --- a/packages/editor/src/hooks/default-autocompleters.js +++ b/packages/editor/src/hooks/default-autocompleters.js @@ -1,14 +1,13 @@ /** * External dependencies */ -import { clone, once } from 'lodash'; +import { clone } from 'lodash'; /** * WordPress dependencies */ import { addFilter } from '@wordpress/hooks'; import { getDefaultBlockName } from '@wordpress/blocks'; -import { dispatch } from '@wordpress/data'; /** * Internal dependencies @@ -17,8 +16,6 @@ import { blockAutocompleter, userAutocompleter } from '../components'; const defaultAutocompleters = [ userAutocompleter ]; -const fetchReusableBlocks = once( () => dispatch( 'core/editor' ).__experimentalFetchReusableBlocks() ); - function setDefaultCompleters( completers, blockName ) { if ( ! completers ) { // Provide copies so filters may directly modify them. @@ -26,14 +23,6 @@ function setDefaultCompleters( completers, blockName ) { // Add blocks autocompleter for Paragraph block if ( blockName === getDefaultBlockName() ) { completers.push( clone( blockAutocompleter ) ); - - /* - * NOTE: This is a hack to help ensure reusable blocks are loaded - * so they may be included in the block completer. It can be removed - * once we have a way for completers to Promise options while - * store-based data dependencies are being resolved. - */ - fetchReusableBlocks(); } } return completers; diff --git a/packages/editor/src/store/effects.js b/packages/editor/src/store/effects.js index 3afa0f1e055a1..39b2d97b5e500 100644 --- a/packages/editor/src/store/effects.js +++ b/packages/editor/src/store/effects.js @@ -26,6 +26,7 @@ import { resetBlocks, setTemplateValidity, insertDefaultBlock, + __experimentalFetchReusableBlocks as fetchReusableBlocksAction, } from './actions'; import { getBlock, @@ -45,7 +46,6 @@ import { deleteReusableBlocks, convertBlockToReusable, convertBlockToStatic, - receiveReusableBlocks, } from './effects/reusable-blocks'; import { requestPostUpdate, @@ -232,6 +232,11 @@ export default { // // See: https://github.com/WordPress/gutenberg/pull/9403 validateBlocksToTemplate( setupAction, store ), + + // Fetch reusable blocks when the editor initialises. This ensures that they + // appear in the block autocompleter. This is temporary until reusable blocks + // are fetched using a select() resolver. + fetchReusableBlocksAction(), ] ); }, RESET_BLOCKS: [ @@ -254,7 +259,6 @@ export default { DELETE_REUSABLE_BLOCK: ( action, store ) => { deleteReusableBlocks( action, store ); }, - RECEIVE_REUSABLE_BLOCKS: receiveReusableBlocks, CONVERT_BLOCK_TO_STATIC: convertBlockToStatic, CONVERT_BLOCK_TO_REUSABLE: convertBlockToReusable, REMOVE_BLOCKS: [ diff --git a/packages/editor/src/store/effects/reusable-blocks.js b/packages/editor/src/store/effects/reusable-blocks.js index 0c569728b6a39..654efe8ae6a1c 100644 --- a/packages/editor/src/store/effects/reusable-blocks.js +++ b/packages/editor/src/store/effects/reusable-blocks.js @@ -212,16 +212,6 @@ export const deleteReusableBlocks = async ( action, store ) => { } }; -/** - * Receive Reusable Blocks Effect Handler. - * - * @param {Object} action action object. - * @return {Object} receive blocks action - */ -export const receiveReusableBlocks = ( action ) => { - return receiveBlocks( map( action.results, 'parsedBlock' ) ); -}; - /** * Convert a reusable block to a static block effect handler * diff --git a/packages/editor/src/store/effects/test/reusable-blocks.js b/packages/editor/src/store/effects/test/reusable-blocks.js index 2cc3b83e4f36f..21f0241bd0571 100644 --- a/packages/editor/src/store/effects/test/reusable-blocks.js +++ b/packages/editor/src/store/effects/test/reusable-blocks.js @@ -19,7 +19,6 @@ import { import { fetchReusableBlocks, saveReusableBlocks, - receiveReusableBlocks, deleteReusableBlocks, convertBlockToStatic, convertBlockToReusable, @@ -262,20 +261,6 @@ describe( 'reusable blocks effects', () => { } ); } ); - describe( 'receiveReusableBlocks', () => { - it( 'should receive parsed blocks', () => { - const action = receiveReusableBlocksAction( [ - { - parsedBlock: { clientId: 'broccoli' }, - }, - ] ); - - expect( receiveReusableBlocks( action ) ).toEqual( receiveBlocks( [ - { clientId: 'broccoli' }, - ] ) ); - } ); - } ); - describe( 'deleteReusableBlocks', () => { it( 'should delete a reusable block', async () => { const deletePromise = Promise.resolve( {} ); diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 6bfb468cd7157..4091e50de087b 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -233,7 +233,7 @@ export const editor = flow( [ // Track undo history, starting at editor initialization. withHistory( { resetTypes: [ 'SETUP_EDITOR_STATE' ], - ignoreTypes: [ 'RECEIVE_BLOCKS', 'RESET_POST', 'UPDATE_POST' ], + ignoreTypes: [ 'RECEIVE_BLOCKS', 'RECEIVE_REUSABLE_BLOCKS', 'RESET_POST', 'UPDATE_POST' ], shouldOverwriteState, } ), ] )( { @@ -284,7 +284,7 @@ export const editor = flow( [ // editor initialization firing post reset as an effect. withChangeDetection( { resetTypes: [ 'SETUP_EDITOR_STATE', 'REQUEST_POST_UPDATE_START' ], - ignoreTypes: [ 'RECEIVE_BLOCKS', 'RESET_POST', 'UPDATE_POST' ], + ignoreTypes: [ 'RECEIVE_BLOCKS', 'RECEIVE_REUSABLE_BLOCKS', 'RESET_POST', 'UPDATE_POST' ], } ), ] )( { byClientId( state = {}, action ) { @@ -294,10 +294,15 @@ export const editor = flow( [ return getFlattenedBlocks( action.blocks ); case 'RECEIVE_BLOCKS': + case 'RECEIVE_REUSABLE_BLOCKS': { + const blocks = action.type === 'RECEIVE_BLOCKS' ? + action.blocks : + action.results.map( ( result ) => result.parsedBlock ); return { ...state, - ...getFlattenedBlocks( action.blocks ), + ...getFlattenedBlocks( blocks ), }; + } case 'UPDATE_BLOCK_ATTRIBUTES': // Ignore updates if block isn't known @@ -397,10 +402,15 @@ export const editor = flow( [ return mapBlockOrder( action.blocks ); case 'RECEIVE_BLOCKS': + case 'RECEIVE_REUSABLE_BLOCKS': { + const blocks = action.type === 'RECEIVE_BLOCKS' ? + action.blocks : + action.results.map( ( result ) => result.parsedBlock ); return { ...state, - ...omit( mapBlockOrder( action.blocks ), '' ), + ...omit( mapBlockOrder( blocks ), '' ), }; + } case 'INSERT_BLOCKS': { const { rootClientId = '', blocks } = action; diff --git a/packages/editor/src/store/test/effects.js b/packages/editor/src/store/test/effects.js index ce54c418d5fa1..7606509c9bdec 100644 --- a/packages/editor/src/store/test/effects.js +++ b/packages/editor/src/store/test/effects.js @@ -25,6 +25,7 @@ import actions, { resetBlocks, selectBlock, setTemplateValidity, + __experimentalFetchReusableBlocks as fetchReusableBlocks, } from '../actions'; import effects, { validateBlocksToTemplate } from '../effects'; import { SAVE_POST_NOTICE_ID } from '../effects/posts'; @@ -420,6 +421,7 @@ describe( 'effects', () => { expect( result ).toEqual( [ setupEditorState( post, [], {} ), + fetchReusableBlocks(), ] ); } ); @@ -450,6 +452,7 @@ describe( 'effects', () => { expect( result[ 0 ].blocks ).toHaveLength( 1 ); expect( result ).toEqual( [ setupEditorState( post, result[ 0 ].blocks, {} ), + fetchReusableBlocks(), ] ); } ); @@ -478,6 +481,7 @@ describe( 'effects', () => { expect( result ).toEqual( [ setupEditorState( post, [], { title: 'A History of Pork' } ), + fetchReusableBlocks(), ] ); } ); } ); diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index d40b07b0b36bd..13eadeac3b2a8 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -1157,6 +1157,76 @@ describe( 'state', () => { expect( state.present.blocks.byClientId ).toBe( state.present.blocks.byClientId ); } ); + + it( 'should receive blocks', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'block1', + attributes: {}, + innerBlocks: [], + } ], + } ) ); + const state = editor( original, { + type: 'RECEIVE_BLOCKS', + blocks: [ + { + clientId: 'block2', + attributes: {}, + innerBlocks: [], + }, + { + clientId: 'block3', + attributes: {}, + innerBlocks: [], + }, + ], + } ); + + expect( state.present.blocks.byClientId ).toEqual( { + block1: { clientId: 'block1', attributes: {} }, + block2: { clientId: 'block2', attributes: {} }, + block3: { clientId: 'block3', attributes: {} }, + } ); + } ); + + it( 'should receive reusable blocks', () => { + const original = deepFreeze( editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'block1', + attributes: {}, + innerBlocks: [], + } ], + } ) ); + const state = editor( original, { + type: 'RECEIVE_REUSABLE_BLOCKS', + results: [ + { + reusableBlock: {}, + parsedBlock: { + clientId: 'block2', + attributes: {}, + innerBlocks: [], + }, + }, + { + reusableBlock: {}, + parsedBlock: { + clientId: 'block3', + attributes: {}, + innerBlocks: [], + }, + }, + ], + } ); + + expect( state.present.blocks.byClientId ).toEqual( { + block1: { clientId: 'block1', attributes: {} }, + block2: { clientId: 'block2', attributes: {} }, + block3: { clientId: 'block3', attributes: {} }, + } ); + } ); } ); } ); diff --git a/test/e2e/specs/reusable-blocks.test.js b/test/e2e/specs/reusable-blocks.test.js index 03f9a0b38d89d..a551fe4e8c93b 100644 --- a/test/e2e/specs/reusable-blocks.test.js +++ b/test/e2e/specs/reusable-blocks.test.js @@ -10,6 +10,14 @@ import { META_KEY, } from '../support/utils'; +async function waitForAndInsertBlock( blockLabel ) { + // Since we are working with new posts, we need to wait for the inserter to + // finish fetching reusable blocks before clicking on the desired block. + await searchForBlock( blockLabel ); + await page.waitForSelector( `button[aria-label="${ blockLabel }"]` ); + await page.click( `button[aria-label="${ blockLabel }"]` ); +} + function waitForAndAcceptDialog() { return new Promise( ( resolve ) => { page.once( 'dialog', () => resolve() ); @@ -41,11 +49,6 @@ describe( 'Reusable Blocks', () => { '//*[contains(@class, "components-notice") and contains(@class, "is-success")]/*[text()="Block created."]' ); - // Select all of the text in the title field by triple-clicking on it. We - // triple-click because, on Mac, Mod+A doesn't work. This step can be removed - // when https://github.com/WordPress/gutenberg/issues/7972 is fixed - await page.click( '.reusable-block-edit-panel__title', { clickCount: 3 } ); - // Give the reusable block a title await page.keyboard.type( 'Greeting block' ); @@ -109,7 +112,7 @@ describe( 'Reusable Blocks', () => { it( 'can be inserted and edited', async () => { // Insert the reusable block we created above - await insertBlock( 'Greeting block' ); + await waitForAndInsertBlock( 'Greeting block' ); // Put the reusable block in edit mode const [ editButton ] = await page.$x( '//button[text()="Edit"]' ); @@ -118,7 +121,7 @@ describe( 'Reusable Blocks', () => { // Change the block's title await page.keyboard.type( 'Surprised greeting block' ); - // Tab three times to navigate to the block's content + // Tab two times to navigate to the block's content await page.keyboard.press( 'Tab' ); await page.keyboard.press( 'Tab' ); @@ -153,7 +156,7 @@ describe( 'Reusable Blocks', () => { it( 'can be converted to a regular block', async () => { // Insert the reusable block we edited above - await insertBlock( 'Surprised greeting block' ); + await waitForAndInsertBlock( 'Surprised greeting block' ); // Convert block to a regular block await page.click( 'button[aria-label="More options"]' ); @@ -176,7 +179,7 @@ describe( 'Reusable Blocks', () => { it( 'can be deleted', async () => { // Insert the reusable block we edited above - await insertBlock( 'Surprised greeting block' ); + await waitForAndInsertBlock( 'Surprised greeting block' ); // Delete the block and accept the confirmation dialog await page.click( 'button[aria-label="More options"]' ); @@ -197,9 +200,7 @@ describe( 'Reusable Blocks', () => { } ); it( 'can be created from multiselection', async () => { - await newPost(); - - // Insert a Two paragraphs block + // Insert two paragraph blocks await insertBlock( 'Paragraph' ); await page.keyboard.type( 'Hello there!' ); await page.keyboard.press( 'Enter' ); @@ -213,7 +214,7 @@ describe( 'Reusable Blocks', () => { await page.mouse.move( 200, 300, { steps: 10 } ); await page.mouse.move( 250, 350, { steps: 10 } ); - // Convert block to a reusable block + // Convert blocks to a reusable block await page.waitForSelector( 'button[aria-label="More options"]' ); await page.click( 'button[aria-label="More options"]' ); const convertButton = await page.waitForXPath( '//button[text()="Add to Reusable Blocks"]' ); @@ -224,11 +225,6 @@ describe( 'Reusable Blocks', () => { '//*[contains(@class, "components-notice") and contains(@class, "is-success")]/*[text()="Block created."]' ); - // Select all of the text in the title field by triple-clicking on it. We - // triple-click because, on Mac, Mod+A doesn't work. This step can be removed - // when https://github.com/WordPress/gutenberg/issues/7972 is fixed - await page.click( '.reusable-block-edit-panel__title', { clickCount: 3 } ); - // Give the reusable block a title await page.keyboard.type( 'Multi-selection reusable block' ); @@ -253,7 +249,7 @@ describe( 'Reusable Blocks', () => { it( 'multi-selection reusable block can be converted back to regular blocks', async () => { // Insert the reusable block we edited above - await insertBlock( 'Multi-selection reusable block' ); + await waitForAndInsertBlock( 'Multi-selection reusable block' ); // Convert block to a regular block await page.click( 'button[aria-label="More options"]' );