From 6854ff6c636137240c558261f810b6214e642ee1 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 25 Jul 2019 16:06:41 -0400 Subject: [PATCH 01/24] Editor: Update the store to use Core Data entities. --- .../developers/data/data-core-editor.md | 18 +- packages/core-data/src/selectors.js | 3 +- packages/editor/src/store/actions.js | 320 ++++-------------- packages/editor/src/store/defaults.js | 7 - packages/editor/src/store/reducer.js | 194 +---------- packages/editor/src/store/selectors.js | 217 ++++++------ .../src/utils/with-change-detection/README.md | 41 --- .../src/utils/with-change-detection/index.js | 55 --- .../utils/with-change-detection/test/index.js | 113 ------- .../editor/src/utils/with-history/README.md | 42 --- .../editor/src/utils/with-history/index.js | 141 -------- .../src/utils/with-history/test/index.js | 253 -------------- 12 files changed, 183 insertions(+), 1221 deletions(-) delete mode 100644 packages/editor/src/utils/with-change-detection/README.md delete mode 100644 packages/editor/src/utils/with-change-detection/index.js delete mode 100644 packages/editor/src/utils/with-change-detection/test/index.js delete mode 100644 packages/editor/src/utils/with-history/README.md delete mode 100644 packages/editor/src/utils/with-history/index.js delete mode 100644 packages/editor/src/utils/with-history/test/index.js diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index b38e4ede0bf678..c1ec84dd33fb90 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -192,6 +192,8 @@ _Related_ # **getBlocksForSerialization** +> **Deprecated** since Gutenberg 6.2.0. + Returns a set of blocks which are to be used in consideration of the post's generated save content. @@ -1041,10 +1043,6 @@ _Parameters_ - _edits_ `Object`: Post attributes to edit. -_Returns_ - -- `Object`: Action object. - # **enablePublishSidebar** Returns an action object used in signalling that the user has enabled the @@ -1143,10 +1141,6 @@ _Related_ Returns an action object used in signalling that undo history should restore last popped state. -_Returns_ - -- `Object`: Action object. - # **refreshPost** Action generator for handling refreshing the current post. @@ -1205,10 +1199,6 @@ _Parameters_ - _blocks_ `Array`: Block Array. - _options_ `?Object`: Optional options. -_Returns_ - -- `Object`: Action object - # **resetPost** Returns an action object used in signalling that the latest version of the @@ -1322,10 +1312,6 @@ Action generator for trashing the current post in the editor. Returns an action object used in signalling that undo history should pop. -_Returns_ - -- `Object`: Action object. - # **unlockPostSaving** Returns an action object used to signal that post saving is unlocked. diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index c8aec286a1107c..a480f65c66fb16 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -156,8 +156,7 @@ export function getEntityRecordEdits( state, kind, name, recordId ) { export const getEntityRecordNonTransientEdits = createSelector( ( state, kind, name, recordId ) => { const { transientEdits = {} } = getEntity( state, kind, name ); - const edits = - getEntityRecordEdits( state, kind, name, recordId ) || []; + const edits = getEntityRecordEdits( state, kind, name, recordId ) || []; return Object.keys( edits ).reduce( ( acc, key ) => { if ( ! transientEdits[ key ] ) { acc[ key ] = edits[ key ]; diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 36190a77b47ade..23d8b0f4be84af 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -1,8 +1,7 @@ /** * External dependencies */ -import { castArray, pick, mapValues, has } from 'lodash'; -import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; +import { has, castArray } from 'lodash'; /** * WordPress dependencies @@ -12,25 +11,22 @@ import { dispatch, select, apiFetch } from '@wordpress/data-controls'; import { parse, synchronizeBlocksWithTemplate, + serialize, + isUnmodifiedDefaultBlock, + getFreeformContentHandlerName, } from '@wordpress/blocks'; +import { removep } from '@wordpress/autop'; import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies */ -import { - getPostRawValue, -} from './reducer'; import { STORE_KEY, POST_UPDATE_TRANSACTION_ID, - SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID, - AUTOSAVE_PROPERTIES, } from './constants'; import { - getNotificationArgumentsForSaveSuccess, - getNotificationArgumentsForSaveFail, getNotificationArgumentsForTrashFail, } from './utils/notice-builder'; import { awaitNextStateChange, getRegistry } from './controls'; @@ -183,7 +179,7 @@ export function* setupEditor( post, edits, template ) { edits, template, }; - yield resetEditorBlocks( blocks ); + yield resetEditorBlocks( blocks, { __unstableShouldCreateUndoLevel: false } ); yield setupEditorState( post ); yield* __experimentalSubscribeSources(); } @@ -295,73 +291,6 @@ export function* resetAutosave( newAutosave ) { export function __experimentalRequestPostUpdateStart( options = {} ) { return { type: 'REQUEST_POST_UPDATE_START', - optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, - options, - }; -} - -/** - * Optimistic action for indicating that the request post update has completed - * successfully. - * - * @param {Object} data The data for the action. - * @param {Object} data.previousPost The previous post prior to update. - * @param {Object} data.post The new post after update - * @param {boolean} data.isRevision Whether the post is a revision or not. - * @param {Object} data.options Options passed through from the original - * action dispatch. - * @param {Object} data.postType The post type object. - * - * @return {Object} Action object. - */ -export function __experimentalRequestPostUpdateSuccess( { - previousPost, - post, - isRevision, - options, - postType, -} ) { - return { - type: 'REQUEST_POST_UPDATE_SUCCESS', - previousPost, - post, - optimist: { - // Note: REVERT is not a failure case here. Rather, it - // is simply reversing the assumption that the updates - // were applied to the post proper, such that the post - // treated as having unsaved changes. - type: isRevision ? REVERT : COMMIT, - id: POST_UPDATE_TRANSACTION_ID, - }, - options, - postType, - }; -} - -/** - * Optimistic action for indicating that the request post update has completed - * with a failure. - * - * @param {Object} data The data for the action - * @param {Object} data.post The post that failed updating. - * @param {Object} data.edits The fields that were being updated. - * @param {*} data.error The error from the failed call. - * @param {Object} data.options Options passed through from the original - * action dispatch. - * @return {Object} An action object - */ -export function __experimentalRequestPostUpdateFailure( { - post, - edits, - error, - options, -} ) { - return { - type: 'REQUEST_POST_UPDATE_FAILURE', - optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID }, - post, - edits, - error, options, }; } @@ -402,13 +331,11 @@ export function setupEditorState( post ) { * * @param {Object} edits Post attributes to edit. * - * @return {Object} Action object. + * @yield {Object} Action object or control. */ -export function editPost( edits ) { - return { - type: 'EDIT_POST', - edits, - }; +export function* editPost( edits ) { + const { id, type } = yield select( 'core/editor', 'getCurrentPost' ); + yield dispatch( 'core', 'editEntityRecord', 'postType', type, id, edits ); } /** @@ -432,175 +359,17 @@ export function __experimentalOptimisticUpdatePost( edits ) { * @param {Object} options */ export function* savePost( options = {} ) { - const isEditedPostSaveable = yield select( - STORE_KEY, - 'isEditedPostSaveable' - ); - if ( ! isEditedPostSaveable ) { - return; - } - let edits = yield select( - STORE_KEY, - 'getPostEdits' - ); - const isAutosave = !! options.isAutosave; - - if ( isAutosave ) { - edits = pick( edits, AUTOSAVE_PROPERTIES ); - } - - const isEditedPostNew = yield select( - STORE_KEY, - 'isEditedPostNew', - ); - - // New posts (with auto-draft status) must be explicitly assigned draft - // status if there is not already a status assigned in edits (publish). - // Otherwise, they are wrongly left as auto-draft. Status is not always - // respected for autosaves, so it cannot simply be included in the pick - // above. This behavior relies on an assumption that an auto-draft post - // would never be saved by anyone other than the owner of the post, per - // logic within autosaves REST controller to save status field only for - // draft/auto-draft by current user. - // - // See: https://core.trac.wordpress.org/ticket/43316#comment:88 - // See: https://core.trac.wordpress.org/ticket/43316#comment:89 - if ( isEditedPostNew ) { - edits = { status: 'draft', ...edits }; - } - - const post = yield select( - STORE_KEY, - 'getCurrentPost' - ); - - const editedPostContent = yield select( - STORE_KEY, - 'getEditedPostContent' - ); - - let toSend = { - ...edits, - content: editedPostContent, - id: post.id, - }; - - const currentPostType = yield select( - STORE_KEY, - 'getCurrentPostType' - ); - - const postType = yield select( - 'core', - 'getPostType', - currentPostType - ); - - yield dispatch( - STORE_KEY, - '__experimentalRequestPostUpdateStart', - options, - ); - - // Optimistically apply updates under the assumption that the post - // will be updated. See below logic in success resolution for revert - // if the autosave is applied as a revision. + yield __experimentalRequestPostUpdateStart( options ); + const postType = yield select( 'core/editor', 'getCurrentPostType' ); + const postId = yield select( 'core/editor', 'getCurrentPostId' ); yield dispatch( - STORE_KEY, - '__experimentalOptimisticUpdatePost', - toSend + 'core', + 'saveEditedEntityRecord', + 'postType', + postType, + postId, + options ); - - let path = `/wp/v2/${ postType.rest_base }/${ post.id }`; - let method = 'PUT'; - if ( isAutosave ) { - const currentUser = yield select( 'core', 'getCurrentUser' ); - const currentUserId = currentUser ? currentUser.id : undefined; - const autosavePost = yield select( 'core', 'getAutosave', post.type, post.id, currentUserId ); - const mappedAutosavePost = mapValues( pick( autosavePost, AUTOSAVE_PROPERTIES ), getPostRawValue ); - - // Ensure autosaves contain all expected fields, using autosave or - // post values as fallback if not otherwise included in edits. - toSend = { - ...pick( post, AUTOSAVE_PROPERTIES ), - ...mappedAutosavePost, - ...toSend, - }; - path += '/autosaves'; - method = 'POST'; - } else { - yield dispatch( - 'core/notices', - 'removeNotice', - SAVE_POST_NOTICE_ID - ); - yield dispatch( - 'core/notices', - 'removeNotice', - 'autosave-exists' - ); - } - - try { - const newPost = yield apiFetch( { - path, - method, - data: toSend, - } ); - - if ( isAutosave ) { - yield dispatch( 'core', 'receiveAutosaves', post.id, newPost ); - } else { - yield dispatch( STORE_KEY, 'resetPost', newPost ); - } - - yield dispatch( - STORE_KEY, - '__experimentalRequestPostUpdateSuccess', - { - previousPost: post, - post: newPost, - options, - postType, - // An autosave may be processed by the server as a regular save - // when its update is requested by the author and the post was - // draft or auto-draft. - isRevision: newPost.id !== post.id, - } - ); - - const notifySuccessArgs = getNotificationArgumentsForSaveSuccess( { - previousPost: post, - post: newPost, - postType, - options, - } ); - if ( notifySuccessArgs.length > 0 ) { - yield dispatch( - 'core/notices', - 'createSuccessNotice', - ...notifySuccessArgs - ); - } - } catch ( error ) { - yield dispatch( - STORE_KEY, - '__experimentalRequestPostUpdateFailure', - { post, edits, error, options } - ); - const notifyFailArgs = getNotificationArgumentsForSaveFail( { - post, - edits, - error, - } ); - if ( notifyFailArgs.length > 0 ) { - yield dispatch( - 'core/notices', - 'createErrorNotice', - ...notifyFailArgs - ); - } - } } /** @@ -698,19 +467,19 @@ export function* autosave( options ) { * Returns an action object used in signalling that undo history should * restore last popped state. * - * @return {Object} Action object. + * @yield {Object} Action object. */ -export function redo() { - return { type: 'REDO' }; +export function* redo() { + yield dispatch( 'core', 'redo' ); } /** * Returns an action object used in signalling that undo history should pop. * - * @return {Object} Action object. + * @yield {Object} Action object. */ -export function undo() { - return { type: 'UNDO' }; +export function* undo() { + yield dispatch( 'core', 'undo' ); } /** @@ -904,7 +673,7 @@ export function unlockPostSaving( lockName ) { * @param {Array} blocks Block Array. * @param {?Object} options Optional options. * - * @return {Object} Action object + * @yield {Object} Action object */ export function* resetEditorBlocks( blocks, options = {} ) { const lastBlockAttributesChange = yield select( 'core/block-editor', '__experimentalGetLastBlockAttributeChanges' ); @@ -943,11 +712,38 @@ export function* resetEditorBlocks( blocks, options = {} ) { yield* resetLastBlockSourceDependencies( Array.from( updatedSources ) ); } - return { - type: 'RESET_EDITOR_BLOCKS', - blocks: yield* getBlocksWithSourcedAttributes( blocks ), - shouldCreateUndoLevel: options.__unstableShouldCreateUndoLevel !== false, - }; + const edits = { blocks: yield* getBlocksWithSourcedAttributes( blocks ) }; + + if ( options.__unstableShouldCreateUndoLevel !== false ) { + edits.content = ( () => { + let blocksForSerialization = edits.blocks; + + // 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; + } )(); + } + + yield* editPost( edits ); } /* diff --git a/packages/editor/src/store/defaults.js b/packages/editor/src/store/defaults.js index 07c92803bd0a13..f158a4664dec7c 100644 --- a/packages/editor/src/store/defaults.js +++ b/packages/editor/src/store/defaults.js @@ -8,13 +8,6 @@ export const PREFERENCES_DEFAULTS = { isPublishSidebarEnabled: true, }; -/** - * Default initial edits state. - * - * @type {Object} - */ -export const INITIAL_EDITS_DEFAULTS = {}; - /** * The default post editor settings * diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index ef6ad6fd798c07..241eb7d974c42b 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -2,14 +2,7 @@ * External dependencies */ import optimist from 'redux-optimist'; -import { - flow, - reduce, - omit, - mapValues, - keys, - isEqual, -} from 'lodash'; +import { reduce, omit, keys, isEqual } from 'lodash'; /** * WordPress dependencies @@ -20,14 +13,7 @@ import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { - PREFERENCES_DEFAULTS, - INITIAL_EDITS_DEFAULTS, - EDITOR_SETTINGS_DEFAULTS, -} from './defaults'; -import { EDIT_MERGE_PROPERTIES } from './constants'; -import withChangeDetection from '../utils/with-change-detection'; -import withHistory from '../utils/with-history'; +import { PREFERENCES_DEFAULTS, EDITOR_SETTINGS_DEFAULTS } from './defaults'; /** * Returns a post attribute value, flattening nested rendered content using its @@ -114,165 +100,23 @@ export function shouldOverwriteState( action, previousAction ) { return isUpdatingSamePostProperty( action, previousAction ); } -/** - * Undoable reducer returning the editor post state, including blocks parsed - * from current HTML markup. - * - * Handles the following state keys: - * - edits: an object describing changes to be made to the current post, in - * the format accepted by the WP REST API - * - blocks: post content blocks - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -export const editor = flow( [ - combineReducers, - - withHistory( { - resetTypes: [ 'SETUP_EDITOR_STATE' ], - ignoreTypes: [ - 'RESET_POST', - 'UPDATE_POST', - ], - shouldOverwriteState, - } ), -] )( { - // Track whether changes exist, resetting at each post save. Relies on - // editor initialization firing post reset as an effect. - blocks: withChangeDetection( { - resetTypes: [ 'SETUP_EDITOR_STATE', 'REQUEST_POST_UPDATE_START' ], - } )( ( state = { value: [] }, action ) => { - switch ( action.type ) { - case 'RESET_EDITOR_BLOCKS': - if ( action.blocks === state.value ) { - return state; - } - return { value: action.blocks }; - } - - return state; - } ), - edits( state = {}, action ) { - switch ( action.type ) { - case 'EDIT_POST': - return reduce( action.edits, ( result, value, key ) => { - // Only assign into result if not already same value - if ( value !== state[ key ] ) { - result = getMutateSafeObject( state, result ); - - if ( EDIT_MERGE_PROPERTIES.has( key ) ) { - // Merge properties should assign to current value. - result[ key ] = { ...result[ key ], ...value }; - } else { - // Otherwise override. - result[ key ] = value; - } - } - - return result; - }, state ); - case 'UPDATE_POST': - case 'RESET_POST': - const getCanonicalValue = action.type === 'UPDATE_POST' ? - ( key ) => action.edits[ key ] : - ( key ) => getPostRawValue( action.post[ key ] ); - - return reduce( state, ( result, value, key ) => { - if ( ! isEqual( value, getCanonicalValue( key ) ) ) { - return result; - } - - result = getMutateSafeObject( state, result ); - delete result[ key ]; - return result; - }, state ); - case 'RESET_EDITOR_BLOCKS': - if ( 'content' in state ) { - return omit( state, 'content' ); - } - - return state; - } - - return state; - }, -} ); - -/** - * Reducer returning the initial edits state. With matching shape to that of - * `editor.edits`, the initial edits are those applied programmatically, are - * not considered in prompting the user for unsaved changes, and are included - * in (and reset by) the next save payload. - * - * @param {Object} state Current state. - * @param {Object} action Action object. - * - * @return {Object} Next state. - */ -export function initialEdits( state = INITIAL_EDITS_DEFAULTS, action ) { +export function postId( state = null, action ) { switch ( action.type ) { - case 'SETUP_EDITOR': - if ( ! action.edits ) { - break; - } - - return action.edits; - case 'SETUP_EDITOR_STATE': - if ( 'content' in state ) { - return omit( state, 'content' ); - } - - return state; - - case 'UPDATE_POST': - return reduce( action.edits, ( result, value, key ) => { - if ( ! result.hasOwnProperty( key ) ) { - return result; - } - - result = getMutateSafeObject( state, result ); - delete result[ key ]; - return result; - }, state ); - case 'RESET_POST': - return INITIAL_EDITS_DEFAULTS; + case 'UPDATE_POST': + return action.post.id; } return state; } -/** - * Reducer returning the last-known state of the current post, in the format - * returned by the WP REST API. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -export function currentPost( state = {}, action ) { +export function postType( state = null, action ) { switch ( action.type ) { case 'SETUP_EDITOR_STATE': case 'RESET_POST': case 'UPDATE_POST': - let post; - if ( action.post ) { - post = action.post; - } else if ( action.edits ) { - post = { - ...state, - ...action.edits, - }; - } else { - return state; - } - - return mapValues( post, getPostRawValue ); + return action.post.type; } return state; @@ -337,25 +181,6 @@ export function saving( state = {}, action ) { switch ( action.type ) { case 'REQUEST_POST_UPDATE_START': return { - requesting: true, - successful: false, - error: null, - options: action.options || {}, - }; - - case 'REQUEST_POST_UPDATE_SUCCESS': - return { - requesting: false, - successful: true, - error: null, - options: action.options || {}, - }; - - case 'REQUEST_POST_UPDATE_FAILURE': - return { - requesting: false, - successful: false, - error: action.error, options: action.options || {}, }; } @@ -587,9 +412,8 @@ export function editorSettings( state = EDITOR_SETTINGS_DEFAULTS, action ) { } export default optimist( combineReducers( { - editor, - initialEdits, - currentPost, + postId, + postType, preferences, saving, postLock, diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index fa6843010ab704..bd90568055ede0 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -16,13 +16,11 @@ import createSelector from 'rememo'; * WordPress dependencies */ import { - serialize, getFreeformContentHandlerName, getDefaultBlockName, isUnmodifiedDefaultBlock, } from '@wordpress/blocks'; import { isInTheFuture, getDate } from '@wordpress/date'; -import { removep } from '@wordpress/autop'; import { addQueryArgs } from '@wordpress/url'; import { createRegistrySelector } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; @@ -49,6 +47,15 @@ import { getPostRawValue } from './reducer'; */ const EMPTY_OBJECT = {}; +/** + * Shared reference to an empty array for cases where it is important to avoid + * returning a new array reference on every invocation, as in a connected or + * other pure component which performs `shouldComponentUpdate` check on props. + * This should be used as a last resort, since the normalized data should be + * maintained by the reducer result in state. + */ +const EMPTY_ARRAY = []; + /** * Returns true if any past editor history snapshots exist, or false otherwise. * @@ -56,9 +63,9 @@ const EMPTY_OBJECT = {}; * * @return {boolean} Whether undo history exists. */ -export function hasEditorUndo( state ) { - return state.editor.past.length > 0; -} +export const hasEditorUndo = createRegistrySelector( ( select ) => () => { + return select( 'core' ).hasUndo(); +} ); /** * Returns true if any future editor history snapshots exist, or false @@ -68,9 +75,9 @@ export function hasEditorUndo( state ) { * * @return {boolean} Whether redo history exists. */ -export function hasEditorRedo( state ) { - return state.editor.future.length > 0; -} +export const hasEditorRedo = createRegistrySelector( ( select ) => () => { + return select( 'core' ).hasRedo(); +} ); /** * Returns true if the currently edited post is yet to be saved, or false if @@ -92,15 +99,17 @@ export function isEditedPostNew( state ) { * @return {boolean} Whether content includes unsaved changes. */ export function hasChangedContent( state ) { + const edits = getPostEdits( state ); + return ( - state.editor.present.blocks.isDirty || + 'blocks' in edits || // `edits` is intended to contain only values which are different from // the saved post, so the mere presence of a property is an indicator // that the value is different than what is known to be saved. While // content in Visual mode is represented by the blocks state, in Text // mode it is tracked by `edits.content`. - 'content' in state.editor.present.edits + 'content' in edits ); } @@ -112,25 +121,16 @@ export function hasChangedContent( state ) { * * @return {boolean} Whether unsaved values exist. */ -export function isEditedPostDirty( state ) { - if ( hasChangedContent( state ) ) { - return true; - } - +export const isEditedPostDirty = createRegistrySelector( ( select ) => ( state ) => { // Edits should contain only fields which differ from the saved post (reset // at initial load and save complete). Thus, a non-empty edits state can be // inferred to contain unsaved values. - if ( Object.keys( state.editor.present.edits ).length > 0 ) { + const postType = getCurrentPostType( state ); + const postId = getCurrentPostId( state ); + if ( select( 'core' ).hasEditsForEntityRecord( 'postType', postType, postId ) ) { return true; } - - // Edits and change detection are reset at the start of a save, but a post - // is still considered dirty until the point at which the save completes. - // Because the save is performed optimistically, the prior states are held - // until committed. These can be referenced to determine whether there's a - // chance that state may be reverted into one considered dirty. - return inSomeHistory( state, isEditedPostDirty ); -} +} ); /** * Returns true if there are no unsaved values for the current edit session and @@ -153,9 +153,20 @@ export function isCleanNewPost( state ) { * * @return {Object} Post object. */ -export function getCurrentPost( state ) { - return state.currentPost; -} +export const getCurrentPost = createRegistrySelector( ( select ) => ( state ) => { + const postId = getCurrentPostId( state ); + const postType = getCurrentPostType( state ); + + const post = select( 'core' ).getEntityRecord( 'postType', postType, postId ); + if ( post ) { + return post; + } + + // This exists for compatibility with the previous selector behavior + // which would guarantee an object return based on the editor reducer's + // default empty object state. + return EMPTY_OBJECT; +} ); /** * Returns the post type of the post currently being edited. @@ -165,7 +176,7 @@ export function getCurrentPost( state ) { * @return {string} Post type. */ export function getCurrentPostType( state ) { - return state.currentPost.type; + return state.postType; } /** @@ -177,7 +188,7 @@ export function getCurrentPostType( state ) { * @return {?number} ID of current post. */ export function getCurrentPostId( state ) { - return getCurrentPost( state ).id || null; + return state.postId; } /** @@ -211,18 +222,11 @@ export function getCurrentPostLastRevisionId( state ) { * * @return {Object} Object of key value pairs comprising unsaved edits. */ -export const getPostEdits = createSelector( - ( state ) => { - return { - ...state.initialEdits, - ...state.editor.present.edits, - }; - }, - ( state ) => [ - state.editor.present.edits, - state.initialEdits, - ] -); +export const getPostEdits = createRegistrySelector( ( select ) => ( state ) => { + const postType = getCurrentPostType( state ); + const postId = getCurrentPostId( state ); + return select( 'core' ).getEntityRecordEdits( 'postType', postType, postId ) || EMPTY_OBJECT; +} ); /** * Returns a new reference when edited values have changed. This is useful in @@ -256,9 +260,20 @@ export const getReferenceByDistinctEdits = createSelector( * @return {*} Post attribute value. */ export function getCurrentPostAttribute( state, attributeName ) { - const post = getCurrentPost( state ); - if ( post.hasOwnProperty( attributeName ) ) { - return post[ attributeName ]; + switch ( attributeName ) { + case 'type': + return getCurrentPostType( state ); + + case 'id': + return getCurrentPostId( state ); + + default: + const post = getCurrentPost( state ); + if ( ! post.hasOwnProperty( attributeName ) ) { + break; + } + + return getPostRawValue( post[ attributeName ] ); } } @@ -272,23 +287,17 @@ export function getCurrentPostAttribute( state, attributeName ) { * * @return {*} Post attribute value. */ -const getNestedEditedPostProperty = createSelector( - ( state, attributeName ) => { - const edits = getPostEdits( state ); - if ( ! edits.hasOwnProperty( attributeName ) ) { - return getCurrentPostAttribute( state, attributeName ); - } +const getNestedEditedPostProperty = ( state, attributeName ) => { + const edits = getPostEdits( state ); + if ( ! edits.hasOwnProperty( attributeName ) ) { + return getCurrentPostAttribute( state, attributeName ); + } - return { - ...getCurrentPostAttribute( state, attributeName ), - ...edits[ attributeName ], - }; - }, - ( state, attributeName ) => [ - get( state.editor.present.edits, [ attributeName ], EMPTY_OBJECT ), - get( state.currentPost, [ attributeName ], EMPTY_OBJECT ), - ] -); + return { + ...getCurrentPostAttribute( state, attributeName ), + ...edits[ attributeName ], + }; +}; /** * Returns a single attribute of the post being edited, preferring the unsaved @@ -478,7 +487,7 @@ export function isEditedPostEmpty( state ) { // condition of the mere existence of blocks. Note that the value of edited // content takes precedent over block content, and must fall through to the // default logic. - const blocks = state.editor.present.blocks.value; + const blocks = getEditorBlocks( state ); if ( blocks.length && ! ( 'content' in getPostEdits( state ) ) ) { // Pierce the abstraction of the serializer in knowing that blocks are @@ -654,9 +663,11 @@ export function isEditedPostDateFloating( state ) { * * @return {boolean} Whether post is being saved. */ -export function isSavingPost( state ) { - return state.saving.requesting; -} +export const isSavingPost = createRegistrySelector( ( select ) => ( state ) => { + const postType = getCurrentPostType( state ); + const postId = getCurrentPostId( state ); + return select( 'core' ).isSavingEntityRecord( 'postType', postType, postId ); +} ); /** * Returns true if a previous post save was attempted successfully, or false @@ -666,9 +677,13 @@ export function isSavingPost( state ) { * * @return {boolean} Whether the post was saved successfully. */ -export function didPostSaveRequestSucceed( state ) { - return state.saving.successful; -} +export const didPostSaveRequestSucceed = createRegistrySelector( + ( select ) => ( state ) => { + const postType = getCurrentPostType( state ); + const postId = getCurrentPostId( state ); + return ! select( 'core' ).getLastEntitySaveError( 'postType', postType, postId ); + } +); /** * Returns true if a previous post save was attempted but failed, or false @@ -678,9 +693,13 @@ export function didPostSaveRequestSucceed( state ) { * * @return {boolean} Whether the post save failed. */ -export function didPostSaveRequestFail( state ) { - return !! state.saving.error; -} +export const didPostSaveRequestFail = createRegistrySelector( + ( select ) => ( state ) => { + const postType = getCurrentPostType( state ); + const postId = getCurrentPostId( state ); + return !! select( 'core' ).getLastEntitySaveError( 'postType', postType, postId ); + } +); /** * Returns true if the post is autosaving, or false otherwise. @@ -690,7 +709,10 @@ export function didPostSaveRequestFail( state ) { * @return {boolean} Whether the post is autosaving. */ export function isAutosavingPost( state ) { - return isSavingPost( state ) && !! state.saving.options.isAutosave; + if ( ! isSavingPost( state ) ) { + return false; + } + return !! state.saving.options.isAutosave; } /** @@ -701,7 +723,10 @@ export function isAutosavingPost( state ) { * @return {boolean} Whether the post is being previewed. */ export function isPreviewingPost( state ) { - return isSavingPost( state ) && !! state.saving.options.isPreview; + if ( ! isSavingPost( state ) ) { + return false; + } + return !! state.saving.options.isPreview; } /** @@ -731,7 +756,7 @@ export function getEditedPostPreviewLink( state ) { * @return {?string} Suggested post format. */ export function getSuggestedPostFormat( state ) { - const blocks = state.editor.present.blocks.value; + const blocks = getEditorBlocks( state ); let name; // If there is only one block in the content of the post grab its name @@ -774,11 +799,19 @@ export function getSuggestedPostFormat( state ) { * Returns a set of blocks which are to be used in consideration of the post's * generated save content. * + * @deprecated since Gutenberg 6.2.0. + * * @param {Object} state Editor state. * * @return {WPBlock[]} Filtered set of blocks for save. */ export function getBlocksForSerialization( state ) { + deprecated( '`core/editor` getBlocksForSerialization selector', { + plugin: 'Gutenberg', + alternative: 'getEditorBlocks', + hint: 'Blocks serialization pre-processing occurs at save time', + } ); + const blocks = state.editor.present.blocks.value; // WARNING: Any changes to the logic of this function should be verified @@ -808,36 +841,12 @@ export function getBlocksForSerialization( state ) { * * @return {string} Post content. */ -export const getEditedPostContent = createSelector( - ( state ) => { - const edits = getPostEdits( state ); - if ( 'content' in edits ) { - return edits.content; - } - - const blocks = getBlocksForSerialization( state ); - const content = serialize( blocks ); - - // For compatibility purposes, treat a post consisting of a single - // freeform block as legacy content and downgrade to a pre-block-editor - // removep'd content format. - const isSingleFreeformBlock = ( - blocks.length === 1 && - blocks[ 0 ].name === getFreeformContentHandlerName() - ); - - if ( isSingleFreeformBlock ) { - return removep( content ); - } - - return content; - }, - ( state ) => [ - state.editor.present.blocks.value, - state.editor.present.edits.content, - state.initialEdits.content, - ], -); +export const getEditedPostContent = createRegistrySelector( ( select ) => ( state ) => { + const postId = getCurrentPostId( state ); + const postType = getCurrentPostType( state ); + const record = select( 'core' ).getEditedEntityRecord( 'postType', postType, postId ); + return record ? record.content : ''; +} ); /** * Returns the reusable block with the given ID. @@ -1130,7 +1139,7 @@ export function isPublishSidebarEnabled( state ) { * @return {Array} Block list. */ export function getEditorBlocks( state ) { - return state.editor.present.blocks.value; + return getEditedPostAttribute( state, 'blocks' ) || EMPTY_ARRAY; } /** diff --git a/packages/editor/src/utils/with-change-detection/README.md b/packages/editor/src/utils/with-change-detection/README.md deleted file mode 100644 index 75f6061483f832..00000000000000 --- a/packages/editor/src/utils/with-change-detection/README.md +++ /dev/null @@ -1,41 +0,0 @@ -withChangeDetection -=================== - -`withChangeDetection` is a [Redux higher-order reducer](http://redux.js.org/docs/recipes/reducers/ReusingReducerLogic.html#customizing-behavior-with-higher-order-reducers) for tracking changes to reducer state over time. - -It does this based on the following assumptions: - -- The original reducer returns an object -- The original reducer returns a new reference only if a change has in-fact occurred - -Using these assumptions, the enhanced reducer returned from `withChangeDetection` will include a new property on the object `isDirty` corresponding to whether the original reference of the reducer has ever changed. - -Leveraging a `resetTypes` option, this can be used to mark intervals at which a state is considered to be clean (without changes) and dirty (with changes). - -## Example - -Considering a simple count reducer, we can enhance it with `withChangeDetection` to reflect whether changes have occurred: - -```js -function counter( state = { count: 0 }, action ) { - switch ( action.type ) { - case 'INCREMENT': - return { ...state, count: state.count + 1 }; - } - - return state; -} - -const enhancedCounter = withChangeDetection( counter, { resetTypes: [ 'RESET' ] } ); - -let state; - -state = enhancedCounter( undefined, {} ); -// { count: 0, isDirty: false } - -state = enhancedCounter( state, { type: 'INCREMENT' } ); -// { count: 1, isDirty: true } - -state = enhancedCounter( state, { type: 'RESET' } ); -// { count: 1, isDirty: false } -``` diff --git a/packages/editor/src/utils/with-change-detection/index.js b/packages/editor/src/utils/with-change-detection/index.js deleted file mode 100644 index e86db9a9905d4b..00000000000000 --- a/packages/editor/src/utils/with-change-detection/index.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * External dependencies - */ -import { includes } from 'lodash'; - -/** - * Higher-order reducer creator for tracking changes to state over time. The - * returned reducer will include a `isDirty` property on the object reflecting - * whether the original reference of the reducer has changed. - * - * @param {?Object} options Optional options. - * @param {?Array} options.ignoreTypes Action types upon which to skip check. - * @param {?Array} options.resetTypes Action types upon which to reset dirty. - * - * @return {Function} Higher-order reducer. - */ -const withChangeDetection = ( options = {} ) => ( reducer ) => { - return ( state, action ) => { - let nextState = reducer( state, action ); - - // Reset at: - // - Initial state - // - Reset types - const isReset = ( - state === undefined || - includes( options.resetTypes, action.type ) - ); - - const isChanging = state !== nextState; - - // If not intending to update dirty flag, return early and avoid clone. - if ( ! isChanging && ! isReset ) { - return state; - } - - // Avoid mutating state, unless it's already changing by original - // reducer and not initial. - if ( ! isChanging || state === undefined ) { - nextState = { ...nextState }; - } - - const isIgnored = includes( options.ignoreTypes, action.type ); - - if ( isIgnored ) { - // Preserve the original value if ignored. - nextState.isDirty = state.isDirty; - } else { - nextState.isDirty = ! isReset && isChanging; - } - - return nextState; - }; -}; - -export default withChangeDetection; diff --git a/packages/editor/src/utils/with-change-detection/test/index.js b/packages/editor/src/utils/with-change-detection/test/index.js deleted file mode 100644 index 06abccb50fc5b6..00000000000000 --- a/packages/editor/src/utils/with-change-detection/test/index.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * External dependencies - */ -import deepFreeze from 'deep-freeze'; - -/** - * Internal dependencies - */ -import withChangeDetection from '../'; - -describe( 'withChangeDetection()', () => { - const initialState = deepFreeze( { count: 0 } ); - - function originalReducer( state = initialState, action ) { - switch ( action.type ) { - case 'INCREMENT': - return { - count: state.count + 1, - }; - - case 'RESET_AND_CHANGE_REFERENCE': - return { - count: state.count, - }; - } - - return state; - } - - it( 'should respect original reducer behavior', () => { - const reducer = withChangeDetection()( originalReducer ); - - const state = reducer( undefined, {} ); - expect( state ).toEqual( { count: 0, isDirty: false } ); - - const nextState = reducer( deepFreeze( state ), { type: 'INCREMENT' } ); - expect( nextState ).not.toBe( state ); - expect( nextState ).toEqual( { count: 1, isDirty: true } ); - } ); - - it( 'should allow reset types as option', () => { - const reducer = withChangeDetection( { resetTypes: [ 'RESET' ] } )( originalReducer ); - - let state; - - state = reducer( undefined, {} ); - expect( state ).toEqual( { count: 0, isDirty: false } ); - - state = reducer( deepFreeze( state ), { type: 'INCREMENT' } ); - expect( state ).toEqual( { count: 1, isDirty: true } ); - - state = reducer( deepFreeze( state ), { type: 'RESET' } ); - expect( state ).toEqual( { count: 1, isDirty: false } ); - } ); - - it( 'should allow ignore types as option', () => { - const reducer = withChangeDetection( { ignoreTypes: [ 'INCREMENT' ] } )( originalReducer ); - - let state; - - state = reducer( undefined, {} ); - expect( state ).toEqual( { count: 0, isDirty: false } ); - - state = reducer( deepFreeze( state ), { type: 'INCREMENT' } ); - expect( state ).toEqual( { count: 1, isDirty: false } ); - } ); - - it( 'should preserve isDirty into non-resetting non-reference-changing types', () => { - const reducer = withChangeDetection( { resetTypes: [ 'RESET' ] } )( originalReducer ); - - let state; - - state = reducer( undefined, {} ); - expect( state ).toEqual( { count: 0, isDirty: false } ); - - state = reducer( deepFreeze( state ), { type: 'INCREMENT' } ); - expect( state ).toEqual( { count: 1, isDirty: true } ); - - const afterState = reducer( deepFreeze( state ), {} ); - expect( afterState ).toEqual( { count: 1, isDirty: true } ); - expect( afterState ).toBe( state ); - } ); - - it( 'should maintain separate states', () => { - const reducer = withChangeDetection()( originalReducer ); - - let firstState; - - firstState = reducer( undefined, {} ); - expect( firstState ).toEqual( { count: 0, isDirty: false } ); - - const secondState = reducer( undefined, { type: 'INCREMENT' } ); - expect( secondState ).toEqual( { count: 1, isDirty: false } ); - - firstState = reducer( deepFreeze( firstState ), {} ); - expect( firstState ).toEqual( { count: 0, isDirty: false } ); - } ); - - it( 'should flag as not dirty even if reset type causes reference change', () => { - const reducer = withChangeDetection( { resetTypes: [ 'RESET_AND_CHANGE_REFERENCE' ] } )( originalReducer ); - - let state; - - state = reducer( undefined, {} ); - expect( state ).toEqual( { count: 0, isDirty: false } ); - - state = reducer( deepFreeze( state ), { type: 'INCREMENT' } ); - expect( state ).toEqual( { count: 1, isDirty: true } ); - - state = reducer( deepFreeze( state ), { type: 'RESET_AND_CHANGE_REFERENCE' } ); - expect( state ).toEqual( { count: 1, isDirty: false } ); - } ); -} ); diff --git a/packages/editor/src/utils/with-history/README.md b/packages/editor/src/utils/with-history/README.md deleted file mode 100644 index 3c90ed2d895be8..00000000000000 --- a/packages/editor/src/utils/with-history/README.md +++ /dev/null @@ -1,42 +0,0 @@ -withHistory -=========== - -`withHistory` is a [Redux higher-order reducer](http://redux.js.org/docs/recipes/reducers/ReusingReducerLogic.html#customizing-behavior-with-higher-order-reducers) for tracking the history of a reducer state over time. The enhanced reducer returned from `withHistory` will return an object shape with properties `past`, `present`, and `future`. The `present` value maintains the current value of state returned from the original reducer. Past and future are respectively maintained as arrays of state values occurring previously and future (if history undone). - -Leveraging a `resetTypes` option, this can be used to mark intervals at which a state history should be reset, emptying the values of the `past` and `future` arrays. - -History can be adjusted by dispatching actions with type `UNDO` (reset to the previous state) and `REDO` (reset to the next state). - -## Example - -Considering a simple count reducer, we can enhance it with `withHistory` to track value over time: - -```js -function counter( state = { count: 0 }, action ) { - switch ( action.type ) { - case 'INCREMENT': - return { ...state, count: state.count + 1 }; - } - - return state; -} - -const enhancedCounter = withHistory( counter, { resetTypes: [ 'RESET' ] } ); - -let state; - -state = enhancedCounter( undefined, {} ); -// { past: [], present: 0, future: [] } - -state = enhancedCounter( state, { type: 'INCREMENT' } ); -// { past: [ 0 ], present: 1, future: [] } - -state = enhancedCounter( state, { type: 'UNDO' } ); -// { past: [], present: 0, future: [ 1 ] } - -state = enhancedCounter( state, { type: 'REDO' } ); -// { past: [ 0 ], present: 1, future: [] } - -state = enhancedCounter( state, { type: 'RESET' } ); -// { past: [], present: 1, future: [] } -``` diff --git a/packages/editor/src/utils/with-history/index.js b/packages/editor/src/utils/with-history/index.js deleted file mode 100644 index 665851e632bd3f..00000000000000 --- a/packages/editor/src/utils/with-history/index.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * External dependencies - */ -import { overSome, includes, first, last, drop, dropRight } from 'lodash'; - -/** - * Default options for withHistory reducer enhancer. Refer to withHistory - * documentation for options explanation. - * - * @see withHistory - * - * @type {Object} - */ -const DEFAULT_OPTIONS = { - resetTypes: [], - ignoreTypes: [], - shouldOverwriteState: () => false, -}; - -/** - * Higher-order reducer creator which transforms the result of the original - * reducer into an object tracking its own history (past, present, future). - * - * @param {?Object} options Optional options. - * @param {?Array} options.resetTypes Action types upon which to - * clear past. - * @param {?Array} options.ignoreTypes Action types upon which to - * avoid history tracking. - * @param {?Function} options.shouldOverwriteState Function receiving last and - * current actions, returning - * boolean indicating whether - * present should be merged, - * rather than add undo level. - * - * @return {Function} Higher-order reducer. - */ -const withHistory = ( options = {} ) => ( reducer ) => { - options = { ...DEFAULT_OPTIONS, ...options }; - - // `ignoreTypes` is simply a convenience for `shouldOverwriteState` - options.shouldOverwriteState = overSome( [ - options.shouldOverwriteState, - ( action ) => includes( options.ignoreTypes, action.type ), - ] ); - - const initialState = { - past: [], - present: reducer( undefined, {} ), - future: [], - lastAction: null, - shouldCreateUndoLevel: false, - }; - - const { - resetTypes = [], - shouldOverwriteState = () => false, - } = options; - - return ( state = initialState, action ) => { - const { past, present, future, lastAction, shouldCreateUndoLevel } = state; - const previousAction = lastAction; - - switch ( action.type ) { - case 'UNDO': - // Can't undo if no past. - if ( ! past.length ) { - return state; - } - - return { - past: dropRight( past ), - present: last( past ), - future: [ present, ...future ], - lastAction: null, - shouldCreateUndoLevel: false, - }; - case 'REDO': - // Can't redo if no future. - if ( ! future.length ) { - return state; - } - - return { - past: [ ...past, present ], - present: first( future ), - future: drop( future ), - lastAction: null, - shouldCreateUndoLevel: false, - }; - - case 'CREATE_UNDO_LEVEL': - return { - ...state, - lastAction: null, - shouldCreateUndoLevel: true, - }; - } - - const nextPresent = reducer( present, action ); - - if ( includes( resetTypes, action.type ) ) { - return { - past: [], - present: nextPresent, - future: [], - lastAction: null, - shouldCreateUndoLevel: false, - }; - } - - if ( present === nextPresent ) { - return state; - } - - let nextPast = past; - // The `lastAction` property is used to compare actions in the - // `shouldOverwriteState` option. If an action should be ignored, do not - // submit that action as the last action, otherwise the ability to - // compare subsequent actions will break. - let lastActionToSubmit = previousAction; - - if ( - shouldCreateUndoLevel || - ! past.length || - ! shouldOverwriteState( action, previousAction ) - ) { - nextPast = [ ...past, present ]; - lastActionToSubmit = action; - } - - return { - past: nextPast, - present: nextPresent, - future: [], - shouldCreateUndoLevel: false, - lastAction: lastActionToSubmit, - }; - }; -}; - -export default withHistory; diff --git a/packages/editor/src/utils/with-history/test/index.js b/packages/editor/src/utils/with-history/test/index.js deleted file mode 100644 index e383988864f77f..00000000000000 --- a/packages/editor/src/utils/with-history/test/index.js +++ /dev/null @@ -1,253 +0,0 @@ -/** - * Internal dependencies - */ -import withHistory from '../'; - -describe( 'withHistory', () => { - const counter = ( state = 0, { type } ) => ( - type === 'INCREMENT' ? state + 1 : state - ); - - it( 'should return a new reducer', () => { - const reducer = withHistory()( counter ); - - expect( typeof reducer ).toBe( 'function' ); - expect( reducer( undefined, {} ) ).toEqual( { - past: [], - present: 0, - future: [], - lastAction: null, - shouldCreateUndoLevel: false, - } ); - } ); - - it( 'should track history', () => { - const reducer = withHistory()( counter ); - - let state; - const action = { type: 'INCREMENT' }; - state = reducer( undefined, {} ); - state = reducer( state, action ); - - expect( state ).toEqual( { - past: [ 0 ], - present: 1, - future: [], - lastAction: action, - shouldCreateUndoLevel: false, - } ); - - state = reducer( state, action ); - - expect( state ).toEqual( { - past: [ 0, 1 ], - present: 2, - future: [], - lastAction: action, - shouldCreateUndoLevel: false, - } ); - } ); - - it( 'should perform undo', () => { - const reducer = withHistory()( counter ); - - let state; - state = reducer( undefined, {} ); - state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'UNDO' } ); - - expect( state ).toEqual( { - past: [], - present: 0, - future: [ 1 ], - lastAction: null, - shouldCreateUndoLevel: false, - } ); - } ); - - it( 'should not perform undo on empty past', () => { - const reducer = withHistory()( counter ); - const state = reducer( undefined, {} ); - - expect( state ).toBe( reducer( state, { type: 'UNDO' } ) ); - } ); - - it( 'should perform redo', () => { - const reducer = withHistory()( counter ); - - let state; - state = reducer( undefined, {} ); - state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'UNDO' } ); - state = reducer( state, { type: 'REDO' } ); - - expect( state ).toEqual( { - past: [ 0 ], - present: 1, - future: [], - lastAction: null, - shouldCreateUndoLevel: false, - } ); - } ); - - it( 'should not perform redo on empty future', () => { - const reducer = withHistory()( counter ); - const state = reducer( undefined, {} ); - - expect( state ).toBe( reducer( state, { type: 'REDO' } ) ); - } ); - - it( 'should reset history by options.resetTypes', () => { - const reducer = withHistory( { resetTypes: [ 'RESET_HISTORY' ] } )( counter ); - - let state; - state = reducer( undefined, {} ); - state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'RESET_HISTORY' } ); - - expect( state ).toEqual( { - past: [], - present: 1, - future: [], - lastAction: null, - shouldCreateUndoLevel: false, - } ); - } ); - - it( 'should ignore history by options.ignoreTypes', () => { - const reducer = withHistory( { ignoreTypes: [ 'INCREMENT' ] } )( counter ); - - let state; - state = reducer( undefined, {} ); - state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'INCREMENT' } ); - - expect( state ).toEqual( { - past: [ 0 ], // Needs at least one history - present: 2, - future: [], - lastAction: { type: 'INCREMENT' }, - shouldCreateUndoLevel: false, - } ); - } ); - - it( 'should return same reference if state has not changed', () => { - const reducer = withHistory()( counter ); - const original = reducer( undefined, {} ); - const state = reducer( original, {} ); - - expect( state ).toBe( original ); - } ); - - it( 'should overwrite present state with option.shouldOverwriteState', () => { - const reducer = withHistory( { - shouldOverwriteState: ( { type } ) => type === 'INCREMENT', - } )( counter ); - - let state; - state = reducer( undefined, {} ); - state = reducer( state, { type: 'INCREMENT' } ); - - expect( state ).toEqual( { - past: [ 0 ], - present: 1, - future: [], - lastAction: { type: 'INCREMENT' }, - shouldCreateUndoLevel: false, - } ); - - state = reducer( state, { type: 'INCREMENT' } ); - - expect( state ).toEqual( { - past: [ 0 ], - present: 2, - future: [], - lastAction: { type: 'INCREMENT' }, - shouldCreateUndoLevel: false, - } ); - } ); - - it( 'should overwrite present state with option.shouldOverwriteState right after ignored action', () => { - const complexCounter = ( state = { count: 0 }, action ) => { - if ( action.type === 'INCREMENT' ) { - return { - ...state, - count: state.count + 1, - }; - } else if ( action.type === 'IGNORE' ) { - return { - ...state, - ignore: action.content, - }; - } - - return state; - }; - - const reducer = withHistory( { - shouldOverwriteState: ( action, previousAction ) => ( - previousAction && action.type === previousAction.type - ), - ignoreTypes: [ 'IGNORE' ], - } )( complexCounter ); - - let state = reducer( reducer( undefined, {} ), { type: 'INCREMENT' } ); - - expect( state ).toEqual( { - past: [ { count: 0 } ], - present: { count: 1 }, - future: [], - lastAction: { type: 'INCREMENT' }, - shouldCreateUndoLevel: false, - } ); - - state = reducer( state, { type: 'IGNORE', content: 'ignore' } ); - - expect( state ).toEqual( { - past: [ { count: 0 } ], - present: { count: 1, ignore: 'ignore' }, - future: [], - lastAction: { type: 'INCREMENT' }, - shouldCreateUndoLevel: false, - } ); - - state = reducer( state, { type: 'INCREMENT' } ); - - expect( state ).toEqual( { - past: [ { count: 0 } ], - present: { count: 2, ignore: 'ignore' }, - future: [], - lastAction: { type: 'INCREMENT' }, - shouldCreateUndoLevel: false, - } ); - } ); - - it( 'should create undo level with option.shouldOverwriteState and CREATE_UNDO_LEVEL', () => { - const reducer = withHistory( { - shouldOverwriteState: ( { type } ) => type === 'INCREMENT', - } )( counter ); - - let state; - state = reducer( undefined, {} ); - state = reducer( state, { type: 'INCREMENT' } ); - state = reducer( state, { type: 'CREATE_UNDO_LEVEL' } ); - - expect( state ).toEqual( { - past: [ 0 ], - present: 1, - future: [], - lastAction: null, - shouldCreateUndoLevel: true, - } ); - - state = reducer( state, { type: 'INCREMENT' } ); - - expect( state ).toEqual( { - past: [ 0, 1 ], - present: 2, - future: [], - lastAction: { type: 'INCREMENT' }, - shouldCreateUndoLevel: false, - } ); - } ); -} ); From f0fb34b55a216e7734a63e759ef16583e6fc8d75 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Tue, 6 Aug 2019 17:24:41 -0400 Subject: [PATCH 02/24] Editor: Fix selector test suites. --- packages/editor/src/store/actions.js | 59 +++-- packages/editor/src/store/selectors.js | 20 +- packages/editor/src/store/test/selectors.js | 257 ++++++++++++-------- 3 files changed, 198 insertions(+), 138 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 23d8b0f4be84af..22208180110e93 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -667,6 +667,38 @@ export function unlockPostSaving( lockName ) { }; } +/** + * Serializes blocks following backwards compatibility conventions. + * + * @param {Array} blocksForSerialization The blocks to serialize. + * + * @return {string} The blocks serialization. + */ +export function serializeBlocks( 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; +} + /** * Returns an action object used to signal that the blocks have been updated. * @@ -715,32 +747,7 @@ export function* resetEditorBlocks( blocks, options = {} ) { const edits = { blocks: yield* getBlocksWithSourcedAttributes( blocks ) }; if ( options.__unstableShouldCreateUndoLevel !== false ) { - edits.content = ( () => { - let blocksForSerialization = edits.blocks; - - // 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; - } )(); + edits.content = serializeBlocks( edits.blocks ); } yield* editPost( edits ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index bd90568055ede0..5cec6084823b8b 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -130,6 +130,7 @@ export const isEditedPostDirty = createRegistrySelector( ( select ) => ( state ) if ( select( 'core' ).hasEditsForEntityRecord( 'postType', postType, postId ) ) { return true; } + return false; } ); /** @@ -401,15 +402,19 @@ export function isCurrentPostPending( state ) { /** * Return true if the current post has already been published. * - * @param {Object} state Global application state. + * @param {Object} state Global application state. + * @param {Object?} currentPost Explicit current post for bypassing registry selector. * * @return {boolean} Whether the post has been published. */ -export function isCurrentPostPublished( state ) { - const post = getCurrentPost( state ); +export function isCurrentPostPublished( state, currentPost ) { + const post = currentPost || getCurrentPost( state ); - return [ 'publish', 'private' ].indexOf( post.status ) !== -1 || - ( post.status === 'future' && ! isInTheFuture( new Date( Number( getDate( post.date ) ) - ONE_MINUTE_IN_MS ) ) ); + return ( + [ 'publish', 'private' ].indexOf( post.status ) !== -1 || + ( post.status === 'future' && + ! isInTheFuture( new Date( Number( getDate( post.date ) ) - ONE_MINUTE_IN_MS ) ) ) + ); } /** @@ -965,7 +970,10 @@ export function isPublishingPost( state ) { // Consider as publishing when current post prior to request was not // considered published - return !! stateBeforeRequest && ! isCurrentPostPublished( stateBeforeRequest ); + return ( + !! stateBeforeRequest && + ! isCurrentPostPublished( null, stateBeforeRequest.currentPost ) + ); } /** diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 6d10b7f863aead..d11bc2350750f6 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -20,10 +20,111 @@ import { RawHTML } from '@wordpress/element'; /** * Internal dependencies */ -import * as selectors from '../selectors'; +import { serializeBlocks } from '../actions'; +import * as _selectors from '../selectors'; import { PREFERENCES_DEFAULTS } from '../defaults'; import { POST_UPDATE_TRANSACTION_ID } from '../constants'; +const selectors = { ..._selectors }; +const selectorNames = Object.keys( selectors ); +selectorNames.forEach( ( name ) => { + selectors[ name ] = ( state, ...args ) => { + const select = () => ( { + getEntityRecord() { + return state.currentPost; + }, + + getEntityRecordEdits() { + const present = state.editor && state.editor.present; + let edits = present && present.edits; + + if ( state.initialEdits ) { + edits = { + ...state.initialEdits, + ...edits, + }; + } + + const { value: blocks, isDirty } = ( present && present.blocks ) || {}; + if ( blocks && isDirty !== false ) { + edits = { + ...edits, + blocks, + }; + } + + return edits; + }, + + hasEditsForEntityRecord() { + return Object.keys( this.getEntityRecordEdits() ).length > 0; + }, + + getEditedEntityRecord() { + let edits = this.getEntityRecordEdits(); + if ( edits.content === undefined && edits.blocks ) { + edits = { + ...edits, + content: serializeBlocks( edits.blocks ), + }; + } + return { + ...this.getEntityRecord(), + ...edits, + }; + }, + + isSavingEntityRecord() { + return state.saving && state.saving.requesting; + }, + + getLastEntitySaveError() { + const saving = state.saving; + const successful = saving && saving.successful; + const error = saving && saving.error; + return successful === undefined ? error : ! successful; + }, + + hasUndo() { + return Boolean( + state.editor && state.editor.past && state.editor.past.length + ); + }, + + hasRedo() { + return Boolean( + state.editor && state.editor.future && state.editor.future.length + ); + }, + + getCurrentUser() { + return state.getCurrentUser && state.getCurrentUser(); + }, + + hasFetchedAutosaves() { + return state.hasFetchedAutosaves && state.hasFetchedAutosaves(); + }, + + getAutosave() { + return state.getAutosave && state.getAutosave(); + }, + } ); + + selectorNames.forEach( ( otherName ) => { + if ( _selectors[ otherName ].isRegistrySelector ) { + _selectors[ otherName ].registry = { select }; + } + } ); + + return _selectors[ name ]( state, ...args ); + }; + selectors[ name ].isRegistrySelector = _selectors[ name ].isRegistrySelector; + if ( selectors[ name ].isRegistrySelector ) { + selectors[ name ].registry = { + select: () => _selectors[ name ].registry.select(), + }; + } +} ); const { hasEditorUndo, hasEditorRedo, @@ -44,7 +145,7 @@ const { isCurrentPostScheduled, isEditedPostPublishable, isEditedPostSaveable, - isEditedPostAutosaveable: _isEditedPostAutosaveableRegistrySelector, + isEditedPostAutosaveable, isEditedPostEmpty, isEditedPostBeingScheduled, isEditedPostDateFloating, @@ -71,14 +172,9 @@ const { describe( 'selectors', () => { let cachedSelectors; - let isEditedPostAutosaveableRegistrySelector; beforeAll( () => { cachedSelectors = filter( selectors, ( selector ) => selector.clear ); - isEditedPostAutosaveableRegistrySelector = ( select ) => { - _isEditedPostAutosaveableRegistrySelector.registry = { select }; - return _isEditedPostAutosaveableRegistrySelector; - }; } ); beforeEach( () => { @@ -354,37 +450,6 @@ describe( 'selectors', () => { expect( isEditedPostDirty( state ) ).toBe( true ); } ); - - it( 'should return true if pending transaction with dirty state', () => { - const state = { - optimist: [ - { - beforeState: { - editor: { - present: { - blocks: { - isDirty: true, - value: [], - }, - edits: {}, - }, - }, - }, - }, - ], - editor: { - present: { - blocks: { - isDirty: false, - value: [], - }, - edits: {}, - }, - }, - }; - - expect( isEditedPostDirty( state ) ).toBe( true ); - } ); } ); describe( 'isCleanNewPost', () => { @@ -471,7 +536,7 @@ describe( 'selectors', () => { describe( 'getCurrentPostId', () => { it( 'should return null if the post has not yet been saved', () => { const state = { - currentPost: {}, + postId: null, }; expect( getCurrentPostId( state ) ).toBeNull(); @@ -479,7 +544,7 @@ describe( 'selectors', () => { it( 'should return the current post ID', () => { const state = { - currentPost: { id: 1 }, + postId: 1, }; expect( getCurrentPostId( state ) ).toBe( 1 ); @@ -673,9 +738,7 @@ describe( 'selectors', () => { describe( 'getCurrentPostType', () => { it( 'should return the post type', () => { const state = { - currentPost: { - type: 'post', - }, + postType: 'post', }; expect( getCurrentPostType( state ) ).toBe( 'post' ); @@ -1272,18 +1335,6 @@ describe( 'selectors', () => { describe( 'isEditedPostAutosaveable', () => { it( 'should return false if existing autosaves have not yet been fetched', () => { - const isEditedPostAutosaveable = isEditedPostAutosaveableRegistrySelector( () => ( { - getCurrentUser() {}, - hasFetchedAutosaves() { - return false; - }, - getAutosave() { - return { - title: 'sassel', - }; - }, - } ) ); - const state = { editor: { present: { @@ -1300,24 +1351,21 @@ describe( 'selectors', () => { saving: { requesting: true, }, - }; - - expect( isEditedPostAutosaveable( state ) ).toBe( false ); - } ); - - it( 'should return false if the post is not saveable', () => { - const isEditedPostAutosaveable = isEditedPostAutosaveableRegistrySelector( () => ( { getCurrentUser() {}, hasFetchedAutosaves() { - return true; + return false; }, getAutosave() { return { title: 'sassel', }; }, - } ) ); + }; + expect( isEditedPostAutosaveable( state ) ).toBe( false ); + } ); + + it( 'should return false if the post is not saveable', () => { const state = { editor: { present: { @@ -1334,20 +1382,21 @@ describe( 'selectors', () => { saving: { requesting: true, }, + getCurrentUser() {}, + hasFetchedAutosaves() { + return true; + }, + getAutosave() { + return { + title: 'sassel', + }; + }, }; expect( isEditedPostAutosaveable( state ) ).toBe( false ); } ); it( 'should return true if there is no autosave', () => { - const isEditedPostAutosaveable = isEditedPostAutosaveableRegistrySelector( () => ( { - getCurrentUser() {}, - hasFetchedAutosaves() { - return true; - }, - getAutosave() {}, - } ) ); - const state = { editor: { present: { @@ -1362,25 +1411,17 @@ describe( 'selectors', () => { title: 'sassel', }, saving: {}, + getCurrentUser() {}, + hasFetchedAutosaves() { + return true; + }, + getAutosave() {}, }; expect( isEditedPostAutosaveable( state ) ).toBe( true ); } ); it( 'should return false if none of title, excerpt, or content have changed', () => { - const isEditedPostAutosaveable = isEditedPostAutosaveableRegistrySelector( () => ( { - getCurrentUser() {}, - hasFetchedAutosaves() { - return true; - }, - getAutosave() { - return { - title: 'foo', - excerpt: 'foo', - }; - }, - } ) ); - const state = { editor: { present: { @@ -1397,13 +1438,6 @@ describe( 'selectors', () => { excerpt: 'foo', }, saving: {}, - }; - - expect( isEditedPostAutosaveable( state ) ).toBe( false ); - } ); - - it( 'should return true if content has changes', () => { - const isEditedPostAutosaveable = isEditedPostAutosaveableRegistrySelector( () => ( { getCurrentUser() {}, hasFetchedAutosaves() { return true; @@ -1414,8 +1448,12 @@ describe( 'selectors', () => { excerpt: 'foo', }; }, - } ) ); + }; + expect( isEditedPostAutosaveable( state ) ).toBe( false ); + } ); + + it( 'should return true if content has changes', () => { const state = { editor: { present: { @@ -1431,6 +1469,16 @@ describe( 'selectors', () => { excerpt: 'foo', }, saving: {}, + getCurrentUser() {}, + hasFetchedAutosaves() { + return true; + }, + getAutosave() { + return { + title: 'foo', + excerpt: 'foo', + }; + }, }; expect( isEditedPostAutosaveable( state ) ).toBe( true ); @@ -1439,19 +1487,6 @@ describe( 'selectors', () => { it( 'should return true if title or excerpt have changed', () => { for ( const variantField of [ 'title', 'excerpt' ] ) { for ( const constantField of without( [ 'title', 'excerpt' ], variantField ) ) { - const isEditedPostAutosaveable = isEditedPostAutosaveableRegistrySelector( () => ( { - getCurrentUser() {}, - hasFetchedAutosaves() { - return true; - }, - getAutosave() { - return { - [ constantField ]: 'foo', - [ variantField ]: 'bar', - }; - }, - } ) ); - const state = { editor: { present: { @@ -1468,6 +1503,16 @@ describe( 'selectors', () => { content: 'foo', }, saving: {}, + getCurrentUser() {}, + hasFetchedAutosaves() { + return true; + }, + getAutosave() { + return { + [ constantField ]: 'foo', + [ variantField ]: 'bar', + }; + }, }; expect( isEditedPostAutosaveable( state ) ).toBe( true ); From b4837092ce2dc9d10c2fb403a6977f7229f3c4ef Mon Sep 17 00:00:00 2001 From: epiqueras Date: Wed, 7 Aug 2019 13:33:47 -0400 Subject: [PATCH 03/24] Editor: Fix some legacy selectors and behaviors. --- packages/core-data/src/reducer.js | 11 +++--- packages/editor/src/store/actions.js | 46 ++++++++++++++------------ packages/editor/src/store/selectors.js | 15 ++++++--- 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 7f0972e19e481b..780828786f3576 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -310,8 +310,8 @@ export function undo( state = UNDO_INITIAL_STATE, action ) { } // Transient edits don't create an undo level, but are - // reachable in the next meaningful edit to which they - // are merged. They are defined in the entity's config. + // added to the last level right before a new level + // is added. if ( ! Object.keys( action.edits ).some( ( key ) => ! action.transientEdits[ key ] ) ) { const nextState = [ ...state ]; nextState.flattenedUndo = { ...state.flattenedUndo, ...action.edits }; @@ -333,7 +333,10 @@ export function undo( state = UNDO_INITIAL_STATE, action ) { } else { // Clear potential redos, because this only supports linear history. nextState = state.slice( 0, state.offset || undefined ); - nextState.flattenedUndo = state.flattenedUndo; + const lastItem = nextState[ nextState.length - 1 ]; + if ( lastItem ) { + lastItem.edits = { ...lastItem.edits, ...state.flattenedUndo }; + } } nextState.offset = 0; @@ -341,7 +344,7 @@ export function undo( state = UNDO_INITIAL_STATE, action ) { kind: action.kind, name: action.name, recordId: action.recordId, - edits: { ...nextState.flattenedUndo, ...action.edits }, + edits: action.edits, } ); return nextState; diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 22208180110e93..da0aca634cf3bc 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -2,6 +2,7 @@ * External dependencies */ import { has, castArray } from 'lodash'; +import memoize from 'memize'; /** * WordPress dependencies @@ -674,30 +675,33 @@ export function unlockPostSaving( lockName ) { * * @return {string} The blocks serialization. */ -export function serializeBlocks( blocksForSerialization ) { - // A single unmodified default block is assumed to - // be equivalent to an empty post. - if ( - blocksForSerialization.length === 1 && - isUnmodifiedDefaultBlock( blocksForSerialization[ 0 ] ) - ) { - blocksForSerialization = []; - } +export 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 ); + 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 ); - } + // 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; -} + return content; + }, + { maxSize: 1 } +); /** * Returns an action object used to signal that the blocks have been updated. diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 5cec6084823b8b..eea8780da473e1 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -37,6 +37,7 @@ import { AUTOSAVE_PROPERTIES, } from './constants'; import { getPostRawValue } from './reducer'; +import { serializeBlocks } from './actions'; /** * Shared reference to an empty object for cases where it is important to avoid @@ -839,8 +840,7 @@ export function getBlocksForSerialization( state ) { } /** - * Returns the content of the post being edited, preferring raw string edit - * before falling back to serialization of block state. + * Returns the content of the post being edited. * * @param {Object} state Global application state. * @@ -849,8 +849,15 @@ export function getBlocksForSerialization( state ) { export const getEditedPostContent = createRegistrySelector( ( select ) => ( state ) => { const postId = getCurrentPostId( state ); const postType = getCurrentPostType( state ); - const record = select( 'core' ).getEditedEntityRecord( 'postType', postType, postId ); - return record ? record.content : ''; + const record = select( 'core' ).getEditedEntityRecord( + 'postType', + postType, + postId + ); + if ( record ) { + return record.blocks ? serializeBlocks( record.blocks ) : record.content || ''; + } + return ''; } ); /** From 892fd2e2d35645b9401f6674aa45de989d68bc7d Mon Sep 17 00:00:00 2001 From: epiqueras Date: Wed, 7 Aug 2019 19:27:22 -0400 Subject: [PATCH 04/24] Editor: Fix action tests. --- packages/editor/src/store/test/actions.js | 475 +++------------------- 1 file changed, 66 insertions(+), 409 deletions(-) diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index ecb588c0912235..0850bdea661e5e 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; - /** * WordPress dependencies */ @@ -14,7 +9,6 @@ import { select, dispatch, apiFetch } from '@wordpress/data-controls'; import * as actions from '../actions'; import { STORE_KEY, - SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID, POST_UPDATE_TRANSACTION_ID, } from '../constants'; @@ -59,36 +53,14 @@ const postType = { }; const postId = 44; const postTypeSlug = 'post'; -const userId = 1; describe( 'Post generator actions', () => { describe( 'savePost()', () => { let fulfillment, - edits, currentPost, currentPostStatus, - currentUser, - editPostToSendOptimistic, - autoSavePost, - autoSavePostToSend, - savedPost, - savedPostStatus, - isAutosave, - isEditedPostNew, - savedPostMessage; + isAutosave; beforeEach( () => { - edits = ( defaultStatus = null ) => { - const postObject = { - title: 'foo', - content: 'bar', - excerpt: 'cheese', - foo: 'bar', - }; - if ( defaultStatus !== null ) { - postObject.status = defaultStatus; - } - return postObject; - }; currentPost = () => ( { id: postId, type: postTypeSlug, @@ -97,323 +69,65 @@ describe( 'Post generator actions', () => { excerpt: 'crackers', status: currentPostStatus, } ); - currentUser = { id: userId }; - editPostToSendOptimistic = () => { - const postObject = { - ...edits(), - content: editedPostContent, - id: currentPost().id, - }; - if ( ! postObject.status && isEditedPostNew ) { - postObject.status = 'draft'; - } - if ( isAutosave ) { - delete postObject.foo; - } - return postObject; - }; - autoSavePost = { status: 'autosave', bar: 'foo' }; - autoSavePostToSend = () => editPostToSendOptimistic(); - savedPost = () => ( - { - ...currentPost(), - ...editPostToSendOptimistic(), - content: editedPostContent, - status: savedPostStatus, - } - ); } ); - const editedPostContent = 'to infinity and beyond'; const reset = ( isAutosaving ) => fulfillment = actions.savePost( { isAutosave: isAutosaving } ); - const rewind = ( isAutosaving, isNewPost ) => { - reset( isAutosaving ); - fulfillment.next(); - fulfillment.next( true ); - fulfillment.next( edits() ); - fulfillment.next( isNewPost ); - fulfillment.next( currentPost() ); - fulfillment.next( editedPostContent ); - fulfillment.next( postTypeSlug ); - fulfillment.next( postType ); - fulfillment.next(); - if ( isAutosaving ) { - fulfillment.next( currentUser ); - fulfillment.next(); - } else { - fulfillment.next(); - fulfillment.next(); - } - }; - const initialTestConditions = [ + const testConditions = [ [ - 'yields action for selecting if edited post is saveable', + 'yields an action for signalling that an update to the post started', () => true, () => { reset( isAutosave ); const { value } = fulfillment.next(); - expect( value ).toEqual( - select( STORE_KEY, 'isEditedPostSaveable' ) - ); - }, - ], - [ - 'yields action for selecting the post edits done', - () => true, - () => { - const { value } = fulfillment.next( true ); - expect( value ).toEqual( - select( STORE_KEY, 'getPostEdits' ) - ); - }, - ], - [ - 'yields action for selecting whether the edited post is new', - () => true, - () => { - const { value } = fulfillment.next( edits() ); - expect( value ).toEqual( - select( STORE_KEY, 'isEditedPostNew' ) - ); - }, - ], - [ - 'yields action for selecting the current post', - () => true, - () => { - const { value } = fulfillment.next( isEditedPostNew ); - expect( value ).toEqual( - select( STORE_KEY, 'getCurrentPost' ) - ); - }, - ], - [ - 'yields action for selecting the edited post content', - () => true, - () => { - const { value } = fulfillment.next( currentPost() ); - expect( value ).toEqual( - select( STORE_KEY, 'getEditedPostContent' ) - ); + expect( value ).toEqual( { + type: 'REQUEST_POST_UPDATE_START', + options: { isAutosave }, + } ); }, ], [ - 'yields action for selecting current post type slug', + 'yields an action for selecting the current post type', () => true, () => { - const { value } = fulfillment.next( editedPostContent ); + const { value } = fulfillment.next(); expect( value ).toEqual( select( STORE_KEY, 'getCurrentPostType' ) ); }, ], [ - 'yields action for selecting the post type object', + 'yields an action for selecting the current post ID', () => true, () => { - const { value } = fulfillment.next( postTypeSlug ); + const { value } = fulfillment.next( currentPost().type ); expect( value ).toEqual( - select( 'core', 'getPostType', postTypeSlug ) + select( STORE_KEY, 'getCurrentPostId' ) ); }, ], [ - 'yields action for dispatching request post update start', + 'yields an action for dispatching an update to the post entity', () => true, () => { - const { value } = fulfillment.next( postType ); + const { value } = fulfillment.next( currentPost().id ); expect( value ).toEqual( dispatch( - STORE_KEY, - '__experimentalRequestPostUpdateStart', + 'core', + 'saveEditedEntityRecord', + 'postType', + currentPost().type, + currentPost().id, { isAutosave } ) ); }, ], [ - 'yields action for dispatching optimistic update of post', + 'implicitly returns undefined', () => true, () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( - dispatch( - STORE_KEY, - '__experimentalOptimisticUpdatePost', - editPostToSendOptimistic() - ) - ); - }, - ], - [ - 'yields action for dispatching the removal of save post notice', - ( isAutosaving ) => ! isAutosaving, - () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( - dispatch( - 'core/notices', - 'removeNotice', - SAVE_POST_NOTICE_ID, - ) - ); - }, - ], - [ - 'yields action for dispatching the removal of autosave notice', - ( isAutosaving ) => ! isAutosaving, - () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( - dispatch( - 'core/notices', - 'removeNotice', - 'autosave-exists' - ) - ); - }, - ], - [ - 'yields action for selecting the currentUser', - ( isAutosaving ) => isAutosaving, - () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( - select( 'core', 'getCurrentUser' ) - ); - }, - ], - [ - 'yields action for selecting the autosavePost', - ( isAutosaving ) => isAutosaving, - () => { - const { value } = fulfillment.next( currentUser ); - expect( value ).toEqual( - select( - 'core', - 'getAutosave', - postTypeSlug, - postId, - userId - ) - ); - }, - ], - ]; - const fetchErrorConditions = [ - [ - 'yields action for dispatching post update failure', - () => { - const error = { foo: 'bar', code: 'fail' }; - apiFetchThrowError( error ); - const editsObject = edits(); - const { value } = isAutosave ? - fulfillment.next( autoSavePost ) : - fulfillment.next(); - if ( isAutosave ) { - delete editsObject.foo; - } - expect( value ).toEqual( - dispatch( - STORE_KEY, - '__experimentalRequestPostUpdateFailure', - { - post: currentPost(), - edits: isEditedPostNew ? - { ...editsObject, status: 'draft' } : - editsObject, - error, - options: { isAutosave }, - } - ) - ); - }, - ], - [ - 'yields action for dispatching an appropriate error notice', - () => { - const { value } = fulfillment.next( [ 'foo', 'bar' ] ); - expect( value ).toEqual( - dispatch( - 'core/notices', - 'createErrorNotice', - ...[ 'Updating failed.', { id: 'SAVE_POST_NOTICE_ID' } ] - ) - ); - }, - ], - ]; - const fetchSuccessConditions = [ - [ - 'yields action for updating the post via the api', - () => { - apiFetchDoActual(); - rewind( isAutosave, isEditedPostNew ); - const { value } = isAutosave ? - fulfillment.next( autoSavePost ) : - fulfillment.next(); - const data = isAutosave ? - autoSavePostToSend() : - editPostToSendOptimistic(); - const path = isAutosave ? '/autosaves' : ''; - expect( value ).toEqual( - apiFetch( - { - path: `/wp/v2/${ postType.rest_base }/${ editPostToSendOptimistic().id }${ path }`, - method: isAutosave ? 'POST' : 'PUT', - data, - } - ) - ); - }, - ], - [ - 'yields action for dispatch the appropriate reset action', - () => { - const { value } = fulfillment.next( savedPost() ); - - if ( isAutosave ) { - expect( value ).toEqual( dispatch( 'core', 'receiveAutosaves', postId, savedPost() ) ); - } else { - expect( value ).toEqual( dispatch( STORE_KEY, 'resetPost', savedPost() ) ); - } - }, - ], - [ - 'yields action for dispatching the post update success', - () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( - dispatch( - STORE_KEY, - '__experimentalRequestPostUpdateSuccess', - { - previousPost: currentPost(), - post: savedPost(), - options: { isAutosave }, - postType, - isRevision: false, - } - ) - ); - }, - ], - [ - 'yields dispatch action for success notification', - () => { - const { value } = fulfillment.next( [ 'foo', 'bar' ] ); - const expected = isAutosave ? - undefined : - dispatch( - 'core/notices', - 'createSuccessNotice', - ...[ - savedPostMessage, - { actions: [], id: 'SAVE_POST_NOTICE_ID', type: 'snackbar' }, - ] - ); - expect( value ).toEqual( expected ); + expect( fulfillment.next() ).toEqual( { done: true, value: undefined } ); }, ], ]; @@ -430,74 +144,28 @@ describe( 'Post generator actions', () => { } }; - const testRunRoutine = ( [ testDescription, testRoutine ] ) => { - it( testDescription, () => { - testRoutine(); - } ); - }; - - describe( 'yields with expected responses when edited post is not saveable', () => { - it( 'yields action for selecting if edited post is saveable', () => { - reset( false ); - const { value } = fulfillment.next(); - expect( value ).toEqual( - select( STORE_KEY, 'isEditedPostSaveable' ) - ); - } ); - it( 'if edited post is not saveable then bails', () => { - const { value, done } = fulfillment.next( false ); - expect( done ).toBe( true ); - expect( value ).toBeUndefined(); - } ); - } ); - describe( 'yields with expected responses for when not autosaving and edited post is new', () => { + describe( 'yields with expected responses for when not autosaving ' + + 'and edited post is new', () => { beforeEach( () => { isAutosave = false; - isEditedPostNew = true; - savedPostStatus = 'publish'; currentPostStatus = 'draft'; - savedPostMessage = 'Post published'; - } ); - initialTestConditions.forEach( conditionalRunTestRoutine( false ) ); - describe( 'fetch action throwing an error', () => { - fetchErrorConditions.forEach( testRunRoutine ); - } ); - describe( 'fetch action not throwing an error', () => { - fetchSuccessConditions.forEach( testRunRoutine ); } ); + testConditions.forEach( conditionalRunTestRoutine( false ) ); } ); describe( 'yields with expected responses for when not autosaving and edited post is not new', () => { beforeEach( () => { isAutosave = false; - isEditedPostNew = false; currentPostStatus = 'publish'; - savedPostStatus = 'publish'; - savedPostMessage = 'Updated Post'; - } ); - initialTestConditions.forEach( conditionalRunTestRoutine( false ) ); - describe( 'fetch action throwing error', () => { - fetchErrorConditions.forEach( testRunRoutine ); - } ); - describe( 'fetch action not throwing error', () => { - fetchSuccessConditions.forEach( testRunRoutine ); } ); + testConditions.forEach( conditionalRunTestRoutine( false ) ); } ); describe( 'yields with expected responses for when autosaving is true and edited post is not new', () => { beforeEach( () => { isAutosave = true; - isEditedPostNew = false; currentPostStatus = 'autosave'; - savedPostStatus = 'publish'; - savedPostMessage = 'Post published'; - } ); - initialTestConditions.forEach( conditionalRunTestRoutine( true ) ); - describe( 'fetch action throwing error', () => { - fetchErrorConditions.forEach( testRunRoutine ); - } ); - describe( 'fetch action not throwing error', () => { - fetchSuccessConditions.forEach( testRunRoutine ); } ); + testConditions.forEach( conditionalRunTestRoutine( true ) ); } ); } ); describe( 'autosave()', () => { @@ -663,7 +331,7 @@ describe( 'Editor actions', () => { } ); it( 'should yield action object for resetEditorBlocks', () => { const { value } = fulfillment.next(); - expect( value ).toEqual( actions.resetEditorBlocks( [] ) ); + expect( Object.keys( value ) ).toEqual( [] ); } ); it( 'should yield action object for setupEditorState', () => { const { value } = fulfillment.next(); @@ -691,47 +359,7 @@ describe( 'Editor actions', () => { const result = actions.__experimentalRequestPostUpdateStart(); expect( result ).toEqual( { type: 'REQUEST_POST_UPDATE_START', - optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, - options: {}, - } ); - } ); - } ); - - describe( 'requestPostUpdateSuccess', () => { - it( 'should return the REQUEST_POST_UPDATE_SUCCESS action', () => { - const testActionData = { - previousPost: {}, - post: {}, options: {}, - postType: 'post', - }; - const result = actions.__experimentalRequestPostUpdateSuccess( { - ...testActionData, - isRevision: false, - } ); - expect( result ).toEqual( { - ...testActionData, - type: 'REQUEST_POST_UPDATE_SUCCESS', - optimist: { type: COMMIT, id: POST_UPDATE_TRANSACTION_ID }, - } ); - } ); - } ); - - describe( 'requestPostUpdateFailure', () => { - it( 'should return the REQUEST_POST_UPDATE_FAILURE action', () => { - const testActionData = { - post: {}, - options: {}, - edits: {}, - error: {}, - }; - const result = actions.__experimentalRequestPostUpdateFailure( - testActionData - ); - expect( result ).toEqual( { - ...testActionData, - type: 'REQUEST_POST_UPDATE_FAILURE', - optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID }, } ); } ); } ); @@ -748,11 +376,28 @@ describe( 'Editor actions', () => { } ); describe( 'editPost', () => { - it( 'should return EDIT_POST action', () => { + it( 'should edit the relevant entity record', () => { const edits = { format: 'sample' }; - expect( actions.editPost( edits ) ).toEqual( { - type: 'EDIT_POST', - edits, + const fulfillment = actions.editPost( edits ); + expect( fulfillment.next() ).toEqual( { + done: false, + value: select( STORE_KEY, 'getCurrentPost' ), + } ); + const post = { id: 1, type: 'post' }; + expect( fulfillment.next( post ) ).toEqual( { + done: false, + value: dispatch( + 'core', + 'editEntityRecord', + 'postType', + post.type, + post.id, + edits + ), + } ); + expect( fulfillment.next() ).toEqual( { + done: true, + value: undefined, } ); } ); } ); @@ -770,17 +415,29 @@ describe( 'Editor actions', () => { } ); describe( 'redo', () => { - it( 'should return REDO action', () => { - expect( actions.redo() ).toEqual( { - type: 'REDO', + it( 'should yield the REDO action', () => { + const fulfillment = actions.redo(); + expect( fulfillment.next() ).toEqual( { + done: false, + value: dispatch( 'core', 'redo' ), + } ); + expect( fulfillment.next() ).toEqual( { + done: true, + value: undefined, } ); } ); } ); describe( 'undo', () => { - it( 'should return UNDO action', () => { - expect( actions.undo() ).toEqual( { - type: 'UNDO', + it( 'should yield the UNDO action', () => { + const fulfillment = actions.undo(); + expect( fulfillment.next() ).toEqual( { + done: false, + value: dispatch( 'core', 'undo' ), + } ); + expect( fulfillment.next() ).toEqual( { + done: true, + value: undefined, } ); } ); } ); From 8bcbd14de08e87aa7dcdd343c1f6e359ab8b85b4 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Wed, 7 Aug 2019 19:55:11 -0400 Subject: [PATCH 05/24] Editor: Fix remaining broken unit tests. --- packages/editor/src/store/test/reducer.js | 361 +------------------- packages/editor/src/store/test/selectors.js | 12 +- 2 files changed, 8 insertions(+), 365 deletions(-) diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index d964c72c0f77a3..3b31970aff30aa 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -11,16 +11,12 @@ import { isUpdatingSamePostProperty, shouldOverwriteState, getPostRawValue, - initialEdits, - editor, - currentPost, preferences, saving, reusableBlocks, postSavingLock, previewLink, } from '../reducer'; -import { INITIAL_EDITS_DEFAULTS } from '../defaults'; describe( 'state', () => { describe( 'hasSameKeys()', () => { @@ -156,326 +152,6 @@ describe( 'state', () => { } ); } ); - describe( 'editor()', () => { - describe( 'blocks()', () => { - it( 'should set its value by RESET_EDITOR_BLOCKS', () => { - const blocks = [ { - clientId: 'block3', - innerBlocks: [ - { clientId: 'block31', innerBlocks: [] }, - { clientId: 'block32', innerBlocks: [] }, - ], - } ]; - const state = editor( undefined, { - type: 'RESET_EDITOR_BLOCKS', - blocks, - } ); - - expect( state.present.blocks.value ).toBe( blocks ); - } ); - } ); - - describe( 'edits()', () => { - it( 'should save newly edited properties', () => { - const original = editor( undefined, { - type: 'EDIT_POST', - edits: { - status: 'draft', - title: 'post title', - }, - } ); - - const state = editor( original, { - type: 'EDIT_POST', - edits: { - tags: [ 1 ], - }, - } ); - - expect( state.present.edits ).toEqual( { - status: 'draft', - title: 'post title', - tags: [ 1 ], - } ); - } ); - - it( 'should return same reference if no changed properties', () => { - const original = editor( undefined, { - type: 'EDIT_POST', - edits: { - status: 'draft', - title: 'post title', - }, - } ); - - const state = editor( original, { - type: 'EDIT_POST', - edits: { - status: 'draft', - }, - } ); - - expect( state.present.edits ).toBe( original.present.edits ); - } ); - - it( 'should save modified properties', () => { - const original = editor( undefined, { - type: 'EDIT_POST', - edits: { - status: 'draft', - title: 'post title', - tags: [ 1 ], - }, - } ); - - const state = editor( original, { - type: 'EDIT_POST', - edits: { - title: 'modified title', - tags: [ 2 ], - }, - } ); - - expect( state.present.edits ).toEqual( { - status: 'draft', - title: 'modified title', - tags: [ 2 ], - } ); - } ); - - it( 'should merge object values', () => { - const original = editor( undefined, { - type: 'EDIT_POST', - edits: { - meta: { - a: 1, - }, - }, - } ); - - const state = editor( original, { - type: 'EDIT_POST', - edits: { - meta: { - b: 2, - }, - }, - } ); - - expect( state.present.edits ).toEqual( { - meta: { - a: 1, - b: 2, - }, - } ); - } ); - - it( 'return state by reference on unchanging update', () => { - const original = editor( undefined, {} ); - - const state = editor( original, { - type: 'UPDATE_POST', - edits: {}, - } ); - - expect( state.present.edits ).toBe( original.present.edits ); - } ); - - it( 'unset reset post values which match by canonical value', () => { - const original = editor( undefined, { - type: 'EDIT_POST', - edits: { - title: 'modified title', - }, - } ); - - const state = editor( original, { - type: 'RESET_POST', - post: { - title: { - raw: 'modified title', - }, - }, - } ); - - expect( state.present.edits ).toEqual( {} ); - } ); - - it( 'unset reset post values by deep match', () => { - const original = editor( undefined, { - type: 'EDIT_POST', - edits: { - title: 'modified title', - meta: { - a: 1, - b: 2, - }, - }, - } ); - - const state = editor( original, { - type: 'UPDATE_POST', - edits: { - title: 'modified title', - meta: { - a: 1, - b: 2, - }, - }, - } ); - - expect( state.present.edits ).toEqual( {} ); - } ); - - it( 'should omit content when resetting', () => { - // Use case: When editing in Text mode, we defer to content on - // the property, but we reset blocks by parse when switching - // back to Visual mode. - const original = deepFreeze( editor( undefined, {} ) ); - let state = editor( original, { - type: 'EDIT_POST', - edits: { - content: 'bananas', - }, - } ); - - expect( state.present.edits ).toHaveProperty( 'content' ); - - state = editor( original, { - type: 'RESET_EDITOR_BLOCKS', - blocks: [ { - clientId: 'kumquat', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - }, { - clientId: 'loquat', - name: 'core/test-block', - attributes: {}, - innerBlocks: [], - } ], - } ); - - expect( state.present.edits ).not.toHaveProperty( 'content' ); - } ); - } ); - } ); - - describe( 'initialEdits', () => { - it( 'should default to initial edits', () => { - const state = initialEdits( undefined, {} ); - - expect( state ).toBe( INITIAL_EDITS_DEFAULTS ); - } ); - - it( 'should return initial edits on post reset', () => { - const state = initialEdits( undefined, { - type: 'RESET_POST', - } ); - - expect( state ).toBe( INITIAL_EDITS_DEFAULTS ); - } ); - - it( 'should return referentially equal state if setup includes no edits', () => { - const original = initialEdits( undefined, {} ); - const state = initialEdits( deepFreeze( original ), { - type: 'SETUP_EDITOR', - } ); - - expect( state ).toBe( original ); - } ); - - it( 'should return referentially equal state if reset while having made no edits', () => { - const original = initialEdits( undefined, {} ); - const state = initialEdits( deepFreeze( original ), { - type: 'RESET_POST', - } ); - - expect( state ).toBe( original ); - } ); - - it( 'should return setup edits', () => { - const original = initialEdits( undefined, {} ); - const state = initialEdits( deepFreeze( original ), { - type: 'SETUP_EDITOR', - edits: { - title: '', - content: '', - }, - } ); - - expect( state ).toEqual( { - title: '', - content: '', - } ); - } ); - - it( 'should unset content on editor setup', () => { - const original = initialEdits( undefined, { - type: 'SETUP_EDITOR', - edits: { - title: '', - content: '', - }, - } ); - const state = initialEdits( deepFreeze( original ), { - type: 'SETUP_EDITOR_STATE', - } ); - - expect( state ).toEqual( { title: '' } ); - } ); - - it( 'should unset values on post update', () => { - const original = initialEdits( undefined, { - type: 'SETUP_EDITOR', - edits: { - title: '', - }, - } ); - const state = initialEdits( deepFreeze( original ), { - type: 'UPDATE_POST', - edits: { - title: '', - }, - } ); - - expect( state ).toEqual( {} ); - } ); - } ); - - describe( 'currentPost()', () => { - it( 'should reset a post object', () => { - const original = deepFreeze( { title: 'unmodified' } ); - - const state = currentPost( original, { - type: 'RESET_POST', - post: { - title: 'new post', - }, - } ); - - expect( state ).toEqual( { - title: 'new post', - } ); - } ); - - it( 'should update the post object with UPDATE_POST', () => { - const original = deepFreeze( { title: 'unmodified', status: 'publish' } ); - - const state = currentPost( original, { - type: 'UPDATE_POST', - edits: { - title: 'updated post object from server', - }, - } ); - - expect( state ).toEqual( { - title: 'updated post object from server', - status: 'publish', - } ); - } ); - } ); - describe( 'preferences()', () => { it( 'should apply all defaults', () => { const state = preferences( undefined, {} ); @@ -508,43 +184,10 @@ describe( 'state', () => { it( 'should update when a request is started', () => { const state = saving( null, { type: 'REQUEST_POST_UPDATE_START', + options: { isAutosave: true }, } ); expect( state ).toEqual( { - requesting: true, - successful: false, - error: null, - options: {}, - } ); - } ); - - it( 'should update when a request succeeds', () => { - const state = saving( null, { - type: 'REQUEST_POST_UPDATE_SUCCESS', - } ); - expect( state ).toEqual( { - requesting: false, - successful: true, - error: null, - options: {}, - } ); - } ); - - it( 'should update when a request fails', () => { - const state = saving( null, { - type: 'REQUEST_POST_UPDATE_FAILURE', - error: { - code: 'pretend_error', - message: 'update failed', - }, - } ); - expect( state ).toEqual( { - requesting: false, - successful: false, - error: { - code: 'pretend_error', - message: 'update failed', - }, - options: {}, + options: { isAutosave: true }, } ); } ); } ); diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index d11bc2350750f6..2185f04c807649 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -1591,7 +1591,7 @@ describe( 'selectors', () => { expect( isEditedPostEmpty( state ) ).toBe( true ); } ); - it( 'should return true if blocks, but empty content edit', () => { + it( 'should return false if blocks, but empty content edit', () => { const state = { editor: { present: { @@ -1616,7 +1616,7 @@ describe( 'selectors', () => { }, }; - expect( isEditedPostEmpty( state ) ).toBe( true ); + expect( isEditedPostEmpty( state ) ).toBe( false ); } ); it( 'should return true if the post has an empty content property', () => { @@ -1638,7 +1638,7 @@ describe( 'selectors', () => { expect( isEditedPostEmpty( state ) ).toBe( true ); } ); - it( 'should return false if edits include a non-empty content property', () => { + it( 'should return true if edits include a non-empty content property, but blocks are empty', () => { const state = { editor: { present: { @@ -1654,7 +1654,7 @@ describe( 'selectors', () => { currentPost: {}, }; - expect( isEditedPostEmpty( state ) ).toBe( false ); + expect( isEditedPostEmpty( state ) ).toBe( true ); } ); it( 'should return true if empty classic block', () => { @@ -2147,7 +2147,7 @@ describe( 'selectors', () => { } ); } ); - it( 'defers to returning an edited post attribute', () => { + it( 'serializes blocks, if any', () => { const block = createBlock( 'core/block' ); const state = { @@ -2167,7 +2167,7 @@ describe( 'selectors', () => { const content = getEditedPostContent( state ); - expect( content ).toBe( 'custom edit' ); + expect( content ).toBe( '' ); } ); it( 'returns serialization of blocks', () => { From 2cc0e18a0c8d24eae71bb5c97393b1c98317baa8 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Thu, 8 Aug 2019 14:47:28 -0400 Subject: [PATCH 06/24] Editor: Fix more tests. --- .../developers/data/data-core-editor.md | 16 +++++++- packages/core-data/src/actions.js | 37 +++++++++++++------ packages/core-data/src/index.js | 3 +- packages/core-data/src/selectors.js | 29 +++++++++------ packages/editor/src/store/actions.js | 25 +++++++++++-- packages/editor/src/store/test/actions.js | 35 ++++++++++++++---- 6 files changed, 110 insertions(+), 35 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index c1ec84dd33fb90..d09dd0a49e0e7d 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -311,8 +311,7 @@ _Returns_ # **getEditedPostContent** -Returns the content of the post being edited, preferring raw string edit -before falling back to serialization of block state. +Returns the content of the post being edited. _Parameters_ @@ -742,6 +741,7 @@ Return true if the current post has already been published. _Parameters_ - _state_ `Object`: Global application state. +- _currentPost_ `?Object`: Explicit current post for bypassing registry selector. _Returns_ @@ -1226,6 +1226,18 @@ _Related_ - selectBlock in core/block-editor store. +# **serializeBlocks** + +Serializes blocks following backwards compatibility conventions. + +_Parameters_ + +- _blocksForSerialization_ `Array`: The blocks to serialize. + +_Returns_ + +- `string`: The blocks serialization. + # **setTemplateValidity** _Related_ diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 43861be226ca5d..338c1fdb3973c9 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -3,6 +3,11 @@ */ import { castArray, get, merge, isEqual, find } from 'lodash'; +/** + * WordPress dependencies + */ +import { dispatch } from '@wordpress/data-controls'; + /** * Internal dependencies */ @@ -147,7 +152,7 @@ export function* editEntityRecord( kind, name, recordId, edits ) { // Clear edits when they are equal to their persisted counterparts // so that the property is not considered dirty. edits: Object.keys( edits ).reduce( ( acc, key ) => { - const recordValue = get( record[ key ], 'raw', record[ key ] ); + const recordValue = record[ key ]; const value = mergedEdits[ key ] ? merge( recordValue, edits[ key ] ) : edits[ key ]; @@ -220,7 +225,7 @@ export function* saveEntityRecord( kind, name, record, - { isAutosave = false } = { isAutosave: false } + { isAutosave = false, getNoticeActionArgs } = { isAutosave: false } ) { const entities = yield getKindEntities( kind ); const entity = find( entities, { kind, name } ); @@ -234,13 +239,13 @@ export function* saveEntityRecord( let error; try { const path = `${ entity.baseURL }${ recordId ? '/' + recordId : '' }`; + let persistedRecord; + let updatedRecord; + if ( isAutosave || getNoticeActionArgs ) { + persistedRecord = yield select( 'getEntityRecord', kind, name, recordId ); + } + if ( isAutosave ) { - const persistedRecord = yield select( - 'getEntityRecord', - kind, - name, - recordId - ); const currentUser = yield select( 'getCurrentUser' ); const currentUserId = currentUser ? currentUser.id : undefined; const autosavePost = yield select( @@ -260,20 +265,30 @@ export function* saveEntityRecord( } return acc; }, {} ); - const autosave = yield apiFetch( { + updatedRecord = yield apiFetch( { path: `${ path }/autosaves`, method: 'POST', data, } ); - yield receiveAutosaves( persistedRecord.id, autosave ); + yield receiveAutosaves( persistedRecord.id, updatedRecord ); } else { - const updatedRecord = yield apiFetch( { + updatedRecord = yield apiFetch( { path, method: recordId ? 'PUT' : 'POST', data: record, } ); yield receiveEntityRecords( kind, name, updatedRecord, undefined, true ); } + + if ( getNoticeActionArgs ) { + yield dispatch( + ...getNoticeActionArgs( + persistedRecord, + updatedRecord, + yield select( 'getPostType', updatedRecord.type ) + ) + ); + } } catch ( _error ) { error = _error; } diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js index b0c5718ab9b888..88d31a557e4bed 100644 --- a/packages/core-data/src/index.js +++ b/packages/core-data/src/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { registerStore } from '@wordpress/data'; +import { controls as dataControls } from '@wordpress/data-controls'; /** * Internal dependencies @@ -43,7 +44,7 @@ const entityActions = defaultEntities.reduce( ( result, entity ) => { registerStore( REDUCER_KEY, { reducer, - controls, + controls: { ...dataControls, ...controls }, actions: { ...actions, ...entityActions }, selectors: { ...selectors, ...entitySelectors }, resolvers: { ...resolvers, ...entityResolvers }, diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index a480f65c66fb16..ee689443510606 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -104,7 +104,20 @@ export function getEntity( state, kind, name ) { * @return {Object?} Record. */ export function getEntityRecord( state, kind, name, key ) { - return get( state.entities.data, [ kind, name, 'queriedData', 'items', key ] ); + const record = get( state.entities.data, [ + kind, + name, + 'queriedData', + 'items', + key, + ] ); + return ( + record && + Object.keys( record ).reduce( ( acc, _key ) => { + acc[ _key ] = get( record[ _key ], 'raw', record[ _key ] ); + return acc; + }, {} ) + ); } /** @@ -193,16 +206,10 @@ export function hasEditsForEntityRecord( state, kind, name, recordId ) { * @return {Object?} The entity record, merged with its edits. */ export const getEditedEntityRecord = createSelector( - ( state, kind, name, recordId ) => { - const record = getEntityRecord( state, kind, name, recordId ); - return { - ...Object.keys( record ).reduce( ( acc, key ) => { - acc[ key ] = get( record[ key ], 'raw', record[ key ] ); - return acc; - }, {} ), - ...getEntityRecordEdits( state, kind, name, recordId ), - }; - }, + ( state, kind, name, recordId ) => ( { + ...getEntityRecord( state, kind, name, recordId ), + ...getEntityRecordEdits( state, kind, name, recordId ), + } ), ( state ) => [ state.entities.data ] ); diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index da0aca634cf3bc..353872fe7c6ca2 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -28,6 +28,7 @@ import { TRASH_POST_NOTICE_ID, } from './constants'; import { + getNotificationArgumentsForSaveSuccess, getNotificationArgumentsForTrashFail, } from './utils/notice-builder'; import { awaitNextStateChange, getRegistry } from './controls'; @@ -161,7 +162,7 @@ export function* setupEditor( post, edits, template ) { if ( has( edits, [ 'content' ] ) ) { content = edits.content; } else { - content = post.content.raw; + content = post.content; } let blocks = parse( content ); @@ -182,6 +183,9 @@ export function* setupEditor( post, edits, template ) { }; yield resetEditorBlocks( blocks, { __unstableShouldCreateUndoLevel: false } ); yield setupEditorState( post ); + if ( edits ) { + yield editPost( edits ); + } yield* __experimentalSubscribeSources(); } @@ -239,7 +243,7 @@ export function* __experimentalSubscribeSources() { } if ( reset ) { - yield resetEditorBlocks( yield select( 'core/editor', 'getEditorBlocks' ) ); + yield resetEditorBlocks( yield select( 'core/editor', 'getEditorBlocks' ), { __unstableShouldCreateUndoLevel: false } ); } } } @@ -360,6 +364,9 @@ export function __experimentalOptimisticUpdatePost( edits ) { * @param {Object} options */ export function* savePost( options = {} ) { + yield dispatch( STORE_KEY, 'editPost', { + content: yield select( 'core/editor', 'getEditedPostContent' ), + } ); yield __experimentalRequestPostUpdateStart( options ); const postType = yield select( 'core/editor', 'getCurrentPostType' ); const postId = yield select( 'core/editor', 'getCurrentPostId' ); @@ -369,7 +376,19 @@ export function* savePost( options = {} ) { 'postType', postType, postId, - options + { + ...options, + getNoticeActionArgs: ( previousEntity, entity, type ) => [ + 'core/notices', + 'createSuccessNotice', + ...getNotificationArgumentsForSaveSuccess( { + previousPost: previousEntity, + post: entity, + postType: type, + options, + } ), + ], + } ); } diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 0850bdea661e5e..0154f1fb802944 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -75,10 +75,31 @@ describe( 'Post generator actions', () => { ); const testConditions = [ [ - 'yields an action for signalling that an update to the post started', + 'yields an action for selecting the current edited post content', () => true, () => { reset( isAutosave ); + const { value } = fulfillment.next(); + expect( value ).toEqual( + select( STORE_KEY, 'getEditedPostContent' ) + ); + }, + ], + [ + "yields an action for editing the post entity's content", + () => true, + () => { + const edits = { content: currentPost().content }; + const { value } = fulfillment.next( edits.content ); + expect( value ).toEqual( + dispatch( STORE_KEY, 'editPost', edits ) + ); + }, + ], + [ + 'yields an action for signalling that an update to the post started', + () => true, + () => { const { value } = fulfillment.next(); expect( value ).toEqual( { type: 'REQUEST_POST_UPDATE_START', @@ -111,6 +132,7 @@ describe( 'Post generator actions', () => { () => true, () => { const { value } = fulfillment.next( currentPost().id ); + value.args[ 3 ] = { ...value.args[ 3 ], getNoticeActionArgs: 'getNoticeActionArgs' }; expect( value ).toEqual( dispatch( 'core', @@ -118,7 +140,7 @@ describe( 'Post generator actions', () => { 'postType', currentPost().type, currentPost().id, - { isAutosave } + { isAutosave, getNoticeActionArgs: 'getNoticeActionArgs' } ) ); }, @@ -144,8 +166,7 @@ describe( 'Post generator actions', () => { } }; - describe( 'yields with expected responses for when not autosaving ' + - 'and edited post is new', () => { + describe( 'yields with expected responses for when not autosaving and edited post is new', () => { beforeEach( () => { isAutosave = false; currentPostStatus = 'draft'; @@ -305,7 +326,7 @@ describe( 'Post generator actions', () => { describe( 'Editor actions', () => { describe( 'setupEditor()', () => { - const post = { content: { raw: '' }, status: 'publish' }; + const post = { content: '', status: 'publish' }; let fulfillment; const reset = ( edits, template ) => fulfillment = actions @@ -326,7 +347,7 @@ describe( 'Editor actions', () => { const { value } = fulfillment.next(); expect( value ).toEqual( { type: 'SETUP_EDITOR', - post: { content: { raw: '' }, status: 'publish' }, + post: { content: '', status: 'publish' }, } ); } ); it( 'should yield action object for resetEditorBlocks', () => { @@ -337,7 +358,7 @@ describe( 'Editor actions', () => { const { value } = fulfillment.next(); expect( value ).toEqual( actions.setupEditorState( - { content: { raw: '' }, status: 'publish' } + { content: '', status: 'publish' } ) ); } ); From a21cb135c4fec22fac12d54b9115fc20dc656a6a Mon Sep 17 00:00:00 2001 From: epiqueras Date: Thu, 8 Aug 2019 15:22:11 -0400 Subject: [PATCH 07/24] Editor: Fix more e2e test behaviors. --- packages/editor/src/store/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 353872fe7c6ca2..1c31ab2cd34f17 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -183,7 +183,7 @@ export function* setupEditor( post, edits, template ) { }; yield resetEditorBlocks( blocks, { __unstableShouldCreateUndoLevel: false } ); yield setupEditorState( post ); - if ( edits ) { + if ( edits && Object.values( edits ).some( ( edit ) => edit ) ) { yield editPost( edits ); } yield* __experimentalSubscribeSources(); From 1045a010e16ca1ae13f5dc9384a80cbf15ade1e8 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Thu, 8 Aug 2019 16:25:46 -0400 Subject: [PATCH 08/24] Editor: Fix preview functionality. --- packages/core-data/src/actions.js | 15 +++-- packages/editor/src/store/actions.js | 30 +++++++--- packages/editor/src/store/reducer.js | 34 +----------- packages/editor/src/store/selectors.js | 13 ++++- packages/editor/src/store/test/actions.js | 11 ++++ packages/editor/src/store/test/reducer.js | 67 +---------------------- 6 files changed, 58 insertions(+), 112 deletions(-) diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 338c1fdb3973c9..02ad7d31768599 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -281,13 +281,16 @@ export function* saveEntityRecord( } if ( getNoticeActionArgs ) { - yield dispatch( - ...getNoticeActionArgs( - persistedRecord, - updatedRecord, - yield select( 'getPostType', updatedRecord.type ) - ) + const args = getNoticeActionArgs( + persistedRecord, + updatedRecord, + yield select( 'getPostType', updatedRecord.type ) ); + if ( args && args.length ) { + yield dispatch( + ...args + ); + } } } catch ( _error ) { error = _error; diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 1c31ab2cd34f17..a8bb937245f717 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -287,7 +287,7 @@ export function* resetAutosave( newAutosave ) { } /** - * Optimistic action for dispatching that a post update request has started. + * Action for dispatching that a post update request has started. * * @param {Object} options * @@ -300,6 +300,20 @@ export function __experimentalRequestPostUpdateStart( options = {} ) { }; } +/** + * Action for dispatching that a post update request has finished. + * + * @param {Object} options + * + * @return {Object} An action object + */ +export function __experimentalRequestPostUpdateFinish( options = {} ) { + return { + type: 'REQUEST_POST_UPDATE_FINISH', + options, + }; +} + /** * Returns an action object used in signalling that a patch of updates for the * latest version of the post have been received. @@ -378,18 +392,20 @@ export function* savePost( options = {} ) { postId, { ...options, - getNoticeActionArgs: ( previousEntity, entity, type ) => [ - 'core/notices', - 'createSuccessNotice', - ...getNotificationArgumentsForSaveSuccess( { + getNoticeActionArgs: ( previousEntity, entity, type ) => { + const args = getNotificationArgumentsForSaveSuccess( { previousPost: previousEntity, post: entity, postType: type, options, - } ), - ], + } ); + if ( args && args.length ) { + return [ 'core/notices', 'createSuccessNotice', ...args ]; + } + }, } ); + yield __experimentalRequestPostUpdateFinish( options ); } /** diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 241eb7d974c42b..a88e05f30ac775 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -8,7 +8,6 @@ import { reduce, omit, keys, isEqual } from 'lodash'; * WordPress dependencies */ import { combineReducers } from '@wordpress/data'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -180,7 +179,9 @@ export function preferences( state = PREFERENCES_DEFAULTS, action ) { export function saving( state = {}, action ) { switch ( action.type ) { case 'REQUEST_POST_UPDATE_START': + case 'REQUEST_POST_UPDATE_FINISH': return { + pending: action.type === 'REQUEST_POST_UPDATE_START', options: action.options || {}, }; } @@ -339,36 +340,6 @@ export const reusableBlocks = combineReducers( { }, } ); -/** - * Reducer returning the post preview link. - * - * @param {string?} state The preview link. - * @param {Object} action Dispatched action. - * - * @return {string?} Updated state. - */ -export function previewLink( state = null, action ) { - switch ( action.type ) { - case 'REQUEST_POST_UPDATE_SUCCESS': - if ( action.post.preview_link ) { - return action.post.preview_link; - } else if ( action.post.link ) { - return addQueryArgs( action.post.link, { preview: true } ); - } - - return state; - - case 'REQUEST_POST_UPDATE_START': - // Invalidate known preview link when autosave starts. - if ( state && action.options.isPreview ) { - return null; - } - break; - } - - return state; -} - /** * Reducer returning whether the editor is ready to be rendered. * The editor is considered ready to be rendered once @@ -419,7 +390,6 @@ export default optimist( combineReducers( { postLock, reusableBlocks, template, - previewLink, postSavingLock, isReady, editorSettings, diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index eea8780da473e1..c4bf57dfb2f687 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -743,8 +743,19 @@ export function isPreviewingPost( state ) { * @return {string?} Preview Link. */ export function getEditedPostPreviewLink( state ) { + if ( state.saving.pending || isSavingPost( state ) ) { + return; + } + + let previewLink = getEditedPostAttribute( state, 'preview_link' ); + if ( ! previewLink ) { + previewLink = getEditedPostAttribute( state, 'link' ); + if ( previewLink ) { + previewLink = addQueryArgs( previewLink, { preview: true } ); + } + } const featuredImageId = getEditedPostAttribute( state, 'featured_media' ); - const previewLink = state.previewLink; + if ( previewLink && featuredImageId ) { return addQueryArgs( previewLink, { _thumbnail_id: featuredImageId } ); } diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 0154f1fb802944..41b5cb772fc341 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -145,6 +145,17 @@ describe( 'Post generator actions', () => { ); }, ], + [ + 'yields an action for signalling that an update to the post finished', + () => true, + () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( { + type: 'REQUEST_POST_UPDATE_FINISH', + options: { isAutosave }, + } ); + }, + ], [ 'implicitly returns undefined', () => true, diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 3b31970aff30aa..3fc6461b34e538 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -15,7 +15,6 @@ import { saving, reusableBlocks, postSavingLock, - previewLink, } from '../reducer'; describe( 'state', () => { @@ -187,6 +186,7 @@ describe( 'state', () => { options: { isAutosave: true }, } ); expect( state ).toEqual( { + pending: true, options: { isAutosave: true }, } ); } ); @@ -489,69 +489,4 @@ describe( 'state', () => { expect( state ).toEqual( {} ); } ); } ); - - describe( 'previewLink', () => { - it( 'returns null by default', () => { - const state = previewLink( undefined, {} ); - - expect( state ).toBe( null ); - } ); - - it( 'returns preview link from save success', () => { - const state = previewLink( null, { - type: 'REQUEST_POST_UPDATE_SUCCESS', - post: { - preview_link: 'https://example.com/?p=2611&preview=true', - }, - } ); - - expect( state ).toBe( 'https://example.com/?p=2611&preview=true' ); - } ); - - it( 'returns post link with query arg from save success if no preview link', () => { - const state = previewLink( null, { - type: 'REQUEST_POST_UPDATE_SUCCESS', - post: { - link: 'https://example.com/sample-post/', - }, - } ); - - expect( state ).toBe( 'https://example.com/sample-post/?preview=true' ); - } ); - - it( 'returns same state if save success without preview link or post link', () => { - // Bug: This can occur for post types which are defined as - // `publicly_queryable => false` (non-viewable). - // - // See: https://github.com/WordPress/gutenberg/issues/12677 - const state = previewLink( null, { - type: 'REQUEST_POST_UPDATE_SUCCESS', - post: { - preview_link: '', - }, - } ); - - expect( state ).toBe( null ); - } ); - - it( 'returns resets on preview start', () => { - const state = previewLink( 'https://example.com/sample-post/', { - type: 'REQUEST_POST_UPDATE_START', - options: { - isPreview: true, - }, - } ); - - expect( state ).toBe( null ); - } ); - - it( 'returns state on non-preview save start', () => { - const state = previewLink( 'https://example.com/sample-post/', { - type: 'REQUEST_POST_UPDATE_START', - options: {}, - } ); - - expect( state ).toBe( 'https://example.com/sample-post/' ); - } ); - } ); } ); From 56d0e9ef473676cd3f2ff823beea36948dcbff46 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Thu, 8 Aug 2019 19:05:14 -0400 Subject: [PATCH 09/24] Core Data: Fix autosaves filtering. --- packages/core-data/src/actions.js | 7 ++++--- packages/editor/src/store/selectors.js | 9 ++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 02ad7d31768599..a56709e0b1d81e 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -260,7 +260,7 @@ export function* saveEntityRecord( // have a value. let data = { ...persistedRecord, ...autosavePost, ...record }; data = Object.keys( data ).reduce( ( acc, key ) => { - if ( key in [ 'title', 'excerpt', 'content' ] ) { + if ( [ 'title', 'excerpt', 'content' ].includes( key ) ) { acc[ key ] = get( data[ key ], 'raw', data[ key ] ); } return acc; @@ -281,10 +281,11 @@ export function* saveEntityRecord( } if ( getNoticeActionArgs ) { - const args = getNoticeActionArgs( + const postType = updatedRecord.type || persistedRecord.type; + const args = postType && getNoticeActionArgs( persistedRecord, updatedRecord, - yield select( 'getPostType', updatedRecord.type ) + yield select( 'getPostType', postType ) ); if ( args && args.length ) { yield dispatch( diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index c4bf57dfb2f687..003bd720861b99 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -347,12 +347,7 @@ export function getEditedPostAttribute( state, attributeName ) { * @return {*} Autosave attribute value. */ export const getAutosaveAttribute = createRegistrySelector( ( select ) => ( state, attributeName ) => { - deprecated( '`wp.data.select( \'core/editor\' ).getAutosaveAttribute( attributeName )`', { - alternative: '`wp.data.select( \'core\' ).getAutosave( postType, postId, userId )`', - plugin: 'Gutenberg', - } ); - - if ( ! includes( AUTOSAVE_PROPERTIES, attributeName ) ) { + if ( ! includes( AUTOSAVE_PROPERTIES, attributeName ) && attributeName !== 'preview_link' ) { return; } @@ -747,7 +742,7 @@ export function getEditedPostPreviewLink( state ) { return; } - let previewLink = getEditedPostAttribute( state, 'preview_link' ); + let previewLink = getAutosaveAttribute( state, 'preview_link' ); if ( ! previewLink ) { previewLink = getEditedPostAttribute( state, 'link' ); if ( previewLink ) { From d93d239ea36343523a925a429adaf129863bc07a Mon Sep 17 00:00:00 2001 From: epiqueras Date: Fri, 9 Aug 2019 10:41:10 -0400 Subject: [PATCH 10/24] Editor: Don't make entity dirty with initial edits. --- packages/editor/src/store/actions.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index a8bb937245f717..43e47429d4aef2 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -183,8 +183,9 @@ export function* setupEditor( post, edits, template ) { }; yield resetEditorBlocks( blocks, { __unstableShouldCreateUndoLevel: false } ); yield setupEditorState( post ); - if ( edits && Object.values( edits ).some( ( edit ) => edit ) ) { - yield editPost( edits ); + if ( edits ) { + const record = { ...post, ...edits }; + yield dispatch( 'core', 'receiveEntityRecords', 'postType', post.type, record ); } yield* __experimentalSubscribeSources(); } From f1b18ad666b714c1a842a4618f8d55e5d58e55d9 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Fri, 9 Aug 2019 11:11:55 -0400 Subject: [PATCH 11/24] Editor: Don't save if the post is not saveable. --- packages/editor/src/store/actions.js | 4 ++++ packages/editor/src/store/test/actions.js | 12 +++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 43e47429d4aef2..87d2648e5d4f88 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -382,6 +382,10 @@ export function* savePost( options = {} ) { yield dispatch( STORE_KEY, 'editPost', { content: yield select( 'core/editor', 'getEditedPostContent' ), } ); + if ( ! ( yield select( STORE_KEY, 'isEditedPostSaveable' ) ) ) { + return; + } + yield __experimentalRequestPostUpdateStart( options ); const postType = yield select( 'core/editor', 'getCurrentPostType' ); const postId = yield select( 'core/editor', 'getCurrentPostId' ); diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 41b5cb772fc341..b4bea1523954e9 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -97,10 +97,20 @@ describe( 'Post generator actions', () => { }, ], [ - 'yields an action for signalling that an update to the post started', + 'yields an action for checking if the post is saveable', () => true, () => { const { value } = fulfillment.next(); + expect( value ).toEqual( + select( STORE_KEY, 'isEditedPostSaveable' ) + ); + }, + ], + [ + 'yields an action for signalling that an update to the post started', + () => true, + () => { + const { value } = fulfillment.next( true ); expect( value ).toEqual( { type: 'REQUEST_POST_UPDATE_START', options: { isAutosave }, From 28bae58890ca72a0ada7d8bfb0b44b10d76d1af0 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Fri, 9 Aug 2019 11:33:08 -0400 Subject: [PATCH 12/24] Core Data: Fix merged edits logic. --- packages/core-data/src/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index a56709e0b1d81e..44bd397e616480 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -154,7 +154,7 @@ export function* editEntityRecord( kind, name, recordId, edits ) { edits: Object.keys( edits ).reduce( ( acc, key ) => { const recordValue = record[ key ]; const value = mergedEdits[ key ] ? - merge( recordValue, edits[ key ] ) : + merge( {}, recordValue, edits[ key ] ) : edits[ key ]; acc[ key ] = isEqual( recordValue, value ) ? undefined : value; return acc; From afaec5e8268b862e5327a73d7dc6421fbd7867b7 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Fri, 9 Aug 2019 12:28:10 -0400 Subject: [PATCH 13/24] Core Data: Fix undo to fit e2e expected behaviors. --- packages/core-data/src/reducer.js | 14 ++++++-------- packages/editor/src/store/actions.js | 6 +++--- packages/editor/src/store/test/actions.js | 20 ++++++++++---------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 780828786f3576..5053e6c5c1aa34 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -330,21 +330,19 @@ export function undo( state = UNDO_INITIAL_STATE, action ) { edits: { ...state.flattenedUndo, ...action.meta.undo.edits }, }, ]; - } else { - // Clear potential redos, because this only supports linear history. - nextState = state.slice( 0, state.offset || undefined ); - const lastItem = nextState[ nextState.length - 1 ]; - if ( lastItem ) { - lastItem.edits = { ...lastItem.edits, ...state.flattenedUndo }; - } + nextState.offset = 0; + return nextState; } + + // Clear potential redos, because this only supports linear history. + nextState = state.slice( 0, state.offset || undefined ); nextState.offset = 0; nextState.push( { kind: action.kind, name: action.name, recordId: action.recordId, - edits: action.edits, + edits: { ...action.edits, ...state.flattenedUndo }, } ); return nextState; diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 87d2648e5d4f88..95e4bf6c7aeb06 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -379,12 +379,12 @@ export function __experimentalOptimisticUpdatePost( edits ) { * @param {Object} options */ export function* savePost( options = {} ) { - yield dispatch( STORE_KEY, 'editPost', { - content: yield select( 'core/editor', 'getEditedPostContent' ), - } ); if ( ! ( yield select( STORE_KEY, 'isEditedPostSaveable' ) ) ) { return; } + yield dispatch( STORE_KEY, 'editPost', { + content: yield select( 'core/editor', 'getEditedPostContent' ), + } ); yield __experimentalRequestPostUpdateStart( options ); const postType = yield select( 'core/editor', 'getCurrentPostType' ); diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index b4bea1523954e9..e6ebd5049aa9c8 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -75,34 +75,34 @@ describe( 'Post generator actions', () => { ); const testConditions = [ [ - 'yields an action for selecting the current edited post content', + 'yields an action for checking if the post is saveable', () => true, () => { reset( isAutosave ); const { value } = fulfillment.next(); expect( value ).toEqual( - select( STORE_KEY, 'getEditedPostContent' ) + select( STORE_KEY, 'isEditedPostSaveable' ) ); }, ], [ - "yields an action for editing the post entity's content", + 'yields an action for selecting the current edited post content', () => true, () => { - const edits = { content: currentPost().content }; - const { value } = fulfillment.next( edits.content ); + const { value } = fulfillment.next( true ); expect( value ).toEqual( - dispatch( STORE_KEY, 'editPost', edits ) + select( STORE_KEY, 'getEditedPostContent' ) ); }, ], [ - 'yields an action for checking if the post is saveable', + "yields an action for editing the post entity's content", () => true, () => { - const { value } = fulfillment.next(); + const edits = { content: currentPost().content }; + const { value } = fulfillment.next( edits.content ); expect( value ).toEqual( - select( STORE_KEY, 'isEditedPostSaveable' ) + dispatch( STORE_KEY, 'editPost', edits ) ); }, ], @@ -110,7 +110,7 @@ describe( 'Post generator actions', () => { 'yields an action for signalling that an update to the post started', () => true, () => { - const { value } = fulfillment.next( true ); + const { value } = fulfillment.next(); expect( value ).toEqual( { type: 'REQUEST_POST_UPDATE_START', options: { isAutosave }, From bfee37a5ae6ea314834019eb2044aef23892294c Mon Sep 17 00:00:00 2001 From: epiqueras Date: Fri, 9 Aug 2019 14:26:04 -0400 Subject: [PATCH 14/24] Core Data: Handle more change detection and saving flows. --- .../src/components/block-list/block.js | 2 + packages/core-data/src/actions.js | 54 +++++++++++++------ packages/core-data/src/selectors.js | 7 ++- .../e2e-tests/specs/change-detection.test.js | 7 ++- packages/editor/src/store/actions.js | 13 ++++- packages/editor/src/store/selectors.js | 2 +- packages/editor/src/store/test/actions.js | 14 ++++- 7 files changed, 74 insertions(+), 25 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index d95ce1f3a93813..2f719f3f79ad92 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -702,6 +702,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { replaceBlocks, toggleSelection, setNavigationMode, + __unstableMarkLastChangeAsPersistent, } = dispatch( 'core/block-editor' ); return { @@ -755,6 +756,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { } }, onReplace( blocks, indexToSelect ) { + __unstableMarkLastChangeAsPersistent(); replaceBlocks( [ ownProps.clientId ], blocks, indexToSelect ); }, onShiftSelection() { diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 44bd397e616480..b50e7cbfa9c7c3 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -225,7 +225,11 @@ export function* saveEntityRecord( kind, name, record, - { isAutosave = false, getNoticeActionArgs } = { isAutosave: false } + { + isAutosave = false, + getSuccessNoticeActionArgs, + getFailureNoticeActionArgs, + } = { isAutosave: false } ) { const entities = yield getKindEntities( kind ); const entity = find( entities, { kind, name } ); @@ -236,14 +240,14 @@ export function* saveEntityRecord( const recordId = record[ entityIdKey ]; yield { type: 'SAVE_ENTITY_RECORD_START', kind, name, recordId, isAutosave }; + let persistedRecord; + if ( isAutosave || getSuccessNoticeActionArgs || getFailureNoticeActionArgs ) { + persistedRecord = yield select( 'getEntityRecord', kind, name, recordId ); + } let error; try { const path = `${ entity.baseURL }${ recordId ? '/' + recordId : '' }`; - let persistedRecord; let updatedRecord; - if ( isAutosave || getNoticeActionArgs ) { - persistedRecord = yield select( 'getEntityRecord', kind, name, recordId ); - } if ( isAutosave ) { const currentUser = yield select( 'getCurrentUser' ); @@ -270,7 +274,20 @@ export function* saveEntityRecord( method: 'POST', data, } ); - yield receiveAutosaves( persistedRecord.id, updatedRecord ); + // An autosave may be processed by the server as a regular save + // when its update is requested by the author and the post had + // draft or auto-draft status. + if ( persistedRecord.id === updatedRecord.id ) { + yield receiveEntityRecords( + kind, + name, + { ...persistedRecord, ...updatedRecord }, + undefined, + true + ); + } else { + yield receiveAutosaves( persistedRecord.id, updatedRecord ); + } } else { updatedRecord = yield apiFetch( { path, @@ -280,21 +297,28 @@ export function* saveEntityRecord( yield receiveEntityRecords( kind, name, updatedRecord, undefined, true ); } - if ( getNoticeActionArgs ) { + if ( getSuccessNoticeActionArgs ) { const postType = updatedRecord.type || persistedRecord.type; - const args = postType && getNoticeActionArgs( - persistedRecord, - updatedRecord, - yield select( 'getPostType', postType ) - ); - if ( args && args.length ) { - yield dispatch( - ...args + const args = + postType && + getSuccessNoticeActionArgs( + persistedRecord, + updatedRecord, + yield select( 'getPostType', postType ) ); + if ( args && args.length ) { + yield dispatch( ...args ); } } } catch ( _error ) { error = _error; + + if ( getFailureNoticeActionArgs ) { + const args = getFailureNoticeActionArgs( persistedRecord, record, error ); + if ( args && args.length ) { + yield dispatch( ...args ); + } + } } yield { type: 'SAVE_ENTITY_RECORD_FINISH', diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index ee689443510606..b5ca3250534d73 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -243,12 +243,11 @@ export function isAutosavingEntityRecord( state, kind, name, recordId ) { * @return {boolean} Whether the entity record is saving or not. */ export function isSavingEntityRecord( state, kind, name, recordId ) { - const { pending, isAutosave } = get( + return get( state.entities.data, - [ kind, name, 'saving', recordId ], - {} + [ kind, name, 'saving', recordId, 'pending' ], + false ); - return Boolean( pending && ! isAutosave ); } /** diff --git a/packages/e2e-tests/specs/change-detection.test.js b/packages/e2e-tests/specs/change-detection.test.js index 30895ce5590dea..8900df4b3ea0df 100644 --- a/packages/e2e-tests/specs/change-detection.test.js +++ b/packages/e2e-tests/specs/change-detection.test.js @@ -151,6 +151,7 @@ describe( 'Change detection', () => { it( 'Should prompt if content added without save', async () => { await clickBlockAppender(); + await page.keyboard.type( 'Paragraph' ); await assertIsDirty( true ); } ); @@ -223,9 +224,9 @@ describe( 'Change detection', () => { // Keyboard shortcut Ctrl+S save. await pressKeyWithModifier( 'primary', 'S' ); - await releaseSaveIntercept(); - await assertIsDirty( true ); + + await releaseSaveIntercept(); } ); it( 'Should prompt if changes made while save is in-flight', async () => { @@ -240,6 +241,7 @@ describe( 'Change detection', () => { await pressKeyWithModifier( 'primary', 'S' ); await page.type( '.editor-post-title__input', '!' ); + await page.waitForSelector( '.editor-post-save-draft' ); await releaseSaveIntercept(); @@ -279,6 +281,7 @@ describe( 'Change detection', () => { await pressKeyWithModifier( 'primary', 'S' ); await clickBlockAppender(); + await page.keyboard.type( 'Paragraph' ); // Allow save to complete. Disabling interception flushes pending. await Promise.all( [ diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 95e4bf6c7aeb06..6c7a0be829e6ce 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -29,6 +29,7 @@ import { } from './constants'; import { getNotificationArgumentsForSaveSuccess, + getNotificationArgumentsForSaveFail, getNotificationArgumentsForTrashFail, } from './utils/notice-builder'; import { awaitNextStateChange, getRegistry } from './controls'; @@ -397,7 +398,7 @@ export function* savePost( options = {} ) { postId, { ...options, - getNoticeActionArgs: ( previousEntity, entity, type ) => { + getSuccessNoticeActionArgs: ( previousEntity, entity, type ) => { const args = getNotificationArgumentsForSaveSuccess( { previousPost: previousEntity, post: entity, @@ -408,6 +409,16 @@ export function* savePost( options = {} ) { return [ 'core/notices', 'createSuccessNotice', ...args ]; } }, + getFailureNoticeActionArgs: ( previousEntity, edits, error ) => { + const args = getNotificationArgumentsForSaveFail( { + post: previousEntity, + edits, + error, + } ); + if ( args && args.length ) { + return [ 'core/notices', 'createErrorNotice', ...args ]; + } + }, } ); yield __experimentalRequestPostUpdateFinish( options ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 003bd720861b99..10c2a0405ed6e8 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -713,7 +713,7 @@ export function isAutosavingPost( state ) { if ( ! isSavingPost( state ) ) { return false; } - return !! state.saving.options.isAutosave; + return !! get( state.saving, [ 'options', 'isAutosave' ] ); } /** diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index e6ebd5049aa9c8..f2940a0937c5d7 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -142,7 +142,11 @@ describe( 'Post generator actions', () => { () => true, () => { const { value } = fulfillment.next( currentPost().id ); - value.args[ 3 ] = { ...value.args[ 3 ], getNoticeActionArgs: 'getNoticeActionArgs' }; + value.args[ 3 ] = { + ...value.args[ 3 ], + getSuccessNoticeActionArgs: 'getSuccessNoticeActionArgs', + getFailureNoticeActionArgs: 'getFailureNoticeActionArgs', + }; expect( value ).toEqual( dispatch( 'core', @@ -150,7 +154,13 @@ describe( 'Post generator actions', () => { 'postType', currentPost().type, currentPost().id, - { isAutosave, getNoticeActionArgs: 'getNoticeActionArgs' } + { + isAutosave, + getSuccessNoticeActionArgs: + 'getSuccessNoticeActionArgs', + getFailureNoticeActionArgs: + 'getFailureNoticeActionArgs', + } ) ); }, From 97502afc1d6d097d59c7069ca96af660a50bc6a4 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Fri, 9 Aug 2019 16:47:18 -0400 Subject: [PATCH 15/24] Block Editor: Fix undo level logic. --- packages/block-editor/src/components/block-list/block.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 2f719f3f79ad92..2c3e8a5b8f5a89 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -756,7 +756,12 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { } }, onReplace( blocks, indexToSelect ) { - __unstableMarkLastChangeAsPersistent(); + if ( + blocks.length && + ! isUnmodifiedDefaultBlock( blocks[ blocks.length - 1 ] ) + ) { + __unstableMarkLastChangeAsPersistent(); + } replaceBlocks( [ ownProps.clientId ], blocks, indexToSelect ); }, onShiftSelection() { From 029b9011f430db74cc906ee7062a536c91977f3f Mon Sep 17 00:00:00 2001 From: epiqueras Date: Fri, 9 Aug 2019 17:14:06 -0400 Subject: [PATCH 16/24] Core Data: Clean up undo reducer comment. --- packages/core-data/src/reducer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 5053e6c5c1aa34..7bcf06afe1b854 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -310,8 +310,8 @@ export function undo( state = UNDO_INITIAL_STATE, action ) { } // Transient edits don't create an undo level, but are - // added to the last level right before a new level - // is added. + // reachable in the next meaningful edit to which they + // are merged. They are defined in the entity's config. if ( ! Object.keys( action.edits ).some( ( key ) => ! action.transientEdits[ key ] ) ) { const nextState = [ ...state ]; nextState.flattenedUndo = { ...state.flattenedUndo, ...action.edits }; From 7af50bbe94fffe6fa4f0b3866243da8825be581d Mon Sep 17 00:00:00 2001 From: epiqueras Date: Mon, 12 Aug 2019 18:25:19 -0700 Subject: [PATCH 17/24] Editor: Implement the `EntityHandlers` component and use it to create a "Post" block. --- lib/blocks.php | 20 ++++++ .../src/components/inner-blocks/index.js | 42 +++++++++++- packages/block-library/src/index.js | 6 ++ packages/block-library/src/post/block.json | 9 +++ packages/block-library/src/post/edit.js | 67 +++++++++++++++++++ packages/block-library/src/post/index.js | 20 ++++++ packages/block-library/src/post/save.js | 8 +++ packages/block-library/src/post/style.scss | 11 +++ packages/block-library/src/style.scss | 1 + packages/blocks/src/store/reducer.js | 1 + .../src/components/entity-handlers/index.js | 38 +++++++++++ packages/editor/src/components/index.js | 1 + .../editor/src/components/provider/index.js | 38 ++++++++++- 13 files changed, 258 insertions(+), 4 deletions(-) create mode 100644 packages/block-library/src/post/block.json create mode 100644 packages/block-library/src/post/edit.js create mode 100644 packages/block-library/src/post/index.js create mode 100644 packages/block-library/src/post/save.js create mode 100644 packages/block-library/src/post/style.scss create mode 100644 packages/editor/src/components/entity-handlers/index.js diff --git a/lib/blocks.php b/lib/blocks.php index cf66e1d4044a89..e6c89aca8c1e3b 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -46,6 +46,26 @@ function gutenberg_reregister_core_block_types() { } add_action( 'init', 'gutenberg_reregister_core_block_types' ); +/** + * Adds new block categories needed by the Gutenberg plugin. + * + * @param array $categories List of block categories. + * + * @return array List of block categories with the new categories added. + */ +function gutenberg_filter_block_categories( $categories ) { + return array_merge( + $categories, + array( + array( + 'slug' => 'theme', + 'title' => __( 'Theme Blocks' ), + ), + ) + ); +} +add_filter( 'block_categories', 'gutenberg_filter_block_categories' ); + /** * Registers a new block style. * diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index 0d1feef14ff39d..843cd484ad2c35 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -44,7 +44,8 @@ class InnerBlocks extends Component { } componentDidMount() { - const { innerBlocks } = this.props.block; + const { block, blocks, replaceInnerBlocks } = this.props; + const { innerBlocks } = block; // only synchronize innerBlocks with template if innerBlocks are empty or a locking all exists if ( innerBlocks.length === 0 || this.getTemplateLock() === 'all' ) { this.synchronizeBlocksWithTemplate(); @@ -55,10 +56,23 @@ class InnerBlocks extends Component { templateInProcess: false, } ); } + + // Set controlled blocks value from parent, if any. + if ( blocks ) { + replaceInnerBlocks( blocks ); + } } componentDidUpdate( prevProps ) { - const { template, block } = this.props; + const { + block, + template, + isLastBlockChangePersistent, + resetEditorBlocks, + resetEditorBlocksWithoutUndoLevel, + blocks, + replaceInnerBlocks, + } = this.props; const { innerBlocks } = block; this.updateNestedSettings(); @@ -69,6 +83,28 @@ class InnerBlocks extends Component { this.synchronizeBlocksWithTemplate(); } } + + // Sync with controlled blocks value from parent, if possible. + if ( + resetEditorBlocks && + resetEditorBlocksWithoutUndoLevel && + prevProps.block.innerBlocks !== innerBlocks + ) { + this.isSyncingBlocks = innerBlocks; + if ( isLastBlockChangePersistent ) { + resetEditorBlocks( innerBlocks ); + } else { + resetEditorBlocksWithoutUndoLevel( innerBlocks ); + } + } + + // Accept changes to controlled blocks value from parent after a sync, if any. + if ( this.isSyncingBlocks === blocks ) { + this.isSyncingBlocks = null; + } else if ( prevProps.blocks !== blocks ) { + this.isSyncingBlocks = null; + replaceInnerBlocks( blocks ); + } } /** @@ -151,6 +187,7 @@ InnerBlocks = compose( [ getBlockListSettings, getBlockRootClientId, getTemplateLock, + isLastBlockChangePersistent, } = select( 'core/block-editor' ); const { clientId } = ownProps; const block = getBlock( clientId ); @@ -161,6 +198,7 @@ InnerBlocks = compose( [ blockListSettings: getBlockListSettings( clientId ), hasOverlay: block.name !== 'core/template' && ! isBlockSelected( clientId ) && ! hasSelectedInnerBlock( clientId, true ), parentLock: getTemplateLock( rootClientId ), + isLastBlockChangePersistent: isLastBlockChangePersistent(), }; } ), withDispatch( ( dispatch, ownProps ) => { diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 0d313bc171b20e..04c8d44f2f6ba8 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -62,6 +62,9 @@ import * as tagCloud from './tag-cloud'; import * as classic from './classic'; +// Custom Entity Blocks +import * as post from './post'; + /** * Function to register an individual block. * @@ -138,6 +141,9 @@ export const registerCoreBlocks = () => { textColumns, verse, video, + + // Register Custom Entity Blocks. + post, ].forEach( registerBlock ); setDefaultBlockName( paragraph.name ); diff --git a/packages/block-library/src/post/block.json b/packages/block-library/src/post/block.json new file mode 100644 index 00000000000000..a8a2be4fe2eb11 --- /dev/null +++ b/packages/block-library/src/post/block.json @@ -0,0 +1,9 @@ +{ + "name": "core/post", + "category": "theme", + "attributes": { + "postId": { + "type": "number" + } + } +} diff --git a/packages/block-library/src/post/edit.js b/packages/block-library/src/post/edit.js new file mode 100644 index 00000000000000..5ed2f476872182 --- /dev/null +++ b/packages/block-library/src/post/edit.js @@ -0,0 +1,67 @@ +/** + * WordPress dependencies + */ +import { useState, useCallback } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { + Placeholder, + TextControl, + Button, + Spinner, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { EntityHandlers } from '@wordpress/editor'; + +export default function PostEdit( { attributes: { postId }, setAttributes } ) { + const [ placeholderPostId, setPlaceholderPostId ] = useState(); + const onPostIdSubmit = useCallback( ( event ) => { + event.preventDefault(); + + const value = event.currentTarget[ 0 ].value; + if ( ! value ) { + return; + } + + setAttributes( { postId: Number( value ) } ); + }, [] ); + + const entity = useSelect( + ( select ) => + postId && select( 'core' ).getEntityRecord( 'postType', 'post', postId ), + [ postId ] + ); + + if ( ! postId ) { + return ( + +
+ + + +
+ ); + } + + return entity ? ( + + ) : ( + + + + ); +} diff --git a/packages/block-library/src/post/index.js b/packages/block-library/src/post/index.js new file mode 100644 index 00000000000000..3d7ab2f8393b93 --- /dev/null +++ b/packages/block-library/src/post/index.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import edit from './edit'; +import save from './save'; + +const { name } = metadata; +export { metadata, name }; + +export const settings = { + title: __( 'Post' ), + edit, + save, +}; diff --git a/packages/block-library/src/post/save.js b/packages/block-library/src/post/save.js new file mode 100644 index 00000000000000..6e3eee15cb6e62 --- /dev/null +++ b/packages/block-library/src/post/save.js @@ -0,0 +1,8 @@ +/** + * WordPress dependencies + */ +import { EntityHandlers } from '@wordpress/editor'; + +export default function PostSave() { + return ; +} diff --git a/packages/block-library/src/post/style.scss b/packages/block-library/src/post/style.scss new file mode 100644 index 00000000000000..f3c57fddbad860 --- /dev/null +++ b/packages/block-library/src/post/style.scss @@ -0,0 +1,11 @@ +.wp-block-post { + // Extra specificity to override default Placeholder styles. + &__placeholder-form.wp-block-post__placeholder-form { + align-items: center; + text-align: left; + } + + &__placeholder-input { + width: 100px; + } +} diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index fd78fcc3b18d11..9443f2798fdf1a 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -23,6 +23,7 @@ @import "./text-columns/style.scss"; @import "./verse/style.scss"; @import "./video/style.scss"; +@import "./post/style.scss"; // The following selectors have increased specificity (using the :root prefix) // to assure colors take effect over another base class color, mainly to let diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 405e60109adda3..64c44c3a37c8be 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -29,6 +29,7 @@ export const DEFAULT_CATEGORIES = [ { slug: 'widgets', title: __( 'Widgets' ) }, { slug: 'embed', title: __( 'Embeds' ) }, { slug: 'reusable', title: __( 'Reusable Blocks' ) }, + { slug: 'theme', title: __( 'Theme Blocks' ) }, ]; /** diff --git a/packages/editor/src/components/entity-handlers/index.js b/packages/editor/src/components/entity-handlers/index.js new file mode 100644 index 00000000000000..8745101e292a78 --- /dev/null +++ b/packages/editor/src/components/entity-handlers/index.js @@ -0,0 +1,38 @@ +/** + * WordPress dependencies + */ +import { useRegistry, useSelect } from '@wordpress/data'; +import { InnerBlocks } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import EditorProvider from '../provider'; +import PostSavedState from '../post-saved-state'; + +export default function EntityHandlers( { entity, ...props } ) { + const registry = useRegistry(); + const editorSettings = useSelect( + ( select ) => select( 'core/editor' ).getEditorSettings(), + [] + ); + + // Using the provider like this will create a separate registry + // and store for the `entity`, while still syncing child blocks + // to the top level registry as inner blocks to maintain a + // seamless editing experience. + return ( + + + + ); +} + +// Expose this so that saving content to the +// top level registry's entity is more semantic. +EntityHandlers.Content = InnerBlocks.Content; diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index f8999c7868042f..b90d0677a05c2b 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -13,6 +13,7 @@ export { default as TextEditorGlobalKeyboardShortcuts } from './global-keyboard- export { default as EditorHistoryRedo } from './editor-history/redo'; export { default as EditorHistoryUndo } from './editor-history/undo'; export { default as EditorNotices } from './editor-notices'; +export { default as EntityHandlers } from './entity-handlers'; export { default as ErrorBoundary } from './error-boundary'; export { default as PageAttributesCheck } from './page-attributes/check'; export { default as PageAttributesOrder } from './page-attributes/order'; diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index b11341e1a33f0f..927fa40154cc58 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -9,9 +9,13 @@ import memize from 'memize'; */ import { compose } from '@wordpress/compose'; import { Component } from '@wordpress/element'; -import { withDispatch, withSelect } from '@wordpress/data'; +import { RegistryProvider, withSelect, withDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { BlockEditorProvider, transformStyles } from '@wordpress/block-editor'; +import { + transformStyles, + InnerBlocks, + BlockEditorProvider, +} from '@wordpress/block-editor'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; import { decodeEntities } from '@wordpress/html-entities'; @@ -148,12 +152,42 @@ class EditorProvider extends Component { reusableBlocks, resetEditorBlocksWithoutUndoLevel, hasUploadPermissions, + topLevelRegistry, } = this.props; if ( ! isReady ) { return null; } + // This indicates that this provider is nested in another. + // When this is the case, we want child blocks to sync to + // this provider's entity and to the top level's entity, as + // inner blocks. The `handles` prop of each provider will + // determine which properties, including the actual serialized + // content, get synced and persisted, or delegated to a parent provider. + if ( topLevelRegistry ) { + return ( + <> + { /* Explicit children components need this provider's entity. */ } + { children } + { /* + Inner blocks need the top level registry's block-editor store, + but we provide props to sync with this provider's entity. Just + like how the block-editor store syncs with the editor store. + */ } + + + + + ); + } + const editorSettings = this.getBlockEditorSettings( settings, reusableBlocks, From 783e97e4c42c310bb9b0517d0cfc74b691625adb Mon Sep 17 00:00:00 2001 From: epiqueras Date: Wed, 14 Aug 2019 15:34:42 -0700 Subject: [PATCH 18/24] Editor: Refactor custom sources into a simple getter/setter approach. --- .../src/components/inner-blocks/index.js | 29 +-- packages/blocks/src/api/test/parser.js | 14 -- .../plugins/meta-attribute-block/index.js | 32 ++- .../src/components/entity-handlers/index.js | 5 +- .../editor/src/components/provider/index.js | 27 +-- .../provider/with-registry-provider.js | 20 +- packages/editor/src/store/actions.js | 218 +----------------- .../editor/src/store/block-sources/README.md | 22 -- .../store/block-sources/__mocks__/index.js | 1 - .../editor/src/store/block-sources/index.js | 6 - .../editor/src/store/block-sources/meta.js | 55 ----- packages/editor/src/store/selectors.js | 22 ++ packages/editor/src/store/test/actions.js | 1 - 13 files changed, 93 insertions(+), 359 deletions(-) delete mode 100644 packages/editor/src/store/block-sources/README.md delete mode 100644 packages/editor/src/store/block-sources/__mocks__/index.js delete mode 100644 packages/editor/src/store/block-sources/index.js delete mode 100644 packages/editor/src/store/block-sources/meta.js diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index 843cd484ad2c35..5ad31113d05690 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -85,24 +85,27 @@ class InnerBlocks extends Component { } // Sync with controlled blocks value from parent, if possible. - if ( - resetEditorBlocks && - resetEditorBlocksWithoutUndoLevel && - prevProps.block.innerBlocks !== innerBlocks - ) { - this.isSyncingBlocks = innerBlocks; - if ( isLastBlockChangePersistent ) { - resetEditorBlocks( innerBlocks ); - } else { - resetEditorBlocksWithoutUndoLevel( innerBlocks ); + if ( this.isSyncingIncomingBlocks === innerBlocks ) { + this.isSyncingIncomingBlocks = null; + } else if ( prevProps.block.innerBlocks !== innerBlocks ) { + this.isSyncingIncomingBlocks = null; + + const resetFunc = isLastBlockChangePersistent ? + resetEditorBlocks : + resetEditorBlocksWithoutUndoLevel; + if ( resetFunc ) { + this.isSyncingOutcomingBlocks = innerBlocks; + resetFunc( innerBlocks ); } } // Accept changes to controlled blocks value from parent after a sync, if any. - if ( this.isSyncingBlocks === blocks ) { - this.isSyncingBlocks = null; + if ( this.isSyncingOutcomingBlocks === blocks ) { + this.isSyncingOutcomingBlocks = null; } else if ( prevProps.blocks !== blocks ) { - this.isSyncingBlocks = null; + this.isSyncingOutcomingBlocks = null; + + this.isSyncingIncomingBlocks = blocks; replaceInnerBlocks( blocks ); } } diff --git a/packages/blocks/src/api/test/parser.js b/packages/blocks/src/api/test/parser.js index f10001cb0c0133..e25e6a382339fa 100644 --- a/packages/blocks/src/api/test/parser.js +++ b/packages/blocks/src/api/test/parser.js @@ -355,20 +355,6 @@ describe( 'block parser', () => { ); expect( value ).toBe( 'chicken' ); } ); - - it( 'should return undefined for meta attributes', () => { - const value = getBlockAttribute( - 'content', - { - type: 'string', - source: 'meta', - meta: 'content', - }, - '
chicken
', - {} - ); - expect( value ).toBeUndefined(); - } ); } ); describe( 'getBlockAttributes()', () => { diff --git a/packages/e2e-tests/plugins/meta-attribute-block/index.js b/packages/e2e-tests/plugins/meta-attribute-block/index.js index 0910e648299aef..0c1d4320ad04a8 100644 --- a/packages/e2e-tests/plugins/meta-attribute-block/index.js +++ b/packages/e2e-tests/plugins/meta-attribute-block/index.js @@ -7,25 +7,21 @@ icon: 'star', category: 'common', - attributes: { - content: { - type: "string", - source: "meta", - meta: "my_meta", - }, - }, - edit: function( props ) { - return el( - 'input', - { - className: 'my-meta-input', - value: props.attributes.content, - onChange: function( event ) { - props.setAttributes( { content: event.target.value } ); - }, - } - ); + var editEntity = wp.data.useDispatch()( 'core/editor' ).editEntity; + return el( 'input', { + className: 'my-meta-input', + value: wp.data.useSelect( + ( select ) => + select( 'core/editor' ).getEditedEntityAttribute( 'meta' ).my_meta, + [] + ), + onChange: function( event ) { + editEntity( { + meta: { my_meta: event.target.value }, + } ); + }, + } ); }, save: function() { diff --git a/packages/editor/src/components/entity-handlers/index.js b/packages/editor/src/components/entity-handlers/index.js index 8745101e292a78..78fe6cedc555d8 100644 --- a/packages/editor/src/components/entity-handlers/index.js +++ b/packages/editor/src/components/entity-handlers/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useRegistry, useSelect } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { InnerBlocks } from '@wordpress/block-editor'; /** @@ -11,7 +11,6 @@ import EditorProvider from '../provider'; import PostSavedState from '../post-saved-state'; export default function EntityHandlers( { entity, ...props } ) { - const registry = useRegistry(); const editorSettings = useSelect( ( select ) => select( 'core/editor' ).getEditorSettings(), [] @@ -23,7 +22,7 @@ export default function EntityHandlers( { entity, ...props } ) { // seamless editing experience. return ( - { /* Explicit children components need this provider's entity. */ } { children } { /* - Inner blocks need the top level registry's block-editor store, + Inner blocks will use the top level registry's block-editor store, but we provide props to sync with this provider's entity. Just - like how the block-editor store syncs with the editor store. + like how the block-editor store syncs with the top level editor store. */ } - - - + ); } diff --git a/packages/editor/src/components/provider/with-registry-provider.js b/packages/editor/src/components/provider/with-registry-provider.js index 367782a82b4a42..fa3408899165b9 100644 --- a/packages/editor/src/components/provider/with-registry-provider.js +++ b/packages/editor/src/components/provider/with-registry-provider.js @@ -14,16 +14,26 @@ import applyMiddlewares from '../../store/middlewares'; const withRegistryProvider = createHigherOrderComponent( ( WrappedComponent ) => withRegistry( ( props ) => { - const { useSubRegistry = true, registry, ...additionalProps } = props; + const { + useSubRegistry = true, + noBlockEditorStore = false, + registry, + ...additionalProps + } = props; if ( ! useSubRegistry ) { return ; } const [ subRegistry, setSubRegistry ] = useState( null ); useEffect( () => { - const newRegistry = createRegistry( { - 'core/block-editor': blockEditorStoreConfig, - }, registry ); + const newRegistry = createRegistry( + noBlockEditorStore ? + {} : + { + 'core/block-editor': blockEditorStoreConfig, + }, + registry + ); const store = newRegistry.registerStore( 'core/editor', storeConfig ); // This should be removed after the refactoring of the effects to controls. applyMiddlewares( store ); @@ -36,7 +46,7 @@ const withRegistryProvider = createHigherOrderComponent( return ( - + ); } ), diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 6c7a0be829e6ce..ed6dceda63a0a6 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -17,7 +17,6 @@ import { getFreeformContentHandlerName, } from '@wordpress/blocks'; import { removep } from '@wordpress/autop'; -import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies @@ -32,120 +31,6 @@ import { getNotificationArgumentsForSaveFail, getNotificationArgumentsForTrashFail, } from './utils/notice-builder'; -import { awaitNextStateChange, getRegistry } from './controls'; -import * as sources from './block-sources'; - -/** - * Map of Registry instance to WeakMap of dependencies by custom source. - * - * @type WeakMap> - */ -const lastBlockSourceDependenciesByRegistry = new WeakMap; - -/** - * Given a blocks array, returns a blocks array with sourced attribute values - * applied. The reference will remain consistent with the original argument if - * no attribute values must be overridden. If sourced values are applied, the - * return value will be a modified copy of the original array. - * - * @param {WPBlock[]} blocks Original blocks array. - * - * @return {WPBlock[]} Blocks array with sourced values applied. - */ -function* getBlocksWithSourcedAttributes( blocks ) { - const registry = yield getRegistry(); - if ( ! lastBlockSourceDependenciesByRegistry.has( registry ) ) { - return blocks; - } - - const blockSourceDependencies = lastBlockSourceDependenciesByRegistry.get( registry ); - - let workingBlocks = blocks; - for ( let i = 0; i < blocks.length; i++ ) { - let block = blocks[ i ]; - const blockType = yield select( 'core/blocks', 'getBlockType', block.name ); - - for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) { - if ( ! sources[ schema.source ] || ! sources[ schema.source ].apply ) { - continue; - } - - if ( ! blockSourceDependencies.has( sources[ schema.source ] ) ) { - continue; - } - - const dependencies = blockSourceDependencies.get( sources[ schema.source ] ); - const sourcedAttributeValue = sources[ schema.source ].apply( schema, dependencies ); - - // It's only necessary to apply the value if it differs from the - // block's locally-assigned value, to avoid needlessly resetting - // the block editor. - if ( sourcedAttributeValue === block.attributes[ attributeName ] ) { - continue; - } - - // Create a shallow clone to mutate, leaving the original intact. - if ( workingBlocks === blocks ) { - workingBlocks = [ ...workingBlocks ]; - } - - block = { - ...block, - attributes: { - ...block.attributes, - [ attributeName ]: sourcedAttributeValue, - }, - }; - - workingBlocks.splice( i, 1, block ); - } - - // Recurse to apply source attributes to inner blocks. - if ( block.innerBlocks.length ) { - const appliedInnerBlocks = yield* getBlocksWithSourcedAttributes( block.innerBlocks ); - if ( appliedInnerBlocks !== block.innerBlocks ) { - if ( workingBlocks === blocks ) { - workingBlocks = [ ...workingBlocks ]; - } - - block = { - ...block, - innerBlocks: appliedInnerBlocks, - }; - - workingBlocks.splice( i, 1, block ); - } - } - } - - return workingBlocks; -} - -/** - * Refreshes the last block source dependencies, optionally for a given subset - * of sources (defaults to the full set of sources). - * - * @param {?Array} sourcesToUpdate Optional subset of sources to reset. - * - * @yield {Object} Yielded actions or control descriptors. - */ -function* resetLastBlockSourceDependencies( sourcesToUpdate = Object.values( sources ) ) { - if ( ! sourcesToUpdate.length ) { - return; - } - - const registry = yield getRegistry(); - if ( ! lastBlockSourceDependenciesByRegistry.has( registry ) ) { - lastBlockSourceDependenciesByRegistry.set( registry, new WeakMap ); - } - - const lastBlockSourceDependencies = lastBlockSourceDependenciesByRegistry.get( registry ); - - for ( const source of sourcesToUpdate ) { - const dependencies = yield* source.getDependencies(); - lastBlockSourceDependencies.set( source, dependencies ); - } -} /** * Returns an action generator used in signalling that editor has initialized with @@ -175,7 +60,6 @@ export function* setupEditor( post, edits, template ) { } yield resetPost( post ); - yield* resetLastBlockSourceDependencies(); yield { type: 'SETUP_EDITOR', post, @@ -188,7 +72,6 @@ export function* setupEditor( post, edits, template ) { const record = { ...post, ...edits }; yield dispatch( 'core', 'receiveEntityRecords', 'postType', post.type, record ); } - yield* __experimentalSubscribeSources(); } /** @@ -201,55 +84,6 @@ export function __experimentalTearDownEditor() { return { type: 'TEAR_DOWN_EDITOR' }; } -/** - * Returns an action generator which loops to await the next state change, - * calling to reset blocks when a block source dependencies change. - * - * @yield {Object} Action object. - */ -export function* __experimentalSubscribeSources() { - while ( true ) { - yield awaitNextStateChange(); - - // The bailout case: If the editor becomes unmounted, it will flag - // itself as non-ready. Effectively unsubscribes from the registry. - const isStillReady = yield select( 'core/editor', '__unstableIsEditorReady' ); - if ( ! isStillReady ) { - break; - } - - const registry = yield getRegistry(); - - let reset = false; - for ( const source of Object.values( sources ) ) { - if ( ! source.getDependencies ) { - continue; - } - - const dependencies = yield* source.getDependencies(); - - if ( ! lastBlockSourceDependenciesByRegistry.has( registry ) ) { - lastBlockSourceDependenciesByRegistry.set( registry, new WeakMap ); - } - - const lastBlockSourceDependencies = lastBlockSourceDependenciesByRegistry.get( registry ); - const lastDependencies = lastBlockSourceDependencies.get( source ); - - if ( ! isShallowEqual( dependencies, lastDependencies ) ) { - lastBlockSourceDependencies.set( source, dependencies ); - - // Allow the loop to continue in order to assign latest - // dependencies values, but mark for reset. - reset = true; - } - } - - if ( reset ) { - yield resetEditorBlocks( yield select( 'core/editor', 'getEditorBlocks' ), { __unstableShouldCreateUndoLevel: false } ); - } - } -} - /** * Returns an action object used in signalling that the latest version of the * post has been received, either by initialization or save. @@ -347,7 +181,7 @@ export function setupEditorState( post ) { } /** - * Returns an action object used in signalling that attributes of the post have + * Yields an action object used in signalling that attributes of the post have * been edited. * * @param {Object} edits Post attributes to edit. @@ -359,6 +193,16 @@ export function* editPost( edits ) { yield dispatch( 'core', 'editEntityRecord', 'postType', type, id, edits ); } +/** + * Yields an action object used in signalling that attributes of the entity have + * been edited. + * + * @param {Object} edits Entity attributes to edit. + * + * @yield {Object} Action object or control. + */ +export const editEntity = editPost; + /** * Returns action object produced by the updatePost creator augmented by * an optimist option that signals optimistically applying updates. @@ -763,48 +607,10 @@ export const serializeBlocks = memoize( * @yield {Object} Action object */ export function* resetEditorBlocks( blocks, options = {} ) { - const lastBlockAttributesChange = yield select( 'core/block-editor', '__experimentalGetLastBlockAttributeChanges' ); - - // Sync to sources from block attributes updates. - if ( lastBlockAttributesChange ) { - const updatedSources = new Set; - const updatedBlockTypes = new Set; - for ( const [ clientId, attributes ] of Object.entries( lastBlockAttributesChange ) ) { - const blockName = yield select( 'core/block-editor', 'getBlockName', clientId ); - if ( updatedBlockTypes.has( blockName ) ) { - continue; - } - - updatedBlockTypes.add( blockName ); - const blockType = yield select( 'core/blocks', 'getBlockType', blockName ); - - for ( const [ attributeName, newAttributeValue ] of Object.entries( attributes ) ) { - if ( ! blockType.attributes.hasOwnProperty( attributeName ) ) { - continue; - } - - const schema = blockType.attributes[ attributeName ]; - const source = sources[ schema.source ]; - - if ( source && source.update ) { - yield* source.update( schema, newAttributeValue ); - updatedSources.add( source ); - } - } - } - - // Dependencies are reset so that source dependencies subscription - // skips a reset which would otherwise occur by dependencies change. - // This assures that at most one reset occurs per block change. - yield* resetLastBlockSourceDependencies( Array.from( updatedSources ) ); - } - - const edits = { blocks: yield* getBlocksWithSourcedAttributes( blocks ) }; - + const edits = { blocks }; if ( options.__unstableShouldCreateUndoLevel !== false ) { edits.content = serializeBlocks( edits.blocks ); } - yield* editPost( edits ); } diff --git a/packages/editor/src/store/block-sources/README.md b/packages/editor/src/store/block-sources/README.md deleted file mode 100644 index 0c16d12b3159d2..00000000000000 --- a/packages/editor/src/store/block-sources/README.md +++ /dev/null @@ -1,22 +0,0 @@ -Block Sources -============= - -By default, the blocks module supports only attributes serialized into a block's comment demarcations, or those sourced from a [standard set of sources](https://developer.wordpress.org/block-editor/developers/block-api/block-attributes/). Since the blocks module is intended to be used in a number of contexts outside the post editor, the implementation of additional context-specific sources must be implemented as an external process. - -The post editor supports such additional sources for attributes (e.g. `meta` source). - -These sources are implemented here using a uniform interface for applying and responding to block updates to sourced attributes. In the future, this interface may be generalized to allow third-party extensions to either extend the post editor sources or implement their own in custom renderings of a block editor. - -## Source API - -### `getDependencies` - -Store control called on every store change, expected to return an object whose values represent the data blocks assigned this source depend on. When these values change, all blocks assigned this source are automatically updated. The value returned from this function is passed as the second argument of the source's `apply` function, where it is expected to be used as shared data relevant for sourcing the attribute value. - -### `apply` - -Function called to retrieve an attribute value for a block. Given the attribute schema and the dependencies defined by the source's `getDependencies`, the function should return the expected attribute value. - -### `update` - -Store control called when a single block's attributes have been updated, before the new block value has taken effect (i.e. before `apply` and `applyAll` are once again called). Given the attribute schema and updated value, the control should reflect the update on the source. diff --git a/packages/editor/src/store/block-sources/__mocks__/index.js b/packages/editor/src/store/block-sources/__mocks__/index.js deleted file mode 100644 index cb0ff5c3b541f6..00000000000000 --- a/packages/editor/src/store/block-sources/__mocks__/index.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/editor/src/store/block-sources/index.js b/packages/editor/src/store/block-sources/index.js deleted file mode 100644 index 542d774c313ce9..00000000000000 --- a/packages/editor/src/store/block-sources/index.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Internal dependencies - */ -import * as meta from './meta'; - -export { meta }; diff --git a/packages/editor/src/store/block-sources/meta.js b/packages/editor/src/store/block-sources/meta.js deleted file mode 100644 index 3910395c4a740d..00000000000000 --- a/packages/editor/src/store/block-sources/meta.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * WordPress dependencies - */ -import { select } from '@wordpress/data-controls'; - -/** - * Internal dependencies - */ -import { editPost } from '../actions'; - -/** - * Store control invoked upon a state change, responsible for returning an - * object of dependencies. When a change in dependencies occurs (by shallow - * equality of the returned object), blocks are reset to apply the new sourced - * value. - * - * @yield {Object} Optional yielded controls. - * - * @return {Object} Dependencies as object. - */ -export function* getDependencies() { - return { - meta: yield select( 'core/editor', 'getEditedPostAttribute', 'meta' ), - }; -} - -/** - * Given an attribute schema and dependencies data, returns a source value. - * - * @param {Object} schema Block type attribute schema. - * @param {Object} dependencies Source dependencies. - * @param {Object} dependencies.meta Post meta. - * - * @return {Object} Block attribute value. - */ -export function apply( schema, { meta } ) { - return meta[ schema.meta ]; -} - -/** - * Store control invoked upon a block attributes update, responsible for - * reflecting an update in a meta value. - * - * @param {Object} schema Block type attribute schema. - * @param {*} value Updated block attribute value. - * - * @yield {Object} Yielded action objects or store controls. - */ -export function* update( schema, value ) { - yield editPost( { - meta: { - [ schema.meta ]: value, - }, - } ); -} diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 10c2a0405ed6e8..1771827c3e4a67 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -279,6 +279,16 @@ export function getCurrentPostAttribute( state, attributeName ) { } } +/** + * Returns an attribute value of the saved entity. + * + * @param {Object} state Global application state. + * @param {string} attributeName Entity attribute name. + * + * @return {*} Entity attribute value. + */ +export const getCurrentEntityAttribute = getCurrentPostAttribute; + /** * Returns a single attribute of the post being edited, preferring the unsaved * edit if one exists, but merging with the attribute value for the last known @@ -333,6 +343,18 @@ export function getEditedPostAttribute( state, attributeName ) { return edits[ attributeName ]; } +/** + * Returns a single attribute of the entity being edited, preferring the unsaved + * edit if one exists, but falling back to the attribute for the last known + * saved state of the entity. + * + * @param {Object} state Global application state. + * @param {string} attributeName Entity attribute name. + * + * @return {*} Entity attribute value. + */ +export const getEditedEntityAttribute = getEditedPostAttribute; + /** * Returns an attribute value of the current autosave revision for a post, or * null if there is no autosave for the post. diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index f2940a0937c5d7..a1968fb6b606f5 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -14,7 +14,6 @@ import { } from '../constants'; jest.mock( '@wordpress/data-controls' ); -jest.mock( '../block-sources' ); select.mockImplementation( ( ...args ) => { const { select: actualSelect } = jest From b56cb434ad60f2329979d6183647c75c8d35d7ad Mon Sep 17 00:00:00 2001 From: epiqueras Date: Wed, 14 Aug 2019 16:43:18 -0700 Subject: [PATCH 19/24] Editor: Implement the `handles` prop for Entity Handlers. --- .../developers/data/data-core-editor.md | 55 +++++++++++++- packages/block-library/src/post/edit.js | 6 +- .../src/components/entity-handlers/index.js | 15 +++- .../editor/src/components/provider/index.js | 23 ++++-- packages/editor/src/store/actions.js | 73 ++++++++++--------- packages/editor/src/store/selectors.js | 54 ++++++++++++-- 6 files changed, 176 insertions(+), 50 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index d09dd0a49e0e7d..ee18138eb4890d 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -217,6 +217,19 @@ _Related_ - getClientIdsWithDescendants in core/block-editor store. +# **getCurrentEntityAttribute** + +Returns an attribute value of the saved entity. + +_Parameters_ + +- _state_ `Object`: Global application state. +- _attributeName_ `string`: Entity attribute name. + +_Returns_ + +- `*`: Entity attribute value. + # **getCurrentPost** Returns the post currently being edited in its last known saved state, not @@ -294,6 +307,21 @@ _Returns_ - `string`: Post type. +# **getEditedEntityAttribute** + +Returns a single attribute of the entity being edited, preferring the unsaved +edit if one exists, but falling back to the attribute for the last known +saved state of the entity. + +_Parameters_ + +- _state_ `Object`: Global application state. +- _attributeName_ `string`: Entity attribute name. + +_Returns_ + +- `*`: Entity attribute value. + # **getEditedPostAttribute** Returns a single attribute of the post being edited, preferring the unsaved @@ -383,6 +411,22 @@ _Related_ - getGlobalBlockCount in core/block-editor store. +# **getHandlesFilteredEdits** + +Returns an object with the edits broken down into a +handled edits object, an edits object that should +be delegated to a parent editor, and the parent's +dispatching function, if any. + +_Parameters_ + +- _state_ `Object`: Editor state. +- _\_edits_ `Object`: The edits, defaults to all of the entity's current edits. + +_Returns_ + +- `Object`: The object with the grouped edits and the parent's dispatching function. + # **getInserterItems** _Related_ @@ -1034,9 +1078,18 @@ _Returns_ - `Object`: Action object +# **editEntity** + +Yields an action object used in signalling that attributes of the entity have +been edited. + +_Parameters_ + +- _edits_ `Object`: Entity attributes to edit. + # **editPost** -Returns an action object used in signalling that attributes of the post have +Yields an action object used in signalling that attributes of the post have been edited. _Parameters_ diff --git a/packages/block-library/src/post/edit.js b/packages/block-library/src/post/edit.js index 5ed2f476872182..b291db4ed9aca0 100644 --- a/packages/block-library/src/post/edit.js +++ b/packages/block-library/src/post/edit.js @@ -58,7 +58,11 @@ export default function PostEdit( { attributes: { postId }, setAttributes } ) { } return entity ? ( - + ) : ( diff --git a/packages/editor/src/components/entity-handlers/index.js b/packages/editor/src/components/entity-handlers/index.js index 78fe6cedc555d8..ebd6373f7a932e 100644 --- a/packages/editor/src/components/entity-handlers/index.js +++ b/packages/editor/src/components/entity-handlers/index.js @@ -1,7 +1,8 @@ /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; import { InnerBlocks } from '@wordpress/block-editor'; /** @@ -10,11 +11,16 @@ import { InnerBlocks } from '@wordpress/block-editor'; import EditorProvider from '../provider'; import PostSavedState from '../post-saved-state'; -export default function EntityHandlers( { entity, ...props } ) { +export default function EntityHandlers( { + entity, + handles = { all: true }, + ...props +} ) { const editorSettings = useSelect( ( select ) => select( 'core/editor' ).getEditorSettings(), [] ); + const parentDispatch = useDispatch(); // Using the provider like this will create a separate registry // and store for the `entity`, while still syncing child blocks @@ -23,7 +29,10 @@ export default function EntityHandlers( { entity, ...props } ) { return ( ( { ...editorSettings, handles, parentDispatch } ), + [ editorSettings, parentDispatch ] + ) } post={ entity } { ...props } > diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index eabc723c8bca07..39efc97a6b79be 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -166,6 +166,21 @@ class EditorProvider extends Component { // determine which properties, including the actual serialized // content, get synced and persisted, or delegated to a parent provider. if ( noBlockEditorStore ) { + // Handle content and blocks if all attributes are implicitly + // handled and they are not explicitly not handled, or if they + // are explicitly handled. + const innerBlocksProps = + settings.handles && + ( ( settings.handles.all && + settings.handles.content !== false && + settings.handles.blocks !== false ) || + ( settings.handles.content && settings.handles.blocks ) ) ? + { + blocks, + resetEditorBlocks, + resetEditorBlocksWithoutUndoLevel, + } : + {}; return ( <> { children } @@ -174,13 +189,7 @@ class EditorProvider extends Component { but we provide props to sync with this provider's entity. Just like how the block-editor store syncs with the top level editor store. */ } - + ); } diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index ed6dceda63a0a6..197de0cf826822 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -189,8 +189,20 @@ export function setupEditorState( post ) { * @yield {Object} Action object or control. */ export function* editPost( edits ) { - const { id, type } = yield select( 'core/editor', 'getCurrentPost' ); - yield dispatch( 'core', 'editEntityRecord', 'postType', type, id, edits ); + const { handled, forParent, parentDispatch } = yield select( + STORE_KEY, + 'getHandlesFilteredEdits', + edits + ); + + if ( Object.keys( handled ).length ) { + const { id, type } = yield select( 'core/editor', 'getCurrentPost' ); + yield dispatch( 'core', 'editEntityRecord', 'postType', type, id, handled ); + } + + if ( parentDispatch && Object.keys( forParent ).length ) { + parentDispatch( 'core/editor' ).editPost( forParent ); + } } /** @@ -234,37 +246,32 @@ export function* savePost( options = {} ) { yield __experimentalRequestPostUpdateStart( options ); const postType = yield select( 'core/editor', 'getCurrentPostType' ); const postId = yield select( 'core/editor', 'getCurrentPostId' ); - yield dispatch( - 'core', - 'saveEditedEntityRecord', - 'postType', - postType, - postId, - { - ...options, - getSuccessNoticeActionArgs: ( previousEntity, entity, type ) => { - const args = getNotificationArgumentsForSaveSuccess( { - previousPost: previousEntity, - post: entity, - postType: type, - options, - } ); - if ( args && args.length ) { - return [ 'core/notices', 'createSuccessNotice', ...args ]; - } - }, - getFailureNoticeActionArgs: ( previousEntity, edits, error ) => { - const args = getNotificationArgumentsForSaveFail( { - post: previousEntity, - edits, - error, - } ); - if ( args && args.length ) { - return [ 'core/notices', 'createErrorNotice', ...args ]; - } - }, - } - ); + const { handled } = yield select( STORE_KEY, 'getHandlesFilteredEdits' ); + const record = { id: postId, ...handled }; + yield dispatch( 'core', 'saveEntityRecord', 'postType', postType, record, { + ...options, + getSuccessNoticeActionArgs: ( previousEntity, entity, type ) => { + const args = getNotificationArgumentsForSaveSuccess( { + previousPost: previousEntity, + post: entity, + postType: type, + options, + } ); + if ( args && args.length ) { + return [ 'core/notices', 'createSuccessNotice', ...args ]; + } + }, + getFailureNoticeActionArgs: ( previousEntity, edits, error ) => { + const args = getNotificationArgumentsForSaveFail( { + post: previousEntity, + edits, + error, + } ); + if ( args && args.length ) { + return [ 'core/notices', 'createErrorNotice', ...args ]; + } + }, + } ); yield __experimentalRequestPostUpdateFinish( options ); } diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 1771827c3e4a67..e123d7c4fc8fa7 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -122,17 +122,15 @@ export function hasChangedContent( state ) { * * @return {boolean} Whether unsaved values exist. */ -export const isEditedPostDirty = createRegistrySelector( ( select ) => ( state ) => { +export function isEditedPostDirty( state ) { // Edits should contain only fields which differ from the saved post (reset // at initial load and save complete). Thus, a non-empty edits state can be // inferred to contain unsaved values. - const postType = getCurrentPostType( state ); - const postId = getCurrentPostId( state ); - if ( select( 'core' ).hasEditsForEntityRecord( 'postType', postType, postId ) ) { + if ( Object.keys( getHandlesFilteredEdits( state ).handled ).length ) { return true; } return false; -} ); +} /** * Returns true if there are no unsaved values for the current edit session and @@ -1206,6 +1204,52 @@ export function getEditorSettings( state ) { return state.editorSettings; } +/** + * Returns an object with the edits broken down into a + * handled edits object, an edits object that should + * be delegated to a parent editor, and the parent's + * dispatching function, if any. + * + * @param {Object} state Editor state. + * @param {Object} _edits The edits, defaults + * to all of the entity's current edits. + * + * @return {Object} The object with the grouped edits and the + * parent's dispatching function. + */ +export const getHandlesFilteredEdits = createRegistrySelector( + ( select ) => ( state, edits ) => { + if ( ! edits ) { + const postId = getCurrentPostId( state ); + const postType = getCurrentPostType( state ); + edits = select( 'core' ).getEntityRecordNonTransientEdits( + 'postType', + postType, + postId + ); + } + const { handles = { all: true }, parentDispatch } = getEditorSettings( state ); + return Object.keys( edits ).reduce( + ( acc, key ) => { + // Handle edits if all attributes are implicitly + // handled and they are not explicitly not handled, + // or if they are explicitly handled. + if ( ( handles.all && handles[ key ] !== false ) || handles[ key ] ) { + acc.handled[ key ] = edits[ key ]; + } else { + acc.forParent[ key ] = edits[ key ]; + } + return acc; + }, + { + handled: {}, + forParent: {}, + parentDispatch, + } + ); + } +); + /* * Backward compatibility */ From f8a97bdd64db43b9ce994bc68591cec0299283dd Mon Sep 17 00:00:00 2001 From: epiqueras Date: Wed, 14 Aug 2019 17:22:16 -0700 Subject: [PATCH 20/24] Block Library: Make the post block delegate content to its parent and add a new post title block. --- packages/block-library/src/index.js | 2 ++ .../block-library/src/post-title/block.json | 4 +++ packages/block-library/src/post-title/edit.js | 29 +++++++++++++++++++ packages/block-library/src/post-title/icon.js | 11 +++++++ .../block-library/src/post-title/index.js | 20 +++++++++++++ .../plugins/meta-attribute-block/index.js | 4 +-- packages/editor/src/store/actions.js | 11 +++++-- 7 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 packages/block-library/src/post-title/block.json create mode 100644 packages/block-library/src/post-title/edit.js create mode 100644 packages/block-library/src/post-title/icon.js create mode 100644 packages/block-library/src/post-title/index.js diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 04c8d44f2f6ba8..e558577ac2a76d 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -64,6 +64,7 @@ import * as classic from './classic'; // Custom Entity Blocks import * as post from './post'; +import * as postTitle from './post-title'; /** * Function to register an individual block. @@ -144,6 +145,7 @@ export const registerCoreBlocks = () => { // Register Custom Entity Blocks. post, + postTitle, ].forEach( registerBlock ); setDefaultBlockName( paragraph.name ); diff --git a/packages/block-library/src/post-title/block.json b/packages/block-library/src/post-title/block.json new file mode 100644 index 00000000000000..11d61129406b8a --- /dev/null +++ b/packages/block-library/src/post-title/block.json @@ -0,0 +1,4 @@ +{ + "name": "core/post-title", + "category": "theme" +} diff --git a/packages/block-library/src/post-title/edit.js b/packages/block-library/src/post-title/edit.js new file mode 100644 index 00000000000000..e771ee0b65022b --- /dev/null +++ b/packages/block-library/src/post-title/edit.js @@ -0,0 +1,29 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { RichText } from '@wordpress/block-editor'; +import { cleanForSlug } from '@wordpress/editor'; +import { __ } from '@wordpress/i18n'; + +export default function PostTitleEdit() { + const title = useSelect( + ( select ) => select( 'core/editor' ).getEditedEntityAttribute( 'title' ), + [] + ); + const dispatch = useDispatch(); + return ( + + dispatch( 'core/editor' ).editEntity( { + title: value, + slug: cleanForSlug( value ), + } ) + } + tagName="h1" + placeholder={ __( 'Title' ) } + formattingControls={ [] } + /> + ); +} diff --git a/packages/block-library/src/post-title/icon.js b/packages/block-library/src/post-title/icon.js new file mode 100644 index 00000000000000..6dc60909619f82 --- /dev/null +++ b/packages/block-library/src/post-title/icon.js @@ -0,0 +1,11 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/components'; + +export default ( + + + + +); diff --git a/packages/block-library/src/post-title/index.js b/packages/block-library/src/post-title/index.js new file mode 100644 index 00000000000000..ba834fe69b0384 --- /dev/null +++ b/packages/block-library/src/post-title/index.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import icon from './icon'; +import edit from './edit'; + +const { name } = metadata; +export { metadata, name }; + +export const settings = { + title: __( 'Post Title' ), + icon, + edit, +}; diff --git a/packages/e2e-tests/plugins/meta-attribute-block/index.js b/packages/e2e-tests/plugins/meta-attribute-block/index.js index 0c1d4320ad04a8..3eb6162c66d5fe 100644 --- a/packages/e2e-tests/plugins/meta-attribute-block/index.js +++ b/packages/e2e-tests/plugins/meta-attribute-block/index.js @@ -8,7 +8,7 @@ category: 'common', edit: function( props ) { - var editEntity = wp.data.useDispatch()( 'core/editor' ).editEntity; + var dispatch = wp.data.useDispatch(); return el( 'input', { className: 'my-meta-input', value: wp.data.useSelect( @@ -17,7 +17,7 @@ [] ), onChange: function( event ) { - editEntity( { + dispatch( 'core/editor' ).editEntity( { meta: { my_meta: event.target.value }, } ); }, diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 197de0cf826822..0bd4b00ad1c692 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -239,9 +239,14 @@ export function* savePost( options = {} ) { if ( ! ( yield select( STORE_KEY, 'isEditedPostSaveable' ) ) ) { return; } - yield dispatch( STORE_KEY, 'editPost', { - content: yield select( 'core/editor', 'getEditedPostContent' ), - } ); + const { + handled: { content: handlesContent }, + } = yield select( STORE_KEY, 'getHandlesFilteredEdits', { content: true } ); + if ( handlesContent ) { + yield dispatch( STORE_KEY, 'editPost', { + content: yield select( 'core/editor', 'getEditedPostContent' ), + } ); + } yield __experimentalRequestPostUpdateStart( options ); const postType = yield select( 'core/editor', 'getCurrentPostType' ); From 0bfcf28198679619aa3eab10ca1a7d2f2e3916ed Mon Sep 17 00:00:00 2001 From: epiqueras Date: Thu, 15 Aug 2019 12:29:27 -0700 Subject: [PATCH 21/24] Block Editor: Provide blocks a way to edit entities without relying on a specific implementation or the "editor" module. --- packages/block-editor/README.md | 62 +++++++++++++++ .../src/components/entity-provider/index.js | 78 +++++++++++++++++++ packages/block-editor/src/components/index.js | 2 + packages/block-library/src/post-title/edit.js | 17 ++-- .../plugins/meta-attribute-block/index.js | 10 +-- packages/edit-post/src/editor.js | 38 +++++---- .../src/components/entity-handlers/index.js | 44 ++++++++--- packages/editor/src/components/index.js | 5 +- 8 files changed, 212 insertions(+), 44 deletions(-) create mode 100644 packages/block-editor/src/components/entity-provider/index.js diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 1d230d45fc3a23..7399ca2221ca94 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -210,6 +210,12 @@ _Returns_ Undocumented declaration. +# **EntityProvider** + +Context provider component for providing +the implementations of the entity getter +and setters. + # **FontSizePicker** Undocumented declaration. @@ -438,6 +444,62 @@ _Related_ - +# **useCurrentEntityAttribute** + +Hook for getting the currently persisted value +for an entity attribute. + +If there is no provider or it does not provide +an implementation for the required hook, +undefined is returned. + +_Parameters_ + +- _args_ `...*`: Any arguments the implementation might require. + +_Returns_ + +- `*`: The value. + +# **useEditedEntityAttribute** + +Hook for getting the edited value for an entity attribute, +falling back to the persisted value if there isn't +one. + +If there is no provider or it does not provide +an implementation for the required hook, +undefined is returned. + +_Parameters_ + +- _args_ `...*`: Any arguments the implementation might require. + +_Returns_ + +- `*`: The value. + +# **useEditEntity** + +Hook for accessing the entity's main edit function. + +If there is no provider or it does not provide +an implementation for the required hook, +a "noop" function is returned. + +_Returns_ + +- `Function`: The function or "noop". + +# **useEntity** + +Hook for accessing the entity getters and setters +provided by the nearest parent entity provider. + +_Returns_ + +- `Object`: The object with getters and setters. + # **Warning** Undocumented declaration. diff --git a/packages/block-editor/src/components/entity-provider/index.js b/packages/block-editor/src/components/entity-provider/index.js new file mode 100644 index 00000000000000..e34f9aa76fb3a8 --- /dev/null +++ b/packages/block-editor/src/components/entity-provider/index.js @@ -0,0 +1,78 @@ +/** + * WordPress dependencies + */ +import { createContext, useContext } from '@wordpress/element'; + +const Context = createContext( {} ); + +/** + * Context provider component for providing + * the implementations of the entity getter + * and setters. + * + * @typedef {Function} EntityProvider + */ +export default Context.Provider; + +/** + * Hook for accessing the entity getters and setters + * provided by the nearest parent entity provider. + * + * @return {Object} The object with getters and setters. + */ +export function useEntity() { + return useContext( Context ); +} + +/** + * Hook for getting the currently persisted value + * for an entity attribute. + * + * If there is no provider or it does not provide + * an implementation for the required hook, + * undefined is returned. + * + * @param {...*} args Any arguments the implementation + * might require. + * + * @return {*} The value. + */ +export function useCurrentEntityAttribute( ...args ) { + const { useCurrentEntityAttribute: func } = useEntity(); + return func ? func( ...args ) : undefined; +} + +/** + * Hook for getting the edited value for an entity attribute, + * falling back to the persisted value if there isn't + * one. + * + * If there is no provider or it does not provide + * an implementation for the required hook, + * undefined is returned. + * + * @param {...*} args Any arguments the implementation + * might require. + * + * @return {*} The value. + */ +export function useEditedEntityAttribute( ...args ) { + const { useEditedEntityAttribute: func } = useEntity(); + return func ? func( ...args ) : undefined; +} + +const editEntityNoop = () => {}; + +/** + * Hook for accessing the entity's main edit function. + * + * If there is no provider or it does not provide + * an implementation for the required hook, + * a "noop" function is returned. + * + * @return {Function} The function or "noop". + */ +export function useEditEntity() { + const { useEditEntity: func } = useEntity(); + return func ? func() : editEntityNoop; +} diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 9ad37a8929b433..d621b6b214177b 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -66,4 +66,6 @@ export { default as WritingFlow } from './writing-flow'; * State Related Components */ +export * from './entity-provider'; +export { default as EntityProvider } from './entity-provider'; export { default as BlockEditorProvider } from './provider'; diff --git a/packages/block-library/src/post-title/edit.js b/packages/block-library/src/post-title/edit.js index e771ee0b65022b..baa1d87932ce96 100644 --- a/packages/block-library/src/post-title/edit.js +++ b/packages/block-library/src/post-title/edit.js @@ -1,22 +1,21 @@ /** * WordPress dependencies */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { RichText } from '@wordpress/block-editor'; +import { + useEditEntity, + RichText, + useEditedEntityAttribute, +} from '@wordpress/block-editor'; import { cleanForSlug } from '@wordpress/editor'; import { __ } from '@wordpress/i18n'; export default function PostTitleEdit() { - const title = useSelect( - ( select ) => select( 'core/editor' ).getEditedEntityAttribute( 'title' ), - [] - ); - const dispatch = useDispatch(); + const editEntity = useEditEntity(); return ( - dispatch( 'core/editor' ).editEntity( { + editEntity( { title: value, slug: cleanForSlug( value ), } ) diff --git a/packages/e2e-tests/plugins/meta-attribute-block/index.js b/packages/e2e-tests/plugins/meta-attribute-block/index.js index 3eb6162c66d5fe..c0d2d5a9bf775a 100644 --- a/packages/e2e-tests/plugins/meta-attribute-block/index.js +++ b/packages/e2e-tests/plugins/meta-attribute-block/index.js @@ -8,16 +8,12 @@ category: 'common', edit: function( props ) { - var dispatch = wp.data.useDispatch(); + var editEntity = wp.blockEditor.useEditEntity(); return el( 'input', { className: 'my-meta-input', - value: wp.data.useSelect( - ( select ) => - select( 'core/editor' ).getEditedEntityAttribute( 'meta' ).my_meta, - [] - ), + value: wp.blockEditor.useEditedEntityAttribute( 'meta' ).my_meta, onChange: function( event ) { - dispatch( 'core/editor' ).editEntity( { + editEntity( { meta: { my_meta: event.target.value }, } ); }, diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index 70a29b54466a1d..d7a22a17184c2b 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -8,7 +8,13 @@ import { size, map, without } from 'lodash'; * WordPress dependencies */ import { withSelect } from '@wordpress/data'; -import { EditorProvider, ErrorBoundary, PostLockedModal } from '@wordpress/editor'; +import { EntityProvider } from '@wordpress/block-editor'; +import { + entityProviderValue, + EditorProvider, + ErrorBoundary, + PostLockedModal, +} from '@wordpress/editor'; import { StrictMode, Component } from '@wordpress/element'; import { KeyboardShortcuts, @@ -97,20 +103,22 @@ class Editor extends Component { - - - - - - - - + + + + + + + + + + diff --git a/packages/editor/src/components/entity-handlers/index.js b/packages/editor/src/components/entity-handlers/index.js index ebd6373f7a932e..8ba7288b77a405 100644 --- a/packages/editor/src/components/entity-handlers/index.js +++ b/packages/editor/src/components/entity-handlers/index.js @@ -2,8 +2,8 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; +import { EntityProvider, InnerBlocks } from '@wordpress/block-editor'; import { useMemo } from '@wordpress/element'; -import { InnerBlocks } from '@wordpress/block-editor'; /** * Internal dependencies @@ -11,6 +11,24 @@ import { InnerBlocks } from '@wordpress/block-editor'; import EditorProvider from '../provider'; import PostSavedState from '../post-saved-state'; +export const entityProviderValue = { + useCurrentEntityAttribute( attribute ) { + return useSelect( + ( select ) => select( 'core/editor' ).getCurrentEntityAttribute( attribute ), + [] + ); + }, + useEditedEntityAttribute( attribute ) { + return useSelect( + ( select ) => select( 'core/editor' ).getEditedEntityAttribute( attribute ), + [] + ); + }, + useEditEntity() { + return useDispatch()( 'core/editor' ).editEntity; + }, +}; + export default function EntityHandlers( { entity, handles = { all: true }, @@ -27,17 +45,19 @@ export default function EntityHandlers( { // to the top level registry as inner blocks to maintain a // seamless editing experience. return ( - ( { ...editorSettings, handles, parentDispatch } ), - [ editorSettings, parentDispatch ] - ) } - post={ entity } - { ...props } - > - - + + ( { ...editorSettings, handles, parentDispatch } ), + [ editorSettings, parentDispatch ] + ) } + post={ entity } + { ...props } + > + + + ); } diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index b90d0677a05c2b..9d643cbc903e8b 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -13,7 +13,10 @@ export { default as TextEditorGlobalKeyboardShortcuts } from './global-keyboard- export { default as EditorHistoryRedo } from './editor-history/redo'; export { default as EditorHistoryUndo } from './editor-history/undo'; export { default as EditorNotices } from './editor-notices'; -export { default as EntityHandlers } from './entity-handlers'; +export { + default as EntityHandlers, + entityProviderValue, +} from './entity-handlers'; export { default as ErrorBoundary } from './error-boundary'; export { default as PageAttributesCheck } from './page-attributes/check'; export { default as PageAttributesOrder } from './page-attributes/order'; From 7245d9989e2bcae222dd68f437daedd28eaf91a8 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Thu, 15 Aug 2019 20:12:42 -0700 Subject: [PATCH 22/24] Editor: Make the store multi-entity-kind compatible and implement the site title block. --- .../developers/data/data-core-editor.md | 15 ++++- packages/block-library/src/index.js | 2 + .../block-library/src/site-title/block.json | 4 ++ packages/block-library/src/site-title/edit.js | 45 ++++++++++++++ packages/block-library/src/site-title/icon.js | 12 ++++ .../block-library/src/site-title/index.js | 20 +++++++ packages/core-data/src/actions.js | 9 +++ packages/core-data/src/entities.js | 1 + packages/core-data/src/resolvers.js | 2 +- .../src/components/entity-handlers/index.js | 2 + .../editor/src/components/provider/index.js | 4 +- packages/editor/src/store/actions.js | 58 ++++++++++--------- packages/editor/src/store/reducer.js | 14 ++++- packages/editor/src/store/selectors.js | 35 ++++++++--- 14 files changed, 185 insertions(+), 38 deletions(-) create mode 100644 packages/block-library/src/site-title/block.json create mode 100644 packages/block-library/src/site-title/edit.js create mode 100644 packages/block-library/src/site-title/icon.js create mode 100644 packages/block-library/src/site-title/index.js diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index ee18138eb4890d..679b048a8fb2a7 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -230,6 +230,19 @@ _Returns_ - `*`: Entity attribute value. +# **getCurrentEntityKind** + +Returns the kind of the entity currently being edited, or null if the entity has +not yet been saved. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `?string`: The entity kind. + # **getCurrentPost** Returns the post currently being edited in its last known saved state, not @@ -259,7 +272,7 @@ _Returns_ # **getCurrentPostId** -Returns the ID of the post currently being edited, or null if the post has +Returns the ID of the post currently being edited, or undefined if the post has not yet been saved. _Parameters_ diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index e558577ac2a76d..86dc9074657f4b 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -63,6 +63,7 @@ import * as tagCloud from './tag-cloud'; import * as classic from './classic'; // Custom Entity Blocks +import * as siteTitle from './site-title'; import * as post from './post'; import * as postTitle from './post-title'; @@ -144,6 +145,7 @@ export const registerCoreBlocks = () => { video, // Register Custom Entity Blocks. + siteTitle, post, postTitle, ].forEach( registerBlock ); diff --git a/packages/block-library/src/site-title/block.json b/packages/block-library/src/site-title/block.json new file mode 100644 index 00000000000000..ab9a59291621ca --- /dev/null +++ b/packages/block-library/src/site-title/block.json @@ -0,0 +1,4 @@ +{ + "name": "core/site-title", + "category": "theme" +} diff --git a/packages/block-library/src/site-title/edit.js b/packages/block-library/src/site-title/edit.js new file mode 100644 index 00000000000000..572e59a80b23d6 --- /dev/null +++ b/packages/block-library/src/site-title/edit.js @@ -0,0 +1,45 @@ +/** + * WordPress dependencies + */ +import { + useEditEntity, + RichText, + useEditedEntityAttribute, +} from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; +import { EntityHandlers } from '@wordpress/editor'; +import { Placeholder, Spinner } from '@wordpress/components'; + +function TitleInput() { + const editEntity = useEditEntity(); + return ( + + editEntity( { + title, + } ) + } + tagName="h1" + placeholder={ __( 'Site Title' ) } + formattingControls={ [] } + /> + ); +} + +export default function SiteTitleEdit() { + const entity = useSelect( + ( select ) => select( 'core' ).getEntityRecord( 'root', 'site' ), + [] + ); + return entity ? ( + + + + ) : ( + + + + ); +} diff --git a/packages/block-library/src/site-title/icon.js b/packages/block-library/src/site-title/icon.js new file mode 100644 index 00000000000000..1ab74dec2c32d3 --- /dev/null +++ b/packages/block-library/src/site-title/icon.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path, Circle } from '@wordpress/components'; + +export default ( + + + + + +); diff --git a/packages/block-library/src/site-title/index.js b/packages/block-library/src/site-title/index.js new file mode 100644 index 00000000000000..4b8fc50b86b34f --- /dev/null +++ b/packages/block-library/src/site-title/index.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import icon from './icon'; +import edit from './edit'; + +const { name } = metadata; +export { metadata, name }; + +export const settings = { + title: __( 'Site Title' ), + icon, + edit, +}; diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index b50e7cbfa9c7c3..2fc6564c3762fa 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -74,6 +74,15 @@ export function addEntities( entities ) { * @return {Object} Action object. */ export function receiveEntityRecords( kind, name, records, query, invalidateCache = false ) { + // Ideally, the API should do this, but it doesn't + // for some entities, so we make sure these properties + // are set correctly here. + records = castArray( records ).map( ( record ) => { + record.kind = kind; + record.type = name; + return record; + } ); + let action; if ( query ) { action = receiveQueriedItems( records, query ); diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index a78142b3360f9a..0541e240a7f6c5 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -12,6 +12,7 @@ import { apiFetch, select } from './controls'; export const DEFAULT_ENTITY_KEY = 'id'; export const defaultEntities = [ + { name: 'site', kind: 'root', baseURL: '/wp/v2/settings' }, { name: 'postType', kind: 'root', key: 'slug', baseURL: '/wp/v2/types' }, { name: 'media', kind: 'root', baseURL: '/wp/v2/media', plural: 'mediaItems' }, { name: 'taxonomy', kind: 'root', key: 'slug', baseURL: '/wp/v2/taxonomies', plural: 'taxonomies' }, diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 8edfdbf895cded..670cb4adf2a750 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -47,7 +47,7 @@ export function* getCurrentUser() { * @param {string} name Entity name. * @param {number} key Record's key */ -export function* getEntityRecord( kind, name, key ) { +export function* getEntityRecord( kind, name, key = '' ) { const entities = yield getKindEntities( kind ); const entity = find( entities, { kind, name } ); if ( ! entity ) { diff --git a/packages/editor/src/components/entity-handlers/index.js b/packages/editor/src/components/entity-handlers/index.js index 8ba7288b77a405..9b57c8ac217ea8 100644 --- a/packages/editor/src/components/entity-handlers/index.js +++ b/packages/editor/src/components/entity-handlers/index.js @@ -32,6 +32,7 @@ export const entityProviderValue = { export default function EntityHandlers( { entity, handles = { all: true }, + children, ...props } ) { const editorSettings = useSelect( @@ -56,6 +57,7 @@ export default function EntityHandlers( { { ...props } > + { children } ); diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 39efc97a6b79be..eeab453815cfee 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -153,6 +153,7 @@ class EditorProvider extends Component { resetEditorBlocksWithoutUndoLevel, hasUploadPermissions, noBlockEditorStore, + noInnerBlocks, } = this.props; if ( ! isReady ) { @@ -170,6 +171,7 @@ class EditorProvider extends Component { // handled and they are not explicitly not handled, or if they // are explicitly handled. const innerBlocksProps = + ! noInnerBlocks && settings.handles && ( ( settings.handles.all && settings.handles.content !== false && @@ -189,7 +191,7 @@ class EditorProvider extends Component { but we provide props to sync with this provider's entity. Just like how the block-editor store syncs with the top level editor store. */ } - + { ! noInnerBlocks && } ); } diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 0bd4b00ad1c692..b76064de0ff5ab 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -51,7 +51,7 @@ export function* setupEditor( post, edits, template ) { content = post.content; } - let blocks = parse( content ); + let blocks = content ? parse( content ) : []; // Apply a template for new posts only, if exists. const isNewPost = post.status === 'auto-draft'; @@ -70,7 +70,7 @@ export function* setupEditor( post, edits, template ) { yield setupEditorState( post ); if ( edits ) { const record = { ...post, ...edits }; - yield dispatch( 'core', 'receiveEntityRecords', 'postType', post.type, record ); + yield dispatch( 'core', 'receiveEntityRecords', post.kind, post.type, record ); } } @@ -196,8 +196,8 @@ export function* editPost( edits ) { ); if ( Object.keys( handled ).length ) { - const { id, type } = yield select( 'core/editor', 'getCurrentPost' ); - yield dispatch( 'core', 'editEntityRecord', 'postType', type, id, handled ); + const { kind, type, id } = yield select( 'core/editor', 'getCurrentPost' ); + yield dispatch( 'core', 'editEntityRecord', kind, type, id, handled ); } if ( parentDispatch && Object.keys( forParent ).length ) { @@ -249,33 +249,39 @@ export function* savePost( options = {} ) { } yield __experimentalRequestPostUpdateStart( options ); + const entityKind = yield select( STORE_KEY, 'getCurrentEntityKind' ); const postType = yield select( 'core/editor', 'getCurrentPostType' ); const postId = yield select( 'core/editor', 'getCurrentPostId' ); const { handled } = yield select( STORE_KEY, 'getHandlesFilteredEdits' ); const record = { id: postId, ...handled }; - yield dispatch( 'core', 'saveEntityRecord', 'postType', postType, record, { + const isPost = entityKind === 'postType'; + yield dispatch( 'core', 'saveEntityRecord', entityKind, postType, record, { ...options, - getSuccessNoticeActionArgs: ( previousEntity, entity, type ) => { - const args = getNotificationArgumentsForSaveSuccess( { - previousPost: previousEntity, - post: entity, - postType: type, - options, - } ); - if ( args && args.length ) { - return [ 'core/notices', 'createSuccessNotice', ...args ]; - } - }, - getFailureNoticeActionArgs: ( previousEntity, edits, error ) => { - const args = getNotificationArgumentsForSaveFail( { - post: previousEntity, - edits, - error, - } ); - if ( args && args.length ) { - return [ 'core/notices', 'createErrorNotice', ...args ]; - } - }, + getSuccessNoticeActionArgs: + isPost && + ( ( previousEntity, entity, type ) => { + const args = getNotificationArgumentsForSaveSuccess( { + previousPost: previousEntity, + post: entity, + postType: type, + options, + } ); + if ( args && args.length ) { + return [ 'core/notices', 'createSuccessNotice', ...args ]; + } + } ), + getFailureNoticeActionArgs: + isPost && + ( ( previousEntity, edits, error ) => { + const args = getNotificationArgumentsForSaveFail( { + post: previousEntity, + edits, + error, + } ); + if ( args && args.length ) { + return [ 'core/notices', 'createErrorNotice', ...args ]; + } + } ), } ); yield __experimentalRequestPostUpdateFinish( options ); } diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index a88e05f30ac775..d55cb2cbf9d05a 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -99,7 +99,18 @@ export function shouldOverwriteState( action, previousAction ) { return isUpdatingSamePostProperty( action, previousAction ); } -export function postId( state = null, action ) { +export function entityKind( state = null, action ) { + switch ( action.type ) { + case 'SETUP_EDITOR_STATE': + case 'RESET_POST': + case 'UPDATE_POST': + return action.post.kind; + } + + return state; +} + +export function postId( state, action ) { switch ( action.type ) { case 'SETUP_EDITOR_STATE': case 'RESET_POST': @@ -383,6 +394,7 @@ export function editorSettings( state = EDITOR_SETTINGS_DEFAULTS, action ) { } export default optimist( combineReducers( { + entityKind, postId, postType, preferences, diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index e123d7c4fc8fa7..acce245f2728c2 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -154,10 +154,11 @@ export function isCleanNewPost( state ) { * @return {Object} Post object. */ export const getCurrentPost = createRegistrySelector( ( select ) => ( state ) => { + const entityKind = getCurrentEntityKind( state ); const postId = getCurrentPostId( state ); const postType = getCurrentPostType( state ); - const post = select( 'core' ).getEntityRecord( 'postType', postType, postId ); + const post = select( 'core' ).getEntityRecord( entityKind, postType, postId ); if ( post ) { return post; } @@ -168,6 +169,18 @@ export const getCurrentPost = createRegistrySelector( ( select ) => ( state ) => return EMPTY_OBJECT; } ); +/** + * Returns the kind of the entity currently being edited, or null if the entity has + * not yet been saved. + * + * @param {Object} state Global application state. + * + * @return {?string} The entity kind. + */ +export function getCurrentEntityKind( state ) { + return state.entityKind; +} + /** * Returns the post type of the post currently being edited. * @@ -180,7 +193,7 @@ export function getCurrentPostType( state ) { } /** - * Returns the ID of the post currently being edited, or null if the post has + * Returns the ID of the post currently being edited, or undefined if the post has * not yet been saved. * * @param {Object} state Global application state. @@ -223,9 +236,10 @@ export function getCurrentPostLastRevisionId( state ) { * @return {Object} Object of key value pairs comprising unsaved edits. */ export const getPostEdits = createRegistrySelector( ( select ) => ( state ) => { + const entityKind = getCurrentEntityKind( state ); const postType = getCurrentPostType( state ); const postId = getCurrentPostId( state ); - return select( 'core' ).getEntityRecordEdits( 'postType', postType, postId ) || EMPTY_OBJECT; + return select( 'core' ).getEntityRecordEdits( entityKind, postType, postId ) || EMPTY_OBJECT; } ); /** @@ -685,9 +699,10 @@ export function isEditedPostDateFloating( state ) { * @return {boolean} Whether post is being saved. */ export const isSavingPost = createRegistrySelector( ( select ) => ( state ) => { + const entityKind = getCurrentEntityKind( state ); const postType = getCurrentPostType( state ); const postId = getCurrentPostId( state ); - return select( 'core' ).isSavingEntityRecord( 'postType', postType, postId ); + return select( 'core' ).isSavingEntityRecord( entityKind, postType, postId ); } ); /** @@ -700,9 +715,10 @@ export const isSavingPost = createRegistrySelector( ( select ) => ( state ) => { */ export const didPostSaveRequestSucceed = createRegistrySelector( ( select ) => ( state ) => { + const entityKind = getCurrentEntityKind( state ); const postType = getCurrentPostType( state ); const postId = getCurrentPostId( state ); - return ! select( 'core' ).getLastEntitySaveError( 'postType', postType, postId ); + return ! select( 'core' ).getLastEntitySaveError( entityKind, postType, postId ); } ); @@ -716,9 +732,10 @@ export const didPostSaveRequestSucceed = createRegistrySelector( */ export const didPostSaveRequestFail = createRegistrySelector( ( select ) => ( state ) => { + const entityKind = getCurrentEntityKind( state ); const postType = getCurrentPostType( state ); const postId = getCurrentPostId( state ); - return !! select( 'core' ).getLastEntitySaveError( 'postType', postType, postId ); + return !! select( 'core' ).getLastEntitySaveError( entityKind, postType, postId ); } ); @@ -873,10 +890,11 @@ export function getBlocksForSerialization( state ) { * @return {string} Post content. */ export const getEditedPostContent = createRegistrySelector( ( select ) => ( state ) => { + const entityKind = getCurrentEntityKind( state ); const postId = getCurrentPostId( state ); const postType = getCurrentPostType( state ); const record = select( 'core' ).getEditedEntityRecord( - 'postType', + entityKind, postType, postId ); @@ -1220,10 +1238,11 @@ export function getEditorSettings( state ) { export const getHandlesFilteredEdits = createRegistrySelector( ( select ) => ( state, edits ) => { if ( ! edits ) { + const entityKind = getCurrentEntityKind( state ); const postId = getCurrentPostId( state ); const postType = getCurrentPostType( state ); edits = select( 'core' ).getEntityRecordNonTransientEdits( - 'postType', + entityKind, postType, postId ); From 0f64686932c72327cd8428fb310f9b6af094ba35 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Thu, 15 Aug 2019 20:46:25 -0700 Subject: [PATCH 23/24] Block Library: Implement post content block. --- packages/block-library/src/index.js | 2 ++ .../block-library/src/post-content/block.json | 4 ++++ .../block-library/src/post-content/edit.js | 10 ++++++++++ .../block-library/src/post-content/icon.js | 11 ++++++++++ .../block-library/src/post-content/index.js | 20 +++++++++++++++++++ .../src/components/entity-handlers/index.js | 9 ++++++--- 6 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 packages/block-library/src/post-content/block.json create mode 100644 packages/block-library/src/post-content/edit.js create mode 100644 packages/block-library/src/post-content/icon.js create mode 100644 packages/block-library/src/post-content/index.js diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 86dc9074657f4b..7097afbb804e3b 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -66,6 +66,7 @@ import * as classic from './classic'; import * as siteTitle from './site-title'; import * as post from './post'; import * as postTitle from './post-title'; +import * as postContent from './post-content'; /** * Function to register an individual block. @@ -148,6 +149,7 @@ export const registerCoreBlocks = () => { siteTitle, post, postTitle, + postContent, ].forEach( registerBlock ); setDefaultBlockName( paragraph.name ); diff --git a/packages/block-library/src/post-content/block.json b/packages/block-library/src/post-content/block.json new file mode 100644 index 00000000000000..7472bd1b04c150 --- /dev/null +++ b/packages/block-library/src/post-content/block.json @@ -0,0 +1,4 @@ +{ + "name": "core/post-content", + "category": "theme" +} diff --git a/packages/block-library/src/post-content/edit.js b/packages/block-library/src/post-content/edit.js new file mode 100644 index 00000000000000..a554f71185a4d6 --- /dev/null +++ b/packages/block-library/src/post-content/edit.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { EntityHandlers } from '@wordpress/editor'; + +export default function PostContentEdit() { + return ( + + ); +} diff --git a/packages/block-library/src/post-content/icon.js b/packages/block-library/src/post-content/icon.js new file mode 100644 index 00000000000000..2cd26e9d5f5a31 --- /dev/null +++ b/packages/block-library/src/post-content/icon.js @@ -0,0 +1,11 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/components'; + +export default ( + + + + +); diff --git a/packages/block-library/src/post-content/index.js b/packages/block-library/src/post-content/index.js new file mode 100644 index 00000000000000..76a1a07148094e --- /dev/null +++ b/packages/block-library/src/post-content/index.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import icon from './icon'; +import edit from './edit'; + +const { name } = metadata; +export { metadata, name }; + +export const settings = { + title: __( 'Post Content' ), + icon, + edit, +}; diff --git a/packages/editor/src/components/entity-handlers/index.js b/packages/editor/src/components/entity-handlers/index.js index 9b57c8ac217ea8..104375e3109c22 100644 --- a/packages/editor/src/components/entity-handlers/index.js +++ b/packages/editor/src/components/entity-handlers/index.js @@ -35,8 +35,11 @@ export default function EntityHandlers( { children, ...props } ) { - const editorSettings = useSelect( - ( select ) => select( 'core/editor' ).getEditorSettings(), + const { editorSettings, parentEntity } = useSelect( + ( select ) => ( { + editorSettings: select( 'core/editor' ).getEditorSettings(), + parentEntity: select( 'core/editor' ).getCurrentPost(), + } ), [] ); const parentDispatch = useDispatch(); @@ -53,7 +56,7 @@ export default function EntityHandlers( { () => ( { ...editorSettings, handles, parentDispatch } ), [ editorSettings, parentDispatch ] ) } - post={ entity } + post={ entity || parentEntity } { ...props } > From 35bc41ccfabab35454be83831deac6193e086db3 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Fri, 16 Aug 2019 13:50:31 -0700 Subject: [PATCH 24/24] Block Library: Implement server side rendering for the site title block. --- lib/blocks.php | 1 + .../block-library/src/site-title/index.php | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 packages/block-library/src/site-title/index.php diff --git a/lib/blocks.php b/lib/blocks.php index e6c89aca8c1e3b..526268afe9929c 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -28,6 +28,7 @@ function gutenberg_reregister_core_block_types() { 'shortcode.php' => 'core/shortcode', 'search.php' => 'core/search', 'tag-cloud.php' => 'core/tag-cloud', + 'site-title.php' => 'core/site-title', ); $registry = WP_Block_Type_Registry::get_instance(); diff --git a/packages/block-library/src/site-title/index.php b/packages/block-library/src/site-title/index.php new file mode 100644 index 00000000000000..6e5b5786436f09 --- /dev/null +++ b/packages/block-library/src/site-title/index.php @@ -0,0 +1,28 @@ +%s', get_bloginfo( 'name' ) ); +} + +/** + * Registers the `core/site-title` block on the server. + */ +function register_block_core_site_title() { + register_block_type( + 'core/site-title', + array( + 'render_callback' => 'render_block_core_site_title', + ) + ); +} +add_action( 'init', 'register_block_core_site_title' );