diff --git a/example/rest-api/art-institute/art-institute.php b/example/rest-api/art-institute/art-institute.php
index d854c407d..d0024065d 100644
--- a/example/rest-api/art-institute/art-institute.php
+++ b/example/rest-api/art-institute/art-institute.php
@@ -156,16 +156,55 @@ function register_aic_block(): void {
],
]);
+ $collection_query = HttpQuery::from_array([
+ 'data_source' => $aic_data_source,
+ 'endpoint' => function ( array $input_variables ) use ( $aic_data_source ): string {
+ $endpoint = $aic_data_source->get_endpoint();
+ return add_query_arg( [
+ 'limit' => 10,
+ 'fields' => 'id,title,image_id,artist_title',
+ ], $endpoint );
+ },
+ 'output_schema' => [
+ 'is_collection' => true,
+ 'path' => '$.data[*]',
+ 'type' => [
+ 'id' => [
+ 'name' => 'Art ID',
+ 'type' => 'id',
+ ],
+ 'artist_title' => [
+ 'name' => 'Artist Title',
+ 'type' => 'string',
+ 'path' => '$.artist_title',
+ ],
+ 'title' => [
+ 'name' => 'Title',
+ 'type' => 'string',
+ 'path' => '$.title',
+ ],
+ 'image_url' => [
+ 'name' => 'Image URL',
+ 'generate' => function ( $data ): string {
+ return 'https://www.artic.edu/iiif/2/' . $data['image_id'] . '/full/843,/0/default.jpg';
+ },
+ 'type' => 'image_url',
+ ],
+ ],
+ ],
+ ]);
+
register_remote_data_block([
'title' => 'Art Institute of Chicago',
'icon' => 'art',
- 'render_query' => [
- 'query' => $get_art_query,
+ 'queries' => [
+ 'display' => $get_art_query,
+ 'collection' => $collection_query,
+ 'search' => $search_art_query,
],
- 'selection_queries' => [
- [
- 'query' => $search_art_query,
- 'type' => 'search',
+ 'query_configurations' => [
+ 'display' => [
+ 'source_query' => 'search',
],
],
]);
diff --git a/inc/Editor/BlockManagement/ConfigRegistry.php b/inc/Editor/BlockManagement/ConfigRegistry.php
index c10f2ffea..d9e83782e 100644
--- a/inc/Editor/BlockManagement/ConfigRegistry.php
+++ b/inc/Editor/BlockManagement/ConfigRegistry.php
@@ -25,6 +25,7 @@ class ConfigRegistry {
public const DISPLAY_QUERY_KEY = 'display';
public const LIST_QUERY_KEY = 'list';
public const SEARCH_QUERY_KEY = 'search';
+ public const COLLECTION_QUERY_KEY = 'collection';
public static function init( ?LoggerInterface $logger = null ): void {
self::$logger = $logger ?? LoggerManager::instance();
@@ -48,33 +49,37 @@ public static function register_block( array $user_config = [] ): bool|WP_Error
return self::create_error( $block_title, sprintf( 'Block %s has already been registered', $block_name ) );
}
- $display_query = self::inflate_query( $user_config[ self::RENDER_QUERY_KEY ]['query'] );
- $input_schema = $display_query->get_input_schema();
+ // Get the display query (always use 'display' key)
+ if (!isset($user_config['queries']['display'])) {
+ return self::create_error($block_title, 'No display query found in queries (must use key "display")');
+ }
+ $display_query = self::inflate_query($user_config['queries']['display']);
+
+ // Initialize queries array with display query
+ $queries = [
+ self::DISPLAY_QUERY_KEY => $display_query,
+ ];
- // Build the base configuration for the block. This is our own internal
- // configuration, not what will be passed to WordPress's register_block_type.
- // @see BlockRegistration::register_block_type::register_blocks.
+ // Build the base configuration
$config = [
'description' => '',
'icon' => $user_config['icon'] ?? 'cloud',
'name' => $block_name,
- 'loop' => $user_config[ self::RENDER_QUERY_KEY ]['loop'] ?? false,
+ 'loop' => $user_config['loop'] ?? false,
'overrides' => $user_config['overrides'] ?? [],
'patterns' => [],
- 'queries' => [
- self::DISPLAY_QUERY_KEY => $display_query,
- ],
+ 'queries' => $queries,
'selectors' => [
[
'image_url' => $display_query->get_image_url(),
- 'inputs' => array_map( function ( $slug, $input_var ) {
+ 'inputs' => array_map(function ($slug, $input_var) {
return [
'name' => $input_var['name'] ?? $slug,
'required' => $input_var['required'] ?? true,
'slug' => $slug,
'type' => $input_var['type'] ?? 'string',
];
- }, array_keys( $input_schema ), array_values( $input_schema ) ),
+ }, array_keys($display_query->get_input_schema()), array_values($display_query->get_input_schema())),
'name' => 'Manual input',
'query_key' => self::DISPLAY_QUERY_KEY,
'type' => 'input',
@@ -83,71 +88,82 @@ public static function register_block( array $user_config = [] ): bool|WP_Error
'title' => $block_title,
];
- // Register "selectors" which allow the user to use a query to assist in
- // selecting data for display by the block.
- foreach ( $user_config[ self::SELECTION_QUERIES_KEY ] ?? [] as $selection_query ) {
- $from_query = self::inflate_query( $selection_query['query'] );
- $from_query_type = $selection_query['type'];
- $to_query = $display_query;
-
- $config['queries'][ $from_query::class ] = $from_query;
-
- $from_input_schema = $from_query->get_input_schema();
- $from_output_schema = $from_query->get_output_schema();
-
- foreach ( array_keys( $to_query->get_input_schema() ) as $to ) {
- if ( ! isset( $from_output_schema['type'][ $to ] ) ) {
- return self::create_error( $block_title, sprintf( 'Cannot map key "%1$s" from %2$s query. The display query for this block requires a "%1$s" key as an input, but it is not present in the output schema for the %2$s query. Try adding a "%1$s" mapping to the output schema for the %2$s query.', esc_html( $to ), $from_query_type ) );
- }
+ // Add other queries and create selectors based on query_configurations
+ foreach ($user_config['queries'] as $key => $query) {
+ if ($key === 'display') {
+ continue;
}
- if ( self::SEARCH_QUERY_KEY === $from_query_type ) {
- $search_input_count = count( array_filter( $from_input_schema, function ( array $input_var ): bool {
- return 'ui:search_input' === $input_var['type'];
- } ) );
-
- if ( 1 !== $search_input_count ) {
- return self::create_error( $block_title, 'A search query must have one input variable with type "ui:search_input"' );
+ $query = self::inflate_query($query);
+ $queries[$key] = $query;
+
+ // Check if this query is configured as a source for another query
+ $is_source_query = false;
+ foreach ($user_config['query_configurations'] ?? [] as $target_key => $target_config) {
+ if ($target_config['source_query'] === $key) {
+ $is_source_query = true;
+ array_unshift(
+ $config['selectors'],
+ [
+ 'image_url' => $query->get_image_url(),
+ 'inputs' => array_map(function ($slug, $input_var) {
+ return [
+ 'name' => $input_var['name'] ?? $slug,
+ 'required' => $input_var['required'] ?? false,
+ 'slug' => $slug,
+ 'type' => $input_var['type'] ?? 'string',
+ ];
+ }, array_keys($query->get_input_schema()), array_values($query->get_input_schema())),
+ 'name' => ucfirst($key),
+ 'query_key' => $key,
+ 'type' => 'search',
+ ]
+ );
+ break;
}
}
- // Add the selector to the configuration.
- array_unshift(
- $config['selectors'],
- [
- 'image_url' => $from_query->get_image_url(),
- 'inputs' => array_map( function ( $slug, $input_var ) {
- return [
- 'name' => $input_var['name'] ?? $slug,
- 'required' => $input_var['required'] ?? false,
- 'slug' => $slug,
- 'type' => $input_var['type'] ?? 'string',
- ];
- }, array_keys( $from_input_schema ), array_values( $from_input_schema ) ),
- 'name' => $selection_query['display_name'] ?? ucfirst( $from_query_type ),
- 'query_key' => $from_query::class,
- 'type' => $from_query_type,
- ]
- );
+ // If not a source query and it's a collection query, add it as collection type
+ if (!$is_source_query && $key === 'collection') {
+ array_unshift(
+ $config['selectors'],
+ [
+ 'image_url' => $query->get_image_url(),
+ 'inputs' => array_map(function ($slug, $input_var) {
+ return [
+ 'name' => $input_var['name'] ?? $slug,
+ 'required' => $input_var['required'] ?? false,
+ 'slug' => $slug,
+ 'type' => $input_var['type'] ?? 'string',
+ ];
+ }, array_keys($query->get_input_schema()), array_values($query->get_input_schema())),
+ 'name' => 'Collection',
+ 'query_key' => $key,
+ 'type' => 'collection',
+ ]
+ );
+ }
}
+ // Set the queries on the config
+ $config['queries'] = $queries;
+
// Register patterns which can be used with the block.
- foreach ( $user_config['patterns'] ?? [] as $pattern ) {
- $parsed_blocks = parse_blocks( $pattern['html'] );
- $parsed_blocks = BlockPatterns::add_block_arg_to_bindings( $block_name, $parsed_blocks );
- $pattern_content = serialize_blocks( $parsed_blocks );
+ foreach ($user_config['patterns'] ?? [] as $pattern) {
+ $parsed_blocks = parse_blocks($pattern['html']);
+ $parsed_blocks = BlockPatterns::add_block_arg_to_bindings($block_name, $parsed_blocks);
+ $pattern_content = serialize_blocks($parsed_blocks);
- $pattern_name = self::register_block_pattern( $block_name, $pattern['title'], $pattern_content );
+ $pattern_name = self::register_block_pattern($block_name, $pattern['title'], $pattern_content);
// If the pattern role is specified and recognized, add it to the block configuration.
- $recognized_roles = [ 'inner_blocks' ];
- if ( isset( $pattern['role'] ) && in_array( $pattern['role'], $recognized_roles, true ) ) {
- $config['patterns'][ $pattern['role'] ] = $pattern_name;
+ $recognized_roles = ['inner_blocks'];
+ if (isset($pattern['role']) && in_array($pattern['role'], $recognized_roles, true)) {
+ $config['patterns'][$pattern['role']] = $pattern_name;
}
}
- ConfigStore::set_block_configuration( $block_name, $config );
-
+ ConfigStore::set_block_configuration($block_name, $config);
return true;
}
@@ -184,4 +200,31 @@ private static function inflate_query( array|QueryInterface $config ): QueryInte
return $config;
}
+
+ // Create a selector for a query
+ private static function create_selector(
+ QueryInterface $query,
+ string $query_key,
+ string $type,
+ ?string $display_name = null
+ ): array {
+ $input_schema = $query->get_input_schema();
+
+ // Convert object input schema to array format
+ $inputs = is_array($input_schema) ? array_map(
+ function($key, $schema) {
+ return array_merge(['slug' => $key], $schema);
+ },
+ array_keys($input_schema),
+ array_values($input_schema)
+ ) : [];
+
+ return [
+ 'query_key' => $query_key,
+ 'type' => $type,
+ 'name' => $display_name ?? ucfirst($type),
+ 'inputs' => $inputs,
+ 'image_url' => null,
+ ];
+ }
}
diff --git a/inc/ExampleApi/ExampleApi.php b/inc/ExampleApi/ExampleApi.php
index 4f297326e..8831e78db 100644
--- a/inc/ExampleApi/ExampleApi.php
+++ b/inc/ExampleApi/ExampleApi.php
@@ -114,17 +114,17 @@ public static function register_remote_data_block(): void {
'query_runner' => new ExampleApiQueryRunner(),
] );
- register_remote_data_block( [
+ register_remote_data_block([
'title' => self::$block_title,
- 'render_query' => [
- 'query' => $get_record_query,
+ 'queries' => [
+ 'display' => $get_record_query,
+ 'list' => $get_table_query,
],
- 'selection_queries' => [
- [
- 'query' => $get_table_query,
- 'type' => 'list',
+ 'query_configurations' => [
+ 'display' => [
+ 'source_query' => 'list',
],
],
- ] );
+ ]);
}
}
diff --git a/inc/Integrations/Airtable/AirtableIntegration.php b/inc/Integrations/Airtable/AirtableIntegration.php
index 5c53f2fc3..aa9e2830d 100644
--- a/inc/Integrations/Airtable/AirtableIntegration.php
+++ b/inc/Integrations/Airtable/AirtableIntegration.php
@@ -34,27 +34,27 @@ public static function register_blocks_for_airtable_data_source(
$tables = $data_source->to_array()['service_config']['tables'];
foreach ( $tables as $table ) {
- $query = self::get_query( $data_source, $table );
- $list_query = self::get_list_query( $data_source, $table );
-
- register_remote_data_block(
- array_merge(
- [
- 'title' => $data_source->get_display_name() . '/' . $table['name'],
- 'icon' => 'editor-table',
- 'render_query' => [
- 'query' => $query,
- ],
- 'selection_queries' => [
- [
- 'query' => $list_query,
- 'type' => 'list',
- ],
+ $query = self::get_query( $data_source, $table );
+ $list_query = self::get_list_query( $data_source, $table );
+
+ register_remote_data_block(
+ array_merge(
+ [
+ 'title' => $data_source->get_display_name() . '/' . $table['name'],
+ 'icon' => 'editor-table',
+ 'queries' => [
+ 'display' => $query,
+ 'list' => $list_query,
+ ],
+ 'query_configurations' => [
+ 'display' => [
+ 'source_query' => 'list',
],
],
- $block_overrides
- )
- );
+ ],
+ $block_overrides
+ )
+ );
}
}
@@ -71,10 +71,10 @@ public static function register_loop_blocks_for_airtable_data_source(
array_merge(
[
'title' => sprintf( '%s/%s Loop', $data_source->get_display_name(), $table['name'] ),
- 'render_query' => [
- 'loop' => true,
- 'query' => $list_query,
+ 'queries' => [
+ 'display' => $list_query,
],
+ 'loop' => true,
],
$block_overrides
)
diff --git a/inc/Integrations/Google/Sheets/GoogleSheetsIntegration.php b/inc/Integrations/Google/Sheets/GoogleSheetsIntegration.php
index 4828b05f3..5ed015af3 100644
--- a/inc/Integrations/Google/Sheets/GoogleSheetsIntegration.php
+++ b/inc/Integrations/Google/Sheets/GoogleSheetsIntegration.php
@@ -42,13 +42,13 @@ public static function register_blocks_for_google_sheets_data_source(
[
'title' => $data_source->get_display_name() . '/' . $sheet['name'],
'icon' => 'media-spreadsheet',
- 'render_query' => [
- 'query' => $query,
+ 'queries' => [
+ 'display' => $query,
+ 'list' => $list_query,
],
- 'selection_queries' => [
- [
- 'query' => $list_query,
- 'type' => 'list',
+ 'query_configurations' => [
+ 'display' => [
+ 'source_query' => 'list',
],
],
],
@@ -71,10 +71,10 @@ public static function register_loop_blocks_for_google_sheets_data_source(
array_merge(
[
'title' => sprintf( '%s/%s Loop', $data_source->get_display_name(), $sheet['name'] ),
- 'render_query' => [
- 'loop' => true,
- 'query' => $list_query,
+ 'queries' => [
+ 'display' => $list_query,
],
+ 'loop' => true,
],
$block_overrides
)
diff --git a/inc/Integrations/SalesforceD2C/SalesforceD2CIntegration.php b/inc/Integrations/SalesforceD2C/SalesforceD2CIntegration.php
index bdb5e24bf..e3051ed9d 100644
--- a/inc/Integrations/SalesforceD2C/SalesforceD2CIntegration.php
+++ b/inc/Integrations/SalesforceD2C/SalesforceD2CIntegration.php
@@ -148,30 +148,28 @@ private static function get_queries( SalesforceD2CDataSource $data_source ): arr
];
}
- public static function register_blocks_for_salesforce_data_source( SalesforceD2CDataSource $data_source ): void {
- $queries = self::get_queries( $data_source );
-
- register_remote_data_block(
- [
- 'title' => $data_source->get_display_name(),
- 'icon' => 'money-alt',
- 'render_query' => [
- 'query' => $queries['display'],
+ public static function register_blocks_for_salesforce_data_source(SalesforceD2CDataSource $data_source): void {
+ $queries = self::get_queries($data_source);
+
+ register_remote_data_block([
+ 'title' => $data_source->get_display_name(),
+ 'icon' => 'money-alt',
+ 'queries' => [
+ 'display' => $queries['display'],
+ 'search' => $queries['search'],
+ ],
+ 'query_configurations' => [
+ 'display' => [
+ 'source_query' => 'search',
],
- 'selection_queries' => [
- [
- 'query' => $queries['search'],
- 'type' => 'search',
- ],
- ],
- 'overrides' => [
- [
- 'display_name' => 'Use Salesforce product from URL',
- 'name' => 'salesforce_sku',
- ],
+ ],
+ 'overrides' => [
+ [
+ 'display_name' => 'Use Salesforce product from URL',
+ 'name' => 'salesforce_sku',
],
- ]
- );
+ ],
+ ]);
add_filter( 'query_vars', function ( array $query_vars ): array {
$query_vars[] = 'sku';
diff --git a/inc/Integrations/Shopify/ShopifyIntegration.php b/inc/Integrations/Shopify/ShopifyIntegration.php
index 619524b15..477c82c53 100644
--- a/inc/Integrations/Shopify/ShopifyIntegration.php
+++ b/inc/Integrations/Shopify/ShopifyIntegration.php
@@ -155,13 +155,13 @@ public static function register_blocks_for_shopify_data_source( ShopifyDataSourc
register_remote_data_block( [
'title' => $block_title,
'icon' => 'cart',
- 'render_query' => [
- 'query' => $queries['shopify_get_product'],
+ 'queries' => [
+ 'display' => $queries['shopify_get_product'],
+ 'search' => $queries['shopify_search_products'],
],
- 'selection_queries' => [
- [
- 'query' => $queries['shopify_search_products'],
- 'type' => 'search',
+ 'query_configurations' => [
+ 'display' => [
+ 'source_query' => 'search',
],
],
'patterns' => [
diff --git a/inc/Validation/ConfigSchemas.php b/inc/Validation/ConfigSchemas.php
index 4b309bf7a..b10499d28 100644
--- a/inc/Validation/ConfigSchemas.php
+++ b/inc/Validation/ConfigSchemas.php
@@ -85,28 +85,22 @@ private static function generate_remote_data_block_config_schema(): array {
] )
)
),
- 'render_query' => Types::object( [
- 'query' => Types::one_of(
+ 'queries' => Types::record(
+ Types::string(),
+ Types::one_of(
Types::instance_of( QueryInterface::class ),
Types::serialized_config_for( HttpQueryInterface::class ),
- ),
- 'loop' => Types::nullable( Types::boolean() ),
- ] ),
- 'selection_queries' => Types::nullable(
- Types::list_of(
+ )
+ ),
+ 'query_configurations' => Types::nullable(
+ Types::record(
+ Types::string(),
Types::object( [
- 'display_name' => Types::nullable( Types::string() ),
- 'query' => Types::one_of(
- Types::instance_of( QueryInterface::class ),
- Types::serialized_config_for( HttpQueryInterface::class ),
- ),
- 'type' => Types::enum(
- ConfigRegistry::LIST_QUERY_KEY,
- ConfigRegistry::SEARCH_QUERY_KEY
- ),
+ 'source_query' => Types::string(),
] )
)
),
+ 'loop' => Types::nullable( Types::boolean() ),
'overrides' => Types::nullable(
Types::list_of(
Types::object( [
diff --git a/src/blocks/remote-data-container/components/InnerBlocks.tsx b/src/blocks/remote-data-container/components/InnerBlocks.tsx
index c0b53f4ff..62f30077b 100644
--- a/src/blocks/remote-data-container/components/InnerBlocks.tsx
+++ b/src/blocks/remote-data-container/components/InnerBlocks.tsx
@@ -13,12 +13,17 @@ interface InnerBlocksProps {
export function InnerBlocks( props: InnerBlocksProps ) {
const {
- blockConfig: { loop },
+ blockConfig: { loop, selectors },
getInnerBlocks,
remoteData,
} = props;
- if ( loop || remoteData.results.length > 1 ) {
+ // Use loop template for both loop blocks, multi-selection, or collection queries
+ if (
+ loop ||
+ remoteData.results.length > 1 ||
+ selectors.some( selector => selector.type === 'collection' )
+ ) {
return ;
}
diff --git a/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx b/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx
index b5a7c33dc..2178780d5 100644
--- a/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx
+++ b/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx
@@ -1,4 +1,4 @@
-import { ButtonGroup } from '@wordpress/components';
+import { Button, ButtonGroup } from '@wordpress/components';
import { InputModal } from '../modals/InputModal';
import { InputPopover } from '../popovers/InputPopover';
@@ -38,6 +38,18 @@ export function ItemSelectQueryType( props: ItemSelectQueryTypeProps ) {
{ ...selectorProps }
/>
);
+ case 'collection':
+ return (
+
+ );
case 'input':
return selector.inputs.length === 1 && selector.inputs[ 0 ] ? (