diff --git a/projects/packages/blocks/changelog/add-enable-block-registration-by-path b/projects/packages/blocks/changelog/add-enable-block-registration-by-path new file mode 100644 index 0000000000000..1e16ea25413aa --- /dev/null +++ b/projects/packages/blocks/changelog/add-enable-block-registration-by-path @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Enable block registration by specifying block.json path diff --git a/projects/packages/blocks/composer.json b/projects/packages/blocks/composer.json index e7767982eefd1..cdd02e3e09c1d 100644 --- a/projects/packages/blocks/composer.json +++ b/projects/packages/blocks/composer.json @@ -46,7 +46,7 @@ "link-template": "https://github.com/Automattic/jetpack-blocks/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "1.4.x-dev" + "dev-trunk": "1.5.x-dev" } }, "config": { diff --git a/projects/packages/blocks/src/class-blocks.php b/projects/packages/blocks/src/class-blocks.php index 9286bd4ebc36d..ca7ba94133908 100644 --- a/projects/packages/blocks/src/class-blocks.php +++ b/projects/packages/blocks/src/class-blocks.php @@ -27,7 +27,7 @@ class Blocks { * * @since 1.1.0 * - * @param string $slug Slug of the block. + * @param string $slug Slug of the block or absolute path to the directory containing the block.json file. * @param array $args { * Arguments that are passed into register_block_type. * See register_block_type for full list of arguments. @@ -40,11 +40,29 @@ class Blocks { * @return WP_Block_Type|false The registered block type on success, or false on failure. */ public static function jetpack_register_block( $slug, $args = array() ) { - if ( 0 !== strpos( $slug, 'jetpack/' ) && ! strpos( $slug, '/' ) ) { + // Slug doesn't start with `jetpack/`, isn't an absolute path, or doesn't contain a slash + // (synonym of a namespace) at all. + if ( 0 !== strpos( $slug, 'jetpack/' ) && 0 !== strpos( $slug, '/' ) && ! strpos( $slug, '/' ) ) { _doing_it_wrong( 'jetpack_register_block', 'Prefix the block with jetpack/ ', 'Jetpack 9.0.0' ); $slug = 'jetpack/' . $slug; } + $block_type = $slug; + + // If the path to block.json is passed, find the slug in the file then create a block type + // object to register the block. + // Note: passing the path directly to register_block_type seems to loose the interactivity of + // the block once in the editor once it's out of focus. + if ( '/' === substr( $slug, 0, 1 ) ) { + $metadata = self::get_block_metadata_from_file( $slug ); + $name = self::get_block_name_from_metadata( $metadata ); + + if ( ! empty( $name ) ) { + $slug = $name; + $block_type = new \WP_Block_Type( $slug, array_merge( $metadata, $args ) ); + } + } + if ( isset( $args['version_requirements'] ) && ! self::is_gutenberg_version_available( $args['version_requirements'], $slug ) @@ -99,7 +117,52 @@ function () use ( $feature_name, $method_name ) { } } - return register_block_type( $slug, $args ); + return register_block_type( $block_type, $args ); + } + + /** + * Read block metadata from a block.json file. + * + * @param string $filename The path to the block.json file or its directory. + * + * @return array The block metadata. + */ + public static function get_block_metadata_from_file( $filename ) { + $metadata = array(); + $needle = '/block.json'; + $filename = $needle === substr( $filename, -strlen( $needle ) ) ? $filename : realpath( $filename . $needle ); + + if ( file_exists( $filename ) ) { + try { + $metadata = wp_json_file_decode( $filename, array( 'associative' => true ) ); + } catch ( \Exception $e ) { + $metadata = array(); + } + } + + return $metadata; + } + + /** + * Get the block name from the its metadata. + * + * @param array $metadata The block metadata. + * + * @return string The block name. + */ + public static function get_block_name_from_metadata( $metadata ) { + return ! isset( $metadata['name'] ) || empty( $metadata['name'] ) ? '' : $metadata['name']; + } + + /** + * Get the block feature name (i.e. the name without the `jetpack` prefix) from its metadata. + * + * @param array $metadata The block metadata. + * + * @return string The block feature name. + */ + public static function get_block_feature_from_metadata( $metadata ) { + return str_replace( 'jetpack/', '', self::get_block_name_from_metadata( $metadata ) ); } /** diff --git a/projects/packages/blocks/tests/php/fixtures/block.json b/projects/packages/blocks/tests/php/fixtures/block.json new file mode 100644 index 0000000000000..84ca1e7c2dc91 --- /dev/null +++ b/projects/packages/blocks/tests/php/fixtures/block.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "jetpack/test-block", + "title": "Test Block", + "description": "", + "keywords": [], + "version": "", + "textdomain": "jetpack", + "category": "", + "icon": "", + "supports": {}, + "editorScript": "" +} diff --git a/projects/packages/blocks/tests/php/test-blocks.php b/projects/packages/blocks/tests/php/test-blocks.php index c84c00678aabd..be4455f7c5c0d 100644 --- a/projects/packages/blocks/tests/php/test-blocks.php +++ b/projects/packages/blocks/tests/php/test-blocks.php @@ -330,4 +330,99 @@ public function test_jetpack_register_block_with_existing_editor_style() { remove_filter( 'jetpack_is_standalone_block', '__return_false' ); } } + + /** + * Test registering a block by specifying the path to its metadata file. + * + * @since $$next-version$$ + * + * @covers Automattic\Jetpack\Blocks::get_block_metadata_from_file + */ + public function test_jetpack_register_block_from_metadata_file() { + $result = Blocks::jetpack_register_block( __DIR__ . '/fixtures/block.json' ); + + $this->assertInstanceOf( 'WP_Block_Type', $result ); + } + + /** + * Test reading metadata from a block.json file by specifying its path. + * + * @since $$next-version$$ + * + * @covers Automattic\Jetpack\Blocks::get_block_metadata_from_file + */ + public function test_get_block_metadata_from_file() { + $result = Blocks::get_block_metadata_from_file( __DIR__ . '/fixtures/block.json' ); + + // phpcs:ignore MediaWiki.PHPUnit.SpecificAssertions.assertIsArray -- assertIsArray not supported by all PHP versions we support. + $this->assertTrue( is_array( $result ) ); + $this->assertNotEmpty( $result ); + } + + /** + * Test reading metadata from a block.json file by specifying its folder. + * + * @since $$next-version$$ + * + * @covers Automattic\Jetpack\Blocks::get_block_metadata_from_file + */ + public function test_get_block_metadata_from_folder() { + $result = Blocks::get_block_metadata_from_file( __DIR__ . '/fixtures/' ); + + // phpcs:ignore MediaWiki.PHPUnit.SpecificAssertions.assertIsArray -- assertIsArray not supported by all PHP versions we support. + $this->assertTrue( is_array( $result ) ); + $this->assertNotEmpty( $result ); + } + + /** + * Test reading metadata from a file that doesn't exist. + * + * @since $$next-version$$ + * + * @covers Automattic\Jetpack\Blocks::get_block_metadata_from_file + */ + public function test_get_block_metadata_from_wrong_file() { + $result = Blocks::get_block_metadata_from_file( __DIR__ . '/fixtures/ghost-folder/block.json' ); + + // phpcs:ignore MediaWiki.PHPUnit.SpecificAssertions.assertIsArray -- assertIsArray not supported by all PHP versions we support. + $this->assertTrue( is_array( $result ) ); + $this->assertEmpty( $result ); + } + + /** + * Test reading the name of a block from its metadata. + * + * @since $$next-version$$ + * + * @covers Automattic\Jetpack\Blocks::get_block_name_from_metadata + */ + public function test_get_block_name_from_metadata() { + $name = 'jetpack/test-block'; + $result = Blocks::get_block_name_from_metadata( array() ); + + $this->assertSame( '', $result ); + + $result = Blocks::get_block_name_from_metadata( array( 'name' => $name ) ); + + $this->assertEquals( $result, $name ); + } + + /** + * Test reading the feature name of a block from its metadata. + * + * @since $$next-version$$ + * + * @covers Automattic\Jetpack\Blocks::get_block_name_from_metadata + */ + public function test_get_block_feature_from_metadata() { + $feature = 'test-block'; + $name = 'jetpack/' . $feature; + $result = Blocks::get_block_feature_from_metadata( array() ); + + $this->assertSame( '', $result ); + + $result = Blocks::get_block_feature_from_metadata( array( 'name' => $name ) ); + + $this->assertEquals( $result, $feature ); + } } diff --git a/projects/plugins/jetpack/changelog/add-enable-block-registration-by-path b/projects/plugins/jetpack/changelog/add-enable-block-registration-by-path new file mode 100644 index 0000000000000..875501b79b681 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-enable-block-registration-by-path @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Enable block registration by specifying block.json path diff --git a/projects/plugins/jetpack/changelog/add-enable-block-registration-by-path#2 b/projects/plugins/jetpack/changelog/add-enable-block-registration-by-path#2 new file mode 100644 index 0000000000000..a1c1831fa1ef7 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-enable-block-registration-by-path#2 @@ -0,0 +1,5 @@ +Significance: patch +Type: other +Comment: Updated composer.lock. + + diff --git a/projects/plugins/jetpack/class.jetpack-gutenberg.php b/projects/plugins/jetpack/class.jetpack-gutenberg.php index 77686715abc48..3e8038921152a 100644 --- a/projects/plugins/jetpack/class.jetpack-gutenberg.php +++ b/projects/plugins/jetpack/class.jetpack-gutenberg.php @@ -466,7 +466,7 @@ public static function should_load() { /** * Only enqueue block assets when needed. * - * @param string $type Slug of the block. + * @param string $type Slug of the block or absolute path to the directory containing the block.json file. * @param array $script_dependencies Script dependencies. Will be merged with automatically * detected script dependencies from the webpack build. * @@ -478,6 +478,15 @@ public static function load_assets_as_required( $type, $script_dependencies = ar return; } + // Retrieve the feature from block.json if its path is passed. + if ( '/' === substr( $type, 0, 1 ) ) { + $feature = Blocks::get_block_feature_from_metadata( Blocks::get_block_metadata_from_file( $type ) ); + + if ( ! empty( $feature ) ) { + $type = $feature; + } + } + $type = sanitize_title_with_dashes( $type ); self::load_styles_as_required( $type ); self::load_scripts_as_required( $type, $script_dependencies ); diff --git a/projects/plugins/jetpack/composer.lock b/projects/plugins/jetpack/composer.lock index 4559346ba9562..d184883edafda 100644 --- a/projects/plugins/jetpack/composer.lock +++ b/projects/plugins/jetpack/composer.lock @@ -526,7 +526,7 @@ "dist": { "type": "path", "url": "../../packages/blocks", - "reference": "21025b86c1837680dae628cb68c66299dcfaa8d6" + "reference": "aa5b7af0bd7d9ae4b54ed9c4664ddc6927ce0c7d" }, "require-dev": { "automattic/jetpack-changelogger": "@dev", @@ -545,7 +545,7 @@ "link-template": "https://github.com/Automattic/jetpack-blocks/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "1.4.x-dev" + "dev-trunk": "1.5.x-dev" } }, "autoload": { diff --git a/projects/plugins/jetpack/extensions/editor.js b/projects/plugins/jetpack/extensions/editor.js index 2a676c9a69bf2..d9dc3d6223023 100644 --- a/projects/plugins/jetpack/extensions/editor.js +++ b/projects/plugins/jetpack/extensions/editor.js @@ -78,10 +78,16 @@ function setBetaBlockTitle( settings, name ) { return settings; } + const { title, keywords } = settings; + const titleSuffix = '(beta)'; + const betaKeyword = 'beta'; + return { ...settings, - title: `${ settings.title } (beta)`, - kewords: [ ...settings.keywords, 'beta' ], + title: title.toLowerCase().endsWith( titleSuffix ) + ? title + : `${ settings.title } ${ titleSuffix }`, + kewords: keywords.includes( betaKeyword ) ? keywords : [ ...keywords, betaKeyword ], }; } diff --git a/projects/plugins/jetpack/extensions/shared/register-jetpack-block.js b/projects/plugins/jetpack/extensions/shared/register-jetpack-block.js index b37c4576a9404..1912002a7b39f 100644 --- a/projects/plugins/jetpack/extensions/shared/register-jetpack-block.js +++ b/projects/plugins/jetpack/extensions/shared/register-jetpack-block.js @@ -6,38 +6,46 @@ import { import { registerBlockType } from '@wordpress/blocks'; import { addFilter } from '@wordpress/hooks'; +const JETPACK_PREFIX = 'jetpack/'; + /** * Registers a gutenberg block if the availability requirements are met. * - * @param {string} name - The block's name. + * @param {string} name - The block's name. Jetpack blocks must be registered with a name prefixed + * with `jetpack/`. This function accepts an unprefixed name too, though (it'd handle both + * `business-hours` and `jetpack/business-hours` similarly, for instance). * @param {object} settings - The block's settings. * @param {object} childBlocks - The block's child blocks. * @param {boolean} prefix - Should this block be prefixed with `jetpack/`? * @returns {object|boolean} Either false if the block is not available, or the results of `registerBlockType` */ export default function registerJetpackBlock( name, settings, childBlocks = [], prefix = true ) { - const { available, details, unavailableReason } = getJetpackExtensionAvailability( name ); + const isNamePrefixed = name.startsWith( JETPACK_PREFIX ); + const rawName = isNamePrefixed ? name.slice( JETPACK_PREFIX.length ) : name; + + const { available, details, unavailableReason } = getJetpackExtensionAvailability( rawName ); const requiredPlan = requiresPaidPlan( unavailableReason, details ); - const jpPrefix = prefix ? 'jetpack/' : ''; + const jpPrefix = prefix || isNamePrefixed ? JETPACK_PREFIX : ''; if ( ! available && ! requiredPlan ) { if ( 'production' !== process.env.NODE_ENV ) { // eslint-disable-next-line no-console console.warn( - `Block ${ name } couldn't be registered because it is unavailable (${ unavailableReason }).` + `Block ${ rawName } couldn't be registered because it is unavailable (${ unavailableReason }).` ); } return false; } - const result = registerBlockType( jpPrefix + name, settings ); + const prefixedName = jpPrefix + rawName; + const result = registerBlockType( prefixedName, settings ); if ( requiredPlan ) { addFilter( 'editor.BlockListBlock', - `${ jpPrefix + name }-with-has-warning-is-interactive-class-names`, - withHasWarningIsInteractiveClassNames( jpPrefix + name ) + `${ prefixedName }-with-has-warning-is-interactive-class-names`, + withHasWarningIsInteractiveClassNames( prefixedName ) ); }