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

Add a control per block to reset pattern overrides #57907

Merged
merged 6 commits into from
Feb 7, 2024
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
17 changes: 15 additions & 2 deletions packages/block-library/src/block/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from '@wordpress/block-editor';
import { privateApis as patternsPrivateApis } from '@wordpress/patterns';
import { parse, cloneBlock } from '@wordpress/blocks';
import { RichTextData } from '@wordpress/rich-text';

/**
* Internal dependencies
Expand Down Expand Up @@ -131,6 +132,16 @@ function applyInitialContentValuesToInnerBlocks(
} );
}

function isAttributeEqual( attribute1, attribute2 ) {
if (
attribute1 instanceof RichTextData &&
attribute2 instanceof RichTextData
) {
return attribute1.toString() === attribute2.toString();
Copy link
Member Author

Choose a reason for hiding this comment

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

A bit of a gotcha here that if the attributes are different RichTextData instances then === won't be true even though we expect them to be most of the time. 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

I can't really think of another way, so this looks ok to me.

}
return attribute1 === attribute2;
}

function getContentValuesFromInnerBlocks( blocks, defaultValues ) {
/** @type {Record<string, { values: Record<string, unknown>}>} */
const content = {};
Expand All @@ -145,8 +156,10 @@ function getContentValuesFromInnerBlocks( blocks, defaultValues ) {
const attributes = getOverridableAttributes( block );
for ( const attributeKey of attributes ) {
if (
block.attributes[ attributeKey ] !==
defaultValues[ blockId ][ attributeKey ]
! isAttributeEqual(
block.attributes[ attributeKey ],
defaultValues[ blockId ][ attributeKey ]
)
) {
content[ blockId ] ??= { values: {} };
content[ blockId ].values[ attributeKey ] =
Expand Down
27 changes: 23 additions & 4 deletions packages/editor/src/hooks/pattern-partial-syncing.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { unlock } from '../lock-unlock';

const {
PartialSyncingControls,
ResetOverridesControl,
PATTERN_TYPES,
PARTIAL_SYNCING_SUPPORTED_BLOCKS,
} = unlock( patternsPrivateApis );
Expand Down Expand Up @@ -54,12 +55,30 @@ function ControlsWithStoreSubscription( props ) {
select( editorStore ).getCurrentPostType() === PATTERN_TYPES.user,
[]
);
const bindings = props.attributes.metadata?.bindings;
const hasPatternBindings =
!! bindings &&
Object.values( bindings ).some(
( binding ) => binding.source === 'core/pattern-overrides'
);

const shouldShowPartialSyncingControls =
isEditingPattern && blockEditingMode === 'default';
const shouldShowResetOverridesControl =
! isEditingPattern &&
!! props.attributes.metadata?.id &&
blockEditingMode !== 'disabled' &&
hasPatternBindings;

return (
isEditingPattern &&
blockEditingMode === 'default' && (
<PartialSyncingControls { ...props } />
)
<>
{ shouldShowPartialSyncingControls && (
<PartialSyncingControls { ...props } />
) }
{ shouldShowResetOverridesControl && (
<ResetOverridesControl { ...props } />
) }
</>
);
}

Expand Down
78 changes: 78 additions & 0 deletions packages/patterns/src/components/reset-overrides-control.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* WordPress dependencies
*/
import {
store as blockEditorStore,
BlockControls,
} from '@wordpress/block-editor';
import { ToolbarButton, ToolbarGroup } from '@wordpress/components';
import { useSelect, useRegistry } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { parse } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';

function recursivelyFindBlockWithId( blocks, id ) {
for ( const block of blocks ) {
if ( block.attributes.metadata?.id === id ) {
return block;
}

const found = recursivelyFindBlockWithId( block.innerBlocks, id );
if ( found ) {
return found;
}
}
}

export default function ResetOverridesControl( props ) {
const registry = useRegistry();
const id = props.attributes.metadata?.id;
const patternWithOverrides = useSelect(
( select ) => {
if ( ! id ) {
return undefined;
}

const { getBlockParentsByBlockName, getBlocksByClientId } =
select( blockEditorStore );
const patternBlock = getBlocksByClientId(
getBlockParentsByBlockName( props.clientId, 'core/block' )
)[ 0 ];

if ( ! patternBlock?.attributes.content?.[ id ] ) {
return undefined;
}

return patternBlock;
},
[ props.clientId, id ]
);

const resetOverrides = async () => {
const editedRecord = await registry
.resolveSelect( coreStore )
.getEditedEntityRecord(
'postType',
'wp_block',
patternWithOverrides.attributes.ref
);
const blocks = editedRecord.blocks ?? parse( editedRecord.content );
const block = recursivelyFindBlockWithId( blocks, id );

props.setAttributes( block.attributes );
};

return (
<BlockControls group="other">
<ToolbarGroup>
<ToolbarButton
onClick={ resetOverrides }
disabled={ ! patternWithOverrides }
__experimentalIsFocusable
>
{ __( 'Reset' ) }
</ToolbarButton>
</ToolbarGroup>
</BlockControls>
);
}
2 changes: 2 additions & 0 deletions packages/patterns/src/private-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import RenamePatternModal from './components/rename-pattern-modal';
import PatternsMenuItems from './components';
import RenamePatternCategoryModal from './components/rename-pattern-category-modal';
import PartialSyncingControls from './components/partial-syncing-controls';
import ResetOverridesControl from './components/reset-overrides-control';
import {
PATTERN_TYPES,
PATTERN_DEFAULT_CATEGORY,
Expand All @@ -33,6 +34,7 @@ lock( privateApis, {
PatternsMenuItems,
RenamePatternCategoryModal,
PartialSyncingControls,
ResetOverridesControl,
PATTERN_TYPES,
PATTERN_DEFAULT_CATEGORY,
PATTERN_USER_CATEGORY,
Expand Down
91 changes: 91 additions & 0 deletions test/e2e/specs/editor/various/pattern-overrides.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,4 +493,95 @@ test.describe( 'Pattern Overrides', () => {
page.getByText( 'Inner paragraph (edited)' )
).toBeVisible();
} );

test( 'resets overrides after clicking the reset button', async ( {
page,
admin,
requestUtils,
editor,
} ) => {
const headingId = 'heading-id';
const paragraphId = 'paragraph-id';
const { id } = await requestUtils.createBlock( {
title: 'Pattern',
content: `<!-- wp:heading {"metadata":{"id":"${ headingId }","bindings":{"content":{"source":"core/pattern-overrides"}}}} -->
<h2 class="wp-block-heading">Heading</h2>
<!-- /wp:heading -->
<!-- wp:paragraph {"metadata":{"id":"${ paragraphId }","bindings":{"content":{"source":"core/pattern-overrides"}}}} -->
<p>Paragraph</p>
<!-- /wp:paragraph -->`,
status: 'publish',
} );

await admin.createNewPost();

await editor.insertBlock( {
name: 'core/block',
attributes: { ref: id },
} );

// Make an edit to the heading.
await editor.canvas
.getByRole( 'document', { name: 'Block: Heading' } )
.fill( 'Heading (edited)' );

const patternBlock = editor.canvas.getByRole( 'document', {
name: 'Block: Pattern',
} );
const headingBlock = patternBlock.getByRole( 'document', {
name: 'Block: Heading',
} );
const paragraphBlock = patternBlock.getByRole( 'document', {
name: 'Block: Paragraph',
} );
const resetButton = page
.getByRole( 'toolbar', { name: 'Block tools' } )
.getByRole( 'button', { name: 'Reset' } );

// Assert the pattern block.
await editor.selectBlocks( patternBlock );
await editor.showBlockToolbar();
await expect(
resetButton,
'The pattern block should have the reset button enabled'
).toBeEnabled();

// Assert the modified heading block with overrides.
await editor.selectBlocks( headingBlock );
await editor.showBlockToolbar();
await expect(
resetButton,
'The heading block should have the reset button enabled'
).toBeEnabled();

// Assert the unmodified paragraph block (no overrides).
await editor.selectBlocks( paragraphBlock );
await editor.showBlockToolbar();
await expect(
resetButton,
'The paragraph block should not have the reset button enabled'
).toBeDisabled();

// Reset the whole pattern.
await editor.selectBlocks( patternBlock );
await editor.showBlockToolbar();
await resetButton.click();
await expect( headingBlock ).toHaveText( 'Heading' );

// Undo should work
await page
.getByRole( 'toolbar', { name: 'Document tools' } )
.getByRole( 'button', { name: 'Undo' } )
.click();
await expect( headingBlock ).toHaveText( 'Heading (edited)' );

// Reset the individual heading block.
await editor.selectBlocks( headingBlock );
await editor.showBlockToolbar();
await resetButton.click();
await expect( headingBlock ).toHaveText( 'Heading' );
await editor.selectBlocks( patternBlock );
await editor.showBlockToolbar();
await expect( resetButton ).toBeDisabled();
} );
} );
Loading