diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index cd251dc92d6787..0622efebcb91c0 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -734,7 +734,7 @@ The editor settings object. ### setupEditor -Returns an action object used in signalling that editor has initialized with +Returns an action generator used in signalling that editor has initialized with the specified post object and editor settings. *Parameters* @@ -882,7 +882,7 @@ Returns an action object used to lock the editor. ### __experimentalFetchReusableBlocks -Returns an action object used to fetch a single reusable block or all +Returns an action generator used to fetch a single reusable block or all reusable blocks from the REST API into the store. *Parameters* @@ -892,7 +892,7 @@ reusable blocks from the REST API into the store. ### __experimentalReceiveReusableBlocks -Returns an action object used in signalling that reusable blocks have been +Returns an action generator used in signalling that reusable blocks have been received. `results` is an array of objects containing: - `reusableBlock` - Details about how the reusable block is persisted. - `parsedBlock` - The original block. @@ -903,7 +903,7 @@ received. `results` is an array of objects containing: ### __experimentalSaveReusableBlock -Returns an action object used to save a reusable block that's in the store to +Returns an action generator used to save a reusable block that's in the store to the REST API. *Parameters* @@ -912,7 +912,7 @@ the REST API. ### __experimentalDeleteReusableBlock -Returns an action object used to delete a reusable block via the REST API. +Returns an action generator used to delete a reusable block via the REST API. *Parameters* @@ -930,7 +930,7 @@ to be updated. ### __experimentalConvertBlockToStatic -Returns an action object used to convert a reusable block into a static +Returns an action generator used to convert a reusable block into a static block. *Parameters* @@ -939,7 +939,7 @@ block. ### __experimentalConvertBlockToReusable -Returns an action object used to convert a static block into a reusable +Returns an action generator used to convert a static block into a reusable block. *Parameters* diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index ce396bfa582ec3..36fdac79f8ff25 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,3 +1,9 @@ +## 9.1.1 (Unreleased) + +### Internal + +- Refactor all reusable blocks and editor effects to action-generators using controls ([#14491](https://github.com/WordPress/gutenberg/pull/14491)) + ## 9.1.0 (2019-03-06) ### New Features diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index c0c88d1072fe9a..cfacafc5f22187 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { castArray, pick } from 'lodash'; +import { castArray, pick, has, compact, map, uniqueId } from 'lodash'; import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; /** @@ -18,6 +18,7 @@ import { POST_UPDATE_TRANSACTION_ID, SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID, + REUSABLE_BLOCK_NOTICE_ID, } from './constants'; import { getNotificationArgumentsForSaveSuccess, @@ -26,22 +27,64 @@ import { } from './utils/notice-builder'; /** - * Returns an action object used in signalling that editor has initialized with + * WordPress dependencies + */ +import { + parse, + serialize, + createBlock, + isReusableBlock, + cloneBlock, + synchronizeBlocksWithTemplate, +} from '@wordpress/blocks'; +import { getPostRawValue } from './reducer'; +import { __ } from '@wordpress/i18n'; + +/** + * Returns an action generator used in signalling that editor has initialized with * the specified post object and editor settings. * * @param {Object} post Post object. * @param {Object} edits Initial edited attributes object. * @param {Array?} template Block Template. - * - * @return {Object} Action object. */ -export function setupEditor( post, edits, template ) { - return { +export function* setupEditor( post, edits, template ) { + yield { type: 'SETUP_EDITOR', post, edits, template, }; + + // In order to ensure maximum of a single parse during setup, edits are + // included as part of editor setup action. Assume edited content as + // canonical if provided, falling back to post. + let content; + if ( has( edits, [ 'content' ] ) ) { + content = edits.content; + } else { + content = post.content.raw; + } + + let blocks = parse( content ); + + // Apply a template for new posts only, if exists. + const isNewPost = post.status === 'auto-draft'; + if ( isNewPost && template ) { + blocks = synchronizeBlocksWithTemplate( blocks, template ); + } + + yield dispatch( + STORE_KEY, + 'resetEditorBlocks', + blocks + ); + + yield dispatch( + STORE_KEY, + 'setupEditorState', + post + ); } /** @@ -523,65 +566,240 @@ export function updatePostLock( lock ) { } /** - * Returns an action object used to fetch a single reusable block or all + * Returns an action generator used to fetch a single reusable block or all * reusable blocks from the REST API into the store. * * @param {?string} id If given, only a single reusable block with this ID will * be fetched. - * - * @return {Object} Action object. */ -export function __experimentalFetchReusableBlocks( id ) { - return { +export function* __experimentalFetchReusableBlocks( id ) { + yield { type: 'FETCH_REUSABLE_BLOCKS', id, }; + // TODO: these are potentially undefined, this fix is in place + // until there is a filter to not use reusable blocks if undefined + const postType = yield apiFetch( { path: '/wp/v2/types/wp_block' } ); + if ( ! postType ) { + return; + } + + try { + let posts; + + if ( id ) { + posts = [ yield apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ id }` } ) ]; + } else { + posts = yield apiFetch( { path: `/wp/v2/${ postType.rest_base }?per_page=-1` } ); + } + + const results = compact( map( posts, ( post ) => { + if ( post.status !== 'publish' || post.content.protected ) { + return null; + } + + const parsedBlocks = parse( post.content.raw ); + return { + reusableBlock: { + id: post.id, + title: getPostRawValue( post.title ), + }, + parsedBlock: parsedBlocks.length === 1 ? + parsedBlocks[ 0 ] : + createBlock( 'core/template', {}, parsedBlocks ), + }; + } ) ); + + if ( results.length ) { + yield dispatch( + STORE_KEY, + '__experimentalReceiveReusableBlocks', + results + ); + } + + yield { + type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', + id, + }; + } catch ( error ) { + yield { + type: 'FETCH_REUSABLE_BLOCKS_FAILURE', + id, + error, + }; + } } /** - * Returns an action object used in signalling that reusable blocks have been + * Returns an action generator used in signalling that reusable blocks have been * received. `results` is an array of objects containing: * - `reusableBlock` - Details about how the reusable block is persisted. * - `parsedBlock` - The original block. * * @param {Object[]} results Reusable blocks received. - * - * @return {Object} Action object. */ -export function __experimentalReceiveReusableBlocks( results ) { - return { +export function* __experimentalReceiveReusableBlocks( results ) { + yield { type: 'RECEIVE_REUSABLE_BLOCKS', results, }; + yield dispatch( + 'core/block-editor', + 'receiveBlocks', + map( results, 'parsedBlock' ) + ); } /** - * Returns an action object used to save a reusable block that's in the store to + * Returns an action generator used to save a reusable block that's in the store to * the REST API. * * @param {Object} id The ID of the reusable block to save. - * - * @return {Object} Action object. */ -export function __experimentalSaveReusableBlock( id ) { - return { +export function* __experimentalSaveReusableBlock( id ) { + yield { type: 'SAVE_REUSABLE_BLOCK', id, }; + // TODO: these are potentially undefined, this fix is in place + // until there is a filter to not use reusable blocks if undefined + const postType = yield apiFetch( { path: '/wp/v2/types/wp_block' } ); + if ( ! postType ) { + return; + } + const { clientId, title, isTemporary } = yield select( + STORE_KEY, + '__experimentalGetReusableBlock', + id + ); + const reusableBlock = yield select( + 'core/block-editor', + 'getBlock', + clientId + ); + const content = serialize( reusableBlock.name === 'core/template' ? + reusableBlock.innerBlocks : + reusableBlock ); + const data = isTemporary ? + { title, content, status: 'publish' } : + { id, title, content, status: 'publish' }; + const path = isTemporary ? + `/wp/v2/${ postType.rest_base }` : + `/wp/v2/${ postType.rest_base }/${ id }`; + const method = isTemporary ? 'POST' : 'PUT'; + + try { + const updatedReusableBlock = yield apiFetch( { path, data, method } ); + yield { + type: 'SAVE_REUSABLE_BLOCK_SUCCESS', + updatedId: updatedReusableBlock.id, + id, + }; + const message = isTemporary ? + __( 'Block created.' ) : + __( 'Block updated.' ); + yield dispatch( + 'core/notices', + 'createSuccessNotice', + message, + { id: REUSABLE_BLOCK_NOTICE_ID } + ); + yield dispatch( + 'core/block-editor', + '__unstableSaveReusableBlock', + id, + updatedReusableBlock.id + ); + } catch ( error ) { + yield { type: 'SAVE_REUSABLE_BLOCK_FAILURE', id }; + yield dispatch( + 'core/notices', + 'createErrorNotice', + error.message, + { id: REUSABLE_BLOCK_NOTICE_ID } + ); + } } /** - * Returns an action object used to delete a reusable block via the REST API. + * Returns an action generator used to delete a reusable block via the REST API. * * @param {number} id The ID of the reusable block to delete. - * - * @return {Object} Action object. */ -export function __experimentalDeleteReusableBlock( id ) { - return { - type: 'DELETE_REUSABLE_BLOCK', +export function* __experimentalDeleteReusableBlock( id ) { + // TODO: these are potentially undefined, this fix is in place + // until there is a filter to not use reusable blocks if undefined + const postType = yield apiFetch( { path: '/wp/v2/types/wp_block' } ); + if ( ! postType ) { + return; + } + + // Don't allow a reusable block with a temporary ID to be deleted + const reusableBlock = yield select( + STORE_KEY, + '__experimentalGetReusableBlock', + id + ); + if ( ! reusableBlock || reusableBlock.isTemporary ) { + return; + } + + // Remove any other blocks that reference this reusable block + const allBlocks = yield select( 'core/block-editor', 'getBlocks' ); + const associatedBlocks = allBlocks.filter( + ( block ) => isReusableBlock( block ) && block.attributes.ref === id + ); + const associatedBlockClientIds = associatedBlocks.map( + ( block ) => block.clientId + ); + + const transactionId = uniqueId(); + + yield { + type: 'REMOVE_REUSABLE_BLOCK', id, + optimist: { type: BEGIN, id: transactionId }, }; + + yield dispatch( + 'core/block-editor', + 'removeBlocks', + [ + ...associatedBlockClientIds, + reusableBlock.clientId, + ] + ); + + try { + yield apiFetch( { + path: `/wp/v2/${ postType.rest_base }/${ id }`, + method: 'DELETE', + } ); + yield { + type: 'DELETE_REUSABLE_BLOCK_SUCCESS', + id, + optimist: { type: COMMIT, id: transactionId }, + }; + + yield dispatch( + 'core/notices', + 'createSuccessNotice', + __( 'Block deleted.' ), + { id: REUSABLE_BLOCK_NOTICE_ID } + ); + } catch ( error ) { + yield { + type: 'DELETE_REUSABLE_BLOCK_FAILURE', + id, + optimist: { type: REVERT, id: transactionId }, + }; + yield dispatch( + 'core/notices', + error.message, + { id: REUSABLE_BLOCK_NOTICE_ID } + ); + } } /** @@ -602,33 +820,110 @@ export function __experimentalUpdateReusableBlockTitle( id, title ) { } /** - * Returns an action object used to convert a reusable block into a static + * Returns an action generator used to convert a reusable block into a static * block. * * @param {string} clientId The client ID of the block to attach. - * - * @return {Object} Action object. */ -export function __experimentalConvertBlockToStatic( clientId ) { - return { - type: 'CONVERT_BLOCK_TO_STATIC', - clientId, - }; +export function* __experimentalConvertBlockToStatic( clientId ) { + const oldBlock = yield select( + 'core/block-editor', + 'getBlock', + clientId + ); + const reusableBlock = yield select( + STORE_KEY, + '__experimentalGetReusableBlock', + oldBlock.attributes.ref + ); + const referencedBlock = yield select( + 'core/block-editor', + 'getBlock', + reusableBlock.clientId + ); + let newBlocks; + if ( referencedBlock.name === 'core/template' ) { + newBlocks = referencedBlock.innerBlocks.map( + ( innerBlock ) => cloneBlock( innerBlock ) + ); + } else { + newBlocks = [ cloneBlock( referencedBlock ) ]; + } + yield dispatch( + 'core/block-editor', + 'replaceBlocks', + oldBlock.clientId, + newBlocks + ); } /** - * Returns an action object used to convert a static block into a reusable + * Returns an action generator used to convert a static block into a reusable * block. * * @param {string} clientIds The client IDs of the block to detach. - * - * @return {Object} Action object. */ -export function __experimentalConvertBlockToReusable( clientIds ) { - return { - type: 'CONVERT_BLOCK_TO_REUSABLE', - clientIds: castArray( clientIds ), +export function* __experimentalConvertBlockToReusable( clientIds ) { + let parsedBlock; + clientIds = castArray( clientIds ); + if ( clientIds.length === 1 ) { + parsedBlock = yield select( + 'core/block-editor', + 'getBlock', + clientIds[ 0 ] + ); + } else { + const blocks = yield select( + 'core/block-editor', + 'getBlocksByClientId', + clientIds + ); + parsedBlock = createBlock( + 'core/template', + {}, + blocks + ); + + // This shouldn't be necessary but at the moment + // we expect the content of the shared blocks to live in the blocks state. + yield dispatch( + 'core/block-editor', + 'receiveBlocks', + [ parsedBlock ] + ); + } + + const reusableBlock = { + id: uniqueId( 'reusable' ), + clientId: parsedBlock.clientId, + title: __( 'Untitled Reusable Block' ), }; + + yield dispatch( + STORE_KEY, + '__experimentalReceiveReusableBlocks', + [ { reusableBlock, parsedBlock } ] + ); + + yield dispatch( + 'core/block-editor', + 'replaceBlocks', + clientIds, + createBlock( 'core/block', { ref: reusableBlock.id } ) + ); + + // Re-add the original block to the store, since replaceBlock() will have removed it + yield dispatch( + 'core/block-editor', + 'receiveBlocks', + [ parsedBlock ] + ); + + yield dispatch( + STORE_KEY, + '__experimentalSaveReusableBlock', + reusableBlock.id + ); } /** diff --git a/packages/editor/src/store/constants.js b/packages/editor/src/store/constants.js index 8f8f1bd0afcef6..2a7acee5be2396 100644 --- a/packages/editor/src/store/constants.js +++ b/packages/editor/src/store/constants.js @@ -19,3 +19,5 @@ export const SAVE_POST_NOTICE_ID = 'SAVE_POST_NOTICE_ID'; export const TRASH_POST_NOTICE_ID = 'TRASH_POST_NOTICE_ID'; export const PERMALINK_POSTNAME_REGEX = /%(?:postname|pagename)%/; export const ONE_MINUTE_IN_MS = 60 * 1000; + +export const REUSABLE_BLOCK_NOTICE_ID = 'REUSABLE_BLOCK_NOTICE_ID'; diff --git a/packages/editor/src/store/effects.js b/packages/editor/src/store/effects.js deleted file mode 100644 index 84f51151137667..00000000000000 --- a/packages/editor/src/store/effects.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * External dependencies - */ -import { has } from 'lodash'; - -/** - * WordPress dependencies - */ -import { - parse, - synchronizeBlocksWithTemplate, -} from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { - setupEditorState, - resetEditorBlocks, -} from './actions'; -import { - fetchReusableBlocks, - saveReusableBlocks, - deleteReusableBlocks, - convertBlockToReusable, - convertBlockToStatic, - receiveReusableBlocks, -} from './effects/reusable-blocks'; - -export default { - SETUP_EDITOR( action ) { - const { post, edits, template } = action; - - // In order to ensure maximum of a single parse during setup, edits are - // included as part of editor setup action. Assume edited content as - // canonical if provided, falling back to post. - let content; - if ( has( edits, [ 'content' ] ) ) { - content = edits.content; - } else { - content = post.content.raw; - } - - let blocks = parse( content ); - - // Apply a template for new posts only, if exists. - const isNewPost = post.status === 'auto-draft'; - if ( isNewPost && template ) { - blocks = synchronizeBlocksWithTemplate( blocks, template ); - } - - return [ - resetEditorBlocks( blocks ), - setupEditorState( post ), - ]; - }, - FETCH_REUSABLE_BLOCKS: ( action, store ) => { - fetchReusableBlocks( action, store ); - }, - SAVE_REUSABLE_BLOCK: ( action, store ) => { - saveReusableBlocks( action, store ); - }, - DELETE_REUSABLE_BLOCK: ( action, store ) => { - deleteReusableBlocks( action, store ); - }, - RECEIVE_REUSABLE_BLOCKS: receiveReusableBlocks, - CONVERT_BLOCK_TO_STATIC: convertBlockToStatic, - CONVERT_BLOCK_TO_REUSABLE: convertBlockToReusable, -}; diff --git a/packages/editor/src/store/effects/reusable-blocks.js b/packages/editor/src/store/effects/reusable-blocks.js deleted file mode 100644 index 63bc00169c543d..00000000000000 --- a/packages/editor/src/store/effects/reusable-blocks.js +++ /dev/null @@ -1,289 +0,0 @@ -/** - * External dependencies - */ -import { compact, map, uniqueId } from 'lodash'; -import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; - -/** - * WordPress dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { - parse, - serialize, - createBlock, - isReusableBlock, - cloneBlock, -} from '@wordpress/blocks'; -import { __ } from '@wordpress/i18n'; -// TODO: Ideally this would be the only dispatch in scope. This requires either -// refactoring editor actions to yielded controls, or replacing direct dispatch -// on the editor store with action creators (e.g. `REMOVE_REUSABLE_BLOCK`). -import { dispatch as dataDispatch, select } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { - __experimentalReceiveReusableBlocks as receiveReusableBlocksAction, - __experimentalSaveReusableBlock as saveReusableBlock, -} from '../actions'; -import { - __experimentalGetReusableBlock as getReusableBlock, -} from '../selectors'; -import { getPostRawValue } from '../reducer'; - -/** - * Module Constants - */ -const REUSABLE_BLOCK_NOTICE_ID = 'REUSABLE_BLOCK_NOTICE_ID'; - -/** - * Fetch Reusable Blocks Effect Handler. - * - * @param {Object} action action object. - * @param {Object} store Redux Store. - */ -export const fetchReusableBlocks = async ( action, store ) => { - const { id } = action; - const { dispatch } = store; - - // TODO: these are potentially undefined, this fix is in place - // until there is a filter to not use reusable blocks if undefined - const postType = await apiFetch( { path: '/wp/v2/types/wp_block' } ); - if ( ! postType ) { - return; - } - - try { - let posts; - - if ( id ) { - posts = [ await apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ id }` } ) ]; - } else { - posts = await apiFetch( { path: `/wp/v2/${ postType.rest_base }?per_page=-1` } ); - } - - const results = compact( map( posts, ( post ) => { - if ( post.status !== 'publish' || post.content.protected ) { - return null; - } - - const parsedBlocks = parse( post.content.raw ); - return { - reusableBlock: { - id: post.id, - title: getPostRawValue( post.title ), - }, - parsedBlock: parsedBlocks.length === 1 ? - parsedBlocks[ 0 ] : - createBlock( 'core/template', {}, parsedBlocks ), - }; - } ) ); - - if ( results.length ) { - dispatch( receiveReusableBlocksAction( results ) ); - } - - dispatch( { - type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', - id, - } ); - } catch ( error ) { - dispatch( { - type: 'FETCH_REUSABLE_BLOCKS_FAILURE', - id, - error, - } ); - } -}; - -/** - * Save Reusable Blocks Effect Handler. - * - * @param {Object} action action object. - * @param {Object} store Redux Store. - */ -export const saveReusableBlocks = async ( action, store ) => { - // TODO: these are potentially undefined, this fix is in place - // until there is a filter to not use reusable blocks if undefined - const postType = await apiFetch( { path: '/wp/v2/types/wp_block' } ); - if ( ! postType ) { - return; - } - - const { id } = action; - const { dispatch } = store; - const state = store.getState(); - const { clientId, title, isTemporary } = getReusableBlock( state, id ); - const reusableBlock = select( 'core/block-editor' ).getBlock( clientId ); - const content = serialize( reusableBlock.name === 'core/template' ? reusableBlock.innerBlocks : reusableBlock ); - - const data = isTemporary ? { title, content, status: 'publish' } : { id, title, content, status: 'publish' }; - const path = isTemporary ? `/wp/v2/${ postType.rest_base }` : `/wp/v2/${ postType.rest_base }/${ id }`; - const method = isTemporary ? 'POST' : 'PUT'; - - try { - const updatedReusableBlock = await apiFetch( { path, data, method } ); - dispatch( { - type: 'SAVE_REUSABLE_BLOCK_SUCCESS', - updatedId: updatedReusableBlock.id, - id, - } ); - const message = isTemporary ? __( 'Block created.' ) : __( 'Block updated.' ); - dataDispatch( 'core/notices' ).createSuccessNotice( message, { - id: REUSABLE_BLOCK_NOTICE_ID, - } ); - - dataDispatch( 'core/block-editor' ).__unstableSaveReusableBlock( id, updatedReusableBlock.id ); - } catch ( error ) { - dispatch( { type: 'SAVE_REUSABLE_BLOCK_FAILURE', id } ); - dataDispatch( 'core/notices' ).createErrorNotice( error.message, { - id: REUSABLE_BLOCK_NOTICE_ID, - } ); - } -}; - -/** - * Delete Reusable Blocks Effect Handler. - * - * @param {Object} action action object. - * @param {Object} store Redux Store. - */ -export const deleteReusableBlocks = async ( action, store ) => { - // TODO: these are potentially undefined, this fix is in place - // until there is a filter to not use reusable blocks if undefined - const postType = await apiFetch( { path: '/wp/v2/types/wp_block' } ); - if ( ! postType ) { - return; - } - - const { id } = action; - const { getState, dispatch } = store; - - // Don't allow a reusable block with a temporary ID to be deleted - const reusableBlock = getReusableBlock( getState(), id ); - if ( ! reusableBlock || reusableBlock.isTemporary ) { - return; - } - - // Remove any other blocks that reference this reusable block - const allBlocks = select( 'core/block-editor' ).getBlocks(); - const associatedBlocks = allBlocks.filter( ( block ) => isReusableBlock( block ) && block.attributes.ref === id ); - const associatedBlockClientIds = associatedBlocks.map( ( block ) => block.clientId ); - - const transactionId = uniqueId(); - - dispatch( { - type: 'REMOVE_REUSABLE_BLOCK', - id, - optimist: { type: BEGIN, id: transactionId }, - } ); - - // Remove the parsed block. - dataDispatch( 'core/block-editor' ).removeBlocks( [ - ...associatedBlockClientIds, - reusableBlock.clientId, - ] ); - - try { - await apiFetch( { - path: `/wp/v2/${ postType.rest_base }/${ id }`, - method: 'DELETE', - } ); - dispatch( { - type: 'DELETE_REUSABLE_BLOCK_SUCCESS', - id, - optimist: { type: COMMIT, id: transactionId }, - } ); - const message = __( 'Block deleted.' ); - dataDispatch( 'core/notices' ).createSuccessNotice( message, { - id: REUSABLE_BLOCK_NOTICE_ID, - } ); - } catch ( error ) { - dispatch( { - type: 'DELETE_REUSABLE_BLOCK_FAILURE', - id, - optimist: { type: REVERT, id: transactionId }, - } ); - dataDispatch( 'core/notices' ).createErrorNotice( error.message, { - id: REUSABLE_BLOCK_NOTICE_ID, - } ); - } -}; - -/** - * Receive Reusable Blocks Effect Handler. - * - * @param {Object} action action object. - */ -export const receiveReusableBlocks = ( action ) => { - dataDispatch( 'core/block-editor' ).receiveBlocks( map( action.results, 'parsedBlock' ) ); -}; - -/** - * Convert a reusable block to a static block effect handler - * - * @param {Object} action action object. - * @param {Object} store Redux Store. - */ -export const convertBlockToStatic = ( action, store ) => { - const state = store.getState(); - const oldBlock = select( 'core/block-editor' ).getBlock( action.clientId ); - const reusableBlock = getReusableBlock( state, oldBlock.attributes.ref ); - const referencedBlock = select( 'core/block-editor' ).getBlock( reusableBlock.clientId ); - let newBlocks; - if ( referencedBlock.name === 'core/template' ) { - newBlocks = referencedBlock.innerBlocks.map( ( innerBlock ) => cloneBlock( innerBlock ) ); - } else { - newBlocks = [ cloneBlock( referencedBlock ) ]; - } - dataDispatch( 'core/block-editor' ).replaceBlocks( oldBlock.clientId, newBlocks ); -}; - -/** - * Convert a static block to a reusable block effect handler - * - * @param {Object} action action object. - * @param {Object} store Redux Store. - */ -export const convertBlockToReusable = ( action, store ) => { - const { dispatch } = store; - let parsedBlock; - if ( action.clientIds.length === 1 ) { - parsedBlock = select( 'core/block-editor' ).getBlock( action.clientIds[ 0 ] ); - } else { - parsedBlock = createBlock( - 'core/template', - {}, - select( 'core/block-editor' ).getBlocksByClientId( action.clientIds ) - ); - - // This shouldn't be necessary but at the moment - // we expect the content of the shared blocks to live in the blocks state. - dataDispatch( 'core/block-editor' ).receiveBlocks( [ parsedBlock ] ); - } - - const reusableBlock = { - id: uniqueId( 'reusable' ), - clientId: parsedBlock.clientId, - title: __( 'Untitled Reusable Block' ), - }; - - dispatch( receiveReusableBlocksAction( [ { - reusableBlock, - parsedBlock, - } ] ) ); - - dispatch( saveReusableBlock( reusableBlock.id ) ); - - dataDispatch( 'core/block-editor' ).replaceBlocks( - action.clientIds, - createBlock( 'core/block', { - ref: reusableBlock.id, - } ) - ); - - // Re-add the original block to the store, since replaceBlock() will have removed it - dataDispatch( 'core/block-editor' ).receiveBlocks( [ parsedBlock ] ); -}; diff --git a/packages/editor/src/store/effects/test/reusable-blocks.js b/packages/editor/src/store/effects/test/reusable-blocks.js deleted file mode 100644 index 1d783b1dd9fe19..00000000000000 --- a/packages/editor/src/store/effects/test/reusable-blocks.js +++ /dev/null @@ -1,548 +0,0 @@ -/** - * External dependencies - */ -import { noop } from 'lodash'; - -/** - * WordPress dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { - registerBlockType, - unregisterBlockType, - createBlock, -} from '@wordpress/blocks'; -import { dispatch as dataDispatch, select as dataSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { - fetchReusableBlocks, - saveReusableBlocks, - receiveReusableBlocks, - deleteReusableBlocks, - convertBlockToStatic, - convertBlockToReusable, -} from '../reusable-blocks'; -import { - __experimentalSaveReusableBlock as saveReusableBlock, - __experimentalDeleteReusableBlock as deleteReusableBlock, - __experimentalConvertBlockToReusable as convertBlockToReusableAction, - __experimentalConvertBlockToStatic as convertBlockToStaticAction, - __experimentalReceiveReusableBlocks as receiveReusableBlocksAction, - __experimentalFetchReusableBlocks as fetchReusableBlocksAction, -} from '../../actions'; -import reducer from '../../reducer'; -import '../../..'; // Ensure store dependencies are imported via root. - -jest.mock( '@wordpress/api-fetch', () => jest.fn() ); - -describe( 'reusable blocks effects', () => { - beforeAll( () => { - registerBlockType( 'core/test-block', { - title: 'Test block', - category: 'common', - save: () => null, - attributes: { - name: { type: 'string' }, - }, - } ); - - registerBlockType( 'core/block', { - title: 'Reusable Block', - category: 'common', - save: () => null, - attributes: { - ref: { type: 'string' }, - }, - } ); - } ); - - afterAll( () => { - unregisterBlockType( 'core/test-block' ); - unregisterBlockType( 'core/block' ); - } ); - - describe( 'fetchReusableBlocks', () => { - it( 'should fetch multiple reusable blocks', async () => { - const blockPromise = Promise.resolve( [ - { - id: 123, - status: 'publish', - title: { - raw: 'My cool block', - }, - content: { - raw: '', - protected: false, - }, - }, - ] ); - const postTypePromise = Promise.resolve( { - slug: 'wp_block', rest_base: 'blocks', - } ); - - apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block' ) { - return postTypePromise; - } - - return blockPromise; - } ); - - const dispatch = jest.fn(); - const store = { getState: noop, dispatch }; - - await fetchReusableBlocks( fetchReusableBlocksAction(), store ); - - expect( dispatch ).toHaveBeenCalledWith( - receiveReusableBlocksAction( [ - { - reusableBlock: { - id: 123, - title: 'My cool block', - }, - parsedBlock: expect.objectContaining( { - name: 'core/test-block', - attributes: { name: 'Big Bird' }, - } ), - }, - ] ) - ); - expect( dispatch ).toHaveBeenCalledWith( { - type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', - id: undefined, - } ); - } ); - - it( 'should fetch a single reusable block', async () => { - const blockPromise = Promise.resolve( { - id: 123, - status: 'publish', - title: { - raw: 'My cool block', - }, - content: { - raw: '', - protected: false, - }, - } ); - const postTypePromise = Promise.resolve( { - slug: 'wp_block', rest_base: 'blocks', - } ); - - apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block' ) { - return postTypePromise; - } - - return blockPromise; - } ); - - const dispatch = jest.fn(); - const store = { getState: noop, dispatch }; - - await fetchReusableBlocks( fetchReusableBlocksAction( 123 ), store ); - - expect( dispatch ).toHaveBeenCalledWith( - receiveReusableBlocksAction( [ - { - reusableBlock: { - id: 123, - title: 'My cool block', - }, - parsedBlock: expect.objectContaining( { - name: 'core/test-block', - attributes: { name: 'Big Bird' }, - } ), - }, - ] ) - ); - expect( dispatch ).toHaveBeenCalledWith( { - type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', - id: 123, - } ); - } ); - - it( 'should ignore reusable blocks with a trashed post status', async () => { - const blockPromise = Promise.resolve( { - id: 123, - status: 'trash', - title: { - raw: 'My cool block', - }, - content: { - raw: '', - protected: false, - }, - } ); - const postTypePromise = Promise.resolve( { - slug: 'wp_block', rest_base: 'blocks', - } ); - - apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block' ) { - return postTypePromise; - } - - return blockPromise; - } ); - - const dispatch = jest.fn(); - const store = { getState: noop, dispatch }; - - await fetchReusableBlocks( fetchReusableBlocksAction( 123 ), store ); - - expect( dispatch ).toHaveBeenCalledTimes( 1 ); - expect( dispatch ).toHaveBeenCalledWith( { - type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', - id: 123, - } ); - } ); - - it( 'should handle an API error', async () => { - const blockPromise = Promise.reject( { - code: 'unknown_error', - message: 'An unknown error occurred.', - } ); - const postTypePromise = Promise.resolve( { - slug: 'wp_block', rest_base: 'blocks', - } ); - - apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block' ) { - return postTypePromise; - } - - return blockPromise; - } ); - - const dispatch = jest.fn(); - const store = { getState: noop, dispatch }; - - await fetchReusableBlocks( fetchReusableBlocksAction(), store ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'FETCH_REUSABLE_BLOCKS_FAILURE', - error: { - code: 'unknown_error', - message: 'An unknown error occurred.', - }, - } ); - } ); - } ); - - describe( 'saveReusableBlocks', () => { - it( 'should save a reusable block and swap its id', async () => { - const savePromise = Promise.resolve( { id: 456 } ); - const postTypePromise = Promise.resolve( { - slug: 'wp_block', rest_base: 'blocks', - } ); - - apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block' ) { - return postTypePromise; - } - - return savePromise; - } ); - - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); - jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( () => parsedBlock ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - await saveReusableBlocks( saveReusableBlock( 123 ), store ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'SAVE_REUSABLE_BLOCK_SUCCESS', - id: 123, - updatedId: 456, - } ); - - dataSelect( 'core/block-editor' ).getBlock.mockReset(); - } ); - - it( 'should handle an API error', async () => { - const savePromise = Promise.reject( {} ); - const postTypePromise = Promise.resolve( { - slug: 'wp_block', rest_base: 'blocks', - } ); - - apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block' ) { - return postTypePromise; - } - - return savePromise; - } ); - - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); - jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( () => parsedBlock ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - await saveReusableBlocks( saveReusableBlock( 123 ), store ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'SAVE_REUSABLE_BLOCK_FAILURE', - id: 123, - } ); - - dataSelect( 'core/block-editor' ).getBlock.mockReset(); - } ); - } ); - - describe( 'receiveReusableBlocks', () => { - it( 'should receive parsed blocks', () => { - const action = receiveReusableBlocksAction( [ - { - parsedBlock: { clientId: 'broccoli' }, - }, - ] ); - - jest.spyOn( dataDispatch( 'core/block-editor' ), 'receiveBlocks' ).mockImplementation( () => {} ); - receiveReusableBlocks( action ); - expect( dataDispatch( 'core/block-editor' ).receiveBlocks ).toHaveBeenCalledWith( [ - { clientId: 'broccoli' }, - ] ); - - dataDispatch( 'core/block-editor' ).receiveBlocks.mockReset(); - } ); - } ); - - describe( 'deleteReusableBlocks', () => { - it( 'should delete a reusable block', async () => { - const deletePromise = Promise.resolve( {} ); - const postTypePromise = Promise.resolve( { - slug: 'wp_block', rest_base: 'blocks', - } ); - - apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block' ) { - return postTypePromise; - } - - return deletePromise; - } ); - - const associatedBlock = createBlock( 'core/block', { ref: 123 } ); - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); - jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlocks' ).mockImplementation( () => [ - associatedBlock, - parsedBlock, - ] ); - jest.spyOn( dataDispatch( 'core/block-editor' ), 'removeBlocks' ).mockImplementation( () => {} ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - await deleteReusableBlocks( deleteReusableBlock( 123 ), store ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'REMOVE_REUSABLE_BLOCK', - id: 123, - optimist: expect.any( Object ), - } ); - - expect( dataDispatch( 'core/block-editor' ).removeBlocks ).toHaveBeenCalledWith( - [ associatedBlock.clientId, parsedBlock.clientId ] - ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'DELETE_REUSABLE_BLOCK_SUCCESS', - id: 123, - optimist: expect.any( Object ), - } ); - - dataDispatch( 'core/block-editor' ).removeBlocks.mockReset(); - dataSelect( 'core/block-editor' ).getBlocks.mockReset(); - } ); - - it( 'should handle an API error', async () => { - const deletePromise = Promise.reject( {} ); - const postTypePromise = Promise.resolve( { - slug: 'wp_block', rest_base: 'blocks', - } ); - - apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block' ) { - return postTypePromise; - } - - return deletePromise; - } ); - - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); - jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlocks' ).mockImplementation( () => [ - parsedBlock, - ] ); - jest.spyOn( dataDispatch( 'core/block-editor' ), 'removeBlocks' ).mockImplementation( () => {} ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - await deleteReusableBlocks( deleteReusableBlock( 123 ), store ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'DELETE_REUSABLE_BLOCK_FAILURE', - id: 123, - optimist: expect.any( Object ), - } ); - dataDispatch( 'core/block-editor' ).removeBlocks.mockReset(); - dataSelect( 'core/block-editor' ).getBlocks.mockReset(); - } ); - - it( 'should not save reusable blocks with temporary IDs', async () => { - const reusableBlock = { id: 'reusable1', title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); - jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlocks' ).mockImplementation( () => [ - parsedBlock, - ] ); - jest.spyOn( dataDispatch( 'core/block-editor' ), 'removeBlocks' ).mockImplementation( () => {} ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - await deleteReusableBlocks( deleteReusableBlock( 'reusable1' ), store ); - - expect( dispatch ).not.toHaveBeenCalled(); - dataDispatch( 'core/block-editor' ).removeBlocks.mockReset(); - dataSelect( 'core/block-editor' ).getBlocks.mockReset(); - } ); - } ); - - describe( 'convertBlockToStatic', () => { - it( 'should convert a reusable block into a static block', () => { - const associatedBlock = createBlock( 'core/block', { ref: 123 } ); - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); - jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( ( id ) => - associatedBlock.clientId === id ? associatedBlock : parsedBlock - ); - jest.spyOn( dataDispatch( 'core/block-editor' ), 'replaceBlocks' ).mockImplementation( () => {} ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - convertBlockToStatic( convertBlockToStaticAction( associatedBlock.clientId ), store ); - - expect( dataDispatch( 'core/block-editor' ).replaceBlocks ).toHaveBeenCalledWith( - associatedBlock.clientId, - [ - expect.objectContaining( { - name: 'core/test-block', - attributes: { name: 'Big Bird' }, - } ), - ] - ); - - dataDispatch( 'core/block-editor' ).replaceBlocks.mockReset(); - dataSelect( 'core/block-editor' ).getBlock.mockReset(); - } ); - - it( 'should convert a reusable block with nested blocks into a static block', () => { - const associatedBlock = createBlock( 'core/block', { ref: 123 } ); - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' }, [ - createBlock( 'core/test-block', { name: 'Oscar the Grouch' } ), - createBlock( 'core/test-block', { name: 'Cookie Monster' } ), - ] ); - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); - jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( ( id ) => - associatedBlock.clientId === id ? associatedBlock : parsedBlock - ); - jest.spyOn( dataDispatch( 'core/block-editor' ), 'replaceBlocks' ).mockImplementation( () => {} ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - convertBlockToStatic( convertBlockToStaticAction( associatedBlock.clientId ), store ); - - expect( dataDispatch( 'core/block-editor' ).replaceBlocks ).toHaveBeenCalledWith( - associatedBlock.clientId, - [ - expect.objectContaining( { - name: 'core/test-block', - attributes: { name: 'Big Bird' }, - innerBlocks: [ - expect.objectContaining( { - attributes: { name: 'Oscar the Grouch' }, - } ), - expect.objectContaining( { - attributes: { name: 'Cookie Monster' }, - } ), - ], - } ), - ] - ); - - dataDispatch( 'core/block-editor' ).replaceBlocks.mockReset(); - dataSelect( 'core/block-editor' ).getBlock.mockReset(); - } ); - } ); - - describe( 'convertBlockToReusable', () => { - it( 'should convert a static block into a reusable block', () => { - const staticBlock = createBlock( 'core/block', { ref: 123 } ); - jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( ( ) => - staticBlock - ); - jest.spyOn( dataDispatch( 'core/block-editor' ), 'replaceBlocks' ).mockImplementation( () => {} ); - jest.spyOn( dataDispatch( 'core/block-editor' ), 'receiveBlocks' ).mockImplementation( () => {} ); - - const dispatch = jest.fn(); - const store = { getState: () => {}, dispatch }; - - convertBlockToReusable( convertBlockToReusableAction( staticBlock.clientId ), store ); - - expect( dispatch ).toHaveBeenCalledWith( - receiveReusableBlocksAction( [ { - reusableBlock: { - id: expect.stringMatching( /^reusable/ ), - clientId: staticBlock.clientId, - title: 'Untitled Reusable Block', - }, - parsedBlock: staticBlock, - } ] ) - ); - - expect( dispatch ).toHaveBeenCalledWith( - saveReusableBlock( expect.stringMatching( /^reusable/ ) ), - ); - - expect( dataDispatch( 'core/block-editor' ).replaceBlocks ).toHaveBeenCalledWith( - [ staticBlock.clientId ], - expect.objectContaining( { - name: 'core/block', - attributes: { ref: expect.stringMatching( /^reusable/ ) }, - } ), - ); - - expect( dataDispatch( 'core/block-editor' ).receiveBlocks ).toHaveBeenCalledWith( - [ staticBlock ] - ); - - dataDispatch( 'core/block-editor' ).replaceBlocks.mockReset(); - dataDispatch( 'core/block-editor' ).receiveBlocks.mockReset(); - dataSelect( 'core/block-editor' ).getBlock.mockReset(); - } ); - } ); -} ); diff --git a/packages/editor/src/store/effects/test/utils.js b/packages/editor/src/store/effects/test/utils.js deleted file mode 100644 index 464edbc50d6759..00000000000000 --- a/packages/editor/src/store/effects/test/utils.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * WordPress dependencies - */ -import { registerStore, select } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { resolveSelector } from '../utils'; - -describe( 'resolveSelector', () => { - const storeConfig = { - reducer: ( state = 'no', action ) => { - if ( action.type === 'resolve' ) { - return 'yes'; - } - return state; - }, - selectors: { - selectAll: ( state, key ) => ( key === 'check' ) ? state : 'no-key', - }, - resolvers: { - selectAll: () => { - return new Promise( ( resolve ) => { - resolve( { type: 'resolve' } ); - } ); - }, - }, - }; - - it( 'Should wait for selector resolution', async () => { - registerStore( 'resolveStore', storeConfig ); - - expect( select( 'resolveStore' ).selectAll( 'check' ) ).toBe( 'no' ); - const value = await resolveSelector( 'resolveStore', 'selectAll', 'check' ); - expect( value ).toBe( 'yes' ); - } ); - - it( 'Should resolve already resolved selectors', async () => { - registerStore( 'resolveStore2', storeConfig ); - - // Trigger resolution - const value = await resolveSelector( 'resolveStore2', 'selectAll', 'check' ); - expect( value ).toBe( 'yes' ); - await resolveSelector( 'resolveStore2', 'selectAll', 'check' ); - expect( value ).toBe( 'yes' ); - } ); -} ); diff --git a/packages/editor/src/store/effects/utils.js b/packages/editor/src/store/effects/utils.js deleted file mode 100644 index 979d907a344324..00000000000000 --- a/packages/editor/src/store/effects/utils.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * WordPress dependencies - */ -import { select, subscribe } from '@wordpress/data'; - -/** - * Waits for the resolution of a selector before returning the selector's value. - * - * @param {string} namespace Store namespace. - * @param {string} selectorName Selector name. - * @param {Array} args Selector args. - * - * @return {Promise} Selector result. - */ -export function resolveSelector( namespace, selectorName, ...args ) { - return new Promise( ( resolve ) => { - const hasFinished = () => select( 'core/data' ).hasFinishedResolution( namespace, selectorName, args ); - const getResult = () => select( namespace )[ selectorName ].apply( null, args ); - - // We need to trigger the selector (to trigger the resolver) - const result = getResult(); - if ( hasFinished() ) { - return resolve( result ); - } - - const unsubscribe = subscribe( () => { - if ( hasFinished() ) { - unsubscribe(); - resolve( getResult() ); - } - } ); - } ); -} diff --git a/packages/editor/src/store/index.js b/packages/editor/src/store/index.js index 42af629bcce0d3..a9729f67ea1286 100644 --- a/packages/editor/src/store/index.js +++ b/packages/editor/src/store/index.js @@ -7,7 +7,6 @@ import { registerStore } from '@wordpress/data'; * Internal dependencies */ import reducer from './reducer'; -import applyMiddlewares from './middlewares'; import * as selectors from './selectors'; import * as actions from './actions'; import controls from './controls'; @@ -20,6 +19,5 @@ const store = registerStore( STORE_KEY, { controls, persist: [ 'preferences' ], } ); -applyMiddlewares( store ); export default store; diff --git a/packages/editor/src/store/middlewares.js b/packages/editor/src/store/middlewares.js deleted file mode 100644 index 6381132bb81e08..00000000000000 --- a/packages/editor/src/store/middlewares.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * External dependencies - */ -import refx from 'refx'; -import multi from 'redux-multi'; -import { flowRight } from 'lodash'; - -/** - * Internal dependencies - */ -import effects from './effects'; - -/** - * Applies the custom middlewares used specifically in the editor module. - * - * @param {Object} store Store Object. - * - * @return {Object} Update Store Object. - */ -function applyMiddlewares( store ) { - const middlewares = [ - refx( effects ), - multi, - ]; - - let enhancedDispatch = () => { - throw new Error( - 'Dispatching while constructing your middleware is not allowed. ' + - 'Other middleware would not be applied to this dispatch.' - ); - }; - let chain = []; - - const middlewareAPI = { - getState: store.getState, - dispatch: ( ...args ) => enhancedDispatch( ...args ), - }; - chain = middlewares.map( ( middleware ) => middleware( middlewareAPI ) ); - enhancedDispatch = flowRight( ...chain )( store.dispatch ); - - store.dispatch = enhancedDispatch; - return store; -} - -export default applyMiddlewares; diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 296a365fe4c490..893db880baf7ed 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -2,6 +2,7 @@ * External dependencies */ import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; +import uuid from 'uuid/v4'; /** * Internal dependencies @@ -13,8 +14,19 @@ import { SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID, POST_UPDATE_TRANSACTION_ID, + REUSABLE_BLOCK_NOTICE_ID, } from '../constants'; +/** + * WordPress dependencies + */ +import { + registerBlockType, + unregisterBlockType, + createBlock, + serialize, +} from '@wordpress/blocks'; + jest.mock( '../controls' ); select.mockImplementation( ( ...args ) => { @@ -625,16 +637,30 @@ describe( 'Post generator actions', () => { } ); } ); -describe( 'actions', () => { - describe( 'setupEditor', () => { - it( 'should return the SETUP_EDITOR action', () => { - const post = {}; - const result = actions.setupEditor( post ); - expect( result ).toEqual( { - type: 'SETUP_EDITOR', +describe( 'Editor actions', () => { + describe( 'setupEditor()', () => { + let fulfillment; + const reset = ( post, edits, template ) => fulfillment = actions + .setupEditor( post, + edits, + template, + ); + it( 'should yield the SETUP_EDITOR action', () => { + reset( { content: { raw: '' }, status: 'publish' } ); + const { value } = fulfillment.next(); + expect( value ).toEqual( { + type: 'SETUP_EDITOR', + post: { content: { raw: '' }, status: 'publish' }, } ); } ); + it( 'should yield dispatch action for resetEditorBlocks', () => { + const { value } = fulfillment.next(); + expect( value.type ).toBe( 'DISPATCH' ); + expect( value.storeKey ).toBe( STORE_KEY ); + expect( value.actionName ).toBe( 'resetEditorBlocks' ); + expect( value.args ).toEqual( [ [] ] ); + } ); } ); describe( 'resetPost', () => { @@ -758,75 +784,824 @@ describe( 'actions', () => { } ); } ); - describe( 'fetchReusableBlocks', () => { - it( 'should return the FETCH_REUSABLE_BLOCKS action', () => { - expect( actions.__experimentalFetchReusableBlocks() ).toEqual( { + describe( 'lockPostSaving', () => { + it( 'should return the LOCK_POST_SAVING action', () => { + const result = actions.lockPostSaving( 'test' ); + expect( result ).toEqual( { + type: 'LOCK_POST_SAVING', + lockName: 'test', + } ); + } ); + } ); + + describe( 'unlockPostSaving', () => { + it( 'should return the UNLOCK_POST_SAVING action', () => { + const result = actions.unlockPostSaving( 'test' ); + expect( result ).toEqual( { + type: 'UNLOCK_POST_SAVING', + lockName: 'test', + } ); + } ); + } ); +} ); + +describe( 'Reusable block actions', () => { + beforeAll( () => { + registerBlockType( 'core/test-block', { + title: 'Test block', + category: 'common', + save: () => null, + attributes: { + name: { type: 'string' }, + }, + } ); + + registerBlockType( 'core/block', { + title: 'Reusable Block', + category: 'common', + save: () => null, + attributes: { + ref: { type: 'string' }, + }, + } ); + + registerBlockType( 'core/template', { + title: 'Reusable Block', + category: 'common', + save: () => null, + attributes: { + ref: { type: 'string' }, + }, + } ); + } ); + + afterAll( () => { + unregisterBlockType( 'core/test-block' ); + unregisterBlockType( 'core/block' ); + unregisterBlockType( 'core/template' ); + } ); + const postTypeResponse = { slug: 'wp_block', rest_base: 'blocks' }; + describe( 'fetchReusableBlocks()', () => { + let fulfillment; + const reset = ( id ) => fulfillment = actions + .__experimentalFetchReusableBlocks( id ); + const sampleBlock = { + id: 123, + status: 'publish', + title: { + raw: 'My cool block', + }, + content: { + raw: '', + protected: false, + }, + }; + it( 'should yield the FETCH_REUSABLE_BLOCKS action', () => { + reset(); + const { value } = fulfillment.next(); + expect( value ).toEqual( { type: 'FETCH_REUSABLE_BLOCKS', } ); } ); it( 'should take an optional id argument', () => { - expect( actions.__experimentalFetchReusableBlocks( 123 ) ).toEqual( { + reset( 123 ); + const { value } = fulfillment.next(); + expect( value ).toEqual( { type: 'FETCH_REUSABLE_BLOCKS', id: 123, } ); } ); - } ); - describe( 'saveReusableBlock', () => { - it( 'should return the SAVE_REUSABLE_BLOCK action', () => { - expect( actions.__experimentalSaveReusableBlock( 123 ) ).toEqual( { - type: 'SAVE_REUSABLE_BLOCK', + it( 'should yield action for fetch block post type', () => { + const { value } = fulfillment.next(); + apiFetchDoActual(); + expect( value ).toEqual( apiFetch( + { path: '/wp/v2/types/wp_block' } + ) ); + } ); + + it( 'should bail if post type retrieval fails', () => { + const { value, done } = fulfillment.next( undefined ); + expect( value ).toBeUndefined(); + expect( done ).toBe( true ); + } ); + + it( 'should yield FETCH_REUSABLE_BLOCKS_FAILURE action if fetch throws ' + + 'error', () => { + reset( 123 ); + apiFetchDoActual(); + fulfillment.next(); + fulfillment.next(); + apiFetchThrowError( 'error' ); + const { value } = fulfillment.next( postTypeResponse ); + expect( value ).toEqual( { + type: 'FETCH_REUSABLE_BLOCKS_FAILURE', id: 123, + error: 'error', } ); } ); - } ); - describe( 'deleteReusableBlock', () => { - it( 'should return the DELETE_REUSABLE_BLOCK action', () => { - expect( actions.__experimentalDeleteReusableBlock( 123 ) ).toEqual( { - type: 'DELETE_REUSABLE_BLOCK', + it( 'should yield specific block fetch action if id is provided', () => { + reset( 123 ); + apiFetchDoActual(); + fulfillment.next(); + fulfillment.next(); + const { value } = fulfillment.next( postTypeResponse ); + expect( value ).toEqual( + apiFetch( + { path: `/wp/v2/blocks/123` } + ) + ); + } ); + + it( 'should yield general blocks fetch action if id not provided', () => { + reset(); + apiFetchDoActual(); + fulfillment.next(); + fulfillment.next(); + const { value } = fulfillment.next( postTypeResponse ); + expect( value ).toEqual( + apiFetch( + { path: `/wp/v2/blocks?per_page=-1` } + ) + ); + } ); + + it( 'should yield dispatch action for receiving multiple reusable ' + + 'blocks', () => { + const { value } = fulfillment.next( [ + { ...sampleBlock, status: 'publish' }, + ] ); + + expect( value ).toEqual( + dispatch( + STORE_KEY, + '__experimentalReceiveReusableBlocks', + [ + { + reusableBlock: { + id: 123, + title: 'My cool block', + }, + parsedBlock: expect.objectContaining( { + name: 'core/test-block', + attributes: { name: 'Big Bird' }, + } ), + }, + ] + ) + ); + } ); + + it( 'should yield dispatch action for receiving single reusable ' + + 'block', () => { + reset( 123 ); + apiFetchDoActual(); + fulfillment.next(); + fulfillment.next(); + fulfillment.next( postTypeResponse ); + const { value } = fulfillment.next( + { ...sampleBlock, status: 'publish' } + ); + expect( value ).toEqual( + dispatch( + STORE_KEY, + '__experimentalReceiveReusableBlocks', + [ + { + reusableBlock: { + id: 123, + title: 'My cool block', + }, + parsedBlock: expect.objectContaining( { + name: 'core/test-block', + attributes: { name: 'Big Bird' }, + } ), + }, + ] + ) + ); + } ); + + it( 'should yield action for successful fetch', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( { + type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', id: 123, } ); } ); - } ); - describe( 'convertBlockToStatic', () => { - it( 'should return the CONVERT_BLOCK_TO_STATIC action', () => { - const clientId = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; - expect( actions.__experimentalConvertBlockToStatic( clientId ) ).toEqual( { - type: 'CONVERT_BLOCK_TO_STATIC', - clientId, + it( 'should ignore reusable blocks with a trashed post status', () => { + const testBlock = { + id: 123, + status: 'trash', + title: { + raw: 'My cool block', + }, + content: { + raw: '', + protected: false, + }, + }; + reset( 123 ); + apiFetchDoActual(); + fulfillment.next(); + fulfillment.next(); + fulfillment.next( postTypeResponse ); + const { value } = fulfillment.next( testBlock ); + expect( value ).toEqual( { + type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', + id: 123, } ); } ); } ); - describe( 'convertBlockToReusable', () => { - it( 'should return the CONVERT_BLOCK_TO_REUSABLE action', () => { - const clientId = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; - expect( actions.__experimentalConvertBlockToReusable( clientId ) ).toEqual( { - type: 'CONVERT_BLOCK_TO_REUSABLE', - clientIds: [ clientId ], + describe( 'receiveReusableBlocks()', () => { + let fulfillment; + const reset = () => fulfillment = actions + .__experimentalReceiveReusableBlocks( + [ { reusableBlock: [], parsedBlock: {} } ] + ); + it( 'yields action for RECEIVE_REUSABLE_BLOCKS', () => { + reset(); + const { value } = fulfillment.next(); + expect( value ).toEqual( { + type: 'RECEIVE_REUSABLE_BLOCKS', + results: [ { reusableBlock: [], parsedBlock: {} } ], } ); } ); + it( 'yields dispatch action for receiveBlocks on core/block-editor ' + + 'store', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + 'core/block-editor', + 'receiveBlocks', + [ {} ] + ) + ); + } ); } ); - describe( 'lockPostSaving', () => { - it( 'should return the LOCK_POST_SAVING action', () => { - const result = actions.lockPostSaving( 'test' ); - expect( result ).toEqual( { - type: 'LOCK_POST_SAVING', - lockName: 'test', + describe( 'saveReusableBlock', () => { + let fulfillment; + const reset = () => fulfillment = actions + .__experimentalSaveReusableBlock( 123 ); + const reusableBlock = { + id: 123, + title: 'My cool block', + clientId: uuid(), + isTemporary: false, + }; + it( 'should yield the SAVE_REUSABLE_BLOCK action', () => { + reset(); + const { value } = fulfillment.next(); + expect( value ).toEqual( { + type: 'SAVE_REUSABLE_BLOCK', + id: 123, } ); } ); + it( 'should yield api action for block post type', () => { + apiFetchDoActual(); + const { value } = fulfillment.next(); + expect( value ).toEqual( + apiFetch( + { path: '/wp/v2/types/wp_block' } + ) + ); + } ); + it( 'returns undefined if the post type is not available', () => { + const { value, done } = fulfillment.next( undefined ); + expect( value ).toBeUndefined(); + expect( done ).toBe( true ); + } ); + it( 'yields select action for getting reusable block for the id', () => { + reset(); + fulfillment.next(); + apiFetchDoActual(); + fulfillment.next(); + const { value } = fulfillment.next( postTypeResponse ); + expect( value ).toEqual( + select( + STORE_KEY, + '__experimentalGetReusableBlock', + 123 + ) + ); + } ); + it( 'yields select action for getBlock from core/block-editor ' + + 'store', () => { + const { value } = fulfillment.next( reusableBlock ); + expect( value ).toEqual( + select( + 'core/block-editor', + 'getBlock', + reusableBlock.clientId + ) + ); + } ); + it( 'yields expected fetch action when reusable block is not ' + + 'temporary', () => { + const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); + apiFetchDoActual(); + const { value } = fulfillment.next( parsedBlock ); + expect( value ).toEqual( + apiFetch( + { + path: '/wp/v2/blocks/123', + data: { + id: 123, + title: 'My cool block', + content: serialize( parsedBlock ), + status: 'publish', + }, + method: 'PUT', + } + ) + ); + } ); + it( 'yields expected fetch action when reusable block is temporary', () => { + const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); + apiFetchDoActual(); + reset(); + fulfillment.next(); + fulfillment.next(); + fulfillment.next( postTypeResponse ); + fulfillment.next( { ...reusableBlock, isTemporary: true } ); + const { value } = fulfillment.next( parsedBlock ); + expect( value ).toEqual( + apiFetch( { + path: '/wp/v2/blocks', + data: { + title: 'My cool block', + content: serialize( parsedBlock ), + status: 'publish', + }, + method: 'POST', + } ) + ); + } ); + it( 'yields expected success action for successful update fetch', () => { + const { value } = fulfillment.next( { id: 456 } ); + expect( value ).toEqual( { + type: 'SAVE_REUSABLE_BLOCK_SUCCESS', + updatedId: 456, + id: 123, + } ); + } ); + it( 'yields expected dispatch action for success notice', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + 'core/notices', + 'createSuccessNotice', + 'Block created.', + { id: REUSABLE_BLOCK_NOTICE_ID } + ) + ); + } ); + it( 'yields expected dispatch action for saving reusable block on the ' + + 'core/block-editor store', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + 'core/block-editor', + '__unstableSaveReusableBlock', + 123, + 456, + ) + ); + // it is done + const { done } = fulfillment.next(); + expect( done ).toBe( true ); + } ); + + it( 'yields expected failure action (and notice dispatch) when api ' + + 'fetch for updating block fails', () => { + const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); + apiFetchDoActual(); + reset(); + fulfillment.next(); + fulfillment.next(); + fulfillment.next( postTypeResponse ); + apiFetchThrowError( { message: 'fail' } ); + fulfillment.next( reusableBlock ); + const { value } = fulfillment.next( parsedBlock ); + expect( value ).toEqual( + { + type: 'SAVE_REUSABLE_BLOCK_FAILURE', + id: 123, + } + ); + const { value: noticeDispatchAction } = fulfillment.next(); + expect( noticeDispatchAction ).toEqual( + dispatch( + 'core/notices', + 'createErrorNotice', + 'fail', + { id: REUSABLE_BLOCK_NOTICE_ID } + ) + ); + } ); } ); - describe( 'unlockPostSaving', () => { - it( 'should return the UNLOCK_POST_SAVING action', () => { - const result = actions.unlockPostSaving( 'test' ); - expect( result ).toEqual( { - type: 'UNLOCK_POST_SAVING', - lockName: 'test', + describe( 'deleteReusableBlock', () => { + let fulfillment; + const sampleBlock = { + id: 123, + clientId: 'clientid1', + status: 'publish', + title: { + raw: 'My cool block', + }, + content: { + raw: '', + protected: false, + }, + }; + const reset = () => fulfillment = actions + .__experimentalDeleteReusableBlock( 123 ); + it( 'should yield apiFetch action for blocks post type', () => { + reset(); + apiFetchDoActual(); + const { value } = fulfillment.next(); + expect( value ).toEqual( + apiFetch( + { path: '/wp/v2/types/wp_block' } + ) + ); + } ); + it( 'should return undefined if retrieved post type is not ' + + 'available', () => { + const { value, done } = fulfillment.next(); + expect( value ).toBeUndefined(); + expect( done ).toBe( true ); + } ); + it( 'should yield select action for __experimentalGetReusableBlock', () => { + reset(); + apiFetchDoActual(); + fulfillment.next(); + const { value } = fulfillment.next( postTypeResponse ); + expect( value ).toEqual( + select( + STORE_KEY, + '__experimentalGetReusableBlock', + 123 + ) + ); + } ); + it( 'should return undefined if there is no reusable block for the given' + + 'id or it is temporary', () => { + const { value: nonBlock, done: nonBlockDone } = fulfillment.next(); + expect( nonBlock ).toBeUndefined(); + expect( nonBlockDone ).toBe( true ); + + reset(); + apiFetchDoActual(); + fulfillment.next(); + fulfillment.next( postTypeResponse ); + const { value: tempBlock, done: tempBlockDone } = fulfillment.next( + { + ...sampleBlock, + isTemporary: true, + } + ); + expect( tempBlock ).toBeUndefined(); + expect( tempBlockDone ).toBe( true ); + } ); + it( 'should return select action for getBlocks selector on the ' + + 'core/block-editor store', () => { + reset(); + apiFetchDoActual(); + fulfillment.next(); + fulfillment.next( postTypeResponse ); + const { value } = fulfillment.next( sampleBlock ); + expect( value ).toEqual( + select( + 'core/block-editor', + 'getBlocks' + ) + ); + } ); + it( 'should yield action for removing reusable block', () => { + const { value } = fulfillment.next( [ { + ...sampleBlock, + clientId: 'clientid2', + } ] ); + expect( value.type ).toBe( 'REMOVE_REUSABLE_BLOCK' ); + expect( value.id ).toBe( 123 ); + expect( value.optimist.type ).toBe( BEGIN ); + } ); + it( 'should yield dispatch action for removeBlocks on the ' + + 'core/block-editor store', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + 'core/block-editor', + 'removeBlocks', + [ 'clientid1' ] + ) + ); + } ); + it( 'should yield the apiFetch action for deleting the block', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + apiFetch( + { + path: `/wp/v2/blocks/123`, + method: 'DELETE', + } + ) + ); + } ); + it( 'should yield the action for DELETE_REUSABLE_BLOCK_SUCCESS', () => { + const { value } = fulfillment.next(); + expect( value.type ).toBe( 'DELETE_REUSABLE_BLOCK_SUCCESS' ); + expect( value.id ).toBe( 123 ); + expect( value.optimist.type ).toBe( COMMIT ); + } ); + it( 'should yield dispatch action for creating a success notice and be ' + + 'complete', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + 'core/notices', + 'createSuccessNotice', + 'Block deleted.', + { id: REUSABLE_BLOCK_NOTICE_ID } + ) + ); + const { done } = fulfillment.next(); + expect( done ).toBe( true ); + } ); + it( 'should yield expected error action and notice if delete request ' + + 'errors', () => { + reset(); + apiFetchDoActual(); + fulfillment.next(); + fulfillment.next( postTypeResponse ); + fulfillment.next( sampleBlock ); + fulfillment.next( [ { ...sampleBlock, clientId: 'clientid2' } ] ); + fulfillment.next(); + apiFetchThrowError( { message: 'error' } ); + const { value } = fulfillment.next(); + expect( value.type ).toBe( 'DELETE_REUSABLE_BLOCK_FAILURE' ); + expect( value.id ).toBe( 123 ); + expect( value.optimist.type ).toBe( REVERT ); + + const { value: noticeAction } = fulfillment.next(); + expect( noticeAction ).toEqual( + dispatch( + 'core/notices', + 'error', + { id: REUSABLE_BLOCK_NOTICE_ID } + ) + ); + + const { done } = fulfillment.next(); + expect( done ).toBe( true ); + } ); + } ); + + describe( 'convertBlockToStatic', () => { + const clientId = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; + let fulfillment; + const reset = () => fulfillment = actions + .__experimentalConvertBlockToStatic( clientId ); + const createAssociatedBlock = () => createBlock( + 'core/block', + { ref: 123 } + ); + const reusableBlock = { id: 123, title: 'My cool block', clientId }; + const createParsedBlock = () => createBlock( + 'core/test-block', + { name: 'Big Bird' } + ); + it( 'should yield the getBlock selector for the core/block-editor ' + + 'store', () => { + reset(); + const { value } = fulfillment.next(); + expect( value ).toEqual( + select( + 'core/block-editor', + 'getBlock', + clientId + ) + ); + } ); + it( 'should yield the select action for ' + + '__experimentalGetReusableBlock', () => { + const { value } = fulfillment.next( createAssociatedBlock() ); + expect( value ).toEqual( + select( + STORE_KEY, + '__experimentalGetReusableBlock', + 123 + ) + ); + } ); + it( 'should yield the select action for getBlock from the ' + + 'core/block-editor store', () => { + const { value } = fulfillment.next( reusableBlock ); + expect( value ).toEqual( + select( + 'core/block-editor', + 'getBlock', + clientId, + ) + ); + } ); + it( 'should yield dispatch action for the replaceBlocks dispatch action ' + + 'in the core/block-editor store', () => { + const associatedBlock = createAssociatedBlock(); + const parsedBlock = createParsedBlock(); + reset(); + fulfillment.next(); + fulfillment.next( associatedBlock ); + fulfillment.next( reusableBlock ); + const { value } = fulfillment.next( parsedBlock ); + expect( value ).toEqual( + dispatch( + 'core/block-editor', + 'replaceBlocks', + associatedBlock.clientId, + [ + expect.objectContaining( { + name: 'core/test-block', + attributes: { name: 'Big Bird' }, + } ), + ] + ) + ); + // should be done + const { done } = fulfillment.next(); + expect( done ).toBe( true ); + } ); + + it( 'should convert a reusable block with nested blocks into a static ' + + 'block', () => { + const associatedBlock = createBlock( 'core/block', { ref: 123 } ); + const parsedBlock = createBlock( + 'core/test-block', + { name: 'Big Bird' }, + [ + createBlock( 'core/test-block', { name: 'Oscar the Grouch' } ), + createBlock( 'core/test-block', { name: 'Cookie Monster' } ), + ] + ); + reset(); + fulfillment.next(); + fulfillment.next( associatedBlock ); + fulfillment.next( reusableBlock ); + const { value } = fulfillment.next( parsedBlock ); + expect( value ).toEqual( + dispatch( + 'core/block-editor', + 'replaceBlocks', + associatedBlock.clientId, + [ + expect.objectContaining( { + name: 'core/test-block', + attributes: { name: 'Big Bird' }, + innerBlocks: [ + expect.objectContaining( { + attributes: { name: 'Oscar the Grouch' }, + } ), + expect.objectContaining( { + attributes: { name: 'Cookie Monster' }, + } ), + ], + } ), + ] + ) + ); + // should be done + const { done } = fulfillment.next(); + expect( done ).toBe( true ); + } ); + } ); + + describe( 'convertBlockToReusable', () => { + const createStaticBlock = () => createBlock( 'core/block', { ref: 123 } ); + let fulfillment; + const reset = ( id ) => fulfillment = actions + .__experimentalConvertBlockToReusable( id ); + describe( 'yielded actions when only one client id is passed in', () => { + it( 'yields select action for the getBlock selector from the ' + + 'core/block-editor store', () => { + reset( 123 ); + const { value } = fulfillment.next(); + expect( value ).toEqual( + select( + 'core/block-editor', + 'getBlock', + 123 + ) + ); + } ); + it( 'yields dispatch action for ' + + '__experimentalReceiveReusableBlocks', () => { + const staticBlock = createStaticBlock(); + const { value } = fulfillment.next( staticBlock ); + expect( value ).toEqual( + dispatch( + STORE_KEY, + '__experimentalReceiveReusableBlocks', + [ + { + parsedBlock: staticBlock, + reusableBlock: { + id: expect.stringMatching( /^reusable/ ), + clientId: staticBlock.clientId, + title: 'Untitled Reusable Block', + }, + }, + ] + ) + ); + } ); + it( 'yields dispatch action for the replaceBlocks action on the ' + + 'core/block-editor store', () => { + const staticBlock = createStaticBlock(); + reset( staticBlock.clientId ); + fulfillment.next(); + fulfillment.next( staticBlock ); + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + 'core/block-editor', + 'replaceBlocks', + [ staticBlock.clientId ], + expect.objectContaining( + { + name: 'core/block', + attributes: { + ref: expect.stringMatching( /^reusable/ ), + }, + } + ) + ) + ); + } ); + it( 'yields the receiveBlocks action on the core/block-editor ' + + 'store', () => { + const staticBlock = createStaticBlock(); + reset( staticBlock.clientId ); + fulfillment.next(); + fulfillment.next( staticBlock ); + fulfillment.next(); + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + 'core/block-editor', + 'receiveBlocks', + [ staticBlock ] + ) + ); + } ); + it( 'yields dispatch action for the __experimentalSaveReusableBlock ' + + 'selector on the core/editor store', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + STORE_KEY, + '__experimentalSaveReusableBlock', + expect.stringMatching( /^reusable/ ), + ) + ); + const { done } = fulfillment.next(); + expect( done ).toBe( true ); + } ); + } ); + describe( 'yielded actions when multiple client ids are passed in', () => { + it( 'should yield select action for the getBlocksByClientId selector ' + + 'on the core/block-editor store', () => { + reset( [ 123, 456 ] ); + const { value } = fulfillment.next(); + expect( value ).toEqual( + select( + 'core/block-editor', + 'getBlocksByClientId', + [ 123, 456 ] + ) + ); + } ); + it( 'should yield dispatch action for receiveBlocks', () => { + const staticBlock = createStaticBlock(); + const { value } = fulfillment.next( staticBlock ); + expect( value ).toEqual( + dispatch( + 'core/block-editor', + 'receiveBlocks', + [ + expect.objectContaining( { + name: 'core/template', + innerBlocks: staticBlock, + } ), + ] + ) + ); } ); } ); } ); diff --git a/packages/editor/src/store/test/effects.js b/packages/editor/src/store/test/effects.js deleted file mode 100644 index cdc7223858e0fe..00000000000000 --- a/packages/editor/src/store/test/effects.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * WordPress dependencies - */ -import { - getBlockTypes, - unregisterBlockType, - registerBlockType, -} from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { setupEditorState, resetEditorBlocks } from '../actions'; -import effects from '../effects'; -import '../../'; - -describe( 'effects', () => { - const defaultBlockSettings = { save: () => 'Saved', category: 'common', title: 'block title' }; - - describe( '.SETUP_EDITOR', () => { - const handler = effects.SETUP_EDITOR; - - afterEach( () => { - getBlockTypes().forEach( ( block ) => { - unregisterBlockType( block.name ); - } ); - } ); - - it( 'should return post reset action', () => { - const post = { - id: 1, - title: { - raw: 'A History of Pork', - }, - content: { - raw: '', - }, - status: 'draft', - }; - - const result = handler( { post, settings: {} } ); - - expect( result ).toEqual( [ - resetEditorBlocks( [] ), - setupEditorState( post, [], {} ), - ] ); - } ); - - it( 'should return block reset with non-empty content', () => { - registerBlockType( 'core/test-block', defaultBlockSettings ); - const post = { - id: 1, - title: { - raw: 'A History of Pork', - }, - content: { - raw: 'Saved', - }, - status: 'draft', - }; - - const result = handler( { post } ); - - expect( result[ 0 ].blocks ).toHaveLength( 1 ); - expect( result[ 1 ] ).toEqual( setupEditorState( post, result[ 0 ].blocks, {} ) ); - } ); - - it( 'should return post setup action only if auto-draft', () => { - const post = { - id: 1, - title: { - raw: 'A History of Pork', - }, - content: { - raw: '', - }, - status: 'auto-draft', - }; - - const result = handler( { post } ); - - expect( result ).toEqual( [ - resetEditorBlocks( [] ), - setupEditorState( post, [], { title: 'A History of Pork' } ), - ] ); - } ); - } ); -} );