diff --git a/core-blocks/block/edit-panel/index.js b/core-blocks/block/edit-panel/index.js index 88b733d0ce63df..5d494d5a9ec550 100644 --- a/core-blocks/block/edit-panel/index.js +++ b/core-blocks/block/edit-panel/index.js @@ -1,10 +1,16 @@ +/** + * External dependencies + */ +import { over, compact } from 'lodash'; + /** * WordPress dependencies */ import { Button, withInstanceId } from '@wordpress/components'; -import { Component, Fragment, createRef } from '@wordpress/element'; +import { Component, Fragment, createRef, compose } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { keycodes } from '@wordpress/utils'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies @@ -117,4 +123,33 @@ class SharedBlockEditPanel extends Component { } } -export default withInstanceId( SharedBlockEditPanel ); +export default compose( [ + withInstanceId, + withSelect( ( select ) => { + const { getEditedPostAttribute } = select( 'core/editor' ); + + return { + title: getEditedPostAttribute( 'title' ), + }; + } ), + withDispatch( ( dispatch, ownProps ) => { + const { + editPost, + undoAll, + savePost, + clearSelectedBlock, + } = dispatch( 'core/editor' ); + + const withClearAndFinish = ( fn ) => over( compact( [ + clearSelectedBlock, + ownProps.onFinishedEditing, + fn, + ] ) ); + + return { + onChangeTitle: ( title ) => editPost( { title } ), + onSave: withClearAndFinish( savePost ), + onCancel: withClearAndFinish( undoAll ), + }; + } ), +] )( SharedBlockEditPanel ); diff --git a/core-blocks/block/edit.js b/core-blocks/block/edit.js index deb56593f1827f..95ec22b361b684 100644 --- a/core-blocks/block/edit.js +++ b/core-blocks/block/edit.js @@ -1,169 +1,111 @@ -/** - * External dependencies - */ -import { noop, partial } from 'lodash'; - /** * WordPress dependencies */ -import { Component, Fragment, compose } from '@wordpress/element'; +import { Component, compose } 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 { EditorProvider, BlockList } from '@wordpress/editor'; +import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies */ import SharedBlockEditPanel from './edit-panel'; import SharedBlockIndicator from './indicator'; +import SharedBlockSelection from './selection'; class SharedBlockEdit extends Component { - constructor( { sharedBlock } ) { + 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 ); + this.startEditing = this.toggleEditing.bind( this, true ); + this.stopEditing = this.toggleEditing.bind( this, false ); + const { sharedBlock, settings } = props; this.state = { isEditing: !! ( sharedBlock && sharedBlock.isTemporary ), - title: null, - changedAttributes: null, + settingsWithLock: { ...settings, templateLock: true }, }; } - componentDidMount() { - if ( ! this.props.sharedBlock ) { - this.props.fetchSharedBlock(); + static getDerivedStateFromProps( props, prevState ) { + if ( isShallowEqual( props.settings, prevState.settings ) ) { + return null; } - } - - startEditing() { - const { sharedBlock } = this.props; - - this.setState( { - isEditing: true, - title: sharedBlock.title, - changedAttributes: {}, - } ); - } - - stopEditing() { - this.setState( { - isEditing: false, - title: null, - changedAttributes: null, - } ); - } - setAttributes( attributes ) { - this.setState( ( prevState ) => { - if ( prevState.changedAttributes !== null ) { - return { changedAttributes: { ...prevState.changedAttributes, ...attributes } }; - } - } ); - } - - setTitle( title ) { - this.setState( { title } ); + return { + settings: props.settings, + settingsWithLock: { + ...props.settings, + templateLock: true, + }, + }; } - save() { - const { sharedBlock, onUpdateTitle, updateAttributes, block, onSave } = this.props; - const { title, changedAttributes } = this.state; - - if ( title !== sharedBlock.title ) { - onUpdateTitle( title ); - } - - updateAttributes( block.uid, changedAttributes ); - onSave(); - - this.stopEditing(); + toggleEditing( isEditing ) { + this.setState( { isEditing } ); } render() { - const { isSelected, sharedBlock, block, isFetching, isSaving } = this.props; - const { isEditing, title, changedAttributes } = this.state; + const { setIsSelected, sharedBlock, isSelected, isSaving } = this.props; + const { settingsWithLock, isEditing } = this.state; - if ( ! sharedBlock && isFetching ) { + if ( ! sharedBlock ) { return ; } - if ( ! sharedBlock || ! 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 { - getSharedBlock, - isFetchingSharedBlock, - isSavingSharedBlock, - getBlock, - } = select( 'core/editor' ); const { ref } = ownProps.attributes; - const sharedBlock = getSharedBlock( ref ); + if ( ! Number.isFinite( ref ) ) { + return; + } + const { getEntityRecord } = select( 'core' ); return { - sharedBlock, - isFetching: isFetchingSharedBlock( ref ), - isSaving: isSavingSharedBlock( ref ), - block: sharedBlock ? getBlock( sharedBlock.uid ) : null, + sharedBlock: getEntityRecord( 'postType', 'wp_block', ref ), + settings: select( 'core/editor' ).getEditorSettings(), }; } ), withDispatch( ( dispatch, ownProps ) => { - const { - fetchSharedBlocks, - updateBlockAttributes, - updateSharedBlockTitle, - saveSharedBlock, - } = dispatch( 'core/editor' ); - const { ref } = ownProps.attributes; + const { selectBlock } = dispatch( 'core/editor' ); + const { id } = ownProps; return { - fetchSharedBlock: partial( fetchSharedBlocks, ref ), - updateAttributes: updateBlockAttributes, - onUpdateTitle: partial( updateSharedBlockTitle, ref ), - onSave: partial( saveSharedBlock, ref ), + setIsSelected: () => selectBlock( id ), }; } ), ] )( SharedBlockEdit ); diff --git a/core-blocks/block/indicator/index.js b/core-blocks/block/indicator/index.js index 403ed6d563f86d..4dbd3b2b4eb50d 100644 --- a/core-blocks/block/indicator/index.js +++ b/core-blocks/block/indicator/index.js @@ -3,6 +3,7 @@ */ import { Tooltip, Dashicon } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies @@ -21,4 +22,10 @@ function SharedBlockIndicator( { title } ) { ); } -export default SharedBlockIndicator; +export default withSelect( ( select ) => { + const { getEditedPostAttribute } = select( 'core/editor' ); + + return { + title: getEditedPostAttribute( 'title' ), + }; +} )( SharedBlockIndicator ); diff --git a/core-blocks/block/selection.js b/core-blocks/block/selection.js new file mode 100644 index 00000000000000..ff65d2816ddf78 --- /dev/null +++ b/core-blocks/block/selection.js @@ -0,0 +1,42 @@ +/** + * WordPress dependencies + */ +import { Component, compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; + +class SharedBlockSelection extends Component { + componentDidUpdate( prevProps ) { + const { + isSharedBlockSelected, + hasSelection, + clearSelectedBlock, + onBlockSelection, + } = this.props; + + if ( ! isSharedBlockSelected && prevProps.isSharedBlockSelected ) { + 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 }; + } ), +] )( SharedBlockSelection ); diff --git a/edit-post/editor.js b/edit-post/editor.js index 42ff1f5da83031..f67f6db37f8477 100644 --- a/edit-post/editor.js +++ b/edit-post/editor.js @@ -9,6 +9,9 @@ import { StrictMode } from '@wordpress/element'; * Internal dependencies */ import Layout from './components/layout'; +import store from './store'; + +const initializeStore = () => store.dispatch( { type: 'INIT' } ); function Editor( { settings, hasFixedToolbar, post, overridePost, onError, ...props } ) { if ( ! post ) { @@ -22,7 +25,11 @@ function Editor( { settings, hasFixedToolbar, post, overridePost, onError, ...pr return ( - + diff --git a/edit-post/store/index.js b/edit-post/store/index.js index d275e8ed209c87..25c97579f2b56d 100644 --- a/edit-post/store/index.js +++ b/edit-post/store/index.js @@ -28,6 +28,5 @@ const store = registerStore( 'core/edit-post', { applyMiddlewares( store ); loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); -store.dispatch( { type: 'INIT' } ); export default store; diff --git a/editor/components/block-settings-menu/shared-block-settings.js b/editor/components/block-settings-menu/shared-block-settings.js index f95d8dc9af8477..983939b2250106 100644 --- a/editor/components/block-settings-menu/shared-block-settings.js +++ b/editor/components/block-settings-menu/shared-block-settings.js @@ -12,10 +12,10 @@ import { __ } from '@wordpress/i18n'; import { isSharedBlock } from '@wordpress/blocks'; import { withSelect, withDispatch } from '@wordpress/data'; -export function SharedBlockSettings( { sharedBlock, onConvertToStatic, onConvertToShared, onDelete, itemsRole } ) { +export function SharedBlockSettings( { isStaticBlock, onConvertToStatic, onConvertToShared, onDelete, itemsRole } ) { return ( - { ! sharedBlock && ( + { isStaticBlock && ( ) } - { sharedBlock && ( + { ! isStaticBlock && ( onDelete( sharedBlock.id ) } + onClick={ onDelete } role={ itemsRole } > { __( 'Delete Shared Block' ) } @@ -52,18 +51,20 @@ export function SharedBlockSettings( { sharedBlock, onConvertToStatic, onConvert export default compose( [ withSelect( ( select, { uid } ) => { - const { getBlock, getSharedBlock } = select( 'core/editor' ); + const { getBlock } = select( 'core/editor' ); const block = getBlock( uid ); return { - sharedBlock: block && isSharedBlock( block ) ? getSharedBlock( block.attributes.ref ) : null, + sharedBlockId: block.attributes.ref, + isStaticBlock: ! block || ! isSharedBlock( block ), }; } ), - withDispatch( ( dispatch, { uid, onToggle = noop } ) => { + withDispatch( ( dispatch, ownProps ) => { const { convertBlockToShared, convertBlockToStatic, deleteSharedBlock, } = dispatch( 'core/editor' ); + const { uid, onToggle = noop, sharedBlockId } = ownProps; return { onConvertToStatic() { @@ -74,7 +75,7 @@ export default compose( [ convertBlockToShared( uid ); onToggle(); }, - onDelete( id ) { + onDelete() { // TODO: Make this a component or similar // eslint-disable-next-line no-alert const hasConfirmed = window.confirm( __( @@ -83,7 +84,7 @@ export default compose( [ ) ); if ( hasConfirmed ) { - deleteSharedBlock( id ); + deleteSharedBlock( sharedBlockId ); onToggle(); } }, diff --git a/editor/components/provider/index.js b/editor/components/provider/index.js index 788c29ea4df4f8..cdd9c19a338a41 100644 --- a/editor/components/provider/index.js +++ b/editor/components/provider/index.js @@ -2,17 +2,19 @@ * External dependencies */ import { flow, pick } from 'lodash'; +import memoize from 'memize'; /** * WordPress Dependencies */ -import { createElement, Component } from '@wordpress/element'; +import { createElement, Component, compose } from '@wordpress/element'; import { APIProvider, DropZoneProvider, SlotFillProvider, } from '@wordpress/components'; -import { withDispatch } from '@wordpress/data'; +import { withDispatch, withCustomReducerKey } from '@wordpress/data'; +import createEditorStore from '../../store'; /** * Internal dependencies @@ -42,7 +44,13 @@ class EditorProvider extends Component { undo, redo, createUndoLevel, + inheritContext, } = this.props; + + if ( inheritContext ) { + return children; + } + const providers = [ // RichText provider: // @@ -99,19 +107,47 @@ class EditorProvider extends Component { } } -export default withDispatch( ( dispatch ) => { - const { - setupEditor, - updateEditorSettings, - undo, - redo, - createUndoLevel, - } = dispatch( 'core/editor' ); - return { - setupEditor, - undo, - redo, - createUndoLevel, - updateEditorSettings, - }; -} )( EditorProvider ); +const createStoreOnce = memoize( ( reducerKey ) => createEditorStore( reducerKey ) ); + +export default compose( [ + ( WrappedComponent ) => class extends Component { + constructor( props ) { + super( ...arguments ); + + createStoreOnce( props.reducerKey ); + } + + componentDidMount() { + if ( this.props.onStoreCreated ) { + this.props.onStoreCreated(); + } + } + + render() { + return ; + } + }, + withCustomReducerKey( ( reducerKey, ownProps ) => { + if ( reducerKey === 'core/editor' && ownProps.reducerKey ) { + return ownProps.reducerKey; + } + + return reducerKey; + } ), + withDispatch( ( dispatch ) => { + const { + setupEditor, + updateEditorSettings, + undo, + redo, + createUndoLevel, + } = dispatch( 'core/editor' ); + return { + setupEditor, + undo, + redo, + createUndoLevel, + updateEditorSettings, + }; + } ), +] )( EditorProvider ); diff --git a/editor/index.js b/editor/index.js index 170617fc80f23d..f980261f984cb8 100644 --- a/editor/index.js +++ b/editor/index.js @@ -1,4 +1,3 @@ -import './store'; import './hooks'; export * from './components'; diff --git a/editor/store/actions.js b/editor/store/actions.js index 278a76e1c0e5f7..54dda941bbad8f 100644 --- a/editor/store/actions.js +++ b/editor/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 UID has been updated. @@ -455,6 +439,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. @@ -580,18 +568,13 @@ export const createErrorNotice = partial( createNotice, 'error' ); export const createWarningNotice = partial( createNotice, 'warning' ); /** - * Returns an action object used to fetch a single shared block or all shared - * blocks from the REST API into the store. - * - * @param {?string} id If given, only a single shared block with this ID will - * be fetched. + * Returns an action object used to fetch all shared blocks from the REST API. * * @return {Object} Action object. */ -export function fetchSharedBlocks( id ) { +export function fetchSharedBlocks() { return { type: 'FETCH_SHARED_BLOCKS', - id, }; } @@ -613,17 +596,18 @@ export function receiveSharedBlocks( results ) { } /** - * Returns an action object used to save a shared block that's in the store to - * the REST API. + * Returns an action object used to create a new shared block. * - * @param {Object} id The ID of the shared block to save. + * @param {Object} sharedBlock Temporary shared block to be updated once saved. + * @param {string} content Content of shared block. * * @return {Object} Action object. */ -export function saveSharedBlock( id ) { +export function saveSharedBlock( sharedBlock, content ) { return { type: 'SAVE_SHARED_BLOCK', - id, + sharedBlock, + content, }; } @@ -641,23 +625,6 @@ export function deleteSharedBlock( id ) { }; } -/** - * Returns an action object used in signalling that a shared block's title is - * to be updated. - * - * @param {number} id The ID of the shared block to update. - * @param {string} title The new title. - * - * @return {Object} Action object. - */ -export function updateSharedBlockTitle( id, title ) { - return { - type: 'UPDATE_SHARED_BLOCK_TITLE', - id, - title, - }; -} - /** * Returns an action object used to convert a shared block into a static block. * diff --git a/editor/store/effects.js b/editor/store/effects.js index 75da4df7821d25..8cb4b07863acf3 100644 --- a/editor/store/effects.js +++ b/editor/store/effects.js @@ -13,7 +13,6 @@ import { switchToBlockType, createBlock, serialize, - isSharedBlock, getDefaultBlockForPostFormat, doBlocksMatchTemplate, synchronizeBlocksWithTemplate, @@ -21,6 +20,7 @@ import { import { __ } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; import apiRequest from '@wordpress/api-request'; +import { select } from '@wordpress/data'; /** * Internal dependencies @@ -30,7 +30,6 @@ import { resetAutosave, resetPost, updatePost, - receiveBlocks, receiveSharedBlocks, replaceBlock, replaceBlocks, @@ -40,7 +39,6 @@ import { removeNotice, saveSharedBlock, insertBlock, - removeBlocks, selectBlock, removeBlock, resetBlocks, @@ -57,7 +55,6 @@ import { getBlockCount, getBlockRootUID, getBlocks, - getSharedBlock, getPreviousBlockUid, getProvisionalBlockUID, getSelectedBlock, @@ -476,35 +473,25 @@ export default { return; } - const { id } = action; const { dispatch } = store; - let result; - if ( id ) { - result = apiRequest( { path: `/wp/v2/${ basePath }/${ id }` } ); - } else { - result = apiRequest( { path: `/wp/v2/${ basePath }?per_page=-1` } ); - } - - result.then( + apiRequest( { path: `/wp/v2/${ basePath }?context=edit&per_page=-1` } ).then( ( sharedBlockOrBlocks ) => { dispatch( receiveSharedBlocks( map( castArray( sharedBlockOrBlocks ), ( sharedBlock ) => ( { sharedBlock, - parsedBlock: parse( sharedBlock.content )[ 0 ], + parsedBlock: parse( sharedBlock.content.raw )[ 0 ], } ) ) ) ); dispatch( { type: 'FETCH_SHARED_BLOCKS_SUCCESS', - id, } ); }, ( error ) => { dispatch( { type: 'FETCH_SHARED_BLOCKS_FAILURE', - id, error: error.responseJSON || { code: 'unknown_error', message: __( 'An unknown error occurred.' ), @@ -513,9 +500,6 @@ export default { } ); }, - RECEIVE_SHARED_BLOCKS( action ) { - return receiveBlocks( map( action.results, 'parsedBlock' ) ); - }, SAVE_SHARED_BLOCK( action, store ) { // TODO: these are potentially undefined, this fix is in place // until there is a filter to not use shared blocks if undefined @@ -524,17 +508,13 @@ export default { return; } - const { id } = action; + const { sharedBlock, content } = action; + const { id, title } = sharedBlock; const { dispatch } = store; - const state = store.getState(); - const { uid, title, isTemporary } = getSharedBlock( state, id ); - const { name, attributes, innerBlocks } = getBlock( state, uid ); - const content = serialize( createBlock( name, attributes, innerBlocks ) ); - - const data = isTemporary ? { title, content } : { id, title, content }; - const path = isTemporary ? `/wp/v2/${ basePath }` : `/wp/v2/${ basePath }/${ id }`; - const method = isTemporary ? 'POST' : 'PUT'; + const data = { title, content }; + const path = `/wp/v2/${ basePath }`; + const method = 'POST'; apiRequest( { path, data, method } ).then( ( updatedSharedBlock ) => { @@ -543,7 +523,7 @@ export default { updatedId: updatedSharedBlock.id, id, } ); - const message = isTemporary ? __( 'Block created.' ) : __( 'Block updated.' ); + const message = __( 'Block created.' ); dispatch( createSuccessNotice( message, { id: SHARED_BLOCK_NOTICE_ID } ) ); }, ( error ) => { @@ -565,18 +545,7 @@ export default { } const { id } = action; - const { getState, dispatch } = store; - - // Don't allow a shared block with a temporary ID to be deleted - const sharedBlock = getSharedBlock( getState(), id ); - if ( ! sharedBlock || sharedBlock.isTemporary ) { - return; - } - - // Remove any other blocks that reference this shared block - const allBlocks = getBlocks( getState() ); - const associatedBlocks = allBlocks.filter( ( block ) => isSharedBlock( block ) && block.attributes.ref === id ); - const associatedBlockUids = associatedBlocks.map( ( block ) => block.uid ); + const { dispatch } = store; const transactionId = uniqueId(); @@ -586,13 +555,7 @@ export default { optimist: { type: BEGIN, id: transactionId }, } ); - // Remove the parsed block. - dispatch( removeBlocks( [ - ...associatedBlockUids, - sharedBlock.uid, - ] ) ); - - apiRequest( { path: `/wp/v2/${ basePath }/${ id }`, method: 'DELETE' } ).then( + apiRequest( { path: `/wp/v2/${ basePath }/${ id }?force=true`, method: 'DELETE' } ).then( () => { dispatch( { type: 'DELETE_SHARED_BLOCK_SUCCESS', @@ -619,8 +582,8 @@ export default { CONVERT_BLOCK_TO_STATIC( action, store ) { const state = store.getState(); const oldBlock = getBlock( state, action.uid ); - const sharedBlock = getSharedBlock( state, oldBlock.attributes.ref ); - const referencedBlock = getBlock( state, sharedBlock.uid ); + const reducerKey = 'core/editor-shared-' + oldBlock.attributes.ref; + const referencedBlock = select( reducerKey ).getBlocks()[ 0 ]; const newBlock = createBlock( referencedBlock.name, referencedBlock.attributes ); store.dispatch( replaceBlock( oldBlock.uid, newBlock ) ); }, @@ -628,6 +591,7 @@ export default { const { getState, dispatch } = store; const parsedBlock = getBlock( getState(), action.uid ); + const content = serialize( parsedBlock ); const sharedBlock = { id: uniqueId( 'shared' ), uid: parsedBlock.uid, @@ -639,7 +603,7 @@ export default { parsedBlock, } ] ) ); - dispatch( saveSharedBlock( sharedBlock.id ) ); + dispatch( saveSharedBlock( sharedBlock, content ) ); dispatch( replaceBlock( parsedBlock.uid, @@ -648,9 +612,6 @@ export default { layout: parsedBlock.attributes.layout, } ) ) ); - - // Re-add the original block to the store, since replaceBlock() will have removed it - dispatch( receiveBlocks( [ parsedBlock ] ) ); }, CREATE_NOTICE( { notice: { content, spokenMessage } } ) { const message = spokenMessage || content; diff --git a/editor/store/index.js b/editor/store/index.js index a602694cf59395..9dbe56169bffd3 100644 --- a/editor/store/index.js +++ b/editor/store/index.js @@ -21,14 +21,18 @@ import * as actions from './actions'; * Module Constants */ const STORAGE_KEY = `GUTENBERG_PREFERENCES_${ window.userSettings.uid }`; -const MODULE_KEY = 'core/editor'; +const REDUCER_KEY = 'core/editor'; -const store = applyMiddlewares( - registerReducer( MODULE_KEY, withRehydration( reducer, 'preferences', STORAGE_KEY ) ) -); -loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); +function createEditorStore( reducerKey = REDUCER_KEY ) { + const store = applyMiddlewares( + registerReducer( reducerKey, withRehydration( reducer, 'preferences', STORAGE_KEY ) ) + ); + loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); -registerSelectors( MODULE_KEY, selectors ); -registerActions( MODULE_KEY, actions ); + registerSelectors( reducerKey, selectors ); + registerActions( reducerKey, actions ); -export default store; + return store; +} + +export default createEditorStore; diff --git a/editor/store/reducer.js b/editor/store/reducer.js index fdfdfa2476ed18..c57393fa243c57 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -217,7 +217,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, } ), @@ -225,7 +225,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 ) { @@ -286,12 +286,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.uid ] ) { @@ -410,12 +404,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 { rootUID = '', blocks } = action; const subState = state[ rootUID ] || []; @@ -924,10 +912,11 @@ export const sharedBlocks = combineReducers( { switch ( action.type ) { case 'RECEIVE_SHARED_BLOCKS': { return reduce( action.results, ( nextState, result ) => { - const { id, title } = result.sharedBlock; - const { uid } = result.parsedBlock; + const { id } = result.sharedBlock; + const title = getPostRawValue( result.sharedBlock.title ); + const { name: blockName } = result.parsedBlock; - const value = { uid, title }; + const value = { blockName, title }; if ( ! isEqual( nextState[ id ], value ) ) { if ( nextState === state ) { @@ -941,22 +930,6 @@ export const sharedBlocks = combineReducers( { }, state ); } - case 'UPDATE_SHARED_BLOCK_TITLE': { - const { id, title } = action; - - if ( ! state[ id ] || state[ id ].title === title ) { - return state; - } - - return { - ...state, - [ id ]: { - ...state[ id ], - title, - }, - }; - } - case 'SAVE_SHARED_BLOCK_SUCCESS': { const { id, updatedId } = action; @@ -980,48 +953,6 @@ export const sharedBlocks = combineReducers( { return state; }, - - isFetching( state = {}, action ) { - switch ( action.type ) { - case 'FETCH_SHARED_BLOCKS': { - const { id } = action; - if ( ! id ) { - return state; - } - - return { - ...state, - [ id ]: true, - }; - } - - case 'FETCH_SHARED_BLOCKS_SUCCESS': - case 'FETCH_SHARED_BLOCKS_FAILURE': { - const { id } = action; - return omit( state, id ); - } - } - - return state; - }, - - isSaving( state = {}, action ) { - switch ( action.type ) { - case 'SAVE_SHARED_BLOCK': - return { - ...state, - [ action.id ]: true, - }; - - case 'SAVE_SHARED_BLOCK_SUCCESS': - case 'SAVE_SHARED_BLOCK_FAILURE': { - const { id } = action; - return omit( state, id ); - } - } - - return state; - }, } ); /** diff --git a/editor/store/selectors.js b/editor/store/selectors.js index 4928ec226ba16e..526f9ea912a88a 100644 --- a/editor/store/selectors.js +++ b/editor/store/selectors.js @@ -1520,12 +1520,7 @@ export const getInserterItems = createSelector( return false; } - const referencedBlock = getBlock( state, sharedBlock.uid ); - if ( ! referencedBlock ) { - return false; - } - - const referencedBlockType = getBlockType( referencedBlock.name ); + const referencedBlockType = getBlockType( sharedBlock.blockName ); if ( ! referencedBlockType ) { return false; } @@ -1540,8 +1535,7 @@ export const getInserterItems = createSelector( const buildSharedBlockInserterItem = ( sharedBlock ) => { const id = `core/block/${ sharedBlock.id }`; - const referencedBlock = getBlock( state, sharedBlock.uid ); - const referencedBlockType = getBlockType( referencedBlock.name ); + const referencedBlockType = getBlockType( sharedBlock.blockName ); const { time, count = 0 } = getInsertUsage( state, id ) || {}; const utility = calculateUtility( 'shared', count, false ); diff --git a/editor/store/test/actions.js b/editor/store/test/actions.js index 401250bfeff59c..1bbff1c58be464 100644 --- a/editor/store/test/actions.js +++ b/editor/store/test/actions.js @@ -6,7 +6,6 @@ import { startTyping, stopTyping, fetchSharedBlocks, - saveSharedBlock, deleteSharedBlock, convertBlockToStatic, convertBlockToShared, @@ -466,22 +465,6 @@ describe( 'actions', () => { type: 'FETCH_SHARED_BLOCKS', } ); } ); - - it( 'should take an optional id argument', () => { - expect( fetchSharedBlocks( 123 ) ).toEqual( { - type: 'FETCH_SHARED_BLOCKS', - id: 123, - } ); - } ); - } ); - - describe( 'saveSharedBlock', () => { - it( 'should return the SAVE_SHARED_BLOCK action', () => { - expect( saveSharedBlock( 123 ) ).toEqual( { - type: 'SAVE_SHARED_BLOCK', - id: 123, - } ); - } ); } ); describe( 'deleteSharedBlock', () => { diff --git a/editor/store/test/effects.js b/editor/store/test/effects.js index 8e135cb7c566b8..b43c7756b2335d 100644 --- a/editor/store/test/effects.js +++ b/editor/store/test/effects.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { noop, set, reduce } from 'lodash'; +import { noop, set } from 'lodash'; /** * WordPress dependencies @@ -26,13 +26,6 @@ import { createErrorNotice, fetchSharedBlocks, receiveSharedBlocks, - receiveBlocks, - saveSharedBlock, - deleteSharedBlock, - removeBlocks, - resetBlocks, - convertBlockToStatic, - convertBlockToShared, setTemplateValidity, editPost, } from '../actions'; @@ -596,8 +589,12 @@ describe( 'effects', () => { const promise = Promise.resolve( [ { id: 123, - title: 'My cool block', - content: '', + title: { + raw: 'My cool block', + }, + content: { + raw: '', + }, }, ] ); apiRequest.mockReturnValue = promise; @@ -614,8 +611,12 @@ describe( 'effects', () => { { sharedBlock: { id: 123, - title: 'My cool block', - content: '', + title: { + raw: 'My cool block', + }, + content: { + raw: '', + }, }, parsedBlock: expect.objectContaining( { name: 'core/test-block', @@ -626,44 +627,6 @@ describe( 'effects', () => { ); expect( dispatch ).toHaveBeenCalledWith( { type: 'FETCH_SHARED_BLOCKS_SUCCESS', - id: undefined, - } ); - } ); - } ); - - it( 'should fetch a single shared block', () => { - const promise = Promise.resolve( { - id: 123, - title: 'My cool block', - content: '', - } ); - apiRequest.mockReturnValue = promise; - set( global, [ 'wp', 'api', 'getPostTypeRoute' ], () => 'blocks' ); - - const dispatch = jest.fn(); - const store = { getState: noop, dispatch }; - - handler( fetchSharedBlocks( 123 ), store ); - - return promise.then( () => { - expect( dispatch ).toHaveBeenCalledWith( - receiveSharedBlocks( [ - { - sharedBlock: { - id: 123, - title: 'My cool block', - content: '', - }, - parsedBlock: expect.objectContaining( { - name: 'core/test-block', - attributes: { name: 'Big Bird' }, - } ), - }, - ] ) - ); - expect( dispatch ).toHaveBeenCalledWith( { - type: 'FETCH_SHARED_BLOCKS_SUCCESS', - id: 123, } ); } ); } ); @@ -689,244 +652,5 @@ describe( 'effects', () => { } ); } ); } ); - - describe( '.RECEIVE_SHARED_BLOCKS', () => { - const handler = effects.RECEIVE_SHARED_BLOCKS; - - it( 'should receive parsed blocks', () => { - const action = receiveSharedBlocks( [ - { - parsedBlock: { uid: 'broccoli' }, - }, - ] ); - - expect( handler( action ) ).toEqual( receiveBlocks( [ - { uid: 'broccoli' }, - ] ) ); - } ); - } ); - - describe( '.SAVE_SHARED_BLOCK', () => { - const handler = effects.SAVE_SHARED_BLOCK; - - it( 'should save a shared block and swap its id', () => { - const promise = Promise.resolve( { id: 456 } ); - apiRequest.mockReturnValue = promise; - - set( global, [ 'wp', 'api', 'getPostTypeRoute' ], () => 'blocks' ); - - const sharedBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reduce( [ - receiveSharedBlocks( [ { sharedBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - handler( saveSharedBlock( 123 ), store ); - - return promise.then( () => { - expect( dispatch ).toHaveBeenCalledWith( { - type: 'SAVE_SHARED_BLOCK_SUCCESS', - id: 123, - updatedId: 456, - } ); - } ); - } ); - - it( 'should handle an API error', () => { - const promise = Promise.reject( {} ); - apiRequest.mockReturnValue = promise; - set( global, [ 'wp', 'api', 'getPostTypeRoute' ], () => 'blocks' ); - - const sharedBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reduce( [ - receiveSharedBlocks( [ { sharedBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - handler( saveSharedBlock( 123 ), store ); - - return promise.catch( () => { - expect( dispatch ).toHaveBeenCalledWith( { - type: 'SAVE_SHARED_BLOCK_FAILURE', - id: 123, - } ); - } ); - } ); - } ); - - describe( '.DELETE_SHARED_BLOCK', () => { - const handler = effects.DELETE_SHARED_BLOCK; - - it( 'should delete a shared block', () => { - const promise = Promise.resolve( {} ); - apiRequest.mockReturnValue = promise; - set( global, [ 'wp', 'api', 'getPostTypeRoute' ], () => 'blocks' ); - - const associatedBlock = createBlock( 'core/block', { ref: 123 } ); - const sharedBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reduce( [ - resetBlocks( [ associatedBlock ] ), - receiveSharedBlocks( [ { sharedBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - handler( deleteSharedBlock( 123 ), store ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'REMOVE_SHARED_BLOCK', - id: 123, - optimist: expect.any( Object ), - } ); - - expect( dispatch ).toHaveBeenCalledWith( - removeBlocks( [ associatedBlock.uid, parsedBlock.uid ] ) - ); - - return promise.then( () => { - expect( dispatch ).toHaveBeenCalledWith( { - type: 'DELETE_SHARED_BLOCK_SUCCESS', - id: 123, - optimist: expect.any( Object ), - } ); - } ); - } ); - - it( 'should handle an API error', () => { - const promise = Promise.reject( {} ); - apiRequest.mockReturnValue = promise; - set( global, [ 'wp', 'api', 'getPostTypeRoute' ], () => 'blocks' ); - - const sharedBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reduce( [ - receiveSharedBlocks( [ { sharedBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - handler( deleteSharedBlock( 123 ), store ); - - return promise.catch( () => { - expect( dispatch ).toHaveBeenCalledWith( { - type: 'DELETE_SHARED_BLOCK_FAILURE', - id: 123, - optimist: expect.any( Object ), - } ); - } ); - } ); - - it( 'should not save shared blocks with temporary IDs', () => { - const sharedBlock = { id: 'shared1', title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reduce( [ - receiveSharedBlocks( [ { sharedBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - handler( deleteSharedBlock( 'shared1' ), store ); - - expect( dispatch ).not.toHaveBeenCalled(); - } ); - } ); - - describe( '.CONVERT_BLOCK_TO_STATIC', () => { - const handler = effects.CONVERT_BLOCK_TO_STATIC; - - it( 'should convert a shared block into a static block', () => { - const associatedBlock = createBlock( 'core/block', { ref: 123 } ); - const sharedBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reduce( [ - resetBlocks( [ associatedBlock ] ), - receiveSharedBlocks( [ { sharedBlock, parsedBlock } ] ), - receiveBlocks( [ parsedBlock ] ), - ], reducer, undefined ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - handler( convertBlockToStatic( associatedBlock.uid ), store ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'REPLACE_BLOCKS', - uids: [ associatedBlock.uid ], - blocks: [ - expect.objectContaining( { - name: 'core/test-block', - attributes: { name: 'Big Bird' }, - } ), - ], - time: expect.any( Number ), - } ); - } ); - } ); - - describe( '.CONVERT_BLOCK_TO_SHARED', () => { - const handler = effects.CONVERT_BLOCK_TO_SHARED; - - it( 'should convert a static block into a shared block', () => { - const staticBlock = createBlock( 'core/block', { ref: 123 } ); - const state = reducer( undefined, resetBlocks( [ staticBlock ] ) ); - - const dispatch = jest.fn(); - const store = { getState: () => state, dispatch }; - - handler( convertBlockToShared( staticBlock.uid ), store ); - - expect( dispatch ).toHaveBeenCalledWith( - receiveSharedBlocks( [ { - sharedBlock: { - id: expect.stringMatching( /^shared/ ), - uid: staticBlock.uid, - title: 'Untitled shared block', - }, - parsedBlock: staticBlock, - } ] ) - ); - - expect( dispatch ).toHaveBeenCalledWith( - saveSharedBlock( expect.stringMatching( /^shared/ ) ), - ); - - expect( dispatch ).toHaveBeenCalledWith( { - type: 'REPLACE_BLOCKS', - uids: [ staticBlock.uid ], - blocks: [ - expect.objectContaining( { - name: 'core/block', - attributes: { ref: expect.stringMatching( /^shared/ ) }, - } ), - ], - time: expect.any( Number ), - } ); - - expect( dispatch ).toHaveBeenCalledWith( - receiveBlocks( [ staticBlock ] ), - ); - } ); - } ); } ); } ); diff --git a/editor/store/test/reducer.js b/editor/store/test/reducer.js index b1afa1c7e6e529..e8bcc603d80bd0 100644 --- a/editor/store/test/reducer.js +++ b/editor/store/test/reducer.js @@ -1944,30 +1944,6 @@ describe( 'state', () => { } ); } ); - it( 'should update a shared block', () => { - const initialState = { - data: { - 123: { uid: '', title: '' }, - }, - isFetching: {}, - isSaving: {}, - }; - - const state = sharedBlocks( initialState, { - type: 'UPDATE_SHARED_BLOCK_TITLE', - id: 123, - title: 'My block', - } ); - - expect( state ).toEqual( { - data: { - 123: { uid: '', title: 'My block' }, - }, - isFetching: {}, - isSaving: {}, - } ); - } ); - it( 'should update the shared block\'s id if it was temporary', () => { const initialState = { data: { diff --git a/editor/utils/with-history/index.js b/editor/utils/with-history/index.js index cc90f0d4b66478..77a68345b59ab3 100644 --- a/editor/utils/with-history/index.js +++ b/editor/utils/with-history/index.js @@ -64,6 +64,17 @@ const withHistory = ( options = {} ) => ( reducer ) => { lastAction = action; 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/lib/class-wp-rest-blocks-controller.php b/lib/class-wp-rest-blocks-controller.php deleted file mode 100644 index 7435c63e56b9e8..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 f1a8b09c66ce80..143db82a7162de 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__ ) . '/rest-api.php'; diff --git a/lib/register.php b/lib/register.php index d5265eb6092f83..4cc722aa6c1a3c 100644 --- a/lib/register.php +++ b/lib/register.php @@ -389,7 +389,6 @@ function gutenberg_register_post_types() { 'public' => false, 'show_in_rest' => true, 'rest_base' => 'blocks', - 'rest_controller_class' => 'WP_REST_Blocks_Controller', 'capability_type' => 'block', 'capabilities' => array( 'read' => 'read_blocks', diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index 7d36a4c65558a5..a97ce9037fcbdd 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -142,6 +142,10 @@ export function getEntityRecord( state, kind, name, key ) { return get( state.entities.data, [ kind, name, 'byKey', 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 14247b8bb601c1..a5cd6ee6423cb3 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -2,7 +2,7 @@ * External dependencies */ import { combineReducers, createStore } from 'redux'; -import { flowRight, without, mapValues, overEvery } from 'lodash'; +import { flowRight, without, mapValues, overEvery, identity } from 'lodash'; /** * WordPress dependencies @@ -12,6 +12,7 @@ import { compose, createElement, createHigherOrderComponent, + createContext, pure, } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; @@ -256,6 +257,11 @@ export function dispatch( reducerKey ) { return actions[ reducerKey ]; } +const { + Consumer: CustomReducerKeyConsumer, + Provider: CustomReducerKeyProvider, +} = createContext( identity ); + /** * Higher-order component used to inject state-derived props using registered * selectors. @@ -269,7 +275,7 @@ export function dispatch( reducerKey ) { export const withSelect = ( mapStateToProps ) => createHigherOrderComponent( ( WrappedComponent ) => { const DEFAULT_MERGE_PROPS = {}; - return class ComponentWithSelect extends Component { + class ComponentWithSelect extends Component { constructor() { super( ...arguments ); @@ -279,11 +285,17 @@ export const withSelect = ( mapStateToProps ) => createHigherOrderComponent( ( W } static getDerivedStateFromProps( props ) { + const { ownProps, mapReducerKey } = props; + const selectWithCustomReducerKey = flowRight( [ + select, + mapReducerKey, + ] ); + // A constant value is used as the fallback since it can be more // efficiently shallow compared in case component is repeatedly // rendered without its own merge props. const mergeProps = ( - mapStateToProps( select, props ) || + mapStateToProps( selectWithCustomReducerKey, ownProps ) || DEFAULT_MERGE_PROPS ); @@ -301,7 +313,7 @@ export const withSelect = ( mapStateToProps ) => createHigherOrderComponent( ( W shouldComponentUpdate( nextProps, nextState ) { return ( - ! isShallowEqual( this.props, nextProps ) || + ! isShallowEqual( this.props.ownProps, nextProps.ownProps ) || ! isShallowEqual( this.state.mergeProps, nextState.mergeProps ) ); } @@ -322,9 +334,25 @@ export const withSelect = ( mapStateToProps ) => createHigherOrderComponent( ( W } render() { - return ; + return ( + + ); } - }; + } + + return ( props ) => ( + + { ( mapReducerKey ) => ( + + ) } + + ); }, 'withSelect' ); /** @@ -343,7 +371,7 @@ export const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent compose( [ pure, ( WrappedComponent ) => { - return class ComponentWithDispatch extends Component { + class ComponentWithDispatch extends Component { constructor( props ) { super( ...arguments ); @@ -356,14 +384,20 @@ export const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent } proxyDispatch( propName, ...args ) { + const { mapReducerKey, ownProps } = this.props; + const dispatchWithCustomReducerKey = flowRight( [ dispatch, mapReducerKey ] ); + // Original dispatcher is a pre-bound (dispatching) action creator. - mapDispatchToProps( dispatch, this.props )[ propName ]( ...args ); + mapDispatchToProps( dispatchWithCustomReducerKey, ownProps )[ propName ]( ...args ); } setProxyProps( props ) { + const { ownProps, mapReducerKey } = props; + const dispatchWithCustomReducerKey = flowRight( [ dispatch, mapReducerKey ] ); + // Assign as instance property so that in reconciling subsequent // renders, the assigned prop values are referentially equal. - const propsToDispatchers = mapDispatchToProps( dispatch, props ); + const propsToDispatchers = mapDispatchToProps( dispatchWithCustomReducerKey, ownProps ); this.proxyProps = mapValues( propsToDispatchers, ( dispatcher, propName ) => { // Prebind with prop name so we have reference to the original // dispatcher to invoke. Track between re-renders to avoid @@ -377,14 +411,59 @@ export const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent } render() { - return ; + return ( + + ); } - }; + } + + return ( props ) => ( + + { ( mapReducerKey ) => ( + + ) } + + ); }, ] ), 'withDispatch' ); +export function withCustomReducerKey( mapReducerKey ) { + return createHigherOrderComponent( + ( WrappedComponent ) => class extends Component { + constructor() { + super( ...arguments ); + + this.mapReducerKey = this.mapReducerKey.bind( this ); + + this.state = { + mapReducerKey: this.mapReducerKey, + }; + } + + mapReducerKey( reducerKey ) { + return mapReducerKey( reducerKey, this.props ); + } + + render() { + return ( + + + + ); + } + }, + 'withCustomReducerKey' + ); +} + /** * Returns true if the given argument appears to be a dispatchable action. * diff --git a/phpunit/class-rest-blocks-controller-test.php b/phpunit/class-rest-blocks-controller-test.php deleted file mode 100644 index 5c81f1278878f8..00000000000000 --- a/phpunit/class-rest-blocks-controller-test.php +++ /dev/null @@ -1,339 +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 shared 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().' ); - } -}