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 ] ? (