Skip to content

Commit

Permalink
Add indicator for metadata changes to Save Panel when reviewing modif…
Browse files Browse the repository at this point in the history
…ied entities (#61811)

* Add rough prototype showing meta in entities panel

* Simplify detection of metadata changes

Removed unnecessary code added in previous commit
and instead modified existing functions to add a flag on
existing dirtyEntityRecord structures to indicate when
metadata changes have been made.

* Remove obsolete code

* Add indicator for bindings to save entities panel

* Modify message to read 'Post Meta'

* Add store function to check if meta has changed

* Remove obsolete check

* Simplify logic to check if meta has changed

* Update tests

* Make hasMetaChanges selector private

* Suggestion: Move logic to `hasPostMetaChanges` selector

* Change test formatting

* Don't show save panel in pre-publish

* Get `hasPostMetaChanges` from the proper place

* Add end-to-end test

* Update class name

* Clarify naming

* Show Post Meta in relevant post

* Remove extra change

* Move test metadata test util

* Update comments

* Prevent save panel from appearing when just footnotes are modified

* Update package-lock.json

---------

Co-authored-by: Mario Santos <[email protected]>
  • Loading branch information
artemiomorales and SantosGuillamot authored May 31, 2024
1 parent db66bc9 commit 5576d89
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 22 deletions.
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"deepmerge": "^4.3.0",
"fast-deep-equal": "^3.1.3",
"is-plain-object": "^5.0.0",
"memize": "^2.1.0",
"react-autosize-textarea": "^7.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,78 @@
/**
* WordPress dependencies
*/
import { CheckboxControl, PanelRow } from '@wordpress/components';
import { Icon, CheckboxControl, Flex, PanelRow } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
import { connection } from '@wordpress/icons';

/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';
import { unlock } from '../../lock-unlock';

export default function EntityRecordItem( { record, checked, onChange } ) {
const { name, kind, title, key } = record;

// Handle templates that might use default descriptive titles.
const entityRecordTitle = useSelect(
const { entityRecordTitle, hasPostMetaChanges } = useSelect(
( select ) => {
if ( 'postType' !== kind || 'wp_template' !== name ) {
return title;
return {
entityRecordTitle: title,
hasPostMetaChanges: unlock(
select( editorStore )
).hasPostMetaChanges( name, key ),
};
}

const template = select( coreStore ).getEditedEntityRecord(
kind,
name,
key
);
return select( editorStore ).__experimentalGetTemplateInfo(
template
).title;
return {
entityRecordTitle:
select( editorStore ).__experimentalGetTemplateInfo(
template
).title,
hasPostMetaChanges: unlock(
select( editorStore )
).hasPostMetaChanges( name, key ),
};
},
[ name, kind, title, key ]
);

return (
<PanelRow>
<CheckboxControl
__nextHasNoMarginBottom
label={
decodeEntities( entityRecordTitle ) || __( 'Untitled' )
}
checked={ checked }
onChange={ onChange }
/>
</PanelRow>
<>
<PanelRow>
<CheckboxControl
__nextHasNoMarginBottom
label={
decodeEntities( entityRecordTitle ) || __( 'Untitled' )
}
checked={ checked }
onChange={ onChange }
/>
</PanelRow>
{ hasPostMetaChanges && (
<PanelRow>
<Flex className="entities-saved-states__post-meta">
<Icon
className="entities-saved-states__connections-icon"
icon={ connection }
size={ 24 }
/>
<span className="entities-saved-states__bindings-text">
{ __( 'Post Meta.' ) }
</span>
</Flex>
</PanelRow>
) }
</>
);
}
13 changes: 13 additions & 0 deletions packages/editor/src/components/entities-saved-states/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,16 @@
height: $header-height + $border-width;
}
}

.entities-saved-states__post-meta {
margin-left: $grid-unit-30;
align-items: center;
}

.entities-saved-states__connections-icon {
flex-grow: 0;
}

.entities-saved-states__bindings-text {
flex-grow: 1;
}
21 changes: 17 additions & 4 deletions packages/editor/src/components/post-publish-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { compose } from '@wordpress/compose';
*/
import PublishButtonLabel from './label';
import { store as editorStore } from '../../store';
import { unlock } from '../../lock-unlock';

const noop = () => {};

