Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block Bindings: Output granular post meta changes in a list inside save panel #62982

Closed
wants to merge 9 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ export default function EntityRecordItem( { record, checked, onChange } ) {
const { name, kind, title, key } = record;

// Handle templates that might use default descriptive titles.
const { entityRecordTitle, hasPostMetaChanges } = useSelect(
const { entityRecordTitle, postMetaChanges } = useSelect(
( select ) => {
const _postMetaChanges = unlock(
select( editorStore )
).getPostMetaChanges();

if ( 'postType' !== kind || 'wp_template' !== name ) {
return {
entityRecordTitle: title,
hasPostMetaChanges: unlock(
select( editorStore )
).hasPostMetaChanges( name, key ),
postMetaChanges: _postMetaChanges,
};
}

Expand All @@ -38,9 +40,7 @@ export default function EntityRecordItem( { record, checked, onChange } ) {
select( editorStore ).__experimentalGetTemplateInfo(
template
).title,
hasPostMetaChanges: unlock(
select( editorStore )
).hasPostMetaChanges( name, key ),
postMetaChanges: _postMetaChanges,
};
},
[ name, kind, title, key ]
Expand All @@ -58,10 +58,15 @@ export default function EntityRecordItem( { record, checked, onChange } ) {
onChange={ onChange }
/>
</PanelRow>
{ hasPostMetaChanges && (
<ul className="entities-saved-states__changes">
<li>{ __( 'Post Meta.' ) }</li>
</ul>
{ postMetaChanges.length > 0 && (
<div className="entities-saved-states__changes">
<p>Post Meta</p>
<ul>
{ postMetaChanges.map( ( postMeta ) => (
<li key={ postMeta }>{ postMeta }</li>
) ) }
</ul>
</div>
) }
</>
);
Expand Down
10 changes: 7 additions & 3 deletions packages/editor/src/components/entities-saved-states/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@
color: $gray-700;
font-size: $helptext-font-size;
margin: $grid-unit-10 $grid-unit-20 0 $grid-unit-20;
list-style: disc;

li {
margin-bottom: $grid-unit-05;
ul {
list-style: disc;
margin: $grid-unit-10 $grid-unit-30 0 $grid-unit-30;

li {
margin-bottom: $grid-unit-05;
}
}
}
12 changes: 7 additions & 5 deletions packages/editor/src/components/post-publish-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class PostPublishButton extends Component {
return ( ...args ) => {
const {
hasNonPostEntityChanges,
hasPostMetaChanges,
postMetaChanges,
setEntitiesSavedStatesCallback,
isPublished,
} = this.props;
Expand All @@ -58,10 +58,12 @@ export class PostPublishButton extends Component {
// We also need to check that the `setEntitiesSavedStatesCallback`
// prop was passed. See https://github.com/WordPress/gutenberg/pull/37383
//
// TODO: Explore how to manage `hasPostMetaChanges` and pre-publish workflow properly.
// TODO: Explore how to manage `getPostMetaChanges` and pre-publish workflow properly.
if (
( hasNonPostEntityChanges ||
( hasPostMetaChanges && isPublished ) ) &&
( postMetaChanges &&
postMetaChanges.length > 0 &&
Comment on lines +64 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is not enough to use postMetaChanges?.length here?

isPublished ) ) &&
setEntitiesSavedStatesCallback
) {
// The modal for multiple entity saving will open,
Expand Down Expand Up @@ -226,7 +228,7 @@ export default compose( [
isSavingNonPostEntityChanges,
getEditedPostAttribute,
getPostEdits,
hasPostMetaChanges,
getPostMetaChanges,
} = unlock( select( editorStore ) );
return {
isSaving: isSavingPost(),
Expand All @@ -244,7 +246,7 @@ export default compose( [
postStatus: getEditedPostAttribute( 'status' ),
postStatusHasChanged: getPostEdits()?.status,
hasNonPostEntityChanges: hasNonPostEntityChanges(),
hasPostMetaChanges: hasPostMetaChanges(),
postMetaChanges: getPostMetaChanges(),
isSavingNonPostEntityChanges: isSavingNonPostEntityChanges(),
};
} ),
Expand Down
3 changes: 1 addition & 2 deletions packages/editor/src/components/save-publish-panels/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function SavePublishPanels( {
} = select( editorStore );
const _hasOtherEntitiesChanges =
hasNonPostEntityChanges() ||
unlock( select( editorStore ) ).hasPostMetaChanges();
unlock( select( editorStore ) ).getPostMetaChanges().length > 0;
return {
publishSidebarOpened: isPublishSidebarOpened(),
isPublishable:
Expand All @@ -52,7 +52,6 @@ export default function SavePublishPanels( {
hasOtherEntitiesChanges: _hasOtherEntitiesChanges,
};
}, [] );

const openEntitiesSavedStates = useCallback(
() => setEntitiesSavedStatesCallback( true ),
[]
Expand Down
71 changes: 53 additions & 18 deletions packages/editor/src/store/private-selectors.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* External dependencies
*/
import fastDeepEqual from 'fast-deep-equal';

/**
* WordPress dependencies
*/
Expand Down Expand Up @@ -34,6 +29,49 @@ const EMPTY_INSERTION_POINT = {
insertionIndex: undefined,
filterValue: undefined,
};
const isObject = ( obj ) => obj !== null && typeof obj === 'object';

/**
* A deep comparison of two objects, optimized for comparing metadata changes.
* @param {Object} changedObject The changed object to compare.
* @param {Object} originalObject The original object to compare against.
* @param {string} parentPath A key/value pair object of block names and their rendered titles.
* @return {string[]} An array of paths whose values have changed.
*/
function deepCompare( changedObject, originalObject, parentPath = '' ) {
// We have two non-object values to compare.
if ( ! isObject( changedObject ) && ! isObject( originalObject ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be the place to have such a util. It has nothing to do with the store.

/*
* Only return a path if the value has changed.
*/
return changedObject !== originalObject
? parentPath.split( '.' ).join( ' > ' )
: undefined;
}

// Enable comparison when an object doesn't have a corresponding property to compare.
changedObject = isObject( changedObject ) ? changedObject : {};
originalObject = isObject( originalObject ) ? originalObject : {};

const allKeys = new Set( [
...Object.keys( changedObject ),
...Object.keys( originalObject ),
] );

let diffs = [];
for ( const key of allKeys ) {
const path = parentPath ? parentPath + '.' + key : key;
const changedPath = deepCompare(
changedObject[ key ],
originalObject[ key ],
path
);
if ( changedPath ) {
diffs = diffs.concat( changedPath );
}
}
return diffs;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this deep equality comparison look? The one mentioned in this comment works well, so I essentially copied it, just changing the appearance of the diff slightly. I'm not sure in what other scenarios metadata might change or how, and if this comparison would suit those scenarios.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, since we're only using the deep comparison in this file, I decided to just keep it local rather than creating a utils file for now. How does that sound?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing that changes is the way we show nested properties, right? (This line)

For me, both cases are really similar so maybe it makes sense to keep consistency between them?

Additionally, I think it's fine to keep it local for now, but I would open a follow-up pull request to move deepCompare to a util to foster the discussion. It seems that at least global styles and post meta could benefit from this. I assume it should be "format agnostic" and it can be formatted after the deepCompare return for each specific use case.

/**
* Get the insertion point for the inserter.
Expand Down Expand Up @@ -153,31 +191,28 @@ export const getCurrentTemplateTemplateParts = createRegistrySelector(
*
* @return {boolean} Whether there are edits or not in the meta fields of the relevant post.
*/
export const hasPostMetaChanges = createRegistrySelector(
export const getPostMetaChanges = 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(
const original = select( coreStore ).getEntityRecord(
'postType',
postType || currentPostType,
postId || currentPostId
);

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

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

if ( ! original?.meta || ! edits?.meta ) {
return [];
}

return ! fastDeepEqual(
{ ...originalPostMeta, footnotes: undefined },
{ ...edits.meta, footnotes: undefined }
return deepCompare( edits.meta, original.meta ).filter(
( item ) => item !== 'footnotes'
);
}
);
Expand Down
28 changes: 27 additions & 1 deletion packages/is-shallow-equal/src/objects.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
/**
* Internal dependencies
*/
import isShallowEqualArrays from './arrays';

/**
* Returns true if the two values are equal, or false otherwise.
* Includes a check for shallow array equality.
*
* @param {any} a
* @param {any} b
*
* @return {boolean} Whether the two values are equal.
*/
function isEqual( a, b ) {
if ( Array.isArray( a ) && Array.isArray( b ) ) {
return isShallowEqualArrays( a, b );
}

if ( a === b ) {
return true;
}

return false;
}

/**
* Returns true if the two objects are shallow equal, or false otherwise.
*
Expand Down Expand Up @@ -31,7 +57,7 @@ export default function isShallowEqualObjects( a, b ) {
//
// Example: isShallowEqualObjects( { a: undefined }, { b: 5 } )
( aValue === undefined && ! b.hasOwnProperty( key ) ) ||
aValue !== b[ key ]
! isEqual( aValue, b[ key ] )
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In using getPostMetaChanges(), I was receiving a warning: The 'use select' hook returns different values when called with the same state and parameters. This can lead to unnecessary re-renders.

Here's a screenshot of the values being compared along with the warning:

Screenshot 2024-07-04 at 5 46 45 AM

This check is called in the following lines:

if ( ! isShallowEqual( mapResult, secondMapResult ) ) {
// eslint-disable-next-line no-console
console.warn(
`The 'useSelect' hook returns different values when called with the same state and parameters. This can lead to unnecessary rerenders.`
);
didWarnUnstableReference = true;

Essentially, a new array is returned each time from getPostMetaChanges(), causing the warning even though the array values are equal. What's the best way to handle this?

I'm not sure the equality check was written with this scenario in mind, so I revised it to remove the warning and aid with discussion. Would appreciate any pointers or thoughts 🙏

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel fully comfortable changing the conditional of the warning. I would like to understand better why the existing implementation is triggering that warning. I've been doing some checks with global styles, which is a really similar use case to what we are trying to achieve and this is what I found:

In trunk

In the Site Editor:

  • when I load a specific post, it doesn't trigger any warning.
  • when I modify a global style and click save, it doesn't trigger any warning.

In this branch without the changes in is-shallow-equal package

  • when I load any post, even if it doesn't have any block connected, it triggers the warning.
  • when I modify a global style and click save, it DOES trigger the warning.
  • when I modify post meta and click save, it DOES trigger the warning.

That makes me think that something might be wrong with this implementation. That's why I would like to understand better the root cause. Why is it working in trunk for global styles, which is a really similar use case, and not in this case?

Copy link
Contributor

@ntsekouras ntsekouras Jul 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't change and the warning tries to protect us from unnecessary rerenders. Probably what we need here is have a selector that gets the data(returns stable reference) and do the comparison in the consumer components.

We could keep the hasPostMetaChanges selector probably and only calculate the diff if it has changes.

) {
return false;
}
Expand Down
Loading