Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions example/rest-api/art-institute/art-institute.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,51 @@ function register_aic_block(): void {
],
]);

$collection_query = HttpQuery::from_array([
'data_source' => $aic_data_source,
'endpoint' => function ( array $input_variables ) use ( $aic_data_source ): string {
$endpoint = $aic_data_source->get_endpoint();
return add_query_arg( [
'limit' => $input_variables['limit'],
'fields' => 'id,title,image_id,artist_title',
], $endpoint );
},
'input_schema' => [
'limit' => [
'name' => 'Limit',
'type' => 'ui:input',
'default_value' => 10,
],
],
'output_schema' => [
'is_collection' => true,
'path' => '$.data[*]',
'type' => [
'id' => [
'name' => 'Art ID',
'type' => 'id',
],
'artist_title' => [
'name' => 'Artist Title',
'type' => 'string',
'path' => '$.artist_title',
],
'title' => [
'name' => 'Title',
'type' => 'title',
'path' => '$.title',
],
'image_url' => [
'name' => 'Image URL',
'generate' => function ( $data ): string {
return 'https://www.artic.edu/iiif/2/' . $data['image_id'] . '/full/843,/0/default.jpg';
},
'type' => 'image_url',
],
],
],
]);

$search_art_query = HttpQuery::from_array([
'data_source' => $aic_data_source,
'endpoint' => function ( array $input_variables ) use ( $aic_data_source ): string {
Expand Down Expand Up @@ -156,6 +201,16 @@ function register_aic_block(): void {
],
]);

register_remote_data_block( [
'title' => 'Art Institute of Chicago Loop',
'icon' => 'art',
'instructions' => 'This block displays a set amount of artworks based on the provided limit.',

@alecgeatches alecgeatches Mar 20, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this! Here's what just this looks like for anyone interested:

image

'render_query' => [
'query' => $collection_query,
'loop' => true,
],
] );

register_remote_data_block([
'title' => 'Art Institute of Chicago',
'icon' => 'art',
Expand Down
1 change: 1 addition & 0 deletions inc/Editor/BlockManagement/BlockRegistration.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public static function register_block_configuration( array $config ): array {
$block_config = [
'availableBindings' => $available_bindings,
'availableOverrides' => $config['overrides'] ?? [],
'instructions' => $config['instructions'],
'loop' => $config['loop'],
'name' => $block_name,
'dataSourceType' => ConfigStore::get_data_source_type( $block_name ),
Expand Down
9 changes: 6 additions & 3 deletions inc/Editor/BlockManagement/ConfigRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,17 @@ 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();

$is_loop = $user_config[ self::RENDER_QUERY_KEY ]['loop'] ?? false;

// Build the base configuration for the block. This is our own internal
// configuration, not what will be passed to WordPress's register_block_type.
// @see BlockRegistration::register_block_type::register_blocks.
$config = [
'description' => '',
'icon' => $user_config['icon'] ?? 'cloud',
'instructions' => $user_config['instructions'] ?? null,
'name' => $block_name,
'loop' => $user_config[ self::RENDER_QUERY_KEY ]['loop'] ?? false,
'loop' => $is_loop,
'overrides' => $user_config['overrides'] ?? [],
'patterns' => [],
'queries' => [
Expand All @@ -75,9 +78,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' => 'Manual input',
'name' => $is_loop ? 'Collection' : 'Manual input',
'query_key' => self::DISPLAY_QUERY_KEY,
'type' => 'input',
'type' => $is_loop ? 'loop' : 'input',
],
],
'title' => $block_title,
Expand Down
1 change: 1 addition & 0 deletions inc/Integrations/Shopify/ShopifyIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public static function get_queries( ShopifyDataSource $data_source ): array {
'input_schema' => [
'id' => [
'type' => 'id',
'name' => 'Product ID',
],
],
'output_schema' => [
Expand Down
3 changes: 3 additions & 0 deletions inc/Validation/ConfigSchemas.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public static function get_remote_data_block_attribute_config_schema(): array {
private static function generate_remote_data_block_config_schema(): array {
return Types::object( [
'icon' => Types::nullable( Types::string() ),
'instructions' => Types::nullable( Types::string() ),
'patterns' => Types::nullable(
Types::list_of(
Types::object( [
Expand Down Expand Up @@ -206,6 +207,8 @@ private static function generate_http_query_config_schema(): array {
// implode an array of IDs into a comma-separated list and map it
// to a query parameter).
'id:list',
// A string that represents an input field to refine the query results.
'ui:input',
// A string that represents search query input. An input variable
// with this type must be present for the query to be considered a
// search query.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Button, PanelBody, TextControl } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

interface QueryInputsPanelProps {
queryInputs: RemoteDataQueryInput[];
onUpdateQueryInputs: ( inputs: RemoteDataQueryInput[] ) => void;
}

export function QueryInputsPanel( { queryInputs, onUpdateQueryInputs }: QueryInputsPanelProps ) {
const [ localInputs, setLocalInputs ] = useState( queryInputs );

return (
<PanelBody title={ __( 'Query Inputs', 'remote-data-blocks' ) }>
<form
onSubmit={ event => {
event.preventDefault();
const cleanedInputs = localInputs.map( input => {
const entries = Object.entries( input ).map( ( [ key, value ] ) => [
key,
typeof value === 'string' && value.includes( ',' )
? value
.split( ',' )
.map( item => item.trim() )
.filter( Boolean )
: value,
] );

return Object.fromEntries( entries ) as RemoteDataQueryInput;
} );

onUpdateQueryInputs( cleanedInputs );
} }
>
{ localInputs.map( ( input, index ) =>
Object.entries( input ).map( ( [ key, value ] ) => {
const displayValue = Array.isArray( value ) ? value.join( ',' ) : ( value as string );

return (
<TextControl
Comment thread
brookewp marked this conversation as resolved.
key={ `${ index }-${ key }` }
label={ key }
value={ displayValue }
onChange={ newValue => {
setLocalInputs(
localInputs.map( ( item, itemIndex ) =>
itemIndex === index ? { ...item, [ key ]: newValue } : item
)
);
} }
onBlur={ () => {
onUpdateQueryInputs( localInputs );
} }
__next40pxDefaultSize
__nextHasNoMarginBottom
/>
);
} )
) }
<Button variant="primary" type="submit">
{ __( 'Update', 'remote-data-blocks' ) }
</Button>
</form>
</PanelBody>
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ButtonGroup } from '@wordpress/components';
import { ButtonGroup, Button } from '@wordpress/components';

import { InputModal } from '../modals/InputModal';
import { InputPopover } from '../popovers/InputPopover';
Expand Down Expand Up @@ -39,13 +39,32 @@ export function ItemSelectQueryType( props: ItemSelectQueryTypeProps ) {
/>
);
case 'input':
return selector.inputs.length === 1 && selector.inputs[ 0 ] ? (
<InputPopover key={ title } input={ selector.inputs[ 0 ] } { ...selectorProps } />
) : (
<InputModal key={ title } inputs={ selector.inputs } { ...selectorProps } />
);
case 'loop':
if ( selector.inputs.length === 1 && selector.inputs[ 0 ] ) {
return (
<InputPopover
key={ title }
input={ selector.inputs[ 0 ] }
{ ...selectorProps }
title={ selector.inputs[ 0 ].name ?? selector.name }
/>
);
}
return <InputModal key={ title } inputs={ selector.inputs } { ...selectorProps } />;
}

return (
<Button
key={ title }
onClick={ () => {
onSelect( [ {} ] );
} }
variant="primary"
>
Load Collection
</Button>
);

return null;
} ) }
</ButtonGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
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';

export interface PlaceholderProps {
import { ItemSelectQueryType } from '@/blocks/remote-data-container/components/placeholders/ItemSelectQueryType';

interface PlaceholderProps {
blockConfig: BlockConfig;
onSelect: ( input: RemoteDataQueryInput[] ) => void;
onSelect: ( data: RemoteDataQueryInput[] ) => void;
}

export function Placeholder( props: PlaceholderProps ) {
const { loop } = props.blockConfig;
const { blockConfig, onSelect } = props;
const { instructions, loop, settings } = blockConfig;

const iconElement: IconType = ( settings.icon as IconType ) ?? cloud;

if ( loop ) {
return <PlaceholderLoop { ...props } />;
}
const defaultInstructions = loop
? __( 'This block displays a list of items.' )
: __( 'This block requires selection of one or more items for display.' );

return <PlaceholderSingle { ...props } />;
return (
<PlaceholderComponent
icon={ iconElement }
label={ settings.title }
instructions={ instructions ?? defaultInstructions }
>
<ItemSelectQueryType blockConfig={ blockConfig } onSelect={ onSelect } />
</PlaceholderComponent>
);
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface InputPopoverProps {
}

export function InputPopover( props: InputPopoverProps ) {
const { input, onSelect } = props;
const { input, onSelect, title } = props;

const dataSourceType = getBlockDataSourceType( props.blockName );

Expand Down Expand Up @@ -62,7 +62,7 @@ export function InputPopover( props: InputPopoverProps ) {
};
default:
return {
buttonText: __( 'Provide manual input' ),
buttonText: __( 'Provide' ) + ' ' + title,
};
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/blocks/remote-data-container/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BlockEditProps } from '@wordpress/blocks';
import { Spinner } from '@wordpress/components';
import { useState } from '@wordpress/element';

import { QueryInputsPanel } from './components/panels/QueryInputsPanel';
import { InnerBlocks } from '@/blocks/remote-data-container/components/InnerBlocks';
import { DataPanel } from '@/blocks/remote-data-container/components/panels/DataPanel';
import { OverridesPanel } from '@/blocks/remote-data-container/components/panels/OverridesPanel';
Expand Down Expand Up @@ -83,6 +84,16 @@ export function Edit( props: BlockEditProps< RemoteDataBlockAttributes > ) {
}
}

function onUpdateQueryInputs( inputs: RemoteDataQueryInput[] ): void {
if ( ! remoteDataAttribute ) return;

updateRemoteData( {
...remoteDataAttribute,
queryInputs: inputs,
} );
refreshRemoteData();
}

// No remote data has been selected yet, show a placeholder.
if ( ! data ) {
return (
Expand Down Expand Up @@ -120,6 +131,10 @@ export function Edit( props: BlockEditProps< RemoteDataBlockAttributes > ) {
remoteData={ data }
resetRemoteData={ resetRemoteData }
/>
<QueryInputsPanel
queryInputs={ migrateRemoteData( props.attributes.remoteData )?.queryInputs ?? [] }
onUpdateQueryInputs={ onUpdateQueryInputs }
/>
</InspectorControls>

<div { ...blockProps }>
Expand Down
Loading