From 7c3028ca1726d6f9f8794b6a0f566e70edd7f7ec Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 26 Dec 2018 11:13:36 +0100 Subject: [PATCH] Fix reusable blocks --- packages/block-editor/src/store/actions.js | 17 +++ packages/block-editor/src/store/reducer.js | 112 ++++++++++++++++-- packages/block-editor/src/store/selectors.js | 9 +- .../editor/src/components/provider/index.js | 18 ++- .../src/store/effects/reusable-blocks.js | 2 + packages/editor/src/store/selectors.js | 15 ++- 6 files changed, 151 insertions(+), 22 deletions(-) diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 3bf63d4a3dece..baa31f1df09fb 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -498,3 +498,20 @@ export function undo() { export function createUndoLevel() { return { type: 'CREATE_UNDO_LEVEL' }; } + +/** + * Returns an action object used in signalling that a temporary reusable blocks have been saved + * in order to switch its temporary id with the real id. + * + * @param {string} id Reusable block's id. + * @param {string} updatedId Updated block's id. + * + * @return {Object} Action object. + */ +export function __unstableSaveResuableBlock( id, updatedId ) { + return { + type: 'SAVE_REUSABLE_BLOCK_SUCCESS', + id, + updatedId, + }; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index aa2cc74b5867b..b8f7179f21b73 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -121,6 +121,28 @@ function getFlattenedBlockAttributes( blocks ) { return flattenBlocks( blocks, ( block ) => block.attributes ); } +/** + * Given a block order map object, returns *all* of the block client IDs that are + * a descendant of the given root client ID. + * + * Calling this with `rootClientId` set to `''` results in a list of client IDs + * that are in the post. That is, it excludes blocks like fetched reusable + * blocks which are stored into state but not visible. + * + * @param {Object} blocksOrder Object that maps block client IDs to a list of + * nested block client IDs. + * @param {?string} rootClientId The root client ID to search. Defaults to ''. + * + * @return {Array} List of descendant client IDs. + */ +function getNestedBlockClientIds( blocksOrder, rootClientId = '' ) { + return reduce( blocksOrder[ rootClientId ], ( result, clientId ) => [ + ...result, + clientId, + ...getNestedBlockClientIds( blocksOrder, clientId ), + ], [] ); +} + /** * Returns an object against which it is safe to perform mutating operations, * given the original object and its current working copy. @@ -231,6 +253,79 @@ const withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => { return reducer( state, action ); }; +/** + * Higher-order reducer which targets the combined blocks reducer and handles + * the `RESET_BLOCKS` action. When dispatched, this action will replace all + * blocks that exist in the post, leaving blocks that exist only in state (e.g. + * reusable blocks) alone. + * + * @param {Function} reducer Original reducer function. + * + * @return {Function} Enhanced reducer function. + */ +const withBlockReset = ( reducer ) => ( state, action ) => { + if ( + state && + ( action.type === 'RESET_BLOCKS' || action.type === 'INIT_BLOCKS' ) + ) { + const visibleClientIds = getNestedBlockClientIds( state.order ); + return { + ...state, + byClientId: { + ...omit( state.byClientId, visibleClientIds ), + ...getFlattenedBlocksWithoutAttributes( action.blocks ), + }, + attributes: { + ...omit( state.attributes, visibleClientIds ), + ...getFlattenedBlockAttributes( action.blocks ), + }, + order: { + ...omit( state.order, visibleClientIds ), + ...mapBlockOrder( action.blocks ), + }, + }; + } + + return reducer( state, action ); +}; + +/** + * Higher-order reducer which targets the combined blocks reducer and handles + * the `SAVE_REUSABLE_BLOCK_SUCCESS` action. This action can't be handled by + * regular reducers and needs a higher-order reducer since it needs access to + * both `byClientId` and `attributes` simultaneously. + * + * @param {Function} reducer Original reducer function. + * + * @return {Function} Enhanced reducer function. + */ +const withSaveReusableBlock = ( reducer ) => ( state, action ) => { + if ( state && action.type === 'SAVE_REUSABLE_BLOCK_SUCCESS' ) { + const { id, updatedId } = action; + + // If a temporary reusable block is saved, we swap the temporary id with the final one + if ( id === updatedId ) { + return state; + } + + state = { ...state }; + + state.attributes = mapValues( state.attributes, ( attributes, clientId ) => { + const { name } = state.byClientId[ clientId ]; + if ( name === 'core/block' && attributes.ref === id ) { + return { + ...attributes, + ref: updatedId, + }; + } + + return attributes; + } ); + } + + return reducer( state, action ); +}; + /** * Undoable reducer returning the editor post state, including blocks parsed * from current HTML markup. @@ -257,12 +352,13 @@ export const editor = flow( [ shouldOverwriteState, } ), ] )( { - blocks: combineReducers( { + blocks: flow( + combineReducers, + withBlockReset, + withSaveReusableBlock, + )( { byClientId( state = {}, action ) { switch ( action.type ) { - case 'INIT_BLOCKS': - case 'RESET_BLOCKS': - return getFlattenedBlocksWithoutAttributes( action.blocks ); case 'RECEIVE_BLOCKS': return { ...state, @@ -314,10 +410,6 @@ export const editor = flow( [ attributes( state = {}, action ) { switch ( action.type ) { - case 'INIT_BLOCKS': - case 'RESET_BLOCKS': - return getFlattenedBlockAttributes( action.blocks ); - case 'RECEIVE_BLOCKS': return { ...state, @@ -391,10 +483,6 @@ export const editor = flow( [ order( state = {}, action ) { switch ( action.type ) { - case 'INIT_BLOCKS': - case 'RESET_BLOCKS': - return mapBlockOrder( action.blocks ); - case 'RECEIVE_BLOCKS': return { ...state, diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index c05890beface1..86d3298b431cc 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1386,6 +1386,13 @@ function getPostMeta( state, key ) { return get( state, [ 'settings', '__experimentalMetaSource', 'value', key ] ); } +/** + * Returns the available reusable blocks + * + * @param {Object} state Global application state. + * + * @return {Array} Reusable blocks + */ function getReusableBlocks( state ) { - return get( state, [ 'settings', 'reusableBlocks' ], EMPTY_ARRAY ); + return get( state, [ 'settings', '__experimentalReusableBlocks' ], EMPTY_ARRAY ); } diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 74eb3fbdc3d62..6c3f502885d44 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -28,12 +28,14 @@ function computeProviderStateFromProps( props ) { settings: props.settings, meta: props.meta, onMetaChange: props.onMetaChange, + reusableBlocks: props.reusableBlocks, editorSettings: { ...props.settings, __experimentalMetaSource: { value: props.meta, onChange: props.onMetaChange, }, + __experimentalReusableBlocks: props.reusableBlocks, }, }; } @@ -93,7 +95,8 @@ class EditorProvider extends Component { if ( props.settings === state.settings && props.meta === state.meta && - props.onMetaChange === state.onMetaChange + props.onMetaChange === state.onMetaChange && + props.reusableBlocks === state.reusableBlocks ) { return null; } @@ -128,10 +131,17 @@ class EditorProvider extends Component { export default compose( [ withSelect( ( select ) => { + const { + isEditorReady, + getEditorBlocks, + getEditedPostAttribute, + __experimentalGetReusableBlocks, + } = select( 'core/editor' ); return { - isReady: select( 'core/editor' ).isEditorReady(), - blocks: select( 'core/editor' ).getEditorBlocks(), - meta: select( 'core/editor' ).getEditedPostAttribute( 'meta' ), + isReady: isEditorReady(), + blocks: getEditorBlocks(), + meta: getEditedPostAttribute( 'meta' ), + reusableBlocks: __experimentalGetReusableBlocks(), }; } ), withDispatch( ( dispatch ) => { diff --git a/packages/editor/src/store/effects/reusable-blocks.js b/packages/editor/src/store/effects/reusable-blocks.js index acfa3d0aaaefb..d9087bee51da4 100644 --- a/packages/editor/src/store/effects/reusable-blocks.js +++ b/packages/editor/src/store/effects/reusable-blocks.js @@ -140,6 +140,8 @@ export const saveReusableBlocks = async ( action, store ) => { dataDispatch( 'core/notices' ).createSuccessNotice( message, { id: REUSABLE_BLOCK_NOTICE_ID, } ); + + dataDispatch( 'core/block-editor' ).__unstableSaveResuableBlock( id, updatedReusableBlock.id ); } catch ( error ) { dispatch( { type: 'SAVE_REUSABLE_BLOCK_FAILURE', id } ); dataDispatch( 'core/notices' ).createErrorNotice( error.message, { diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 6c33c3db09777..3f7f1cecdaa86 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -814,12 +814,17 @@ export function __experimentalIsFetchingReusableBlock( state, ref ) { * * @return {Array} An array of all reusable blocks. */ -export function __experimentalGetReusableBlocks( state ) { - return map( +export const __experimentalGetReusableBlocks = createSelector( + ( state ) => { + return map( + state.reusableBlocks.data, + ( value, ref ) => __experimentalGetReusableBlock( state, ref ) + ); + }, + ( state ) => [ state.reusableBlocks.data, - ( value, ref ) => __experimentalGetReusableBlock( state, ref ) - ); -} + ] +); /** * Returns state object prior to a specified optimist transaction ID, or `null`