From 472767c5d3c7ef7c90fe88854af792ed677b3868 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Fri, 18 Oct 2019 13:09:42 -0700 Subject: [PATCH 01/15] Core Data: Add selector for getting all entity changes. --- .../developers/data/data-core.md | 13 ++++++ packages/core-data/README.md | 13 ++++++ packages/core-data/src/selectors.js | 46 +++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/docs/designers-developers/developers/data/data-core.md b/docs/designers-developers/developers/data/data-core.md index 79f3f41991c08..025203a27121d 100644 --- a/docs/designers-developers/developers/data/data-core.md +++ b/docs/designers-developers/developers/data/data-core.md @@ -153,6 +153,19 @@ _Returns_ - `?Object`: Record. +# **getEntityRecordChangesByRecord** + +Returns a list of objects with each edited entity +record and its corresponding edits. + +_Parameters_ + +- _state_ `Object`: State tree. + +_Returns_ + +- `Array`: The list of edited records with their edits. + # **getEntityRecordEdits** Returns the specified entity record's edits. diff --git a/packages/core-data/README.md b/packages/core-data/README.md index a2bb3ccbe6dee..ddd3d96056b5a 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -366,6 +366,19 @@ _Returns_ - `?Object`: Record. +# **getEntityRecordChangesByRecord** + +Returns a list of objects with each edited entity +record and its corresponding edits. + +_Parameters_ + +- _state_ `Object`: State tree. + +_Returns_ + +- `Array`: The list of edited records with their edits. + # **getEntityRecordEdits** Returns the specified entity record's edits. diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index 1475ffd2562f8..a5451d4f61029 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -153,6 +153,52 @@ export function getEntityRecords( state, kind, name, query ) { return getQueriedItems( queriedState, query ); } +/** + * Returns a list of objects with each edited entity + * record and its corresponding edits. + * + * @param {Object} state State tree. + * + * @return {Array} The list of edited records with their edits. + */ +export const getEntityRecordChangesByRecord = createSelector( + ( state ) => { + const { + entities: { data }, + } = state; + return Object.keys( data ).reduce( ( acc, kind ) => { + Object.keys( data[ kind ] ).forEach( ( name ) => { + const editsKeys = Object.keys( data[ kind ][ name ].edits ).filter( ( editsKey ) => + hasEditsForEntityRecord( state, kind, name, editsKey ) + ); + if ( editsKeys.length ) { + if ( ! acc[ kind ] ) { + acc[ kind ] = {}; + } + if ( ! acc[ kind ][ name ] ) { + acc[ kind ][ name ] = {}; + } + editsKeys.forEach( + ( editsKey ) => + ( acc[ kind ][ name ][ editsKey ] = { + rawRecord: getRawEntityRecord( state, kind, name, editsKey ), + edits: getEntityRecordNonTransientEdits( + state, + kind, + name, + editsKey + ), + } ) + ); + } + } ); + + return acc; + }, {} ); + }, + ( state ) => [ state.entities.data ] +); + /** * Returns the specified entity record's edits. * From a3528398d038529d3b529cadd0b3ded9d45031cf Mon Sep 17 00:00:00 2001 From: epiqueras Date: Fri, 18 Oct 2019 13:22:13 -0700 Subject: [PATCH 02/15] Editor: Add global changes save button. --- .../edit-post/src/components/header/index.js | 4 + .../components/entities-saved-states/index.js | 111 ++++++++++++++++++ .../entities-saved-states/style.scss | 5 + packages/editor/src/components/index.js | 1 + packages/editor/src/style.scss | 1 + 5 files changed, 122 insertions(+) create mode 100644 packages/editor/src/components/entities-saved-states/index.js create mode 100644 packages/editor/src/components/entities-saved-states/style.scss diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 26dd1812d6b5e..026d5901a28c0 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -4,6 +4,7 @@ import { __ } from '@wordpress/i18n'; import { IconButton } from '@wordpress/components'; import { + EntitiesSavedStates, PostPreviewButton, PostSavedState, } from '@wordpress/editor'; @@ -26,6 +27,7 @@ function Header( { isEditorSidebarOpened, isPublishSidebarOpened, isSaving, + enableFullSiteEditing, openGeneralSidebar, } ) { const toggleGeneralSidebar = isEditorSidebarOpened ? closeGeneralSidebar : openGeneralSidebar; @@ -37,6 +39,7 @@ function Header( {
+ { enableFullSiteEditing && } { ! isPublishSidebarOpened && ( // This button isn't completely hidden by the publish sidebar. // We can't hide the whole toolbar when the publish sidebar is open because @@ -77,6 +80,7 @@ export default compose( isEditorSidebarOpened: select( 'core/edit-post' ).isEditorSidebarOpened(), isPublishSidebarOpened: select( 'core/edit-post' ).isPublishSidebarOpened(), isSaving: select( 'core/edit-post' ).isSavingMetaBoxes(), + enableFullSiteEditing: select( 'core/editor' ).getEditorSettings( '__experimentalEnableFullSiteEditing' ), } ) ), withDispatch( ( dispatch, ownProps, { select } ) => { const { getBlockSelectionStart } = select( 'core/block-editor' ); diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js new file mode 100644 index 0000000000000..4b09a25df6171 --- /dev/null +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -0,0 +1,111 @@ +/** + * External dependencies + */ +import { startCase } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { useState, useCallback } from '@wordpress/element'; +import { Button, Modal, CheckboxControl } from '@wordpress/components'; + +const EntitiesSavedStatesCheckbox = ( { + id, + name, + changes: { rawRecord, edits }, + checked, + setCheckedById, +} ) => ( + setCheckedById( id, nextChecked ), [ id ] ) } + /> +); + +export default function EntitiesSavedStates() { + const entityRecordChangesByRecord = useSelect( ( select ) => + select( 'core' ).getEntityRecordChangesByRecord() + ); + const { saveEditedEntityRecord } = useDispatch( 'core' ); + + const [ isOpen, setIsOpen ] = useState( false ); + const [ checkedById, _setCheckedById ] = useState( {} ); + + const openModal = useCallback( setIsOpen.bind( null, true ), [] ); + const closeModal = useCallback( setIsOpen.bind( null, false ), [] ); + const setCheckedById = useCallback( + ( id, checked ) => + _setCheckedById( ( prevCheckedById ) => { + const nextCheckedById = { + ...prevCheckedById, + }; + if ( checked ) { + nextCheckedById[ id ] = true; + } else { + delete nextCheckedById[ id ]; + } + return nextCheckedById; + } ), + [] + ); + const saveCheckedEntities = useCallback( () => { + closeModal(); + Object.keys( checkedById ).forEach( ( id ) => + saveEditedEntityRecord( ...id.split( ' | ' ) ) + ); + }, [ checkedById ] ); + + const changedKinds = Object.keys( entityRecordChangesByRecord ); + return ( + changedKinds.length > 0 && ( + <> + + { isOpen && ( + + { changedKinds.map( ( changedKind ) => + Object.keys( entityRecordChangesByRecord[ changedKind ] ).map( + ( changedName ) => + Object.keys( + entityRecordChangesByRecord[ changedKind ][ changedName ] + ).map( ( changedKey ) => { + const id = `${ changedKind } | ${ changedName } | ${ changedKey }`; + return ( + + ); + } ) + ) + ) } + + + ) } + + ) + ); +} diff --git a/packages/editor/src/components/entities-saved-states/style.scss b/packages/editor/src/components/entities-saved-states/style.scss new file mode 100644 index 0000000000000..6751b3ef2673b --- /dev/null +++ b/packages/editor/src/components/entities-saved-states/style.scss @@ -0,0 +1,5 @@ +.editor-entities-saved-states__save-button { + display: block; + margin-left: auto; + margin-right: 0; +} diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index d5af7f4ff443b..ce7f2e60c528d 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 EntitiesSavedStates } from './entities-saved-states'; export { default as ErrorBoundary } from './error-boundary'; export { default as LocalAutosaveMonitor } from './local-autosave-monitor'; export { default as PageAttributesCheck } from './page-attributes/check'; diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index 949f751bd72af..91255a878fd50 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -1,6 +1,7 @@ @import "./components/autocompleters/style.scss"; @import "./components/document-outline/style.scss"; @import "./components/editor-notices/style.scss"; +@import "./components/entities-saved-states/style.scss"; @import "./components/error-boundary/style.scss"; @import "./components/page-attributes/style.scss"; @import "./components/post-excerpt/style.scss"; From d5191534a3049177ebd9194eabe46bcc75315cff Mon Sep 17 00:00:00 2001 From: epiqueras Date: Wed, 30 Oct 2019 21:52:21 -0700 Subject: [PATCH 03/15] Editor: Implement pre-pre-publish-flow global save flow. --- .../edit-post/src/components/header/index.js | 4 - .../components/entities-saved-states/index.js | 97 ++++++++----------- .../components/post-publish-button/index.js | 68 +++++++++++-- packages/editor/src/store/selectors.js | 59 +++++++++++ 4 files changed, 160 insertions(+), 68 deletions(-) diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 026d5901a28c0..26dd1812d6b5e 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -4,7 +4,6 @@ import { __ } from '@wordpress/i18n'; import { IconButton } from '@wordpress/components'; import { - EntitiesSavedStates, PostPreviewButton, PostSavedState, } from '@wordpress/editor'; @@ -27,7 +26,6 @@ function Header( { isEditorSidebarOpened, isPublishSidebarOpened, isSaving, - enableFullSiteEditing, openGeneralSidebar, } ) { const toggleGeneralSidebar = isEditorSidebarOpened ? closeGeneralSidebar : openGeneralSidebar; @@ -39,7 +37,6 @@ function Header( {
- { enableFullSiteEditing && } { ! isPublishSidebarOpened && ( // This button isn't completely hidden by the publish sidebar. // We can't hide the whole toolbar when the publish sidebar is open because @@ -80,7 +77,6 @@ export default compose( isEditorSidebarOpened: select( 'core/edit-post' ).isEditorSidebarOpened(), isPublishSidebarOpened: select( 'core/edit-post' ).isPublishSidebarOpened(), isSaving: select( 'core/edit-post' ).isSavingMetaBoxes(), - enableFullSiteEditing: select( 'core/editor' ).getEditorSettings( '__experimentalEnableFullSiteEditing' ), } ) ), withDispatch( ( dispatch, ownProps, { select } ) => { const { getBlockSelectionStart } = select( 'core/block-editor' ); diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index 4b09a25df6171..308826283af25 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -6,9 +6,9 @@ import { startCase } from 'lodash'; /** * WordPress dependencies */ +import { Button, CheckboxControl, Modal } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { useState, useCallback } from '@wordpress/element'; -import { Button, Modal, CheckboxControl } from '@wordpress/components'; const EntitiesSavedStatesCheckbox = ( { id, @@ -25,17 +25,13 @@ const EntitiesSavedStatesCheckbox = ( { /> ); -export default function EntitiesSavedStates() { +export default function EntitiesSavedStates( { isOpen, onRequestClose } ) { const entityRecordChangesByRecord = useSelect( ( select ) => select( 'core' ).getEntityRecordChangesByRecord() ); const { saveEditedEntityRecord } = useDispatch( 'core' ); - const [ isOpen, setIsOpen ] = useState( false ); const [ checkedById, _setCheckedById ] = useState( {} ); - - const openModal = useCallback( setIsOpen.bind( null, true ), [] ); - const closeModal = useCallback( setIsOpen.bind( null, false ), [] ); const setCheckedById = useCallback( ( id, checked ) => _setCheckedById( ( prevCheckedById ) => { @@ -52,60 +48,51 @@ export default function EntitiesSavedStates() { [] ); const saveCheckedEntities = useCallback( () => { - closeModal(); Object.keys( checkedById ).forEach( ( id ) => - saveEditedEntityRecord( ...id.split( ' | ' ) ) + saveEditedEntityRecord( ...id.split( ' | ' ).filter( ( s ) => s !== 'undefined' ) ) ); + onRequestClose( checkedById ); }, [ checkedById ] ); - - const changedKinds = Object.keys( entityRecordChangesByRecord ); return ( - changedKinds.length > 0 && ( - <> - - { isOpen && ( - - { changedKinds.map( ( changedKind ) => - Object.keys( entityRecordChangesByRecord[ changedKind ] ).map( - ( changedName ) => - Object.keys( - entityRecordChangesByRecord[ changedKind ][ changedName ] - ).map( ( changedKey ) => { - const id = `${ changedKind } | ${ changedName } | ${ changedKey }`; - return ( - - ); - } ) - ) - ) } - - + isOpen && ( + + { Object.keys( entityRecordChangesByRecord ).map( ( changedKind ) => + Object.keys( entityRecordChangesByRecord[ changedKind ] ).map( + ( changedName ) => + Object.keys( + entityRecordChangesByRecord[ changedKind ][ changedName ] + ).map( ( changedKey ) => { + const id = `${ changedKind } | ${ changedName } | ${ changedKey }`; + return ( + + ); + } ) + ) ) } - + + ) ); } diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index b378fe7ae798d..e92d4c9128f1d 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -6,7 +6,7 @@ import { noop, get } from 'lodash'; /** * WordPress dependencies */ -import { Button } from '@wordpress/components'; +import { Button, Dashicon } from '@wordpress/components'; import { Component, createRef } from '@wordpress/element'; import { withSelect, withDispatch } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -15,11 +15,20 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ +import EntitiesSavedStates from '../entities-saved-states'; import PublishButtonLabel from './label'; + export class PostPublishButton extends Component { constructor( props ) { super( props ); this.buttonNode = createRef(); + + this.createOnClick = this.createOnClick.bind( this ); + this.closeEntitiesSavedStates = this.closeEntitiesSavedStates.bind( this ); + + this.state = { + entitiesSavedStatesCallback: false, + }; } componentDidMount() { if ( this.props.focusOnMount ) { @@ -27,6 +36,29 @@ export class PostPublishButton extends Component { } } + createOnClick( callback ) { + return ( ...args ) => { + const { hasNonPostEntityChanges } = this.props; + if ( hasNonPostEntityChanges ) { + return this.setState( { + entitiesSavedStatesCallback: () => callback( ...args ), + } ); + } + + return callback( ...args ); + }; + } + + closeEntitiesSavedStates( savedById ) { + const { postType, postId } = this.props; + const { entitiesSavedStatesCallback } = this.state; + this.setState( { entitiesSavedStatesCallback: false }, () => { + if ( savedById[ `postType | ${ postType } | ${ postId }` ] ) { + entitiesSavedStatesCallback(); + } + } ); + } + render() { const { forceIsDirty, @@ -45,7 +77,12 @@ export class PostPublishButton extends Component { onSubmit = noop, onToggle, visibility, + hasNonPostEntityChanges, } = this.props; + const { + entitiesSavedStatesCallback, + } = this.state; + const isButtonDisabled = isSaving || forceIsSaving || @@ -92,7 +129,7 @@ export class PostPublishButton extends Component { className: 'editor-post-publish-button', isBusy: isSaving && isPublished, isPrimary: true, - onClick: onClickButton, + onClick: this.createOnClick( onClickButton ), }; const toggleProps = { @@ -101,7 +138,7 @@ export class PostPublishButton extends Component { className: 'editor-post-publish-panel__toggle', isBusy: isSaving && isPublished, isPrimary: true, - onClick: onClickToggle, + onClick: this.createOnClick( onClickToggle ), }; const toggleChildren = isBeingScheduled ? __( 'Schedule…' ) : __( 'Publish…' ); @@ -110,12 +147,21 @@ export class PostPublishButton extends Component { const componentProps = isToggle ? toggleProps : buttonProps; const componentChildren = isToggle ? toggleChildren : buttonChildren; return ( - + <> + + + ); } } @@ -132,6 +178,8 @@ export default compose( [ isPostSavingLocked, getCurrentPost, getCurrentPostType, + getCurrentPostId, + hasNonPostEntityChanges, } = select( 'core/editor' ); return { isSaving: isSavingPost(), @@ -143,6 +191,8 @@ export default compose( [ isPublished: isCurrentPostPublished(), hasPublishAction: get( getCurrentPost(), [ '_links', 'wp:action-publish' ], false ), postType: getCurrentPostType(), + postId: getCurrentPostId(), + hasNonPostEntityChanges: hasNonPostEntityChanges(), }; } ), withDispatch( ( dispatch ) => { diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 9cabf7bff4708..d8777cb5b590a 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -134,6 +134,65 @@ export const isEditedPostDirty = createRegistrySelector( ( select ) => ( state ) return false; } ); +/** + * Returns true if there are unsaved edits for entities other than + * the editor's post, and false otherwise. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether there are edits or not. + */ +export const hasNonPostEntityChanges = createRegistrySelector( + ( select ) => ( state ) => { + const enableFullSiteEditing = getEditorSettings( state ) + .__experimentalEnableFullSiteEditing; + if ( ! enableFullSiteEditing ) { + return false; + } + + const entityRecordChangesByRecord = select( + 'core' + ).getEntityRecordChangesByRecord(); + const changedKinds = Object.keys( entityRecordChangesByRecord ); + if ( + changedKinds.length > 1 || + ( changedKinds.length === 1 && ! entityRecordChangesByRecord.postType ) + ) { + // Return true if there is more than one edited entity kind + // or the edited entity kind is not the editor's post's kind. + return true; + } else if ( ! entityRecordChangesByRecord.postType ) { + // Don't continue if there are no edited entity kinds. + return false; + } + + const { type, id } = getCurrentPost( state ); + const changedPostTypes = Object.keys( entityRecordChangesByRecord.postType ); + if ( + changedPostTypes.length > 1 || + ( changedPostTypes.length === 1 && + ! entityRecordChangesByRecord.postType[ type ] ) + ) { + // Return true if there is more than one edited post type + // or the edited entity's post type is not the editor's post's post type. + return true; + } + + const changedPosts = Object.keys( entityRecordChangesByRecord.postType[ type ] ); + if ( + changedPosts.length > 1 || + ( changedPosts.length === 1 && + ! entityRecordChangesByRecord.postType[ type ][ id ] ) + ) { + // Return true if there is more than one edited post + // or the edited post is not the editor's post. + return true; + } + + return false; + } +); + /** * Returns true if there are no unsaved values for the current edit session and * if the currently edited post is new (has never been saved before). From 7dbcae6f65b543bee68a832355364e3de1b7b502 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Thu, 31 Oct 2019 18:35:05 -0500 Subject: [PATCH 04/15] Post Publish Button: Change dashicon to dot pseudo element. --- .../src/components/post-publish-button/index.js | 17 +++++++++-------- .../components/post-publish-button/style.scss | 8 ++++++++ packages/editor/src/style.scss | 1 + 3 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 packages/editor/src/components/post-publish-button/style.scss diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index e92d4c9128f1d..77f8193ab2f5a 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -2,11 +2,12 @@ * External dependencies */ import { noop, get } from 'lodash'; +import classnames from 'classnames'; /** * WordPress dependencies */ -import { Button, Dashicon } from '@wordpress/components'; +import { Button } from '@wordpress/components'; import { Component, createRef } from '@wordpress/element'; import { withSelect, withDispatch } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -152,13 +153,13 @@ export class PostPublishButton extends Component { isOpen={ Boolean( entitiesSavedStatesCallback ) } onRequestClose={ this.closeEntitiesSavedStates } /> - diff --git a/packages/editor/src/components/post-publish-button/style.scss b/packages/editor/src/components/post-publish-button/style.scss new file mode 100644 index 0000000000000..2e61d067ad46f --- /dev/null +++ b/packages/editor/src/components/post-publish-button/style.scss @@ -0,0 +1,8 @@ +.editor-post-publish-button__has-changes-dot::before { + background: $white; + border-radius: 4px; + content: ""; + height: 8px; + margin: auto 5px auto -3px; + width: 8px; +} diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index 91255a878fd50..83a75edd9013a 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -10,6 +10,7 @@ @import "./components/post-last-revision/style.scss"; @import "./components/post-locked-modal/style.scss"; @import "./components/post-permalink/style.scss"; +@import "./components/post-publish-button/style.scss"; @import "./components/post-publish-panel/style.scss"; @import "./components/post-saved-state/style.scss"; @import "./components/post-taxonomies/style.scss"; From 4a7faf49e51c23c6b8120d1ce2eb58cd4a7de57e Mon Sep 17 00:00:00 2001 From: epiqueras Date: Fri, 1 Nov 2019 09:38:46 -0500 Subject: [PATCH 05/15] Post Publish Button: Also add trailing ellipsis when there are global changes. --- packages/editor/src/components/post-publish-button/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index 77f8193ab2f5a..a4a999e0af18d 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -146,7 +146,8 @@ export class PostPublishButton extends Component { const buttonChildren = ; const componentProps = isToggle ? toggleProps : buttonProps; - const componentChildren = isToggle ? toggleChildren : buttonChildren; + const componentChildren = + isToggle || hasNonPostEntityChanges ? toggleChildren : buttonChildren; return ( <> Date: Tue, 12 Nov 2019 12:04:17 -0800 Subject: [PATCH 06/15] Entities Saved States: Remove changed properties text. --- packages/editor/src/components/entities-saved-states/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index 308826283af25..365f65b4edd02 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -13,13 +13,12 @@ import { useState, useCallback } from '@wordpress/element'; const EntitiesSavedStatesCheckbox = ( { id, name, - changes: { rawRecord, edits }, + changes: { rawRecord }, checked, setCheckedById, } ) => ( setCheckedById( id, nextChecked ), [ id ] ) } /> From 4265794f14719dba1f0bbaca549e34b4f0a50d78 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Tue, 12 Nov 2019 12:11:42 -0800 Subject: [PATCH 07/15] Entities Saved States: Use the correct label. --- .../src/components/post-publish-button/index.js | 10 +++++++--- .../src/components/post-publish-button/label.js | 11 +++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index a4a999e0af18d..9cea5300dc8e0 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -143,11 +143,15 @@ export class PostPublishButton extends Component { }; const toggleChildren = isBeingScheduled ? __( 'Schedule…' ) : __( 'Publish…' ); - const buttonChildren = ; + const buttonChildren = ( + + ); const componentProps = isToggle ? toggleProps : buttonProps; - const componentChildren = - isToggle || hasNonPostEntityChanges ? toggleChildren : buttonChildren; + const componentChildren = isToggle ? toggleChildren : buttonChildren; return ( <> Date: Mon, 9 Dec 2019 15:18:32 -0800 Subject: [PATCH 08/15] Post Publish Button: Review suggestions. --- .../developers/data/data-core-editor.md | 13 ++++ .../developers/data/data-core.md | 6 +- packages/core-data/README.md | 6 +- packages/core-data/src/selectors.js | 6 +- packages/editor/package.json | 1 + .../components/entities-saved-states/index.js | 63 ++++++++++--------- .../components/post-publish-button/index.js | 12 ++-- .../components/post-publish-button/style.scss | 4 +- 8 files changed, 68 insertions(+), 43 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index 9cd27c390b29e..babe2b12d1c96 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -655,6 +655,19 @@ _Related_ - hasMultiSelection in core/block-editor store. +# **hasNonPostEntityChanges** + +Returns true if there are unsaved edits for entities other than +the editor's post, and false otherwise. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `boolean`: Whether there are edits or not. + # **hasSelectedBlock** _Related_ diff --git a/docs/designers-developers/developers/data/data-core.md b/docs/designers-developers/developers/data/data-core.md index 025203a27121d..c2d8c8a99b9aa 100644 --- a/docs/designers-developers/developers/data/data-core.md +++ b/docs/designers-developers/developers/data/data-core.md @@ -155,16 +155,18 @@ _Returns_ # **getEntityRecordChangesByRecord** -Returns a list of objects with each edited entity +Returns a map of objects with each edited entity record and its corresponding edits. +The map is keyed by entity `kind => name => id`. + _Parameters_ - _state_ `Object`: State tree. _Returns_ -- `Array`: The list of edited records with their edits. +- `Object`: The map of edited records with their edits. # **getEntityRecordEdits** diff --git a/packages/core-data/README.md b/packages/core-data/README.md index ddd3d96056b5a..403a27a937d72 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -368,16 +368,18 @@ _Returns_ # **getEntityRecordChangesByRecord** -Returns a list of objects with each edited entity +Returns a map of objects with each edited entity record and its corresponding edits. +The map is keyed by entity `kind => name => id`. + _Parameters_ - _state_ `Object`: State tree. _Returns_ -- `Array`: The list of edited records with their edits. +- `Object`: The map of edited records with their edits. # **getEntityRecordEdits** diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index a5451d4f61029..21e7d2a07df89 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -154,12 +154,14 @@ export function getEntityRecords( state, kind, name, query ) { } /** - * Returns a list of objects with each edited entity + * Returns a map of objects with each edited entity * record and its corresponding edits. * + * The map is keyed by entity `kind => name => id`. + * * @param {Object} state State tree. * - * @return {Array} The list of edited records with their edits. + * @return {Object} The map of edited records with their edits. */ export const getEntityRecordChangesByRecord = createSelector( ( state ) => { diff --git a/packages/editor/package.json b/packages/editor/package.json index b2055f150bb3e..fecbbc9542934 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -50,6 +50,7 @@ "@wordpress/viewport": "file:../viewport", "@wordpress/wordcount": "file:../wordcount", "classnames": "^2.2.5", + "equivalent-key-map": "^0.2.2", "lodash": "^4.17.15", "memize": "^1.0.5", "react-autosize-textarea": "^3.0.2", diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index 365f65b4edd02..69fd0997f7010 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -2,13 +2,15 @@ * External dependencies */ import { startCase } from 'lodash'; +import EquivalentKeyMap from 'equivalent-key-map'; /** * WordPress dependencies */ -import { Button, CheckboxControl, Modal } from '@wordpress/components'; +import { CheckboxControl, Modal, Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useState, useCallback } from '@wordpress/element'; +import { useState } from '@wordpress/element'; const EntitiesSavedStatesCheckbox = ( { id, @@ -18,9 +20,11 @@ const EntitiesSavedStatesCheckbox = ( { setCheckedById, } ) => ( setCheckedById( id, nextChecked ), [ id ] ) } + onChange={ ( nextChecked ) => setCheckedById( id, nextChecked ) } /> ); @@ -30,34 +34,31 @@ export default function EntitiesSavedStates( { isOpen, onRequestClose } ) { ); const { saveEditedEntityRecord } = useDispatch( 'core' ); - const [ checkedById, _setCheckedById ] = useState( {} ); - const setCheckedById = useCallback( - ( id, checked ) => - _setCheckedById( ( prevCheckedById ) => { - const nextCheckedById = { - ...prevCheckedById, - }; - if ( checked ) { - nextCheckedById[ id ] = true; - } else { - delete nextCheckedById[ id ]; - } - return nextCheckedById; - } ), - [] - ); - const saveCheckedEntities = useCallback( () => { - Object.keys( checkedById ).forEach( ( id ) => - saveEditedEntityRecord( ...id.split( ' | ' ).filter( ( s ) => s !== 'undefined' ) ) + const [ checkedById, _setCheckedById ] = useState( new EquivalentKeyMap() ); + const setCheckedById = ( id, checked ) => + _setCheckedById( ( prevCheckedById ) => { + const nextCheckedById = new EquivalentKeyMap( prevCheckedById ); + if ( checked ) { + nextCheckedById.set( id, true ); + } else { + nextCheckedById.delete( id ); + } + return nextCheckedById; + } ); + const saveCheckedEntities = () => { + checkedById.forEach( ( _checked, id ) => + saveEditedEntityRecord( + ...id.filter( ( s, i ) => i !== id.length - 1 || s !== 'undefined' ) + ) ); onRequestClose( checkedById ); - }, [ checkedById ] ); + }; return ( isOpen && ( { Object.keys( entityRecordChangesByRecord ).map( ( changedKind ) => Object.keys( entityRecordChangesByRecord[ changedKind ] ).map( @@ -65,10 +66,10 @@ export default function EntitiesSavedStates( { isOpen, onRequestClose } ) { Object.keys( entityRecordChangesByRecord[ changedKind ][ changedName ] ).map( ( changedKey ) => { - const id = `${ changedKind } | ${ changedName } | ${ changedKey }`; + const id = [ changedKind, changedName, changedKey ]; return ( ); @@ -85,11 +86,11 @@ export default function EntitiesSavedStates( { isOpen, onRequestClose } ) { ) } ) diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index 9cea5300dc8e0..c5d81403c5720 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -54,7 +54,7 @@ export class PostPublishButton extends Component { const { postType, postId } = this.props; const { entitiesSavedStatesCallback } = this.state; this.setState( { entitiesSavedStatesCallback: false }, () => { - if ( savedById[ `postType | ${ postType } | ${ postId }` ] ) { + if ( savedById.has( [ 'postType', postType, String( postId ) ] ) ) { entitiesSavedStatesCallback(); } } ); @@ -161,9 +161,13 @@ export class PostPublishButton extends Component { diff --git a/packages/editor/src/components/post-publish-button/style.scss b/packages/editor/src/components/post-publish-button/style.scss index 2e61d067ad46f..11aed2ab58e3e 100644 --- a/packages/editor/src/components/post-publish-button/style.scss +++ b/packages/editor/src/components/post-publish-button/style.scss @@ -1,5 +1,5 @@ -.editor-post-publish-button__has-changes-dot::before { - background: $white; +.editor-post-publish-button__button.has-changes-dot::before { + background: currentcolor; border-radius: 4px; content: ""; height: 8px; From 2b88ed5a75c52f17e7356d5461dd0d109f6d8804 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Mon, 9 Dec 2019 15:46:50 -0800 Subject: [PATCH 09/15] Package: Update lockfile. --- package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package-lock.json b/package-lock.json index fee8be7cc525c..e9d3e71fa5c1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7948,6 +7948,7 @@ "@wordpress/viewport": "file:packages/viewport", "@wordpress/wordcount": "file:packages/wordcount", "classnames": "^2.2.5", + "equivalent-key-map": "^0.2.2", "lodash": "^4.17.15", "memize": "^1.0.5", "react-autosize-textarea": "^3.0.2", From cf9e4fe42c4d6b875aafb04ee431b61632c59eed Mon Sep 17 00:00:00 2001 From: epiqueras Date: Wed, 11 Dec 2019 16:24:34 -0800 Subject: [PATCH 10/15] Core Data: Improve `getEntityRecordChangesByRecord` types and docs. --- docs/designers-developers/developers/data/data-core.md | 8 ++++---- packages/core-data/README.md | 8 ++++---- packages/core-data/src/selectors.js | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core.md b/docs/designers-developers/developers/data/data-core.md index c2d8c8a99b9aa..f5e06a7659e03 100644 --- a/docs/designers-developers/developers/data/data-core.md +++ b/docs/designers-developers/developers/data/data-core.md @@ -155,10 +155,10 @@ _Returns_ # **getEntityRecordChangesByRecord** -Returns a map of objects with each edited entity -record and its corresponding edits. +Returns a map of objects with each edited +raw entity record and its corresponding edits. -The map is keyed by entity `kind => name => id`. +The map is keyed by entity `kind => name => key => { rawRecord, edits }`. _Parameters_ @@ -166,7 +166,7 @@ _Parameters_ _Returns_ -- `Object`: The map of edited records with their edits. +- `null`: The map of edited records with their edits. # **getEntityRecordEdits** diff --git a/packages/core-data/README.md b/packages/core-data/README.md index 403a27a937d72..5b5cda9580b62 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -368,10 +368,10 @@ _Returns_ # **getEntityRecordChangesByRecord** -Returns a map of objects with each edited entity -record and its corresponding edits. +Returns a map of objects with each edited +raw entity record and its corresponding edits. -The map is keyed by entity `kind => name => id`. +The map is keyed by entity `kind => name => key => { rawRecord, edits }`. _Parameters_ @@ -379,7 +379,7 @@ _Parameters_ _Returns_ -- `Object`: The map of edited records with their edits. +- `null`: The map of edited records with their edits. # **getEntityRecordEdits** diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index 21e7d2a07df89..a4ea47ac8c745 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -154,14 +154,14 @@ export function getEntityRecords( state, kind, name, query ) { } /** - * Returns a map of objects with each edited entity - * record and its corresponding edits. + * Returns a map of objects with each edited + * raw entity record and its corresponding edits. * - * The map is keyed by entity `kind => name => id`. + * The map is keyed by entity `kind => name => key => { rawRecord, edits }`. * * @param {Object} state State tree. * - * @return {Object} The map of edited records with their edits. + * @return {{ [kind: string]: { [name: string]: { [key: string]: { rawRecord: Object, edits: Object } } } }} The map of edited records with their edits. */ export const getEntityRecordChangesByRecord = createSelector( ( state ) => { From 944d112f85bc15c542104967d2928925b137a333 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Wed, 11 Dec 2019 16:27:46 -0800 Subject: [PATCH 11/15] Entities Saved States: Add inline comments and fix post saving race condition. --- .../components/entities-saved-states/index.js | 22 ++++++++++++------- .../components/post-publish-button/index.js | 17 +++++++++++++- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index 69fd0997f7010..2f85c715cfc54 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -28,13 +28,17 @@ const EntitiesSavedStatesCheckbox = ( { /> ); -export default function EntitiesSavedStates( { isOpen, onRequestClose } ) { +export default function EntitiesSavedStates( { + isOpen, + onRequestClose, + ignoredForSave = new EquivalentKeyMap(), +} ) { const entityRecordChangesByRecord = useSelect( ( select ) => select( 'core' ).getEntityRecordChangesByRecord() ); const { saveEditedEntityRecord } = useDispatch( 'core' ); - const [ checkedById, _setCheckedById ] = useState( new EquivalentKeyMap() ); + const [ checkedById, _setCheckedById ] = useState( () => new EquivalentKeyMap() ); const setCheckedById = ( id, checked ) => _setCheckedById( ( prevCheckedById ) => { const nextCheckedById = new EquivalentKeyMap( prevCheckedById ); @@ -46,11 +50,13 @@ export default function EntitiesSavedStates( { isOpen, onRequestClose } ) { return nextCheckedById; } ); const saveCheckedEntities = () => { - checkedById.forEach( ( _checked, id ) => - saveEditedEntityRecord( - ...id.filter( ( s, i ) => i !== id.length - 1 || s !== 'undefined' ) - ) - ); + checkedById.forEach( ( _checked, id ) => { + if ( ! ignoredForSave.has( id ) ) { + saveEditedEntityRecord( + ...id.filter( ( s, i ) => i !== id.length - 1 || s !== 'undefined' ) + ); + } + } ); onRequestClose( checkedById ); }; return ( @@ -69,7 +75,7 @@ export default function EntitiesSavedStates( { isOpen, onRequestClose } ) { const id = [ changedKind, changedName, changedKey ]; return ( + new EquivalentKeyMap( [ [ [ 'postType', postType, String( postId ) ], true ] ] ), + { maxSize: 1 } + ); } componentDidMount() { if ( this.props.focusOnMount ) { @@ -41,9 +48,13 @@ export class PostPublishButton extends Component { return ( ...args ) => { const { hasNonPostEntityChanges } = this.props; if ( hasNonPostEntityChanges ) { - return this.setState( { + // The modal for multiple entity saving will open, + // hold the callback for saving/publishing the post + // so that we can call it if the post entity is checked. + this.setState( { entitiesSavedStatesCallback: () => callback( ...args ), } ); + return noop; } return callback( ...args ); @@ -55,6 +66,7 @@ export class PostPublishButton extends Component { const { entitiesSavedStatesCallback } = this.state; this.setState( { entitiesSavedStatesCallback: false }, () => { if ( savedById.has( [ 'postType', postType, String( postId ) ] ) ) { + // The post entity was checked, call the held callback from `createOnClick`. entitiesSavedStatesCallback(); } } ); @@ -79,6 +91,8 @@ export class PostPublishButton extends Component { onToggle, visibility, hasNonPostEntityChanges, + postType, + postId, } = this.props; const { entitiesSavedStatesCallback, @@ -157,6 +171,7 @@ export class PostPublishButton extends Component {