diff --git a/plugins/woocommerce/changelog/add-template-api-remove-block b/plugins/woocommerce/changelog/add-template-api-remove-block new file mode 100644 index 00000000000..9f041169287 --- /dev/null +++ b/plugins/woocommerce/changelog/add-template-api-remove-block @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add ability to remove blocks from templates. diff --git a/plugins/woocommerce/src/Admin/BlockTemplates/BlockInterface.php b/plugins/woocommerce/src/Admin/BlockTemplates/BlockInterface.php index 9d22c1a464b..0302272f0b2 100644 --- a/plugins/woocommerce/src/Admin/BlockTemplates/BlockInterface.php +++ b/plugins/woocommerce/src/Admin/BlockTemplates/BlockInterface.php @@ -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. * diff --git a/plugins/woocommerce/src/Admin/BlockTemplates/ContainerInterface.php b/plugins/woocommerce/src/Admin/BlockTemplates/ContainerInterface.php index 5656fe4e9f6..3119e67b0c8 100644 --- a/plugins/woocommerce/src/Admin/BlockTemplates/ContainerInterface.php +++ b/plugins/woocommerce/src/Admin/BlockTemplates/ContainerInterface.php @@ -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(); } diff --git a/plugins/woocommerce/src/Internal/Admin/BlockTemplates/AbstractBlock.php b/plugins/woocommerce/src/Internal/Admin/BlockTemplates/AbstractBlock.php index 54a21323ce0..1551b7cd959 100644 --- a/plugins/woocommerce/src/Internal/Admin/BlockTemplates/AbstractBlock.php +++ b/plugins/woocommerce/src/Internal/Admin/BlockTemplates/AbstractBlock.php @@ -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. * diff --git a/plugins/woocommerce/src/Internal/Admin/BlockTemplates/AbstractBlockTemplate.php b/plugins/woocommerce/src/Internal/Admin/BlockTemplates/AbstractBlockTemplate.php index 3a72feb1fda..437cde48658 100644 --- a/plugins/woocommerce/src/Internal/Admin/BlockTemplates/AbstractBlockTemplate.php +++ b/plugins/woocommerce/src/Internal/Admin/BlockTemplates/AbstractBlockTemplate.php @@ -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. * diff --git a/plugins/woocommerce/src/Internal/Admin/BlockTemplates/BlockContainerTrait.php b/plugins/woocommerce/src/Internal/Admin/BlockTemplates/BlockContainerTrait.php index d994c475a2e..ad2983ada42 100644 --- a/plugins/woocommerce/src/Internal/Admin/BlockTemplates/BlockContainerTrait.php +++ b/plugins/woocommerce/src/Internal/Admin/BlockTemplates/BlockContainerTrait.php @@ -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. */ diff --git a/plugins/woocommerce/src/Internal/Admin/BlockTemplates/BlockTemplate.php b/plugins/woocommerce/src/Internal/Admin/BlockTemplates/BlockTemplate.php index 7c2453b52c5..c2655cdea40 100644 --- a/plugins/woocommerce/src/Internal/Admin/BlockTemplates/BlockTemplate.php +++ b/plugins/woocommerce/src/Internal/Admin/BlockTemplates/BlockTemplate.php @@ -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. */ diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/BlockTemplateTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/BlockTemplateTest.php index 4b156d793d6..30678bba3fe 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/BlockTemplateTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/BlockTemplateTest.php @@ -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.' + ); + } } diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/BlockTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/BlockTest.php index 79f5c054e1c..2b37a715189 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/BlockTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/BlockTest.php @@ -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. */ diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlock.php b/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlock.php index 46f524aaa3f..f601d196db2 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlock.php +++ b/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlock.php @@ -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 diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlockInterface.php b/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlockInterface.php index dbaf133a8c8..f920aadcc2d 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlockInterface.php +++ b/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlockInterface.php @@ -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; } diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlockTemplateTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlockTemplateTest.php index bc4ba384748..4eef05aab3a 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlockTemplateTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlockTemplateTest.php @@ -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' ) ); + } } diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlockTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlockTest.php index f6f81d52c31..d9914b44390 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlockTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Admin/BlockTemplates/CustomBlockTest.php @@ -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' + ); + } }