From f59687dd830b2d72beb7ff740baba3b7a85fb1fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 6 Oct 2020 15:02:30 +0200 Subject: [PATCH 01/43] Move reusable blocks logic to separate package --- docs/manifest.json | 6 + package-lock.json | 31 ++ package.json | 1 + packages/block-library/src/block/edit.js | 4 +- packages/editor/package.json | 1 + .../editor/src/components/provider/index.js | 8 +- .../provider/with-registry-provider.js | 8 +- .../reusable-block-convert-button.js | 4 +- .../reusable-block-delete-button.js | 4 +- packages/editor/src/index.js | 1 + packages/editor/src/store/actions.js | 111 +------ packages/editor/src/store/index.js | 2 - packages/editor/src/store/reducer.js | 94 +----- packages/editor/src/store/reducer.native.js | 2 - packages/editor/src/store/selectors.js | 70 +---- packages/editor/src/store/test/actions.js | 61 ---- packages/editor/src/store/test/reducer.js | 256 ---------------- packages/editor/src/store/test/selectors.js | 141 --------- packages/reusable-blocks/.npmrc | 1 + packages/reusable-blocks/CHANGELOG.md | 7 + packages/reusable-blocks/README.md | 48 +++ packages/reusable-blocks/package.json | 44 +++ packages/reusable-blocks/src/index.js | 13 + packages/reusable-blocks/src/index.native.js | 9 + packages/reusable-blocks/src/store/actions.js | 113 +++++++ .../reusable-blocks/src/store/constants.js | 14 + .../src/store/effects.js | 0 .../src/store/effects/reusable-blocks.js | 5 +- .../src/store/effects/test/reusable-blocks.js | 0 packages/reusable-blocks/src/store/index.js | 34 +++ .../src/store/middlewares.js | 2 +- packages/reusable-blocks/src/store/reducer.js | 103 +++++++ .../reusable-blocks/src/store/selectors.js | 72 +++++ .../reusable-blocks/src/store/test/actions.js | 88 ++++++ .../reusable-blocks/src/store/test/reducer.js | 284 ++++++++++++++++++ .../src/store/test/selectors.js | 165 ++++++++++ 36 files changed, 1055 insertions(+), 752 deletions(-) create mode 100644 packages/reusable-blocks/.npmrc create mode 100644 packages/reusable-blocks/CHANGELOG.md create mode 100644 packages/reusable-blocks/README.md create mode 100644 packages/reusable-blocks/package.json create mode 100644 packages/reusable-blocks/src/index.js create mode 100644 packages/reusable-blocks/src/index.native.js create mode 100644 packages/reusable-blocks/src/store/actions.js create mode 100644 packages/reusable-blocks/src/store/constants.js rename packages/{editor => reusable-blocks}/src/store/effects.js (100%) rename packages/{editor => reusable-blocks}/src/store/effects/reusable-blocks.js (97%) rename packages/{editor => reusable-blocks}/src/store/effects/test/reusable-blocks.js (100%) create mode 100644 packages/reusable-blocks/src/store/index.js rename packages/{editor => reusable-blocks}/src/store/middlewares.js (89%) create mode 100644 packages/reusable-blocks/src/store/reducer.js create mode 100644 packages/reusable-blocks/src/store/selectors.js create mode 100644 packages/reusable-blocks/src/store/test/actions.js create mode 100644 packages/reusable-blocks/src/store/test/reducer.js create mode 100644 packages/reusable-blocks/src/store/test/selectors.js diff --git a/docs/manifest.json b/docs/manifest.json index 4983d6c4b34804..5cc645a66cad8e 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1757,6 +1757,12 @@ "markdown_source": "../packages/redux-routine/README.md", "parent": "packages" }, + { + "title": "@wordpress/reusable-blocks", + "slug": "packages-reusable-blocks", + "markdown_source": "../packages/reusable-blocks/README.md", + "parent": "packages" + }, { "title": "@wordpress/rich-text", "slug": "packages-rich-text", diff --git a/package-lock.json b/package-lock.json index e9538d2b09edb5..9642d78789e669 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17466,6 +17466,7 @@ "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", "@wordpress/plugins": "file:packages/plugins", + "@wordpress/reusable-blocks": "file:packages/reusable-blocks", "@wordpress/server-side-render": "file:packages/server-side-render", "@wordpress/url": "file:packages/url", "classnames": "^2.2.5", @@ -17515,6 +17516,7 @@ "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", + "@wordpress/reusable-blocks": "file:packages/reusable-blocks", "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/server-side-render": "file:packages/server-side-render", "@wordpress/url": "file:packages/url", @@ -18319,6 +18321,35 @@ } } }, + "@wordpress/reusable-blocks": { + "version": "file:packages/reusable-blocks", + "requires": { + "@wordpress/api-fetch": "file:packages/api-fetch", + "@wordpress/blocks": "file:packages/blocks", + "@wordpress/core-data": "file:packages/core-data", + "@wordpress/data": "file:packages/data", + "@wordpress/i18n": "file:packages/i18n", + "lodash": "^4.17.19", + "redux-optimist": "^1.0.0", + "refx": "^3.0.0", + "rememo": "^3.0.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", + "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + } + } + }, "@wordpress/rich-text": { "version": "file:packages/rich-text", "requires": { diff --git a/package.json b/package.json index 20a52f0dbd5b66..f78e6e07a84650 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@wordpress/react-native-bridge": "file:packages/react-native-bridge", "@wordpress/react-native-editor": "file:packages/react-native-editor", "@wordpress/redux-routine": "file:packages/redux-routine", + "@wordpress/reusable-blocks": "file:packages/reusable-blocks", "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/server-side-render": "file:packages/server-side-render", "@wordpress/shortcode": "file:packages/shortcode", diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 5f64e2308370b5..8ff4babcfde63e 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -192,7 +192,7 @@ export default compose( [ __experimentalGetReusableBlock: getReusableBlock, __experimentalIsFetchingReusableBlock: isFetchingReusableBlock, __experimentalIsSavingReusableBlock: isSavingReusableBlock, - } = select( 'core/editor' ); + } = select( 'core/reusable-blocks' ); const { canUser } = select( 'core' ); const { __experimentalGetParsedReusableBlock, getSettings } = select( 'core/block-editor' @@ -220,7 +220,7 @@ export default compose( [ __experimentalFetchReusableBlocks: fetchReusableBlocks, __experimentalUpdateReusableBlock: updateReusableBlock, __experimentalSaveReusableBlock: saveReusableBlock, - } = dispatch( 'core/editor' ); + } = dispatch( 'core/reusable-blocks' ); const { ref } = ownProps.attributes; return { diff --git a/packages/editor/package.json b/packages/editor/package.json index 13c817470b822a..d80d483e08e1da 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -49,6 +49,7 @@ "@wordpress/keycodes": "file:../keycodes", "@wordpress/media-utils": "file:../media-utils", "@wordpress/notices": "file:../notices", + "@wordpress/reusable-blocks": "file:../reusable-blocks", "@wordpress/rich-text": "file:../rich-text", "@wordpress/server-side-render": "file:../server-side-render", "@wordpress/url": "file:../url", diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 257ab1adb738bc..16801f76014b65 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -316,9 +316,11 @@ export default compose( [ getEditorBlocks, getEditorSelectionStart, getEditorSelectionEnd, - __experimentalGetReusableBlocks, isPostTitleSelected, } = select( 'core/editor' ); + const { __experimentalGetReusableBlocks } = select( + 'core/reusable-blocks' + ); const { canUser } = select( 'core' ); return { @@ -342,10 +344,12 @@ export default compose( [ updatePostLock, resetEditorBlocks, updateEditorSettings, - __experimentalFetchReusableBlocks, __experimentalTearDownEditor, undo, } = dispatch( 'core/editor' ); + const { __experimentalFetchReusableBlocks } = dispatch( + 'core/reusable-blocks' + ); const { createWarningNotice } = dispatch( 'core/notices' ); return { diff --git a/packages/editor/src/components/provider/with-registry-provider.js b/packages/editor/src/components/provider/with-registry-provider.js index 62624d7c33e53c..d463c04f74d98e 100644 --- a/packages/editor/src/components/provider/with-registry-provider.js +++ b/packages/editor/src/components/provider/with-registry-provider.js @@ -14,7 +14,6 @@ import { storeConfig as blockEditorStoreConfig } from '@wordpress/block-editor'; * Internal dependencies */ import { storeConfig } from '../../store'; -import applyMiddlewares from '../../store/middlewares'; const withRegistryProvider = createHigherOrderComponent( ( WrappedComponent ) => @@ -36,12 +35,7 @@ const withRegistryProvider = createHigherOrderComponent( }, registry ); - const store = newRegistry.registerStore( - 'core/editor', - storeConfig - ); - // This should be removed after the refactoring of the effects to controls. - applyMiddlewares( store ); + newRegistry.registerStore( 'core/editor', storeConfig ); setSubRegistry( newRegistry ); }, [ registry ] ); diff --git a/packages/editor/src/components/reusable-blocks-buttons/reusable-block-convert-button.js b/packages/editor/src/components/reusable-blocks-buttons/reusable-block-convert-button.js index ea255bbc0d0646..d8ecf1fc8f9ffc 100644 --- a/packages/editor/src/components/reusable-blocks-buttons/reusable-block-convert-button.js +++ b/packages/editor/src/components/reusable-blocks-buttons/reusable-block-convert-button.js @@ -24,7 +24,7 @@ export default function ReusableBlockConvertButton( { clientIds } ) { 'core/block-editor' ); const { __experimentalGetReusableBlock: getReusableBlock } = select( - 'core/editor' + 'core/reusable-blocks' ); const blocks = getBlocksByClientId( clientIds ) ?? []; @@ -59,7 +59,7 @@ export default function ReusableBlockConvertButton( { clientIds } ) { const { __experimentalConvertBlockToReusable: convertBlockToReusable, - } = useDispatch( 'core/editor' ); + } = useDispatch( 'core/reusable-blocks' ); if ( ! canConvert ) { return null; diff --git a/packages/editor/src/components/reusable-blocks-buttons/reusable-block-delete-button.js b/packages/editor/src/components/reusable-blocks-buttons/reusable-block-delete-button.js index 7f5e29f009d3f5..2f60ed34457d44 100644 --- a/packages/editor/src/components/reusable-blocks-buttons/reusable-block-delete-button.js +++ b/packages/editor/src/components/reusable-blocks-buttons/reusable-block-delete-button.js @@ -41,7 +41,7 @@ export default compose( [ const { getBlock } = select( 'core/block-editor' ); const { canUser } = select( 'core' ); const { __experimentalGetReusableBlock: getReusableBlock } = select( - 'core/editor' + 'core/reusable-blocks' ); const block = getBlock( clientId ); @@ -61,7 +61,7 @@ export default compose( [ withDispatch( ( dispatch, { clientId }, { select } ) => { const { __experimentalDeleteReusableBlock: deleteReusableBlock, - } = dispatch( 'core/editor' ); + } = dispatch( 'core/reusable-blocks' ); const { getBlock } = select( 'core/block-editor' ); return { diff --git a/packages/editor/src/index.js b/packages/editor/src/index.js index 988fae724440c5..5e119148adb1de 100644 --- a/packages/editor/src/index.js +++ b/packages/editor/src/index.js @@ -6,6 +6,7 @@ import '@wordpress/blocks'; import '@wordpress/core-data'; import '@wordpress/keyboard-shortcuts'; import '@wordpress/notices'; +import '@wordpress/reusable-blocks'; import '@wordpress/rich-text'; import '@wordpress/viewport'; diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index d58fbef47254a2..0a83eaa9233f38 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { has, castArray } from 'lodash'; +import { has } from 'lodash'; /** * WordPress dependencies @@ -417,115 +417,6 @@ export function updatePostLock( lock ) { }; } -/** - * Returns an action object 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 { - type: 'FETCH_REUSABLE_BLOCKS', - id, - }; -} - -/** - * Returns an action object 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 { - type: 'RECEIVE_REUSABLE_BLOCKS', - results, - }; -} - -/** - * Returns an action object 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 { - type: 'SAVE_REUSABLE_BLOCK', - id, - }; -} - -/** - * Returns an action object 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', - id, - }; -} - -/** - * Returns an action object used in signalling that a reusable block is - * to be updated. - * - * @param {number} id The ID of the reusable block to update. - * @param {Object} changes The changes to apply. - * - * @return {Object} Action object. - */ -export function __experimentalUpdateReusableBlock( id, changes ) { - return { - type: 'UPDATE_REUSABLE_BLOCK', - id, - changes, - }; -} - -/** - * Returns an action object 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, - }; -} - -/** - * Returns an action object 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 ), - }; -} - /** * Returns an action object used in signalling that the user has enabled the * publish sidebar. diff --git a/packages/editor/src/store/index.js b/packages/editor/src/store/index.js index 8f377151e08d43..af10a60cf74369 100644 --- a/packages/editor/src/store/index.js +++ b/packages/editor/src/store/index.js @@ -8,7 +8,6 @@ import { controls as dataControls } from '@wordpress/data-controls'; * Internal dependencies */ import reducer from './reducer'; -import applyMiddlewares from './middlewares'; import * as selectors from './selectors'; import * as actions from './actions'; import controls from './controls'; @@ -35,6 +34,5 @@ const store = registerStore( STORE_KEY, { ...storeConfig, persist: [ 'preferences' ], } ); -applyMiddlewares( store ); export default store; diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 9fd9c910360d3d..071b2492945aa3 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -2,7 +2,7 @@ * External dependencies */ import optimist from 'redux-optimist'; -import { omit, keys, isEqual, keyBy } from 'lodash'; +import { omit, keys, isEqual } from 'lodash'; /** * WordPress dependencies @@ -242,97 +242,6 @@ export function postAutosavingLock( state = {}, action ) { return state; } -export const reusableBlocks = combineReducers( { - data( state = {}, action ) { - switch ( action.type ) { - case 'RECEIVE_REUSABLE_BLOCKS': { - return { - ...state, - ...keyBy( action.results, 'id' ), - }; - } - - case 'UPDATE_REUSABLE_BLOCK': { - const { id, changes } = action; - return { - ...state, - [ id ]: { - ...state[ id ], - ...changes, - }, - }; - } - - case '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; - } - - const value = state[ id ]; - return { - ...omit( state, id ), - [ updatedId ]: { - ...value, - id: updatedId, - }, - }; - } - - case 'REMOVE_REUSABLE_BLOCK': { - const { id } = action; - return omit( state, id ); - } - } - - 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; - return omit( state, id ); - } - } - - return state; - }, - - isSaving( state = {}, action ) { - switch ( action.type ) { - case 'SAVE_REUSABLE_BLOCK': - return { - ...state, - [ action.id ]: true, - }; - - case 'SAVE_REUSABLE_BLOCK_SUCCESS': - case 'SAVE_REUSABLE_BLOCK_FAILURE': { - const { id } = action; - return omit( state, id ); - } - } - - return state; - }, -} ); - /** * Reducer returning whether the editor is ready to be rendered. * The editor is considered ready to be rendered once @@ -382,7 +291,6 @@ export default optimist( preferences, saving, postLock, - reusableBlocks, template, postSavingLock, isReady, diff --git a/packages/editor/src/store/reducer.native.js b/packages/editor/src/store/reducer.native.js index 8aa3ddda38dadb..3036cb5704a480 100644 --- a/packages/editor/src/store/reducer.native.js +++ b/packages/editor/src/store/reducer.native.js @@ -18,7 +18,6 @@ import { saving, postLock, postSavingLock, - reusableBlocks, template, isReady, editorSettings, @@ -95,7 +94,6 @@ export default optimist( saving, postLock, postSavingLock, - reusableBlocks, template, isReady, editorSettings, diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 5603c4940436ef..764c3c63ba0e06 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1,8 +1,7 @@ /** * External dependencies */ -import { find, get, has, map, pick, mapValues, includes, some } from 'lodash'; -import createSelector from 'rememo'; +import { find, get, has, pick, mapValues, includes, some } from 'lodash'; /** * WordPress dependencies @@ -998,73 +997,6 @@ export const getEditedPostContent = createRegistrySelector( } ); -/** - * Returns the reusable block with the given ID. - * - * @param {Object} state Global application state. - * @param {number|string} ref The reusable block's ID. - * - * @return {Object} The reusable block, or null if none exists. - */ -export const __experimentalGetReusableBlock = createSelector( - ( state, ref ) => { - const block = state.reusableBlocks.data[ ref ]; - if ( ! block ) { - return null; - } - - const isTemporary = isNaN( parseInt( ref ) ); - - return { - ...block, - id: isTemporary ? ref : +ref, - isTemporary, - }; - }, - ( state, ref ) => [ state.reusableBlocks.data[ ref ] ] -); - -/** - * Returns whether or not the reusable block with the given ID is being saved. - * - * @param {Object} state Global application state. - * @param {string} ref The reusable block's ID. - * - * @return {boolean} Whether or not the reusable block is being saved. - */ -export function __experimentalIsSavingReusableBlock( state, ref ) { - return state.reusableBlocks.isSaving[ ref ] || false; -} - -/** - * Returns true if the reusable block with the given ID is being fetched, or - * false otherwise. - * - * @param {Object} state Global application state. - * @param {string} ref The reusable block's ID. - * - * @return {boolean} Whether the reusable block is being fetched. - */ -export function __experimentalIsFetchingReusableBlock( state, ref ) { - return !! state.reusableBlocks.isFetching[ ref ]; -} - -/** - * Returns an array of all reusable blocks. - * - * @param {Object} state Global application state. - * - * @return {Array} An array of all reusable blocks. - */ -export const __experimentalGetReusableBlocks = createSelector( - ( state ) => { - return map( state.reusableBlocks.data, ( value, ref ) => - __experimentalGetReusableBlock( state, ref ) - ); - }, - ( state ) => [ state.reusableBlocks.data ] -); - /** * Returns state object prior to a specified optimist transaction ID, or `null` * if the transaction corresponding to the given ID cannot be found. diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index c7359b90134afd..686fd067a47e95 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -561,67 +561,6 @@ describe( 'Editor actions', () => { } ); } ); - describe( 'fetchReusableBlocks', () => { - it( 'should return the FETCH_REUSABLE_BLOCKS action', () => { - expect( actions.__experimentalFetchReusableBlocks() ).toEqual( { - type: 'FETCH_REUSABLE_BLOCKS', - } ); - } ); - - it( 'should take an optional id argument', () => { - expect( actions.__experimentalFetchReusableBlocks( 123 ) ).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', - id: 123, - } ); - } ); - } ); - - describe( 'deleteReusableBlock', () => { - it( 'should return the DELETE_REUSABLE_BLOCK action', () => { - expect( actions.__experimentalDeleteReusableBlock( 123 ) ).toEqual( - { - type: 'DELETE_REUSABLE_BLOCK', - 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, - } ); - } ); - } ); - - 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( 'lockPostSaving', () => { it( 'should return the LOCK_POST_SAVING action', () => { const result = actions.lockPostSaving( 'test' ); diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 219fe26c7406cb..d06b90dd672fea 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -13,7 +13,6 @@ import { getPostRawValue, preferences, saving, - reusableBlocks, postSavingLock, postAutosavingLock, } from '../reducer'; @@ -207,261 +206,6 @@ describe( 'state', () => { } ); } ); - describe( 'reusableBlocks()', () => { - it( 'should start out empty', () => { - const state = reusableBlocks( undefined, {} ); - expect( state ).toEqual( { - data: {}, - isFetching: {}, - isSaving: {}, - } ); - } ); - - it( 'should add received reusable blocks', () => { - const state = reusableBlocks( - {}, - { - type: 'RECEIVE_REUSABLE_BLOCKS', - results: [ - { - id: 123, - title: 'My cool block', - }, - ], - } - ); - - expect( state ).toEqual( { - data: { - 123: { id: 123, title: 'My cool block' }, - }, - isFetching: {}, - isSaving: {}, - } ); - } ); - - it( 'should update a reusable block', () => { - const initialState = { - data: { - 123: { clientId: '', title: '' }, - }, - isFetching: {}, - isSaving: {}, - }; - - const state = reusableBlocks( initialState, { - type: 'UPDATE_REUSABLE_BLOCK', - id: 123, - changes: { - title: 'My block', - }, - } ); - - expect( state ).toEqual( { - data: { - 123: { clientId: '', title: 'My block' }, - }, - isFetching: {}, - isSaving: {}, - } ); - } ); - - it( "should update the reusable block's id if it was temporary", () => { - const initialState = { - data: { - reusable1: { id: 'reusable1', title: '' }, - }, - isSaving: {}, - }; - - const state = reusableBlocks( initialState, { - type: 'SAVE_REUSABLE_BLOCK_SUCCESS', - id: 'reusable1', - updatedId: 123, - } ); - - expect( state ).toEqual( { - data: { - 123: { id: 123, title: '' }, - }, - isFetching: {}, - isSaving: {}, - } ); - } ); - - it( 'should remove a reusable block', () => { - const id = 123; - const initialState = { - data: { - [ id ]: { - id, - name: 'My cool block', - type: 'core/paragraph', - attributes: { - content: 'Hello!', - dropCap: true, - }, - }, - }, - isFetching: {}, - isSaving: {}, - }; - - const state = reusableBlocks( deepFreeze( initialState ), { - type: 'REMOVE_REUSABLE_BLOCK', - id, - } ); - - 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 = 123; - const initialState = { - data: {}, - isFetching: {}, - isSaving: {}, - }; - - const state = reusableBlocks( initialState, { - type: 'SAVE_REUSABLE_BLOCK', - id, - } ); - - expect( state ).toEqual( { - data: {}, - isFetching: {}, - isSaving: { - [ id ]: true, - }, - } ); - } ); - - it( 'should stop indicating that a reusable block is saving when the save succeeded', () => { - const id = 123; - const initialState = { - data: { - [ id ]: { id }, - }, - isFetching: {}, - isSaving: { - [ id ]: true, - }, - }; - - const state = reusableBlocks( initialState, { - type: 'SAVE_REUSABLE_BLOCK_SUCCESS', - id, - updatedId: id, - } ); - - expect( state ).toEqual( { - data: { - [ id ]: { id }, - }, - isFetching: {}, - isSaving: {}, - } ); - } ); - - it( 'should stop indicating that a reusable block is saving when there is an error', () => { - const id = 123; - const initialState = { - data: {}, - isFetching: {}, - isSaving: { - [ id ]: true, - }, - }; - - const state = reusableBlocks( initialState, { - type: 'SAVE_REUSABLE_BLOCK_FAILURE', - id, - } ); - - expect( state ).toEqual( { - data: {}, - isFetching: {}, - isSaving: {}, - } ); - } ); - } ); - describe( 'postSavingLock', () => { it( 'returns empty object by default', () => { const state = postSavingLock( undefined, {} ); diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 8fe17171d73bd7..4d6bf5f9a98dc1 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -161,10 +161,6 @@ const { didPostSaveRequestFail, getSuggestedPostFormat, getEditedPostContent, - __experimentalGetReusableBlock: getReusableBlock, - __experimentalIsSavingReusableBlock: isSavingReusableBlock, - __experimentalIsFetchingReusableBlock: isFetchingReusableBlock, - __experimentalGetReusableBlocks: getReusableBlocks, getStateBeforeOptimisticTransaction, isPublishingPost, isPublishSidebarEnabled, @@ -2484,87 +2480,6 @@ describe( 'selectors', () => { } ); } ); - describe( 'getReusableBlock', () => { - it( 'should return a reusable block', () => { - const state = { - reusableBlocks: { - data: { - 8109: { - clientId: 'foo', - title: 'My cool block', - }, - }, - }, - }; - - const actualReusableBlock = getReusableBlock( state, 8109 ); - expect( actualReusableBlock ).toEqual( { - id: 8109, - isTemporary: false, - clientId: 'foo', - title: 'My cool block', - } ); - } ); - - it( 'should return a temporary reusable block', () => { - const state = { - reusableBlocks: { - data: { - reusable1: { - clientId: 'foo', - title: 'My cool block', - }, - }, - }, - }; - - const actualReusableBlock = getReusableBlock( state, 'reusable1' ); - expect( actualReusableBlock ).toEqual( { - id: 'reusable1', - isTemporary: true, - clientId: 'foo', - title: 'My cool block', - } ); - } ); - - it( 'should return null when no reusable block exists', () => { - const state = { - reusableBlocks: { - data: {}, - }, - }; - - const reusableBlock = getReusableBlock( state, 123 ); - expect( reusableBlock ).toBeNull(); - } ); - } ); - - describe( 'isSavingReusableBlock', () => { - it( 'should return false when the block is not being saved', () => { - const state = { - reusableBlocks: { - isSaving: {}, - }, - }; - - const isSaving = isSavingReusableBlock( state, 5187 ); - expect( isSaving ).toBe( false ); - } ); - - it( 'should return true when the block is being saved', () => { - const state = { - reusableBlocks: { - isSaving: { - 5187: true, - }, - }, - }; - - const isSaving = isSavingReusableBlock( state, 5187 ); - expect( isSaving ).toBe( true ); - } ); - } ); - describe( 'isPublishSidebarEnabled', () => { it( 'should return the value on state if it is thruthy', () => { const state = { @@ -2598,62 +2513,6 @@ describe( 'selectors', () => { } ); } ); - describe( 'isFetchingReusableBlock', () => { - it( 'should return false when the block is not being fetched', () => { - const state = { - reusableBlocks: { - isFetching: {}, - }, - }; - - const isFetching = isFetchingReusableBlock( state, 5187 ); - expect( isFetching ).toBe( false ); - } ); - - it( 'should return true when the block is being fetched', () => { - const state = { - reusableBlocks: { - isFetching: { - 5187: true, - }, - }, - }; - - const isFetching = isFetchingReusableBlock( state, 5187 ); - expect( isFetching ).toBe( true ); - } ); - } ); - - describe( 'getReusableBlocks', () => { - it( 'should return an array of reusable blocks', () => { - const state = { - reusableBlocks: { - data: { - 123: { clientId: 'carrot' }, - reusable1: { clientId: 'broccoli' }, - }, - }, - }; - - const reusableBlocks = getReusableBlocks( state ); - expect( reusableBlocks ).toEqual( [ - { id: 123, isTemporary: false, clientId: 'carrot' }, - { id: 'reusable1', isTemporary: true, clientId: 'broccoli' }, - ] ); - } ); - - it( 'should return an empty array when no reusable blocks exist', () => { - const state = { - reusableBlocks: { - data: {}, - }, - }; - - const reusableBlocks = getReusableBlocks( state ); - expect( reusableBlocks ).toEqual( [] ); - } ); - } ); - describe( 'getStateBeforeOptimisticTransaction', () => { it( 'should return null if no transaction can be found', () => { const beforeState = getStateBeforeOptimisticTransaction( diff --git a/packages/reusable-blocks/.npmrc b/packages/reusable-blocks/.npmrc new file mode 100644 index 00000000000000..43c97e719a5a82 --- /dev/null +++ b/packages/reusable-blocks/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/reusable-blocks/CHANGELOG.md b/packages/reusable-blocks/CHANGELOG.md new file mode 100644 index 00000000000000..a5fb105e4e67fa --- /dev/null +++ b/packages/reusable-blocks/CHANGELOG.md @@ -0,0 +1,7 @@ + + +## Unreleased + +### Internal + +- Reusable block utilities moved from `@wordpress/editor` to `@wordpress/reusable-blocks`. diff --git a/packages/reusable-blocks/README.md b/packages/reusable-blocks/README.md new file mode 100644 index 00000000000000..4dc6dd20d9d837 --- /dev/null +++ b/packages/reusable-blocks/README.md @@ -0,0 +1,48 @@ +# Reusable blocks + +Building blocks for WordPress editors. + +## Installation + +Install the module + +```bash +npm install @wordpress/reusable-blocks --save +``` + +_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. Learn more about it in [Babel docs](https://babeljs.io/docs/en/next/caveats)._ + +## How it works + +This experimental module provides support for reusable blocks. + +The most basic usage of this package would involve only telling the ` ( { + __experimentalReusableBlocks: select( + 'core/reusable-blocks' + ).__experimentalGetReusableBlocks(), + } ) +); + +const { __experimentalFetchReusableBlocks } = useDispatch( + 'core/reusable-blocks' +); +return ( + +); +``` + +

Code is Poetry.

diff --git a/packages/reusable-blocks/package.json b/packages/reusable-blocks/package.json new file mode 100644 index 00000000000000..fd8753f2dfd467 --- /dev/null +++ b/packages/reusable-blocks/package.json @@ -0,0 +1,44 @@ +{ + "name": "@wordpress/reusable-blocks", + "version": "9.22.0", + "description": "Reusable blocks utilities.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "gutenberg", + "reusable blocks" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/reusable-blocks/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/reusable-blocks" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "sideEffects": [ + "build-style/**", + "!((src|build|build-module)/(components|utils)/**)" + ], + "dependencies": { + "@wordpress/api-fetch": "file:../api-fetch", + "@wordpress/block-editor": "file:../block-editor", + "@wordpress/blocks": "file:../blocks", + "@wordpress/core-data": "file:../core-data", + "@wordpress/data": "file:../data", + "@wordpress/i18n": "file:../i18n", + "@wordpress/notices": "file:../notices", + "lodash": "^4.17.19", + "redux-optimist": "^1.0.0", + "refx": "^3.0.0", + "rememo": "^3.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/reusable-blocks/src/index.js b/packages/reusable-blocks/src/index.js new file mode 100644 index 00000000000000..c0f41f77ea5775 --- /dev/null +++ b/packages/reusable-blocks/src/index.js @@ -0,0 +1,13 @@ +/** + * WordPress dependencies + */ +import '@wordpress/block-editor'; +import '@wordpress/core-data'; +import '@wordpress/notices'; + +/** + * Internal dependencies + */ +import './store'; + +export { storeConfig } from './store'; diff --git a/packages/reusable-blocks/src/index.native.js b/packages/reusable-blocks/src/index.native.js new file mode 100644 index 00000000000000..fea02253022f79 --- /dev/null +++ b/packages/reusable-blocks/src/index.native.js @@ -0,0 +1,9 @@ +/** + * WordPress dependencies + */ +import '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import './store'; diff --git a/packages/reusable-blocks/src/store/actions.js b/packages/reusable-blocks/src/store/actions.js new file mode 100644 index 00000000000000..711bc1dd68045e --- /dev/null +++ b/packages/reusable-blocks/src/store/actions.js @@ -0,0 +1,113 @@ +/** + * External dependencies + */ +import { castArray } from 'lodash'; + +/** + * Returns an action object 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 { + type: 'FETCH_REUSABLE_BLOCKS', + id, + }; +} + +/** + * Returns an action object 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 { + type: 'RECEIVE_REUSABLE_BLOCKS', + results, + }; +} + +/** + * Returns an action object 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 { + type: 'SAVE_REUSABLE_BLOCK', + id, + }; +} + +/** + * Returns an action object 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', + id, + }; +} + +/** + * Returns an action object used in signalling that a reusable block is + * to be updated. + * + * @param {number} id The ID of the reusable block to update. + * @param {Object} changes The changes to apply. + * + * @return {Object} Action object. + */ +export function __experimentalUpdateReusableBlock( id, changes ) { + return { + type: 'UPDATE_REUSABLE_BLOCK', + id, + changes, + }; +} + +/** + * Returns an action object 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, + }; +} + +/** + * Returns an action object 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 ), + }; +} diff --git a/packages/reusable-blocks/src/store/constants.js b/packages/reusable-blocks/src/store/constants.js new file mode 100644 index 00000000000000..6a7e41d59cc03b --- /dev/null +++ b/packages/reusable-blocks/src/store/constants.js @@ -0,0 +1,14 @@ +/** + * Set of post properties for which edits should assume a merging behavior, + * assuming an object value. + * + * @type {Set} + */ +export const EDIT_MERGE_PROPERTIES = new Set( [ 'meta' ] ); + +/** + * Constant for the store module (or reducer) key. + * + * @type {string} + */ +export const STORE_KEY = 'core/reusable-blocks'; diff --git a/packages/editor/src/store/effects.js b/packages/reusable-blocks/src/store/effects.js similarity index 100% rename from packages/editor/src/store/effects.js rename to packages/reusable-blocks/src/store/effects.js diff --git a/packages/editor/src/store/effects/reusable-blocks.js b/packages/reusable-blocks/src/store/effects/reusable-blocks.js similarity index 97% rename from packages/editor/src/store/effects/reusable-blocks.js rename to packages/reusable-blocks/src/store/effects/reusable-blocks.js index 4cef632d7c0ba3..41c5bc202b1d27 100644 --- a/packages/editor/src/store/effects/reusable-blocks.js +++ b/packages/reusable-blocks/src/store/effects/reusable-blocks.js @@ -15,9 +15,10 @@ import { isReusableBlock, } 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`). +// refactoring reusable-blocks actions to yielded controls, or replacing direct dispatch +// on the reusable-blocks store with action creators (e.g. `REMOVE_REUSABLE_BLOCK`). import { dispatch as dataDispatch, select } from '@wordpress/data'; /** diff --git a/packages/editor/src/store/effects/test/reusable-blocks.js b/packages/reusable-blocks/src/store/effects/test/reusable-blocks.js similarity index 100% rename from packages/editor/src/store/effects/test/reusable-blocks.js rename to packages/reusable-blocks/src/store/effects/test/reusable-blocks.js diff --git a/packages/reusable-blocks/src/store/index.js b/packages/reusable-blocks/src/store/index.js new file mode 100644 index 00000000000000..4a0bd72547db16 --- /dev/null +++ b/packages/reusable-blocks/src/store/index.js @@ -0,0 +1,34 @@ +/** + * WordPress dependencies + */ +import { registerStore } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import { STORE_KEY } from './constants'; +import applyMiddlewares from './middlewares'; + +/** + * Data store configuration. + * + * @see https://github.com/WordPress/gutenberg/blob/master/packages/data/README.md#registerStore + * + * @type {Object} + */ +export const storeConfig = { + reducer, + selectors, + actions, +}; + +const store = registerStore( STORE_KEY, { + ...storeConfig, + persist: [ 'preferences' ], +} ); +applyMiddlewares( store ); + +export default store; diff --git a/packages/editor/src/store/middlewares.js b/packages/reusable-blocks/src/store/middlewares.js similarity index 89% rename from packages/editor/src/store/middlewares.js rename to packages/reusable-blocks/src/store/middlewares.js index f7d73bb0a828c9..1740d622896b99 100644 --- a/packages/editor/src/store/middlewares.js +++ b/packages/reusable-blocks/src/store/middlewares.js @@ -9,7 +9,7 @@ import refx from 'refx'; import effects from './effects'; /** - * Applies the custom middlewares used specifically in the editor module. + * Applies the custom middlewares used specifically in the reusable-blocks module. * * @param {Object} store Store Object. * diff --git a/packages/reusable-blocks/src/store/reducer.js b/packages/reusable-blocks/src/store/reducer.js new file mode 100644 index 00000000000000..d4ac07425550ab --- /dev/null +++ b/packages/reusable-blocks/src/store/reducer.js @@ -0,0 +1,103 @@ +/** + * External dependencies + */ +import optimist from 'redux-optimist'; +import { omit, keyBy } from 'lodash'; + +/** + * WordPress dependencies + */ +import { combineReducers } from '@wordpress/data'; + +export const reusableBlocks = combineReducers( { + data( state = {}, action ) { + switch ( action.type ) { + case 'RECEIVE_REUSABLE_BLOCKS': { + return { + ...state, + ...keyBy( action.results, 'id' ), + }; + } + + case 'UPDATE_REUSABLE_BLOCK': { + const { id, changes } = action; + return { + ...state, + [ id ]: { + ...state[ id ], + ...changes, + }, + }; + } + + case '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; + } + + const value = state[ id ]; + return { + ...omit( state, id ), + [ updatedId ]: { + ...value, + id: updatedId, + }, + }; + } + + case 'REMOVE_REUSABLE_BLOCK': { + const { id } = action; + return omit( state, id ); + } + } + + 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; + return omit( state, id ); + } + } + + return state; + }, + + isSaving( state = {}, action ) { + switch ( action.type ) { + case 'SAVE_REUSABLE_BLOCK': + return { + ...state, + [ action.id ]: true, + }; + + case 'SAVE_REUSABLE_BLOCK_SUCCESS': + case 'SAVE_REUSABLE_BLOCK_FAILURE': { + const { id } = action; + return omit( state, id ); + } + } + + return state; + }, +} ); + +export default optimist( reusableBlocks ); diff --git a/packages/reusable-blocks/src/store/selectors.js b/packages/reusable-blocks/src/store/selectors.js new file mode 100644 index 00000000000000..4cad0bf696900a --- /dev/null +++ b/packages/reusable-blocks/src/store/selectors.js @@ -0,0 +1,72 @@ +/** + * External dependencies + */ +import { map } from 'lodash'; +import createSelector from 'rememo'; + +/** + * Returns the reusable block with the given ID. + * + * @param {Object} state Global application state. + * @param {number|string} ref The reusable block's ID. + * + * @return {Object} The reusable block, or null if none exists. + */ +export const __experimentalGetReusableBlock = createSelector( + ( state, ref ) => { + const block = state.data[ ref ]; + if ( ! block ) { + return null; + } + + const isTemporary = isNaN( parseInt( ref ) ); + + return { + ...block, + id: isTemporary ? ref : +ref, + isTemporary, + }; + }, + ( state, ref ) => [ state.data[ ref ] ] +); + +/** + * Returns whether or not the reusable block with the given ID is being saved. + * + * @param {Object} state Global application state. + * @param {string} ref The reusable block's ID. + * + * @return {boolean} Whether or not the reusable block is being saved. + */ +export function __experimentalIsSavingReusableBlock( state, ref ) { + return state.isSaving[ ref ] || false; +} + +/** + * Returns true if the reusable block with the given ID is being fetched, or + * false otherwise. + * + * @param {Object} state Global application state. + * @param {string} ref The reusable block's ID. + * + * @return {boolean} Whether the reusable block is being fetched. + */ +export function __experimentalIsFetchingReusableBlock( state, ref ) { + return !! state.isFetching[ ref ]; +} + +/** + * Returns an array of all reusable blocks. + * + * @param {Object} state Global application state. + * + * @return {Array} An array of all reusable blocks. + */ +export const __experimentalGetReusableBlocks = createSelector( + ( state ) => { + return map( state.data, ( value, ref ) => + __experimentalGetReusableBlock( state, ref ) + ); + }, + ( state ) => [ state.data ] +); diff --git a/packages/reusable-blocks/src/store/test/actions.js b/packages/reusable-blocks/src/store/test/actions.js new file mode 100644 index 00000000000000..725e2747818dd9 --- /dev/null +++ b/packages/reusable-blocks/src/store/test/actions.js @@ -0,0 +1,88 @@ +/** + * WordPress dependencies + */ +import { select, dispatch } from '@wordpress/data-controls'; + +/** + * Internal dependencies + */ +import * as actions from '../actions'; + +jest.mock( '@wordpress/data-controls' ); + +select.mockImplementation( ( ...args ) => { + const { select: actualSelect } = jest.requireActual( + '@wordpress/data-controls' + ); + return actualSelect( ...args ); +} ); + +dispatch.mockImplementation( ( ...args ) => { + const { dispatch: actualDispatch } = jest.requireActual( + '@wordpress/data-controls' + ); + return actualDispatch( ...args ); +} ); + +describe( 'Actions', () => { + describe( 'fetchReusableBlocks', () => { + it( 'should return the FETCH_REUSABLE_BLOCKS action', () => { + expect( actions.__experimentalFetchReusableBlocks() ).toEqual( { + type: 'FETCH_REUSABLE_BLOCKS', + } ); + } ); + + it( 'should take an optional id argument', () => { + expect( actions.__experimentalFetchReusableBlocks( 123 ) ).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', + id: 123, + } ); + } ); + } ); + + describe( 'deleteReusableBlock', () => { + it( 'should return the DELETE_REUSABLE_BLOCK action', () => { + expect( actions.__experimentalDeleteReusableBlock( 123 ) ).toEqual( + { + type: 'DELETE_REUSABLE_BLOCK', + 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, + } ); + } ); + } ); + + 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 ], + } ); + } ); + } ); +} ); diff --git a/packages/reusable-blocks/src/store/test/reducer.js b/packages/reusable-blocks/src/store/test/reducer.js new file mode 100644 index 00000000000000..18ef6f5b2b2a37 --- /dev/null +++ b/packages/reusable-blocks/src/store/test/reducer.js @@ -0,0 +1,284 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import reusableBlocks from '../reducer'; + +describe( 'state', () => { + it( 'should start out empty', () => { + const state = reusableBlocks( undefined, {} ); + expect( state ).toEqual( { + data: {}, + isFetching: {}, + isSaving: {}, + optimist: [], + } ); + } ); + + it( 'should add received reusable blocks', () => { + const state = reusableBlocks( + {}, + { + type: 'RECEIVE_REUSABLE_BLOCKS', + results: [ + { + id: 123, + title: 'My cool block', + }, + ], + } + ); + + expect( state ).toEqual( { + data: { + 123: { id: 123, title: 'My cool block' }, + }, + isFetching: {}, + isSaving: {}, + optimist: [], + } ); + } ); + + it( 'should update a reusable block', () => { + const initialState = { + data: { + 123: { clientId: '', title: '' }, + }, + isFetching: {}, + isSaving: {}, + optimist: [], + }; + + const state = reusableBlocks( initialState, { + type: 'UPDATE_REUSABLE_BLOCK', + id: 123, + changes: { + title: 'My block', + }, + } ); + + expect( state ).toEqual( { + data: { + 123: { clientId: '', title: 'My block' }, + }, + isFetching: {}, + isSaving: {}, + optimist: [], + } ); + } ); + + it( "should update the reusable block's id if it was temporary", () => { + const initialState = { + data: { + reusable1: { id: 'reusable1', title: '' }, + }, + isSaving: {}, + optimist: [], + }; + + const state = reusableBlocks( initialState, { + type: 'SAVE_REUSABLE_BLOCK_SUCCESS', + id: 'reusable1', + updatedId: 123, + } ); + + expect( state ).toEqual( { + data: { + 123: { id: 123, title: '' }, + }, + isFetching: {}, + isSaving: {}, + optimist: [], + } ); + } ); + + it( 'should remove a reusable block', () => { + const id = 123; + const initialState = { + data: { + [ id ]: { + id, + name: 'My cool block', + type: 'core/paragraph', + attributes: { + content: 'Hello!', + dropCap: true, + }, + }, + }, + isFetching: {}, + isSaving: {}, + optimist: [], + }; + + const state = reusableBlocks( deepFreeze( initialState ), { + type: 'REMOVE_REUSABLE_BLOCK', + id, + } ); + + expect( state ).toEqual( { + data: {}, + isFetching: {}, + isSaving: {}, + optimist: [], + } ); + } ); + + it( 'should indicate that a reusable block is fetching', () => { + const id = 123; + const initialState = { + data: {}, + isFetching: {}, + isSaving: {}, + optimist: [], + }; + + const state = reusableBlocks( initialState, { + type: 'FETCH_REUSABLE_BLOCKS', + id, + } ); + + expect( state ).toEqual( { + data: {}, + isFetching: { + [ id ]: true, + }, + isSaving: {}, + optimist: [], + } ); + } ); + + 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: {}, + optimist: [], + }; + + const state = reusableBlocks( initialState, { + type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', + id, + updatedId: id, + } ); + + expect( state ).toEqual( { + data: { + [ id ]: { id }, + }, + isFetching: {}, + isSaving: {}, + optimist: [], + } ); + } ); + + 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: {}, + optimist: [], + }; + + const state = reusableBlocks( initialState, { + type: 'FETCH_REUSABLE_BLOCKS_FAILURE', + id, + } ); + + expect( state ).toEqual( { + data: {}, + isFetching: {}, + isSaving: {}, + optimist: [], + } ); + } ); + + it( 'should indicate that a reusable block is saving', () => { + const id = 123; + const initialState = { + data: {}, + isFetching: {}, + isSaving: {}, + optimist: [], + }; + + const state = reusableBlocks( initialState, { + type: 'SAVE_REUSABLE_BLOCK', + id, + } ); + + expect( state ).toEqual( { + data: {}, + isFetching: {}, + isSaving: { + [ id ]: true, + }, + optimist: [], + } ); + } ); + + it( 'should stop indicating that a reusable block is saving when the save succeeded', () => { + const id = 123; + const initialState = { + data: { + [ id ]: { id }, + }, + isFetching: {}, + isSaving: { + [ id ]: true, + }, + optimist: [], + }; + + const state = reusableBlocks( initialState, { + type: 'SAVE_REUSABLE_BLOCK_SUCCESS', + id, + updatedId: id, + } ); + + expect( state ).toEqual( { + data: { + [ id ]: { id }, + }, + isFetching: {}, + isSaving: {}, + optimist: [], + } ); + } ); + + it( 'should stop indicating that a reusable block is saving when there is an error', () => { + const id = 123; + const initialState = { + data: {}, + isFetching: {}, + isSaving: { + [ id ]: true, + }, + optimist: [], + }; + + const state = reusableBlocks( initialState, { + type: 'SAVE_REUSABLE_BLOCK_FAILURE', + id, + } ); + + expect( state ).toEqual( { + data: {}, + isFetching: {}, + isSaving: {}, + optimist: [], + } ); + } ); +} ); diff --git a/packages/reusable-blocks/src/store/test/selectors.js b/packages/reusable-blocks/src/store/test/selectors.js new file mode 100644 index 00000000000000..b28b6b37e3a18e --- /dev/null +++ b/packages/reusable-blocks/src/store/test/selectors.js @@ -0,0 +1,165 @@ +/** + * External dependencies + */ +import { filter } from 'lodash'; + +/** + * WordPress dependencies + */ +import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import * as selectors from '../selectors'; + +const { + __experimentalGetReusableBlock: getReusableBlock, + __experimentalIsSavingReusableBlock: isSavingReusableBlock, + __experimentalIsFetchingReusableBlock: isFetchingReusableBlock, + __experimentalGetReusableBlocks: getReusableBlocks, +} = selectors; + +describe( 'selectors', () => { + let cachedSelectors; + + beforeAll( () => { + cachedSelectors = filter( selectors, ( selector ) => selector.clear ); + } ); + + beforeEach( () => { + registerBlockType( 'core/block', { + save: () => null, + category: 'reusable', + title: 'Reusable Block Stub', + supports: { + inserter: false, + }, + } ); + + cachedSelectors.forEach( ( { clear } ) => clear() ); + } ); + + afterEach( () => { + unregisterBlockType( 'core/block' ); + } ); + + describe( 'getReusableBlock', () => { + it( 'should return a reusable block', () => { + const state = { + data: { + 8109: { + clientId: 'foo', + title: 'My cool block', + }, + }, + }; + + const actualReusableBlock = getReusableBlock( state, 8109 ); + expect( actualReusableBlock ).toEqual( { + id: 8109, + isTemporary: false, + clientId: 'foo', + title: 'My cool block', + } ); + } ); + + it( 'should return a temporary reusable block', () => { + const state = { + data: { + reusable1: { + clientId: 'foo', + title: 'My cool block', + }, + }, + }; + + const actualReusableBlock = getReusableBlock( state, 'reusable1' ); + expect( actualReusableBlock ).toEqual( { + id: 'reusable1', + isTemporary: true, + clientId: 'foo', + title: 'My cool block', + } ); + } ); + + it( 'should return null when no reusable block exists', () => { + const state = { + data: {}, + }; + + const reusableBlock = getReusableBlock( state, 123 ); + expect( reusableBlock ).toBeNull(); + } ); + } ); + + describe( 'isSavingReusableBlock', () => { + it( 'should return false when the block is not being saved', () => { + const state = { + isSaving: {}, + }; + + const isSaving = isSavingReusableBlock( state, 5187 ); + expect( isSaving ).toBe( false ); + } ); + + it( 'should return true when the block is being saved', () => { + const state = { + isSaving: { + 5187: true, + }, + }; + + const isSaving = isSavingReusableBlock( state, 5187 ); + expect( isSaving ).toBe( true ); + } ); + } ); + + describe( 'isFetchingReusableBlock', () => { + it( 'should return false when the block is not being fetched', () => { + const state = { + isFetching: {}, + }; + + const isFetching = isFetchingReusableBlock( state, 5187 ); + expect( isFetching ).toBe( false ); + } ); + + it( 'should return true when the block is being fetched', () => { + const state = { + isFetching: { + 5187: true, + }, + }; + + const isFetching = isFetchingReusableBlock( state, 5187 ); + expect( isFetching ).toBe( true ); + } ); + } ); + + describe( 'getReusableBlocks', () => { + it( 'should return an array of reusable blocks', () => { + const state = { + data: { + 123: { clientId: 'carrot' }, + reusable1: { clientId: 'broccoli' }, + }, + }; + + const reusableBlocks = getReusableBlocks( state ); + expect( reusableBlocks ).toEqual( [ + { id: 123, isTemporary: false, clientId: 'carrot' }, + { id: 'reusable1', isTemporary: true, clientId: 'broccoli' }, + ] ); + } ); + + it( 'should return an empty array when no reusable blocks exist', () => { + const state = { + data: {}, + }; + + const reusableBlocks = getReusableBlocks( state ); + expect( reusableBlocks ).toEqual( [] ); + } ); + } ); +} ); From 61abe81d56f15e87f2c8b2f8d1cc8bd8a633184f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 6 Oct 2020 15:20:43 +0200 Subject: [PATCH 02/43] Add reusable-blocks as a dependency to edit-post package --- packages/edit-post/package.json | 1 + packages/edit-post/src/index.js | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index ed7f99fccc80ae..655b4862e38a81 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -48,6 +48,7 @@ "@wordpress/keycodes": "file:../keycodes", "@wordpress/media-utils": "file:../media-utils", "@wordpress/notices": "file:../notices", + "@wordpress/reusable-blocks": "file:../reusable-blocks", "@wordpress/plugins": "file:../plugins", "@wordpress/primitives": "file:../primitives", "@wordpress/url": "file:../url", diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 2bd1cc75e07518..f93ece6ce7f236 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -5,6 +5,7 @@ import '@wordpress/core-data'; import '@wordpress/block-editor'; import '@wordpress/editor'; import '@wordpress/keyboard-shortcuts'; +import '@wordpress/reusable-blocks'; import '@wordpress/viewport'; import '@wordpress/notices'; import { From 1dc20cb725edbb5b54eedd9ad97f54725d4e7c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 6 Oct 2020 15:28:19 +0200 Subject: [PATCH 03/43] Import reusable-blocks in edit-post/index.native.js --- packages/edit-post/src/index.native.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/edit-post/src/index.native.js b/packages/edit-post/src/index.native.js index 9afc8f8450b4e8..93502e3701d091 100644 --- a/packages/edit-post/src/index.native.js +++ b/packages/edit-post/src/index.native.js @@ -5,6 +5,7 @@ import '@wordpress/core-data'; import '@wordpress/viewport'; import '@wordpress/notices'; import '@wordpress/format-library'; +import '@wordpress/reusable-blocks'; import { render } from '@wordpress/element'; /** From 7c84d52e0837f8b4cd5fbf83706c2e41a3c60cdd Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 7 Oct 2020 14:58:38 +1100 Subject: [PATCH 04/43] WIP: Use Core Data in Reusable Blocks --- .../src/block/edit-panel/index.js | 10 -- packages/block-library/src/block/edit.js | 135 ++++++++++++++++-- 2 files changed, 123 insertions(+), 22 deletions(-) diff --git a/packages/block-library/src/block/edit-panel/index.js b/packages/block-library/src/block/edit-panel/index.js index abbd8ca0c12f8d..c3b60a963cf3e2 100644 --- a/packages/block-library/src/block/edit-panel/index.js +++ b/packages/block-library/src/block/edit-panel/index.js @@ -5,7 +5,6 @@ import { Button } from '@wordpress/components'; import { useInstanceId, usePrevious } from '@wordpress/compose'; import { useEffect, useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { ESCAPE } from '@wordpress/keycodes'; /** @typedef {import('@wordpress/element').WPComponent} WPComponent */ @@ -44,7 +43,6 @@ export default function ReusableBlockEditPanel( { isEditDisabled, isEditing, isSaving, - onCancel, onChangeTitle, onEdit, onSave, @@ -79,13 +77,6 @@ export default function ReusableBlockEditPanel( { onChangeTitle( event.target.value ); } - function handleTitleKeyDown( event ) { - if ( event.keyCode === ESCAPE ) { - event.stopPropagation(); - onCancel(); - } - } - return ( <> { ! isEditing && ! isSaving && ( @@ -120,7 +111,6 @@ export default function ReusableBlockEditPanel( { className="reusable-block-edit-panel__title" value={ title } onChange={ handleTitleChange } - onKeyDown={ handleTitleKeyDown } id={ `reusable-block-edit-panel__title-${ instanceId }` } /> + ); +} + +function MyConvertToReusableButton( { clientId } ) { + const { __experimentalConvertBlocksToReusable } = useDispatch('@wordpress/reusable-blocks'); + return ( + + ); +} +``` +

Code is Poetry.

diff --git a/packages/reusable-blocks/src/store/actions.js b/packages/reusable-blocks/src/store/actions.js index 16aca37cae9b1e..802d552b118e1b 100644 --- a/packages/reusable-blocks/src/store/actions.js +++ b/packages/reusable-blocks/src/store/actions.js @@ -13,7 +13,7 @@ export function* __experimentalConvertBlockToStatic( clientId ) { } /** - * Returns a generator converting a static block into a reusable block. + * Returns a generator converting one or more static blocks into a reusable block. * * @param {string} clientIds The client IDs of the block to detach. */ From ce35f7f12360a25b44c85ad8efbf73ddae01f711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 11:31:17 +0200 Subject: [PATCH 20/43] Deduplicate logic from block/edit.js --- packages/block-library/package.json | 1 + packages/block-library/src/block/edit.js | 12 ++++-------- packages/block-library/src/index.js | 1 + packages/reusable-blocks/src/store/controls.js | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index a117ceb32f70b1..3f7838316a65c1 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -49,6 +49,7 @@ "@wordpress/keycodes": "file:../keycodes", "@wordpress/notices": "file:../notices", "@wordpress/primitives": "file:../primitives", + "@wordpress/reusable-blocks": "file:../reusable-blocks", "@wordpress/rich-text": "file:../rich-text", "@wordpress/server-side-render": "file:../server-side-render", "@wordpress/url": "file:../url", diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 4882753cf1c43b..d2232f9123d0a8 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -18,7 +18,6 @@ import { BlockList, BlockControls, } from '@wordpress/block-editor'; -import { parse } from '@wordpress/blocks'; /** * Internal dependencies @@ -56,7 +55,9 @@ export default function ReusableBlockEdit( { const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' ); - const { replaceBlock } = useDispatch( 'core/block-editor' ); + const { + __experimentalConvertBlockToStatic: convertBlockToStatic, + } = useDispatch( 'core/reusable-blocks' ); const [ blocks, onInput, onChange ] = useEntityBlockEditor( 'postType', @@ -104,12 +105,7 @@ export default function ReusableBlockEdit( { - replaceBlock( - clientId, - parse( reusableBlock.content ) - ) - } + onClick={ () => convertBlockToStatic( clientId ) } > { __( 'Convert to regular blocks' ) } diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index e99a917ec839b0..ab0abf9456519e 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -4,6 +4,7 @@ import '@wordpress/core-data'; import '@wordpress/notices'; import '@wordpress/block-editor'; +import '@wordpress/reusable-blocks'; import { registerBlockType, setDefaultBlockName, diff --git a/packages/reusable-blocks/src/store/controls.js b/packages/reusable-blocks/src/store/controls.js index 2e6c551af6d554..fd315676b7baa3 100644 --- a/packages/reusable-blocks/src/store/controls.js +++ b/packages/reusable-blocks/src/store/controls.js @@ -38,7 +38,7 @@ const controls = { .select( 'core/block-editor' ) .getBlock( clientId ); const reusableBlock = registry - .select( 'core/block-editor' ) + .select( 'core' ) .getEditedEntityRecord( 'postType', 'wp_block', From 3dae6293eeeeaaf0b9554c0faeef16e5b7c382c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 11:31:29 +0200 Subject: [PATCH 21/43] Update README --- packages/reusable-blocks/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/reusable-blocks/README.md b/packages/reusable-blocks/README.md index 2a2aa62ea70c9a..f07864b756e7b9 100644 --- a/packages/reusable-blocks/README.md +++ b/packages/reusable-blocks/README.md @@ -73,7 +73,7 @@ This package also provides convenient utilities for managing reusable blocks thr ```js function MyConvertToStaticButton( { clientId } ) { - const { __experimentalConvertBlockToStatic } = useDispatch('@wordpress/reusable-blocks'); + const { __experimentalConvertBlockToStatic } = useDispatch( 'core/reusable-blocks' ); return ( ); } + +function MyDeleteReusableBlockButton( { id } ) { + const { __experimentalDeleteReusableBlock } = useDispatch( 'core/reusable-blocks' ); + return ( + + ); +} ```

Code is Poetry.

From 9c759b288431d028e4fcf326682f302900d7241d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 12:44:57 +0200 Subject: [PATCH 25/43] Refactor ReusableBlockDeleteButton to use useSelect and useDispatch instead of withSelect and withDispatch --- packages/reusable-blocks/package.json | 2 + .../reusable-block-delete-button.js | 115 +++++++++--------- packages/reusable-blocks/src/index.js | 1 + 3 files changed, 58 insertions(+), 60 deletions(-) diff --git a/packages/reusable-blocks/package.json b/packages/reusable-blocks/package.json index 7545ced1635b3b..2d9672f9f0c1be 100644 --- a/packages/reusable-blocks/package.json +++ b/packages/reusable-blocks/package.json @@ -32,8 +32,10 @@ "@wordpress/compose": "file:../compose", "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", + "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/notices": "file:../notices", "lodash": "^4.17.19" }, "publishConfig": { diff --git a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js index f747b9f464f5b7..10d563f974f834 100644 --- a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js +++ b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js @@ -1,11 +1,11 @@ /** * WordPress dependencies */ -import { compose } from '@wordpress/compose'; import { MenuItem } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { isReusableBlock } from '@wordpress/blocks'; -import { withSelect, withDispatch } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useCallback } from '@wordpress/element'; import { BlockSettingsMenuControls } from '@wordpress/block-editor'; /** @@ -13,11 +13,58 @@ import { BlockSettingsMenuControls } from '@wordpress/block-editor'; */ import { STORE_KEY } from '../../store/constants'; -export function ReusableBlockDeleteButton( { - isVisible, - isDisabled, - onDelete, -} ) { +const REUSABLE_BLOCK_NOTICE_ID = 'REUSABLE_BLOCK_NOTICE_ID'; + +export function ReusableBlockDeleteButton( { clientId } ) { + const { isVisible, isDisabled, block } = useSelect( ( select ) => { + const { getBlock } = select( 'core/block-editor' ); + const { canUser } = select( 'core' ); + const blockObj = getBlock( clientId ); + + const reusableBlock = + blockObj && isReusableBlock( blockObj ) + ? select( 'core' ).getEntityRecord( + 'postType', + 'wp_block', + blockObj.attributes.ref + ) + : null; + + return { + block: blockObj, + isVisible: + !! reusableBlock && + ( reusableBlock.isTemporary || + !! canUser( 'delete', 'blocks', reusableBlock.id ) ), + isDisabled: reusableBlock && reusableBlock.isTemporary, + }; + } ); + + const { + __experimentalDeleteReusableBlock: deleteReusableBlock, + } = useDispatch( STORE_KEY ); + + const { createSuccessNotice, createErrorNotice } = useDispatch( + 'core/notices' + ); + const onDelete = useCallback( + async function () { + try { + await deleteReusableBlock( block.attributes.ref ); + createSuccessNotice( __( 'Block deleted.' ), { + id: REUSABLE_BLOCK_NOTICE_ID, + type: 'snackbar', + } ); + } catch ( error ) { + createErrorNotice( error.message, { + id: REUSABLE_BLOCK_NOTICE_ID, + type: 'snackbar', + } ); + } + }, + [ block ] + ); + if ( ! isVisible ) { return null; } @@ -50,56 +97,4 @@ export function ReusableBlockDeleteButton( { ); } -const REUSABLE_BLOCK_NOTICE_ID = 'REUSABLE_BLOCK_NOTICE_ID'; - -export default compose( [ - withSelect( ( select, { clientId } ) => { - const { getBlock } = select( 'core/block-editor' ); - const { canUser } = select( 'core' ); - const block = getBlock( clientId ); - - const reusableBlock = - block && isReusableBlock( block ) - ? select( 'core' ).getEntityRecord( - 'postType', - 'wp_block', - block.attributes.ref - ) - : null; - - return { - isVisible: - !! reusableBlock && - ( reusableBlock.isTemporary || - !! canUser( 'delete', 'blocks', reusableBlock.id ) ), - isDisabled: reusableBlock && reusableBlock.isTemporary, - }; - } ), - withDispatch( ( dispatch, { clientId }, { select } ) => { - const { - __experimentalDeleteReusableBlock: deleteReusableBlock, - } = dispatch( STORE_KEY ); - const { getBlock } = select( 'core/block-editor' ); - - const { createSuccessNotice, createErrorNotice } = dispatch( - 'core/notices' - ); - return { - async onDelete() { - const block = getBlock( clientId ); - try { - await deleteReusableBlock( block.attributes.ref ); - createSuccessNotice( __( 'Block deleted.' ), { - id: REUSABLE_BLOCK_NOTICE_ID, - type: 'snackbar', - } ); - } catch ( error ) { - createErrorNotice( error.message, { - id: REUSABLE_BLOCK_NOTICE_ID, - type: 'snackbar', - } ); - } - }, - }; - } ), -] )( ReusableBlockDeleteButton ); +export default ReusableBlockDeleteButton; diff --git a/packages/reusable-blocks/src/index.js b/packages/reusable-blocks/src/index.js index 353570faf38275..b66f329c6fc3c5 100644 --- a/packages/reusable-blocks/src/index.js +++ b/packages/reusable-blocks/src/index.js @@ -3,6 +3,7 @@ */ import '@wordpress/block-editor'; import '@wordpress/core-data'; +import '@wordpress/notices'; /** * Internal dependencies From 5efbb95204fc5d1764e5e3f112fd40bc67a2c579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 12:46:58 +0200 Subject: [PATCH 26/43] Remove @TODO --- .../reusable-blocks-buttons/reusable-block-delete-button.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js index 10d563f974f834..1cc96396a3a96d 100644 --- a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js +++ b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js @@ -75,7 +75,6 @@ export function ReusableBlockDeleteButton( { clientId } ) { { - // TODO: Make this a component or similar // eslint-disable-next-line no-alert const hasConfirmed = window.confirm( // eslint-disable-next-line @wordpress/i18n-no-collapsible-whitespace From ecc095b8217c04916a0354f0389d851ccaa6c7ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 12:54:26 +0200 Subject: [PATCH 27/43] Use notices as before the refactor --- .../reusable-block-convert-button.js | 26 +++++++++- .../reusable-block-delete-button.js | 49 ++++++++++--------- .../reusable-blocks/src/store/constants.js | 7 +++ 3 files changed, 56 insertions(+), 26 deletions(-) diff --git a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-convert-button.js b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-convert-button.js index fbea4cf303f10e..d407ad163ad812 100644 --- a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-convert-button.js +++ b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-convert-button.js @@ -3,6 +3,7 @@ */ import { hasBlockSupport, isReusableBlock } from '@wordpress/blocks'; import { BlockSettingsMenuControls } from '@wordpress/block-editor'; +import { useCallback } from '@wordpress/element'; import { MenuItem } from '@wordpress/components'; import { reusableBlock } from '@wordpress/icons'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -11,7 +12,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { STORE_KEY } from '../../store/constants'; +import { STORE_KEY, REUSABLE_BLOCK_NOTICE_ID } from '../../store/constants'; /** * Menu control to convert block(s) to reusable block. @@ -67,6 +68,27 @@ export default function ReusableBlockConvertButton( { clientIds } ) { __experimentalConvertBlocksToReusable: convertBlocksToReusable, } = useDispatch( STORE_KEY ); + const { createSuccessNotice, createErrorNotice } = useDispatch( + 'core/notices' + ); + const onConvert = useCallback( + async function () { + try { + await convertBlocksToReusable( clientIds ); + createSuccessNotice( __( 'Block created.' ), { + id: REUSABLE_BLOCK_NOTICE_ID, + type: 'snackbar', + } ); + } catch ( error ) { + createErrorNotice( error.message, { + id: REUSABLE_BLOCK_NOTICE_ID, + type: 'snackbar', + } ); + } + }, + [ clientIds ] + ); + if ( ! canConvert ) { return null; } @@ -77,7 +99,7 @@ export default function ReusableBlockConvertButton( { clientIds } ) { { - convertBlocksToReusable( clientIds ); + onConvert(); onClose(); } } > diff --git a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js index 1cc96396a3a96d..c31ec6ba24433b 100644 --- a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js +++ b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js @@ -11,34 +11,35 @@ import { BlockSettingsMenuControls } from '@wordpress/block-editor'; /** * Internal dependencies */ -import { STORE_KEY } from '../../store/constants'; - -const REUSABLE_BLOCK_NOTICE_ID = 'REUSABLE_BLOCK_NOTICE_ID'; +import { STORE_KEY, REUSABLE_BLOCK_NOTICE_ID } from '../../store/constants'; export function ReusableBlockDeleteButton( { clientId } ) { - const { isVisible, isDisabled, block } = useSelect( ( select ) => { - const { getBlock } = select( 'core/block-editor' ); - const { canUser } = select( 'core' ); - const blockObj = getBlock( clientId ); + const { isVisible, isDisabled, block } = useSelect( + ( select ) => { + const { getBlock } = select( 'core/block-editor' ); + const { canUser } = select( 'core' ); + const blockObj = getBlock( clientId ); - const reusableBlock = - blockObj && isReusableBlock( blockObj ) - ? select( 'core' ).getEntityRecord( - 'postType', - 'wp_block', - blockObj.attributes.ref - ) - : null; + const reusableBlock = + blockObj && isReusableBlock( blockObj ) + ? select( 'core' ).getEntityRecord( + 'postType', + 'wp_block', + blockObj.attributes.ref + ) + : null; - return { - block: blockObj, - isVisible: - !! reusableBlock && - ( reusableBlock.isTemporary || - !! canUser( 'delete', 'blocks', reusableBlock.id ) ), - isDisabled: reusableBlock && reusableBlock.isTemporary, - }; - } ); + return { + block: blockObj, + isVisible: + !! reusableBlock && + ( reusableBlock.isTemporary || + !! canUser( 'delete', 'blocks', reusableBlock.id ) ), + isDisabled: reusableBlock && reusableBlock.isTemporary, + }; + }, + [ clientId ] + ); const { __experimentalDeleteReusableBlock: deleteReusableBlock, diff --git a/packages/reusable-blocks/src/store/constants.js b/packages/reusable-blocks/src/store/constants.js index 3563b7456c6c78..7b29c79eaf31d5 100644 --- a/packages/reusable-blocks/src/store/constants.js +++ b/packages/reusable-blocks/src/store/constants.js @@ -4,3 +4,10 @@ * @type {string} */ export const STORE_KEY = 'core/reusable-blocks'; + +/** + * ID used for all the notices created by this module + * + * @type {string} + */ +export const REUSABLE_BLOCK_NOTICE_ID = 'REUSABLE_BLOCK_NOTICE_ID'; From 1202251738fc1333c2ef990a3ff9ea42f3f8d2aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 13:01:56 +0200 Subject: [PATCH 28/43] Show notices when reusable block is updated --- packages/block-library/src/block/edit.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index d2232f9123d0a8..42bbe91c8401f4 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -3,7 +3,7 @@ */ import { useSelect, useDispatch } from '@wordpress/data'; import { useEntityBlockEditor } from '@wordpress/core-data'; -import { useState } from '@wordpress/element'; +import { useState, useCallback } from '@wordpress/element'; import { Placeholder, Spinner, @@ -59,6 +59,22 @@ export default function ReusableBlockEdit( { __experimentalConvertBlockToStatic: convertBlockToStatic, } = useDispatch( 'core/reusable-blocks' ); + const { createSuccessNotice, createErrorNotice } = useDispatch( + 'core/notices' + ); + const save = useCallback( async function () { + try { + await saveEditedEntityRecord( ...recordArgs ); + createSuccessNotice( __( 'Block updated.' ), { + type: 'snackbar', + } ); + } catch ( error ) { + createErrorNotice( error.message, { + type: 'snackbar', + } ); + } + }, recordArgs ); + const [ blocks, onInput, onChange ] = useEntityBlockEditor( 'postType', 'wp_block', @@ -124,7 +140,7 @@ export default function ReusableBlockEdit( { editEntityRecord( ...recordArgs, { title } ) } onSave={ () => { - saveEditedEntityRecord( ...recordArgs ); + save(); setIsEditing( false ); } } /> From 43334193b5cdbeefea80b9b7cbb554177b0df707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 13:02:15 +0200 Subject: [PATCH 29/43] Remove REUSABLE_BLOCK_NOTICE_ID notice id --- .../reusable-block-convert-button.js | 4 +--- .../reusable-block-delete-button.js | 4 +--- packages/reusable-blocks/src/store/constants.js | 7 ------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-convert-button.js b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-convert-button.js index d407ad163ad812..b4b72220a1daab 100644 --- a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-convert-button.js +++ b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-convert-button.js @@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { STORE_KEY, REUSABLE_BLOCK_NOTICE_ID } from '../../store/constants'; +import { STORE_KEY } from '../../store/constants'; /** * Menu control to convert block(s) to reusable block. @@ -76,12 +76,10 @@ export default function ReusableBlockConvertButton( { clientIds } ) { try { await convertBlocksToReusable( clientIds ); createSuccessNotice( __( 'Block created.' ), { - id: REUSABLE_BLOCK_NOTICE_ID, type: 'snackbar', } ); } catch ( error ) { createErrorNotice( error.message, { - id: REUSABLE_BLOCK_NOTICE_ID, type: 'snackbar', } ); } diff --git a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js index c31ec6ba24433b..943fb3cacd2531 100644 --- a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js +++ b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js @@ -11,7 +11,7 @@ import { BlockSettingsMenuControls } from '@wordpress/block-editor'; /** * Internal dependencies */ -import { STORE_KEY, REUSABLE_BLOCK_NOTICE_ID } from '../../store/constants'; +import { STORE_KEY } from '../../store/constants'; export function ReusableBlockDeleteButton( { clientId } ) { const { isVisible, isDisabled, block } = useSelect( @@ -53,12 +53,10 @@ export function ReusableBlockDeleteButton( { clientId } ) { try { await deleteReusableBlock( block.attributes.ref ); createSuccessNotice( __( 'Block deleted.' ), { - id: REUSABLE_BLOCK_NOTICE_ID, type: 'snackbar', } ); } catch ( error ) { createErrorNotice( error.message, { - id: REUSABLE_BLOCK_NOTICE_ID, type: 'snackbar', } ); } diff --git a/packages/reusable-blocks/src/store/constants.js b/packages/reusable-blocks/src/store/constants.js index 7b29c79eaf31d5..3563b7456c6c78 100644 --- a/packages/reusable-blocks/src/store/constants.js +++ b/packages/reusable-blocks/src/store/constants.js @@ -4,10 +4,3 @@ * @type {string} */ export const STORE_KEY = 'core/reusable-blocks'; - -/** - * ID used for all the notices created by this module - * - * @type {string} - */ -export const REUSABLE_BLOCK_NOTICE_ID = 'REUSABLE_BLOCK_NOTICE_ID'; From bc06f59cf759b43dc6b8c4b29c070ad7484310de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 13:11:42 +0200 Subject: [PATCH 30/43] Update package-lock.json --- package-lock.json | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8fdc8d56c4ec5c..a87cb1213210de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16853,6 +16853,7 @@ "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/notices": "file:packages/notices", "@wordpress/primitives": "file:packages/primitives", + "@wordpress/reusable-blocks": "file:packages/reusable-blocks", "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/server-side-render": "file:packages/server-side-render", "@wordpress/url": "file:packages/url", @@ -18324,17 +18325,17 @@ "@wordpress/reusable-blocks": { "version": "file:packages/reusable-blocks", "requires": { - "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/block-editor": "file:packages/block-editor", "@wordpress/blocks": "file:packages/blocks", + "@wordpress/components": "file:packages/components", + "@wordpress/compose": "file:packages/compose", "@wordpress/core-data": "file:packages/core-data", "@wordpress/data": "file:packages/data", + "@wordpress/element": "file:packages/element", "@wordpress/i18n": "file:packages/i18n", + "@wordpress/icons": "file:packages/icons", "@wordpress/notices": "file:packages/notices", - "lodash": "^4.17.19", - "redux-optimist": "^1.0.0", - "refx": "^3.0.0", - "rememo": "^3.0.0" + "lodash": "^4.17.19" }, "dependencies": { "@babel/runtime": { From 8bf36e21325a2d2d812840724c51f6197f3f8b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 13:22:28 +0200 Subject: [PATCH 31/43] Embrace entity data format instead of augmenting reusable blocks to legacy data shape --- packages/block-editor/src/store/selectors.js | 4 ++-- packages/editor/src/components/provider/index.js | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index b84a7ff24aed79..e656f0b425e843 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1497,7 +1497,7 @@ export const getInserterItems = createSelector( id, name: 'core/block', initialAttributes: { ref: reusableBlock.id }, - title: reusableBlock.title, + title: reusableBlock.title.raw, icon: referencedBlockType ? referencedBlockType.icon : templateIcon, @@ -1688,7 +1688,7 @@ export const __experimentalGetParsedReusableBlock = createSelector( return null; } - return parse( reusableBlock.content ); + return parse( reusableBlock.content.raw ); }, ( state ) => [ getReusableBlocks( state ) ] ); diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index c0b34d3aa8d72c..2b6915b0ec00fd 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -207,11 +207,7 @@ class EditorProvider extends Component { 'titlePlaceholder', ] ), mediaUpload: hasUploadPermissions ? mediaUpload : undefined, - __experimentalReusableBlocks: reusableBlocks?.map( ( c ) => ( { - ...c, - content: c.content.raw, - title: c.title.raw, - } ) ), + __experimentalReusableBlocks: reusableBlocks, __experimentalFetchLinkSuggestions: partialRight( fetchLinkSuggestions, settings From 9d9032e1ee7fa287e316ff59f80b045d4fa9b82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 13:40:20 +0200 Subject: [PATCH 32/43] Don't export storeConfig --- packages/reusable-blocks/src/index.js | 1 - packages/reusable-blocks/src/store/index.js | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/reusable-blocks/src/index.js b/packages/reusable-blocks/src/index.js index b66f329c6fc3c5..55ba8837aa1512 100644 --- a/packages/reusable-blocks/src/index.js +++ b/packages/reusable-blocks/src/index.js @@ -11,4 +11,3 @@ import '@wordpress/notices'; import './store'; export * from './components'; -export { storeConfig } from './store'; diff --git a/packages/reusable-blocks/src/store/index.js b/packages/reusable-blocks/src/store/index.js index 557c42bccb132a..8bdbea25dd5151 100644 --- a/packages/reusable-blocks/src/store/index.js +++ b/packages/reusable-blocks/src/store/index.js @@ -17,10 +17,8 @@ import { STORE_KEY } from './constants'; * * @type {Object} */ -export const storeConfig = { +export default registerStore( STORE_KEY, { actions, controls, reducer: () => {}, -}; - -export default registerStore( STORE_KEY, storeConfig ); +} ); From c2bdc50fdb8910a1572f9e4e3198f915a95c9cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 14:21:54 +0200 Subject: [PATCH 33/43] Fix mobile unit tests --- packages/reusable-blocks/src/index.native.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/reusable-blocks/src/index.native.js b/packages/reusable-blocks/src/index.native.js index fea02253022f79..14c1977326fd52 100644 --- a/packages/reusable-blocks/src/index.native.js +++ b/packages/reusable-blocks/src/index.native.js @@ -7,3 +7,5 @@ import '@wordpress/core-data'; * Internal dependencies */ import './store'; + +export * from './components'; From 186071c2b0fea297c58ba454e3dcd331b403425c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 14:22:23 +0200 Subject: [PATCH 34/43] Fix alphabetical ordering --- packages/edit-post/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 655b4862e38a81..3e4879b035b31e 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -48,9 +48,9 @@ "@wordpress/keycodes": "file:../keycodes", "@wordpress/media-utils": "file:../media-utils", "@wordpress/notices": "file:../notices", - "@wordpress/reusable-blocks": "file:../reusable-blocks", "@wordpress/plugins": "file:../plugins", "@wordpress/primitives": "file:../primitives", + "@wordpress/reusable-blocks": "file:../reusable-blocks", "@wordpress/url": "file:../url", "@wordpress/viewport": "file:../viewport", "@wordpress/warning": "file:../warning", From 764a59e1804b9d142aee5d9fa9fd03b6a503b280 Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Fri, 9 Oct 2020 14:31:13 +0200 Subject: [PATCH 35/43] Update docstring --- packages/reusable-blocks/src/store/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/reusable-blocks/src/store/actions.js b/packages/reusable-blocks/src/store/actions.js index 6c9bac64556ec7..e1b4eeece122e7 100644 --- a/packages/reusable-blocks/src/store/actions.js +++ b/packages/reusable-blocks/src/store/actions.js @@ -28,7 +28,7 @@ export function* __experimentalConvertBlocksToReusable( clientIds ) { /** * Returns a generator deleting a reusable block. * - * @param {string} id The client IDs of the block to detach. + * @param {string} id The ID of the reusable block to delete. */ export function* __experimentalDeleteReusableBlock( id ) { yield deleteReusableBlock( id ); From 694a241e911c7caa81dd34931756c797418fb213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 14:41:11 +0200 Subject: [PATCH 36/43] Adjust tests to the new data shape --- packages/block-editor/src/store/test/selectors.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 187a5fb6dc767e..791557195b8741 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -2462,8 +2462,8 @@ describe( 'selectors', () => { id: 1, isTemporary: false, clientId: 'block1', - title: 'Reusable Block 1', - content: '', + title: { raw: 'Reusable Block 1' }, + content: { raw: '' }, }, ], }, @@ -2542,15 +2542,15 @@ describe( 'selectors', () => { id: 1, isTemporary: false, clientId: 'block1', - title: 'Reusable Block 1', - content: '', + title: { raw: 'Reusable Block 1' }, + content: { raw: '' }, }, { id: 2, isTemporary: false, clientId: 'block2', - title: 'Reusable Block 2', - content: '', + title: { raw: 'Reusable Block 2' }, + content: { raw: '' }, }, ], }, From 7bfce26db70811afc8e18b56af86061e57c67165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 15:56:36 +0200 Subject: [PATCH 37/43] Initialize newly created reusable blocks as in editing state --- packages/block-library/src/block/edit.js | 19 ++++++++-- packages/reusable-blocks/src/store/actions.js | 15 ++++++++ .../reusable-blocks/src/store/controls.js | 18 ++++++--- packages/reusable-blocks/src/store/index.js | 7 +++- packages/reusable-blocks/src/store/reducer.js | 19 ++++++++++ .../reusable-blocks/src/store/selectors.js | 10 +++++ .../reusable-blocks/src/store/test/actions.js | 17 +++++++++ .../src/store/test/controls.js | 3 ++ .../reusable-blocks/src/store/test/reducer.js | 38 +++++++++++++++++++ .../src/store/test/selectors.js | 27 +++++++++++++ 10 files changed, 161 insertions(+), 12 deletions(-) create mode 100644 packages/reusable-blocks/src/store/reducer.js create mode 100644 packages/reusable-blocks/src/store/selectors.js create mode 100644 packages/reusable-blocks/src/store/test/actions.js create mode 100644 packages/reusable-blocks/src/store/test/reducer.js create mode 100644 packages/reusable-blocks/src/store/test/selectors.js diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 42bbe91c8401f4..da5600c2c9a266 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -3,7 +3,7 @@ */ import { useSelect, useDispatch } from '@wordpress/data'; import { useEntityBlockEditor } from '@wordpress/core-data'; -import { useState, useCallback } from '@wordpress/element'; +import { useCallback } from '@wordpress/element'; import { Placeholder, Spinner, @@ -34,6 +34,7 @@ export default function ReusableBlockEdit( { const { reusableBlock, hasResolved, + isEditing, isSaving, canUserUpdate, settings, @@ -48,12 +49,24 @@ export default function ReusableBlockEdit( { ), isSaving: select( 'core' ).isSavingEntityRecord( ...recordArgs ), canUserUpdate: select( 'core' ).canUser( 'update', 'blocks', ref ), + isEditing: select( + 'core/reusable-blocks' + ).__experimentalIsEditingReusableBlock( clientId ), settings: select( 'core/block-editor' ).getSettings(), } ), - [ ref ] + [ ref, clientId ] ); const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' ); + const { __experimentalSetEditingReusableBlock } = useDispatch( + 'core/reusable-blocks' + ); + const setIsEditing = useCallback( + ( value ) => { + __experimentalSetEditingReusableBlock( clientId, value ); + }, + [ clientId ] + ); const { __experimentalConvertBlockToStatic: convertBlockToStatic, @@ -81,8 +94,6 @@ export default function ReusableBlockEdit( { { id: ref } ); - const [ isEditing, setIsEditing ] = useState( false ); // TODO: Start in edit mode when newly created - if ( ! hasResolved ) { return ( diff --git a/packages/reusable-blocks/src/store/actions.js b/packages/reusable-blocks/src/store/actions.js index e1b4eeece122e7..964caa8ef2b2a8 100644 --- a/packages/reusable-blocks/src/store/actions.js +++ b/packages/reusable-blocks/src/store/actions.js @@ -33,3 +33,18 @@ export function* __experimentalConvertBlocksToReusable( clientIds ) { export function* __experimentalDeleteReusableBlock( id ) { yield deleteReusableBlock( id ); } + +/** + * Returns an action descriptor for SET_EDITING_REUSABLE_BLOCK action. + * + * @param {string} clientId The clientID of the reusable block to target. + * @param {boolean} isEditing Whether the block should be in editing state. + * @return {Object} Action descriptor. + */ +export function __experimentalSetEditingReusableBlock( clientId, isEditing ) { + return { + type: 'SET_EDITING_REUSABLE_BLOCK', + clientId, + isEditing, + }; +} diff --git a/packages/reusable-blocks/src/store/controls.js b/packages/reusable-blocks/src/store/controls.js index d90432b3e26048..310a0d3533b7bb 100644 --- a/packages/reusable-blocks/src/store/controls.js +++ b/packages/reusable-blocks/src/store/controls.js @@ -87,12 +87,18 @@ const controls = { .dispatch( 'core' ) .saveEntityRecord( 'postType', 'wp_block', reusableBlock ); - registry.dispatch( 'core/block-editor' ).replaceBlocks( - clientIds, - createBlock( 'core/block', { - ref: updatedRecord.id, - } ) - ); + const newBlock = createBlock( 'core/block', { + ref: updatedRecord.id, + } ); + registry + .dispatch( 'core/block-editor' ) + .replaceBlocks( clientIds, newBlock ); + registry + .dispatch( 'core/reusable-blocks' ) + .__experimentalSetEditingReusableBlock( + newBlock.clientId, + true + ); } ), diff --git a/packages/reusable-blocks/src/store/index.js b/packages/reusable-blocks/src/store/index.js index 8bdbea25dd5151..b6b212ad968a88 100644 --- a/packages/reusable-blocks/src/store/index.js +++ b/packages/reusable-blocks/src/store/index.js @@ -6,8 +6,10 @@ import { registerStore } from '@wordpress/data'; /** * Internal dependencies */ -import controls from './controls'; import * as actions from './actions'; +import controls from './controls'; +import reducer from './reducer'; +import * as selectors from './selectors'; import { STORE_KEY } from './constants'; /** @@ -20,5 +22,6 @@ import { STORE_KEY } from './constants'; export default registerStore( STORE_KEY, { actions, controls, - reducer: () => {}, + reducer, + selectors, } ); diff --git a/packages/reusable-blocks/src/store/reducer.js b/packages/reusable-blocks/src/store/reducer.js new file mode 100644 index 00000000000000..de33a72acfba50 --- /dev/null +++ b/packages/reusable-blocks/src/store/reducer.js @@ -0,0 +1,19 @@ +/** + * WordPress dependencies + */ +import { combineReducers } from '@wordpress/data'; + +export function isEditingReusableBlock( state = {}, action ) { + if ( action?.type === 'SET_EDITING_REUSABLE_BLOCK' ) { + return { + ...state, + [ action.clientId ]: action.isEditing, + }; + } + + return state; +} + +export default combineReducers( { + isEditingReusableBlock, +} ); diff --git a/packages/reusable-blocks/src/store/selectors.js b/packages/reusable-blocks/src/store/selectors.js new file mode 100644 index 00000000000000..ac290c59a1cd6b --- /dev/null +++ b/packages/reusable-blocks/src/store/selectors.js @@ -0,0 +1,10 @@ +/** + * Returns true if reusable block is in the editing state. + * + * @param {Object} state Global application state. + * @param {number} clientId the clientID of the block. + * @return {boolean} Whether the reusable block is in the editing state. + */ +export function __experimentalIsEditingReusableBlock( state, clientId ) { + return state.isEditingReusableBlock[ clientId ]; +} diff --git a/packages/reusable-blocks/src/store/test/actions.js b/packages/reusable-blocks/src/store/test/actions.js new file mode 100644 index 00000000000000..6d189c6b535f4b --- /dev/null +++ b/packages/reusable-blocks/src/store/test/actions.js @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import { __experimentalSetEditingReusableBlock } from '../actions'; + +describe( 'Actions', () => { + describe( '__experimentalSetEditingReusableBlock', () => { + it( 'should return the SET_EDITING_REUSABLE_BLOCK action', () => { + const result = __experimentalSetEditingReusableBlock( 3, true ); + expect( result ).toEqual( { + type: 'SET_EDITING_REUSABLE_BLOCK', + clientId: 3, + isEditing: true, + } ); + } ); + } ); +} ); diff --git a/packages/reusable-blocks/src/store/test/controls.js b/packages/reusable-blocks/src/store/test/controls.js index 5e9ce1332cfcf6..a3387263630f58 100644 --- a/packages/reusable-blocks/src/store/test/controls.js +++ b/packages/reusable-blocks/src/store/test/controls.js @@ -51,6 +51,7 @@ describe( 'reusable blocks effects', () => { } ); const saveEntityRecord = jest.fn( () => ( { id: 456 } ) ); const replaceBlocks = jest.fn(); + const __experimentalSetEditingReusableBlock = jest.fn(); const getBlocksByClientId = jest.fn( () => [ staticBlock ] ); const registry = { select: jest.fn( () => ( { @@ -59,6 +60,7 @@ describe( 'reusable blocks effects', () => { dispatch: jest.fn( () => ( { saveEntityRecord, replaceBlocks, + __experimentalSetEditingReusableBlock, } ) ), }; @@ -83,6 +85,7 @@ describe( 'reusable blocks effects', () => { name: 'core/block', } ) ); + expect( __experimentalSetEditingReusableBlock ).toHaveBeenCalled(); } ); describe( 'CONVERT_BLOCK_TO_STATIC', () => { diff --git a/packages/reusable-blocks/src/store/test/reducer.js b/packages/reusable-blocks/src/store/test/reducer.js new file mode 100644 index 00000000000000..cd0d26bb1b965a --- /dev/null +++ b/packages/reusable-blocks/src/store/test/reducer.js @@ -0,0 +1,38 @@ +/** + * Internal dependencies + */ +import { isEditingReusableBlock } from '../reducer'; + +describe( 'isEditingReusableBlock', () => { + it( 'should initialize empty state when there is no original state', () => { + expect( isEditingReusableBlock() ).toEqual( {} ); + } ); + + it( 'should set the value in state', () => { + const originalState = {}; + const newState = isEditingReusableBlock( originalState, { + type: 'SET_EDITING_REUSABLE_BLOCK', + clientId: 1, + isEditing: true, + } ); + expect( newState ).not.toBe( originalState ); + expect( newState ).toEqual( { + 1: true, + } ); + } ); + + it( 'should replace the value in state', () => { + const originalState = { + 1: false, + }; + const newState = isEditingReusableBlock( originalState, { + type: 'SET_EDITING_REUSABLE_BLOCK', + clientId: 1, + isEditing: true, + } ); + expect( newState ).not.toBe( originalState ); + expect( newState ).toEqual( { + 1: true, + } ); + } ); +} ); diff --git a/packages/reusable-blocks/src/store/test/selectors.js b/packages/reusable-blocks/src/store/test/selectors.js new file mode 100644 index 00000000000000..1e0dc01df63f9d --- /dev/null +++ b/packages/reusable-blocks/src/store/test/selectors.js @@ -0,0 +1,27 @@ +/** + * Internal dependencies + */ +import { __experimentalIsEditingReusableBlock } from '../selectors'; + +describe( '__experimentalIsEditingReusableBlock', () => { + it( 'gets the value for clientId', () => { + expect( + __experimentalIsEditingReusableBlock( + { isEditingReusableBlock: { 1: true } }, + 1 + ) + ).toBe( true ); + expect( + __experimentalIsEditingReusableBlock( + { isEditingReusableBlock: { 2: false } }, + 2 + ) + ).toBe( false ); + expect( + __experimentalIsEditingReusableBlock( + { isEditingReusableBlock: { 2: false } }, + 3 + ) + ).toBe( undefined ); + } ); +} ); From c7fff760155e7dc2bd97e4aeb4da8fbfa8a3bea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Oct 2020 16:19:32 +0200 Subject: [PATCH 38/43] Remove outdated test --- .../editor/various/change-detection.test.js | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/packages/e2e-tests/specs/editor/various/change-detection.test.js b/packages/e2e-tests/specs/editor/various/change-detection.test.js index 514a7fdd106f10..8bac378e8aaa1a 100644 --- a/packages/e2e-tests/specs/editor/various/change-detection.test.js +++ b/packages/e2e-tests/specs/editor/various/change-detection.test.js @@ -306,27 +306,6 @@ describe( 'Change detection', () => { await assertIsDirty( true ); } ); - it( 'should not prompt when receiving reusable blocks', async () => { - // Regression Test: Verify that non-modifying behaviors does not incur - // dirtiness. Previously, this could occur as a result of either (a) - // selecting a block, (b) opening the inserter, or (c) editing a post - // which contained a reusable block. The root issue was changes in - // block editor state as a result of reusable blocks data having been - // received, reflected here in this test. - // - // TODO: This should be considered a temporary test, existing only so - // long as the experimental reusable blocks fetching data flow exists. - // - // See: https://github.com/WordPress/gutenberg/issues/14766 - await page.evaluate( () => - window.wp.data - .dispatch( 'core/editor' ) - .__experimentalReceiveReusableBlocks( [] ) - ); - - await assertIsDirty( false ); - } ); - it( 'should save posts without titles and persist and overwrite the auto draft title', async () => { // Enter content. await clickBlockAppender(); From eebe62540cb243494de27c5043daf35bddbb9db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 12 Oct 2020 17:25:52 +0200 Subject: [PATCH 39/43] Adjust e2e tests --- packages/e2e-test-utils/src/inserter.js | 4 ++++ .../specs/editor/various/reusable-blocks.test.js | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/e2e-test-utils/src/inserter.js b/packages/e2e-test-utils/src/inserter.js index 94ffccdf2ae5bb..8c51fb306ee73f 100644 --- a/packages/e2e-test-utils/src/inserter.js +++ b/packages/e2e-test-utils/src/inserter.js @@ -168,6 +168,10 @@ export async function insertReusableBlock( searchTerm ) { await insertButton.click(); // We should wait until the inserter closes and the focus moves to the content. await waitForInserterCloseAndContentFocus(); + // We should wait until the block is loaded + await page.waitForXPath( + '//*[@class="block-library-block__reusable-block-container"]' + ); } /** diff --git a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js index 05a04312dfa3e3..813ab9686e06d6 100644 --- a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js +++ b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js @@ -52,6 +52,9 @@ describe( 'Reusable blocks', () => { await page.waitForXPath( '//*[contains(@class, "components-snackbar")]/*[text()="Block created."]' ); + await page.waitForXPath( + '//*[@class="block-library-block__reusable-block-container"]' + ); // Select all of the text in the title field. await pressKeyWithModifier( 'primary', 'a' ); @@ -96,6 +99,9 @@ describe( 'Reusable blocks', () => { await page.waitForXPath( '//*[contains(@class, "components-snackbar")]/*[text()="Block created."]' ); + await page.waitForXPath( + '//*[@class="block-library-block__reusable-block-container"]' + ); // Save the reusable block const [ saveButton ] = await page.$x( '//button[text()="Save"]' ); @@ -187,6 +193,9 @@ describe( 'Reusable blocks', () => { await page.waitForXPath( '//*[contains(@class, "components-snackbar")]/*[text()="Block created."]' ); + await page.waitForXPath( + '//*[@class="block-library-block__reusable-block-container"]' + ); // Select all of the text in the title field. await pressKeyWithModifier( 'primary', 'a' ); @@ -294,6 +303,9 @@ describe( 'Reusable blocks', () => { await page.waitForXPath( '//*[contains(@class, "components-snackbar")]/*[text()="Block created."]' ); + await page.waitForXPath( + '//*[@class="block-library-block__reusable-block-container"]' + ); // Select all of the text in the title field. await pressKeyWithModifier( 'primary', 'a' ); From 96a33f37c9dbedd1775fd10662906ae076b0a6f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 13 Oct 2020 12:52:35 +0200 Subject: [PATCH 40/43] Remove obsolete isTemporary --- packages/reusable-blocks/src/store/controls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/reusable-blocks/src/store/controls.js b/packages/reusable-blocks/src/store/controls.js index 310a0d3533b7bb..8b9b9613c05217 100644 --- a/packages/reusable-blocks/src/store/controls.js +++ b/packages/reusable-blocks/src/store/controls.js @@ -110,7 +110,7 @@ const controls = { .getEditedEntityRecord( 'postType', 'wp_block', id ); // Don't allow a reusable block with a temporary ID to be deleted - if ( ! reusableBlock || reusableBlock.isTemporary ) { + if ( ! reusableBlock ) { return; } From f255483a78861d96dfaf6383cf30ee38e852274e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 13 Oct 2020 12:53:01 +0200 Subject: [PATCH 41/43] Don't export ReusableBlockDeleteButton twice --- .../reusable-blocks-buttons/reusable-block-delete-button.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js index 943fb3cacd2531..bc59105d2e44b3 100644 --- a/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js +++ b/packages/reusable-blocks/src/components/reusable-blocks-buttons/reusable-block-delete-button.js @@ -13,7 +13,7 @@ import { BlockSettingsMenuControls } from '@wordpress/block-editor'; */ import { STORE_KEY } from '../../store/constants'; -export function ReusableBlockDeleteButton( { clientId } ) { +function ReusableBlockDeleteButton( { clientId } ) { const { isVisible, isDisabled, block } = useSelect( ( select ) => { const { getBlock } = select( 'core/block-editor' ); From d5ac446073ed888c829898f417099f1e43882bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 13 Oct 2020 14:17:15 +0200 Subject: [PATCH 42/43] Don't export ReusableBlockDeleteButton twice --- packages/block-editor/src/store/selectors.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index e656f0b425e843..ce16fd52768db3 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1688,7 +1688,9 @@ export const __experimentalGetParsedReusableBlock = createSelector( return null; } - return parse( reusableBlock.content.raw ); + // Only reusableBlock.content.raw should be used here, `reusableBlock.content` is a + // workaround until #22127 is fixed. + return parse( reusableBlock.content.raw || reusableBlock.content ); }, ( state ) => [ getReusableBlocks( state ) ] ); From 6d2243c7cfdaf900fc9042af3064e7bbc2461a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 14 Oct 2020 11:02:20 +0200 Subject: [PATCH 43/43] Prettier --- packages/block-library/src/block/edit.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index a8f80c57a4bbf8..da5600c2c9a266 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -19,7 +19,6 @@ import { BlockControls, } from '@wordpress/block-editor'; - /** * Internal dependencies */