From 63f43df65cc5db9a0afdc95e993927567208bd79 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 1 Jan 2021 13:23:20 +0100 Subject: [PATCH 1/5] consolidate block editor initializations --- packages/core-data/src/entity-provider.js | 53 ++++++++++--- .../editor/src/components/provider/index.js | 21 +++-- .../provider/use-post-content-editor.js | 77 ------------------- packages/editor/src/store/actions.js | 21 ----- 4 files changed, 51 insertions(+), 121 deletions(-) delete mode 100644 packages/editor/src/components/provider/use-post-content-editor.js diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js index 9deed11ba00cd7..2593e16a0dfaf2 100644 --- a/packages/core-data/src/entity-provider.js +++ b/packages/core-data/src/entity-provider.js @@ -137,11 +137,21 @@ export function useEntityProp( kind, type, prop, _id ) { export function useEntityBlockEditor( kind, type, { id: _id } = {} ) { const providerId = useEntityId( kind, type ); const id = _id ?? providerId; + const { content, blocks } = useSelect( + ( select ) => { + const { getEditedEntityRecord } = select( 'core' ); + const editedEntity = getEditedEntityRecord( 'postType', type, id ); + return { + blocks: editedEntity.blocks, + content: editedEntity.content, + }; + }, + [ type, id ] + ); + const { __unstableCreateUndoLevel, editEntityRecord } = useDispatch( + 'core' + ); - const [ content, setContent ] = useEntityProp( kind, type, 'content', id ); - const [ blocks, onInput ] = useEntityProp( kind, type, 'blocks', id ); - - const { editEntityRecord } = useDispatch( 'core' ); useEffect( () => { // Load the blocks from the content if not already in state // Guard against other instances that might have @@ -161,14 +171,35 @@ export function useEntityBlockEditor( kind, type, { id: _id } = {} ) { }, [ content ] ); const onChange = useCallback( - ( nextBlocks ) => { - onInput( nextBlocks ); - // Use a function edit to avoid serializing often. - setContent( ( { blocks: blocksToSerialize } ) => - serialize( blocksToSerialize ) - ); + ( newBlocks, options ) => { + const { selectionStart, selectionEnd } = options; + const edits = { blocks: newBlocks, selectionStart, selectionEnd }; + const noChange = blocks === edits.blocks; + if ( noChange ) { + return __unstableCreateUndoLevel( 'postType', type, id ); + } + + // We create a new function here on every persistent edit + // to make sure the edit makes the post dirty and creates + // a new undo level. + edits.content = ( { blocks: blocksForSerialization = [] } ) => + serialize( blocksForSerialization ); + + editEntityRecord( 'postType', type, id, edits ); }, - [ onInput, setContent ] + [ blocks, id, type ] ); + + const onInput = useCallback( + ( newBlocks, options ) => { + const { selectionStart, selectionEnd } = options; + const edits = { blocks: newBlocks, selectionStart, selectionEnd }; + editEntityRecord( 'postType', type, id, edits, { + undoIgnore: true, + } ); + }, + [ type, id ] + ); + return [ blocks ?? EMPTY_ARRAY, onInput, onChange ]; } diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 2f056ea3dbd727..5469bbbbd7b87b 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -4,7 +4,7 @@ import { useEffect, useLayoutEffect, useMemo } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { EntityProvider } from '@wordpress/core-data'; +import { EntityProvider, useEntityBlockEditor } from '@wordpress/core-data'; import { BlockEditorProvider, BlockContextProvider, @@ -17,7 +17,6 @@ import { store as noticesStore } from '@wordpress/notices'; */ import withRegistryProvider from './with-registry-provider'; import ConvertToGroupButtons from '../convert-to-group-buttons'; -import usePostContentEditor from './use-post-content-editor'; import { store as editorStore } from '../../store'; import useBlockEditorSettings from './use-block-editor-settings'; @@ -48,7 +47,11 @@ function EditorProvider( { }; }, [] ); const { id, type } = __unstableTemplate ?? post; - const blockEditorProps = usePostContentEditor( type, id ); + const [ blocks, onInput, onChange ] = useEntityBlockEditor( + 'postType', + type, + { id } + ); const editorSettings = useBlockEditorSettings( settings, !! __unstableTemplate @@ -58,7 +61,6 @@ function EditorProvider( { setupEditor, updateEditorSettings, __experimentalTearDownEditor, - __unstableSetupTemplate, } = useDispatch( editorStore ); const { createWarningNotice } = useDispatch( noticesStore ); @@ -99,13 +101,6 @@ function EditorProvider( { updateEditorSettings( settings ); }, [ settings ] ); - // Synchronize the template as it changes - useEffect( () => { - if ( __unstableTemplate ) { - __unstableSetupTemplate( __unstableTemplate ); - } - }, [ __unstableTemplate?.id ] ); - if ( ! isReady ) { return null; } @@ -115,7 +110,9 @@ function EditorProvider( { { - const { getEditedEntityRecord } = select( coreStore ); - return getEditedEntityRecord( 'postType', postType, postId ).blocks; - }, - [ postType, postId ] - ); - const { __unstableCreateUndoLevel, editEntityRecord } = useDispatch( - coreStore - ); - - const onChange = useCallback( - ( newBlocks, options ) => { - const { - __unstableShouldCreateUndoLevel, - selectionStart, - selectionEnd, - } = options; - const edits = { blocks: newBlocks, selectionStart, selectionEnd }; - - if ( __unstableShouldCreateUndoLevel !== false ) { - const noChange = blocks === edits.blocks; - if ( noChange ) { - return __unstableCreateUndoLevel( - 'postType', - postType, - postId - ); - } - - // We create a new function here on every persistent edit - // to make sure the edit makes the post dirty and creates - // a new undo level. - edits.content = ( { blocks: blocksForSerialization = [] } ) => - serializeBlocks( blocksForSerialization ); - } - - editEntityRecord( 'postType', postType, postId, edits ); - }, - [ blocks, postId, postType ] - ); - - const onInput = useCallback( - ( newBlocks, options ) => { - onChange( newBlocks, { - ...options, - __unstableShouldCreateUndoLevel: false, - } ); - }, - [ onChange ] - ); - - return { value: blocks, onChange, onInput }; -} - -export default usePostContentEditor; diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 6f4b2ed1cd8592..50dd8fc07c6f50 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -73,27 +73,6 @@ export function* setupEditor( post, edits, template ) { } } -/** - * Initiliazes an FSE template into the core-data store. - * We could avoid this action entirely by having a fallback if the edit is undefined. - * - * @param {Object} template Template object. - */ -export function* __unstableSetupTemplate( template ) { - const blocks = parse( template.content.raw ); - yield controls.dispatch( - 'core', - 'editEntityRecord', - 'postType', - template.type, - template.id, - { - blocks, - }, - { undoIgnore: true } - ); -} - /** * Returns an action object signalling that the editor is being destroyed and * that any necessary state or side-effect cleanup should occur. From 71ccafbe68be06e75eba6729ddf9d7ad5d41d547 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 5 Jan 2021 10:02:35 +0100 Subject: [PATCH 2/5] Restore block cleanup --- packages/blocks/src/api/index.js | 1 + packages/blocks/src/api/serializer.js | 25 ++++++++- packages/core-data/src/entity-provider.js | 4 +- packages/editor/src/store/actions.js | 9 ++-- packages/editor/src/store/selectors.js | 4 +- .../src/store/utils/serialize-blocks.js | 51 ------------------- 6 files changed, 35 insertions(+), 59 deletions(-) delete mode 100644 packages/editor/src/store/utils/serialize-blocks.js diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 6952ebe9223db7..cedca941340778 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -61,6 +61,7 @@ export { getSaveElement, getSaveContent, getBlockProps as __unstableGetBlockProps, + __unstableSerializeAndClean, } from './serializer'; // Validation is the process of comparing a block source with its output before diff --git a/packages/blocks/src/api/serializer.js b/packages/blocks/src/api/serializer.js index 86b0147beea13a..cf12a1625894de 100644 --- a/packages/blocks/src/api/serializer.js +++ b/packages/blocks/src/api/serializer.js @@ -9,6 +9,7 @@ import { isEmpty, reduce, isObject, castArray, startsWith } from 'lodash'; import { Component, cloneElement, renderToString } from '@wordpress/element'; import { hasFilter, applyFilters } from '@wordpress/hooks'; import isShallowEqual from '@wordpress/is-shallow-equal'; +import { removep } from '@wordpress/autop'; /** * Internal dependencies @@ -19,7 +20,7 @@ import { getUnregisteredTypeHandlerName, hasBlockSupport, } from './registration'; -import { normalizeBlockType } from './utils'; +import { isUnmodifiedDefaultBlock, normalizeBlockType } from './utils'; import BlockContentProvider from '../block-content-provider'; /** @@ -349,6 +350,28 @@ export function serializeBlock( block, { isInnerBlocks = false } = {} ) { return getCommentDelimitedContent( blockName, saveAttributes, saveContent ); } +export function __unstableSerializeAndClean( blocks ) { + // A single unmodified default block is assumed to + // be equivalent to an empty post. + if ( blocks.length === 1 && isUnmodifiedDefaultBlock( blocks[ 0 ] ) ) { + blocks = []; + } + + let content = serialize( blocks ); + + // For compatibility, treat a post consisting of a + // single freeform block as legacy content and apply + // pre-block-editor removep'd content formatting. + if ( + blocks.length === 1 && + blocks[ 0 ].name === getFreeformContentHandlerName() + ) { + content = removep( content ); + } + + return content; +} + /** * Takes a block or set of blocks and returns the serialized post content. * diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js index 2593e16a0dfaf2..de62f1d1388604 100644 --- a/packages/core-data/src/entity-provider.js +++ b/packages/core-data/src/entity-provider.js @@ -8,7 +8,7 @@ import { useEffect, } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; -import { parse, serialize } from '@wordpress/blocks'; +import { parse, __unstableSerializeAndClean } from '@wordpress/blocks'; const EMPTY_ARRAY = []; @@ -183,7 +183,7 @@ export function useEntityBlockEditor( kind, type, { id: _id } = {} ) { // to make sure the edit makes the post dirty and creates // a new undo level. edits.content = ( { blocks: blocksForSerialization = [] } ) => - serialize( blocksForSerialization ); + __unstableSerializeAndClean( blocksForSerialization ); editEntityRecord( 'postType', type, id, edits ); }, diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 50dd8fc07c6f50..072f0c19e77c8d 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -9,7 +9,11 @@ import { has } from 'lodash'; import deprecated from '@wordpress/deprecated'; import { controls } from '@wordpress/data'; import { apiFetch } from '@wordpress/data-controls'; -import { parse, synchronizeBlocksWithTemplate } from '@wordpress/blocks'; +import { + parse, + synchronizeBlocksWithTemplate, + __unstableSerializeAndClean, +} from '@wordpress/blocks'; import { store as noticesStore } from '@wordpress/notices'; /** @@ -21,7 +25,6 @@ import { getNotificationArgumentsForSaveFail, getNotificationArgumentsForTrashFail, } from './utils/notice-builder'; -import serializeBlocks from './utils/serialize-blocks'; /** * Returns an action generator used in signalling that editor has initialized with @@ -624,7 +627,7 @@ export function* resetEditorBlocks( blocks, options = {} ) { // to make sure the edit makes the post dirty and creates // a new undo level. edits.content = ( { blocks: blocksForSerialization = [] } ) => - serializeBlocks( blocksForSerialization ); + __unstableSerializeAndClean( blocksForSerialization ); } yield* editPost( edits ); } diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 9c5a92d25da234..40136136cbf5aa 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -20,6 +20,7 @@ import { getFreeformContentHandlerName, getDefaultBlockName, isUnmodifiedDefaultBlock, + __unstableSerializeAndClean, } from '@wordpress/blocks'; import { isInTheFuture, getDate } from '@wordpress/date'; import { addQueryArgs } from '@wordpress/url'; @@ -38,7 +39,6 @@ import { AUTOSAVE_PROPERTIES, } from './constants'; import { getPostRawValue } from './reducer'; -import serializeBlocks from './utils/serialize-blocks'; import { cleanForSlug } from '../utils/url'; /** @@ -991,7 +991,7 @@ export const getEditedPostContent = createRegistrySelector( if ( typeof record.content === 'function' ) { return record.content( record ); } else if ( record.blocks ) { - return serializeBlocks( record.blocks ); + return __unstableSerializeAndClean( record.blocks ); } else if ( record.content ) { return record.content; } diff --git a/packages/editor/src/store/utils/serialize-blocks.js b/packages/editor/src/store/utils/serialize-blocks.js deleted file mode 100644 index 7301350399ca50..00000000000000 --- a/packages/editor/src/store/utils/serialize-blocks.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * External dependencies - */ -import memoize from 'memize'; - -/** - * WordPress dependencies - */ -import { - isUnmodifiedDefaultBlock, - serialize, - getFreeformContentHandlerName, -} from '@wordpress/blocks'; -import { removep } from '@wordpress/autop'; - -/** - * Serializes blocks following backwards compatibility conventions. - * - * @param {Array} blocksForSerialization The blocks to serialize. - * - * @return {string} The blocks serialization. - */ -const serializeBlocks = memoize( - ( blocksForSerialization ) => { - // A single unmodified default block is assumed to - // be equivalent to an empty post. - if ( - blocksForSerialization.length === 1 && - isUnmodifiedDefaultBlock( blocksForSerialization[ 0 ] ) - ) { - blocksForSerialization = []; - } - - let content = serialize( blocksForSerialization ); - - // For compatibility, treat a post consisting of a - // single freeform block as legacy content and apply - // pre-block-editor removep'd content formatting. - if ( - blocksForSerialization.length === 1 && - blocksForSerialization[ 0 ].name === getFreeformContentHandlerName() - ) { - content = removep( content ); - } - - return content; - }, - { maxSize: 1 } -); - -export default serializeBlocks; From e36f1b36a40db30570ee65f24fcb202eecde0e01 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 5 Jan 2021 16:41:44 +0100 Subject: [PATCH 3/5] Avoid hardcoding the post type --- packages/core-data/src/entity-provider.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js index de62f1d1388604..6f64e803194e2f 100644 --- a/packages/core-data/src/entity-provider.js +++ b/packages/core-data/src/entity-provider.js @@ -140,13 +140,13 @@ export function useEntityBlockEditor( kind, type, { id: _id } = {} ) { const { content, blocks } = useSelect( ( select ) => { const { getEditedEntityRecord } = select( 'core' ); - const editedEntity = getEditedEntityRecord( 'postType', type, id ); + const editedEntity = getEditedEntityRecord( kind, type, id ); return { blocks: editedEntity.blocks, content: editedEntity.content, }; }, - [ type, id ] + [ kind, type, id ] ); const { __unstableCreateUndoLevel, editEntityRecord } = useDispatch( 'core' @@ -176,7 +176,7 @@ export function useEntityBlockEditor( kind, type, { id: _id } = {} ) { const edits = { blocks: newBlocks, selectionStart, selectionEnd }; const noChange = blocks === edits.blocks; if ( noChange ) { - return __unstableCreateUndoLevel( 'postType', type, id ); + return __unstableCreateUndoLevel( kind, type, id ); } // We create a new function here on every persistent edit @@ -185,20 +185,20 @@ export function useEntityBlockEditor( kind, type, { id: _id } = {} ) { edits.content = ( { blocks: blocksForSerialization = [] } ) => __unstableSerializeAndClean( blocksForSerialization ); - editEntityRecord( 'postType', type, id, edits ); + editEntityRecord( kind, type, id, edits ); }, - [ blocks, id, type ] + [ blocks, id, type, kind ] ); const onInput = useCallback( ( newBlocks, options ) => { const { selectionStart, selectionEnd } = options; const edits = { blocks: newBlocks, selectionStart, selectionEnd }; - editEntityRecord( 'postType', type, id, edits, { + editEntityRecord( kind, type, id, edits, { undoIgnore: true, } ); }, - [ type, id ] + [ kind, type, id ] ); return [ blocks ?? EMPTY_ARRAY, onInput, onChange ]; From e81edb985ae0f7300076c5fa2fa95a08248a0d42 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 5 Jan 2021 16:47:08 +0100 Subject: [PATCH 4/5] stabilize reusable blocks test --- packages/e2e-tests/specs/editor/various/reusable-blocks.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js index 46d887e4376efb..6cd4b93e93c1dd 100644 --- a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js +++ b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js @@ -292,7 +292,7 @@ describe( 'Reusable blocks', () => { ); paragraphBlock.focus(); await pressKeyWithModifier( 'primary', 'a' ); - await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'End' ); await page.keyboard.type( ' modified' ); // Wait for async mode to dispatch the update. From 484248ba353e4f71974eb34a5f3e4a8e463fd35d Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 5 Jan 2021 17:20:04 +0100 Subject: [PATCH 5/5] Fix undo --- packages/core-data/src/entity-provider.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js index 6f64e803194e2f..1c772014a4051b 100644 --- a/packages/core-data/src/entity-provider.js +++ b/packages/core-data/src/entity-provider.js @@ -155,8 +155,8 @@ export function useEntityBlockEditor( kind, type, { id: _id } = {} ) { useEffect( () => { // Load the blocks from the content if not already in state // Guard against other instances that might have - // set content to a function already. - if ( content && typeof content !== 'function' ) { + // set content to a function already or the blocks are already in state. + if ( content && typeof content !== 'function' && ! blocks ) { const parsedContent = parse( content ); editEntityRecord( kind, @@ -174,6 +174,7 @@ export function useEntityBlockEditor( kind, type, { id: _id } = {} ) { ( newBlocks, options ) => { const { selectionStart, selectionEnd } = options; const edits = { blocks: newBlocks, selectionStart, selectionEnd }; + const noChange = blocks === edits.blocks; if ( noChange ) { return __unstableCreateUndoLevel( kind, type, id ); @@ -187,16 +188,14 @@ export function useEntityBlockEditor( kind, type, { id: _id } = {} ) { editEntityRecord( kind, type, id, edits ); }, - [ blocks, id, type, kind ] + [ kind, type, id, blocks ] ); const onInput = useCallback( ( newBlocks, options ) => { const { selectionStart, selectionEnd } = options; const edits = { blocks: newBlocks, selectionStart, selectionEnd }; - editEntityRecord( kind, type, id, edits, { - undoIgnore: true, - } ); + editEntityRecord( kind, type, id, edits ); }, [ kind, type, id ] );