diff --git a/lib/class-wp-rest-blocks-controller.php b/lib/class-wp-rest-blocks-controller.php deleted file mode 100644 index 75f69afe917598..00000000000000 --- a/lib/class-wp-rest-blocks-controller.php +++ /dev/null @@ -1,123 +0,0 @@ -post_type ); - if ( ! current_user_can( $post_type->cap->read_post, $post->ID ) ) { - return false; - } - - return parent::check_read_permission( $post ); - } - - /** - * Handle a DELETE request. - * - * @since 1.10.0 - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. - */ - public function delete_item( $request ) { - // Always hard-delete a block. - $request->set_param( 'force', true ); - - return parent::delete_item( $request ); - } - - /** - * Given an update or create request, build the post object that is saved to - * the database. - * - * @since 1.10.0 - * - * @param WP_REST_Request $request Request object. - * @return stdClass|WP_Error Post object or WP_Error. - */ - public function prepare_item_for_database( $request ) { - $prepared_post = parent::prepare_item_for_database( $request ); - - // Force blocks to always be published. - $prepared_post->post_status = 'publish'; - - return $prepared_post; - } - - /** - * Given a block from the database, build the array that is returned from an - * API response. - * - * @since 1.10.0 - * - * @param WP_Post $post Post object that backs the block. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response Response object. - */ - public function prepare_item_for_response( $post, $request ) { - $data = array( - 'id' => $post->ID, - 'title' => $post->post_title, - 'content' => $post->post_content, - ); - - $response = rest_ensure_response( $data ); - - return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request ); - } - - /** - * Builds the block's schema, conforming to JSON Schema. - * - * @since 1.10.0 - * - * @return array Item schema data. - */ - public function get_item_schema() { - return array( - '$schema' => 'http://json-schema.org/schema#', - 'title' => $this->post_type, - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the block.', 'gutenberg' ), - 'type' => 'integer', - 'readonly' => true, - ), - 'title' => array( - 'description' => __( 'The block’s title.', 'gutenberg' ), - 'type' => 'string', - 'required' => true, - ), - 'content' => array( - 'description' => __( 'The block’s HTML content.', 'gutenberg' ), - 'type' => 'string', - 'required' => true, - ), - ), - ); - } -} diff --git a/lib/load.php b/lib/load.php index 0f18f6e4d15f9e..b3e02a0bbe1465 100644 --- a/lib/load.php +++ b/lib/load.php @@ -12,7 +12,6 @@ // These files only need to be loaded if within a rest server instance // which this class will exist if that is the case. if ( class_exists( 'WP_REST_Controller' ) ) { - require dirname( __FILE__ ) . '/class-wp-rest-blocks-controller.php'; require dirname( __FILE__ ) . '/class-wp-rest-autosaves-controller.php'; require dirname( __FILE__ ) . '/class-wp-rest-block-renderer-controller.php'; require dirname( __FILE__ ) . '/class-wp-rest-search-controller.php'; diff --git a/lib/register.php b/lib/register.php index 69674033c6a280..49144bf11fd6e7 100644 --- a/lib/register.php +++ b/lib/register.php @@ -461,21 +461,20 @@ function gutenberg_register_post_types() { register_post_type( 'wp_block', array( - 'labels' => array( + 'labels' => array( 'name' => 'Blocks', 'singular_name' => 'Block', ), - 'public' => false, - 'rewrite' => false, - 'show_in_rest' => true, - 'rest_base' => 'blocks', - 'rest_controller_class' => 'WP_REST_Blocks_Controller', - 'capability_type' => 'block', + 'public' => false, + 'rewrite' => false, + 'show_in_rest' => true, + 'rest_base' => 'blocks', + 'capability_type' => 'block', 'capabilities' => array( 'read' => 'read_blocks', 'create_posts' => 'create_blocks', ), - 'map_meta_cap' => true, + 'map_meta_cap' => true, ) ); diff --git a/packages/block-library/src/block/edit-panel/index.js b/packages/block-library/src/block/edit-panel/index.js index c971f5245d5dc4..211572109ca89b 100644 --- a/packages/block-library/src/block/edit-panel/index.js +++ b/packages/block-library/src/block/edit-panel/index.js @@ -5,7 +5,8 @@ import { Button } from '@wordpress/components'; import { Component, Fragment, createRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { ESCAPE } from '@wordpress/keycodes'; -import { withInstanceId } from '@wordpress/compose'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { withInstanceId, compose } from '@wordpress/compose'; class ReusableBlockEditPanel extends Component { constructor() { @@ -115,4 +116,38 @@ class ReusableBlockEditPanel extends Component { } } -export default withInstanceId( ReusableBlockEditPanel ); +export default compose( [ + withInstanceId, + withSelect( ( select ) => { + const { getEditedPostAttribute, isSavingPost } = select( 'core/editor' ); + + return { + title: getEditedPostAttribute( 'title' ), + isSaving: isSavingPost(), + }; + } ), + withDispatch( ( dispatch, ownProps ) => { + const { + editPost, + undoAll, + savePost, + clearSelectedBlock, + } = dispatch( 'core/editor' ); + + return { + onChangeTitle( title ) { + editPost( { title } ); + }, + onSave() { + clearSelectedBlock(); + savePost(); + ownProps.onSave(); + }, + onCancel() { + clearSelectedBlock(); + undoAll(); + ownProps.onCancel(); + }, + }; + } ), +] )( ReusableBlockEditPanel ); diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 15848857288649..9031c324d51896 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -1,16 +1,16 @@ -/** - * External dependencies - */ -import { noop, partial } from 'lodash'; - /** * WordPress dependencies */ -import { Component, Fragment } from '@wordpress/element'; +import { Component } from '@wordpress/element'; import { Placeholder, Spinner, Disabled } from '@wordpress/components'; -import { withSelect, withDispatch } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; -import { BlockEdit } from '@wordpress/editor'; +import { + withSelect, + withDispatch, + defaultRegistry, + RegistryProvider, +} from '@wordpress/data'; +import { EditorProvider, BlockList, createStore } from '@wordpress/editor'; +import isShallowEqual from '@wordpress/is-shallow-equal'; import { compose } from '@wordpress/compose'; /** @@ -18,163 +18,146 @@ import { compose } from '@wordpress/compose'; */ import ReusableBlockEditPanel from './edit-panel'; import ReusableBlockIndicator from './indicator'; +import ReusableBlockSelection from './selection'; class ReusableBlockEdit extends Component { - constructor( { reusableBlock } ) { + constructor( props ) { super( ...arguments ); - this.startEditing = this.startEditing.bind( this ); - this.stopEditing = this.stopEditing.bind( this ); - this.setAttributes = this.setAttributes.bind( this ); - this.setTitle = this.setTitle.bind( this ); - this.save = this.save.bind( this ); - - if ( reusableBlock && reusableBlock.isTemporary ) { - // Start in edit mode when we're working with a newly created reusable block - this.state = { - isEditing: true, - title: reusableBlock.title, - changedAttributes: {}, - }; - } else { - // Start in preview mode when we're working with an existing reusable block - this.state = { - isEditing: false, - title: null, - changedAttributes: null, - }; - } + this.startEdit = this.toggleIsEditing.bind( this, true ); + this.cancelEdit = this.toggleIsEditing.bind( this, false ); + this.saveEdit = this.saveEdit.bind( this ); + this.registry = defaultRegistry.clone(); + createStore( this.registry ); + + const { isTemporaryReusableBlock, settings } = props; + this.state = { + isEditing: isTemporaryReusableBlock, + settingsWithLock: { ...settings, templateLock: true }, + reusableBlockInstanceId: 0, + }; } - componentDidMount() { - if ( ! this.props.reusableBlock ) { - this.props.fetchReusableBlock(); + static getDerivedStateFromProps( props, prevState ) { + if ( isShallowEqual( props.settings, prevState.settings ) ) { + return null; } - } - - startEditing() { - const { reusableBlock } = this.props; - this.setState( { - isEditing: true, - title: reusableBlock.title, - changedAttributes: {}, - } ); - } - - stopEditing() { - this.setState( { - isEditing: false, - title: null, - changedAttributes: null, - } ); + return { + settings: props.settings, + settingsWithLock: { + ...props.settings, + templateLock: true, + }, + }; } - setAttributes( attributes ) { - this.setState( ( prevState ) => { - if ( prevState.changedAttributes !== null ) { - return { changedAttributes: { ...prevState.changedAttributes, ...attributes } }; - } - } ); + componentDidUpdate( prevProps ) { + if ( this.props.reusableBlock !== prevProps.reusableBlock ) { + this.setState( { reusableBlockInstanceId: this.state.reusableBlockInstanceId + 1 } ); + } } - setTitle( title ) { - this.setState( { title } ); + toggleIsEditing( isEditing ) { + this.setState( { isEditing } ); } - save() { - const { reusableBlock, onUpdateTitle, updateAttributes, block, onSave } = this.props; - const { title, changedAttributes } = this.state; + saveEdit() { + this.toggleIsEditing( false ); - if ( title !== reusableBlock.title ) { - onUpdateTitle( title ); - } + const { + getEditedPostAttribute, + getEditedPostContent, + getBlocks, + } = this.registry.select( 'core/editor' ); + + const reusableBlock = { + ...this.props.reusableBlock, + title: getEditedPostAttribute( 'title' ), + content: { + raw: getEditedPostContent(), + }, + }; - updateAttributes( block.clientId, changedAttributes ); - onSave(); + const [ parsedBlock ] = getBlocks(); - this.stopEditing(); + this.props.onSave( reusableBlock, parsedBlock ); } render() { - const { isSelected, reusableBlock, block, isFetching, isSaving } = this.props; - const { isEditing, title, changedAttributes } = this.state; + const { setIsSelected, reusableBlock, isSelected } = this.props; + const { settingsWithLock, isEditing, reusableBlockInstanceId } = this.state; - if ( ! reusableBlock && isFetching ) { + if ( ! reusableBlock ) { return ; } - if ( ! reusableBlock || ! block ) { - return { __( 'Block has been deleted or is unavailable.' ) }; - } - - let element = ( - - ); - + let list = ; if ( ! isEditing ) { - element = { element }; + list = { list }; } return ( - - { element } - { ( isSelected || isEditing ) && ( - - ) } - { ! isSelected && ! isEditing && } - + + + + { list } + { ( isSelected || isEditing ) && ( + + ) } + { ! isSelected && ! isEditing && } + + + ); } } export default compose( [ withSelect( ( select, ownProps ) => { - const { - getReusableBlock, - isFetchingReusableBlock, - isSavingReusableBlock, - getBlock, - } = select( 'core/editor' ); const { ref } = ownProps.attributes; - const reusableBlock = getReusableBlock( ref ); + const isTemporaryReusableBlock = ! Number.isFinite( ref ); + + let reusableBlock = null; + if ( ! isTemporaryReusableBlock ) { + reusableBlock = select( 'core' ).getEntityRecord( 'postType', 'wp_block', ref ); + } return { + isTemporaryReusableBlock, reusableBlock, - isFetching: isFetchingReusableBlock( ref ), - isSaving: isSavingReusableBlock( ref ), - block: reusableBlock ? getBlock( reusableBlock.clientId ) : null, + settings: select( 'core/editor' ).getEditorSettings(), }; } ), withDispatch( ( dispatch, ownProps ) => { - const { - fetchReusableBlocks, - updateBlockAttributes, - updateReusableBlockTitle, - saveReusableBlock, - } = dispatch( 'core/editor' ); - const { ref } = ownProps.attributes; - + const { selectBlock, receiveReusableBlocks } = dispatch( 'core/editor' ); + const { receiveEntityRecords } = dispatch( 'core' ); return { - fetchReusableBlock: partial( fetchReusableBlocks, ref ), - updateAttributes: updateBlockAttributes, - onUpdateTitle: partial( updateReusableBlockTitle, ref ), - onSave: partial( saveReusableBlock, ref ), + setIsSelected() { + selectBlock( ownProps.clientId ); + }, + onSave( reusableBlock, parsedBlock ) { + // Update the editor's store when the reusable block is changed. This + // ensures other instances of the same reusable block are updated. + receiveReusableBlocks( [ { reusableBlock, parsedBlock } ] ); + receiveEntityRecords( 'postType', 'wp_block', reusableBlock ); + }, }; } ), ] )( ReusableBlockEdit ); diff --git a/packages/block-library/src/block/indicator/index.js b/packages/block-library/src/block/indicator/index.js index 7e5310feceaddc..0919b739b5c965 100644 --- a/packages/block-library/src/block/indicator/index.js +++ b/packages/block-library/src/block/indicator/index.js @@ -3,6 +3,7 @@ */ import { Tooltip, Dashicon } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; +import { withSelect } from '@wordpress/data'; function ReusableBlockIndicator( { title } ) { // translators: %s: title/name of the reusable block @@ -16,4 +17,10 @@ function ReusableBlockIndicator( { title } ) { ); } -export default ReusableBlockIndicator; +export default withSelect( ( select ) => { + const { getEditedPostAttribute } = select( 'core/editor' ); + + return { + title: getEditedPostAttribute( 'title' ), + }; +} )( ReusableBlockIndicator ); diff --git a/packages/block-library/src/block/selection.js b/packages/block-library/src/block/selection.js new file mode 100644 index 00000000000000..532a5d401169b6 --- /dev/null +++ b/packages/block-library/src/block/selection.js @@ -0,0 +1,43 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { compose } from '@wordpress/compose'; + +class ReusableBlockSelection extends Component { + componentDidUpdate( prevProps ) { + const { + isReusableBlockSelected, + hasSelection, + clearSelectedBlock, + onBlockSelection, + } = this.props; + + if ( ! isReusableBlockSelected && prevProps.isReusableBlockSelected ) { + clearSelectedBlock(); + } + + if ( hasSelection && ! prevProps.hasSelection ) { + onBlockSelection(); + } + } + + render() { + return this.props.children; + } +} + +export default compose( [ + withSelect( ( select ) => { + const { getBlockSelectionStart } = select( 'core/editor' ); + + return { + hasSelection: !! getBlockSelectionStart(), + }; + } ), + withDispatch( ( dispatch ) => { + const { clearSelectedBlock } = dispatch( 'core/editor' ); + return { clearSelectedBlock }; + } ), +] )( ReusableBlockSelection ); diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index 00af63aa6e147d..b7783599aacda8 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -177,6 +177,10 @@ export function getEntityRecord( state, kind, name, key ) { return get( state.entities.data, [ kind, name, 'items', key ] ); } +export function isRequestingEntityRecord( ...args ) { + return isResolving( 'getEntityRecord', ...args ); +} + /** * Returns the Entity's records. * diff --git a/packages/data/src/index.js b/packages/data/src/index.js index d2405dd0fa4efe..daab8b19dc99df 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -17,7 +17,7 @@ export { restrictPersistence, setPersistenceStorage, } from './deprecated'; -export { plugins }; +export { plugins, defaultRegistry }; /** * The combineReducers helper function turns an object whose values are different diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 273389c556d672..0fc68a19bdfb00 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -144,7 +144,7 @@ export function createRegistry( storeConfigs = {} ) { enhancers.push( window.__REDUX_DEVTOOLS_EXTENSION__( { name: reducerKey, instanceId: reducerKey } ) ); } const store = createStore( reducer, flowRight( enhancers ) ); - namespaces[ reducerKey ] = { store, reducer }; + namespaces[ reducerKey ] = { store, reducer, _cloneConfig: { reducer } }; // Customize subscribe behavior to call listeners only on effective change, // not on every dispatch. @@ -175,6 +175,7 @@ export function createRegistry( storeConfigs = {} ) { const store = namespaces[ reducerKey ].store; const createStateSelector = ( selector ) => ( ...args ) => selector( store.getState(), ...args ); namespaces[ reducerKey ].selectors = mapValues( newSelectors, createStateSelector ); + namespaces[ reducerKey ]._cloneConfig.selectors = newSelectors; } /** @@ -253,6 +254,7 @@ export function createRegistry( storeConfigs = {} ) { }; namespaces[ reducerKey ].selectors = mapValues( namespaces[ reducerKey ].selectors, createResolver ); + namespaces[ reducerKey ]._cloneConfig.resolvers = newResolvers; } /** @@ -266,6 +268,7 @@ export function createRegistry( storeConfigs = {} ) { const store = namespaces[ reducerKey ].store; const createBoundAction = ( action ) => ( ...args ) => store.dispatch( action( ...args ) ); namespaces[ reducerKey ].actions = mapValues( newActions, createBoundAction ); + namespaces[ reducerKey ]._cloneConfig.actions = newActions; } /** @@ -368,6 +371,11 @@ export function createRegistry( storeConfigs = {} ) { } ); } + function clone() { + const storeCloneConfigs = mapValues( namespaces, '_cloneConfig' ); + return createRegistry( storeCloneConfigs ); + } + let registry = { registerReducer, registerSelectors, @@ -379,6 +387,7 @@ export function createRegistry( storeConfigs = {} ) { dispatch, setupPersistence, use, + clone, }; /** diff --git a/packages/editor/src/components/block-settings-menu/reusable-block-convert-button.js b/packages/editor/src/components/block-settings-menu/reusable-block-convert-button.js index 8178ef66162fbc..69b83c5372d7a8 100644 --- a/packages/editor/src/components/block-settings-menu/reusable-block-convert-button.js +++ b/packages/editor/src/components/block-settings-menu/reusable-block-convert-button.js @@ -49,7 +49,7 @@ export function ReusableBlockConvertButton( { export default compose( [ withSelect( ( select, { clientId } ) => { - const { getBlock, getReusableBlock } = select( 'core/editor' ); + const { getBlock } = select( 'core/editor' ); const { getFallbackBlockName } = select( 'core/blocks' ); const block = getBlock( clientId ); @@ -61,7 +61,7 @@ export default compose( [ // Hide 'Add to Reusable Blocks' on Classic blocks. Showing it causes a // confusing UX, because of its similarity to the 'Convert to Blocks' button. isVisible: block.name !== getFallbackBlockName(), - isStaticBlock: ! isReusableBlock( block ) || ! getReusableBlock( block.attributes.ref ), + isStaticBlock: ! isReusableBlock( block ), }; } ), withDispatch( ( dispatch, { clientId, onToggle = noop } ) => { diff --git a/packages/editor/src/components/block-settings-menu/reusable-block-delete-button.js b/packages/editor/src/components/block-settings-menu/reusable-block-delete-button.js index 537af329a9d3aa..757aa8c1ca7fe5 100644 --- a/packages/editor/src/components/block-settings-menu/reusable-block-delete-button.js +++ b/packages/editor/src/components/block-settings-menu/reusable-block-delete-button.js @@ -12,8 +12,8 @@ import { __ } from '@wordpress/i18n'; import { isReusableBlock } from '@wordpress/blocks'; import { withSelect, withDispatch } from '@wordpress/data'; -export function ReusableBlockDeleteButton( { reusableBlock, onDelete } ) { - if ( ! reusableBlock ) { +export function ReusableBlockDeleteButton( { isVisible, reusableBlockId, onDelete } ) { + if ( ! isVisible ) { return null; } @@ -21,8 +21,7 @@ export function ReusableBlockDeleteButton( { reusableBlock, onDelete } ) { onDelete( reusableBlock.id ) } + onClick={ () => onDelete( reusableBlockId ) } > { __( 'Remove from Reusable Blocks' ) } @@ -31,19 +30,23 @@ export function ReusableBlockDeleteButton( { reusableBlock, onDelete } ) { export default compose( [ withSelect( ( select, { clientId } ) => { - const { getBlock, getReusableBlock } = select( 'core/editor' ); + const { getBlock } = select( 'core/editor' ); + const block = getBlock( clientId ); + if ( ! block ) { + return { isVisible: false }; + } + return { - reusableBlock: block && isReusableBlock( block ) ? getReusableBlock( block.attributes.ref ) : null, + isVisible: isReusableBlock( block ), + reusableBlockId: block.attributes.ref, }; } ), withDispatch( ( dispatch, { onToggle = noop } ) => { - const { - deleteReusableBlock, - } = dispatch( 'core/editor' ); + const { deleteReusableBlock } = dispatch( 'core/editor' ); return { - onDelete( id ) { + onDelete( reusableBlockId ) { // TODO: Make this a component or similar // eslint-disable-next-line no-alert const hasConfirmed = window.confirm( __( @@ -52,7 +55,7 @@ export default compose( [ ) ); if ( hasConfirmed ) { - deleteReusableBlock( id ); + deleteReusableBlock( reusableBlockId ); onToggle(); } }, diff --git a/packages/editor/src/index.js b/packages/editor/src/index.js index 170617fc80f23d..03310e467e9f53 100644 --- a/packages/editor/src/index.js +++ b/packages/editor/src/index.js @@ -3,3 +3,4 @@ import './hooks'; export * from './components'; export * from './utils'; +export { createStore } from './store'; diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 52da09f02eeb36..4b10dde5bffdc2 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -108,22 +108,6 @@ export function resetBlocks( blocks ) { }; } -/** - * Returns an action object used in signalling that blocks have been received. - * Unlike resetBlocks, these should be appended to the existing known set, not - * replacing. - * - * @param {Object[]} blocks Array of block objects. - * - * @return {Object} Action object. - */ -export function receiveBlocks( blocks ) { - return { - type: 'RECEIVE_BLOCKS', - blocks, - }; -} - /** * Returns an action object used in signalling that the block attributes with * the specified client ID has been updated. @@ -468,6 +452,10 @@ export function undo() { return { type: 'UNDO' }; } +export function undoAll() { + return { type: 'UNDO_ALL' }; +} + /** * Returns an action object used in signalling that undo history record should * be created. @@ -596,18 +584,13 @@ export const createErrorNotice = partial( createNotice, 'error' ); export const createWarningNotice = partial( createNotice, 'warning' ); /** - * 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. + * Returns an action object used to fetch all reusable blocks from the REST API. * * @return {Object} Action object. */ -export function fetchReusableBlocks( id ) { +export function fetchReusableBlocks() { return { type: 'FETCH_REUSABLE_BLOCKS', - id, }; } @@ -629,17 +612,18 @@ export function receiveReusableBlocks( results ) { } /** - * Returns an action object used to save a reusable block that's in the store to - * the REST API. + * Returns an action object used to create a new reusable block. * - * @param {Object} id The ID of the reusable block to save. + * @param {Object} reusableBlock Temporary reusable block to be updated once saved. + * @param {string} content Content of reusable block. * * @return {Object} Action object. */ -export function saveReusableBlock( id ) { +export function saveReusableBlock( reusableBlock, content ) { return { type: 'SAVE_REUSABLE_BLOCK', - id, + reusableBlock, + content, }; } @@ -657,23 +641,6 @@ export function deleteReusableBlock( id ) { }; } -/** - * Returns an action object used in signalling that a reusable block's title is - * to be updated. - * - * @param {number} id The ID of the reusable block to update. - * @param {string} title The new title. - * - * @return {Object} Action object. - */ -export function updateReusableBlockTitle( id, title ) { - return { - type: 'UPDATE_REUSABLE_BLOCK_TITLE', - id, - title, - }; -} - /** * Returns an action object used to convert a reusable block into a static block. * diff --git a/packages/editor/src/store/effects.js b/packages/editor/src/store/effects.js index 6b61df24bb734d..e7b73e9aa905f4 100644 --- a/packages/editor/src/store/effects.js +++ b/packages/editor/src/store/effects.js @@ -42,11 +42,10 @@ import { } from './selectors'; import { fetchReusableBlocks, - saveReusableBlocks, + saveReusableBlock, deleteReusableBlocks, convertBlockToReusable, convertBlockToStatic, - receiveReusableBlocks, } from './effects/reusable-blocks'; import { requestPostUpdate, @@ -200,12 +199,11 @@ export default { fetchReusableBlocks( action, store ); }, SAVE_REUSABLE_BLOCK: ( action, store ) => { - saveReusableBlocks( action, store ); + saveReusableBlock( action, store ); }, DELETE_REUSABLE_BLOCK: ( action, store ) => { deleteReusableBlocks( action, store ); }, - RECEIVE_REUSABLE_BLOCKS: receiveReusableBlocks, CONVERT_BLOCK_TO_STATIC: convertBlockToStatic, CONVERT_BLOCK_TO_REUSABLE: convertBlockToReusable, CREATE_NOTICE( { notice: { content, spokenMessage } } ) { diff --git a/packages/editor/src/store/effects/reusable-blocks.js b/packages/editor/src/store/effects/reusable-blocks.js index 27dd4e86696f7c..1baf305938ec54 100644 --- a/packages/editor/src/store/effects/reusable-blocks.js +++ b/packages/editor/src/store/effects/reusable-blocks.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { castArray, map, uniqueId } from 'lodash'; +import { map, uniqueId } from 'lodash'; import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; /** @@ -15,6 +15,7 @@ import { isReusableBlock, } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; +import { select } from '@wordpress/data'; /** * Internal dependencies @@ -26,11 +27,9 @@ import { createErrorNotice, removeBlocks, replaceBlock, - receiveBlocks, - saveReusableBlock, + saveReusableBlock as saveReusableBlockAction, } from '../actions'; import { - getReusableBlock, getBlock, getBlocks, } from '../selectors'; @@ -47,7 +46,6 @@ const REUSABLE_BLOCK_NOTICE_ID = 'REUSABLE_BLOCK_NOTICE_ID'; * @param {Object} store Redux Store. */ export const fetchReusableBlocks = async ( action, store ) => { - const { id } = action; const { dispatch } = store; // TODO: these are potentially undefined, this fix is in place @@ -57,43 +55,36 @@ export const fetchReusableBlocks = async ( action, store ) => { return; } - let result; - if ( id ) { - result = apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ id }` } ); - } else { - result = apiFetch( { path: `/wp/v2/${ postType.rest_base }?per_page=-1` } ); - } - try { - const reusableBlockOrBlocks = await result; + const reusableBlocks = await apiFetch( { + path: `/wp/v2/${ postType.rest_base }?context=edit&per_page=-1`, + } ); dispatch( receiveReusableBlocksAction( map( - castArray( reusableBlockOrBlocks ), + reusableBlocks, ( reusableBlock ) => ( { reusableBlock, - parsedBlock: parse( reusableBlock.content )[ 0 ], + parsedBlock: parse( reusableBlock.content.raw )[ 0 ], } ) ) ) ); dispatch( { type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', - id, } ); } catch ( error ) { dispatch( { type: 'FETCH_REUSABLE_BLOCKS_FAILURE', - id, error, } ); } }; /** - * Save Reusable Blocks Effect Handler. + * Save Reusable Block Effect Handler. * * @param {Object} action action object. * @param {Object} store Redux Store. */ -export const saveReusableBlocks = async ( action, store ) => { +export const saveReusableBlock = async ( action, store ) => { // TODO: these are potentially undefined, this fix is in place // until there is a filter to not use reusable blocks if undefined const postType = await resolveSelector( 'core', 'getPostType', 'wp_block' ); @@ -101,16 +92,13 @@ export const saveReusableBlocks = async ( action, store ) => { return; } - const { id } = action; + const { reusableBlock, content } = action; + const { id, title } = reusableBlock; const { dispatch } = store; - const state = store.getState(); - const { clientId, title, isTemporary } = getReusableBlock( state, id ); - const { name, attributes, innerBlocks } = getBlock( state, clientId ); - const content = serialize( createBlock( name, attributes, innerBlocks ) ); - const data = isTemporary ? { title, content } : { id, title, content }; - const path = isTemporary ? `/wp/v2/${ postType.rest_base }` : `/wp/v2/${ postType.rest_base }/${ id }`; - const method = isTemporary ? 'POST' : 'PUT'; + const data = { title, content, status: 'publish' }; + const path = `/wp/v2/${ postType.rest_base }`; + const method = 'POST'; try { const updatedReusableBlock = await apiFetch( { path, data, method } ); @@ -119,14 +107,19 @@ export const saveReusableBlocks = async ( action, store ) => { updatedId: updatedReusableBlock.id, id, } ); - const message = isTemporary ? __( 'Block created.' ) : __( 'Block updated.' ); - dispatch( createSuccessNotice( message, { id: REUSABLE_BLOCK_NOTICE_ID } ) ); + dispatch( createSuccessNotice( +

{ __( 'Block created.' ) }

, + { id: REUSABLE_BLOCK_NOTICE_ID } + ) ); } catch ( error ) { dispatch( { type: 'SAVE_REUSABLE_BLOCK_FAILURE', id } ); - dispatch( createErrorNotice( error.message, { - id: REUSABLE_BLOCK_NOTICE_ID, - spokenMessage: error.message, - } ) ); + dispatch( createErrorNotice( +

{ error.message }

, + { + id: REUSABLE_BLOCK_NOTICE_ID, + spokenMessage: error.message, + } + ) ); } }; @@ -147,16 +140,11 @@ export const deleteReusableBlocks = async ( action, store ) => { const { id } = action; const { getState, dispatch } = store; - // Don't allow a reusable block with a temporary ID to be deleted - const reusableBlock = getReusableBlock( getState(), id ); - if ( ! reusableBlock || reusableBlock.isTemporary ) { - return; - } - - // Remove any other blocks that reference this reusable block + // Remove any blocks that reference this reusable block const allBlocks = getBlocks( getState() ); const associatedBlocks = allBlocks.filter( ( block ) => isReusableBlock( block ) && block.attributes.ref === id ); const associatedBlockClientIds = associatedBlocks.map( ( block ) => block.clientId ); + dispatch( removeBlocks( associatedBlockClientIds ) ); const transactionId = uniqueId(); @@ -166,44 +154,36 @@ export const deleteReusableBlocks = async ( action, store ) => { optimist: { type: BEGIN, id: transactionId }, } ); - // Remove the parsed block. - dispatch( removeBlocks( [ - ...associatedBlockClientIds, - reusableBlock.clientId, - ] ) ); - try { - await apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ id }`, method: 'DELETE' } ); + await apiFetch( { + path: `/wp/v2/${ postType.rest_base }/${ id }?force=true`, + method: 'DELETE', + } ); dispatch( { type: 'DELETE_REUSABLE_BLOCK_SUCCESS', id, optimist: { type: COMMIT, id: transactionId }, } ); - const message = __( 'Block deleted.' ); - dispatch( createSuccessNotice( message, { id: REUSABLE_BLOCK_NOTICE_ID } ) ); + dispatch( createSuccessNotice( +

{ __( 'Block deleted.' ) }

, + { id: REUSABLE_BLOCK_NOTICE_ID } + ) ); } catch ( error ) { dispatch( { type: 'DELETE_REUSABLE_BLOCK_FAILURE', id, optimist: { type: REVERT, id: transactionId }, } ); - dispatch( createErrorNotice( error.message, { - id: REUSABLE_BLOCK_NOTICE_ID, - spokenMessage: error.message, - } ) ); + dispatch( createErrorNotice( +

{ error.message }

, + { + id: REUSABLE_BLOCK_NOTICE_ID, + spokenMessage: error.message, + } + ) ); } }; -/** - * Receive Reusable Blocks Effect Handler. - * - * @param {Object} action action object. - * @return {Object} receive blocks action - */ -export const receiveReusableBlocks = ( action ) => { - return receiveBlocks( map( action.results, 'parsedBlock' ) ); -}; - /** * Convert a reusable block to a static block effect handler * @@ -213,9 +193,9 @@ export const receiveReusableBlocks = ( action ) => { export const convertBlockToStatic = ( action, store ) => { const state = store.getState(); const oldBlock = getBlock( state, action.clientId ); - const reusableBlock = getReusableBlock( state, oldBlock.attributes.ref ); - const referencedBlock = getBlock( state, reusableBlock.clientId ); - const newBlock = createBlock( referencedBlock.name, referencedBlock.attributes ); + const { getEntityRecord } = select( 'core' ); + const reusableBlock = getEntityRecord( 'postType', 'wp_block', oldBlock.attributes.ref ); + const [ newBlock ] = parse( reusableBlock.content.raw ); store.dispatch( replaceBlock( oldBlock.clientId, newBlock ) ); }; @@ -229,6 +209,7 @@ export const convertBlockToReusable = ( action, store ) => { const { getState, dispatch } = store; const parsedBlock = getBlock( getState(), action.clientId ); + const content = serialize( parsedBlock ); const reusableBlock = { id: uniqueId( 'reusable' ), clientId: parsedBlock.clientId, @@ -240,7 +221,7 @@ export const convertBlockToReusable = ( action, store ) => { parsedBlock, } ] ) ); - dispatch( saveReusableBlock( reusableBlock.id ) ); + dispatch( saveReusableBlockAction( reusableBlock, content ) ); dispatch( replaceBlock( parsedBlock.clientId, @@ -249,7 +230,4 @@ export const convertBlockToReusable = ( action, store ) => { layout: parsedBlock.attributes.layout, } ) ) ); - - // Re-add the original block to the store, since replaceBlock() will have removed it - dispatch( receiveBlocks( [ parsedBlock ] ) ); }; diff --git a/packages/editor/src/store/effects/test/reusable-blocks.js b/packages/editor/src/store/effects/test/reusable-blocks.js index 2ff4081a650a79..48f1e1150ae848 100644 --- a/packages/editor/src/store/effects/test/reusable-blocks.js +++ b/packages/editor/src/store/effects/test/reusable-blocks.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { noop, reduce } from 'lodash'; +import { noop } from 'lodash'; /** * WordPress dependencies @@ -10,7 +10,6 @@ import apiFetch from '@wordpress/api-fetch'; import { registerBlockType, unregisterBlockType, - createBlock, } from '@wordpress/blocks'; import '@wordpress/core-data'; // Needed to load the core store @@ -19,24 +18,11 @@ import '@wordpress/core-data'; // Needed to load the core store */ import { fetchReusableBlocks, - saveReusableBlocks, - receiveReusableBlocks, - deleteReusableBlocks, - convertBlockToStatic, - convertBlockToReusable, } from '../reusable-blocks'; import { - resetBlocks, - receiveBlocks, - saveReusableBlock, - deleteReusableBlock, - removeBlocks, - convertBlockToReusable as convertBlockToReusableAction, - convertBlockToStatic as convertBlockToStaticAction, receiveReusableBlocks as receiveReusableBlocksAction, fetchReusableBlocks as fetchReusableBlocksAction, } from '../../actions'; -import reducer from '../../reducer'; jest.mock( '@wordpress/api-fetch', () => jest.fn() ); @@ -70,8 +56,12 @@ describe( 'reusable blocks effects', () => { const blockPromise = Promise.resolve( [ { id: 123, - title: 'My cool block', - content: '', + title: { + raw: 'My cool block', + }, + content: { + raw: '', + }, }, ] ); const postTypePromise = Promise.resolve( { @@ -96,8 +86,12 @@ describe( 'reusable blocks effects', () => { { reusableBlock: { id: 123, - title: 'My cool block', - content: '', + title: { + raw: 'My cool block', + }, + content: { + raw: '', + }, }, parsedBlock: expect.objectContaining( { name: 'core/test-block', @@ -112,50 +106,6 @@ describe( 'reusable blocks effects', () => { } ); } ); - it( 'should fetch a single reusable block', async () => { - const blockPromise = Promise.resolve( { - id: 123, - title: 'My cool block', - content: '', - } ); - const postTypePromise = Promise.resolve( { - slug: 'wp_block', rest_base: 'blocks', - } ); - - apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block?context=edit' ) { - return postTypePromise; - } - - return blockPromise; - } ); - - const dispatch = jest.fn(); - const store = { getState: noop, dispatch }; - - await fetchReusableBlocks( fetchReusableBlocksAction( 123 ), store ); - - expect( dispatch ).toHaveBeenCalledWith( - receiveReusableBlocksAction( [ - { - reusableBlock: { - id: 123, - title: 'My cool block', - content: '', - }, - parsedBlock: expect.objectContaining( { - name: 'core/test-block', - attributes: { name: 'Big Bird' }, - } ), - }, - ] ) - ); - expect( dispatch ).toHaveBeenCalledWith( { - type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', - id: 123, - } ); - } ); - it( 'should handle an API error', async () => { const blockPromise = Promise.reject( { code: 'unknown_error', @@ -187,259 +137,4 @@ describe( 'reusable blocks effects', () => { } ); } ); } ); - - describe( 'saveReusableBlocks', () => { - it( 'should save a reusable block and swap its id', async () => { - const savePromise = Promise.resolve( { id: 456 } ); - const postTypePromise = Promise.resolve( { - slug: 'wp_block', rest_base: 'blocks', - } ); - - apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block?context=edit' ) { - return postTypePromise; - } - - return savePromise; - } ); - - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reduce( [ - receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - await saveReusableBlocks( saveReusableBlock( 123 ), store ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'SAVE_REUSABLE_BLOCK_SUCCESS', - id: 123, - updatedId: 456, - } ); - } ); - - it( 'should handle an API error', async () => { - const savePromise = Promise.reject( {} ); - const postTypePromise = Promise.resolve( { - slug: 'wp_block', rest_base: 'blocks', - } ); - - apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block?context=edit' ) { - return postTypePromise; - } - - return savePromise; - } ); - - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reduce( [ - receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - await saveReusableBlocks( saveReusableBlock( 123 ), store ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'SAVE_REUSABLE_BLOCK_FAILURE', - id: 123, - } ); - } ); - } ); - - describe( 'receiveReusableBlocks', () => { - it( 'should receive parsed blocks', () => { - const action = receiveReusableBlocksAction( [ - { - parsedBlock: { clientId: 'broccoli' }, - }, - ] ); - - expect( receiveReusableBlocks( action ) ).toEqual( receiveBlocks( [ - { clientId: 'broccoli' }, - ] ) ); - } ); - } ); - - describe( 'deleteReusableBlocks', () => { - it( 'should delete a reusable block', async () => { - const deletePromise = Promise.resolve( {} ); - const postTypePromise = Promise.resolve( { - slug: 'wp_block', rest_base: 'blocks', - } ); - - apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block?context=edit' ) { - return postTypePromise; - } - - return deletePromise; - } ); - - const associatedBlock = createBlock( 'core/block', { ref: 123 } ); - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reduce( [ - resetBlocks( [ associatedBlock ] ), - receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - await deleteReusableBlocks( deleteReusableBlock( 123 ), store ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'REMOVE_REUSABLE_BLOCK', - id: 123, - optimist: expect.any( Object ), - } ); - - expect( dispatch ).toHaveBeenCalledWith( - removeBlocks( [ associatedBlock.clientId, parsedBlock.clientId ] ) - ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'DELETE_REUSABLE_BLOCK_SUCCESS', - id: 123, - optimist: expect.any( Object ), - } ); - } ); - - it( 'should handle an API error', async () => { - const deletePromise = Promise.reject( {} ); - const postTypePromise = Promise.resolve( { - slug: 'wp_block', rest_base: 'blocks', - } ); - - apiFetch.mockImplementation( ( options ) => { - if ( options.path === '/wp/v2/types/wp_block?context=edit' ) { - return postTypePromise; - } - - return deletePromise; - } ); - - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reduce( [ - receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - await deleteReusableBlocks( deleteReusableBlock( 123 ), store ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'DELETE_REUSABLE_BLOCK_FAILURE', - id: 123, - optimist: expect.any( Object ), - } ); - } ); - - it( 'should not save reusable blocks with temporary IDs', async () => { - const reusableBlock = { id: 'reusable1', title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reduce( [ - receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - await deleteReusableBlocks( deleteReusableBlock( 'reusable1' ), store ); - - expect( dispatch ).not.toHaveBeenCalled(); - } ); - } ); - - describe( 'convertBlockToStatic', () => { - it( 'should convert a reusable block into a static block', () => { - const associatedBlock = createBlock( 'core/block', { ref: 123 } ); - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reduce( [ - resetBlocks( [ associatedBlock ] ), - receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - convertBlockToStatic( convertBlockToStaticAction( associatedBlock.clientId ), store ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'REPLACE_BLOCKS', - clientIds: [ associatedBlock.clientId ], - blocks: [ - expect.objectContaining( { - name: 'core/test-block', - attributes: { name: 'Big Bird' }, - } ), - ], - time: expect.any( Number ), - } ); - } ); - } ); - - describe( 'convertBlockToReusable', () => { - it( 'should convert a static block into a reusable block', () => { - const staticBlock = createBlock( 'core/block', { ref: 123 } ); - const state = reducer( undefined, resetBlocks( [ staticBlock ] ) ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - convertBlockToReusable( convertBlockToReusableAction( staticBlock.clientId ), store ); - - expect( dispatch ).toHaveBeenCalledWith( - receiveReusableBlocksAction( [ { - reusableBlock: { - id: expect.stringMatching( /^reusable/ ), - clientId: staticBlock.clientId, - title: 'Untitled Reusable Block', - }, - parsedBlock: staticBlock, - } ] ) - ); - - expect( dispatch ).toHaveBeenCalledWith( - saveReusableBlock( expect.stringMatching( /^reusable/ ) ), - ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'REPLACE_BLOCKS', - clientIds: [ staticBlock.clientId ], - blocks: [ - expect.objectContaining( { - name: 'core/block', - attributes: { ref: expect.stringMatching( /^reusable/ ) }, - } ), - ], - time: expect.any( Number ), - } ); - - expect( dispatch ).toHaveBeenCalledWith( - receiveBlocks( [ staticBlock ] ), - ); - } ); - } ); } ); diff --git a/packages/editor/src/store/index.js b/packages/editor/src/store/index.js index 542ab0a908b56d..65909858960b46 100644 --- a/packages/editor/src/store/index.js +++ b/packages/editor/src/store/index.js @@ -6,7 +6,7 @@ import { forOwn } from 'lodash'; /** * WordPress Dependencies */ -import { registerStore } from '@wordpress/data'; +import { defaultRegistry } from '@wordpress/data'; /** * Internal dependencies @@ -23,20 +23,22 @@ import { validateTokenSettings } from '../components/rich-text/tokens'; */ const MODULE_KEY = 'core/editor'; -const store = registerStore( MODULE_KEY, { - reducer, - selectors, - actions, - persist: [ 'preferences' ], -} ); -applyMiddlewares( store ); +export function createStore( registry ) { + const store = registry.registerStore( MODULE_KEY, { + reducer, + selectors, + actions, + persist: [ 'preferences' ], + } ); + applyMiddlewares( store ); -forOwn( tokens, ( { name, settings } ) => { - settings = validateTokenSettings( name, settings, store.getState() ); + forOwn( tokens, ( { name, settings } ) => { + settings = validateTokenSettings( name, settings, store.getState() ); - if ( settings ) { - store.dispatch( actions.registerToken( name, settings ) ); - } -} ); + if ( settings ) { + store.dispatch( actions.registerToken( name, settings ) ); + } + } ); +} -export default store; +export default createStore( defaultRegistry ); diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 09d58a57be956f..273d6e5b96b686 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -216,7 +216,7 @@ export const editor = flow( [ // Track undo history, starting at editor initialization. withHistory( { resetTypes: [ 'SETUP_EDITOR_STATE' ], - ignoreTypes: [ 'RECEIVE_BLOCKS', 'RESET_POST', 'UPDATE_POST' ], + ignoreTypes: [ 'RESET_POST', 'UPDATE_POST' ], shouldOverwriteState, } ), @@ -224,7 +224,7 @@ export const editor = flow( [ // editor initialization firing post reset as an effect. withChangeDetection( { resetTypes: [ 'SETUP_EDITOR_STATE', 'REQUEST_POST_UPDATE_START' ], - ignoreTypes: [ 'RECEIVE_BLOCKS', 'RESET_POST', 'UPDATE_POST' ], + ignoreTypes: [ 'RESET_POST', 'UPDATE_POST' ], } ), ] )( { edits( state = {}, action ) { @@ -285,12 +285,6 @@ export const editor = flow( [ case 'SETUP_EDITOR_STATE': return getFlattenedBlocks( action.blocks ); - case 'RECEIVE_BLOCKS': - return { - ...state, - ...getFlattenedBlocks( action.blocks ), - }; - case 'UPDATE_BLOCK_ATTRIBUTES': // Ignore updates if block isn't known if ( ! state[ action.clientId ] ) { @@ -409,12 +403,6 @@ export const editor = flow( [ case 'SETUP_EDITOR_STATE': return mapBlockOrder( action.blocks ); - case 'RECEIVE_BLOCKS': - return { - ...state, - ...omit( mapBlockOrder( action.blocks ), '' ), - }; - case 'INSERT_BLOCKS': { const { rootClientId = '', blocks } = action; const subState = state[ rootClientId ] || []; @@ -881,10 +869,11 @@ export const reusableBlocks = combineReducers( { switch ( action.type ) { case 'RECEIVE_REUSABLE_BLOCKS': { return reduce( action.results, ( nextState, result ) => { - const { id, title } = result.reusableBlock; - const { clientId } = result.parsedBlock; + const { id } = result.reusableBlock; + const title = getPostRawValue( result.reusableBlock.title ); + const { name: blockName } = result.parsedBlock; - const value = { clientId, title }; + const value = { blockName, title }; if ( ! isEqual( nextState[ id ], value ) ) { if ( nextState === state ) { @@ -898,22 +887,6 @@ export const reusableBlocks = combineReducers( { }, state ); } - case 'UPDATE_REUSABLE_BLOCK_TITLE': { - const { id, title } = action; - - if ( ! state[ id ] || state[ id ].title === title ) { - return state; - } - - return { - ...state, - [ id ]: { - ...state[ id ], - title, - }, - }; - } - case 'SAVE_REUSABLE_BLOCK_SUCCESS': { const { id, updatedId } = action; @@ -937,48 +910,6 @@ export const reusableBlocks = combineReducers( { return state; }, - - isFetching( state = {}, action ) { - switch ( action.type ) { - case 'FETCH_REUSABLE_BLOCKS': { - const { id } = action; - if ( ! id ) { - return state; - } - - return { - ...state, - [ id ]: true, - }; - } - - case 'FETCH_REUSABLE_BLOCKS_SUCCESS': - case 'FETCH_REUSABLE_BLOCKS_FAILURE': { - const { id } = action; - 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; - }, } ); /** diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 3af261f5fde081..22424bb8d28448 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1604,12 +1604,7 @@ export const getInserterItems = createSelector( return false; } - const referencedBlock = getBlock( state, reusableBlock.clientId ); - if ( ! referencedBlock ) { - return false; - } - - const referencedBlockType = getBlockType( referencedBlock.name ); + const referencedBlockType = getBlockType( reusableBlock.blockName ); if ( ! referencedBlockType ) { return false; } @@ -1624,8 +1619,7 @@ export const getInserterItems = createSelector( const buildReusableBlockInserterItem = ( reusableBlock ) => { const id = `core/block/${ reusableBlock.id }`; - const referencedBlock = getBlock( state, reusableBlock.clientId ); - const referencedBlockType = getBlockType( referencedBlock.name ); + const referencedBlockType = getBlockType( reusableBlock.blockName ); const { time, count = 0 } = getInsertUsage( state, id ) || {}; const utility = calculateUtility( 'reusable', count, false ); diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 1346a5477aa43c..db5b8b3914a1d7 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -466,20 +466,12 @@ describe( 'actions', () => { type: 'FETCH_REUSABLE_BLOCKS', } ); } ); - - it( 'should take an optional id argument', () => { - expect( fetchReusableBlocks( 123 ) ).toEqual( { - type: 'FETCH_REUSABLE_BLOCKS', - id: 123, - } ); - } ); } ); describe( 'saveReusableBlock', () => { it( 'should return the SAVE_REUSABLE_BLOCK action', () => { - expect( saveReusableBlock( 123 ) ).toEqual( { + expect( saveReusableBlock() ).toEqual( { type: 'SAVE_REUSABLE_BLOCK', - id: 123, } ); } ); } ); diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 9445b32a11e7ca..4030f935b454ed 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -1844,31 +1844,7 @@ describe( 'state', () => { } ); } ); - it( 'should update a reusable block', () => { - const initialState = { - data: { - 123: { clientId: '', title: '' }, - }, - isFetching: {}, - isSaving: {}, - }; - - const state = reusableBlocks( initialState, { - type: 'UPDATE_REUSABLE_BLOCK_TITLE', - id: 123, - 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", () => { + it( 'should update the reusable block\'s id if it was temporary', () => { const initialState = { data: { reusable1: { clientId: '', title: '' }, diff --git a/packages/editor/src/utils/with-history/index.js b/packages/editor/src/utils/with-history/index.js index 9a6bf045df94c7..77fc8c82a1cfc5 100644 --- a/packages/editor/src/utils/with-history/index.js +++ b/packages/editor/src/utils/with-history/index.js @@ -61,6 +61,17 @@ const withHistory = ( options = {} ) => ( reducer ) => { const previousAction = lastAction; switch ( action.type ) { + case 'UNDO_ALL': + // Can't undo if no past. + if ( ! past.length ) { + return state; + } + + return { + past: [], + present: first( past ), + future: [], + }; case 'UNDO': // Can't undo if no past. if ( ! past.length ) { diff --git a/phpunit/class-rest-blocks-controller-test.php b/phpunit/class-rest-blocks-controller-test.php deleted file mode 100644 index 7b92f200a2afce..00000000000000 --- a/phpunit/class-rest-blocks-controller-test.php +++ /dev/null @@ -1,341 +0,0 @@ - 'wp_block', - 'post_status' => 'publish', - 'post_title' => 'My cool block', - 'post_content' => '

Hello!

', - ) - ); - - self::$user_id = $factory->user->create( - array( - 'role' => 'editor', - ) - ); - } - - /** - * Delete our fake data after our tests run. - */ - public static function wpTearDownAfterClass() { - wp_delete_post( self::$post_id ); - - self::delete_user( self::$user_id ); - } - - /** - * Check that our routes get set up properly. - */ - public function test_register_routes() { - $routes = rest_get_server()->get_routes(); - - $this->assertArrayHasKey( '/wp/v2/blocks', $routes ); - $this->assertCount( 2, $routes['/wp/v2/blocks'] ); - $this->assertArrayHasKey( '/wp/v2/blocks/(?P[\d]+)', $routes ); - $this->assertCount( 3, $routes['/wp/v2/blocks/(?P[\d]+)'] ); - } - - /** - * Check that we can GET a collection of blocks. - */ - public function test_get_items() { - wp_set_current_user( self::$user_id ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/blocks' ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( - array( - array( - 'id' => self::$post_id, - 'title' => 'My cool block', - 'content' => '

Hello!

', - ), - ), - $response->get_data() - ); - } - - /** - * Check that we can GET a single block. - */ - public function test_get_item() { - wp_set_current_user( self::$user_id ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/blocks/' . self::$post_id ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( - array( - 'id' => self::$post_id, - 'title' => 'My cool block', - 'content' => '

Hello!

', - ), - $response->get_data() - ); - } - - /** - * Check that we can POST to create a new block. - */ - public function test_create_item() { - wp_set_current_user( self::$user_id ); - - $request = new WP_REST_Request( 'POST', '/wp/v2/blocks/' . self::$post_id ); - $request->set_body_params( - array( - 'title' => 'New cool block', - 'content' => '

Wow!

', - ) - ); - - $response = rest_get_server()->dispatch( $request ); - - $this->assertEquals( 200, $response->get_status() ); - - $data = $response->get_data(); - - $this->assertArrayHasKey( 'id', $data ); - $this->assertArrayHasKey( 'title', $data ); - $this->assertArrayHasKey( 'content', $data ); - - $this->assertEquals( self::$post_id, $data['id'] ); - $this->assertEquals( 'New cool block', $data['title'] ); - $this->assertEquals( '

Wow!

', $data['content'] ); - } - - /** - * Check that we can PUT to update a block. - */ - public function test_update_item() { - wp_set_current_user( self::$user_id ); - - $request = new WP_REST_Request( 'PUT', '/wp/v2/blocks/' . self::$post_id ); - $request->set_body_params( - array( - 'title' => 'Updated cool block', - 'content' => '

Nice!

', - ) - ); - - $response = rest_get_server()->dispatch( $request ); - - $this->assertEquals( 200, $response->get_status() ); - - $data = $response->get_data(); - - $this->assertArrayHasKey( 'id', $data ); - $this->assertArrayHasKey( 'title', $data ); - $this->assertArrayHasKey( 'content', $data ); - - $this->assertEquals( self::$post_id, $data['id'] ); - $this->assertEquals( 'Updated cool block', $data['title'] ); - $this->assertEquals( '

Nice!

', $data['content'] ); - } - - /** - * Check that we can DELETE a block. - */ - public function test_delete_item() { - wp_set_current_user( self::$user_id ); - - $request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . self::$post_id ); - - $response = rest_get_server()->dispatch( $request ); - - $this->assertEquals( 200, $response->get_status() ); - - $data = $response->get_data(); - - $this->assertArrayHasKey( 'deleted', $data ); - $this->assertArrayHasKey( 'previous', $data ); - - $this->assertTrue( $data['deleted'] ); - - $this->assertArrayHasKey( 'id', $data['previous'] ); - $this->assertArrayHasKey( 'title', $data['previous'] ); - $this->assertArrayHasKey( 'content', $data['previous'] ); - - $this->assertEquals( self::$post_id, $data['previous']['id'] ); - $this->assertEquals( 'My cool block', $data['previous']['title'] ); - $this->assertEquals( '

Hello!

', $data['previous']['content'] ); - } - - /** - * Check that we have defined a JSON schema. - */ - public function test_get_item_schema() { - $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/blocks' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $properties = $data['schema']['properties']; - - $this->assertEquals( 3, count( $properties ) ); - $this->assertArrayHasKey( 'id', $properties ); - $this->assertArrayHasKey( 'title', $properties ); - $this->assertArrayHasKey( 'content', $properties ); - } - - /** - * Test cases for test_capabilities(). - */ - public function data_capabilities() { - return array( - array( 'create', 'editor', 201 ), - array( 'create', 'author', 201 ), - array( 'create', 'contributor', 403 ), - array( 'create', null, 401 ), - - array( 'read', 'editor', 200 ), - array( 'read', 'author', 200 ), - array( 'read', 'contributor', 200 ), - array( 'read', null, 401 ), - - array( 'update_delete_own', 'editor', 200 ), - array( 'update_delete_own', 'author', 200 ), - array( 'update_delete_own', 'contributor', 403 ), - - array( 'update_delete_others', 'editor', 200 ), - array( 'update_delete_others', 'author', 403 ), - array( 'update_delete_others', 'contributor', 403 ), - array( 'update_delete_others', null, 401 ), - ); - } - - /** - * Exhaustively check that each role either can or cannot create, edit, - * update, and delete reusable blocks. - * - * @dataProvider data_capabilities - */ - public function test_capabilities( $action, $role, $expected_status ) { - if ( $role ) { - $user_id = $this->factory->user->create( array( 'role' => $role ) ); - wp_set_current_user( $user_id ); - } else { - wp_set_current_user( 0 ); - } - - switch ( $action ) { - case 'create': - $request = new WP_REST_Request( 'POST', '/wp/v2/blocks' ); - $request->set_body_params( - array( - 'title' => 'Test', - 'content' => '

Test

', - ) - ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - break; - - case 'read': - $request = new WP_REST_Request( 'GET', '/wp/v2/blocks/' . self::$post_id ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - break; - - case 'update_delete_own': - $post_id = wp_insert_post( - array( - 'post_type' => 'wp_block', - 'post_status' => 'publish', - 'post_title' => 'My cool block', - 'post_content' => '

Hello!

', - 'post_author' => $user_id, - ) - ); - - $request = new WP_REST_Request( 'PUT', '/wp/v2/blocks/' . $post_id ); - $request->set_body_params( - array( - 'title' => 'Test', - 'content' => '

Test

', - ) - ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - $request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . $post_id ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - wp_delete_post( $post_id ); - - break; - - case 'update_delete_others': - $request = new WP_REST_Request( 'PUT', '/wp/v2/blocks/' . self::$post_id ); - $request->set_body_params( - array( - 'title' => 'Test', - 'content' => '

Test

', - ) - ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - $request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . self::$post_id ); - - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( $expected_status, $response->get_status() ); - - break; - - default: - $this->fail( "'$action' is not a valid action." ); - } - - if ( isset( $user_id ) ) { - self::delete_user( $user_id ); - } - } - - public function test_context_param() { - $this->markTestSkipped( 'Controller doesn\'t implement get_context_param().' ); - } - public function test_prepare_item() { - $this->markTestSkipped( 'Controller doesn\'t implement prepare_item().' ); - } -}