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

Simplify meta attribute sources and attempt to prepare a generalization of custom sources #16075

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,18 @@ _Returns_

- `Array<Editor.InserterItem>`: Items that appear in inserter.

<a name="getLastBlockAttributesChange" href="#getLastBlockAttributesChange">#</a> **getLastBlockAttributesChange**

Returns an object describing the last block attributes changes

_Parameters_

- _state_ `Object`: Block editor state.

_Returns_

- `Object`: the clientId and the block attributes changes

<a name="getLastMultiSelectedBlockClientId" href="#getLastMultiSelectedBlockClientId">#</a> **getLastMultiSelectedBlockClientId**

Returns the client ID of the last block in the multi-selection set, or null
Expand Down
8 changes: 0 additions & 8 deletions docs/designers-developers/developers/data/data-core-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -1047,10 +1047,6 @@ _Parameters_

- _edits_ `Object`: Post attributes to edit.

_Returns_

- `Object`: Action object.

<a name="enablePublishSidebar" href="#enablePublishSidebar">#</a> **enablePublishSidebar**

Returns an action object used in signalling that the user has enabled the
Expand Down Expand Up @@ -1211,10 +1207,6 @@ _Parameters_
- _blocks_ `Array`: Block Array.
- _options_ `?Object`: Optional options.

_Returns_

- `Object`: Action object

<a name="resetPost" href="#resetPost">#</a> **resetPost**

Returns an action object used in signalling that the latest version of the
Expand Down
40 changes: 3 additions & 37 deletions packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import classnames from 'classnames';
import { get, reduce, size, first, last } from 'lodash';
import { first, last } from 'lodash';

