Skip to content

Commit

Permalink
Merge pull request #61 from Automattic/fix/wpgraphql-table-array-values
Browse files Browse the repository at this point in the history
Fix "Array" values in GraphQL block array data
  • Loading branch information
ingeniumed authored May 9, 2024
2 parents b0fc4b7 + c6e265a commit 6ece52e
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 37 deletions.
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -1405,13 +1485,15 @@ composer run test
<!-- Links -->
[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
[media-example-heading-paragraph]: https://github.com/Automattic/vip-block-data-api/blob/media/example-header-paragraph.png
[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
Expand Down
41 changes: 32 additions & 9 deletions src/graphql/graphql-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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'] )
);
Expand Down Expand Up @@ -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.',
],
],
],
);
Expand Down Expand Up @@ -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();
136 changes: 110 additions & 26 deletions tests/graphql/test-graphql-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,37 +25,41 @@ 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 = '
<!-- wp:paragraph -->
<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>
<!-- /wp:paragraph -->
<!-- wp:quote -->
<blockquote class="wp-block-quote"><!-- wp:paragraph -->
<p>This is a heading inside a quote</p>
<!-- /wp:paragraph -->
<!-- wp:quote -->
<blockquote class="wp-block-quote"><!-- wp:paragraph -->
<p>This is a heading inside a quote</p>
<!-- /wp:paragraph -->
<!-- wp:quote -->
<blockquote class="wp-block-quote"><!-- wp:heading -->
<h2 class="wp-block-heading">This is a heading</h2>
<!-- /wp:heading --></blockquote>
<!-- /wp:quote --></blockquote>
<!-- /wp:quote -->
';
<!-- wp:quote -->
<blockquote class="wp-block-quote"><!-- wp:heading -->
<h2 class="wp-block-heading">This is a heading</h2>
<!-- /wp:heading --></blockquote>
<!-- /wp:quote --></blockquote>
<!-- /wp:quote -->
';

$expected_blocks = [
'blocks' => [
[
'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',
Expand All @@ -64,21 +68,24 @@ public function test_get_blocks_data() {
'name' => 'core/quote',
'attributes' => [
[
'name' => 'value',
'value' => '',
'name' => 'value',
'value' => '',
'isValueJsonEncoded' => false,
],
],
'innerBlocks' => [
[
'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',
Expand All @@ -87,8 +94,9 @@ public function test_get_blocks_data() {
'name' => 'core/quote',
'attributes' => [
[
'name' => 'value',
'value' => '',
'name' => 'value',
'value' => '',
'isValueJsonEncoded' => false,
],
],
'innerBlocks' => [
Expand All @@ -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',
Expand All @@ -124,6 +134,80 @@ public function test_get_blocks_data() {
$this->assertEquals( $expected_blocks, $blocks_data );
}

public function test_array_data_in_attribute() {
$html = '
<!-- wp:table -->
<figure class="wp-block-table">
<table>
<thead>
<tr>
<th>Header A</th>
<th>Header B</th>
</tr>
</thead>
<tbody>
<tr>
<td>Value A</td>
<td>Value B</td>
</tr>
<tr>
<td>Value C</td>
<td>Value D</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>Footer A</td>
<td>Footer B</td>
</tr>
</tfoot>
</table>
</figure>
<!-- /wp:table -->
';

$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 = [
[
Expand Down
4 changes: 2 additions & 2 deletions vip-block-data-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down

0 comments on commit 6ece52e

Please sign in to comment.