diff --git a/README.md b/README.md index 816aeee..0bed27d 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ This plugin is currently developed for use on WordPress sites hosted on the VIP - [Setup](#setup) - [Usage](#usage-1) - [Block Attributes](#block-attributes) + - [Complex attributes](#complex-attributes) - [Example: Simple nested blocks: `core/list` and `core/quote`](#example-simple-nested-blocks-corelist-and-corequote) - [API Consumption](#api-consumption) - [Preact](#preact) @@ -376,6 +377,85 @@ The attributes of a block in GraphQL are available in a list of `name` / `value` This is used instead of a key-value structure. This is a trade-off that makes it easy to retrieve block attributes without specifying the the block type ahead of time, but attribute type information is lost. + +#### Complex attributes + +Some block attributes contain arrays or complex nested values. Demonstrated below, [the `core/table` block uses an array of objects][gutenberg-code-table-body] to represent head, body, and footer cell contents. The GraphQL Block Data API implementation represents these attributes as JSON-encoded strings along with the `isValueJsonEncoded` boolean field. When `isValueJsonEncoded` is `true`, an attribute's value must be JSON decoded to get the original complex value. + +For example, using this table: + +![Example core/table block with a two header cells and two body cells][media-example-table] + +We can query for attributes along with the `isValueJsonEncoded` field in a GraphQL query: + +```graphql +query PostQuery { + post(id: 1, idType: DATABASE_ID) { + blocksData { + blocks { + attributes { + name + value + isValueJsonEncoded + } + id + name + innerBlocks { + attributes { + name + value + isValueJsonEncoded + } + id + name + parentId + } + } + } + } +} +``` + +The result will contain JSON-encoded attributes designated by the `isValueJsonEncoded` field: + +```json +{ + "data": { + "post": { + "blocksData": { + "blocks": [ + { + "name": "core/table", + "attributes": [ + { + "name": "hasFixedLayout", + "value": "", + "isValueJsonEncoded": false + }, + { + "name": "head", + "value": "[{\"cells\":[{\"content\":\"Header A\",\"tag\":\"th\"},{\"content\":\"Header B\",\"tag\":\"th\"}]}]", + "isValueJsonEncoded": true + }, + { + "name": "body", + "value": "[{\"cells\":[{\"content\":\"Value 1\",\"tag\":\"td\"},{\"content\":\"Value 2\",\"tag\":\"td\"}]}]", + "isValueJsonEncoded": true + }, + { + "name": "foot", + "value": "[]", + "isValueJsonEncoded": true + } + ] + } + ] + } + } + } +} +``` + --- #### Example: Simple nested blocks: `core/list` and `core/quote` @@ -1405,6 +1485,7 @@ composer run test [gutenberg-code-image-caption]: https://github.com/WordPress/gutenberg/blob/3d2a6d7eaa4509c4d89bde674e9b73743868db2c/packages/block-library/src/image/block.json#L30-L35 +[gutenberg-code-table-body]: https://github.com/WordPress/gutenberg/blob/74a06c73613d9f90d66905c14d36eda19101999e/packages/block-library/src/table/block.json#L64-L108 [gutenberg-pr-core-list-innerblocks]: https://href.li/?https://github.com/WordPress/gutenberg/pull/39487 [media-example-caption-plain]: https://github.com/Automattic/vip-block-data-api/blob/media/example-caption-plain.png [media-example-caption-rich-text]: https://github.com/Automattic/vip-block-data-api/blob/media/example-caption-rich-text.png @@ -1412,6 +1493,7 @@ composer run test [media-example-list-quote]: https://github.com/Automattic/vip-block-data-api/blob/media/example-utility-quote-list.png [media-example-media-text]: https://github.com/Automattic/vip-block-data-api/blob/media/example-media-text.png [media-example-pullquote]: https://github.com/Automattic/vip-block-data-api/blob/media/example-pullquote.png +[media-example-table]: https://github.com/Automattic/vip-block-data-api/blob/media/example-table.png [media-example-utility-quote-list]: https://github.com/Automattic/vip-block-data-api/blob/media/example-list-quote.png [media-plugin-activate]: https://github.com/Automattic/vip-block-data-api/blob/media/plugin-activate.png [media-preact-media-text]: https://github.com/Automattic/vip-block-data-api/blob/media/preact-media-text.png diff --git a/src/graphql/graphql-api.php b/src/graphql/graphql-api.php index 33da566..1a51b2e 100644 --- a/src/graphql/graphql-api.php +++ b/src/graphql/graphql-api.php @@ -25,7 +25,7 @@ public static function init() { } /** - * Extract the blocks data for a post, and return back in the format expected by the graphQL API. + * Extract the blocks data for a post, and return back in the format expected by the GraphQL API. * * @param \WPGraphQL\Model\Post $post_model Post model for post. * @@ -91,12 +91,7 @@ public static function map_attributes( $block ) { unset( $block['attributes'] ); } elseif ( isset( $block['attributes'] ) && ! empty( $block['attributes'] ) ) { $block['attributes'] = array_map( - function ( $name, $value ) { - return [ - 'name' => $name, - 'value' => strval( $value ), - ]; - }, + [ __CLASS__, 'get_block_attribute_pair' ], array_keys( $block['attributes'] ), array_values( $block['attributes'] ) ); @@ -159,14 +154,18 @@ public static function register_types() { [ 'description' => 'Block attribute', 'fields' => [ - 'name' => [ + 'name' => [ 'type' => [ 'non_null' => 'String' ], 'description' => 'Block data attribute name', ], - 'value' => [ + 'value' => [ 'type' => [ 'non_null' => 'String' ], 'description' => 'Block data attribute value', ], + 'isValueJsonEncoded' => [ + 'type' => [ 'non_null' => 'Boolean' ], + 'description' => 'True if value is a complex JSON-encoded field. This is used to encode attribute types like arrays and objects.', + ], ], ], ); @@ -252,6 +251,30 @@ public static function register_types() { ] ); } + + /** + * Given a block attribute name and value, return a BlockAttribute array. + * + * @param string $name The name of the block attribute. + * @param mixed $value The value of the block attribute. + * + * @return array + */ + public static function get_block_attribute_pair( $name, $value ) { + // Unknown array types (table cells, for example) are encoded as JSON strings. + $is_value_json_encoded = false; + + if ( ! is_scalar( $value ) ) { + $value = wp_json_encode( $value ); + $is_value_json_encoded = true; + } + + return [ + 'name' => $name, + 'value' => strval( $value ), + 'isValueJsonEncoded' => $is_value_json_encoded, + ]; + } } GraphQLApi::init(); diff --git a/tests/graphql/test-graphql-api.php b/tests/graphql/test-graphql-api.php index 777cd39..32759a3 100644 --- a/tests/graphql/test-graphql-api.php +++ b/tests/graphql/test-graphql-api.php @@ -25,24 +25,26 @@ public function test_is_graphql_enabled_false() { remove_filter( 'vip_block_data_api__is_graphql_enabled', $is_graphql_enabled_function, 10, 0 ); } + // get_blocks_data() tests + public function test_get_blocks_data() { $html = ' - -

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

- + +

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

+ - -
-

This is a heading inside a quote

- + +
+

This is a heading inside a quote

+ - -
-

This is a heading

-
-
- - '; + +
+

This is a heading

+
+
+ + '; $expected_blocks = [ 'blocks' => [ @@ -50,12 +52,14 @@ public function test_get_blocks_data() { 'name' => 'core/paragraph', 'attributes' => [ [ - 'name' => 'content', - 'value' => 'Welcome to WordPress. This is your first post. Edit or delete it, then start writing!', + 'name' => 'content', + 'value' => 'Welcome to WordPress. This is your first post. Edit or delete it, then start writing!', + 'isValueJsonEncoded' => false, ], [ - 'name' => 'dropCap', - 'value' => '', + 'name' => 'dropCap', + 'value' => '', + 'isValueJsonEncoded' => false, ], ], 'id' => '1', @@ -64,8 +68,9 @@ public function test_get_blocks_data() { 'name' => 'core/quote', 'attributes' => [ [ - 'name' => 'value', - 'value' => '', + 'name' => 'value', + 'value' => '', + 'isValueJsonEncoded' => false, ], ], 'innerBlocks' => [ @@ -73,12 +78,14 @@ public function test_get_blocks_data() { 'name' => 'core/paragraph', 'attributes' => [ [ - 'name' => 'content', - 'value' => 'This is a heading inside a quote', + 'name' => 'content', + 'value' => 'This is a heading inside a quote', + 'isValueJsonEncoded' => false, ], [ - 'name' => 'dropCap', - 'value' => '', + 'name' => 'dropCap', + 'value' => '', + 'isValueJsonEncoded' => false, ], ], 'id' => '3', @@ -87,8 +94,9 @@ public function test_get_blocks_data() { 'name' => 'core/quote', 'attributes' => [ [ - 'name' => 'value', - 'value' => '', + 'name' => 'value', + 'value' => '', + 'isValueJsonEncoded' => false, ], ], 'innerBlocks' => [ @@ -98,10 +106,12 @@ public function test_get_blocks_data() { [ 'name' => 'content', 'value' => 'This is a heading', + 'isValueJsonEncoded' => false, ], [ 'name' => 'level', 'value' => '2', + 'isValueJsonEncoded' => false, ], ], 'id' => '5', @@ -124,6 +134,80 @@ public function test_get_blocks_data() { $this->assertEquals( $expected_blocks, $blocks_data ); } + public function test_array_data_in_attribute() { + $html = ' + +
+ + + + + + + + + + + + + + + + + + + + + + + +
Header AHeader B
Value AValue B
Value CValue D
Footer AFooter B
+
+ + '; + + $expected_blocks = [ + 'blocks' => [ + [ + 'name' => 'core/table', + 'attributes' => [ + [ + 'name' => 'hasFixedLayout', + 'value' => false, + 'isValueJsonEncoded' => false, + ], + [ + 'name' => 'head', + 'value' => '[{"cells":[{"content":"Header A","tag":"th"},{"content":"Header B","tag":"th"}]}]', + 'isValueJsonEncoded' => true, + ], + [ + 'name' => 'body', + 'value' => '[{"cells":[{"content":"Value A","tag":"td"},{"content":"Value B","tag":"td"}]},{"cells":[{"content":"Value C","tag":"td"},{"content":"Value D","tag":"td"}]}]', + 'isValueJsonEncoded' => true, + ], + [ + 'name' => 'foot', + 'value' => '[{"cells":[{"content":"Footer A","tag":"td"},{"content":"Footer B","tag":"td"}]}]', + 'isValueJsonEncoded' => true, + ], + ], + 'id' => '6', + ], + ], + ]; + + $post = $this->factory()->post->create_and_get( [ + 'post_content' => $html, + ] ); + + $blocks_data = GraphQLApi::get_blocks_data( $post ); + + $this->assertEquals( $expected_blocks, $blocks_data ); + } + + // flatten_inner_blocks() tests + public function test_flatten_inner_blocks() { $inner_blocks = [ [ diff --git a/vip-block-data-api.php b/vip-block-data-api.php index 21246ea..07d7e43 100644 --- a/vip-block-data-api.php +++ b/vip-block-data-api.php @@ -5,7 +5,7 @@ * Description: Access Gutenberg block data in JSON via the REST API. * Author: WordPress VIP * Text Domain: vip-block-data-api - * Version: 1.2.2 + * Version: 1.2.3 * Requires at least: 5.9.0 * Tested up to: 6.4 * Requires PHP: 8.0 @@ -20,7 +20,7 @@ if ( ! defined( 'VIP_BLOCK_DATA_API_LOADED' ) ) { define( 'VIP_BLOCK_DATA_API_LOADED', true ); - define( 'WPCOMVIP__BLOCK_DATA_API__PLUGIN_VERSION', '1.2.2' ); + define( 'WPCOMVIP__BLOCK_DATA_API__PLUGIN_VERSION', '1.2.3' ); define( 'WPCOMVIP__BLOCK_DATA_API__REST_ROUTE', 'vip-block-data-api/v1' ); // Analytics related configs.