Remove block from block template (#39900)

This commit is contained in:
Matt Sherman 2023-08-28 10:17:55 -04:00 committed by GitHub
commit 7da226ad63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 442 additions and 7 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add ability to remove blocks from templates.

View File

@ -62,14 +62,23 @@ interface BlockInterface {
/**
* Get the parent container that the block belongs to.
*
* @throws \RuntimeException If the block does not have a parent.
*/
public function &get_parent(): ?ContainerInterface;
/**
* Get the root template that the block belongs to.
*
* @throws \RuntimeException If the block does not have a root template.
*/
public function &get_root_template(): BlockTemplateInterface;
/**
* Detach the block from its parent and root template.
*/
public function detach();
/**
* Get the block configuration as a formatted template.
*

View File

@ -15,4 +15,18 @@ interface ContainerInterface {
* Get the block configuration as a formatted template.
*/
public function get_formatted_template(): array;
/**
* Removes a block from the container.
*
* @param string $block_id The block ID.
*
* @throws \UnexpectedValueException If the block container is not an ancestor of the block.
*/
public function remove_block( string $block_id );
/**
* Removes all blocks from the container.
*/
public function remove_blocks();
}

View File

@ -161,18 +161,38 @@ class AbstractBlock implements BlockInterface {
/**
* Get the template that this block belongs to.
*
* @throws \RuntimeException If the block does not have a root template.
*/
public function &get_root_template(): BlockTemplateInterface {
if ( is_null( $this->root_template ) ) {
throw new \RuntimeException( 'The block does not have a root template.' );
}
return $this->root_template;
}
/**
* Get the parent block container.
*
* @throws \RuntimeException If the block does not have a parent.
*/
public function &get_parent(): ContainerInterface {
if ( is_null( $this->parent ) ) {
throw new \RuntimeException( 'The block does not have a parent.' );
}
return $this->parent;
}
/**
* Detach the block from its parent block container and the template it belongs to.
*/
public function detach() {
$this->parent = null;
$this->root_template = null;
}
/**
* Get the block configuration as a formatted template.
*

View File

@ -55,7 +55,7 @@ abstract class AbstractBlockTemplate implements BlockTemplateInterface {
/**
* Caches a block in the template. This is an internal method and should not be called directly
* except for classes that implement BlockContainerInterface, in their add_block() method.
* except for from the BlockContainerTrait's add_inner_block() method.
*
* @param BlockInterface $block The block to cache.
*
@ -78,6 +78,20 @@ abstract class AbstractBlockTemplate implements BlockTemplateInterface {
$this->block_cache[ $id ] = $block;
}
/**
* Uncaches a block in the template. This is an internal method and should not be called directly
* except for from the BlockContainerTrait's remove_block() method.
*
* @param string $block_id The block ID.
*
* @ignore
*/
public function uncache_block( string $block_id ) {
if ( isset( $this->block_cache[ $block_id ] ) ) {
unset( $this->block_cache[ $block_id ] );
}
}
/**
* Generate a block ID based on a base.
*

View File

@ -3,6 +3,7 @@
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
/**
* Trait for block containers.
@ -44,6 +45,91 @@ trait BlockContainerTrait {
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
/**
* Checks if a block is a descendant of the block container.
*
* @param BlockInterface $block The block.
*/
private function is_block_descendant( BlockInterface $block ): bool {
$parent = $block->get_parent();
if ( $parent === $this ) {
return true;
}
if ( ! $parent instanceof BlockInterface ) {
return false;
}
return $this->is_block_descendant( $parent );
}
/**
* Remove a block from the block container.
*
* @param string $block_id The block ID.
*
* @throws \UnexpectedValueException If the block container is not an ancestor of the block.
*/
public function remove_block( string $block_id ) {
$root_template = $this->get_root_template();
$block = $root_template->get_block( $block_id );
if ( ! $block ) {
return;
}
if ( ! $this->is_block_descendant( $block ) ) {
throw new \UnexpectedValueException( 'The block container is not an ancestor of the block.' );
}
// If the block is a container, remove all of its blocks.
if ( $block instanceof ContainerInterface ) {
$block->remove_blocks();
}
// Remove block from root template's cache.
$root_template = $this->get_root_template();
$root_template->uncache_block( $block->get_id() );
$parent = $block->get_parent();
$parent->remove_inner_block( $block );
// Detach block from parent and root template.
$block->detach();
}
/**
* Remove all blocks from the block container.
*/
public function remove_blocks() {
array_map(
function ( BlockInterface $block ) {
$this->remove_block( $block->get_id() );
},
$this->inner_blocks
);
}
/**
* Remove a block from the block container's inner blocks. This is an internal method and should not be called directly
* except for from the BlockContainerTrait's remove_block() method.
*
* @param BlockInterface $block The block.
*/
public function remove_inner_block( BlockInterface $block ) {
$this->inner_blocks = array_filter(
$this->inner_blocks,
function ( BlockInterface $inner_block ) use ( $block ) {
return $inner_block !== $block;
}
);
}
/**
* Get the inner blocks sorted by order.
*/

View File

@ -18,7 +18,7 @@ class BlockTemplate extends AbstractBlockTemplate {
}
/**
* Generate a block ID based on a base.
* Add an inner block to this template.
*
* @param array $block_config The block data.
*/

View File

@ -364,4 +364,112 @@ class BlockTemplateTest extends WC_Unit_Test_Case {
'Failed asserting that the template is converted to a formatted template correctly.'
);
}
/**
* Test that removing a block in the template works.
*/
public function test_removing_blocks() {
$template = new BlockTemplate();
$template->add_block(
[
'blockName' => 'test-block-name-c',
'order' => 100,
'attributes' => [
'attr-c1' => 'value-c1',
'attr-c2' => 'value-c2',
],
]
);
$block_b = $template->add_block(
[
'id' => 'b',
'blockName' => 'test-block-name-b',
'order' => 50,
'attributes' => [
'attr-1' => 'value-1',
'attr-2' => 'value-2',
],
]
);
$template->add_block(
[
'id' => 'a',
'blockName' => 'test-block-name-a',
'order' => 10,
'attributes' => [
'attr-1' => 'value-1',
'attr-2' => 'value-2',
],
]
);
$block_b->add_block(
[
'blockName' => 'test-block-name-2',
'order' => 20,
'attributes' => [
'attr-1' => 'value-1',
'attr-2' => 'value-2',
],
]
);
$block_b->add_block(
[
'blockName' => 'test-block-name-1',
'order' => 10,
'attributes' => [
'attr-3' => 'value-3',
'attr-4' => 'value-4',
],
]
);
$block_b->add_block(
[
'blockName' => 'test-block-name-3',
'order' => 30,
]
);
$block_to_insert_in = $template->get_block( 'a' );
$block_to_insert_in->add_block(
[
'blockName' => 'inserted-block',
]
);
$template->remove_block( 'b' );
$this->assertSame(
[
[
'test-block-name-a',
[
'attr-1' => 'value-1',
'attr-2' => 'value-2',
],
[
[
'inserted-block',
[],
],
],
],
[
'test-block-name-c',
[
'attr-c1' => 'value-c1',
'attr-c2' => 'value-c2',
],
],
],
$template->get_formatted_template(),
'Failed asserting that the template is converted to a formatted template correctly.'
);
}
}

