diff --git a/inc/Config/Block/RemoteDataBlock.php b/inc/Config/Block/RemoteDataBlock.php new file mode 100644 index 000000000..314ad5b2d --- /dev/null +++ b/inc/Config/Block/RemoteDataBlock.php @@ -0,0 +1,62 @@ + 'Display', + 'query_key' => ConfigRegistry::DEPRECATED_DISPLAY_QUERY_KEY, + ]; + unset( $config[ self::DEPRECATED_RENDER_QUERY_KEY ] ); + } + + if ( isset( $config[ self::DEPRECATED_SELECTION_QUERIES_KEY ] ) ) { + // Get the selection queries, inflate them, and add them to the queries array using the type as the key. + foreach ( $config[ self::DEPRECATED_SELECTION_QUERIES_KEY ] as $selection_query ) { + $queries[ $selection_query['type'] ] = $selection_query['query']; + } + + unset( $config[ self::DEPRECATED_SELECTION_QUERIES_KEY ] ); + } + + // Set queries. + $config[ ConfigRegistry::QUERIES_KEY ] = $queries; + $config[ ConfigRegistry::PLACEHOLDERS_KEY ] = $placeholders; + + return $config; + } +} diff --git a/inc/Editor/BlockManagement/BlockRegistration.php b/inc/Editor/BlockManagement/BlockRegistration.php index 7519b4ca9..25ce6d8ec 100644 --- a/inc/Editor/BlockManagement/BlockRegistration.php +++ b/inc/Editor/BlockManagement/BlockRegistration.php @@ -8,6 +8,7 @@ use RemoteDataBlocks\Telemetry\Telemetry; use RemoteDataBlocks\Editor\BlockPatterns\BlockPatterns; use RemoteDataBlocks\REST\RemoteDataController; + use function register_block_type; class BlockRegistration { @@ -83,24 +84,40 @@ public static function register_block_configuration( array $config ): array { $block_path = REMOTE_DATA_BLOCKS__PLUGIN_DIRECTORY . '/build/blocks/remote-data-container'; // Set available bindings from the display query output mappings. - $available_bindings = []; - $output_schema = $config['queries'][ ConfigRegistry::DISPLAY_QUERY_KEY ]->get_output_schema(); - foreach ( $output_schema['type'] ?? [] as $key => $mapping ) { - $available_bindings[ $key ] = [ - 'name' => $mapping['name'], - 'type' => $mapping['type'], - ]; + $available_bindings_for_queries = []; + $display_queries_to_selectors = $config['display_queries_to_selectors'] ?? []; + + $patterns = $config['patterns'] ?? []; + + // Using array_keys here triggers a psalm error, so it's set to $_ instead. + // Supressing the psalm error is a not a good idea, so instead this is the better solution. + // ToDo: Fix the psalm error, and see if array_keys could be used here again. + foreach ( $display_queries_to_selectors as $display_query_key => $_ ) { + $display_query = $config['queries'][ $display_query_key ]; + $available_bindings_for_query = []; + $output_schema = $display_query->get_output_schema(); + foreach ( $output_schema['type'] ?? [] as $key => $mapping ) { + $available_bindings_for_query[ $key ] = [ + 'name' => $mapping['name'], + 'type' => $mapping['type'], + ]; + } + + $available_bindings_for_queries[ $display_query_key ] = $available_bindings_for_query; + + $default_pattern_name = BlockPatterns::register_default_block_pattern( $block_name, $config['title'], $display_query_key, $display_query ); + $patterns[ $display_query_key ] = $default_pattern_name; } // Create the localized data that will be used by our block editor script. $block_config = [ - 'availableBindings' => $available_bindings, + 'availableBindings' => $available_bindings_for_queries, 'availableOverrides' => $config['overrides'] ?? [], 'instructions' => $config['instructions'], 'name' => $block_name, 'dataSourceType' => ConfigStore::get_data_source_type( $block_name ), - 'patterns' => $config['patterns'], - 'selectors' => $config['selectors'], + 'patterns' => $patterns, + 'displayQueriesToSelectors' => $display_queries_to_selectors, 'settings' => [ 'category' => self::$block_category['slug'], 'icon' => $config['icon'] ?? 'cloud', @@ -117,10 +134,6 @@ public static function register_block_configuration( array $config ): array { $script_handle = $block_type->editor_script_handles[0] ?? ''; - // Register a default pattern that simply displays the available data. - $default_pattern_name = BlockPatterns::register_default_block_pattern( $block_name, $config['title'], $config['queries'][ ConfigRegistry::DISPLAY_QUERY_KEY ] ); - $block_config['patterns']['default'] = $default_pattern_name; - return [ $block_config, $script_handle ]; } } diff --git a/inc/Editor/BlockManagement/ConfigRegistry.php b/inc/Editor/BlockManagement/ConfigRegistry.php index 59b05a691..d747d5abe 100644 --- a/inc/Editor/BlockManagement/ConfigRegistry.php +++ b/inc/Editor/BlockManagement/ConfigRegistry.php @@ -4,13 +4,12 @@ defined( 'ABSPATH' ) || exit(); +use RemoteDataBlocks\Config\Block\RemoteDataBlock; use RemoteDataBlocks\Logging\Logger; use RemoteDataBlocks\Config\Query\HttpQuery; use RemoteDataBlocks\Config\Query\QueryInterface; use RemoteDataBlocks\Editor\BlockPatterns\BlockPatterns; use RemoteDataBlocks\Logging\LoggerInterface; -use RemoteDataBlocks\Validation\ConfigSchemas; -use RemoteDataBlocks\Validation\Validator; use WP_Error; use function parse_blocks; @@ -20,11 +19,11 @@ class ConfigRegistry { private static LoggerInterface $logger; - public const RENDER_QUERY_KEY = 'render_query'; - public const SELECTION_QUERIES_KEY = 'selection_queries'; - public const DISPLAY_QUERY_KEY = 'display'; + public const DEPRECATED_DISPLAY_QUERY_KEY = 'display'; + public const PLACEHOLDERS_KEY = 'placeholders'; public const LIST_QUERY_KEY = 'list'; public const SEARCH_QUERY_KEY = 'search'; + public const QUERIES_KEY = 'queries'; public static function init( ?LoggerInterface $logger = null ): void { self::$logger = $logger ?? new Logger(); @@ -32,100 +31,156 @@ public static function init( ?LoggerInterface $logger = null ): void { } public static function register_block( array $block_config = [] ): bool|WP_Error { - // Validate the provided user configuration. - $schema = ConfigSchemas::get_remote_data_block_config_schema(); - $validator = new Validator( $schema, static::class, '$block_config' ); - $validated = $validator->validate( $block_config ); + // Validate the provided block configuration. + $block_config = RemoteDataBlock::from_array( $block_config ); - if ( is_wp_error( $validated ) ) { - return $validated; + if ( is_wp_error( $block_config ) ) { + self::$logger->error( $block_config->get_error_message() ); + return $block_config; } // Check if the block has already been registered. + $block_config = $block_config->to_array(); $block_title = $block_config['title']; $block_name = ConfigStore::get_block_name( $block_title ); if ( ConfigStore::is_registered_block( $block_name ) ) { return self::create_error( $block_title, sprintf( 'Block %s has already been registered', $block_name ) ); } - $display_query = self::inflate_query( $block_config[ self::RENDER_QUERY_KEY ]['query'] ); - $input_schema = $display_query->get_input_schema(); - $output_schema = $display_query->get_output_schema(); - $is_collection = true === ( $output_schema['is_collection'] ?? false ); + // Ensure that the block has queries. + if ( empty( $block_config[ self::QUERIES_KEY ] ) ) { + return self::create_error( $block_title, 'Block configuration must have a non-empty "queries" array' ); + } - // Check if any variables are required - $has_required_variables = array_reduce( - array_column( $input_schema, 'required' ), - fn( $carry, $required ) => $carry || ( $required ?? true ), - false - ); + $queries = []; + $display_queries_to_selectors_map = []; + + // Either use the placeholders from the config, or build them from the queries. + // Placeholders are meant to determine the queries that'll be visually represented + // in the block's UI upon initial insertion. + if ( ! empty( $block_config[ self::PLACEHOLDERS_KEY ] ) ) { + $placeholders = $block_config[ self::PLACEHOLDERS_KEY ]; + // Pre-validate the placeholders, to ensure they exist. + foreach ( $placeholders as $placeholder ) { + if ( ! isset( $block_config[ self::QUERIES_KEY ][ $placeholder['query_key'] ] ) ) { + return self::create_error( $block_title, sprintf( 'Query "%s" not found for placeholder "%s"', $placeholder['query_key'], $placeholder['name'] ) ); + } + } + } else { + $placeholders = []; + // Using array_keys here triggers a psalm error, so it's set to $_ instead. + // Supressing the psalm error is a not a good idea, so instead this is the better solution. + // ToDo: Fix the psalm error, and see if array_keys could be used here again. + foreach ( $block_config[ self::QUERIES_KEY ] as $query_key => $_ ) { + $placeholders[] = [ + 'name' => self::get_query_name_from_key( $query_key ), + 'query_key' => $query_key, + ]; + } + } - // 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. - $config = [ - 'description' => '', - 'icon' => $block_config['icon'] ?? 'cloud', - 'instructions' => $block_config['instructions'] ?? null, - 'name' => $block_name, - 'overrides' => $block_config['overrides'] ?? [], - 'patterns' => [], - 'queries' => [ - self::DISPLAY_QUERY_KEY => $display_query, - ], - 'selectors' => [ - [ - 'image_url' => $display_query->get_image_url(), - 'inputs' => self::map_input_variables( $input_schema ), - 'name' => $has_required_variables ? 'Manual input' : ( $is_collection ? 'Load collection' : 'Load item' ), - 'query_key' => self::DISPLAY_QUERY_KEY, - 'type' => $has_required_variables ? 'manual-input' : 'load-without-input', - ], - ], - 'title' => $block_title, - ]; + foreach ( $placeholders as $placeholder ) { + $selectors = []; - // Register "selectors" which allow the user to use a query to assist in - // selecting data for display by the block. - foreach ( $block_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; + $placeholder_query_key = $placeholder['query_key']; + $placeholder_query = self::inflate_query( $block_config[ self::QUERIES_KEY ][ $placeholder_query_key ] ); + $queries[ $placeholder_query_key ] = $placeholder_query; - $config['queries'][ $from_query::class ] = $from_query; + $placeholder_query_input_schema = $placeholder_query->get_input_schema(); + $placeholder_query_output_schema = $placeholder_query->get_output_schema(); - $from_input_schema = $from_query->get_input_schema(); - $from_output_schema = $from_query->get_output_schema(); + // We first generate the manual input selector for the placeholder query. + $is_collection = true === ( $placeholder_query_output_schema['is_collection'] ?? false ); + $has_required_variables = array_reduce( + array_column( $placeholder_query_input_schema, 'required' ), + fn( $carry, $required ) => $carry || ( $required ?? true ), + false + ); - 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 ) ); + $selector_config = [ + 'image_url' => $placeholder_query->get_image_url(), + 'inputs' => self::map_input_variables( $placeholder_query_input_schema ), + 'name' => $has_required_variables ? 'Manual input' : ( $is_collection ? 'Load collection' : 'Load item' ), + 'query_key' => $placeholder_query_key, + 'type' => $has_required_variables ? 'manual-input' : 'load-without-input', + ]; + + $selectors[] = $selector_config; + + // We run through all the queries to find compatible selectors. + foreach ( $block_config[ self::QUERIES_KEY ] as $selector_query_key => $selector_query ) { + // Don't match the placeholder to itself, as that's already been done. + if ( $selector_query_key === $placeholder_query_key ) { + continue; + } + + $selector_query = self::inflate_query( $selector_query ); + $queries[ $selector_query_key ] = $selector_query; + + $selector_query_input_schema = $selector_query->get_input_schema(); + $selector_query_output_schema = $selector_query->get_output_schema(); + + // Infer the type of the selector query. + // ToDo: Add support for multiple types. + $inferred_selector_query_type = self::infer_query_type( $selector_query_input_schema, $selector_query_output_schema ); + if ( 'unknown' === $inferred_selector_query_type ) { + 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 the output schema is not an array, skip. + if ( ! is_array( $selector_query_output_schema['type'] ) ) { + continue; + } + + // Find the fields that are present in both the selector's output schema and the placeholder's input schema. + $intersecting_keys = array_intersect_key( $selector_query_output_schema['type'], $placeholder_query_input_schema ); + + // Skip this, if they don't intersect. + if ( empty( $intersecting_keys ) ) { + continue; + } - if ( 1 !== $search_input_count ) { - return self::create_error( $block_title, 'A search query must have one input variable with type "ui:search_input"' ); + // Ensure the fields found have the same name and type in both the schemas. + $valid_intersecting_keys = self::validate_selector_query_mapping( $intersecting_keys, $placeholder_query_input_schema, $selector_query_output_schema ); + if ( empty( $valid_intersecting_keys ) ) { + continue; } + + // Now we generate the selector query's config as a selector for the placeholder query. + $selector_config = [ + 'image_url' => $selector_query->get_image_url(), + 'inputs' => self::map_input_variables( $selector_query_input_schema ), + 'name' => self::get_query_name_from_key( $selector_query_key ), + 'query_key' => $selector_query_key, + 'type' => $inferred_selector_query_type, + ]; + + // Add the selector to the beginning of the selectors array. + array_unshift( + $selectors, + $selector_config + ); } - // Add the selector to the configuration. - array_unshift( - $config['selectors'], - [ - 'image_url' => $from_query->get_image_url(), - 'inputs' => self::map_input_variables( $from_input_schema ), - 'name' => $selection_query['display_name'] ?? ucfirst( $from_query_type ), - 'query_key' => $from_query::class, - 'type' => $from_query_type, - ] - ); + $display_queries_to_selectors_map[ $placeholder_query_key ] = [ + 'name' => $placeholder['name'], + 'selectors' => $selectors, + ]; } + // Build the block configuration. + $config = [ + 'description' => '', + 'icon' => $block_config['icon'] ?? 'cloud', + 'instructions' => $block_config['instructions'] ?? null, + 'name' => $block_name, + 'overrides' => $block_config['overrides'] ?? [], + 'patterns' => [], + 'queries' => $queries, + 'display_queries_to_selectors' => $display_queries_to_selectors_map, + 'title' => $block_title, + ]; + // Register patterns which can be used with the block. foreach ( $block_config['patterns'] ?? [] as $pattern ) { $parsed_blocks = parse_blocks( $pattern['html'] ); @@ -146,6 +201,30 @@ public static function register_block( array $block_config = [] ): bool|WP_Error return true; } + private static function validate_selector_query_mapping( array $intersecting_keys, array $display_query_input_schema, array $output_schema ): array { + return array_filter( $intersecting_keys, function ( $key ) use ( $display_query_input_schema, $output_schema ) { + $display_query_fields = $display_query_input_schema[ $key ]; + + // If the name doesn't match, skip. + if ( $display_query_fields['name'] !== $output_schema['type'][ $key ]['name'] ) { + return false; + } + + // If the display query field is an id:list and the output schema field is an id, allow it as that's valid. + if ( 'id:list' === $display_query_fields['type'] && 'id' === $output_schema['type'][ $key ]['type'] ) { + return true; + } + + // If the types don't match, skip. + if ( $display_query_fields['type'] !== $output_schema['type'][ $key ]['type'] ) { + return false; + } + + // If the types match, allow it. + return true; + }, ARRAY_FILTER_USE_KEY ); + } + private static function register_block_pattern( string $block_name, string $pattern_title, string $pattern_content ): string { // Add the block arg to any bindings present in the pattern. $pattern_name = 'remote-data-blocks/' . sanitize_title_with_dashes( $pattern_title, '', 'save' ); @@ -195,4 +274,27 @@ function ( string $slug, array $input_var ): array { array_values( $input_schema ) ); } + + private static function get_query_name_from_key( string $key ): string { + // Replace any non-alphanumeric characters with spaces and convert to title case + return ucwords( preg_replace( '/[^a-zA-Z0-9]/', ' ', $key ) ); + } + + private static function infer_query_type( array $input_schema, array $output_schema ): string { + // If any input variable has type 'ui:search_input', it's a search query. + foreach ( $input_schema as $input_var ) { + if ( isset( $input_var['type'] ) && 'ui:search_input' === $input_var['type'] ) { + return self::SEARCH_QUERY_KEY; + } + } + + // If output_schema has 'is_collection' true, it's a list query. + if ( isset( $output_schema['is_collection'] ) && true === $output_schema['is_collection'] ) { + return self::LIST_QUERY_KEY; + } + + // This will happen if a query has not been configured correctly as a search or list query. + // So we error out, to replace the previous way of validating when the type was set. + return 'unknown'; + } } diff --git a/inc/Editor/BlockManagement/ConfigStore.php b/inc/Editor/BlockManagement/ConfigStore.php index 6ed252ce0..562c5f7f2 100644 --- a/inc/Editor/BlockManagement/ConfigStore.php +++ b/inc/Editor/BlockManagement/ConfigStore.php @@ -4,7 +4,6 @@ defined( 'ABSPATH' ) || exit(); -use RemoteDataBlocks\Config\Query\QueryInterface; use RemoteDataBlocks\Integrations\GenericHttp\GenericHttpDataSource; use RemoteDataBlocks\Logging\Logger; use RemoteDataBlocks\Logging\LoggerInterface; @@ -78,12 +77,15 @@ public static function get_data_source_type( string $block_name ): ?string { return null; } - $query = $config['queries'][ ConfigRegistry::DISPLAY_QUERY_KEY ] ?? null; - if ( ! ( $query instanceof QueryInterface ) ) { + $display_queries_to_selectors = $config['display_queries_to_selectors'] ?? []; + if ( empty( $display_queries_to_selectors ) ) { return null; } - $data_source = $query->get_data_source(); + // Get the first display query's data source type. + $display_query_key = array_keys( $display_queries_to_selectors )[0]; + $display_query = $config['queries'][ $display_query_key ]; + $data_source = $display_query->get_data_source(); if ( $data_source instanceof GenericHttpDataSource ) { return $data_source->get_service_name(); } diff --git a/inc/Editor/BlockPatterns/BlockPatterns.php b/inc/Editor/BlockPatterns/BlockPatterns.php index 02f614d9f..bb5a12921 100644 --- a/inc/Editor/BlockPatterns/BlockPatterns.php +++ b/inc/Editor/BlockPatterns/BlockPatterns.php @@ -73,7 +73,7 @@ private static function populate_template( string $template_name, array $attribu * @param QueryInterface $display_query The display query. * @return string The registered pattern name. */ - public static function register_default_block_pattern( string $block_name, string $block_title, QueryInterface $display_query ): string { + public static function register_default_block_pattern( string $block_name, string $block_title, string $display_query_key, QueryInterface $display_query ): string { self::load_templates(); // Loop through output variables and generate a pattern. Each text field will @@ -112,7 +112,7 @@ public static function register_default_block_pattern( string $block_name, strin $bindings['heading']['content'] = [ $field, $name ]; break; } - + $bindings['paragraphs'][] = [ 'content' => [ $field, $name ], ]; @@ -177,7 +177,7 @@ public static function register_default_block_pattern( string $block_name, strin $content = self::populate_template( 'empty', [] ); } - $pattern_name = sprintf( '%s/pattern', $block_name ); + $pattern_name = sprintf( '%s/%s-pattern', $block_name, $display_query_key ); register_block_pattern( $pattern_name, @@ -188,6 +188,7 @@ public static function register_default_block_pattern( string $block_name, strin 'content' => $content, 'inserter' => true, 'source' => 'plugin', + 'keywords' => [ $display_query_key ], ] ); diff --git a/inc/Editor/DataBinding/BlockBindings.php b/inc/Editor/DataBinding/BlockBindings.php index 79ffbccf6..a0825a377 100644 --- a/inc/Editor/DataBinding/BlockBindings.php +++ b/inc/Editor/DataBinding/BlockBindings.php @@ -133,7 +133,7 @@ private static function execute_queries( array $block_context ): array|WP_Error $remote_data = $remote_data->to_array(); $block_name = $remote_data['blockName']; $enabled_overrides = $remote_data['enabledOverrides'] ?? []; - $query_key = $remote_data['queryKey'] ?? ConfigRegistry::DISPLAY_QUERY_KEY; + $query_key = $remote_data['queryKey'] ?? ConfigRegistry::DEPRECATED_DISPLAY_QUERY_KEY; $array_of_input_variables = $remote_data['queryInputs']; $block_config = ConfigStore::get_block_configuration( $block_name ); diff --git a/inc/ExampleApi/ExampleApi.php b/inc/ExampleApi/ExampleApi.php index 28751ee98..94e06114a 100644 --- a/inc/ExampleApi/ExampleApi.php +++ b/inc/ExampleApi/ExampleApi.php @@ -119,13 +119,14 @@ public static function register_remote_data_block(): void { register_remote_data_block( [ 'title' => self::$block_title, - 'render_query' => [ - 'query' => $get_record_query, + 'queries' => [ + 'display' => $get_record_query, + 'get_table' => $get_table_query, ], - 'selection_queries' => [ + 'placeholders' => [ [ - 'query' => $get_table_query, - 'type' => 'list', + 'query_key' => 'display', + 'name' => 'Get Record', ], ], ] ); diff --git a/inc/REST/RemoteDataController.php b/inc/REST/RemoteDataController.php index 21667bd71..ab78763b7 100644 --- a/inc/REST/RemoteDataController.php +++ b/inc/REST/RemoteDataController.php @@ -36,7 +36,13 @@ public static function register_rest_routes(): void { return null !== ConfigStore::get_block_configuration( $value ); }, ], - 'query_key' => [ + 'display_query_key' => [ + 'required' => true, + 'sanitize_callback' => function ( $value ) { + return strval( $value ); + }, + ], + 'selector_query_key' => [ 'required' => true, 'sanitize_callback' => function ( $value ) { return strval( $value ); @@ -54,11 +60,12 @@ public static function register_rest_routes(): void { public static function execute_queries( WP_REST_Request $request ): array|WP_Error { $block_name = $request->get_param( 'block_name' ); - $query_key = $request->get_param( 'query_key' ); + $display_query_key = $request->get_param( 'display_query_key' ); + $selector_query_key = $request->get_param( 'selector_query_key' ); $query_inputs = $request->get_param( 'query_inputs' ); $block_config = ConfigStore::get_block_configuration( $block_name ); - $query = $block_config['queries'][ $query_key ]; + $query = $block_config['queries'][ $selector_query_key ]; $query_response = $query->execute_batch( $query_inputs ); if ( is_wp_error( $query_response ) ) { @@ -72,7 +79,8 @@ public static function execute_queries( WP_REST_Request $request ): array|WP_Err [ 'block_name' => $block_name, 'result_id' => wp_generate_uuid4(), - 'query_key' => $query_key, + 'display_query_key' => $display_query_key, + 'selector_query_key' => $selector_query_key, ], $query_response ); diff --git a/inc/Validation/ConfigSchemas.php b/inc/Validation/ConfigSchemas.php index 1ef1970fb..3760f456e 100644 --- a/inc/Validation/ConfigSchemas.php +++ b/inc/Validation/ConfigSchemas.php @@ -7,7 +7,6 @@ use RemoteDataBlocks\Config\Query\HttpQuery; use RemoteDataBlocks\Config\Query\QueryInterface; use RemoteDataBlocks\Config\QueryRunner\QueryRunnerInterface; -use RemoteDataBlocks\Editor\BlockManagement\ConfigRegistry; /** * ConfigSchemas class. @@ -86,27 +85,21 @@ private static function generate_remote_data_block_config_schema(): array { ] ) ) ), - 'render_query' => Types::object( [ - 'query' => Types::one_of( - Types::instance_of( QueryInterface::class ), - Types::serialized_config_for( HttpQuery::class ), - ), - ] ), - 'selection_queries' => Types::nullable( + 'placeholders' => 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( HttpQuery::class ), - ), - 'type' => Types::enum( - ConfigRegistry::LIST_QUERY_KEY, - ConfigRegistry::SEARCH_QUERY_KEY - ), - ] ) + 'name' => Types::string(), + 'query_key' => Types::string(), + ] ), ) ), + 'queries' => Types::record( + Types::string(), + Types::one_of( + Types::instance_of( QueryInterface::class ), + Types::serialized_config_for( HttpQuery::class ), + ), + ), 'overrides' => Types::nullable( Types::list_of( Types::object( [ diff --git a/src/block-editor/filters/withBlockBinding.tsx b/src/block-editor/filters/withBlockBinding.tsx index 5cbef56c1..115a94c58 100644 --- a/src/block-editor/filters/withBlockBinding.tsx +++ b/src/block-editor/filters/withBlockBinding.tsx @@ -17,11 +17,11 @@ import { PATTERN_OVERRIDES_CONTEXT_KEY, } from '@/config/constants'; import { getBoundBlockClassName, getMismatchedAttributes } from '@/utils/block-binding'; -import { getBlockAvailableBindings, getBlockTitle } from '@/utils/localized-block-data'; +import { getAvailableBindingsForQuery, getBlockTitle } from '@/utils/localized-block-data'; interface BoundBlockEditProps { attributes: RemoteDataInnerBlockAttributes; - availableBindings: AvailableBindings; + availableBindings: AvailableBindingsForQuery; blockName: string; children: JSX.Element; remoteDataName: string; @@ -105,12 +105,24 @@ export const withBlockBinding = createHigherOrderComponent( BlockEdit => { ) => { const { attributes, context, name, previewIndex: index = 0, setAttributes } = props; const { remoteData } = useRemoteDataContext( context ); - const availableBindings = getBlockAvailableBindings( remoteData?.blockName ?? '' ); - const hasAvailableBindings = Boolean( Object.keys( availableBindings ).length ); const { hasMultiSelection } = useSelect< BlockEditorStoreSelectors >( blockEditorStore ); // If the block does not have a remote data context, render it as usual. - if ( ! remoteData || ! hasAvailableBindings ) { + if ( ! remoteData ) { + return ; + } + + const displayQueryKey = remoteData.displayQueryKey; + + const availableBindings = getAvailableBindingsForQuery( + remoteData?.blockName ?? '', + displayQueryKey ?? '' + ); + + const hasAvailableBindings = Boolean( Object.keys( availableBindings ).length ); + + // If the block does not have any bindings, render it as usual. + if ( ! hasAvailableBindings ) { return ; } diff --git a/src/block-editor/format-types/inline-binding/components/InlineBindingSelectFieldPopover.tsx b/src/block-editor/format-types/inline-binding/components/InlineBindingSelectFieldPopover.tsx index a00a12517..c597bc15d 100644 --- a/src/block-editor/format-types/inline-binding/components/InlineBindingSelectFieldPopover.tsx +++ b/src/block-editor/format-types/inline-binding/components/InlineBindingSelectFieldPopover.tsx @@ -11,6 +11,7 @@ import { __ } from '@wordpress/i18n'; import { WPFormat, useAnchor } from '@wordpress/rich-text'; import { InlineBindingSelectField } from '@/block-editor/format-types/inline-binding/components/InlineBindingSelection'; +import { getFirstDisplayQueryKey } from '@/utils/localized-block-data'; interface InlineBindingSelectFieldPopoverProps { contentRef: React.RefObject< HTMLElement >; @@ -28,6 +29,10 @@ export function InlineBindingSelectFieldPopover( props: InlineBindingSelectField } ); const { remoteData, selectedField, type } = props.fieldSelection; + // ToDo: We are picking the first display query for now. + const displayQueryKey = + remoteData?.displayQueryKey ?? getFirstDisplayQueryKey( remoteData?.blockName ?? '' ); + return ( props.onSelectField( { ...data, action: 'update_field_shortcode' }, fieldValue ) diff --git a/src/block-editor/format-types/inline-binding/components/InlineBindingSelectNew.tsx b/src/block-editor/format-types/inline-binding/components/InlineBindingSelectNew.tsx index 93646a47e..58d5e6196 100644 --- a/src/block-editor/format-types/inline-binding/components/InlineBindingSelectNew.tsx +++ b/src/block-editor/format-types/inline-binding/components/InlineBindingSelectNew.tsx @@ -4,7 +4,11 @@ import { __ } from '@wordpress/i18n'; import { chevronRightSmall } from '@wordpress/icons'; import { DataViewsModal } from '@/blocks/remote-data-container/components/modals/DataViewsModal'; -import { getBlocksConfig } from '@/utils/localized-block-data'; +import { + getBlocksConfig, + getFirstDisplayQueryKey, + getSelectorsForDisplayQuery, +} from '@/utils/localized-block-data'; type InlineBindingSelectNewProps = Omit< DropdownMenuProps, 'label' > & { onSelectField: ( data: FieldSelection, fieldValue: string ) => void; @@ -41,9 +45,10 @@ export function InlineBindingSelectNew( props: InlineBindingSelectNewProps ) { Object.entries( blocksByType ).map( ( [ dataSourceType, configs ] ) => ( { configs.map( blockConfig => { - // For now, we will use the first compatible selector, but this - // should be improved. - const compatibleSelector = blockConfig.selectors.find( selector => + // ToDo: We are picking the first display query, and the first compatible selector for now. + const displayQueryKey = getFirstDisplayQueryKey( blockConfig.name ); + const selectors = getSelectorsForDisplayQuery( blockConfig.name, displayQueryKey ); + const compatibleSelector = selectors.find( selector => [ 'list', 'search' ].includes( selector.type ) ); @@ -57,7 +62,8 @@ export function InlineBindingSelectNew( props: InlineBindingSelectNewProps ) { blockName={ blockConfig.name } headerImage={ compatibleSelector.image_url } onSelectField={ onSelectField } - queryKey={ compatibleSelector.query_key } + selectorQueryKey={ compatibleSelector.query_key } + displayQueryKey={ displayQueryKey } renderTrigger={ ( { onClick } ) => ( { blockConfig.settings?.title ?? blockConfig.name } diff --git a/src/block-editor/format-types/inline-binding/components/InlineBindingSelection.tsx b/src/block-editor/format-types/inline-binding/components/InlineBindingSelection.tsx index 8223cc0b7..44603d438 100644 --- a/src/block-editor/format-types/inline-binding/components/InlineBindingSelection.tsx +++ b/src/block-editor/format-types/inline-binding/components/InlineBindingSelection.tsx @@ -2,12 +2,9 @@ import { BaseControl, Icon, MenuItem, Spinner } from '@wordpress/components'; import { useEffect } from '@wordpress/element'; import { check } from '@wordpress/icons'; -import { - DISPLAY_QUERY_KEY, - TEXT_FIELD_TYPES, -} from '@/blocks/remote-data-container/config/constants'; +import { TEXT_FIELD_TYPES } from '@/blocks/remote-data-container/config/constants'; import { useRemoteData } from '@/blocks/remote-data-container/hooks/useRemoteData'; -import { getBlockAvailableBindings } from '@/utils/localized-block-data'; +import { getAvailableBindingsForQuery } from '@/utils/localized-block-data'; import { getRemoteDataResultValue } from '@/utils/remote-data'; interface FieldSelectionProps { @@ -76,7 +73,10 @@ export function FieldSelection( props: FieldSelectionProps ) { type FieldSelectionWithFieldsProps = Omit< FieldSelectionProps, 'fields' | 'fieldType' >; export function FieldSelectionFromAvailableBindings( props: FieldSelectionWithFieldsProps ) { - const availableBindings = getBlockAvailableBindings( props.remoteData.blockName ); + const availableBindings = getAvailableBindingsForQuery( + props.remoteData.blockName, + props.remoteData.displayQueryKey ?? '' + ); const fields = Object.entries( availableBindings ).reduce< FieldSelectionProps[ 'fields' ] >( ( acc, [ fieldName, binding ] ) => { @@ -119,12 +119,15 @@ interface InlineBindingSelectFieldProps { onSelectField: ( data: FieldSelection, fieldValue: string ) => void; queryInputs: RemoteDataQueryInput[]; selectedField?: string; + displayQueryKey: string; + selectorQueryKey: string; } export function InlineBindingSelectField( props: InlineBindingSelectFieldProps ) { const { data, fetch, loading } = useRemoteData( { blockName: props.blockName, - queryKey: DISPLAY_QUERY_KEY, + displayQueryKey: props.displayQueryKey, + selectorQueryKey: props.selectorQueryKey, } ); useEffect( () => { diff --git a/src/blocks/remote-data-container/components/BlockBindingControls.tsx b/src/blocks/remote-data-container/components/BlockBindingControls.tsx index 26821781c..aff523593 100644 --- a/src/blocks/remote-data-container/components/BlockBindingControls.tsx +++ b/src/blocks/remote-data-container/components/BlockBindingControls.tsx @@ -12,7 +12,7 @@ import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks'; import { getBlockDataSourceType } from '@/utils/localized-block-data'; interface BlockBindingFieldControlProps { - availableBindings: AvailableBindings; + availableBindings: AvailableBindingsForQuery; fieldTypes: string[]; label: string; target: string; @@ -44,7 +44,7 @@ export function BlockBindingFieldControl( props: BlockBindingFieldControlProps ) interface BlockBindingControlsProps { attributes: RemoteDataInnerBlockAttributes; - availableBindings: AvailableBindings; + availableBindings: AvailableBindingsForQuery; blockName: string; remoteDataName: string; removeBinding: ( target: string ) => void; @@ -118,6 +118,7 @@ export function BlockBindingControls( props: BlockBindingControlsProps ) { label="Show label" name="show_label" onChange={ updateFieldLabel } + __nextHasNoMarginBottom /> ); diff --git a/src/blocks/remote-data-container/components/QueryComponent.tsx b/src/blocks/remote-data-container/components/QueryComponent.tsx new file mode 100644 index 000000000..59ae38c35 --- /dev/null +++ b/src/blocks/remote-data-container/components/QueryComponent.tsx @@ -0,0 +1,188 @@ +import { + BlockEditorStoreSelectors, + BlockPattern, + InnerBlocks, + InspectorControls, + useBlockProps, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { Spinner } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { useEffect, useState } from '@wordpress/element'; + +import { EditErrorBoundary } from './EditErrorBoundary'; +import { DataPanel } from './panels/DataPanel'; +import { OverridesPanel } from './panels/OverridesPanel'; +import { QueryInputsPanel } from './panels/QueryInputsPanel'; +import { PatternSelection } from '@/blocks/remote-data-container/components/pattern-selection/PatternSelection'; +import { CONTAINER_CLASS_NAME } from '@/blocks/remote-data-container/config/constants'; +import { usePatterns } from '@/blocks/remote-data-container/hooks/usePatterns'; +import { useRemoteData } from '@/blocks/remote-data-container/hooks/useRemoteData'; +import { hasRemoteDataChanged } from '@/utils/block-binding'; +import { getBlockTitle, getSelectorsForDisplayQuery } from '@/utils/localized-block-data'; + +export interface QueryComponentProps { + displayQueryKey: string; + blockConfig: BlockConfig; + blockName: string; + rootClientId: string; + remoteDataAttribute: RemoteData | undefined; + setAttributes: ( attributes: RemoteDataBlockAttributes ) => void; + queryInputs: RemoteDataQueryInput[]; + onQueryInputsChange?: ( inputs: RemoteDataQueryInput[] ) => void; + resetQuery: () => void; +} + +export function RemoteDataBlockComponent( props: QueryComponentProps ) { + const { + displayQueryKey, + blockConfig, + blockName, + rootClientId, + remoteDataAttribute, + setAttributes, + queryInputs, + resetQuery, + } = props; + + const { getSupportedPatterns, innerBlocksPattern, insertPatternBlocks, resetInnerBlocks } = + usePatterns( blockName, rootClientId, displayQueryKey ); + const { data, fetch, reset, supportsPagination, loading } = useRemoteData( { + blockName, + externallyManagedRemoteData: remoteDataAttribute, + externallyManagedUpdateRemoteData: updateRemoteData, + // This is done on purpose, as the selector query is the input to the display query. + // So the real query being executed is the display query. + displayQueryKey, + selectorQueryKey: displayQueryKey, + } ); + + const { hasMultiSelection } = useSelect< BlockEditorStoreSelectors >( blockEditorStore ); + const [ showPatternSelection, setShowPatternSelection ] = useState< boolean >( false ); + + useEffect( () => { + onSelectRemoteData( queryInputs ); + }, [ queryInputs ] ); + + function onSelectRemoteData( inputs: RemoteDataQueryInput[] ): void { + // if the old queryInputs and new ones are the same, skip this call. + if ( JSON.stringify( remoteDataAttribute?.queryInputs ) === JSON.stringify( inputs ) ) { + return; + } + + void fetch( inputs ).then( () => { + if ( innerBlocksPattern ) { + insertPatternBlocks( innerBlocksPattern, supportsPagination ); + return; + } + + setShowPatternSelection( true ); + } ); + } + + function refreshRemoteData(): void { + void fetch( remoteDataAttribute?.queryInputs ?? [ {} ] ); + } + + function resetPatternSelection(): void { + resetInnerBlocks(); + setShowPatternSelection( false ); + } + + function resetRemoteData(): void { + reset(); + resetPatternSelection(); + resetQuery(); + } + + function onSelectPattern( pattern: BlockPattern ): void { + insertPatternBlocks( pattern, supportsPagination ); + setShowPatternSelection( false ); + } + + function updateRemoteData( remoteData?: RemoteData ): void { + if ( hasRemoteDataChanged( remoteDataAttribute, remoteData ) ) { + setAttributes( { remoteData } ); + } + } + + function onUpdateQueryInputs( + newSelectorQueryKey: string, + inputs: RemoteDataQueryInput[] + ): void { + if ( ! remoteDataAttribute ) { + return; + } + + updateRemoteData( { + ...remoteDataAttribute, + queryInputs: inputs, + // This will always be the display query key. + selectorQueryKey: newSelectorQueryKey, + displayQueryKey, + } ); + refreshRemoteData(); + } + + if ( showPatternSelection ) { + const supportedPatterns = getSupportedPatterns( data?.results[ 0 ] ); + + return ( + + ); + } + + return ( + <> + { ! hasMultiSelection() && data && ( + + + + + + ) } + { loading && ( +
+ +
+ ) } + + + ); +} + +export function QueryComponent( props: QueryComponentProps ) { + const blockProps = useBlockProps( { className: CONTAINER_CLASS_NAME } ); + + return ( + <> +
+ + + +
+ + ); +} diff --git a/src/blocks/remote-data-container/components/modals/DataViewsModal.tsx b/src/blocks/remote-data-container/components/modals/DataViewsModal.tsx index 264fec01f..2fe6af915 100644 --- a/src/blocks/remote-data-container/components/modals/DataViewsModal.tsx +++ b/src/blocks/remote-data-container/components/modals/DataViewsModal.tsx @@ -15,13 +15,23 @@ interface DataViewsModalProps { headerImage?: string; onSelect?: ( data: RemoteDataQueryInput[] ) => void; onSelectField?: ( data: FieldSelection, fieldValue: string ) => void; - queryKey: string; + selectorQueryKey: string; + displayQueryKey: string; renderTrigger?: ( props: { onClick: () => void } ) => React.ReactNode; title?: string; } export const DataViewsModal: React.FC< DataViewsModalProps > = props => { - const { className, blockName, onSelect, onSelectField, queryKey, renderTrigger, title } = props; + const { + className, + blockName, + onSelect, + onSelectField, + selectorQueryKey, + displayQueryKey, + renderTrigger, + title, + } = props; const blockConfig = getBlockConfig( blockName ); @@ -33,6 +43,7 @@ export const DataViewsModal: React.FC< DataViewsModalProps > = props => { selection.length > 1 ? __( 'items selected in total' ) : __( 'item selected in total' ); const { close, isOpen, open } = useModalState(); + const { data, hasNextPage, @@ -46,7 +57,7 @@ export const DataViewsModal: React.FC< DataViewsModalProps > = props => { supportsSearch, totalItems, totalPages, - } = useRemoteData( { blockName, fetchOnMount: true, queryKey } ); + } = useRemoteData( { blockName, fetchOnMount: true, displayQueryKey, selectorQueryKey } ); // For selection, DataViews transacts only in IDs, so we provide the UUID from // the API response as a synthetic ID and map them to the full result. diff --git a/src/blocks/remote-data-container/components/panels/QueryInputsPanel.tsx b/src/blocks/remote-data-container/components/panels/QueryInputsPanel.tsx index 983053166..25520bb4f 100644 --- a/src/blocks/remote-data-container/components/panels/QueryInputsPanel.tsx +++ b/src/blocks/remote-data-container/components/panels/QueryInputsPanel.tsx @@ -2,12 +2,10 @@ import { Button, PanelBody, TextControl } from '@wordpress/components'; import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { DISPLAY_QUERY_KEY } from '@/blocks/remote-data-container/config/constants'; - interface QueryInputsPanelProps { - onUpdateQueryInputs: ( queryKey: string, inputs: RemoteDataQueryInput[] ) => void; + onUpdateQueryInputs: ( selectorQueryKey: string, inputs: RemoteDataQueryInput[] ) => void; remoteData: RemoteData; - selectors: BlockConfig[ 'selectors' ]; + selectors: Selector[]; } export function QueryInputsPanel( { @@ -15,10 +13,10 @@ export function QueryInputsPanel( { remoteData, selectors, }: QueryInputsPanelProps ) { - const { queryInputs = [], queryKey = DISPLAY_QUERY_KEY } = remoteData; + const { queryInputs = [], selectorQueryKey = '' } = remoteData; const [ localInputs, setLocalInputs ] = useState( queryInputs ); const inputDefinitions = - selectors?.find( selector => selector.query_key === queryKey )?.inputs ?? []; + selectors?.find( selector => selector.query_key === selectorQueryKey )?.inputs ?? []; return ( @@ -39,7 +37,7 @@ export function QueryInputsPanel( { return Object.fromEntries( entries ) as RemoteDataQueryInput; } ); - onUpdateQueryInputs( queryKey, cleanedInputs ); + onUpdateQueryInputs( selectorQueryKey, cleanedInputs ); } } > { localInputs.map( ( input, index ) => @@ -60,7 +58,7 @@ export function QueryInputsPanel( { ); } } onBlur={ () => { - onUpdateQueryInputs( queryKey, localInputs ); + onUpdateQueryInputs( selectorQueryKey, localInputs ); } } __next40pxDefaultSize __nextHasNoMarginBottom diff --git a/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx b/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx index 007dd3321..e0dd89a3d 100644 --- a/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx +++ b/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx @@ -9,15 +9,14 @@ import { InputModal } from '@/blocks/remote-data-container/components/modals/Inp import { InputPopover } from '@/blocks/remote-data-container/components/popovers/InputPopover'; interface ItemSelectQueryTypeProps { - blockConfig: BlockConfig; + blockName: string; + selectors: Selector[]; + displayQueryKey: string; onSelect: ( data: RemoteDataQueryInput[] ) => void; } export function ItemSelectQueryType( props: ItemSelectQueryTypeProps ) { - const { - blockConfig: { name: blockName, selectors }, - onSelect, - } = props; + const { blockName, selectors, displayQueryKey, onSelect } = props; return ( onSelect( [ {} ] ) } variant="primary"> + ); diff --git a/src/blocks/remote-data-container/components/placeholders/Placeholder.tsx b/src/blocks/remote-data-container/components/placeholders/Placeholder.tsx deleted file mode 100644 index f4eaa0a18..000000000 --- a/src/blocks/remote-data-container/components/placeholders/Placeholder.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { IconType, Placeholder as PlaceholderComponent } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { cloud } from '@wordpress/icons'; - -import { ItemSelectQueryType } from '@/blocks/remote-data-container/components/placeholders/ItemSelectQueryType'; - -export interface PlaceholderProps { - blockConfig: BlockConfig; - onSelect: ( input: RemoteDataQueryInput[] ) => void; -} - -export function Placeholder( props: PlaceholderProps ) { - const { blockConfig, onSelect } = props; - const { instructions, settings } = blockConfig; - - const iconElement: IconType = ( settings.icon as IconType ) ?? cloud; - - return ( - - - - ); -} diff --git a/src/blocks/remote-data-container/components/placeholders/QuerySelectionPlaceholder.tsx b/src/blocks/remote-data-container/components/placeholders/QuerySelectionPlaceholder.tsx new file mode 100644 index 000000000..4d0df19a3 --- /dev/null +++ b/src/blocks/remote-data-container/components/placeholders/QuerySelectionPlaceholder.tsx @@ -0,0 +1,80 @@ +import { + Button, + IconType, + Placeholder as PlaceholderComponent, + __experimentalToggleGroupControl as ToggleGroupControl, +} from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { cloud } from '@wordpress/icons'; + +import { ItemSelectQueryType } from './ItemSelectQueryType'; +import { getSelectorsForDisplayQuery } from '@/utils/localized-block-data'; + +export interface QuerySelectionPlaceholderProps { + blockConfig: BlockConfig; + onDisplayQueryKeySelect: ( displayQueryKey: string ) => void; + onQueryInputsSelect: ( inputs: RemoteDataQueryInput[] ) => void; +} + +export function QuerySelectionPlaceholder( props: QuerySelectionPlaceholderProps ) { + const { blockConfig, onDisplayQueryKeySelect, onQueryInputsSelect } = props; + const { instructions, settings, displayQueriesToSelectors } = blockConfig; + + const iconElement: IconType = ( settings.icon as IconType ) ?? cloud; + + const [ selectedDisplayQueryKey, setSelectedDisplayQueryKey ] = useState< string >( '' ); + const [ showSelectors, setShowSelectors ] = useState< boolean >( false ); + + function handleSelectorOnSelect( inputs: RemoteDataQueryInput[] ) { + setShowSelectors( false ); + onDisplayQueryKeySelect( selectedDisplayQueryKey ); + onQueryInputsSelect( inputs ); + } + + function handleDisplayQueryKeyOnSelect( displayQueryKey: string ) { + setSelectedDisplayQueryKey( displayQueryKey ); + setShowSelectors( true ); + } + + return ( + + { ! showSelectors && ( + + { Object.entries( displayQueriesToSelectors ).map( + ( [ displayQueryKey, displayQueryConfig ] ) => ( + + ) + ) } + + ) } + { showSelectors && ( + + ) } + + ); +} diff --git a/src/blocks/remote-data-container/config/constants.ts b/src/blocks/remote-data-container/config/constants.ts index 8a50a21a9..589e7ee10 100644 --- a/src/blocks/remote-data-container/config/constants.ts +++ b/src/blocks/remote-data-container/config/constants.ts @@ -8,7 +8,6 @@ export const SUPPORTED_CORE_BLOCKS = [ 'core/paragraph', ]; -export const DISPLAY_QUERY_KEY = 'display'; export const REMOTE_DATA_CONTEXT_KEY = 'remote-data-blocks/remoteData'; export const REMOTE_DATA_REST_API_URL = getRestUrl(); diff --git a/src/blocks/remote-data-container/edit.tsx b/src/blocks/remote-data-container/edit.tsx index 75b2fa00d..c7f302267 100644 --- a/src/blocks/remote-data-container/edit.tsx +++ b/src/blocks/remote-data-container/edit.tsx @@ -1,34 +1,15 @@ -import { - BlockEditorStoreSelectors, - BlockPattern, - InspectorControls, - store as blockEditorStore, - useBlockProps, -} from '@wordpress/block-editor'; import { BlockEditProps } from '@wordpress/blocks'; -import { Spinner } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; import { useState } from '@wordpress/element'; -import { QueryInputsPanel } from './components/panels/QueryInputsPanel'; -import { EditErrorBoundary } from '@/blocks/remote-data-container/components/EditErrorBoundary'; -import { InnerBlocks } from '@/blocks/remote-data-container/components/InnerBlocks'; -import { DataPanel } from '@/blocks/remote-data-container/components/panels/DataPanel'; -import { OverridesPanel } from '@/blocks/remote-data-container/components/panels/OverridesPanel'; -import { PatternSelection } from '@/blocks/remote-data-container/components/pattern-selection/PatternSelection'; -import { Placeholder } from '@/blocks/remote-data-container/components/placeholders/Placeholder'; -import { - CONTAINER_CLASS_NAME, - DISPLAY_QUERY_KEY, -} from '@/blocks/remote-data-container/config/constants'; -import { usePatterns } from '@/blocks/remote-data-container/hooks/usePatterns'; -import { useRemoteData } from '@/blocks/remote-data-container/hooks/useRemoteData'; -import { hasRemoteDataChanged } from '@/utils/block-binding'; -import { getBlockConfig, getBlockTitle } from '@/utils/localized-block-data'; +import { EditErrorBoundary } from './components/EditErrorBoundary'; +import { QueryComponent } from './components/QueryComponent'; +import { QuerySelectionPlaceholder } from '@/blocks/remote-data-container/components/placeholders/QuerySelectionPlaceholder'; +import { getBlockConfig } from '@/utils/localized-block-data'; import { migrateRemoteData } from '@/utils/remote-data'; + import './editor.scss'; -function RemoteDataBlockEdit( props: BlockEditProps< RemoteDataBlockAttributes > ) { +export function Edit( props: BlockEditProps< RemoteDataBlockAttributes > ): JSX.Element { const blockName = props.name; const blockConfig = getBlockConfig( blockName ); @@ -39,131 +20,47 @@ function RemoteDataBlockEdit( props: BlockEditProps< RemoteDataBlockAttributes > const rootClientId = props.clientId; const remoteDataAttribute = migrateRemoteData( props.attributes.remoteData ); - const { getSupportedPatterns, innerBlocksPattern, insertPatternBlocks, resetInnerBlocks } = - usePatterns( blockName, rootClientId ); - - const { data, fetch, loading, reset, supportsPagination } = useRemoteData( { - blockName, - externallyManagedRemoteData: remoteDataAttribute, - externallyManagedUpdateRemoteData: updateRemoteData, - queryKey: DISPLAY_QUERY_KEY, - } ); - - const { hasMultiSelection } = useSelect< BlockEditorStoreSelectors >( blockEditorStore ); - const [ showPatternSelection, setShowPatternSelection ] = useState< boolean >( false ); - - function refreshRemoteData(): void { - void fetch( remoteDataAttribute?.queryInputs ?? [ {} ] ); - } - - function resetPatternSelection(): void { - resetInnerBlocks(); - setShowPatternSelection( false ); - } - - function resetRemoteData(): void { - reset(); - resetPatternSelection(); - } - - function onSelectPattern( pattern: BlockPattern ): void { - insertPatternBlocks( pattern, supportsPagination ); - setShowPatternSelection( false ); - } - - function onSelectRemoteData( inputs: RemoteDataQueryInput[] ): void { - void fetch( inputs ).then( () => { - if ( innerBlocksPattern ) { - insertPatternBlocks( innerBlocksPattern, supportsPagination ); - return; - } - - setShowPatternSelection( true ); - } ); - } - - function updateRemoteData( remoteData?: RemoteData ): void { - if ( hasRemoteDataChanged( remoteDataAttribute, remoteData ) ) { - props.setAttributes( { remoteData } ); - } - } + const [ displayQueryKey, setDisplayQueryKey ] = useState< string >( + remoteDataAttribute?.displayQueryKey ?? '' + ); - function onUpdateQueryInputs( queryKey: string, inputs: RemoteDataQueryInput[] ): void { - if ( ! remoteDataAttribute ) { - return; - } + const [ queryInputs, setQueryInputs ] = useState< RemoteDataQueryInput[] >( + remoteDataAttribute?.queryInputs ?? [] + ); - updateRemoteData( { - ...remoteDataAttribute, - queryInputs: inputs, - queryKey, - } ); - refreshRemoteData(); + function setAttributes( attributes: RemoteDataBlockAttributes ): void { + props.setAttributes( attributes ); } - // No remote data has been selected yet, show a placeholder. - if ( ! data ) { - return ; + function resetQuery(): void { + setDisplayQueryKey( '' ); + setQueryInputs( [] ); } - if ( showPatternSelection ) { - const supportedPatterns = getSupportedPatterns( data.results[ 0 ] ); - + if ( ! displayQueryKey ) { return ( - ); } return ( <> - { ! hasMultiSelection() && ( - - - - - - ) } - - { loading && ( -
- -
- ) } - - - ); -} - -export function Edit( props: BlockEditProps< RemoteDataBlockAttributes > ) { - const blockProps = useBlockProps( { className: CONTAINER_CLASS_NAME } ); - - return ( -
- - + + -
+ ); } diff --git a/src/blocks/remote-data-container/hooks/usePatterns.ts b/src/blocks/remote-data-container/hooks/usePatterns.ts index c9fc2db72..549d77f8b 100644 --- a/src/blocks/remote-data-container/hooks/usePatterns.ts +++ b/src/blocks/remote-data-container/hooks/usePatterns.ts @@ -15,7 +15,11 @@ import { } from '@/utils/block-binding'; import { getBlockConfig } from '@/utils/localized-block-data'; -export function usePatterns( remoteDataBlockName: string, rootClientId: string = '' ) { +export function usePatterns( + remoteDataBlockName: string, + rootClientId: string = '', + displayQueryKey: string = '' +) { const { patterns } = getBlockConfig( remoteDataBlockName ) ?? {}; const { replaceInnerBlocks } = useDispatch< BlockEditorStoreActions >( blockEditorStore ); const { getPatternsByBlockTypes, allowedPatterns } = useSelect< @@ -87,8 +91,9 @@ export function usePatterns( remoteDataBlockName: string, rootClientId: string = getSupportedPatterns: ( result?: RemoteDataApiResult ): BlockPattern[] => { const supportedPatterns = allowedPatterns.filter( pattern => - pattern?.blockTypes?.includes( remoteDataBlockName ) || - pattern.blocks.some( block => hasBlockBinding( block, remoteDataBlockName ) ) + ( pattern?.blockTypes?.includes( remoteDataBlockName ) || + pattern.blocks.some( block => hasBlockBinding( block, remoteDataBlockName ) ) ) && + ( ! pattern.keywords || pattern.keywords.includes( displayQueryKey ) ) ); // If no result is provided, return the supported patterns as is. diff --git a/src/blocks/remote-data-container/hooks/useRemoteData.ts b/src/blocks/remote-data-container/hooks/useRemoteData.ts index 1b5d30fa9..ece17351f 100644 --- a/src/blocks/remote-data-container/hooks/useRemoteData.ts +++ b/src/blocks/remote-data-container/hooks/useRemoteData.ts @@ -7,7 +7,7 @@ import { useSearchVariables } from '@/blocks/remote-data-container/hooks/useSear import { ensureError } from '@/utils/errors'; import { memoizeFn } from '@/utils/function'; import { isQueryInputValid, validateQueryInput } from '@/utils/input-validation'; -import { getBlockConfig } from '@/utils/localized-block-data'; +import { getSelectorsForDisplayQuery } from '@/utils/localized-block-data'; async function unmemoizedfetchRemoteData( requestData: RemoteDataApiRequest @@ -26,7 +26,8 @@ async function unmemoizedfetchRemoteData( blockName: body.block_name, metadata: body.metadata, pagination: body.pagination, - queryKey: body.query_key, + displayQueryKey: body.display_query_key, + selectorQueryKey: body.selector_query_key, queryInputs: body.query_inputs, resultId: body.result_id, results: body.results, @@ -65,7 +66,8 @@ interface UseRemoteDataInput { initialPerPage?: number; initialSearchInput?: string; onSuccess?: () => void; - queryKey: string; + displayQueryKey: string; + selectorQueryKey: string; } // This hook fetches remote data and manages state for the requests. @@ -85,7 +87,8 @@ export function useRemoteData( { initialPerPage, initialSearchInput, onSuccess, - queryKey, + displayQueryKey, + selectorQueryKey, }: UseRemoteDataInput ): UseRemoteData { const [ data, setData ] = useState< RemoteData >(); const [ error, setError ] = useState< Error >(); @@ -95,14 +98,16 @@ export function useRemoteData( { const resolvedUpdater = externallyManagedUpdateRemoteData ?? setData; const hasResolvedData = Boolean( resolvedData ); - const blockConfig = getBlockConfig( blockName ); - const query = blockConfig?.selectors?.find( selector => selector.query_key === queryKey ); + const selectors = getSelectorsForDisplayQuery( blockName, displayQueryKey ); + const query = selectors.find( selector => selector.query_key === selectorQueryKey ); if ( ! query ) { // Here we intentionally throw an error instead of calling setError, because // this indicates a misconfiguration somewhere in our code, not a runtime / // query error. - throw new Error( `Query not found for block "${ blockName }" and key "${ queryKey }".` ); + throw new Error( + `Query not found for block "${ blockName }" and key "${ selectorQueryKey }".` + ); } // Overrides must be provided via externallyManagedRemoteData @@ -192,7 +197,8 @@ export function useRemoteData( { const requestData: RemoteDataApiRequest = { block_name: blockName, - query_key: queryKey, + display_query_key: displayQueryKey, + selector_query_key: selectorQueryKey, query_inputs: inputs, }; diff --git a/src/blocks/remote-data-container/hooks/useRemoteDataContext.ts b/src/blocks/remote-data-container/hooks/useRemoteDataContext.ts index 21be1821e..30eec1ea4 100644 --- a/src/blocks/remote-data-container/hooks/useRemoteDataContext.ts +++ b/src/blocks/remote-data-container/hooks/useRemoteDataContext.ts @@ -21,18 +21,21 @@ export function useRemoteDataContext( context: Record< string, unknown > ): Remo const blockConfig = getBlockConfig( remoteDataBlockName ); if ( blockConfig ) { + const availableBindings = Object.values( blockConfig.availableBindings )[ 0 ] ?? {}; + return { remoteData: { blockName: remoteDataBlockName, metadata: {}, queryInputs: [], - queryKey: 'Example Query Key', + displayQueryKey: 'Example Display Query Key', + selectorQueryKey: 'Example Selector Query Key', resultId: '', results: [ { // Example result for patterns. result: Object.fromEntries( - Object.entries( blockConfig.availableBindings ).map( ( [ key, value ] ) => [ + Object.entries( availableBindings ).map( ( [ key, value ] ) => [ key, { name: value.name, diff --git a/src/data-sources/components/DataSourceForm.tsx b/src/data-sources/components/DataSourceForm.tsx index e6c2cc43c..8a57283f9 100644 --- a/src/data-sources/components/DataSourceForm.tsx +++ b/src/data-sources/components/DataSourceForm.tsx @@ -370,6 +370,7 @@ const DataSourceFormBlocks = ( { } > [ 'type' => 'string' ], ] ), ], + 'display_queries_to_selectors' => [ + 'display' => [ 'display' ], + ], ] ); $this->assertEquals( 'airtable', ConfigStore::get_data_source_type( 'airtable_remote_blocks' ) ); diff --git a/tests/inc/Editor/DataBinding/BlockBindingsTest.php b/tests/inc/Editor/DataBinding/BlockBindingsTest.php index 6726a3da7..879fbb6e0 100644 --- a/tests/inc/Editor/DataBinding/BlockBindingsTest.php +++ b/tests/inc/Editor/DataBinding/BlockBindingsTest.php @@ -543,7 +543,7 @@ private function create_mock_query_runner_with_error( WP_Error $error ): MockQue private function create_mock_block_config( MockQueryRunner $query_runner ): array { return [ 'queries' => [ - ConfigRegistry::DISPLAY_QUERY_KEY => MockQuery::create( [ + ConfigRegistry::DEPRECATED_DISPLAY_QUERY_KEY => MockQuery::create( [ 'input_schema' => self::MOCK_INPUT_SCHEMA, 'output_schema' => self::MOCK_OUTPUT_SCHEMA, 'query_runner' => $query_runner, diff --git a/tests/inc/Functions/FunctionsTest.php b/tests/inc/Functions/FunctionsTest.php index d4b674027..28336968e 100644 --- a/tests/inc/Functions/FunctionsTest.php +++ b/tests/inc/Functions/FunctionsTest.php @@ -20,22 +20,45 @@ class FunctionsTest extends TestCase { protected function setUp(): void { parent::setUp(); $this->mock_logger = new MockLogger(); - $this->mock_query = MockQuery::create(); + $this->mock_query = MockQuery::create( [ + 'input_schema' => [ + 'id' => [ + 'name' => 'ID', + 'type' => 'id', + ], + ], + ] ); $this->mock_list_query = MockQuery::create( [ 'output_schema' => [ 'is_collection' => true, + 'type' => [ + 'id' => [ + 'name' => 'ID', + 'path' => '$.id', + 'type' => 'id', + ], + ], ], ] ); $this->mock_search_query = MockQuery::create( [ 'input_schema' => [ 'search' => [ 'type' => 'ui:search_input' ], ], + 'output_schema' => [ + 'type' => [ + 'id' => [ + 'name' => 'ID', + 'path' => '$.id', + 'type' => 'id', + ], + ], + ], ] ); ConfigRegistry::init( $this->mock_logger ); } - public function testRegisterBlock(): void { + public function testRegisterBlockWithOldConfigSchema(): void { register_remote_data_block( [ 'title' => 'Test Block', 'render_query' => [ @@ -95,7 +118,17 @@ public function testRegisterListQuery(): void { $block_name = 'remote-data-blocks/test-block-with-list-query'; $config = ConfigStore::get_block_configuration( $block_name ); - $this->assertSame( 'list', $config['selectors'][0]['type'] ?? null ); + + // Ensure that display query is the only key in the display_queries_to_selectors. + $this->assertCount( 1, $config['display_queries_to_selectors'] ); + $this->assertSame( 'display', array_keys( $config['display_queries_to_selectors'] )[0] ); + + // Ensure that there are 2 selectors for the display query. + $this->assertCount( 2, $config['display_queries_to_selectors']['display']['selectors'] ); + + // Ensure that the query_key of the first selector is list, and the second one is display. + $this->assertSame( 'list', $config['display_queries_to_selectors']['display']['selectors'][0]['query_key'] ); + $this->assertSame( 'display', $config['display_queries_to_selectors']['display']['selectors'][1]['query_key'] ); } public function testRegisterSearchQuery(): void { @@ -114,7 +147,17 @@ public function testRegisterSearchQuery(): void { $block_name = 'remote-data-blocks/test-block-with-search-query'; $config = ConfigStore::get_block_configuration( $block_name ); - $this->assertSame( 'search', $config['selectors'][0]['type'] ?? null ); + + // Ensure that display query is the only key in the display_queries_to_selectors. + $this->assertCount( 1, $config['display_queries_to_selectors'] ); + $this->assertSame( 'display', array_keys( $config['display_queries_to_selectors'] )[0] ); + + // Ensure that there are 2 selectors for the display query. + $this->assertCount( 2, $config['display_queries_to_selectors']['display']['selectors'] ); + + // Ensure that the query_key of the first selector is search, and the second one is display. + $this->assertSame( 'search', $config['display_queries_to_selectors']['display']['selectors'][0]['query_key'] ); + $this->assertSame( 'display', $config['display_queries_to_selectors']['display']['selectors'][1]['query_key'] ); } public function testIsRegisteredBlockReturnsTrueForRegisteredBlock(): void { @@ -172,8 +215,90 @@ public function testRegisterSearchQueryWithoutSearchTerms(): void { ], ] ); + $block_name = 'remote-data-blocks/invalid-search-block'; + $config = ConfigStore::get_block_configuration( $block_name ); + + // Ensure that display query is the only key in the display_queries_to_selectors. + $this->assertCount( 1, $config['display_queries_to_selectors'] ); + $this->assertSame( 'display', array_keys( $config['display_queries_to_selectors'] )[0] ); + + // Ensure that there is 1 selector for the display query. + $this->assertCount( 1, $config['display_queries_to_selectors']['display']['selectors'] ); + + // Ensure that the query_key of the selector is search. + $this->assertSame( 'display', $config['display_queries_to_selectors']['display']['selectors'][0]['query_key'] ); + } + + public function testRegisterBlockWithOldSchemaFormatNoRenderQuery(): void { + register_remote_data_block( [ + 'title' => 'Test Block with Old Schema Format No Render Query', + ] ); + + $this->assertTrue( $this->mock_logger->hasLoggedLevel( LogLevel::ERROR ) ); + $error_logs = $this->mock_logger->getLogsByLevel( LogLevel::ERROR ); + $this->assertStringContainsString( 'Error registering block Test Block with Old Schema Format No Render Query: Block configuration must have a non-empty "queries" array', $error_logs[0]['message'] ); + } + + public function testRegisterBlockWithNewConfigSchema(): void { + register_remote_data_block( [ + 'title' => 'Test Block with New Config Schema', + 'queries' => [ + 'display' => $this->mock_query, + 'search' => $this->mock_search_query, + 'list' => $this->mock_list_query, + ], + 'placeholders' => [ + [ + 'name' => 'Get', + 'query_key' => 'display', + ], + [ + 'name' => 'List', + 'query_key' => 'list', + ], + ], + ] ); + + $block_name = 'remote-data-blocks/test-block-with-new-config-schema'; + $config = ConfigStore::get_block_configuration( $block_name ); + + // Ensure that there are 2 selectors for the display query. + $this->assertCount( 3, $config['display_queries_to_selectors']['display']['selectors'] ); + + // Ensure that the query_key of the first selector is search, the second one is list, and the third one is display. + $this->assertSame( 'list', $config['display_queries_to_selectors']['display']['selectors'][0]['query_key'] ); + $this->assertSame( 'search', $config['display_queries_to_selectors']['display']['selectors'][1]['query_key'] ); + $this->assertSame( 'display', $config['display_queries_to_selectors']['display']['selectors'][2]['query_key'] ); + + // Ensure that there is 1 selector for the list query. + $this->assertCount( 1, $config['display_queries_to_selectors']['list']['selectors'] ); + $this->assertSame( 'list', $config['display_queries_to_selectors']['list']['selectors'][0]['query_key'] ); + } + + public function testRegisterBlockWithBadDisplayQueries(): void { + register_remote_data_block( [ + 'title' => 'Test Block with Bad Display Queries', + 'queries' => [ + 'display' => $this->mock_query, + ], + 'placeholders' => [ + [ + 'name' => 'Get', + 'query_key' => 'display', + ], + [ + 'name' => 'List', + 'query_key' => 'list', + ], + [ + 'name' => 'Test', + 'query_key' => 'test', + ], + ], + ] ); + $this->assertTrue( $this->mock_logger->hasLoggedLevel( LogLevel::ERROR ) ); $error_logs = $this->mock_logger->getLogsByLevel( LogLevel::ERROR ); - $this->assertStringContainsString( 'ui:search_input', $error_logs[0]['message'] ); + $this->assertStringContainsString( 'Error registering block Test Block with Bad Display Queries: Query "list" not found for placeholder "List"', $error_logs[0]['message'] ); } } diff --git a/tests/integration/RDBTestCase.php b/tests/integration/RDBTestCase.php index 8c3ee3ba0..340761823 100644 --- a/tests/integration/RDBTestCase.php +++ b/tests/integration/RDBTestCase.php @@ -1,6 +1,5 @@ [ 'display' => [ 'display' ] ], 'queries' => [ 'display' => MockQuery::create(), ], @@ -109,6 +110,7 @@ public function test_track_remote_data_blocks_usage_tracks_nested_blocks(): void ] ); ConfigStore::set_block_configuration( 'remote-data-blocks/example', [ + 'display_queries_to_selectors' => [ 'display' => [ 'display' ] ], 'queries' => [ 'display' => MockQuery::create(), ], @@ -156,6 +158,7 @@ public function test_track_remote_data_blocks_usage_tracks_multiple_nested_block ] ); ConfigStore::set_block_configuration( 'remote-data-blocks/example', [ + 'display_queries_to_selectors' => [ 'display' => [ 'display' ] ], 'queries' => [ 'display' => MockQuery::create(), ], @@ -203,6 +206,7 @@ public function test_track_remote_data_blocks_usage_tracks_fallback_blocks_in_ne ] ); ConfigStore::set_block_configuration( 'remote-data-blocks/example', [ + 'display_queries_to_selectors' => [ 'display' => [ 'display' ] ], 'queries' => [ 'display' => MockQuery::create(), ], @@ -273,6 +277,7 @@ public function test_track_remote_data_blocks_usage_tracks_nested_remote_data_bl ] ); ConfigStore::set_block_configuration( 'remote-data-blocks/example', [ + 'display_queries_to_selectors' => [ 'display' => [ 'display' ] ], 'queries' => [ 'display' => MockQuery::create(), ], diff --git a/tests/src/block-editor/filters/withBlockBinding.test.tsx b/tests/src/block-editor/filters/withBlockBinding.test.tsx index 209a74262..ab544bebb 100644 --- a/tests/src/block-editor/filters/withBlockBinding.test.tsx +++ b/tests/src/block-editor/filters/withBlockBinding.test.tsx @@ -42,12 +42,24 @@ describe( 'withBlockBinding', () => { const testBlockConfig: LocalizedBlockData = { config: { 'test/block': { - availableBindings: { field1: { name: 'Field 1', type: 'string' } }, + availableBindings: { key: { field1: { name: 'Field 1', type: 'string' } } }, availableOverrides: [], dataSourceType: 'test-source', name: 'test/block', - patterns: { default: 'test/block/pattern' }, - selectors: [], + patterns: { key: 'test/block/pattern' }, + displayQueriesToSelectors: { + key: { + name: 'test-name', + selectors: [ + { + query_key: 'key', + type: 'manual-input', + inputs: [], + name: 'test-name', + }, + ], + }, + }, settings: { category: 'widget', title: 'Test block', @@ -94,6 +106,8 @@ describe( 'withBlockBinding', () => { const remoteData = { blockName: 'test/block', results: createResults( [ { field1: 'value1' } ] ), + displayQueryKey: 'key', + selectorQueryKey: 'key', }; render( @@ -117,6 +131,8 @@ describe( 'withBlockBinding', () => { const remoteData = { blockName: 'test/block', results: createResults( [ { field1: 'value1' } ] ), + displayQueryKey: 'key', + selectorQueryKey: 'key', }; render( { const remoteData = { blockName: 'test/block', results: createResults( [ { field1: 'value1' } ] ), + displayQueryKey: 'key', + selectorQueryKey: 'key', }; render( { [ REMOTE_DATA_CONTEXT_KEY ]: { blockName: 'test/block', results: createResults( [ { title: 'New Title' } ] ), + displayQueryKey: 'key', + selectorQueryKey: 'key', }, }, name: 'test/block', @@ -222,6 +242,8 @@ describe( 'withBlockBinding', () => { [ REMOTE_DATA_CONTEXT_KEY ]: { blockName: 'test/block', results: createResults( [ { title: 'Matching Title' } ] ), + displayQueryKey: 'key', + selectorQueryKey: 'key', }, }, name: 'test/block', @@ -257,6 +279,8 @@ describe( 'withBlockBinding', () => { [ REMOTE_DATA_CONTEXT_KEY ]: { blockName: 'test/block', results: createResults( [ { title: 'New Title' } ] ), + displayQueryKey: 'key', + selectorQueryKey: 'key', }, }, name: 'test/block', diff --git a/tests/src/blocks/remote-data-container/components/modals/InputModal.test.tsx b/tests/src/blocks/remote-data-container/components/modals/InputModal.test.tsx index 28678d277..254c82541 100644 --- a/tests/src/blocks/remote-data-container/components/modals/InputModal.test.tsx +++ b/tests/src/blocks/remote-data-container/components/modals/InputModal.test.tsx @@ -17,7 +17,9 @@ describe( 'InputModal', () => { { slug: 'input2', name: 'Input 2', required: false, type: 'text' }, ] as InputVariable[], onSelect: mockOnSelect, + onKeySelect: vi.fn(), title: 'Test Modal', + selectorQueryKey: 'test-key', }; afterEach( cleanup ); diff --git a/tests/src/blocks/remote-data-container/components/panels/QueryInputsPanel.test.tsx b/tests/src/blocks/remote-data-container/components/panels/QueryInputsPanel.test.tsx index 1cb86c29e..bc4b9bcf5 100644 --- a/tests/src/blocks/remote-data-container/components/panels/QueryInputsPanel.test.tsx +++ b/tests/src/blocks/remote-data-container/components/panels/QueryInputsPanel.test.tsx @@ -5,7 +5,7 @@ import { describe, expect, it, vi } from 'vitest'; import { QueryInputsPanel } from '@/blocks/remote-data-container/components/panels/QueryInputsPanel'; describe( 'QueryInputsPanel', () => { - const selectors: BlockConfig[ 'selectors' ] = [ + const selectors: Selector[] = [ { inputs: [ { @@ -32,7 +32,8 @@ describe( 'QueryInputsPanel', () => { const remoteData: RemoteData = { blockName: 'test/block', metadata: {}, - queryKey: 'test_query_key', + selectorQueryKey: 'test_query_key', + displayQueryKey: 'test_display_key', queryInputs: [], resultId: 'test', results: [], diff --git a/tests/src/blocks/remote-data-template/components/loop-template/LoopTemplate.test.tsx b/tests/src/blocks/remote-data-template/components/loop-template/LoopTemplate.test.tsx index 765392761..cd5ff324d 100644 --- a/tests/src/blocks/remote-data-template/components/loop-template/LoopTemplate.test.tsx +++ b/tests/src/blocks/remote-data-template/components/loop-template/LoopTemplate.test.tsx @@ -9,7 +9,8 @@ describe( 'LoopTemplate', () => { blockName: 'test/block', metadata: {}, queryInputs: [ {} ], - queryKey: 'test-query', + selectorQueryKey: 'test-query', + displayQueryKey: 'test-display', resultId: 'test-result', results: [ { diff --git a/tests/src/utils/remote-data.test.ts b/tests/src/utils/remote-data.test.ts index 6b78853e2..985bd7427 100644 --- a/tests/src/utils/remote-data.test.ts +++ b/tests/src/utils/remote-data.test.ts @@ -129,6 +129,8 @@ describe( 'remote-data utils', () => { const migrated = migrateRemoteData( remoteData ); expect( migrated ).toEqual( { + displayQueryKey: 'display', + selectorQueryKey: 'display', queryInputs: [ { title: 'Title 1' } ], results: [ { diff --git a/types/localized-block-data.d.ts b/types/localized-block-data.d.ts index 2f27209c3..07161a29e 100644 --- a/types/localized-block-data.d.ts +++ b/types/localized-block-data.d.ts @@ -1,5 +1,6 @@ type RemoteDataBinding = Pick< RemoteDataResultFields, 'name' | 'type' >; -type AvailableBindings = Record< string, RemoteDataBinding >; +type AvailableBindingsForQueries = Record< string, AvailableBindingsForQuery >; +type AvailableBindingsForQuery = Record< string, RemoteDataBinding >; /** * This corresponds directly to the input schema defined by a query. @@ -17,6 +18,19 @@ interface InputVariable { type: string; } +interface DisplayQueryConfig { + name: string; + selectors: Selector[]; +} + +interface Selector { + image_url?: string; + inputs: InputVariable[]; + name: string; + query_key: string; + type: string; +} + interface InputVariableOverride { display_name?: string; help_text?: string; @@ -24,22 +38,13 @@ interface InputVariableOverride { } interface BlockConfig { - availableBindings: AvailableBindings; + availableBindings: AvailableBindingsForQueries; availableOverrides: InputVariableOverride[]; dataSourceType: string; instructions?: string; name: string; - patterns: { - default: string; - inner_blocks?: string; - }; - selectors: { - image_url?: string; - inputs: InputVariable[]; - name: string; - query_key: string; - type: string; - }[]; + patterns: Record< string, string >; + displayQueriesToSelectors: Record< string, DisplayQueryConfig >; settings: { category: string; description?: string; diff --git a/types/remote-data.d.ts b/types/remote-data.d.ts index 8364b6ca1..3e8186f6d 100644 --- a/types/remote-data.d.ts +++ b/types/remote-data.d.ts @@ -35,7 +35,8 @@ interface RemoteData { /** @deprecated */ queryInput?: RemoteDataQueryInput; queryInputs: RemoteDataQueryInput[]; - queryKey?: string; + displayQueryKey?: string; + selectorQueryKey?: string; resultId: string; results: RemoteDataApiResult[]; } @@ -54,7 +55,10 @@ interface RemoteDataTemplateBlockAttributes {} interface FieldSelection { action: 'add_field_shortcode' | 'update_field_shortcode' | 'reset_field_shortcode'; - remoteData?: Pick< RemoteData, 'blockName' | 'metadata' | 'queryInputs' | 'queryKey' >; + remoteData?: Pick< + RemoteData, + 'blockName' | 'metadata' | 'queryInputs' | 'displayQueryKey' | 'selectorQueryKey' + >; selectedField: string; selectionPath: 'select_new_tab' | 'select_existing_tab' | 'select_meta_tab' | 'popover'; type: 'field' | 'meta'; @@ -93,7 +97,8 @@ interface RemoteDataInnerBlockAttributes { interface RemoteDataApiRequest { block_name: string; query_inputs: RemoteDataQueryInput[]; - query_key: string; + display_query_key: string; + selector_query_key: string; } interface RemoteDataApiResult { @@ -108,7 +113,8 @@ interface RemoteDataApiResponseBody { metadata: Record< string, RemoteDataResultFields >; pagination?: RemoteDataPagination; query_inputs: RemoteDataQueryInput[]; - query_key: string; + display_query_key: string; + selector_query_key: string; result_id: string; results: RemoteDataApiResult[]; } diff --git a/types/wordpress__block-editor/index.d.ts b/types/wordpress__block-editor/index.d.ts index d3f4df97b..94e4e6004 100644 --- a/types/wordpress__block-editor/index.d.ts +++ b/types/wordpress__block-editor/index.d.ts @@ -34,6 +34,7 @@ declare module '@wordpress/block-editor' { source: string; syncStatus: string; title: string; + keywords?: string[]; } interface BlockEditorStoreActions {