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 4acaa3a4e..1bfd88c86 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; @@ -45,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 { @@ -89,7 +90,6 @@ public static function register_block_configuration( array $config ): array { 'availableBindings' => $available_bindings, 'availableOverrides' => $config['overrides'] ?? [], 'instructions' => $config['instructions'], - 'loop' => $config['loop'], 'name' => $block_name, 'dataSourceType' => ConfigStore::get_data_source_type( $block_name ), 'patterns' => $config['patterns'], @@ -103,7 +103,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/BlockManagement/ConfigRegistry.php b/inc/Editor/BlockManagement/ConfigRegistry.php index ca719b324..e9bf7fa1a 100644 --- a/inc/Editor/BlockManagement/ConfigRegistry.php +++ b/inc/Editor/BlockManagement/ConfigRegistry.php @@ -50,6 +50,8 @@ 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( @@ -66,7 +68,6 @@ public static function register_block( array $user_config = [] ): bool|WP_Error 'icon' => $user_config['icon'] ?? 'cloud', 'instructions' => $user_config['instructions'] ?? null, 'name' => $block_name, - 'loop' => $user_config[ self::RENDER_QUERY_KEY ]['loop'] ?? false, 'overrides' => $user_config['overrides'] ?? [], 'patterns' => [], 'queries' => [ @@ -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/inc/Editor/DataBinding/BlockBindings.php b/inc/Editor/DataBinding/BlockBindings.php index 1f78b1732..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 ); } - public static function remote_data_block_render_callback( array $attributes, string $content, WP_Block $block ): string { - // This is the parent block that provides the context, so we don't have + /** + * 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 { + // 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 remote_data_block_render_callback( array $attributes, str $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 remote_data_block_render_callback( array $attributes, str // 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/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 41bd756d5..874a377b8 100644 --- a/inc/Validation/ConfigSchemas.php +++ b/inc/Validation/ConfigSchemas.php @@ -91,7 +91,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/psalm.xml b/psalm.xml index eb2a53074..afb376890 100644 --- a/psalm.xml +++ b/psalm.xml @@ -24,6 +24,11 @@ + + + + + diff --git a/src/block-editor/filters/withBlockBinding.tsx b/src/block-editor/filters/withBlockBinding.tsx index 2d9acaa0c..c7871bd38 100644 --- a/src/block-editor/filters/withBlockBinding.tsx +++ b/src/block-editor/filters/withBlockBinding.tsx @@ -23,6 +23,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 +84,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/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/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/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/components/placeholders/ItemSelectQueryType.tsx b/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx index 05d41377b..e2d4f158b 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, Button } 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,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 ( void; + onSelect: ( input: RemoteDataQueryInput[] ) => void; } export function Placeholder( props: PlaceholderProps ) { const { blockConfig, onSelect } = props; - const { instructions, loop, settings } = blockConfig; + const { instructions, settings } = blockConfig; const iconElement: IconType = ( settings.icon as IconType ) ?? cloud; - const defaultInstructions = loop - ? __( 'This block displays a list of items.' ) - : __( 'This block requires selection of one or more items for display.' ); - return ( 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/edit.tsx b/src/blocks/remote-data-container/edit.tsx index bbeb17c80..63c376cd7 100644 --- a/src/blocks/remote-data-container/edit.tsx +++ b/src/blocks/remote-data-container/edit.tsx @@ -32,13 +32,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, @@ -148,11 +143,7 @@ export function Edit( props: BlockEditProps< RemoteDataBlockAttributes > ) { /> ) } - + ); 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/usePatterns.ts b/src/blocks/remote-data-container/hooks/usePatterns.ts index 4e43b6dfe..40638da5d 100644 --- a/src/blocks/remote-data-container/hooks/usePatterns.ts +++ b/src/blocks/remote-data-container/hooks/usePatterns.ts @@ -4,39 +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, allowedPatterns } = useSelect< + const { getPatternsByBlockTypes, allowedPatterns } = useSelect< BlockEditorStoreSelectors, Pick< BlockEditorStoreSelectors, 'getBlocks' | 'getPatternsByBlockTypes' > & { allowedPatterns: BlockPattern[]; @@ -62,13 +44,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 = allowedPatterns.filter( pattern => @@ -86,7 +61,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 ) ), } ) ); }, @@ -95,7 +70,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; } @@ -113,8 +89,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-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/render.php b/src/blocks/remote-data-container/render.php new file mode 100644 index 000000000..b4f8ef739 --- /dev/null +++ b/src/blocks/remote-data-container/render.php @@ -0,0 +1,10 @@ + { 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/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..c8b79d6c6 --- /dev/null +++ b/src/blocks/remote-data-template/render.php @@ -0,0 +1,10 @@ +; +} 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 @@
> , + 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/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/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 ); diff --git a/types/localized-block-data.d.ts b/types/localized-block-data.d.ts index 08ad38cb1..4c8bc14a4 100644 --- a/types/localized-block-data.d.ts +++ b/types/localized-block-data.d.ts @@ -26,7 +26,6 @@ interface BlockConfig { availableOverrides: InputVariableOverride[]; dataSourceType: string; instructions?: string; - loop: boolean; name: string; patterns: { default: string; 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;