View File

@ -105,6 +105,110 @@ class BlockTest extends WC_Unit_Test_Case {
);
}
/**
* Test that removing a block from a block sets the parent and root template to null
* and that the block is removed from the root template.
*/
public function test_remove_block() {
$template = new BlockTemplate();
$block = $template->add_block(
[
'id' => 'test-block-id',
'blockName' => 'test-block-name',
]
);
$child_block = $block->add_block(
[
'id' => 'test-block-id-2',
'blockName' => 'test-block-name-2',
]
);
$block->remove_block( 'test-block-id-2' );
$this->assertNull(
$template->get_block( 'test-block-id-2' ),
'Failed asserting that the child block was removed from the root template.'
);
$this->expectException( \RuntimeException::class );
$child_block->get_parent();
}
/**
* Test that removing a block from a block sets the parent and root template to null
* and that the block is removed from the root template, as well as any descendants.
*/
public function test_remove_nested_block() {
$template = new BlockTemplate();
$block = $template->add_block(
[
'id' => 'test-block-id',
'blockName' => 'test-block-name',
]
);
$child_block = $block->add_block(
[
'id' => 'test-block-id-2',
'blockName' => 'test-block-name-2',
]
);
$template->remove_block( 'test-block-id-2' );
$this->assertNull(
$template->get_block( 'test-block-id-2' ),
'Failed asserting that the nested descendent block was removed from the root template.'
);
$this->expectException( \RuntimeException::class );
$child_block->get_parent();
}
/**
* Test that removing a block from a block sets the parent and root template to null
* and that the block is removed from the root template, as well as any descendants.
*/
public function test_remove_block_and_descendants() {
$template = new BlockTemplate();
$block = $template->add_block(
[
'id' => 'test-block-id',
'blockName' => 'test-block-name',
]
);
$child_block = $block->add_block(
[
'id' => 'test-block-id-2',
'blockName' => 'test-block-name-2',
]
);
$template->remove_block( 'test-block-id' );
$this->assertNull(
$template->get_block( 'test-block-id' ),
'Failed asserting that the child block was removed from the root template.'
);
$this->assertNull(
$template->get_block( 'test-block-id-2' ),
'Failed asserting that the nested descendent block was removed from the root template.'
);
$this->expectException( \RuntimeException::class );
$child_block->get_parent();
}
/**
* Test that adding nested blocks sets the parent and root template correctly.
*/