Expand Down Expand Up @@ -45,14 +46,24 @@ export class PostPublishButton extends Component {

createOnClick( callback ) {
return ( ...args ) => {
const { hasNonPostEntityChanges, setEntitiesSavedStatesCallback } =
this.props;
const {
hasNonPostEntityChanges,
hasPostMetaChanges,
setEntitiesSavedStatesCallback,
isPublished,
} = this.props;
// If a post with non-post entities is published, but the user
// elects to not save changes to the non-post entities, those
// entities will still be dirty when the Publish button is clicked.
// We also need to check that the `setEntitiesSavedStatesCallback`
// prop was passed. See https://github.com/WordPress/gutenberg/pull/37383
if ( hasNonPostEntityChanges && setEntitiesSavedStatesCallback ) {
//
// TODO: Explore how to manage `hasPostMetaChanges` and pre-publish workflow properly.
if (
( hasNonPostEntityChanges ||
( hasPostMetaChanges && isPublished ) ) &&
setEntitiesSavedStatesCallback
) {
// 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.
Expand Down Expand Up @@ -212,7 +223,8 @@ export default compose( [
isSavingNonPostEntityChanges,
getEditedPostAttribute,
getPostEdits,
} = select( editorStore );
hasPostMetaChanges,
} = unlock( select( editorStore ) );
return {
isSaving: isSavingPost(),
isAutoSaving: isAutosavingPost(),
Expand All @@ -229,6 +241,7 @@ export default compose( [
postStatus: getEditedPostAttribute( 'status' ),
postStatusHasChanged: getPostEdits()?.status,
hasNonPostEntityChanges: hasNonPostEntityChanges(),
hasPostMetaChanges: hasPostMetaChanges(),
isSavingNonPostEntityChanges: isSavingNonPostEntityChanges(),
};
} ),
Expand Down
12 changes: 10 additions & 2 deletions packages/editor/src/components/save-publish-panels/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import PostPublishPanel from '../post-publish-panel';
import PluginPrePublishPanel from '../plugin-pre-publish-panel';
import PluginPostPublishPanel from '../plugin-post-publish-panel';
import { store as editorStore } from '../../store';
import { unlock } from '../../lock-unlock';

const { Fill, Slot } = createSlotFill( 'ActionsPanel' );

Expand All @@ -27,12 +28,19 @@ export default function SavePublishPanels( {
} ) {
const { closePublishSidebar, togglePublishSidebar } =
useDispatch( editorStore );
const { publishSidebarOpened, hasNonPostEntityChanges } = useSelect(
const {
publishSidebarOpened,
hasNonPostEntityChanges,
hasPostMetaChanges,
} = useSelect(
( select ) => ( {
publishSidebarOpened:
select( editorStore ).isPublishSidebarOpened(),
hasNonPostEntityChanges:
select( editorStore ).hasNonPostEntityChanges(),
hasPostMetaChanges: unlock(
select( editorStore )
).hasPostMetaChanges(),
} ),
[]
);
Expand All @@ -54,7 +62,7 @@ export default function SavePublishPanels( {
PostPublishExtension={ PluginPostPublishPanel.Slot }
/>
);
} else if ( hasNonPostEntityChanges ) {
} else if ( hasNonPostEntityChanges || hasPostMetaChanges ) {
unmountableContent = (
<div className="editor-layout__toggle-entities-saved-states-panel">
<Button
Expand Down
45 changes: 45 additions & 0 deletions packages/editor/src/store/private-selectors.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* External dependencies
*/
import fastDeepEqual from 'fast-deep-equal';

/**
* WordPress dependencies
*/
Expand All @@ -17,6 +22,7 @@ import { store as coreStore } from '@wordpress/core-data';
*/
import {
getRenderingMode,
getCurrentPost,
__experimentalGetDefaultTemplatePartAreas,
} from './selectors';
import { TEMPLATE_PART_POST_TYPE } from './constants';
Expand Down Expand Up @@ -135,3 +141,42 @@ export const getCurrentTemplateTemplateParts = createRegistrySelector(
return getFilteredTemplatePartBlocks( blocks, templateParts );
}
);

/**
* Returns true if there are unsaved changes to the
* post's meta fields, and false otherwise.
*
* @param {Object} state Global application state.
* @param {string} postType The post type of the post.
* @param {number} postId The ID of the post.
*
* @return {boolean} Whether there are edits or not in the meta fields of the relevant post.
*/
export const hasPostMetaChanges = createRegistrySelector(
( select ) => ( state, postType, postId ) => {
const { type: currentPostType, id: currentPostId } =
getCurrentPost( state );
// If no postType or postId is passed, use the current post.
const edits = select( coreStore ).getEntityRecordNonTransientEdits(
'postType',
postType || currentPostType,
postId || currentPostId
);

if ( ! edits?.meta ) {
return false;
}

// Compare if anything apart from `footnotes` has changed.
const originalPostMeta = select( coreStore ).getEntityRecord(
'postType',
postType || currentPostType,
postId || currentPostId
)?.meta;

return ! fastDeepEqual(
{ ...originalPostMeta, footnotes: undefined },
{ ...edits.meta, footnotes: undefined }
);
}
);
94 changes: 94 additions & 0 deletions test/e2e/specs/editor/various/publish-panel.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,98 @@ test.describe( 'Post publish panel', () => {
} )
).toBeFocused();
} );

test( 'should show panel and indicator when metadata has been modified', async ( {
admin,
editor,
page,
} ) => {
await admin.createNewPost( {
title: 'Test metadata changes with save panel',
} );
await editor.insertBlock( {
name: 'core/paragraph',
attributes: {
content: 'paragraph default content',
metadata: {
bindings: {
content: {
source: 'core/post-meta',
args: { key: 'text_custom_field' },
},
},
},
},
} );
const postId = await editor.publishPost();
const metadataUtils = new MetadataUtils( page );
await metadataUtils.modifyPostMetadata(
'post',
postId,
'text_custom_field',
'test value'
);
const editorTopBar = page.getByRole( 'region', {
name: 'Editor top bar',
} );

const saveButton = editorTopBar.getByRole( 'button', {
name: 'Save',
exact: true,
} );

await expect( saveButton ).toBeVisible();

await saveButton.click();

const publishPanel = page.getByRole( 'region', {
name: 'Editor publish',
} );

await expect( publishPanel ).toBeVisible();

const postMetaPanel = publishPanel.locator(
'.entities-saved-states__post-meta'
);

await expect( postMetaPanel ).toBeVisible();
} );
} );

/**
* Utilities for working with metadata.
*
* @param postType
* @param postId
* @param metaKey
* @param metaValue
*/
class MetadataUtils {
constructor( page ) {
this.page = page;
}

async modifyPostMetadata( postType, postId, metaKey, metaValue ) {
const parameters = {
postType,
postId,
metaKey,
metaValue,
};

await this.page.evaluate( ( _parameters ) => {
window.wp.data
.dispatch( 'core' )
.editEntityRecord(
'postType',
_parameters.postType,
_parameters.postId,
{
meta: {
[ _parameters.metaKey ]: _parameters.metaValue,
},
}
);
}, parameters );
}
}

0 comments on commit 5576d89

Please sign in to comment.