/**
* WordPress dependencies
Expand Down Expand Up @@ -649,42 +649,8 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => {

return {
setAttributes( newAttributes ) {
const { name, clientId } = ownProps;
const type = getBlockType( name );

function isMetaAttribute( key ) {
return get( type, [ 'attributes', key, 'source' ] ) === 'meta';
}

// Partition new attributes to delegate update behavior by source.
//
// TODO: A consolidated approach to external attributes sourcing
// should be devised to avoid specific handling for meta, enable
// additional attributes sources.
//
// See: https://github.com/WordPress/gutenberg/issues/2759
const {
blockAttributes,
metaAttributes,
} = reduce( newAttributes, ( result, value, key ) => {
if ( isMetaAttribute( key ) ) {
result.metaAttributes[ type.attributes[ key ].meta ] = value;
} else {
result.blockAttributes[ key ] = value;
}

return result;
}, { blockAttributes: {}, metaAttributes: {} } );

if ( size( blockAttributes ) ) {
updateBlockAttributes( clientId, blockAttributes );
}

if ( size( metaAttributes ) ) {
const { getSettings } = select( 'core/block-editor' );
const onChangeMeta = getSettings().__experimentalMetaSource.onChange;
onChangeMeta( metaAttributes );
}
const { clientId } = ownProps;
updateBlockAttributes( clientId, newAttributes );
},
onSelect( clientId = ownProps.clientId, initialPosition ) {
selectBlock( clientId, initialPosition );
Expand Down
27 changes: 16 additions & 11 deletions packages/block-editor/src/components/provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { compose } from '@wordpress/compose';
import withRegistryProvider from './with-registry-provider';

class BlockEditorProvider extends Component {
constructor() {
super( ...arguments );
this.lastPersistedBlocks = [];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not satisfied with this trick (this value being an array) but it's the only way I found right now to fix all the use-cases which are:

  • We need to avoid calling resetBlocks if the new "value" prop received is the same as the one already in the block-editor store (to avoid resetting the "undo" levels). (undo e2e test failing)
  • Sometimes calling some changes like inserting a block with a template (columns block for instance) result in two onChange calls being performed synchronously which will eventually trigger two componentDidUpdate calls but eventually the first componentDidUpdate will call resetBlocks if we only compare against the latest blocks value in the block-editor state causing the editor to reset to the previous blocks value (and not the last one). (columns e2e test failing)

The fix here is a bit fragile though, it basically says: don't call "resetBlocks" unless the new prop received is not one of the last persisted blocks value passed to onChange.

}
componentDidMount() {
this.props.updateSettings( this.props.settings );
this.props.resetBlocks( this.props.value );
Expand All @@ -35,11 +39,15 @@ class BlockEditorProvider extends Component {
this.attachChangeObserver( registry );
}

if ( this.isSyncingOutcomingValue ) {
this.isSyncingOutcomingValue = false;
} else if ( value !== prevProps.value ) {
if ( value !== prevProps.value && this.lastPersistedBlocks.indexOf( value ) === -1 ) {
this.isSyncingIncomingValue = true;
resetBlocks( value );
this.lastPersistedBlocks = [];
}

// Reset the last persisted values once the last change is performed
if ( value === this.lastPersistedBlocks[ 0 ] ) {
this.lastPersistedBlocks = [];
}
}

Expand Down Expand Up @@ -69,6 +77,7 @@ class BlockEditorProvider extends Component {
const {
getBlocks,
isLastBlockChangePersistent,
getLastBlockAttributesChange,
__unstableIsLastBlockChangeIgnored,
} = registry.select( 'core/block-editor' );

Expand Down Expand Up @@ -99,19 +108,15 @@ class BlockEditorProvider extends Component {
// This happens when a previous input is explicitely marked as persistent.
( newIsPersistent && ! isPersistent )
) {
// When knowing the blocks value is changing, assign instance
// value to skip reset in subsequent `componentDidUpdate`.
if ( newBlocks !== blocks ) {
this.isSyncingOutcomingValue = true;
}

blocks = newBlocks;
isPersistent = newIsPersistent;
const lastChanges = getLastBlockAttributesChange();
this.lastPersistedBlocks.push( blocks );

if ( isPersistent ) {
onChange( blocks );
onChange( blocks, lastChanges );
} else {
onInput( blocks );
onInput( blocks, lastChanges );
}
}
} );
Expand Down
27 changes: 27 additions & 0 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -983,8 +983,35 @@ export const blockListSettings = ( state = {}, action ) => {
return state;
};

export function lastBlockAttributesChanges( state, action ) {
switch ( action.type ) {
case 'UPDATE_BLOCK':
if ( ! action.updates.attributes ) {
return null;
}
return {
[ action.clientId ]: action.updates.attributes,
};

case 'UPDATE_BLOCK_ATTRIBUTES':
return {
[ action.clientId ]: action.attributes,
};

case 'RESET_BLOCKS':
Copy link
Member

Choose a reason for hiding this comment

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

I'm wondering if we need to account for these actions. It seems that interpreting something like a block replacement to trigger meta updates might not always be expected (though conversely, maybe it is).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, I don't know. I agree that it's less important in the way we use "RESET_BLOCKS" in Core but there's nothing stopping third-party plugins from using the actions right.

case 'INSERT_BLOCKS':
case 'RECEIVE_BLOCKS':
case 'REPLACE_BLOCKS':
return getFlattenedBlockAttributes( action.blocks );
}

return null;
Copy link
Member

Choose a reason for hiding this comment

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

Did you mean to return null here, as it would cause any other action than the ones above to reset state? I guess if the idea is that it's only effective for the single action which updates the block?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, I didn't give it too much thoughts but yeah the idea is that we don't care about the value after. I don't think it would harm if we return "state" but I wonder if we'd need to "reset" the value in some actions I might have missed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tbh, this "last changes" thing was consider very experimental. If we follow the approach of this PR, I wouldn't mind if we do a full sync on update. Maybe the performance impact is small? we should measure.

}

export default combineReducers( {
blocks,
// This is ideally embedded in blocks, the issues is that it alterns the isPersistent behavior
lastBlockAttributesChanges,
isTyping,
isCaretWithinFormattedText,
blockSelection,
Expand Down
84 changes: 21 additions & 63 deletions packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,6 @@ const MILLISECONDS_PER_WEEK = 7 * 24 * 3600 * 1000;
*/
const EMPTY_ARRAY = [];

/**
* Shared reference to an empty object for cases where it is important to avoid
* returning a new object reference on every invocation.
*
* @type {Object}
*/
const EMPTY_OBJECT = {};

