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
{ __( '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/(?PHello!
', - ), - ), - $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().' ); - } -}