diff --git a/inc/Editor/DataBinding/BlockBindings.php b/inc/Editor/DataBinding/BlockBindings.php index 9e6efbd8..be706c4d 100644 --- a/inc/Editor/DataBinding/BlockBindings.php +++ b/inc/Editor/DataBinding/BlockBindings.php @@ -236,7 +236,7 @@ public static function get_remote_value( array $block_context, array $source_arg return sprintf( '%s %s', $source_args['label'], $value ); } - return $value; + return strval( $value ); } public static function loop_block_render_callback( array $attributes, string $content, WP_Block $block ): string { diff --git a/src/blocks/remote-data-container/components/InnerBlocks.tsx b/src/blocks/remote-data-container/components/InnerBlocks.tsx index 0d8521ca..30c7e5f3 100644 --- a/src/blocks/remote-data-container/components/InnerBlocks.tsx +++ b/src/blocks/remote-data-container/components/InnerBlocks.tsx @@ -5,9 +5,7 @@ import { LoopTemplate } from '@/blocks/remote-data-container/components/loop-tem interface InnerBlocksProps { blockConfig: BlockConfig; - getInnerBlocks: ( - result: Record< string, string > - ) => BlockInstance< RemoteDataInnerBlockAttributes >[]; + getInnerBlocks: ( result: RemoteDataResult ) => BlockInstance< RemoteDataInnerBlockAttributes >[]; remoteData: RemoteData; } diff --git a/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelection.tsx b/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelection.tsx index 0c8eb3b5..83eab6a0 100644 --- a/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelection.tsx +++ b/src/blocks/remote-data-container/components/field-shortcode/FieldShortcodeSelection.tsx @@ -74,7 +74,8 @@ export function FieldSelectionFromAvailableBindings( props: FieldSelectionWithFi ...acc, [ fieldName ]: { name: binding.name, - value: fieldValue, + // eslint-disable-next-line @typescript-eslint/no-base-to-string + value: fieldValue?.toString() ?? '', }, }; }, diff --git a/src/blocks/remote-data-container/components/item-list/ItemList.tsx b/src/blocks/remote-data-container/components/item-list/ItemList.tsx index 45a6abdd..a4a713fd 100644 --- a/src/blocks/remote-data-container/components/item-list/ItemList.tsx +++ b/src/blocks/remote-data-container/components/item-list/ItemList.tsx @@ -9,7 +9,7 @@ interface ItemListProps { blockName: string; loading: boolean; onSelect: ( data: RemoteDataQueryInput ) => void; - results?: RemoteData[ 'results' ]; + results?: RemoteDataResult[]; searchTerms: string; setSearchTerms: ( newValue: string ) => void; } @@ -31,7 +31,7 @@ export function ItemList( props: ItemListProps ) { ? item[ Object.keys( item ).find( key => /(^|_)(id)$/i.test( key ) ) as string ] : instanceId, } - ) as RemoteData[ 'results' ]; + ) as RemoteDataResult[]; }, [ results ] ); // get fields from results data to use as columns @@ -62,7 +62,7 @@ export function ItemList( props: ItemListProps ) { id: string; label: string; enableGlobalSearch: boolean; - render?: ( { item }: { item: RemoteData[ 'results' ][ 0 ] } ) => JSX.Element; + render?: ( { item }: { item: RemoteDataResult } ) => JSX.Element; enableSorting: boolean; }[] = getFields.map( field => { return { @@ -71,7 +71,7 @@ export function ItemList( props: ItemListProps ) { enableGlobalSearch: true, render: field === media - ? ( { item }: { item: RemoteData[ 'results' ][ 0 ] } ) => { + ? ( { item }: { item: RemoteDataResult } ) => { return ( { __( 'Choose' ) }, isPrimary: true, label: '', - callback: ( items: RemoteData[ 'results' ] ) => { + callback: ( items: RemoteDataResult[] ) => { items.map( item => onSelect( item ) ); }, }, diff --git a/src/blocks/remote-data-container/components/loop-template/LoopTemplate.tsx b/src/blocks/remote-data-container/components/loop-template/LoopTemplate.tsx index 4e6c41d5..e6adcc14 100644 --- a/src/blocks/remote-data-container/components/loop-template/LoopTemplate.tsx +++ b/src/blocks/remote-data-container/components/loop-template/LoopTemplate.tsx @@ -13,9 +13,7 @@ import { LoopTemplateInnerBlocks } from '@/blocks/remote-data-container/componen import { LoopIndexContext } from '@/blocks/remote-data-container/context/LoopIndexContext'; interface LoopTemplateProps { - getInnerBlocks: ( - result: Record< string, string > - ) => BlockInstance< RemoteDataInnerBlockAttributes >[]; + getInnerBlocks: ( result: RemoteDataResult ) => BlockInstance< RemoteDataInnerBlockAttributes >[]; remoteData: RemoteData; } diff --git a/src/blocks/remote-data-container/hooks/usePatterns.ts b/src/blocks/remote-data-container/hooks/usePatterns.ts index d8c6be8e..336c3414 100644 --- a/src/blocks/remote-data-container/hooks/usePatterns.ts +++ b/src/blocks/remote-data-container/hooks/usePatterns.ts @@ -18,7 +18,7 @@ import { getBlockConfig } from '@/utils/localized-block-data'; export function cloneBlockWithAttributes( block: BlockInstance, - attributes: Record< string, string >, + attributes: RemoteDataResult, remoteDataBlockName: string ): BlockInstance { const mismatchedAttributes = getMismatchedAttributes( @@ -53,13 +53,13 @@ export function usePatterns( remoteDataBlockName: string, rootClientId: string = const returnValue = { defaultPattern, getInnerBlocks: ( - result: Record< string, string > + result: RemoteDataResult ): BlockInstance< RemoteDataInnerBlockAttributes >[] => { return getBlocks< RemoteDataInnerBlockAttributes >( rootClientId ).map( block => cloneBlockWithAttributes( block, result, remoteDataBlockName ) ); }, - getSupportedPatterns: ( result?: Record< string, string > ): BlockPattern[] => { + getSupportedPatterns: ( result?: RemoteDataResult ): BlockPattern[] => { const supportedPatterns = __experimentalGetAllowedPatterns( rootClientId ).filter( pattern => pattern?.blockTypes?.includes( remoteDataBlockName ) || diff --git a/src/utils/block-binding.ts b/src/utils/block-binding.ts index 311977b6..9c848840 100644 --- a/src/utils/block-binding.ts +++ b/src/utils/block-binding.ts @@ -19,20 +19,21 @@ function getAttributeValue( attributes: unknown, key: string | undefined | null } function getExpectedAttributeValue( - result?: Record< string, string >, + result?: Record< string, unknown >, args?: RemoteDataBlockBindingArgs ): string | null { if ( ! args?.field || ! result?.[ args.field ] ) { return null; } - let expectedValue = result[ args.field ]; + // See comment on toString() in getAttributeValue. + let expectedValue = result[ args.field ]?.toString() ?? ''; if ( args.label ) { const labelClass = getClassName( 'block-label' ); expectedValue = `${ args.label } ${ expectedValue }`; } - return expectedValue ?? null; + return expectedValue; } export function getBoundAttributeEntries( @@ -64,7 +65,7 @@ export function getBoundBlockClassName( export function getMismatchedAttributes( attributes: RemoteDataInnerBlockAttributes, - results: RemoteData[ 'results' ], + results: RemoteDataResult[], remoteDataBlockName: string, index = 0 ): Partial< RemoteDataInnerBlockAttributes > { diff --git a/tests/inc/Editor/DataBinding/BlockBindingsTest.php b/tests/inc/Editor/DataBinding/BlockBindingsTest.php index 1a67fe18..6fd4c772 100644 --- a/tests/inc/Editor/DataBinding/BlockBindingsTest.php +++ b/tests/inc/Editor/DataBinding/BlockBindingsTest.php @@ -334,4 +334,100 @@ public function execute( HttpQueryInterface $query, array $input_variables ): ar 'test_input_field' => 'override_value transformed', ] ); } + + /** + * @runInSeparateProcess + */ + public function test_get_remote_value(): void { + /** + * Mock the QueryRunner to return a result. + */ + $mock_qr = new MockQueryRunner(); + $mock_qr->addResult( 'output_field', 'Test Output Value' ); + + $block_context = [ + 'blockName' => self::MOCK_BLOCK_NAME, + 'queryInput' => [ + 'test_input_field' => 'test_value', + ], + ]; + + $input_schema = [ + 'test_input_field' => [ + 'name' => 'Test Input Field', + 'type' => 'string', + ], + ]; + + $mock_block_config = [ + 'queries' => [ + ConfigRegistry::DISPLAY_QUERY_KEY => MockQuery::from_array( [ + 'input_schema' => $input_schema, + 'output_schema' => self::MOCK_OUTPUT_SCHEMA, + 'query_runner' => $mock_qr, + ] ), + ], + ]; + + $mock_config_store = Mockery::namedMock( ConfigStore::class ); + $mock_config_store->shouldReceive( 'get_block_configuration' ) + ->once() + ->with( self::MOCK_BLOCK_NAME ) + ->andReturn( $mock_block_config ); + + $source_args = [ + 'field' => self::MOCK_OUTPUT_FIELD_NAME, + ]; + + $remote_value = BlockBindings::get_remote_value( $block_context, $source_args ); + $this->assertSame( $remote_value, self::MOCK_OUTPUT_FIELD_VALUE ); + } + + /** + * @runInSeparateProcess + */ + public function test_get_remote_value_with_non_string(): void { + /** + * Mock the QueryRunner to return a result. + */ + $mock_qr = new MockQueryRunner(); + $mock_qr->addResult( 'output_field', 123 ); + + $block_context = [ + 'blockName' => self::MOCK_BLOCK_NAME, + 'queryInput' => [ + 'test_input_field' => 'test_value', + ], + ]; + + $input_schema = [ + 'test_input_field' => [ + 'name' => 'Test Input Field', + 'type' => 'string', + ], + ]; + + $mock_block_config = [ + 'queries' => [ + ConfigRegistry::DISPLAY_QUERY_KEY => MockQuery::from_array( [ + 'input_schema' => $input_schema, + 'output_schema' => self::MOCK_OUTPUT_SCHEMA, + 'query_runner' => $mock_qr, + ] ), + ], + ]; + + $mock_config_store = Mockery::namedMock( ConfigStore::class ); + $mock_config_store->shouldReceive( 'get_block_configuration' ) + ->once() + ->with( self::MOCK_BLOCK_NAME ) + ->andReturn( $mock_block_config ); + + $source_args = [ + 'field' => self::MOCK_OUTPUT_FIELD_NAME, + ]; + + $remote_value = BlockBindings::get_remote_value( $block_context, $source_args ); + $this->assertSame( $remote_value, '123' ); + } } diff --git a/tests/src/utils/block-binding.test.ts b/tests/src/utils/block-binding.test.ts index dd249182..83edd21b 100644 --- a/tests/src/utils/block-binding.test.ts +++ b/tests/src/utils/block-binding.test.ts @@ -127,5 +127,43 @@ describe( 'block-binding utils', () => { content: 'Title My Title', } ); } ); + + it( 'should handle mismatched types', () => { + const block = 'test/block'; + const attributes: RemoteDataInnerBlockAttributes = { + content: '123', + metadata: { + bindings: { + content: { source: BLOCK_BINDING_SOURCE, args: { block, field: 'a_field' } }, + }, + }, + }; + + const results: Record< string, unknown >[] = [ { a_field: 123 } ]; + + const result = getMismatchedAttributes( attributes, results, block ); + + expect( result ).toEqual( {} ); + } ); + + it( 'should handle convert mismatched attributes to string', () => { + const block = 'test/block'; + const attributes: RemoteDataInnerBlockAttributes = { + content: '123', + metadata: { + bindings: { + content: { source: BLOCK_BINDING_SOURCE, args: { block, field: 'a_field' } }, + }, + }, + }; + + const results: Record< string, unknown >[] = [ { a_field: 1234 } ]; + + const result = getMismatchedAttributes( attributes, results, block ); + + expect( result ).toEqual( { + content: '1234', + } ); + } ); } ); } ); diff --git a/types/remote-data.d.ts b/types/remote-data.d.ts index f4e23d13..faa244ee 100644 --- a/types/remote-data.d.ts +++ b/types/remote-data.d.ts @@ -14,14 +14,17 @@ interface QueryInputOverride { sourceType: 'query_var'; } +type RemoteDataResult = Record< string, unknown >; +type RemoteDataQueryInput = Record< string, unknown >; + interface RemoteData { blockName: string; isCollection: boolean; metadata: Record< string, RemoteDataResultFields >; - queryInput: Record< string, string >; + queryInput: RemoteDataQueryInput; queryInputOverrides?: Record< string, QueryInputOverride >; resultId: string; - results: Record< string, string >[]; + results: RemoteDataResult[]; } interface RemoteDataBlockAttributes { @@ -62,8 +65,6 @@ interface RemoteDataInnerBlockAttributes { url?: string | RichTextData; } -type RemoteDataQueryInput = Record< string, string >; - interface RemoteDataApiRequest { block_name: string; query_key: string;