From 03e72098bdabd5e003cdd022b0a03bda9c27a43e Mon Sep 17 00:00:00 2001 From: chriszarate Date: Wed, 19 Mar 2025 15:33:28 -0400 Subject: [PATCH 01/10] Remove loop config property --- docs/ai.md | 1 - docs/extending/block-registration.md | 1 - .../westeros-houses/register.php | 1 - .../BlockManagement/BlockRegistration.php | 1 - inc/Editor/BlockManagement/ConfigRegistry.php | 1 - .../Airtable/AirtableIntegration.php | 1 - .../templates/block_registration.template | 1 - .../Google/Sheets/GoogleSheetsIntegration.php | 1 - .../templates/block_registration.template | 1 - inc/Validation/ConfigSchemas.php | 1 - tests/inc/Functions/FunctionsTest.php | 19 ------------------- .../filters/withBlockBinding.test.tsx | 1 - types/localized-block-data.d.ts | 1 - 13 files changed, 31 deletions(-) diff --git a/docs/ai.md b/docs/ai.md index 8be98d82c..570ce4aa4 100644 --- a/docs/ai.md +++ b/docs/ai.md @@ -3289,7 +3289,6 @@ function register_google_sheets_westeros_houses_blocks(): void { register_remote_data_block( [ 'title' => 'Westeros Houses List', 'render_query' => [ - 'loop' => true, 'query' => $list_westeros_houses_query, ], ] ); diff --git a/docs/extending/block-registration.md b/docs/extending/block-registration.md index 7902cb853..91f51736a 100644 --- a/docs/extending/block-registration.md +++ b/docs/extending/block-registration.md @@ -66,7 +66,6 @@ The human-friendly name of the block. It is also used to construct the block's n The render query is executed when the block is rendered and fetches the data that will be provided to block bindings. It is an array with the following properties: - `query` (required): An instance of [`QueryInterface`](./query.md) that fetches the data. -- `loop`: A boolean that indicates if the query returns a collection of data. If `true`, the block will be rendered for each item in the collection. If not provided `false` is the default. ### `selection_queries`: array (optional) diff --git a/example/google-sheets/westeros-houses/register.php b/example/google-sheets/westeros-houses/register.php index e54803729..08f41557e 100644 --- a/example/google-sheets/westeros-houses/register.php +++ b/example/google-sheets/westeros-houses/register.php @@ -148,7 +148,6 @@ function register_google_sheets_westeros_houses_blocks(): void { register_remote_data_block( [ 'title' => 'Westeros Houses List', 'render_query' => [ - 'loop' => true, 'query' => $list_westeros_houses_query, ], ] ); diff --git a/inc/Editor/BlockManagement/BlockRegistration.php b/inc/Editor/BlockManagement/BlockRegistration.php index 2da4b0515..06a2ed48b 100644 --- a/inc/Editor/BlockManagement/BlockRegistration.php +++ b/inc/Editor/BlockManagement/BlockRegistration.php @@ -88,7 +88,6 @@ public static function register_block_configuration( array $config ): array { $block_config = [ 'availableBindings' => $available_bindings, 'availableOverrides' => $config['overrides'] ?? [], - 'loop' => $config['loop'], 'name' => $block_name, 'dataSourceType' => ConfigStore::get_data_source_type( $block_name ), 'patterns' => $config['patterns'], diff --git a/inc/Editor/BlockManagement/ConfigRegistry.php b/inc/Editor/BlockManagement/ConfigRegistry.php index c10f2ffea..3d3887c2e 100644 --- a/inc/Editor/BlockManagement/ConfigRegistry.php +++ b/inc/Editor/BlockManagement/ConfigRegistry.php @@ -58,7 +58,6 @@ public static function register_block( array $user_config = [] ): bool|WP_Error 'description' => '', 'icon' => $user_config['icon'] ?? 'cloud', 'name' => $block_name, - 'loop' => $user_config[ self::RENDER_QUERY_KEY ]['loop'] ?? false, 'overrides' => $user_config['overrides'] ?? [], 'patterns' => [], 'queries' => [ diff --git a/inc/Integrations/Airtable/AirtableIntegration.php b/inc/Integrations/Airtable/AirtableIntegration.php index 4eb65769a..a12ed1918 100644 --- a/inc/Integrations/Airtable/AirtableIntegration.php +++ b/inc/Integrations/Airtable/AirtableIntegration.php @@ -68,7 +68,6 @@ public static function register_loop_blocks_for_airtable_data_source( [ 'title' => sprintf( '%s/%s Loop', $data_source->get_display_name(), $table['name'] ), 'render_query' => [ - 'loop' => true, 'query' => $list_query, ], ], diff --git a/inc/Integrations/Airtable/templates/block_registration.template b/inc/Integrations/Airtable/templates/block_registration.template index 4567964c7..df1b8d2f3 100644 --- a/inc/Integrations/Airtable/templates/block_registration.template +++ b/inc/Integrations/Airtable/templates/block_registration.template @@ -73,7 +73,6 @@ function register_airtable__{{BLOCK_REG_FN_SLUG}}__block(): void { register_remote_data_block( [ 'title' => sprintf( '%s/%s Loop', $data_source->get_display_name(), '{{TABLE_NAME}}' ), 'render_query' => [ - 'loop' => true, 'query' => $list_query, ], ] ); diff --git a/inc/Integrations/Google/Sheets/GoogleSheetsIntegration.php b/inc/Integrations/Google/Sheets/GoogleSheetsIntegration.php index 4828b05f3..dba36c40a 100644 --- a/inc/Integrations/Google/Sheets/GoogleSheetsIntegration.php +++ b/inc/Integrations/Google/Sheets/GoogleSheetsIntegration.php @@ -72,7 +72,6 @@ public static function register_loop_blocks_for_google_sheets_data_source( [ 'title' => sprintf( '%s/%s Loop', $data_source->get_display_name(), $sheet['name'] ), 'render_query' => [ - 'loop' => true, 'query' => $list_query, ], ], diff --git a/inc/Integrations/Google/Sheets/templates/block_registration.template b/inc/Integrations/Google/Sheets/templates/block_registration.template index bbaae71d4..cd83d986f 100644 --- a/inc/Integrations/Google/Sheets/templates/block_registration.template +++ b/inc/Integrations/Google/Sheets/templates/block_registration.template @@ -74,7 +74,6 @@ function register_google_sheets__{{BLOCK_REG_FN_SLUG}}__blocks(): void { register_remote_data_block( [ 'title' => sprintf( '%s List', $block_title ), 'render_query' => [ - 'loop' => true, 'query' => $list_query, ], ] ); diff --git a/inc/Validation/ConfigSchemas.php b/inc/Validation/ConfigSchemas.php index 03c9c68b5..e21fd1d0b 100644 --- a/inc/Validation/ConfigSchemas.php +++ b/inc/Validation/ConfigSchemas.php @@ -90,7 +90,6 @@ private static function generate_remote_data_block_config_schema(): array { Types::instance_of( QueryInterface::class ), Types::serialized_config_for( HttpQueryInterface::class ), ), - 'loop' => Types::nullable( Types::boolean() ), ] ), 'selection_queries' => Types::nullable( Types::list_of( diff --git a/tests/inc/Functions/FunctionsTest.php b/tests/inc/Functions/FunctionsTest.php index de2e4be33..49e573a0e 100644 --- a/tests/inc/Functions/FunctionsTest.php +++ b/tests/inc/Functions/FunctionsTest.php @@ -50,24 +50,6 @@ public function testRegisterBlock(): void { $this->assertIsArray( $config ); $this->assertSame( $block_name, $config['name'] ); $this->assertSame( 'Test Block', $config['title'] ); - $this->assertFalse( $config['loop'] ); - } - - public function testRegisterLoopBlock(): void { - register_remote_data_block( [ - 'title' => 'Loop Block', - 'render_query' => [ - 'loop' => true, - 'query' => $this->mock_list_query, - ], - ] ); - - $block_name = 'remote-data-blocks/loop-block'; - $this->assertTrue( ConfigStore::is_registered_block( $block_name ) ); - - $config = ConfigStore::get_block_configuration( $block_name ); - $this->assertIsArray( $config ); - $this->assertTrue( $config['loop'] ); } public function testRegisterBlockWithNestedConfig(): void { @@ -98,7 +80,6 @@ public function testRegisterBlockWithNestedConfig(): void { $this->assertIsArray( $config ); $this->assertSame( $block_name, $config['name'] ); $this->assertSame( 'Test Block with Nested Config', $config['title'] ); - $this->assertFalse( $config['loop'] ); } public function testRegisterListQuery(): void { diff --git a/tests/src/block-editor/filters/withBlockBinding.test.tsx b/tests/src/block-editor/filters/withBlockBinding.test.tsx index c62c1ad43..1fa2e8068 100644 --- a/tests/src/block-editor/filters/withBlockBinding.test.tsx +++ b/tests/src/block-editor/filters/withBlockBinding.test.tsx @@ -40,7 +40,6 @@ describe( 'withBlockBinding', () => { availableBindings: { field1: { name: 'Field 1', type: 'string' } }, availableOverrides: [], dataSourceType: 'test-source', - loop: false, name: 'test/block', patterns: { default: 'test/block/pattern' }, selectors: [], diff --git a/types/localized-block-data.d.ts b/types/localized-block-data.d.ts index 2856c562f..73e26b937 100644 --- a/types/localized-block-data.d.ts +++ b/types/localized-block-data.d.ts @@ -25,7 +25,6 @@ interface BlockConfig { availableBindings: AvailableBindings; availableOverrides: InputVariableOverride[]; dataSourceType: string; - loop: boolean; name: string; patterns: { default: string; From 1a815b11f5f22f70864310550ed2c290031d2d02 Mon Sep 17 00:00:00 2001 From: chriszarate Date: Wed, 19 Mar 2025 15:33:43 -0400 Subject: [PATCH 02/10] Remove loop-dependent code from remote-data-container block --- inc/Editor/BlockManagement/ConfigRegistry.php | 20 +++++++++++++++++++ .../components/FieldShortcodeSelectNew.tsx | 3 +-- .../placeholders/ItemSelectQueryType.tsx | 12 ++++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/inc/Editor/BlockManagement/ConfigRegistry.php b/inc/Editor/BlockManagement/ConfigRegistry.php index 3d3887c2e..0ce68e5e3 100644 --- a/inc/Editor/BlockManagement/ConfigRegistry.php +++ b/inc/Editor/BlockManagement/ConfigRegistry.php @@ -50,6 +50,7 @@ public static function register_block( array $user_config = [] ): bool|WP_Error $display_query = self::inflate_query( $user_config[ self::RENDER_QUERY_KEY ]['query'] ); $input_schema = $display_query->get_input_schema(); + $output_schema = $display_query->get_output_schema(); // Build the base configuration for the block. This is our own internal // configuration, not what will be passed to WordPress's register_block_type. @@ -82,6 +83,25 @@ public static function register_block( array $user_config = [] ): bool|WP_Error 'title' => $block_title, ]; + // If the render query has no required input variables and outputs a + // collection, we should provide a "load collection" selector. + $required_variables = array_filter( $input_schema, function ( array $input_var ): bool { + return true === ( $input_var['required'] ?? true ); + } ); + if ( empty( $required_variables ) && true === ( $output_schema['is_collection'] ?? false ) ) { + // Add a "load collection" selector. + array_unshift( + $config['selectors'], + [ + 'image_url' => $display_query->get_image_url(), + 'inputs' => [], + 'name' => 'Load collection', + 'query_key' => self::DISPLAY_QUERY_KEY, + 'type' => 'load-collection', + ] + ); + } + // 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 ) { diff --git a/src/block-editor/format-types/field-shortcode/components/FieldShortcodeSelectNew.tsx b/src/block-editor/format-types/field-shortcode/components/FieldShortcodeSelectNew.tsx index 2f2101cc4..194dc3bee 100644 --- a/src/block-editor/format-types/field-shortcode/components/FieldShortcodeSelectNew.tsx +++ b/src/block-editor/format-types/field-shortcode/components/FieldShortcodeSelectNew.tsx @@ -14,8 +14,7 @@ type FieldShortcodeSelectNewProps = Omit< DropdownMenuProps, 'label' > & { export function FieldShortcodeSelectNew( props: FieldShortcodeSelectNewProps ) { const { onSelectField, ...restProps } = props; const blockConfigs = getBlocksConfig(); - const nonLoopBlocks = Object.values( blockConfigs ).filter( ( { loop } ) => ! loop ); - const blocksByType = nonLoopBlocks.reduce< + const blocksByType = Object.values( blockConfigs ).reduce< Record< string, Array< BlocksConfig[ keyof BlocksConfig ] > > >( ( source, blockConfig ) => { const type = blockConfig.dataSourceType; diff --git a/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx b/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx index b5a7c33dc..f382eb78f 100644 --- a/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx +++ b/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx @@ -1,8 +1,8 @@ -import { ButtonGroup } from '@wordpress/components'; +import { Button, ButtonGroup } from '@wordpress/components'; -import { InputModal } from '../modals/InputModal'; -import { InputPopover } from '../popovers/InputPopover'; import { DataViewsModal } from '@/blocks/remote-data-container/components/modals/DataViewsModal'; +import { InputModal } from '@/blocks/remote-data-container/components/modals/InputModal'; +import { InputPopover } from '@/blocks/remote-data-container/components/popovers/InputPopover'; interface ItemSelectQueryTypeProps { blockConfig: BlockConfig; @@ -38,6 +38,12 @@ export function ItemSelectQueryType( props: ItemSelectQueryTypeProps ) { { ...selectorProps } /> ); + case 'load-collection': + return ( + + ); case 'input': return selector.inputs.length === 1 && selector.inputs[ 0 ] ? ( From 3a17158fe30deec7e6f6cb3f699b273a9ed02fc9 Mon Sep 17 00:00:00 2001 From: chriszarate Date: Thu, 20 Mar 2025 16:57:22 -0400 Subject: [PATCH 03/10] Move loop components to template block --- src/block-editor/filters/withBlockBinding.tsx | 15 ++++++++++++--- .../context/LoopIndexContext.ts | 3 --- src/blocks/remote-data-container/editor.scss | 6 ------ .../hooks/useRemoteDataContext.ts | 7 ------- .../components/item-preview}/ItemPreview.tsx | 0 .../components/loop-template/LoopTemplate.tsx | 10 +++++----- .../loop-template/LoopTemplateInnerBlocks.tsx | 0 .../loop-template/LoopTemplate.test.tsx | 2 +- .../LoopTemplateInnerBlocks.test.tsx | 2 +- 9 files changed, 19 insertions(+), 26 deletions(-) delete mode 100644 src/blocks/remote-data-container/context/LoopIndexContext.ts rename src/blocks/{remote-data-container/components/item-list => remote-data-template/components/item-preview}/ItemPreview.tsx (100%) rename src/blocks/{remote-data-container => remote-data-template}/components/loop-template/LoopTemplate.tsx (80%) rename src/blocks/{remote-data-container => remote-data-template}/components/loop-template/LoopTemplateInnerBlocks.tsx (100%) rename tests/src/blocks/{remote-data-container => remote-data-template}/components/loop-template/LoopTemplate.test.tsx (94%) rename tests/src/blocks/{remote-data-container => remote-data-template}/components/loop-template/LoopTemplateInnerBlocks.test.tsx (82%) diff --git a/src/block-editor/filters/withBlockBinding.tsx b/src/block-editor/filters/withBlockBinding.tsx index 2d9acaa0c..b00982871 100644 --- a/src/block-editor/filters/withBlockBinding.tsx +++ b/src/block-editor/filters/withBlockBinding.tsx @@ -6,6 +6,7 @@ import { __ } from '@wordpress/i18n'; import { BlockBindingControls } from '@/blocks/remote-data-container/components/BlockBindingControls'; import { useRemoteDataContext } from '@/blocks/remote-data-container/hooks/useRemoteDataContext'; +import { useLoopContext } from '@/blocks/remote-data-template/hooks/useLoopContext'; import { BLOCK_BINDING_SOURCE, PATTERN_OVERRIDES_BINDING_SOURCE, @@ -23,6 +24,12 @@ interface BoundBlockEditProps { setAttributes: ( attributes: RemoteDataInnerBlockAttributes ) => void; } +// This prop is provided by the `withPreviewIndex` filter, which is bundled with +// the Remote Data Template block. +interface BlockEditWithPreviewIndex { + previewIndex?: number; +} + function BoundBlockEdit( props: BoundBlockEditProps ) { const { attributes, availableBindings, blockName, remoteDataName, setAttributes } = props; const existingBindings = attributes.metadata?.bindings ?? {}; @@ -78,9 +85,11 @@ function BoundBlockEdit( props: BoundBlockEditProps ) { } export const withBlockBinding = createHigherOrderComponent( BlockEdit => { - return ( props: BlockEditProps< RemoteDataInnerBlockAttributes > ) => { - const { attributes, context, name, setAttributes } = props; - const { remoteData, index } = useRemoteDataContext( context ); + return ( + props: BlockEditProps< RemoteDataInnerBlockAttributes > & BlockEditWithPreviewIndex + ) => { + 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 ); diff --git a/src/blocks/remote-data-container/context/LoopIndexContext.ts b/src/blocks/remote-data-container/context/LoopIndexContext.ts deleted file mode 100644 index 5dafbda61..000000000 --- a/src/blocks/remote-data-container/context/LoopIndexContext.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from '@wordpress/element'; - -export const LoopIndexContext = createContext( { index: 0 } ); diff --git a/src/blocks/remote-data-container/editor.scss b/src/blocks/remote-data-container/editor.scss index 238fc2d4e..659f6da5e 100644 --- a/src/blocks/remote-data-container/editor.scss +++ b/src/blocks/remote-data-container/editor.scss @@ -33,12 +33,6 @@ h4.remote-data-blocks-new-item-heading { margin: 0.5em 0; } -// Loop template -.remote-data-blocks-loop-template { - list-style: none; - padding: 0; -} - // Block previews inside selector modals .remote-data-blocks-modal { // Column gap diff --git a/src/blocks/remote-data-container/hooks/useRemoteDataContext.ts b/src/blocks/remote-data-container/hooks/useRemoteDataContext.ts index ad5fce2c6..21be1821e 100644 --- a/src/blocks/remote-data-container/hooks/useRemoteDataContext.ts +++ b/src/blocks/remote-data-container/hooks/useRemoteDataContext.ts @@ -1,13 +1,9 @@ -import { useContext } from '@wordpress/element'; - import { REMOTE_DATA_CONTEXT_KEY } from '@/blocks/remote-data-container/config/constants'; -import { LoopIndexContext } from '@/blocks/remote-data-container/context/LoopIndexContext'; import { PATTERN_BLOCK_TYPE_POST_META_KEY } from '@/config/constants'; import { useEditedPostAttribute } from '@/hooks/useEditedPostAttribute'; import { getBlockConfig } from '@/utils/localized-block-data'; export interface RemoteDataContext { - index: number; remoteData?: RemoteData; } @@ -19,7 +15,6 @@ export function useRemoteDataContext( context: Record< string, unknown > ): Remo postMeta: getEditedPostAttribute< Record< string, unknown > >( 'meta' ) ?? {}, postType: getEditedPostAttribute< string >( 'type' ) ?? '', } ) ); - const { index } = useContext( LoopIndexContext ); if ( 'wp_block' === postType ) { const remoteDataBlockName = String( postMeta[ PATTERN_BLOCK_TYPE_POST_META_KEY ] ?? '' ); @@ -27,7 +22,6 @@ export function useRemoteDataContext( context: Record< string, unknown > ): Remo if ( blockConfig ) { return { - index, remoteData: { blockName: remoteDataBlockName, metadata: {}, @@ -56,7 +50,6 @@ export function useRemoteDataContext( context: Record< string, unknown > ): Remo } return { - index, remoteData: context[ REMOTE_DATA_CONTEXT_KEY ] as RemoteData | undefined, }; } diff --git a/src/blocks/remote-data-container/components/item-list/ItemPreview.tsx b/src/blocks/remote-data-template/components/item-preview/ItemPreview.tsx similarity index 100% rename from src/blocks/remote-data-container/components/item-list/ItemPreview.tsx rename to src/blocks/remote-data-template/components/item-preview/ItemPreview.tsx diff --git a/src/blocks/remote-data-container/components/loop-template/LoopTemplate.tsx b/src/blocks/remote-data-template/components/loop-template/LoopTemplate.tsx similarity index 80% rename from src/blocks/remote-data-container/components/loop-template/LoopTemplate.tsx rename to src/blocks/remote-data-template/components/loop-template/LoopTemplate.tsx index d0996f0d3..28a5ab529 100644 --- a/src/blocks/remote-data-container/components/loop-template/LoopTemplate.tsx +++ b/src/blocks/remote-data-template/components/loop-template/LoopTemplate.tsx @@ -8,9 +8,9 @@ import { useSelect } from '@wordpress/data'; import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { ItemPreview } from '@/blocks/remote-data-container/components/item-list/ItemPreview'; -import { LoopTemplateInnerBlocks } from '@/blocks/remote-data-container/components/loop-template/LoopTemplateInnerBlocks'; -import { LoopIndexContext } from '@/blocks/remote-data-container/context/LoopIndexContext'; +import { ItemPreview } from '@/blocks/remote-data-template/components/item-preview/ItemPreview'; +import { LoopTemplateInnerBlocks } from '@/blocks/remote-data-template/components/loop-template/LoopTemplateInnerBlocks'; +import { PreviewIndexContext } from '@/blocks/remote-data-template/context/PreviewIndexContext'; interface LoopTemplateProps { getInnerBlocks: ( @@ -44,14 +44,14 @@ export function LoopTemplate( props: LoopTemplateProps ) { { remoteData.results.map( ( result, index ) => { const isActive = index === activeBlockIndex; return ( - + setActiveBlockIndex( index ) } /> - + ); } ) } diff --git a/src/blocks/remote-data-container/components/loop-template/LoopTemplateInnerBlocks.tsx b/src/blocks/remote-data-template/components/loop-template/LoopTemplateInnerBlocks.tsx similarity index 100% rename from src/blocks/remote-data-container/components/loop-template/LoopTemplateInnerBlocks.tsx rename to src/blocks/remote-data-template/components/loop-template/LoopTemplateInnerBlocks.tsx diff --git a/tests/src/blocks/remote-data-container/components/loop-template/LoopTemplate.test.tsx b/tests/src/blocks/remote-data-template/components/loop-template/LoopTemplate.test.tsx similarity index 94% rename from tests/src/blocks/remote-data-container/components/loop-template/LoopTemplate.test.tsx rename to tests/src/blocks/remote-data-template/components/loop-template/LoopTemplate.test.tsx index e35770a6d..cdc2859fd 100644 --- a/tests/src/blocks/remote-data-container/components/loop-template/LoopTemplate.test.tsx +++ b/tests/src/blocks/remote-data-template/components/loop-template/LoopTemplate.test.tsx @@ -1,7 +1,7 @@ import { cleanup, render, screen } from '@testing-library/react'; import { afterEach, describe, expect, it } from 'vitest'; -import { LoopTemplate } from '@/blocks/remote-data-container/components/loop-template/LoopTemplate'; +import { LoopTemplate } from '@/blocks/remote-data-template/components/loop-template/LoopTemplate'; describe( 'LoopTemplate', () => { const mockGetInnerBlocks = () => []; diff --git a/tests/src/blocks/remote-data-container/components/loop-template/LoopTemplateInnerBlocks.test.tsx b/tests/src/blocks/remote-data-template/components/loop-template/LoopTemplateInnerBlocks.test.tsx similarity index 82% rename from tests/src/blocks/remote-data-container/components/loop-template/LoopTemplateInnerBlocks.test.tsx rename to tests/src/blocks/remote-data-template/components/loop-template/LoopTemplateInnerBlocks.test.tsx index 39c8c5cae..b69624fa1 100644 --- a/tests/src/blocks/remote-data-container/components/loop-template/LoopTemplateInnerBlocks.test.tsx +++ b/tests/src/blocks/remote-data-template/components/loop-template/LoopTemplateInnerBlocks.test.tsx @@ -1,7 +1,7 @@ import { cleanup, render } from '@testing-library/react'; import { afterEach, describe, expect, it } from 'vitest'; -import { LoopTemplateInnerBlocks } from '@/blocks/remote-data-container/components/loop-template/LoopTemplateInnerBlocks'; +import { LoopTemplateInnerBlocks } from '@/blocks/remote-data-template/components/loop-template/LoopTemplateInnerBlocks'; describe( 'LoopTemplateInnerBlocks', () => { afterEach( cleanup ); From 392e34ed9d4d5cc3fcacffeee6c138d8676d1139 Mon Sep 17 00:00:00 2001 From: chriszarate Date: Thu, 20 Mar 2025 23:27:37 -0400 Subject: [PATCH 04/10] Remove PlaceholderLoop --- .../components/placeholders/Placeholder.tsx | 23 ++++++++++----- .../placeholders/PlaceholderLoop.tsx | 29 ------------------- .../placeholders/PlaceholderSingle.tsx | 26 ----------------- 3 files changed, 16 insertions(+), 62 deletions(-) delete mode 100644 src/blocks/remote-data-container/components/placeholders/PlaceholderLoop.tsx delete mode 100644 src/blocks/remote-data-container/components/placeholders/PlaceholderSingle.tsx diff --git a/src/blocks/remote-data-container/components/placeholders/Placeholder.tsx b/src/blocks/remote-data-container/components/placeholders/Placeholder.tsx index 5b43a5f3b..953176fc6 100644 --- a/src/blocks/remote-data-container/components/placeholders/Placeholder.tsx +++ b/src/blocks/remote-data-container/components/placeholders/Placeholder.tsx @@ -1,5 +1,8 @@ -import { PlaceholderLoop } from '@/blocks/remote-data-container/components/placeholders/PlaceholderLoop'; -import { PlaceholderSingle } from '@/blocks/remote-data-container/components/placeholders/PlaceholderSingle'; +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; @@ -7,11 +10,17 @@ export interface PlaceholderProps { } export function Placeholder( props: PlaceholderProps ) { - const { loop } = props.blockConfig; + const { blockConfig, onSelect } = props; - if ( loop ) { - return ; - } + const iconElement: IconType = ( blockConfig.settings.icon as IconType ) ?? cloud; - return ; + return ( + + + + ); } diff --git a/src/blocks/remote-data-container/components/placeholders/PlaceholderLoop.tsx b/src/blocks/remote-data-container/components/placeholders/PlaceholderLoop.tsx deleted file mode 100644 index eec13fa52..000000000 --- a/src/blocks/remote-data-container/components/placeholders/PlaceholderLoop.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Button, Placeholder } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { cloud } from '@wordpress/icons'; - -interface PlaceholderLoopProps { - blockConfig: BlockConfig; - onSelect: ( data: RemoteDataQueryInput[] ) => void; -} - -export function PlaceholderLoop( props: PlaceholderLoopProps ) { - const { - blockConfig: { - settings: { title }, - }, - onSelect, - } = props; - - return ( - - - - ); -} diff --git a/src/blocks/remote-data-container/components/placeholders/PlaceholderSingle.tsx b/src/blocks/remote-data-container/components/placeholders/PlaceholderSingle.tsx deleted file mode 100644 index ff8a59507..000000000 --- a/src/blocks/remote-data-container/components/placeholders/PlaceholderSingle.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { IconType, Placeholder } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { cloud } from '@wordpress/icons'; - -import { ItemSelectQueryType } from '@/blocks/remote-data-container/components/placeholders/ItemSelectQueryType'; - -interface PlaceholderSingleProps { - blockConfig: BlockConfig; - onSelect: ( data: RemoteDataQueryInput[] ) => void; -} - -export function PlaceholderSingle( props: PlaceholderSingleProps ) { - const { blockConfig, onSelect } = props; - - const iconElement: IconType = ( blockConfig.settings.icon as IconType ) ?? cloud; - - return ( - - - - ); -} From effaac40dc4199577740be8b78a326f8213f14ec Mon Sep 17 00:00:00 2001 From: chriszarate Date: Fri, 21 Mar 2025 10:13:21 -0400 Subject: [PATCH 05/10] Explicitly use render.php for dynamic block rendering --- inc/Editor/BlockManagement/BlockRegistration.php | 2 -- inc/Editor/DataBinding/BlockBindings.php | 2 +- src/blocks/remote-data-container/block.json | 1 + src/blocks/remote-data-container/render.php | 12 ++++++++++++ 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 src/blocks/remote-data-container/render.php diff --git a/inc/Editor/BlockManagement/BlockRegistration.php b/inc/Editor/BlockManagement/BlockRegistration.php index 06a2ed48b..159830a5d 100644 --- a/inc/Editor/BlockManagement/BlockRegistration.php +++ b/inc/Editor/BlockManagement/BlockRegistration.php @@ -7,7 +7,6 @@ use RemoteDataBlocks\Editor\Assets\Assets; use RemoteDataBlocks\Telemetry\TracksTelemetry; use RemoteDataBlocks\Editor\BlockPatterns\BlockPatterns; -use RemoteDataBlocks\Editor\DataBinding\BlockBindings; use RemoteDataBlocks\REST\RemoteDataController; use function register_block_type; @@ -101,7 +100,6 @@ public static function register_block_configuration( array $config ): array { $block_options = [ 'name' => $block_name, - 'render_callback' => [ BlockBindings::class, 'remote_data_block_render_callback' ], 'title' => $config['title'], ]; diff --git a/inc/Editor/DataBinding/BlockBindings.php b/inc/Editor/DataBinding/BlockBindings.php index 1f78b1732..c1eaf0f7a 100644 --- a/inc/Editor/DataBinding/BlockBindings.php +++ b/inc/Editor/DataBinding/BlockBindings.php @@ -264,7 +264,7 @@ private static function get_block_fallback_content( string $field_name, array $b return Sanitizer::sanitize_primitive_type( 'string', $fallback_content ); } - public static function remote_data_block_render_callback( array $attributes, string $content, WP_Block $block ): string { + public static function render_remote_data_block( array $attributes, string $content, WP_Block $block ): string { // This is the parent block that provides the context, so we don't have // context available on the block's context property. However, context for // children blocks comes from this block's `remoteData` attribtue (see diff --git a/src/blocks/remote-data-container/block.json b/src/blocks/remote-data-container/block.json index 4fbab8f0a..8f732fa03 100644 --- a/src/blocks/remote-data-container/block.json +++ b/src/blocks/remote-data-container/block.json @@ -32,5 +32,6 @@ "textdomain": "remote-data-blocks", "editorScript": [ "file:./index.js", "remote-data-blocks-block-editor" ], "editorStyle": "file:./index.css", + "render": "file:./render.php", "style": "file:./style-index.css" } diff --git a/src/blocks/remote-data-container/render.php b/src/blocks/remote-data-container/render.php new file mode 100644 index 000000000..ac0174680 --- /dev/null +++ b/src/blocks/remote-data-container/render.php @@ -0,0 +1,12 @@ + Date: Fri, 21 Mar 2025 11:35:22 -0400 Subject: [PATCH 06/10] Add Remote Data Template block --- .../BlockManagement/BlockRegistration.php | 6 +- inc/Editor/DataBinding/BlockBindings.php | 57 ++++++++++++++++--- src/block-editor/filters/withBlockBinding.tsx | 1 - .../components/InnerBlocks.tsx | 24 +------- src/blocks/remote-data-container/edit.tsx | 15 +---- .../hooks/usePatterns.ts | 38 +++---------- src/blocks/remote-data-template/block.json | 20 +++++++ .../context/PreviewIndexContext.ts | 3 + src/blocks/remote-data-template/edit.tsx | 36 ++++++++++++ src/blocks/remote-data-template/editor.scss | 4 ++ .../remote-data-template/filters/index.ts | 9 +++ .../filters/withPreviewIndex.tsx | 12 ++++ .../hooks/useGetInnerBlocks.ts | 23 ++++++++ src/blocks/remote-data-template/index.ts | 15 +++++ src/blocks/remote-data-template/render.php | 12 ++++ src/blocks/remote-data-template/save.tsx | 5 ++ src/utils/block-binding.ts | 24 ++++++++ types/remote-data.d.ts | 3 +- 18 files changed, 232 insertions(+), 75 deletions(-) create mode 100644 src/blocks/remote-data-template/block.json create mode 100644 src/blocks/remote-data-template/context/PreviewIndexContext.ts create mode 100644 src/blocks/remote-data-template/edit.tsx create mode 100644 src/blocks/remote-data-template/editor.scss create mode 100644 src/blocks/remote-data-template/filters/index.ts create mode 100644 src/blocks/remote-data-template/filters/withPreviewIndex.tsx create mode 100644 src/blocks/remote-data-template/hooks/useGetInnerBlocks.ts create mode 100644 src/blocks/remote-data-template/index.ts create mode 100644 src/blocks/remote-data-template/render.php create mode 100644 src/blocks/remote-data-template/save.tsx diff --git a/inc/Editor/BlockManagement/BlockRegistration.php b/inc/Editor/BlockManagement/BlockRegistration.php index 159830a5d..e0906552b 100644 --- a/inc/Editor/BlockManagement/BlockRegistration.php +++ b/inc/Editor/BlockManagement/BlockRegistration.php @@ -44,8 +44,10 @@ public static function enqueue_block_assets(): void { public static function register_helper_blocks(): void { // Remote data HTML block - used to render HTML content in the absence of a proper binding. - $remote_data_html_block_path = REMOTE_DATA_BLOCKS__PLUGIN_DIRECTORY . '/build/blocks/remote-html'; - register_block_type( $remote_data_html_block_path ); + register_block_type( REMOTE_DATA_BLOCKS__PLUGIN_DIRECTORY . '/build/blocks/remote-html' ); + + // Remote data template - used to render remote data collections. + register_block_type( REMOTE_DATA_BLOCKS__PLUGIN_DIRECTORY . '/build/blocks/remote-data-template' ); } public static function register_container_blocks(): void { diff --git a/inc/Editor/DataBinding/BlockBindings.php b/inc/Editor/DataBinding/BlockBindings.php index c1eaf0f7a..562e8c3ef 100644 --- a/inc/Editor/DataBinding/BlockBindings.php +++ b/inc/Editor/DataBinding/BlockBindings.php @@ -264,14 +264,51 @@ private static function get_block_fallback_content( string $field_name, array $b return Sanitizer::sanitize_primitive_type( 'string', $fallback_content ); } + /** + * Find a "template block" in a parsed block's inner blocks. + * + * @param array $parsed_block The parsed block. + * @return bool True if a template block was found. + */ + private static function has_template_block( array $parsed_block ): bool { + foreach ( ( $parsed_block['innerBlocks'] ?? [] ) as $inner_block ) { + if ( 'remote-data-blocks/template' === $inner_block['blockName'] ) { + return true; + } + + // Recurse inner blocks. + if ( true === self::has_template_block( $inner_block ) ) { + return true; + } + } + + return false; + } + public static function render_remote_data_block( array $attributes, string $content, WP_Block $block ): string { - // This is the parent block that provides the context, so we don't have + // Look for a template block in the parsed block's inner blocks. If + // there is one, we can delegate to it for template rendering. + if ( self::has_template_block( $block->parsed_block ) ) { + return $block->render( [ 'dynamic' => false ] ); + } + + // Otherwise, use this block's inner blocks as the template. + return self::render_remote_data_template_block( $attributes, $content, $block ); + } + + public static function render_remote_data_template_block( array $attributes, string $content, WP_Block $block ): string { + // If already rendered, don't render dynamically again. + if ( isset( $block->parsed_block['dynamicallyRenderedContent'] ) ) { + return $block->parsed_block['dynamicallyRenderedContent']; + } + + // If this is the parent block that *provides* the context, we won't have // context available on the block's context property. However, context for // children blocks comes from this block's `remoteData` attribtue (see // block.json#providesContext), so we can access it directly. - $block_context = $attributes['remoteData'] ?? []; + $block_context = $block->context[ self::$context_name ] ?? $attributes['remoteData'] ?? []; $block_name = $block_context['blockName'] ?? null; - $operation_name = 'remote_data_block_render_callback'; + $operation_name = 'remote_data_block_render'; $query_response = self::execute_queries( $block_context, [], $operation_name ); @@ -289,7 +326,8 @@ public static function render_remote_data_block( array $attributes, string $cont $loop_template = $block->parsed_block['innerBlocks']; $loop_template_content = $block->parsed_block['innerContent']; - + + // Remove the existing blocks and content so that we can repopulate it. $block->parsed_block['innerBlocks'] = []; $block->parsed_block['innerContent'] = []; @@ -311,9 +349,14 @@ public static function render_remote_data_block( array $attributes, string $cont // Create an updated block with the new inner blocks and content. $updated_block = new WP_Block( $block->parsed_block ); - // Render the updated block but set dynamic to false so that we don't have - // recursion. - return $updated_block->render( [ 'dynamic' => false ] ); + // Render the updated block but set dynamic to false so that we don't + // have recursion. Save the rendered output in a property on the + // parsed block, which will not be persisted. This is needed because + // our container block can trigger a non-dynamic re-render. This helps + // avoid descendant dynamic blocks from being rendered twice. + $block->parsed_block['dynamicallyRenderedContent'] = $updated_block->render( [ 'dynamic' => false ] ); + + return $block->parsed_block['dynamicallyRenderedContent']; } /** diff --git a/src/block-editor/filters/withBlockBinding.tsx b/src/block-editor/filters/withBlockBinding.tsx index b00982871..c7871bd38 100644 --- a/src/block-editor/filters/withBlockBinding.tsx +++ b/src/block-editor/filters/withBlockBinding.tsx @@ -6,7 +6,6 @@ import { __ } from '@wordpress/i18n'; import { BlockBindingControls } from '@/blocks/remote-data-container/components/BlockBindingControls'; import { useRemoteDataContext } from '@/blocks/remote-data-container/hooks/useRemoteDataContext'; -import { useLoopContext } from '@/blocks/remote-data-template/hooks/useLoopContext'; import { BLOCK_BINDING_SOURCE, PATTERN_OVERRIDES_BINDING_SOURCE, diff --git a/src/blocks/remote-data-container/components/InnerBlocks.tsx b/src/blocks/remote-data-container/components/InnerBlocks.tsx index c0b53f4ff..ec886a38b 100644 --- a/src/blocks/remote-data-container/components/InnerBlocks.tsx +++ b/src/blocks/remote-data-container/components/InnerBlocks.tsx @@ -1,26 +1,6 @@ import { InnerBlocks as CoreInnerBlocks } from '@wordpress/block-editor'; -import { BlockInstance } from '@wordpress/blocks'; - -import { LoopTemplate } from '@/blocks/remote-data-container/components/loop-template/LoopTemplate'; - -interface InnerBlocksProps { - blockConfig: BlockConfig; - getInnerBlocks: ( - result: RemoteDataApiResult - ) => BlockInstance< RemoteDataInnerBlockAttributes >[]; - remoteData: RemoteData; -} - -export function InnerBlocks( props: InnerBlocksProps ) { - const { - blockConfig: { loop }, - getInnerBlocks, - remoteData, - } = props; - - if ( loop || remoteData.results.length > 1 ) { - return ; - } +// This component wraps the Core InnerBlocks component to enable the renderAppender. +export function InnerBlocks() { return ; } diff --git a/src/blocks/remote-data-container/edit.tsx b/src/blocks/remote-data-container/edit.tsx index 2eb305d14..d7b0d2768 100644 --- a/src/blocks/remote-data-container/edit.tsx +++ b/src/blocks/remote-data-container/edit.tsx @@ -31,13 +31,8 @@ export function Edit( props: BlockEditProps< RemoteDataBlockAttributes > ) { const blockProps = useBlockProps( { className: CONTAINER_CLASS_NAME } ); const remoteDataAttribute = migrateRemoteData( props.attributes.remoteData ); - const { - getInnerBlocks, - getSupportedPatterns, - innerBlocksPattern, - insertPatternBlocks, - resetInnerBlocks, - } = usePatterns( blockName, rootClientId ); + const { getSupportedPatterns, innerBlocksPattern, insertPatternBlocks, resetInnerBlocks } = + usePatterns( blockName, rootClientId ); const { data, fetch, loading, reset } = useRemoteData( { blockName, @@ -133,11 +128,7 @@ export function Edit( props: BlockEditProps< RemoteDataBlockAttributes > ) { /> ) } - + ); diff --git a/src/blocks/remote-data-container/hooks/usePatterns.ts b/src/blocks/remote-data-container/hooks/usePatterns.ts index 683c07643..db87459f8 100644 --- a/src/blocks/remote-data-container/hooks/usePatterns.ts +++ b/src/blocks/remote-data-container/hooks/usePatterns.ts @@ -4,38 +4,21 @@ import { BlockPattern, store as blockEditorStore, } from '@wordpress/block-editor'; -import { BlockInstance, cloneBlock, createBlock } from '@wordpress/blocks'; +import { cloneBlock, createBlock } from '@wordpress/blocks'; import { useDispatch, useSelect } from '@wordpress/data'; import { + cloneBlockForPreview, getBoundAttributeEntries, - getMismatchedAttributes, hasBlockBinding, isSyncedPattern, } from '@/utils/block-binding'; import { getBlockConfig } from '@/utils/localized-block-data'; -export function cloneBlockWithAttributes( - block: BlockInstance, - attributes: RemoteDataApiResult, - remoteDataBlockName: string -): BlockInstance { - const mismatchedAttributes = getMismatchedAttributes( - block.attributes, - [ attributes ], - remoteDataBlockName - ); - const newInnerBlocks = block.innerBlocks?.map( innerBlock => - cloneBlockWithAttributes( innerBlock, attributes, remoteDataBlockName ) - ); - - return cloneBlock( block, mismatchedAttributes, newInnerBlocks ); -} - export function usePatterns( remoteDataBlockName: string, rootClientId: string = '' ) { const { patterns } = getBlockConfig( remoteDataBlockName ) ?? {}; const { replaceInnerBlocks } = useDispatch< BlockEditorStoreActions >( blockEditorStore ); - const { getBlocks, getPatternsByBlockTypes, __experimentalGetAllowedPatterns } = + const { getPatternsByBlockTypes, __experimentalGetAllowedPatterns } = useSelect< BlockEditorStoreSelectors >( blockEditorStore, [ remoteDataBlockName, [ remoteDataBlockName, rootClientId ], @@ -50,13 +33,6 @@ export function usePatterns( remoteDataBlockName: string, rootClientId: string = const returnValue = { defaultPattern, - getInnerBlocks: ( - result: RemoteDataApiResult - ): BlockInstance< RemoteDataInnerBlockAttributes >[] => { - return getBlocks< RemoteDataInnerBlockAttributes >( rootClientId ).map( block => - cloneBlockWithAttributes( block, result, remoteDataBlockName ) - ); - }, getSupportedPatterns: ( result?: RemoteDataApiResult ): BlockPattern[] => { const supportedPatterns = __experimentalGetAllowedPatterns( rootClientId ).filter( pattern => @@ -74,7 +50,7 @@ export function usePatterns( remoteDataBlockName: string, rootClientId: string = return supportedPatterns.map( pattern => ( { ...pattern, blocks: pattern.blocks.map( block => - cloneBlockWithAttributes( block, result, remoteDataBlockName ) + cloneBlockForPreview( block, result, remoteDataBlockName ) ), } ) ); }, @@ -83,7 +59,8 @@ export function usePatterns( remoteDataBlockName: string, rootClientId: string = // If the pattern is a synced pattern, insert it directly. if ( isSyncedPattern( pattern ) ) { const syncedPattern = createBlock( 'core/block', { ref: pattern.id } ); - replaceInnerBlocks( rootClientId, [ syncedPattern ] ).catch( () => {} ); + const loopTemplate = createBlock( 'remote-data-blocks/template', {}, [ syncedPattern ] ); + replaceInnerBlocks( rootClientId, [ loopTemplate ] ).catch( () => {} ); return; } @@ -101,8 +78,9 @@ export function usePatterns( remoteDataBlockName: string, rootClientId: string = return cloneBlock( block ); } ) ?? []; + const loopTemplate = createBlock( 'remote-data-blocks/template', {}, patternBlocks ); - replaceInnerBlocks( rootClientId, patternBlocks ).catch( () => {} ); + replaceInnerBlocks( rootClientId, [ loopTemplate ] ).catch( () => {} ); }, resetInnerBlocks: (): void => { replaceInnerBlocks( rootClientId, [] ).catch( () => {} ); diff --git a/src/blocks/remote-data-template/block.json b/src/blocks/remote-data-template/block.json new file mode 100644 index 000000000..62af8ef59 --- /dev/null +++ b/src/blocks/remote-data-template/block.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "remote-data-blocks/template", + "version": "0.1.0", + "usesContext": [ "remote-data-blocks/remoteData" ], + "title": "Remote Data Template", + "category": "widgets", + "description": "Template block for displaying remote data collections", + "example": {}, + "supports": { + "customClassName": false, + "className": false, + "html": false + }, + "textdomain": "remote-data-blocks", + "editorScript": "file:./index.js", + "render": "file:./render.php", + "editorStyle": "file:./index.css" +} diff --git a/src/blocks/remote-data-template/context/PreviewIndexContext.ts b/src/blocks/remote-data-template/context/PreviewIndexContext.ts new file mode 100644 index 000000000..19103a763 --- /dev/null +++ b/src/blocks/remote-data-template/context/PreviewIndexContext.ts @@ -0,0 +1,3 @@ +import { createContext } from '@wordpress/element'; + +export const PreviewIndexContext = createContext< number >( 0 ); diff --git a/src/blocks/remote-data-template/edit.tsx b/src/blocks/remote-data-template/edit.tsx new file mode 100644 index 000000000..1fcd6b7eb --- /dev/null +++ b/src/blocks/remote-data-template/edit.tsx @@ -0,0 +1,36 @@ +/** + * WordPress dependencies + */ +import { useBlockProps } from '@wordpress/block-editor'; +import { BlockEditProps } from '@wordpress/blocks'; +import { Placeholder } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +import { useRemoteDataContext } from '@/blocks/remote-data-container/hooks/useRemoteDataContext'; +import { LoopTemplate } from '@/blocks/remote-data-template/components/loop-template/LoopTemplate'; +import { useGetInnerBlocks } from '@/blocks/remote-data-template/hooks/useGetInnerBlocks'; + +import './editor.scss'; + +export function Edit( props: BlockEditProps< RemoteDataTemplateBlockAttributes > ): JSX.Element { + const { clientId, context, name } = props; + const blockProps = useBlockProps(); + + const { remoteData } = useRemoteDataContext( context ); + const getInnerBlocks = useGetInnerBlocks( name, clientId, remoteData?.blockName ); + + if ( ! remoteData?.blockName ) { + return ( +
+ +
+ ); + } + + return ; +} diff --git a/src/blocks/remote-data-template/editor.scss b/src/blocks/remote-data-template/editor.scss new file mode 100644 index 000000000..a4f2c2cdc --- /dev/null +++ b/src/blocks/remote-data-template/editor.scss @@ -0,0 +1,4 @@ +.remote-data-blocks-loop-template { + list-style: none; + padding: 0; +} diff --git a/src/blocks/remote-data-template/filters/index.ts b/src/blocks/remote-data-template/filters/index.ts new file mode 100644 index 000000000..5190990d5 --- /dev/null +++ b/src/blocks/remote-data-template/filters/index.ts @@ -0,0 +1,9 @@ +import { addFilter } from '@wordpress/hooks'; + +import { withPreviewIndex } from './withPreviewIndex'; + +/** + * Use a filter to wrap the block edit component and inject the preview index + * when we are rendering the template block for collections. + */ +addFilter( 'editor.BlockEdit', 'remote-data-blocks/withPreviewIndex', withPreviewIndex ); diff --git a/src/blocks/remote-data-template/filters/withPreviewIndex.tsx b/src/blocks/remote-data-template/filters/withPreviewIndex.tsx new file mode 100644 index 000000000..b9d7e6318 --- /dev/null +++ b/src/blocks/remote-data-template/filters/withPreviewIndex.tsx @@ -0,0 +1,12 @@ +import { BlockEditProps } from '@wordpress/blocks'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { useContext } from '@wordpress/element'; + +import { PreviewIndexContext } from '../context/PreviewIndexContext'; + +export const withPreviewIndex = createHigherOrderComponent( BlockEdit => { + return ( props: BlockEditProps< RemoteDataInnerBlockAttributes > ) => { + const previewIndex = useContext( PreviewIndexContext ); + return ; + }; +}, 'withPreviewIndex' ); diff --git a/src/blocks/remote-data-template/hooks/useGetInnerBlocks.ts b/src/blocks/remote-data-template/hooks/useGetInnerBlocks.ts new file mode 100644 index 000000000..bacf38bf4 --- /dev/null +++ b/src/blocks/remote-data-template/hooks/useGetInnerBlocks.ts @@ -0,0 +1,23 @@ +import { BlockEditorStoreSelectors, store as blockEditorStore } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; + +import { cloneBlockForPreview } from '@/utils/block-binding'; + +import type { BlockInstance } from '@wordpress/blocks'; + +export function useGetInnerBlocks( + blockName: string, + clientId: string, + remoteDataBlockName?: string +) { + const { getBlocks } = useSelect< BlockEditorStoreSelectors >( blockEditorStore, [ + blockName, + [ blockName, clientId ], + ] ); + + return ( result: RemoteDataApiResult ): BlockInstance< RemoteDataInnerBlockAttributes >[] => { + return getBlocks( clientId ).map( block => + cloneBlockForPreview( block, result, remoteDataBlockName ?? blockName ) + ); + }; +} diff --git a/src/blocks/remote-data-template/index.ts b/src/blocks/remote-data-template/index.ts new file mode 100644 index 000000000..0d4d661fc --- /dev/null +++ b/src/blocks/remote-data-template/index.ts @@ -0,0 +1,15 @@ +import { registerBlockType } from '@wordpress/blocks'; +import { post } from '@wordpress/icons'; + +import metadata from './block.json'; +import { Edit } from './edit'; +import { Save } from './save'; +import './filters'; + +registerBlockType< RemoteDataTemplateBlockAttributes >( metadata.name, { + edit: Edit, + icon: { + src: post, + }, + save: Save, +} ); diff --git a/src/blocks/remote-data-template/render.php b/src/blocks/remote-data-template/render.php new file mode 100644 index 000000000..319a97971 --- /dev/null +++ b/src/blocks/remote-data-template/render.php @@ -0,0 +1,12 @@ +; +} diff --git a/src/utils/block-binding.ts b/src/utils/block-binding.ts index 987dcceb2..a282094a9 100644 --- a/src/utils/block-binding.ts +++ b/src/utils/block-binding.ts @@ -1,3 +1,5 @@ +import { cloneBlock } from '@wordpress/blocks'; + import { BLOCK_BINDING_SOURCE } from '@/config/constants'; import { getRemoteDataResultValue } from '@/utils/remote-data'; import { getClassName } from '@/utils/string'; @@ -6,6 +8,28 @@ import { isObjectWithStringKeys } from '@/utils/type-narrowing'; import type { BlockPattern } from '@wordpress/block-editor'; import type { BlockInstance } from '@wordpress/blocks'; +/** + * Clone a block and inject remote data so that it can be previewed, either for + * a pattern preview or a template preview. + */ +export function cloneBlockForPreview( + block: BlockInstance< RemoteDataInnerBlockAttributes >, + result: RemoteDataApiResult, + remoteDataBlockName: string +): BlockInstance { + const newInnerBlocks = block.innerBlocks?.map( innerBlock => + cloneBlockForPreview( innerBlock, result, remoteDataBlockName ) + ); + + const mismatchedAttributes = getMismatchedAttributes( + block.attributes, + [ result ], + remoteDataBlockName + ); + + return cloneBlock( block, mismatchedAttributes, newInnerBlocks ); +} + function getAttributeValue( attributes: unknown, key: string | undefined | null ): string { if ( ! key || ! isObjectWithStringKeys( attributes ) ) { return ''; diff --git a/types/remote-data.d.ts b/types/remote-data.d.ts index a2f87e1e9..d189504d9 100644 --- a/types/remote-data.d.ts +++ b/types/remote-data.d.ts @@ -36,6 +36,8 @@ interface RemoteDataBlockAttributes { remoteData?: RemoteData; } +interface RemoteDataTemplateBlockAttributes {} + interface FieldSelection { action: 'add_field_shortcode' | 'update_field_shortcode' | 'reset_field_shortcode'; remoteData?: Pick< RemoteData, 'blockName' | 'metadata' | 'queryInputs' | 'queryKey' >; @@ -67,7 +69,6 @@ interface RemoteDataInnerBlockAttributes { alt?: string | StringSeriablizable; className?: string; content?: string | StringSeriablizable; - index?: number; metadata?: { bindings?: Record< string, RemoteDataBlockBinding >; name?: string; From f1e9718eae46278d6df155ec191c114d6f757ccc Mon Sep 17 00:00:00 2001 From: chriszarate Date: Fri, 21 Mar 2025 17:16:56 -0400 Subject: [PATCH 07/10] Psalm calm --- psalm.xml | 5 +++++ src/blocks/remote-data-container/render.php | 1 - src/blocks/remote-data-template/render.php | 1 - src/blocks/remote-html/render.php | 8 -------- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/psalm.xml b/psalm.xml index eb2a53074..afb376890 100644 --- a/psalm.xml +++ b/psalm.xml @@ -24,6 +24,11 @@ + + + + + diff --git a/src/blocks/remote-data-container/render.php b/src/blocks/remote-data-container/render.php index ac0174680..7bed1afde 100644 --- a/src/blocks/remote-data-container/render.php +++ b/src/blocks/remote-data-container/render.php @@ -8,5 +8,4 @@ // $content (string): The block default content. // $block (WP_Block): The block instance. -// @psalm-suppress UndefinedGlobalVariable echo wp_kses_post( BlockBindings::render_remote_data_block( $attributes, $content, $block ) ); diff --git a/src/blocks/remote-data-template/render.php b/src/blocks/remote-data-template/render.php index 319a97971..68ddba4a6 100644 --- a/src/blocks/remote-data-template/render.php +++ b/src/blocks/remote-data-template/render.php @@ -8,5 +8,4 @@ // $content (string): The block default content. // $block (WP_Block): The block instance. -// @psalm-suppress UndefinedGlobalVariable echo wp_kses_post( BlockBindings::render_remote_data_template_block( $attributes, $content, $block ) ); diff --git a/src/blocks/remote-html/render.php b/src/blocks/remote-html/render.php index 137a69e14..c803a3940 100644 --- a/src/blocks/remote-html/render.php +++ b/src/blocks/remote-html/render.php @@ -12,20 +12,12 @@
> Date: Fri, 21 Mar 2025 17:36:31 -0400 Subject: [PATCH 08/10] Yes chef --- src/blocks/remote-data-container/render.php | 1 - src/blocks/remote-data-template/render.php | 1 - 2 files changed, 2 deletions(-) diff --git a/src/blocks/remote-data-container/render.php b/src/blocks/remote-data-container/render.php index 7bed1afde..b4f8ef739 100644 --- a/src/blocks/remote-data-container/render.php +++ b/src/blocks/remote-data-container/render.php @@ -1,7 +1,6 @@ Date: Wed, 26 Mar 2025 18:44:06 -0100 Subject: [PATCH 09/10] Fix bad merge from earlier --- inc/Editor/BlockManagement/ConfigRegistry.php | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/inc/Editor/BlockManagement/ConfigRegistry.php b/inc/Editor/BlockManagement/ConfigRegistry.php index dae68694d..cc6421d47 100644 --- a/inc/Editor/BlockManagement/ConfigRegistry.php +++ b/inc/Editor/BlockManagement/ConfigRegistry.php @@ -91,25 +91,6 @@ public static function register_block( array $user_config = [] ): bool|WP_Error 'title' => $block_title, ]; - // If the render query has no required input variables and outputs a - // collection, we should provide a "load collection" selector. - $required_variables = array_filter( $input_schema, function ( array $input_var ): bool { - return true === ( $input_var['required'] ?? true ); - } ); - if ( empty( $required_variables ) && true === ( $output_schema['is_collection'] ?? false ) ) { - // Add a "load collection" selector. - array_unshift( - $config['selectors'], - [ - 'image_url' => $display_query->get_image_url(), - 'inputs' => [], - 'name' => 'Load collection', - 'query_key' => self::DISPLAY_QUERY_KEY, - 'type' => 'load-collection', - ] - ); - } - // Register "selectors" which allow the user to use a query to assist in // selecting data for display by the block. foreach ( $user_config[ self::SELECTION_QUERIES_KEY ] ?? [] as $selection_query ) { From 3ba760afcc419718230012e0e6a8ddaf35a28f08 Mon Sep 17 00:00:00 2001 From: chriszarate Date: Wed, 26 Mar 2025 18:57:02 -0100 Subject: [PATCH 10/10] Separate collection from input decision --- inc/Editor/BlockManagement/ConfigRegistry.php | 5 +++-- .../components/placeholders/ItemSelectQueryType.tsx | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/inc/Editor/BlockManagement/ConfigRegistry.php b/inc/Editor/BlockManagement/ConfigRegistry.php index cc6421d47..e9bf7fa1a 100644 --- a/inc/Editor/BlockManagement/ConfigRegistry.php +++ b/inc/Editor/BlockManagement/ConfigRegistry.php @@ -51,6 +51,7 @@ public static function register_block( array $user_config = [] ): bool|WP_Error $display_query = self::inflate_query( $user_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 ); // Check if any variables are required $has_required_variables = array_reduce( @@ -83,9 +84,9 @@ public static function register_block( array $user_config = [] ): bool|WP_Error 'type' => $input_var['type'] ?? 'string', ]; }, array_keys( $input_schema ), array_values( $input_schema ) ), - 'name' => $has_required_variables ? 'Manual input' : 'Load collection', + 'name' => $has_required_variables ? 'Manual input' : ( $is_collection ? 'Load collection' : 'Load item' ), 'query_key' => self::DISPLAY_QUERY_KEY, - 'type' => $has_required_variables ? 'input' : 'collection', + 'type' => $has_required_variables ? 'manual-input' : 'load-without-input', ], ], 'title' => $block_title, diff --git a/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx b/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx index 523bd7410..e2d4f158b 100644 --- a/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx +++ b/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx @@ -38,13 +38,13 @@ export function ItemSelectQueryType( props: ItemSelectQueryTypeProps ) { { ...selectorProps } /> ); - case 'collection': + case 'load-without-input': return ( ); - case 'input': + case 'manual-input': if ( selector.inputs.length === 1 && selector.inputs[ 0 ] ) { return (