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 Editor: Consider received blocks state change as ignored #14916

Merged
merged 4 commits into from
Apr 11, 2019
Merged
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 @@ -775,6 +775,20 @@ via its `onChange` callback, in addition to `onInput`.

Whether the most recent block change was persistent.

### __unstableIsLastBlockChangeIgnored
Copy link
Contributor

Choose a reason for hiding this comment

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

hmm... I didn't think __unstable or __experimental was supposed to show up in docs?

Copy link
Member Author

Choose a reason for hiding this comment

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

hmm... I didn't think __unstable or __experimental was supposed to show up in docs?

It caught me off guard as well. I think they may only be omitted in documentation generated by the docgen tool. The data modules documentation is a separate process. It should be improved (I'll create a follow-up task).


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
BlockEditorProvider, neither via `onChange` nor `onInput`.

*Parameters*

* state: Block editor state.

*Returns*

Whether the most recent block change was ignored.

## Actions

### resetBlocks
Expand Down
5 changes: 5 additions & 0 deletions packages/block-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@

- `CopyHandler` will now only catch cut/copy events coming from its `props.children`, instead of from anywhere in the `document`.

### Internal

- Improved handling of blocks state references for unchanging states.
- Updated handling of blocks state to effectively ignored programmatically-received blocks data (e.g. reusable blocks received from editor).

## 1.0.0 (2019-03-06)

### New Features
Expand Down
8 changes: 7 additions & 1 deletion packages/block-editor/src/components/provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class BlockEditorProvider extends Component {
const {
getBlocks,
isLastBlockChangePersistent,
__unstableIsLastBlockChangeIgnored,
} = registry.select( 'core/block-editor' );

let blocks = getBlocks();
Expand All @@ -81,7 +82,12 @@ class BlockEditorProvider extends Component {
} = this.props;
const newBlocks = getBlocks();
const newIsPersistent = isLastBlockChangePersistent();
if ( newBlocks !== blocks && this.isSyncingIncomingValue ) {
if (
newBlocks !== blocks && (
this.isSyncingIncomingValue ||
__unstableIsLastBlockChangeIgnored()
)
) {
this.isSyncingIncomingValue = false;
blocks = newBlocks;
isPersistent = newIsPersistent;
Expand Down
57 changes: 37 additions & 20 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,6 @@ export function isUpdatingSameBlockAttribute( action, lastAction ) {
function withPersistentBlockChange( reducer ) {
let lastAction;

/**
* Set of action types for which a blocks state change should be considered
* non-persistent.
*
* @type {Set}
*/
const IGNORED_ACTION_TYPES = new Set( [
'RECEIVE_BLOCKS',
] );

return ( state, action ) => {
let nextState = reducer( state, action );

Expand All @@ -206,19 +196,14 @@ function withPersistentBlockChange( reducer ) {
// Defer to previous state value (or default) unless changing or
// explicitly marking as persistent.
if ( state === nextState && ! isExplicitPersistentChange ) {
return {
...nextState,
isPersistentChange: get( state, [ 'isPersistentChange' ], true ),
};
}
const nextIsPersistentChange = get( state, [ 'isPersistentChange' ], true );
if ( state.isPersistentChange === nextIsPersistentChange ) {
return state;
}

// Some state changes should not be considered persistent, namely those
// which are not a direct result of user interaction.
const isIgnoredActionType = IGNORED_ACTION_TYPES.has( action.type );
if ( isIgnoredActionType ) {
return {
...nextState,
isPersistentChange: false,
isPersistentChange: nextIsPersistentChange,
};
}

Expand All @@ -239,6 +224,37 @@ function withPersistentBlockChange( reducer ) {
};
}

/**
* Higher-order reducer intended to augment the blocks reducer, assigning an
* `isIgnoredChange` property value corresponding to whether a change in state
* can be considered as ignored. A change is considered ignored when the result
* of an action not incurred by direct user interaction.
*
* @param {Function} reducer Original reducer function.
*
* @return {Function} Enhanced reducer function.
*/
function withIgnoredBlockChange( reducer ) {
/**
* Set of action types for which a blocks state change should be ignored.
*
* @type {Set}
*/
const IGNORED_ACTION_TYPES = new Set( [
'RECEIVE_BLOCKS',
] );
Copy link
Contributor

Choose a reason for hiding this comment

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

so for now is this just going to be a hardcoded list? Would there be value in being able to customize what action types are ignored? Eg. being able to call it like this:

withIgnoredBlockChange( [ 'RECEIVE_BLOCKS' ] )

If so, and that's useful, then the name of the HOC could maybe be changed to withIgnoredActionChange?

Copy link
Contributor

Choose a reason for hiding this comment

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

Re 'RECEIVE_BLOCKS being ignored, does that mean we expect change to be ignored for any block addition to the editor (and I'm assuming changes would get picked up when there is content added to the block?)

Copy link
Member Author

Choose a reason for hiding this comment

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

Re 'RECEIVE_BLOCKS being ignored, does that mean we expect change to be ignored for any block addition to the editor (and I'm assuming changes would get picked up when there is content added to the block?)

So, considering all of the above: Our use of RECEIVE_BLOCKS is and always has been exclusively a way to get blocks data for reusable blocks into state so they can be treated the same as any other content block by selectors such as getBlock, etc. But they're not part of the actual content. Regular user workflows to add blocks will go through INSERT_BLOCKS and friends.

Long-term then: RECEIVE_BLOCKS shouldn't be ignored, this list won't be necessary. We probably don't need to optimize too much for it now. In fact, RECEIVE_BLOCKS could probably be removed altogether as part of #7119, if not for compatibility's sake. Once reusable blocks are refactored as an embedded editor, we'd probably remove all of this ignoring behavior, and treat it as a real change that should trigger change detection.

In the meantime though, this basically restores the behavior we had prior to #13088 rather faithfully (including all imperfections therein).


return ( state, action ) => {
const nextState = reducer( state, action );

if ( nextState !== state ) {
nextState.isIgnoredChange = IGNORED_ACTION_TYPES.has( action.type );
Copy link
Contributor

Choose a reason for hiding this comment

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

so what is exactly the difference between isIgnoredChange and ! isPersistentChange?

Copy link
Member Author

Choose a reason for hiding this comment

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

From __unstableIsLastBlockChangeIgnored docs:

An ignored change is one not to be committed by BlockEditorProvider, neither via onChange nor onInput.

From isLastBlockChangePersistent docs:

A persistent change is one committed by BlockEditorProvider via its onChange callback, in addition to onInput.

Effectively: Non-persistent changes are still surfaced up to the rendering Editor, but treated as omitted from history. Ignored changes are not surfaced to the rendering Editor at all.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I think I got it, while one is about the onChange vs onInput the other is about not calling them entirely.
It does seems like they have the same roles though. Avoid undo levels, can't we unify and just use one?

Copy link
Contributor

Choose a reason for hiding this comment

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

(I replied to myself before I see your comment)

Copy link
Member Author

Choose a reason for hiding this comment

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

It does seems like they have the same roles though. Avoid undo levels, can't we unify and just use one?

This is a good summary of the problem. They're not the same. One is considered by withHistory, the other by withChangeDetection. The bug exists because we only accounted for the first, not the second.

If the question is whether "changes" should be a reflection of the presence of undo levels: I think long ago we had it this way, and it changed (presumably necessarily). Possibly as a result of some of the work to merge undo levels while still needing to consider them as changed.

Copy link
Contributor

Choose a reason for hiding this comment

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

👍 Thanks for the clarification, it makes sense.

Copy link
Member Author

Choose a reason for hiding this comment

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

If the question is whether "changes" should be a reflection of the presence of undo levels: I think long ago we had it this way, and it changed (presumably necessarily). Possibly as a result of some of the work to merge undo levels while still needing to consider them as changed.

I think the reason we don't consider Undo levels as part of post dirtiness is because Undo history doesn't reset when a Save occurs, but the post is "clean".

}

return nextState;
};
}

/**
* Higher-order reducer targeting the combined blocks reducer, augmenting
* block client IDs in remove action to include cascade of inner blocks.
Expand Down Expand Up @@ -380,6 +396,7 @@ export const blocks = flow(
withBlockReset,
withSaveReusableBlock,
withPersistentBlockChange,
withIgnoredBlockChange,
)( {
byClientId( state = {}, action ) {
switch ( action.type ) {
Expand Down
18 changes: 18 additions & 0 deletions packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1387,6 +1387,24 @@ export function isLastBlockChangePersistent( state ) {
return state.blocks.isPersistentChange;
}

/**
* 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
* BlockEditorProvider, neither via `onChange` nor `onInput`.
*
* @param {Object} state Block editor state.
*
* @return {boolean} Whether the most recent block change was ignored.
*/
export function __unstableIsLastBlockChangeIgnored( state ) {
// TODO: Removal Plan: Changes incurred by RECEIVE_BLOCKS should not be
// ignored if in-fact they result in a change in blocks state. The current
// need to ignore changes not a result of user interaction should be
// accounted for in the refactoring of reusable blocks as occurring within
// their own separate block editor / state (#7119).
return state.blocks.isIgnoredChange;
}

/**
* Returns the value of a post meta from the editor settings.
*
Expand Down
32 changes: 26 additions & 6 deletions packages/block-editor/src/store/test/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ describe( 'state', () => {
const state = blocks( existingState, action );

expect( state ).toEqual( {
isPersistentChange: expect.anything(),
isPersistentChange: true,
isIgnoredChange: false,
byClientId: {
clicken: {
clientId: 'chicken',
Expand Down Expand Up @@ -295,7 +296,8 @@ describe( 'state', () => {
const state = blocks( existingState, action );

expect( state ).toEqual( {
isPersistentChange: expect.anything(),
isPersistentChange: true,
isIgnoredChange: false,
byClientId: {
clicken: {
clientId: 'chicken',
Expand Down Expand Up @@ -386,7 +388,8 @@ describe( 'state', () => {
const state = blocks( existingState, action );

expect( state ).toEqual( {
isPersistentChange: expect.anything(),
isPersistentChange: true,
isIgnoredChange: false,
byClientId: {
clicken: {
clientId: 'chicken',
Expand Down Expand Up @@ -478,7 +481,8 @@ describe( 'state', () => {
const state = blocks( existingState, action );

expect( state ).toEqual( {
isPersistentChange: expect.anything(),
isPersistentChange: true,
isIgnoredChange: false,
byClientId: {
clicken: {
clientId: 'chicken',
Expand Down Expand Up @@ -512,6 +516,7 @@ describe( 'state', () => {
attributes: {},
order: {},
isPersistentChange: true,
isIgnoredChange: false,
} );
} );

Expand Down Expand Up @@ -1500,7 +1505,22 @@ describe( 'state', () => {
expect( state.isPersistentChange ).toBe( true );
} );

it( 'should not consider received blocks as persistent change', () => {
it( 'should retain reference for same state, same persistence', () => {
const original = deepFreeze( blocks( undefined, {
type: 'RESET_BLOCKS',
blocks: [],
} ) );

const state = blocks( original, {
type: '__INERT__',
} );

expect( state ).toBe( original );
} );
} );

describe( 'isIgnoredChange', () => {
it( 'should consider received blocks as ignored change', () => {
const state = blocks( undefined, {
type: 'RECEIVE_BLOCKS',
blocks: [ {
Expand All @@ -1510,7 +1530,7 @@ describe( 'state', () => {
} ],
} );

expect( state.isPersistentChange ).toBe( false );
expect( state.isIgnoredChange ).toBe( true );
} );
} );
} );
Expand Down
17 changes: 17 additions & 0 deletions packages/e2e-tests/specs/change-detection.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,21 @@ describe( 'Change detection', () => {

await assertIsDirty( true );
} );

it( 'should not prompt when receiving reusable blocks', async () => {
// Regression Test: Verify that non-modifying behaviors does not incur
// dirtiness. Previously, this could occur as a result of either (a)
// selecting a block, (b) opening the inserter, or (c) editing a post
// which contained a reusable block. The root issue was changes in
// block editor state as a result of reusable blocks data having been
// received, reflected here in this test.
//
// TODO: This should be considered a temporary test, existing only so
// long as the experimental reusable blocks fetching data flow exists.
//
// See: https://github.com/WordPress/gutenberg/issues/14766
Copy link
Contributor

Choose a reason for hiding this comment

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

Very good comment block explaining things 👍

await page.evaluate( () => window.wp.data.dispatch( 'core/editor' ).__experimentalReceiveReusableBlocks( [] ) );

await assertIsDirty( false );
} );
} );