/**
* Returns a new reference when the inner blocks of a given block client ID
* change. This is used exclusively as a memoized selector dependant, relying
Expand Down Expand Up @@ -130,42 +122,14 @@ export function isBlockValid( state, clientId ) {
*
* @return {Object?} Block attributes.
*/
export const getBlockAttributes = createSelector(
( state, clientId ) => {
const block = state.blocks.byClientId[ clientId ];
if ( ! block ) {
return null;
}

let attributes = state.blocks.attributes[ clientId ];

// Inject custom source attribute values.
//
// TODO: Create generic external sourcing pattern, not explicitly
// targeting meta attributes.
const type = getBlockType( block.name );
if ( type ) {
attributes = reduce( type.attributes, ( result, value, key ) => {
if ( value.source === 'meta' ) {
if ( result === attributes ) {
result = { ...result };
}

result[ key ] = getPostMeta( state, value.meta );
}

return result;
}, attributes );
}
export function getBlockAttributes( state, clientId ) {
Copy link
Member

Choose a reason for hiding this comment

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

Oh, this refactor makes me quite happy to see 😄 This should serve as a nice optimization.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was a bit disappointed to notice that it doesn't change a lot in terms for performance impact. (The cache bust is the bottleneck right now)

const block = state.blocks.byClientId[ clientId ];
if ( ! block ) {
return null;
}

return attributes;
},
( state, clientId ) => [
state.blocks.byClientId[ clientId ],
state.blocks.attributes[ clientId ],
getPostMeta( state ),
]
);
return state.blocks.attributes[ clientId ];
}

/**
* Returns a block given its client ID. This is a parsed copy of the block,
Expand All @@ -192,7 +156,8 @@ export const getBlock = createSelector(
};
},
( state, clientId ) => [
...getBlockAttributes.getDependants( state, clientId ),
state.blocks.attributes[ clientId ],
state.blocks.byClientId[ clientId ],
getBlockDependantsCacheBust( state, clientId ),
]
);
Expand All @@ -211,7 +176,7 @@ export const __unstableGetBlockWithoutInnerBlocks = createSelector(
},
( state, clientId ) => [
state.blocks.byClientId[ clientId ],
...getBlockAttributes.getDependants( state, clientId ),
state.blocks.attributes[ clientId ],
]
);

Expand Down Expand Up @@ -314,7 +279,6 @@ export const getBlocksByClientId = createSelector(
( clientId ) => getBlock( state, clientId )
),
( state ) => [
getPostMeta( state ),
state.blocks.byClientId,
state.blocks.order,
state.blocks.attributes,
Expand Down Expand Up @@ -691,7 +655,6 @@ export const getMultiSelectedBlocks = createSelector(
state.blocks.byClientId,
state.blocks.order,
state.blocks.attributes,
getPostMeta( state ),
]
);

Expand Down Expand Up @@ -1437,6 +1400,17 @@ export function isLastBlockChangePersistent( state ) {
return state.blocks.isPersistentChange;
}

/**
* Returns an object describing the last block attributes changes
*
* @param {Object} state Block editor state.
*
* @return {Object} the clientId and the block attributes changes
*/
export function getLastBlockAttributesChange( state ) {
return state.lastBlockAttributesChanges;
}

/**
* Returns true if the most recent block change is be considered ignored, or
* false otherwise. An ignored change is one not to be committed by
Expand All @@ -1455,22 +1429,6 @@ export function __unstableIsLastBlockChangeIgnored( state ) {
return state.blocks.isIgnoredChange;
}

/**
* Returns the value of a post meta from the editor settings.
*
* @param {Object} state Global application state.
* @param {string} key Meta Key to retrieve
*
* @return {*} Meta value
*/
function getPostMeta( state, key ) {
if ( key === undefined ) {
return get( state, [ 'settings', '__experimentalMetaSource', 'value' ], EMPTY_OBJECT );
}

return get( state, [ 'settings', '__experimentalMetaSource', 'value', key ] );
}

/**
* Returns the available reusable blocks
*
Expand Down
48 changes: 0 additions & 48 deletions packages/block-editor/src/store/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,54 +473,6 @@ describe( 'selectors', () => {
} ],
} );
} );

it( 'should merge meta attributes for the block', () => {
registerBlockType( 'core/meta-block', {
save: ( props ) => props.attributes.text,
category: 'common',
title: 'test block',
attributes: {
foo: {
type: 'string',
source: 'meta',
meta: 'foo',
},
},
} );

const state = {
settings: {
__experimentalMetaSource: {
value: {
foo: 'bar',
},
},
},
blocks: {
byClientId: {
123: { clientId: 123, name: 'core/meta-block' },
},
attributes: {
123: {},
},
order: {
'': [ 123 ],
123: [],
},
},
};

expect( getBlock( state, 123 ) ).toEqual( {
clientId: 123,
name: 'core/meta-block',
attributes: {
foo: 'bar',
},
innerBlocks: [],
} );

unregisterBlockType( 'core/meta-block' );
} );
} );

describe( 'getBlocks', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e-tests/specs/performance.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe( 'Performance', () => {
}
} );

dispatch( 'core/editor' ).resetBlocks( blocks );
dispatch( 'core/block-editor' ).resetBlocks( blocks );
}, html );
await saveDraft();

Expand Down
Loading