diff --git a/example/rest-api/art-institute/art-institute.php b/example/rest-api/art-institute/art-institute.php index c8f3202c..217d9be7 100644 --- a/example/rest-api/art-institute/art-institute.php +++ b/example/rest-api/art-institute/art-institute.php @@ -32,18 +32,18 @@ function register_aic_block(): void { 'data_source' => $aic_data_source, 'endpoint' => function ( array $input_variables ) use ( $aic_data_source ): string { $endpoint = $aic_data_source->get_endpoint(); - + // Get and clean IDs from comma-separated string $ids = array_filter( array_map( 'trim', explode( ',', (string) $input_variables['id'] ) ), 'strlen' ); - - if ( !empty( $ids ) ) { + + if ( ! empty( $ids ) ) { return add_query_arg([ 'ids' => implode( ',', $ids ), 'fields' => 'id,title,image_id,artist_title', - ], $endpoint); + ], $endpoint ); } return $endpoint; @@ -53,6 +53,7 @@ function register_aic_block(): void { 'name' => 'Art ID', 'type' => 'id', 'supports_bulk' => true, + 'required' => false, ], ], 'output_schema' => [ @@ -157,11 +158,56 @@ 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, + 'additional_queries' => [ + [ + 'query' => $collection_query, + 'type' => 'collection', + 'display_name' => 'Collection', + ], + ], ], 'selection_queries' => [ [ diff --git a/inc/Editor/BlockManagement/ConfigRegistry.php b/inc/Editor/BlockManagement/ConfigRegistry.php index 312234dd..86880cf8 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,7 +49,21 @@ 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 ) ); } + // Process default render query first $display_query = self::inflate_query( $user_config[ self::RENDER_QUERY_KEY ]['query'] ); + + // Initialize queries array with display query as default + $queries = [ + self::DISPLAY_QUERY_KEY => $display_query, + ]; + + // Process additional render queries if present + if ( isset( $user_config[ self::RENDER_QUERY_KEY ]['additional_queries'] ) ) { + foreach ( $user_config[ self::RENDER_QUERY_KEY ]['additional_queries'] as $query_config ) { + $queries[ $query_config['type'] ] = self::inflate_query( $query_config['query'] ); + } + } + $input_schema = $display_query->get_input_schema(); // Check if render query has any bulk-supporting inputs @@ -81,29 +96,35 @@ function ( $input ) { 'loop' => $user_config[ self::RENDER_QUERY_KEY ]['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 ) { - return [ - 'name' => $input_var['name'] ?? $slug, - 'required' => $input_var['required'] ?? true, - 'slug' => $slug, - 'type' => $input_var['type'] ?? 'string', - 'supports_bulk' => $input_var['supports_bulk'] ?? false, - ]; - }, array_keys( $input_schema ), array_values( $input_schema ) ), - 'name' => 'Manual input', - 'query_key' => self::DISPLAY_QUERY_KEY, - 'type' => 'input', - ], + self::create_selector( + $display_query, + self::DISPLAY_QUERY_KEY, + 'input', + 'Manual input', + $has_bulk_support + ), ], 'title' => $block_title, ]; + // Add collection queries to selectors if present + if ( isset( $user_config[ self::RENDER_QUERY_KEY ]['additional_queries'] ) ) { + foreach ( $user_config[ self::RENDER_QUERY_KEY ]['additional_queries'] as $query_config ) { + array_unshift( + $config['selectors'], + self::create_selector( + $queries[ $query_config['type'] ], + $query_config['type'], + $query_config['type'], + $query_config['display_name'] ?? null, + $has_bulk_support + ) + ); + } + } + // 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 ) { @@ -135,21 +156,13 @@ function ( $input ) { // 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, - 'supports_bulk' => $has_bulk_support, - 'type' => $from_query_type, - ] + self::create_selector( + $from_query, + $from_query::class, + $from_query_type, + $selection_query['display_name'] ?? null, + $has_bulk_support + ) ); } @@ -206,4 +219,30 @@ 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, + bool $supports_bulk = false + ): array { + return [ + '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', + 'supports_bulk' => $input_var['supports_bulk'] ?? false, + ]; + }, array_keys( $query->get_input_schema() ), array_values( $query->get_input_schema() ) ), + 'name' => $display_name ?? ucfirst( $type ), + 'query_key' => $query_key, + 'type' => $type, + 'supports_bulk' => $supports_bulk, + ]; + } } diff --git a/inc/Validation/ConfigSchemas.php b/inc/Validation/ConfigSchemas.php index cc8e3106..05898ab9 100644 --- a/inc/Validation/ConfigSchemas.php +++ b/inc/Validation/ConfigSchemas.php @@ -80,6 +80,20 @@ private static function generate_remote_data_block_config_schema(): array { Types::instance_of( QueryInterface::class ), Types::serialized_config_for( HttpQueryInterface::class ), ), + 'additional_queries' => Types::nullable( + Types::list_of( + 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::COLLECTION_QUERY_KEY, + ), + ] ) + ) + ), 'loop' => Types::nullable( Types::boolean() ), ] ), 'selection_queries' => Types::nullable( diff --git a/src/blocks/remote-data-container/components/InnerBlocks.tsx b/src/blocks/remote-data-container/components/InnerBlocks.tsx index 03acaaf7..62f30077 100644 --- a/src/blocks/remote-data-container/components/InnerBlocks.tsx +++ b/src/blocks/remote-data-container/components/InnerBlocks.tsx @@ -5,18 +5,25 @@ import { LoopTemplate } from '@/blocks/remote-data-container/components/loop-tem interface InnerBlocksProps { blockConfig: BlockConfig; - getInnerBlocks: ( result: RemoteDataResult ) => BlockInstance< RemoteDataInnerBlockAttributes >[]; + getInnerBlocks: ( + result: RemoteDataApiResult + ) => BlockInstance< RemoteDataInnerBlockAttributes >[]; remoteData: RemoteData; } 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 898f1bec..b1e77add 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,21 @@ export function ItemSelectQueryType( props: ItemSelectQueryTypeProps ) { { ...selectorProps } /> ); + case 'collection': + return ( + + ); case 'input': return selector.inputs.length === 1 && selector.inputs[ 0 ] ? (