From 0b124ebb99b7ed199e088478d5d6e913131c99db Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Fri, 15 Dec 2017 17:13:19 +1100 Subject: [PATCH 1/5] Refactor Reusable Blocks controller to use WP_REST_Posts_Controller We can remove a lot of code by subclassing WP_REST_Posts_Controller, which gives us methods for creating, reading, updating and deleting posts (in this case, blocks) for free. This also adds support for DELETEing a block to the API, and renames the 'name' field to 'title'. --- blocks/api/factory.js | 2 +- blocks/api/test/factory.js | 2 +- blocks/library/block/edit-panel/index.js | 14 +- blocks/library/block/edit-panel/style.scss | 2 +- blocks/library/block/index.js | 20 +- editor/components/inserter/menu.js | 2 +- editor/store/effects.js | 14 +- editor/store/test/effects.js | 24 +- lib/class-wp-rest-blocks-controller.php | 116 +++++++ ...ass-wp-rest-reusable-blocks-controller.php | 306 ------------------ lib/client-assets.php | 2 +- lib/load.php | 2 +- lib/register.php | 22 +- phpunit/class-rest-blocks-controller-test.php | 206 ++++++++++++ ...s-rest-reusable-blocks-controller-test.php | 270 ---------------- 15 files changed, 373 insertions(+), 631 deletions(-) create mode 100644 lib/class-wp-rest-blocks-controller.php delete mode 100644 lib/class-wp-rest-reusable-blocks-controller.php create mode 100644 phpunit/class-rest-blocks-controller-test.php delete mode 100644 phpunit/class-rest-reusable-blocks-controller-test.php diff --git a/blocks/api/factory.js b/blocks/api/factory.js index 60dfc8ce1bf7f3..f2cff80082af76 100644 --- a/blocks/api/factory.js +++ b/blocks/api/factory.js @@ -224,7 +224,7 @@ export function createReusableBlock( type, attributes ) { return { id: +uniqueId(), // Temorary id replaced when the block is saved server side isTemporary: true, - name: __( 'Untitled block' ), + title: __( 'Untitled block' ), type, attributes, }; diff --git a/blocks/api/test/factory.js b/blocks/api/test/factory.js index 0bbb4837c69105..9932007d88f82b 100644 --- a/blocks/api/test/factory.js +++ b/blocks/api/test/factory.js @@ -624,7 +624,7 @@ describe( 'block factory', () => { expect( createReusableBlock( type, attributes ) ).toMatchObject( { id: expect.any( Number ), - name: 'Untitled block', + title: 'Untitled block', type, attributes, } ); diff --git a/blocks/library/block/edit-panel/index.js b/blocks/library/block/edit-panel/index.js index 793ef1f932ca0e..357c9dd683aa82 100644 --- a/blocks/library/block/edit-panel/index.js +++ b/blocks/library/block/edit-panel/index.js @@ -10,13 +10,13 @@ import { __ } from '@wordpress/i18n'; import './style.scss'; function ReusableBlockEditPanel( props ) { - const { isEditing, name, isSaving, onEdit, onDetach, onChangeName, onSave, onCancel } = props; + const { isEditing, title, isSaving, onEdit, onDetach, onChangeTitle, onSave, onCancel } = props; return (
{ ! isEditing && ! isSaving && [ - { name } + { title } , , - , ] } { ( isEditing || isSaving ) && [ +
{ possibleBlockTransformations.map( ( { name, title, icon } ) => { /* translators: label indicating the transformation of a block into another block */ const shownText = sprintf( __( 'Turn into %s' ), title ); diff --git a/editor/components/block-settings-menu/index.js b/editor/components/block-settings-menu/index.js index b903cc9f196f32..f3775a327251d0 100644 --- a/editor/components/block-settings-menu/index.js +++ b/editor/components/block-settings-menu/index.js @@ -18,7 +18,7 @@ import BlockInspectorButton from './block-inspector-button'; import BlockModeToggle from './block-mode-toggle'; import BlockDeleteButton from './block-delete-button'; import BlockTransformations from './block-transformations'; -import ReusableBlockToggle from './reusable-block-toggle'; +import ReusableBlockSettings from './reusable-block-settings'; import UnknownConverter from './unknown-converter'; import { selectBlock } from '../../store/actions'; @@ -58,7 +58,7 @@ function BlockSettingsMenu( { uids, onSelect, focus } ) { { count === 1 && } { count === 1 && } - { count === 1 && } + { count === 1 && } ) } diff --git a/editor/components/block-settings-menu/reusable-block-settings.js b/editor/components/block-settings-menu/reusable-block-settings.js new file mode 100644 index 00000000000000..6bb9950b17333b --- /dev/null +++ b/editor/components/block-settings-menu/reusable-block-settings.js @@ -0,0 +1,81 @@ +/** + * External dependencies + */ +import { connect } from 'react-redux'; +import { noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Fragment } from '@wordpress/element'; +import { IconButton } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { isReusableBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { getBlock } from '../../store/selectors'; +import { convertBlockToStatic, convertBlockToReusable, deleteReusableBlock } from '../../store/actions'; + +export function ReusableBlockSettings( { block, onConvertToStatic, onConvertToReusable, onDelete } ) { + const isReusable = isReusableBlock( block ); + + return ( + + { ! isReusable && ( + + { __( 'Convert to Reusable Block' ) } + + ) } + { isReusable && ( +
+ + { __( 'Detach from Reusable Block' ) } + + onDelete( block.attributes.ref ) } + > + { __( 'Delete Reusable Block' ) } + +
+ ) } +
+ ); +} + +export default connect( + ( state, { uid } ) => { + return { + block: getBlock( state, uid ), + }; + }, + ( dispatch, { uid, onToggle = noop } ) => ( { + onConvertToStatic() { + dispatch( convertBlockToStatic( uid ) ); + onToggle(); + }, + onConvertToReusable() { + dispatch( convertBlockToReusable( uid ) ); + onToggle(); + }, + onDelete( id ) { + // TODO: Make this a component or similar + // eslint-disable-next-line no-alert + if ( window.confirm( __( 'Are you sure you want to permanently delete this Reusable Block?' ) ) ) { + dispatch( deleteReusableBlock( id ) ); + onToggle(); + } + }, + } ) +)( ReusableBlockSettings ); diff --git a/editor/components/block-settings-menu/reusable-block-toggle.js b/editor/components/block-settings-menu/reusable-block-toggle.js deleted file mode 100644 index b8c5301a4d0133..00000000000000 --- a/editor/components/block-settings-menu/reusable-block-toggle.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; -import { noop } from 'lodash'; - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { IconButton } from '@wordpress/components'; -import { isReusableBlock } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { getBlock } from '../../store/selectors'; -import { convertBlockToStatic, convertBlockToReusable } from '../../store/actions'; - -export function ReusableBlockToggle( { isReusable, convertToStatic, convertToReusable } ) { - return ( - - { isReusable ? __( 'Detach from Reusable Block' ) : __( 'Convert to Reusable Block' ) } - - ); -} - -export default connect( - ( state, { uid } ) => { - const block = getBlock( state, uid ); - return { - isReusable: isReusableBlock( block ), - }; - }, - ( dispatch, { uid, onToggle = noop } ) => ( { - convertToStatic() { - dispatch( convertBlockToStatic( uid ) ); - onToggle(); - }, - convertToReusable() { - dispatch( convertBlockToReusable( uid ) ); - onToggle(); - }, - } ) -)( ReusableBlockToggle ); diff --git a/editor/components/block-settings-menu/style.scss b/editor/components/block-settings-menu/style.scss index c6ac5f1ebe1b12..72b0e3a8c77142 100644 --- a/editor/components/block-settings-menu/style.scss +++ b/editor/components/block-settings-menu/style.scss @@ -46,7 +46,7 @@ } } -.editor-block-settings-menu__block-transformations { +.editor-block-settings-menu__section { margin-top: $item-spacing; padding-top: $item-spacing; border-top: 1px solid $light-gray-500; diff --git a/editor/components/block-settings-menu/test/reusable-block-settings.js b/editor/components/block-settings-menu/test/reusable-block-settings.js new file mode 100644 index 00000000000000..39a943d3a1a114 --- /dev/null +++ b/editor/components/block-settings-menu/test/reusable-block-settings.js @@ -0,0 +1,59 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { ReusableBlockSettings } from '../reusable-block-settings'; + +describe( 'ReusableBlockSettings', () => { + it( 'should allow converting a static block to reusable', () => { + const onConvert = jest.fn(); + const wrapper = shallow( + + ); + + const text = wrapper.find( 'IconButton' ).children().text(); + expect( text ).toEqual( 'Convert to Reusable Block' ); + + wrapper.find( 'IconButton' ).simulate( 'click' ); + expect( onConvert ).toHaveBeenCalled(); + } ); + + it( 'should allow converting a reusable block to static', () => { + const onConvert = jest.fn(); + const wrapper = shallow( + + ); + + const text = wrapper.find( 'IconButton' ).first().children().text(); + expect( text ).toEqual( 'Detach from Reusable Block' ); + + wrapper.find( 'IconButton' ).first().simulate( 'click' ); + expect( onConvert ).toHaveBeenCalled(); + } ); + + it( 'should allow deleting a reusable block', () => { + const onDelete = jest.fn(); + const wrapper = shallow( + + ); + + const text = wrapper.find( 'IconButton' ).last().children().text(); + expect( text ).toEqual( 'Delete Reusable Block' ); + + wrapper.find( 'IconButton' ).last().simulate( 'click' ); + expect( onDelete ).toHaveBeenCalledWith( 123 ); + } ); +} ); diff --git a/editor/components/block-settings-menu/test/reusable-block-toggle.js b/editor/components/block-settings-menu/test/reusable-block-toggle.js deleted file mode 100644 index 4c6258d49f1947..00000000000000 --- a/editor/components/block-settings-menu/test/reusable-block-toggle.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * External dependencies - */ -import { shallow } from 'enzyme'; - -/** - * Internal dependencies - */ -import { ReusableBlockToggle } from '../reusable-block-toggle'; - -describe( 'ReusableBlockToggle', () => { - it( 'should allow converting a reusable block to static', () => { - const wrapper = shallow( - - ); - const text = wrapper.find( 'IconButton' ).first().prop( 'children' ); - - expect( text ).toEqual( 'Detach from Reusable Block' ); - } ); - - it( 'should allow converting a static block to reusable', () => { - const wrapper = shallow( - - ); - const text = wrapper.find( 'IconButton' ).first().prop( 'children' ); - - expect( text ).toEqual( 'Convert to Reusable Block' ); - } ); -} ); diff --git a/element/index.js b/element/index.js index 0094a8392ff74b..f850aa80b79553 100644 --- a/element/index.js +++ b/element/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { createElement, Component, cloneElement, Children } from 'react'; +import { createElement, Component, cloneElement, Children, Fragment } from 'react'; import { render, findDOMNode, createPortal, unmountComponentAtNode } from 'react-dom'; import { renderToStaticMarkup } from 'react-dom/server'; import { camelCase, flowRight, isString, upperFirst } from 'lodash'; @@ -58,6 +58,11 @@ export { findDOMNode }; export { Children }; +/** + * A component which renders its children without any wrapping element. + */ +export { Fragment }; + /** * Creates a portal into which a component can be rendered. * From 7be6489a46f33d714a6a434af4e8c7c877e3b048 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 21 Dec 2017 12:20:00 +1100 Subject: [PATCH 4/5] Rename 'Delete' button in block settings menu to 'Remove' This helps reduce the ambiguity between removing a block and deleting a Reusable Block. --- editor/components/block-list/block-mobile-toolbar.js | 4 ++-- .../{block-delete-button.js => block-remove-button.js} | 10 +++++----- editor/components/block-settings-menu/index.js | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) rename editor/components/block-settings-menu/{block-delete-button.js => block-remove-button.js} (83%) diff --git a/editor/components/block-list/block-mobile-toolbar.js b/editor/components/block-list/block-mobile-toolbar.js index 006181bd71afb4..5a45f331518057 100644 --- a/editor/components/block-list/block-mobile-toolbar.js +++ b/editor/components/block-list/block-mobile-toolbar.js @@ -2,14 +2,14 @@ * Internal dependencies */ import BlockMover from '../block-mover'; -import BlockDeleteButton from '../block-settings-menu/block-delete-button'; +import BlockRemoveButton from '../block-settings-menu/block-remove-button'; import BlockSettingsMenu from '../block-settings-menu'; function BlockMobileToolbar( { uid } ) { return (
- +
); diff --git a/editor/components/block-settings-menu/block-delete-button.js b/editor/components/block-settings-menu/block-remove-button.js similarity index 83% rename from editor/components/block-settings-menu/block-delete-button.js rename to editor/components/block-settings-menu/block-remove-button.js index 0037e6039e6320..8756112e61fa25 100644 --- a/editor/components/block-settings-menu/block-delete-button.js +++ b/editor/components/block-settings-menu/block-remove-button.js @@ -16,17 +16,17 @@ import { compose } from '@wordpress/element'; */ import { removeBlocks } from '../../store/actions'; -export function BlockDeleteButton( { onDelete, onClick = noop, isLocked, small = false } ) { +export function BlockRemoveButton( { onRemove, onClick = noop, isLocked, small = false } ) { if ( isLocked ) { return null; } - const label = __( 'Delete' ); + const label = __( 'Remove' ); return ( @@ -39,7 +39,7 @@ export default compose( connect( undefined, ( dispatch, ownProps ) => ( { - onDelete() { + onRemove() { dispatch( removeBlocks( ownProps.uids ) ); }, } ) @@ -51,4 +51,4 @@ export default compose( isLocked: !! templateLock, }; } ), -)( BlockDeleteButton ); +)( BlockRemoveButton ); diff --git a/editor/components/block-settings-menu/index.js b/editor/components/block-settings-menu/index.js index f3775a327251d0..b314f4a62d47f9 100644 --- a/editor/components/block-settings-menu/index.js +++ b/editor/components/block-settings-menu/index.js @@ -16,7 +16,7 @@ import { IconButton, Dropdown, NavigableMenu } from '@wordpress/components'; import './style.scss'; import BlockInspectorButton from './block-inspector-button'; import BlockModeToggle from './block-mode-toggle'; -import BlockDeleteButton from './block-delete-button'; +import BlockRemoveButton from './block-remove-button'; import BlockTransformations from './block-transformations'; import ReusableBlockSettings from './reusable-block-settings'; import UnknownConverter from './unknown-converter'; @@ -57,7 +57,7 @@ function BlockSettingsMenu( { uids, onSelect, focus } ) { { count === 1 && } { count === 1 && } - + { count === 1 && } From 968bd14ea1020cc2e217c00acfa5f6594a37425e Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Fri, 22 Dec 2017 14:22:52 +1100 Subject: [PATCH 5/5] When deleting a Reusable Block, remove associated blocks from the post Makes the REMOVE_REUSABLE_BLOCK action also remove any blocks that are using the removed Reusable Block from the editor state. --- blocks/library/block/index.js | 11 ++- editor/store/effects.js | 11 ++- editor/store/reducer.js | 34 +++++++++ editor/store/test/effects.js | 30 +++++--- editor/store/test/reducer.js | 128 ++++++++++++++++++++++++++++++++-- 5 files changed, 194 insertions(+), 20 deletions(-) diff --git a/blocks/library/block/index.js b/blocks/library/block/index.js index 4a32a82c28deae..0b5e6cb68a813a 100644 --- a/blocks/library/block/index.js +++ b/blocks/library/block/index.js @@ -78,13 +78,17 @@ class ReusableBlockEdit extends Component { } render() { - const { focus, reusableBlock, isSaving } = this.props; + const { focus, reusableBlock, isFetching, isSaving } = this.props; const { isEditing, title, attributes } = this.state; - if ( ! reusableBlock ) { + if ( ! reusableBlock && isFetching ) { return ; } + if ( ! reusableBlock ) { + return { __( 'Block has been deleted or is unavailable.' ) }; + } + const reusableBlockAttributes = { ...reusableBlock.attributes, ...attributes }; return [ @@ -103,7 +107,7 @@ class ReusableBlockEdit extends Component { key="panel" isEditing={ isEditing } title={ title !== null ? title : reusableBlock.title } - isSaving={ isSaving } + isSaving={ isSaving && ! reusableBlock.isTemporary } onEdit={ this.startEditing } onChangeTitle={ this.setTitle } onSave={ this.updateReusableBlock } @@ -117,6 +121,7 @@ class ReusableBlockEdit extends Component { const ConnectedReusableBlockEdit = connect( ( state, ownProps ) => ( { reusableBlock: state.reusableBlocks.data[ ownProps.attributes.ref ], + isFetching: state.reusableBlocks.isFetching[ ownProps.attributes.ref ], isSaving: state.reusableBlocks.isSaving[ ownProps.attributes.ref ], } ), ( dispatch, ownProps ) => ( { diff --git a/editor/store/effects.js b/editor/store/effects.js index 1aa1bb7e204880..77ac0d8623ee17 100644 --- a/editor/store/effects.js +++ b/editor/store/effects.js @@ -14,6 +14,7 @@ import { createBlock, serialize, createReusableBlock, + isReusableBlock, getDefaultBlockName, } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; @@ -49,6 +50,7 @@ import { isEditedPostNew, isEditedPostSaveable, getBlock, + getBlocks, getReusableBlock, POST_UPDATE_TRANSACTION_ID, } from './selectors'; @@ -330,6 +332,7 @@ export default { ( reusableBlockOrBlocks ) => { dispatch( { type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', + id, reusableBlocks: castArray( reusableBlockOrBlocks ).map( ( { id: itemId, title, content } ) => { const [ { name: type, attributes } ] = parse( content ); return { id: itemId, title, type, attributes }; @@ -339,6 +342,7 @@ export default { ( error ) => { dispatch( { type: 'FETCH_REUSABLE_BLOCKS_FAILURE', + id, error: error.responseJSON || { code: 'unknown_error', message: __( 'An unknown error occurred.' ), @@ -377,13 +381,18 @@ export default { }, DELETE_REUSABLE_BLOCK( action, store ) { const { id } = action; - const { dispatch } = store; + const { getState, dispatch } = store; + + const allBlocks = getBlocks( getState() ); + const associatedBlocks = allBlocks.filter( block => isReusableBlock( block ) && block.attributes.ref === id ); + const associatedBlockUids = associatedBlocks.map( block => block.uid ); const transactionId = uniqueId(); dispatch( { type: 'REMOVE_REUSABLE_BLOCK', id, + associatedBlockUids, optimist: { type: BEGIN, id: transactionId }, } ); diff --git a/editor/store/reducer.js b/editor/store/reducer.js index 88d1e9296d7d52..86ed4971bf5ebf 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -217,6 +217,9 @@ export const editor = flow( [ return block; } ); } + + case 'REMOVE_REUSABLE_BLOCK': + return omit( state, action.associatedBlockUids ); } return state; @@ -293,6 +296,9 @@ export const editor = flow( [ case 'REMOVE_BLOCKS': return without( state, ...action.uids ); + + case 'REMOVE_REUSABLE_BLOCK': + return without( state, ...action.associatedBlockUids ); } return state; @@ -769,6 +775,34 @@ export const reusableBlocks = combineReducers( { return state; }, + isFetching( state = {}, action ) { + switch ( action.type ) { + case 'FETCH_REUSABLE_BLOCKS': { + const { id } = action; + if ( ! id ) { + return state; + } + + return { + ...state, + [ id ]: true, + }; + } + + case 'FETCH_REUSABLE_BLOCKS_SUCCESS': + case 'FETCH_REUSABLE_BLOCKS_FAILURE': { + const { id } = action; + if ( ! id ) { + return state; + } + + return omit( state, id ); + } + } + + return state; + }, + isSaving( state = {}, action ) { switch ( action.type ) { case 'SAVE_REUSABLE_BLOCK': diff --git a/editor/store/test/effects.js b/editor/store/test/effects.js index dfe5dfef39441b..08b54d59911644 100644 --- a/editor/store/test/effects.js +++ b/editor/store/test/effects.js @@ -546,7 +546,7 @@ describe( 'effects', () => { } ); it( 'should fetch a single reusable block', () => { - const id = 'a9691cf9-ecaa-42bd-a9ca-49587e817647'; + const id = 123; let modelAttributes; const promise = Promise.resolve( { @@ -574,9 +574,10 @@ describe( 'effects', () => { return promise.then( () => { expect( dispatch ).toHaveBeenCalledWith( { type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', + id, reusableBlocks: [ { - id: 'a9691cf9-ecaa-42bd-a9ca-49587e817647', + id, title: 'My cool block', type: 'core/test-block', attributes: { @@ -716,24 +717,35 @@ describe( 'effects', () => { } } ); - const state = reducer( undefined, {} ); + const id = 123; + + const associatedBlock = { + uid: 'd6b55aa9-16b5-4123-9675-749d75a7f14d', + name: 'core/block', + attributes: { + ref: id, + }, + }; + + const initialState = reducer( undefined, {} ); + const state = reducer( initialState, resetBlocks( [ associatedBlock ] ) ); + const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; - handler( deleteReusableBlock( 123 ), store ); + handler( deleteReusableBlock( id ), store ); expect( dispatch ).toHaveBeenCalledWith( { type: 'REMOVE_REUSABLE_BLOCK', - id: 123, + id, + associatedBlockUids: [ associatedBlock.uid ], optimist: expect.any( Object ), } ); - expect( modelAttributes ).toEqual( { - id: 123, - } ); + expect( modelAttributes ).toEqual( { id } ); return promise.then( () => { expect( dispatch ).toHaveBeenCalledWith( { type: 'DELETE_REUSABLE_BLOCK_SUCCESS', - id: 123, + id, optimist: expect.any( Object ), } ); } ); diff --git a/editor/store/test/reducer.js b/editor/store/test/reducer.js index bbc814c9d31bef..6d8e4df9c8bb49 100644 --- a/editor/store/test/reducer.js +++ b/editor/store/test/reducer.js @@ -403,6 +403,35 @@ describe( 'state', () => { expect( state.present.blockOrder ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] ); } ); + it( 'should remove associated blocks when deleting a reusable block', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'chicken', + name: 'core/test-block', + attributes: {}, + }, { + uid: 'ribs', + name: 'core/test-block', + attributes: {}, + } ], + } ); + const state = editor( original, { + type: 'REMOVE_REUSABLE_BLOCK', + id: 123, + associatedBlockUids: [ 'chicken', 'veggies' ], + } ); + + expect( state.present.blockOrder ).toEqual( [ 'ribs' ] ); + expect( state.present.blocksByUid ).toEqual( { + ribs: { + uid: 'ribs', + name: 'core/test-block', + attributes: {}, + }, + } ); + } ); + describe( 'edits()', () => { it( 'should save newly edited properties', () => { const original = editor( undefined, { @@ -1329,13 +1358,14 @@ describe( 'state', () => { const state = reusableBlocks( undefined, {} ); expect( state ).toEqual( { data: {}, + isFetching: {}, isSaving: {}, } ); } ); it( 'should add fetched reusable blocks', () => { const reusableBlock = { - id: '358b59ee-bab3-4d6f-8445-e8c6971a5605', + id: 123, name: 'My cool block', type: 'core/paragraph', attributes: { @@ -1352,13 +1382,14 @@ describe( 'state', () => { data: { [ reusableBlock.id ]: reusableBlock, }, + isFetching: {}, isSaving: {}, } ); } ); it( 'should add a reusable block', () => { const reusableBlock = { - id: '358b59ee-bab3-4d6f-8445-e8c6971a5605', + id: 123, name: 'My cool block', type: 'core/paragraph', attributes: { @@ -1376,12 +1407,13 @@ describe( 'state', () => { data: { [ reusableBlock.id ]: reusableBlock, }, + isFetching: {}, isSaving: {}, } ); } ); it( 'should update a reusable block', () => { - const id = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; + const id = 123; const initialState = { data: { [ id ]: { @@ -1394,6 +1426,7 @@ describe( 'state', () => { }, }, }, + isFetching: {}, isSaving: {}, }; @@ -1420,12 +1453,13 @@ describe( 'state', () => { }, }, }, + isFetching: {}, isSaving: {}, } ); } ); it( 'should update the reusable block\'s id if it was temporary', () => { - const id = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; + const id = 123; const initialState = { data: { [ id ]: { @@ -1460,6 +1494,7 @@ describe( 'state', () => { }, }, }, + isFetching: {}, isSaving: {}, } ); } ); @@ -1478,6 +1513,7 @@ describe( 'state', () => { }, }, }, + isFetching: {}, isSaving: {}, }; @@ -1488,14 +1524,87 @@ describe( 'state', () => { expect( state ).toEqual( { data: {}, + isFetching: {}, + isSaving: {}, + } ); + } ); + + it( 'should indicate that a reusable block is fetching', () => { + const id = 123; + const initialState = { + data: {}, + isFetching: {}, + isSaving: {}, + }; + + const state = reusableBlocks( initialState, { + type: 'FETCH_REUSABLE_BLOCKS', + id, + } ); + + expect( state ).toEqual( { + data: {}, + isFetching: { + [ id ]: true, + }, + isSaving: {}, + } ); + } ); + + it( 'should stop indicating that a reusable block is saving when the fetch succeeded', () => { + const id = 123; + const initialState = { + data: { + [ id ]: { id }, + }, + isFetching: { + [ id ]: true, + }, + isSaving: {}, + }; + + const state = reusableBlocks( initialState, { + type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', + id, + updatedId: id, + } ); + + expect( state ).toEqual( { + data: { + [ id ]: { id }, + }, + isFetching: {}, + isSaving: {}, + } ); + } ); + + it( 'should stop indicating that a reusable block is fetching when there is an error', () => { + const id = 123; + const initialState = { + data: {}, + isFetching: { + [ id ]: true, + }, + isSaving: {}, + }; + + const state = reusableBlocks( initialState, { + type: 'FETCH_REUSABLE_BLOCKS_FAILURE', + id, + } ); + + expect( state ).toEqual( { + data: {}, + isFetching: {}, isSaving: {}, } ); } ); it( 'should indicate that a reusable block is saving', () => { - const id = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; + const id = 123; const initialState = { data: {}, + isFetching: {}, isSaving: {}, }; @@ -1506,6 +1615,7 @@ describe( 'state', () => { expect( state ).toEqual( { data: {}, + isFetching: {}, isSaving: { [ id ]: true, }, @@ -1513,11 +1623,12 @@ describe( 'state', () => { } ); it( 'should stop indicating that a reusable block is saving when the save succeeded', () => { - const id = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; + const id = 123; const initialState = { data: { [ id ]: { id }, }, + isFetching: {}, isSaving: { [ id ]: true, }, @@ -1533,14 +1644,16 @@ describe( 'state', () => { data: { [ id ]: { id }, }, + isFetching: {}, isSaving: {}, } ); } ); it( 'should stop indicating that a reusable block is saving when there is an error', () => { - const id = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; + const id = 123; const initialState = { data: {}, + isFetching: {}, isSaving: { [ id ]: true, }, @@ -1553,6 +1666,7 @@ describe( 'state', () => { expect( state ).toEqual( { data: {}, + isFetching: {}, isSaving: {}, } ); } );