Skip to content

Commit

Permalink
PostPublishButton: Disable if saving non-post entities (#33140)
Browse files Browse the repository at this point in the history
We're currently not disabling the Publish/Save/Update button while saving non-post entity changes (such as changes made to a reusable block, or to general site settings through a special block such as the Site Title block).

To fix this, this commit introduces a selector called `isSavingNonPostEntityChanges`, and a few helpers.
  • Loading branch information
ockham authored Jul 8, 2021
1 parent 14624ba commit a6908dc
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 13 deletions.
50 changes: 50 additions & 0 deletions packages/core-data/src/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,56 @@ export const __experimentalGetDirtyEntityRecords = createSelector(
( state ) => [ state.entities.data ]
);

/**
* Returns the list of entities currently being saved.
*
* @param {Object} state State tree.
*
* @return {[{ title: string, key: string, name: string, kind: string }]} The list of records being saved.
*/
export const __experimentalGetEntitiesBeingSaved = createSelector(
( state ) => {
const {
entities: { data },
} = state;
const recordsBeingSaved = [];
Object.keys( data ).forEach( ( kind ) => {
Object.keys( data[ kind ] ).forEach( ( name ) => {
const primaryKeys = Object.keys(
data[ kind ][ name ].saving
).filter( ( primaryKey ) =>
isSavingEntityRecord( state, kind, name, primaryKey )
);

if ( primaryKeys.length ) {
const entity = getEntity( state, kind, name );
primaryKeys.forEach( ( primaryKey ) => {
const entityRecord = getEditedEntityRecord(
state,
kind,
name,
primaryKey
);
recordsBeingSaved.push( {
// We avoid using primaryKey because it's transformed into a string
// when it's used as an object key.
key:
entityRecord[
entity.key || DEFAULT_ENTITY_KEY
],
title: entity?.getTitle?.( entityRecord ) || '',
name,
kind,
} );
} );
}
} );
} );
return recordsBeingSaved;
},
( state ) => [ state.entities.data ]
);

/**
* Returns the specified entity record's edits.
*
Expand Down
49 changes: 49 additions & 0 deletions packages/core-data/src/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
hasEntityRecords,
getEntityRecords,
__experimentalGetDirtyEntityRecords,
__experimentalGetEntitiesBeingSaved,
getEntityRecordNonTransientEdits,
getEmbedPreview,
isPreviewEmbedFallback,
Expand Down Expand Up @@ -396,6 +397,54 @@ describe( '__experimentalGetDirtyEntityRecords', () => {
} );
} );

describe( '__experimentalGetEntitiesBeingSaved', () => {
it( "should return a map of objects with each raw entity record that's being saved", () => {
const state = deepFreeze( {
entities: {
config: [
{
kind: 'someKind',
name: 'someName',
transientEdits: { someTransientEditProperty: true },
},
],
data: {
someKind: {
someName: {
queriedData: {
items: {
default: {
someKey: {
someProperty: 'somePersistedValue',
someRawProperty: {
raw: 'somePersistedRawValue',
},
id: 'someKey',
},
},
},
itemIsComplete: {
default: {
someKey: true,
},
},
},
saving: {
someKey: {
pending: true,
},
},
},
},
},
},
} );
expect( __experimentalGetEntitiesBeingSaved( state ) ).toEqual( [
{ kind: 'someKind', name: 'someName', key: 'someKey', title: '' },
] );
} );
} );

describe( 'getEntityRecordNonTransientEdits', () => {
it( 'should return an empty object when the entity does not have a loaded config.', () => {
const state = deepFreeze( {
Expand Down
29 changes: 17 additions & 12 deletions packages/editor/src/components/post-publish-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,21 +103,24 @@ export class PostPublishButton extends Component {
onToggle,
visibility,
hasNonPostEntityChanges,
isSavingNonPostEntityChanges,
} = this.props;

const isButtonDisabled =
isSaving ||
forceIsSaving ||
! isSaveable ||
isPostSavingLocked ||
( ! isPublishable && ! forceIsDirty );
( isSaving ||
forceIsSaving ||
! isSaveable ||
isPostSavingLocked ||
( ! isPublishable && ! forceIsDirty ) ) &&
( ! hasNonPostEntityChanges || isSavingNonPostEntityChanges );

const isToggleDisabled =
isPublished ||
isSaving ||
forceIsSaving ||
! isSaveable ||
( ! isPublishable && ! forceIsDirty );
( isPublished ||
isSaving ||
forceIsSaving ||
! isSaveable ||
( ! isPublishable && ! forceIsDirty ) ) &&
( ! hasNonPostEntityChanges || isSavingNonPostEntityChanges );

let publishStatus;
if ( ! hasPublishAction ) {
Expand Down Expand Up @@ -147,15 +150,15 @@ export class PostPublishButton extends Component {
};

const buttonProps = {
'aria-disabled': isButtonDisabled && ! hasNonPostEntityChanges,
'aria-disabled': isButtonDisabled,
className: 'editor-post-publish-button',
isBusy: ! isAutoSaving && isSaving && isPublished,
variant: 'primary',
onClick: this.createOnClick( onClickButton ),
};

const toggleProps = {
'aria-disabled': isToggleDisabled && ! hasNonPostEntityChanges,
'aria-disabled': isToggleDisabled,
'aria-expanded': isOpen,
className: 'editor-post-publish-panel__toggle',
isBusy: isSaving && isPublished,
Expand Down Expand Up @@ -210,6 +213,7 @@ export default compose( [
getCurrentPostType,
getCurrentPostId,
hasNonPostEntityChanges,
isSavingNonPostEntityChanges,
} = select( editorStore );
const _isAutoSaving = isAutosavingPost();
return {
Expand All @@ -229,6 +233,7 @@ export default compose( [
postType: getCurrentPostType(),
postId: getCurrentPostId(),
hasNonPostEntityChanges: hasNonPostEntityChanges(),
isSavingNonPostEntityChanges: isSavingNonPostEntityChanges(),
};
} ),
withDispatch( ( dispatch ) => {
Expand Down
9 changes: 8 additions & 1 deletion packages/editor/src/components/post-publish-panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export class PostPublishPanel extends Component {
isPublishSidebarEnabled,
isScheduled,
isSaving,
isSavingNonPostEntityChanges,
onClose,
onTogglePublishSidebar,
PostPublishExtension,
Expand Down Expand Up @@ -97,7 +98,11 @@ export class PostPublishPanel extends Component {
/>
</div>
<div className="editor-post-publish-panel__header-cancel-button">
<Button onClick={ onClose } variant="secondary">
<Button
disabled={ isSavingNonPostEntityChanges }
onClick={ onClose }
variant="secondary"
>
{ __( 'Cancel' ) }
</Button>
</div>
Expand Down Expand Up @@ -140,6 +145,7 @@ export default compose( [
isEditedPostBeingScheduled,
isEditedPostDirty,
isSavingPost,
isSavingNonPostEntityChanges,
} = select( editorStore );
const { isPublishSidebarEnabled } = select( editorStore );
const postType = getPostType( getEditedPostAttribute( 'type' ) );
Expand All @@ -156,6 +162,7 @@ export default compose( [
isPublished: isCurrentPostPublished(),
isPublishSidebarEnabled: isPublishSidebarEnabled(),
isSaving: isSavingPost(),
isSavingNonPostEntityChanges: isSavingNonPostEntityChanges(),
isScheduled: isCurrentPostScheduled(),
};
} ),
Expand Down
23 changes: 23 additions & 0 deletions packages/editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,29 @@ export const isSavingPost = createRegistrySelector( ( select ) => ( state ) => {
);
} );

/**
* Returns true if non-post entities are currently being saved, or false otherwise.
*
* @param {Object} state Global application state.
*
* @return {boolean} Whether non-post entities are being saved.
*/
export const isSavingNonPostEntityChanges = createRegistrySelector(
( select ) => ( state ) => {
const entitiesBeingSaved = select(
coreStore
).__experimentalGetEntitiesBeingSaved();
const { type, id } = getCurrentPost( state );
return some(
entitiesBeingSaved,
( entityRecord ) =>
entityRecord.kind !== 'postType' ||
entityRecord.name !== type ||
entityRecord.key !== id
);
}
);

/**
* Returns true if a previous post save was attempted successfully, or false
* otherwise.
Expand Down
55 changes: 55 additions & 0 deletions packages/editor/src/store/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ selectorNames.forEach( ( name ) => {
);
},

__experimentalGetEntitiesBeingSaved() {
return (
state.__experimentalGetEntitiesBeingSaved &&
state.__experimentalGetEntitiesBeingSaved()
);
},

getEntityRecordEdits() {
const present = state.editor && state.editor.present;
let edits = present && present.edits;
Expand Down Expand Up @@ -170,6 +177,7 @@ const {
getCurrentPostAttribute,
getEditedPostAttribute,
isSavingPost,
isSavingNonPostEntityChanges,
didPostSaveRequestSucceed,
didPostSaveRequestFail,
getSuggestedPostFormat,
Expand Down Expand Up @@ -2085,6 +2093,53 @@ describe( 'selectors', () => {
} );
} );

describe( 'isSavingNonPostEntityChanges', () => {
it( 'should return true if changes to an arbitrary entity are being saved', () => {
const state = {
currentPost: { id: 1, type: 'post' },
__experimentalGetEntitiesBeingSaved() {
return [
{ kind: 'someKind', name: 'someName', key: 'someKey' },
];
},
};
expect( isSavingNonPostEntityChanges( state ) ).toBe( true );
} );
it( 'should return false if the only changes being saved are for the current post', () => {
const state = {
currentPost: { id: 1, type: 'post' },
__experimentalGetEntitiesBeingSaved() {
return [ { kind: 'postType', name: 'post', key: 1 } ];
},
};
expect( isSavingNonPostEntityChanges( state ) ).toBe( false );
} );
it( 'should return true if changes to multiple posts are being saved', () => {
const state = {
currentPost: { id: 1, type: 'post' },
__experimentalGetEntitiesBeingSaved() {
return [
{ kind: 'postType', name: 'post', key: 1 },
{ kind: 'postType', name: 'post', key: 2 },
];
},
};
expect( isSavingNonPostEntityChanges( state ) ).toBe( true );
} );
it( 'should return true if changes to multiple posts of different post types are being saved', () => {
const state = {
currentPost: { id: 1, type: 'post' },
__experimentalGetEntitiesBeingSaved() {
return [
{ kind: 'postType', name: 'post', key: 1 },
{ kind: 'postType', name: 'wp_template', key: 1 },
];
},
};
expect( isSavingNonPostEntityChanges( state ) ).toBe( true );
} );
} );

describe( 'didPostSaveRequestSucceed', () => {
it( 'should return true if the post save request is successful', () => {
const state = {
Expand Down

0 comments on commit a6908dc

Please sign in to comment.