View File

@ -18,11 +18,16 @@ class CustomBlock extends AbstractBlock implements CustomBlockInterface {
/**
* Custom method.
*
* @param string $title The title.
*/
public function add_custom_inner_block(): BlockInterface {
public function add_custom_inner_block( string $title ): BlockInterface {
$block = new Block(
[
'blockName' => 'custom-inner-block',
'blockName' => 'custom-inner-block',
'attributes' => [
'title' => $title,
],
],
$this->get_root_template(),
$this

View File

@ -8,6 +8,8 @@ use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
interface CustomBlockInterface extends BlockContainerInterface {
/**
* Adds a method to insert a specific custom inner block.
*
* @param string $title The title.
*/
public function add_custom_inner_block(): BlockInterface;
public function add_custom_inner_block( string $title ): BlockInterface;
}

View File

@ -62,4 +62,29 @@ class CustomBlockTemplateTest extends WC_Unit_Test_Case {
$block = $template->get_block( 'test-block-name' );
$this->assertInstanceOf( CustomBlock::class, $block );
}
/**
* Test that a custom block can be removed as expected.
*/
public function test_remove_custom_block() {
$template = new CustomBlockTemplate();
$template->add_custom_block(
[
'id' => 'test-block-name-1',
'blockName' => 'test-block-name',
]
);
$template->add_custom_block(
[
'id' => 'test-block-name-2',
'blockName' => 'test-block-name',
]
);
$template->remove_block( 'test-block-name-1' );
$this->assertNull( $template->get_block( 'test-block-name-1' ) );
}
}

View File

@ -41,7 +41,8 @@ class CustomBlockTest extends WC_Unit_Test_Case {
$template
);
$block->add_custom_inner_block();
$block->add_custom_inner_block( 'a' );
$block->add_custom_inner_block( 'b' );
$this->assertSame(
[
@ -50,7 +51,15 @@ class CustomBlockTest extends WC_Unit_Test_Case {
[
[
'custom-inner-block',
[],
[
'title' => 'a',
],
],
[
'custom-inner-block',
[
'title' => 'b',
],
],
],
],
@ -58,4 +67,39 @@ class CustomBlockTest extends WC_Unit_Test_Case {
'Failed asserting that the inner block was added'
);
}
/**
* Test that a custom block is removed as expected.
*/
public function test_remove_custom_inner_block() {
$template = new BlockTemplate();
$block = new CustomBlock(
[
'blockName' => 'test-block-name',
],
$template
);
$block->add_custom_inner_block( 'a' );
$block->add_custom_inner_block( 'b' );
$template->remove_block( 'custom-inner-block-1' );
$this->assertSame(
[
'test-block-name',
[],
[
[
'custom-inner-block',
[
'title' => 'b',
],
],
],
],
$block->get_formatted_template(),
'Failed asserting that the inner block was removed'
);
}
}