diff --git a/.wp-env.json b/.wp-env.json
index c4f1cd99a..3bfc57f45 100644
--- a/.wp-env.json
+++ b/.wp-env.json
@@ -7,11 +7,13 @@
"WP_REDIS_DISABLE_BANNERS": true,
"WP_REDIS_HOST": "host.docker.internal"
},
+ "core": "WordPress/WordPress#master",
"mappings": {
".htaccess": "./bin/conf/htaccess"
},
"plugins": [
".",
+ "../gutenberg",
"https://downloads.wordpress.org/plugin/query-monitor.latest-stable.zip",
"https://downloads.wordpress.org/plugin/redis-cache.latest-stable.zip"
]
diff --git a/inc/Editor/BlockManagement/BlockRegistration.php b/inc/Editor/BlockManagement/BlockRegistration.php
index 7519b4ca9..140a07b6f 100644
--- a/inc/Editor/BlockManagement/BlockRegistration.php
+++ b/inc/Editor/BlockManagement/BlockRegistration.php
@@ -39,7 +39,8 @@ public static function add_block_category( array $block_categories ): array {
*/
public static function enqueue_block_assets(): void {
Assets::enqueue_build_asset( 'remote-data-blocks-dataviews', 'dataviews' );
- Assets::enqueue_build_asset( 'remote-data-blocks-block-editor', 'block-editor', [ 'remote-data-blocks-dataviews' ] );
+ Assets::enqueue_build_asset( 'remote-data-blocks-store', 'store' );
+ Assets::enqueue_build_asset( 'remote-data-blocks-block-editor', 'block-editor', [ 'remote-data-blocks-dataviews', 'remote-data-blocks-store' ] );
}
public static function register_helper_blocks(): void {
diff --git a/src/block-editor/binding-sources/remote-data-binding.ts b/src/block-editor/binding-sources/remote-data-binding.ts
deleted file mode 100644
index 7d9670623..000000000
--- a/src/block-editor/binding-sources/remote-data-binding.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { registerBlockBindingsSource } from '@wordpress/blocks';
-
-registerBlockBindingsSource( {
- name: 'remote-data/binding',
- label: 'Remote Data Blocks',
- usesContext: [ 'remote-data-blocks/remoteData' ],
- getValues() {
- return {};
- },
-} );
diff --git a/src/block-editor/binding-sources/remote-data-binding.tsx b/src/block-editor/binding-sources/remote-data-binding.tsx
new file mode 100644
index 000000000..b19e5cbcf
--- /dev/null
+++ b/src/block-editor/binding-sources/remote-data-binding.tsx
@@ -0,0 +1,130 @@
+import { BlockEditorStoreSelectors } from '@wordpress/block-editor';
+import { BlockBindingsSource, registerBlockBindingsSource } from '@wordpress/blocks';
+
+import { BlockBindingControls } from '@/blocks/remote-data-container/components/BlockBindingControls';
+import { REMOTE_DATA_CONTEXT_KEY } from '@/blocks/remote-data-container/config/constants';
+import { BLOCK_BINDING_SOURCE, STORE_NAME as rdbStore } from '@/config/constants';
+import { getBlockAvailableBindings } from '@/utils/localized-block-data';
+
+import type { Selectors } from '@/store';
+
+interface RemoteDataRawContext {
+ [ REMOTE_DATA_CONTEXT_KEY ]?: RemoteData;
+}
+
+interface EditorUIDatum {
+ field: string;
+ label: string;
+ type: string;
+ value?: string;
+}
+
+type EditorUIFn = BlockBindingsSource< RemoteDataRawContext, RemoteDataBlockBinding >[ 'editorUI' ];
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const dropDownEditorUI: EditorUIFn = ( { context } ) => {
+ const remoteData = context[ REMOTE_DATA_CONTEXT_KEY ];
+ const availableBindings = getBlockAvailableBindings( remoteData?.blockName ?? '' );
+ const hasAvailableBindings = Boolean( Object.keys( availableBindings ).length );
+
+ if ( ! remoteData || ! hasAvailableBindings ) {
+ return {};
+ }
+
+ return {
+ mode: 'dropdown',
+ data: Object.entries( availableBindings ).map(
+ ( [ field, binding ] ): EditorUIDatum => ( {
+ field,
+ label: binding.name,
+ type: 'string',
+ value: remoteData.results?.[ 0 ]?.result?.[ field ]?.value as string | undefined,
+ } )
+ ),
+ getArgs( { item }: { item: EditorUIDatum } ) {
+ return {
+ block: remoteData.blockName,
+ field: item.field,
+ };
+ },
+ isSelected( {
+ item,
+ binding,
+ }: {
+ item: EditorUIDatum;
+ binding: RemoteDataBlockBinding;
+ } ): boolean {
+ return binding?.args?.field === item.field;
+ },
+ };
+};
+
+const modalEditorUI: EditorUIFn = ( { context, select } ) => {
+ const remoteData = context[ REMOTE_DATA_CONTEXT_KEY ];
+ const availableBindings = getBlockAvailableBindings( remoteData?.blockName ?? '' );
+ const hasAvailableBindings = Boolean( Object.keys( availableBindings ).length );
+ const block =
+ select< BlockEditorStoreSelectors >(
+ 'core/block-editor'
+ )?.getSelectedBlock< RemoteDataInnerBlockAttributes >();
+
+ if ( ! remoteData || ! hasAvailableBindings || ! block ) {
+ return {};
+ }
+
+ return {
+ mode: 'modal',
+ data: Object.entries( availableBindings ).map( ( [ key, binding ] ) => ( {
+ key,
+ label: binding.name,
+ type: binding.type,
+ value: key,
+ } ) ),
+ isSelected( {
+ item,
+ binding,
+ }: {
+ item: EditorUIDatum;
+ binding: RemoteDataBlockBinding;
+ } ): boolean {
+ return binding?.args?.field === item.field;
+ },
+ renderModalContent( { attribute } ) {
+ return (
+
+ );
+ },
+ };
+};
+
+registerBlockBindingsSource< RemoteDataRawContext, RemoteDataBlockBinding >( {
+ name: BLOCK_BINDING_SOURCE,
+ usesContext: [ 'remote-data-blocks/remoteData' ],
+ // editorUI: dropDownEditorUI,
+ editorUI: modalEditorUI,
+ getValues( { bindings, context, select } ): Record< string, string > {
+ if ( ! context[ REMOTE_DATA_CONTEXT_KEY ]?.results?.length ) {
+ return {};
+ }
+
+ const remoteData = context[ REMOTE_DATA_CONTEXT_KEY ];
+ const previewIndex = select< Selectors >( rdbStore ).getPreviewIndex( remoteData.resultId );
+
+ return Object.fromEntries(
+ Object.entries( bindings ).map( ( [ targetAttribute, binding ] ): [ string, string ] => {
+ const index = binding.args.previewIndex ?? previewIndex;
+ const label = binding.args.label ? `${ binding.args.label }: ` : '';
+ const value = String(
+ remoteData.results?.[ index ?? 0 ]?.result?.[ binding.args.field ]?.value ?? ''
+ );
+
+ return [ targetAttribute, `${ label }${ value }` ];
+ } )
+ );
+ },
+} );
diff --git a/src/block-editor/filters/addUsesContext.ts b/src/block-editor/filters/addUsesContext.ts
deleted file mode 100644
index 622e26dfe..000000000
--- a/src/block-editor/filters/addUsesContext.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { BlockConfiguration } from '@wordpress/blocks';
-
-import {
- REMOTE_DATA_CONTEXT_KEY,
- SUPPORTED_CORE_BLOCKS,
-} from '@/blocks/remote-data-container/config/constants';
-
-export function addUsesContext(
- settings: BlockConfiguration< RemoteDataInnerBlockAttributes >,
- name: string
-) {
- if ( ! SUPPORTED_CORE_BLOCKS.includes( name ) ) {
- return settings;
- }
-
- const { usesContext = [] } = settings;
-
- if ( ! usesContext?.includes( REMOTE_DATA_CONTEXT_KEY ) ) {
- return {
- ...settings,
- usesContext: [ ...usesContext, REMOTE_DATA_CONTEXT_KEY ],
- };
- }
-
- return settings;
-}
diff --git a/src/block-editor/filters/index.ts b/src/block-editor/filters/index.ts
deleted file mode 100644
index 4b1ea2f2a..000000000
--- a/src/block-editor/filters/index.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { addFilter } from '@wordpress/hooks';
-
-import { addUsesContext } from '@/block-editor/filters/addUsesContext';
-import { withBlockBindingShim } from '@/block-editor/filters/withBlockBinding';
-
-/**
- * Use a filter to wrap the block edit component with our block binding HOC.
- * We are intentionally using the `blocks.registerBlockType` filter instead of
- * `editor.BlockEdit` so that we can make sure our HOC is applied after any
- * other HOCs from Core -- specifically this one, which injects the binding label
- * as the attribute value:
- *
- * https://github.com/WordPress/gutenberg/blob/f56dbeb9257c19acf6fbd8b45d87ae8a841624da/packages/block-editor/src/hooks/use-bindings-attributes.js#L159
- */
-addFilter(
- 'blocks.registerBlockType',
- 'remote-data-blocks/withBlockBinding',
- withBlockBindingShim,
- 5 // Ensure this runs before core filters
-);
-
-/**
- * Use a filter to inject usesContext to core block settings.
- */
-addFilter( 'blocks.registerBlockType', 'remote-data-blocks/addUsesContext', addUsesContext, 10 );
diff --git a/src/block-editor/filters/withBlockBinding.tsx b/src/block-editor/filters/withBlockBinding.tsx
deleted file mode 100644
index 5cbef56c1..000000000
--- a/src/block-editor/filters/withBlockBinding.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import {
- BlockEditorStoreSelectors,
- InspectorControls,
- store as blockEditorStore,
-} from '@wordpress/block-editor';
-import { BlockConfiguration, BlockEditProps } from '@wordpress/blocks';
-import { PanelBody } from '@wordpress/components';
-import { createHigherOrderComponent } from '@wordpress/compose';
-import { useSelect } from '@wordpress/data';
-import { __, sprintf } from '@wordpress/i18n';
-
-import { BlockBindingControls } from '@/blocks/remote-data-container/components/BlockBindingControls';
-import { useRemoteDataContext } from '@/blocks/remote-data-container/hooks/useRemoteDataContext';
-import {
- BLOCK_BINDING_SOURCE,
- PATTERN_OVERRIDES_BINDING_SOURCE,
- PATTERN_OVERRIDES_CONTEXT_KEY,
-} from '@/config/constants';
-import { getBoundBlockClassName, getMismatchedAttributes } from '@/utils/block-binding';
-import { getBlockAvailableBindings, getBlockTitle } from '@/utils/localized-block-data';
-
-interface BoundBlockEditProps {
- attributes: RemoteDataInnerBlockAttributes;
- availableBindings: AvailableBindings;
- blockName: string;
- children: JSX.Element;
- remoteDataName: string;
- remoteDataTitle: string;
- 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,
- remoteDataTitle,
- setAttributes,
- } = props;
- const existingBindings = attributes.metadata?.bindings ?? {};
-
- function removeBinding( target: string ) {
- const { [ target ]: _remove, ...newBindings } = existingBindings;
- setAttributes( {
- metadata: {
- ...attributes.metadata,
- bindings: newBindings,
- name: undefined,
- },
- } );
- }
-
- function updateBinding( target: string, args: Omit< RemoteDataBlockBindingArgs, 'block' > ) {
- setAttributes( {
- className: getBoundBlockClassName( attributes, remoteDataName ),
- metadata: {
- ...attributes.metadata,
- bindings: {
- ...attributes.metadata?.bindings,
- [ target ]: {
- source: BLOCK_BINDING_SOURCE,
- args: {
- ...args,
- block: remoteDataName, // Remote Data Block name
- },
- },
- },
- name: availableBindings[ args.field ]?.name, // Changes block name in list view.
- },
- } );
- }
-
- return (
- <>
-
-
-
- { sprintf( __( 'Connected to %s', 'remote-data-blocks' ), remoteDataTitle ) }
-
-
-
-
- { props.children }
- >
- );
-}
-
-export const withBlockBinding = createHigherOrderComponent( BlockEdit => {
- 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 );
- const { hasMultiSelection } = useSelect< BlockEditorStoreSelectors >( blockEditorStore );
-
- // If the block does not have a remote data context, render it as usual.
- if ( ! remoteData || ! hasAvailableBindings ) {
- return ;
- }
-
- // Synced pattern overrides are provided via context and the value can be:
- //
- // - undefined (block is not in a synced pattern)
- // - an empty array (block is in a synced pattern, but no overrides are applied)
- // - an object defining the applied overrides
- //
- // This gives no indication of whether overrides are enabled or not. For
- // that, we need to check the block's metadata bindings for the pattern
- // overrides binding source.
- //
- // This seems likely to change, so the code here may need maintenance. For
- // our purposes, though, we just want to know whether the block is in a
- // synced pattern and whether overrides are enabled. Trying to update
- // a synced block without overrides enabled is useless and can cause issues.
-
- const patternOverrides = context[ PATTERN_OVERRIDES_CONTEXT_KEY ] as string[] | undefined;
- const isInSyncedPattern = Boolean( patternOverrides );
- const hasEnabledOverrides = Object.values( attributes.metadata?.bindings ?? {} ).some(
- binding => binding.source === PATTERN_OVERRIDES_BINDING_SOURCE
- );
-
- // If the block has a binding and the attributes do not match their expected
- // values, update and merge the attributes.
- const mergedAttributes = {
- ...attributes,
- ...getMismatchedAttributes( attributes, remoteData.results, remoteData.blockName, index ),
- };
-
- // If multiple blocks are being selected, render it as usual.
- if ( hasMultiSelection() ) {
- return ;
- }
-
- // If the block is not writable, render it as usual.
- if ( isInSyncedPattern && ! hasEnabledOverrides ) {
- return ;
- }
-
- // lookup the title of the remote data block
- const remoteBlockTitle = getBlockTitle( remoteData?.blockName );
-
- return (
-
-
-
- );
- };
-}, 'withBlockBinding' );
-
-/**
- * Shim for the block binding HOC to be used with the `blocks.registerBlockType` filter.
- */
-export function withBlockBindingShim(
- settings: BlockConfiguration< RemoteDataInnerBlockAttributes >
-): BlockConfiguration< RemoteDataInnerBlockAttributes > {
- return {
- ...settings,
- edit: withBlockBinding( settings.edit ?? ( () => null ) ),
- };
-}
diff --git a/src/block-editor/index.ts b/src/block-editor/index.ts
index f71937de3..cd07bb273 100644
--- a/src/block-editor/index.ts
+++ b/src/block-editor/index.ts
@@ -1,3 +1,2 @@
import '@/block-editor/binding-sources/remote-data-binding';
-import '@/block-editor/filters';
import '@/block-editor/format-types/inline-binding';
diff --git a/src/blocks/remote-data-container/components/BlockBindingControls.tsx b/src/blocks/remote-data-container/components/BlockBindingControls.tsx
index 26821781c..9ee8cd8d6 100644
--- a/src/blocks/remote-data-container/components/BlockBindingControls.tsx
+++ b/src/blocks/remote-data-container/components/BlockBindingControls.tsx
@@ -1,3 +1,4 @@
+import { useBlockBindingsUtils } from '@wordpress/block-editor';
import { CheckboxControl, SelectControl } from '@wordpress/components';
import {
@@ -9,6 +10,7 @@ import {
TEXT_FIELD_TYPES,
} from '@/blocks/remote-data-container/config/constants';
import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks';
+import { BLOCK_BINDING_SOURCE } from '@/config/constants';
import { getBlockDataSourceType } from '@/utils/localized-block-data';
interface BlockBindingFieldControlProps {
@@ -17,7 +19,7 @@ interface BlockBindingFieldControlProps {
label: string;
target: string;
updateFieldBinding: ( target: string, field: string ) => void;
- value: string;
+ value?: string;
}
export function BlockBindingFieldControl( props: BlockBindingFieldControlProps ) {
@@ -43,23 +45,37 @@ export function BlockBindingFieldControl( props: BlockBindingFieldControlProps )
}
interface BlockBindingControlsProps {
- attributes: RemoteDataInnerBlockAttributes;
+ args?: RemoteDataBlockBindingArgs;
availableBindings: AvailableBindings;
blockName: string;
remoteDataName: string;
- removeBinding: ( target: string ) => void;
- updateBinding: ( target: string, args: Omit< RemoteDataBlockBindingArgs, 'block' > ) => void;
}
export function BlockBindingControls( props: BlockBindingControlsProps ) {
- const { attributes, availableBindings, blockName, remoteDataName, removeBinding, updateBinding } =
- props;
- const contentArgs = attributes.metadata?.bindings?.content?.args;
- const contentField = contentArgs?.field ?? '';
- const imageAltField = attributes.metadata?.bindings?.alt?.args?.field ?? '';
- const imageUrlField = attributes.metadata?.bindings?.url?.args?.field ?? '';
- const buttonUrlField = attributes.metadata?.bindings?.url?.args?.field ?? '';
- const buttonTextField = attributes.metadata?.bindings?.text?.args?.field ?? '';
+ const { args, availableBindings, blockName, remoteDataName } = props;
+
+ const { updateBlockBindings } = useBlockBindingsUtils();
+
+ function removeBinding( target: string ): void {
+ updateBlockBindings( {
+ [ target ]: undefined,
+ } );
+ }
+
+ function updateBinding(
+ target: string,
+ newArgs: Omit< RemoteDataBlockBindingArgs, 'block' >
+ ): void {
+ updateBlockBindings( {
+ [ target ]: {
+ source: BLOCK_BINDING_SOURCE,
+ args: {
+ ...newArgs,
+ block: remoteDataName,
+ },
+ },
+ } );
+ }
function updateFieldBinding( target: string, field: string ): void {
if ( ! field ) {
@@ -69,11 +85,9 @@ export function BlockBindingControls( props: BlockBindingControlsProps ) {
data_source_type: getBlockDataSourceType( remoteDataName ),
block_target_attribute: target,
} );
-
return;
}
- const args = attributes.metadata?.bindings?.[ target ]?.args ?? {};
updateBinding( target, { ...args, field } );
sendTracksEvent( 'remote_data_container_actions', {
action: 'update_binding',
@@ -84,15 +98,14 @@ export function BlockBindingControls( props: BlockBindingControlsProps ) {
}
function updateFieldLabel( showLabel: boolean ): void {
- if ( ! contentField ) {
- // Form input should be disabled in this state, but check anyway.
+ if ( ! args?.field ) {
return;
}
const label = showLabel
- ? Object.entries( availableBindings ).find( ( [ key ] ) => key === contentField )?.[ 1 ]?.name
+ ? Object.entries( availableBindings ).find( ( [ key ] ) => key === args?.field )?.[ 1 ]?.name
: undefined;
- updateBinding( 'content', { ...contentArgs, field: contentField, label } );
+ updateBinding( 'content', { ...args, label } );
sendTracksEvent( 'remote_data_container_actions', {
action: showLabel ? 'show_label' : 'hide_label',
data_source_type: getBlockDataSourceType( remoteDataName ),
@@ -110,11 +123,11 @@ export function BlockBindingControls( props: BlockBindingControlsProps ) {
label="Content"
target="content"
updateFieldBinding={ updateFieldBinding }
- value={ contentField }
+ value={ args?.field }
/>
>
);
@@ -152,7 +165,7 @@ export function BlockBindingControls( props: BlockBindingControlsProps ) {
label="Button URL"
target="url"
updateFieldBinding={ updateFieldBinding }
- value={ buttonUrlField }
+ value={ args?.field }
/>
>
);
@@ -174,7 +187,7 @@ export function BlockBindingControls( props: BlockBindingControlsProps ) {
label="Raw HTML"
target="content"
updateFieldBinding={ updateFieldBinding }
- value={ contentField }
+ value={ args?.field }
/>
>
);
diff --git a/src/blocks/remote-data-template/components/loop-template/LoopTemplate.tsx b/src/blocks/remote-data-template/components/loop-template/LoopTemplate.tsx
index 0cf17e03d..087c25975 100644
--- a/src/blocks/remote-data-template/components/loop-template/LoopTemplate.tsx
+++ b/src/blocks/remote-data-template/components/loop-template/LoopTemplate.tsx
@@ -4,24 +4,31 @@ import {
useBlockEditContext,
} from '@wordpress/block-editor';
import { BlockInstance } from '@wordpress/blocks';
-import { useSelect } from '@wordpress/data';
-import { useState } from '@wordpress/element';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { Fragment, useState } from '@wordpress/element';
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';
+import { STORE_NAME as remoteDataBlocksStore } from '@/config/constants';
+
+import type { ActionCreators } from '@/store';
interface LoopTemplateProps {
getInnerBlocks: (
- result: RemoteDataApiResult
+ result: RemoteDataApiResult,
+ index: number
) => BlockInstance< RemoteDataInnerBlockAttributes >[];
remoteData: RemoteData;
}
export function LoopTemplate( props: LoopTemplateProps ) {
- const [ activeBlockIndex, setActiveBlockIndex ] = useState< number >( 0 );
const { getInnerBlocks, remoteData } = props;
+ // Use local state instead of selecting the preview index from the store so
+ // that re-renders are limited to this component only.
+ const [ activeBlockIndex, setActiveBlockIndex ] = useState< number >( 0 );
+ const { setPreviewIndex } = useDispatch< ActionCreators >( remoteDataBlocksStore, [] );
+
// Hammer approach, forces re-render of the whole loop when user input is detected.
const { clientId } = useBlockEditContext();
useSelect< BlockEditorStoreSelectors, BlockInstance[] >(
@@ -29,6 +36,11 @@ export function LoopTemplate( props: LoopTemplateProps ) {
[ clientId ]
);
+ function onSelect( index: number ): void {
+ setActiveBlockIndex( index );
+ setPreviewIndex( remoteData.resultId, index );
+ }
+
// To avoid flicker when switching active block contexts, a preview is rendered
// for each block context, but the preview for the active block context is hidden.
// This ensures that when it is displayed again, the cached rendering of the
@@ -39,14 +51,14 @@ export function LoopTemplate( props: LoopTemplateProps ) {
{ remoteData.results.map( ( result, index ) => {
const isActive = index === activeBlockIndex;
return (
-
+
setActiveBlockIndex( index ) }
+ onSelect={ () => onSelect( index ) }
/>
-
+
);
} ) }
diff --git a/src/blocks/remote-data-template/context/PreviewIndexContext.ts b/src/blocks/remote-data-template/context/PreviewIndexContext.ts
deleted file mode 100644
index 19103a763..000000000
--- a/src/blocks/remote-data-template/context/PreviewIndexContext.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-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
index 6bff5c997..e42ee3380 100644
--- a/src/blocks/remote-data-template/edit.tsx
+++ b/src/blocks/remote-data-template/edit.tsx
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
-import { useBlockProps } from '@wordpress/block-editor';
+import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { Placeholder } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
@@ -15,6 +15,7 @@ import './editor.scss';
export function Edit( props: BlockEditProps< RemoteDataTemplateBlockAttributes > ): JSX.Element {
const { clientId, context, name } = props;
const blockProps = useBlockProps();
+ const innerBlocksProps = useInnerBlocksProps();
const { remoteData } = useRemoteDataContext( context );
const getInnerBlocks = useGetInnerBlocks( name, clientId, remoteData?.blockName );
@@ -38,5 +39,9 @@ export function Edit( props: BlockEditProps< RemoteDataTemplateBlockAttributes >
return
;
}
+ if ( 1 === remoteData.results.length ) {
+ return ;
+ }
+
return ;
}
diff --git a/src/blocks/remote-data-template/filters/index.ts b/src/blocks/remote-data-template/filters/index.ts
deleted file mode 100644
index 5190990d5..000000000
--- a/src/blocks/remote-data-template/filters/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-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
deleted file mode 100644
index b9d7e6318..000000000
--- a/src/blocks/remote-data-template/filters/withPreviewIndex.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-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
index bacf38bf4..26d0a5849 100644
--- a/src/blocks/remote-data-template/hooks/useGetInnerBlocks.ts
+++ b/src/blocks/remote-data-template/hooks/useGetInnerBlocks.ts
@@ -15,9 +15,12 @@ export function useGetInnerBlocks(
[ blockName, clientId ],
] );
- return ( result: RemoteDataApiResult ): BlockInstance< RemoteDataInnerBlockAttributes >[] => {
+ return (
+ result: RemoteDataApiResult,
+ previewIndex: number
+ ): BlockInstance< RemoteDataInnerBlockAttributes >[] => {
return getBlocks( clientId ).map( block =>
- cloneBlockForPreview( block, result, remoteDataBlockName ?? blockName )
+ cloneBlockForPreview( block, result, remoteDataBlockName ?? blockName, previewIndex )
);
};
}
diff --git a/src/blocks/remote-data-template/index.ts b/src/blocks/remote-data-template/index.ts
index 36c3263b4..baad2b7ef 100644
--- a/src/blocks/remote-data-template/index.ts
+++ b/src/blocks/remote-data-template/index.ts
@@ -3,7 +3,6 @@ import { layout } from '@wordpress/icons';
import metadata from './block.json';
import { Edit } from './edit';
-import './filters';
import { Save } from './save';
registerBlockType< RemoteDataTemplateBlockAttributes >( metadata.name, {
diff --git a/src/blocks/remote-html/edit.tsx b/src/blocks/remote-html/edit.tsx
index 2206886c3..354c6273d 100644
--- a/src/blocks/remote-html/edit.tsx
+++ b/src/blocks/remote-html/edit.tsx
@@ -11,7 +11,6 @@ import {
import { BlockEditProps } from '@wordpress/blocks';
import { SandBox, Placeholder } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
-import { useEffect } from '@wordpress/element';
import { __ } from '@/utils/i18n';
@@ -33,28 +32,10 @@ const DEFAULT_STYLES = `
}
`;
-interface RemoteHtmlAttributes extends RemoteDataInnerBlockAttributes {
- saveContent?: string;
-}
-
-export function Edit( props: BlockEditProps< RemoteHtmlAttributes > ): JSX.Element {
- const { attributes, setAttributes, isSelected } = props;
+export function Edit( props: BlockEditProps< RemoteDataInnerBlockAttributes > ): JSX.Element {
+ const { attributes, isSelected } = props;
const blockProps = useBlockProps();
-
- // HACK:
- // The remote data binding passes merged remote attributes into the Edit component,
- // but the Save component does not receive the same augmented attributes. In order to
- // persist fallback content during save, we need to store the content in an attribute
- // (saveContent) and call setAttributes to expose it to Save.
- //
- // This should be removed once we replace mergedAttributes/getMismatchedAttributes()
- // with a getValues() implementation.
const { content } = attributes;
- useEffect( () => {
- if ( content !== undefined ) {
- setAttributes( { saveContent: content.toString() } );
- }
- }, [ content, setAttributes ] );
const settingStyles = useSelect< BlockEditorStoreSelectors, EditorStyle[] >(
select => select( blockEditorStore ).getSettings().styles,
diff --git a/src/blocks/remote-html/save.tsx b/src/blocks/remote-html/save.tsx
index 27665424c..91d58a60d 100644
--- a/src/blocks/remote-html/save.tsx
+++ b/src/blocks/remote-html/save.tsx
@@ -3,13 +3,8 @@ import { RawHTML } from '@wordpress/element';
interface RemoteDataHTMLSaveAttributes {
content?: string | StringSeriablizable;
- saveContent?: string | StringSeriablizable;
}
export function Save( props: BlockSaveProps< RemoteDataHTMLSaveAttributes > ) {
- const { attributes } = props;
-
- const content = attributes?.saveContent ?? attributes?.content ?? '';
-
- return { content?.toString() };
+ return { props.attributes.content?.toString() ?? '' };
}
diff --git a/src/config/constants.ts b/src/config/constants.ts
index acf7a9a81..238c8abdf 100644
--- a/src/config/constants.ts
+++ b/src/config/constants.ts
@@ -2,4 +2,5 @@ export const BLOCK_BINDING_SOURCE = 'remote-data/binding';
export const PATTERN_BLOCK_TYPE_POST_META_KEY: string = '_remote_data_blocks_block_type'; // Must match PatternEditor::$block_type_meta_key.
export const PATTERN_OVERRIDES_BINDING_SOURCE = 'core/pattern-overrides';
export const PATTERN_OVERRIDES_CONTEXT_KEY = 'pattern/overrides';
+export const STORE_NAME = 'remote-data-blocks/store';
export const TEXT_DOMAIN = 'remote-data-blocks';
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 000000000..d5e82540c
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,67 @@
+import { createReduxStore, register, type StoreDescriptor } from '@wordpress/data';
+
+import { STORE_NAME } from '@/config/constants';
+
+import type { ActionCreator, ReduxStoreConfig } from '@wordpress/data/build-types/types';
+
+interface State {
+ previewIndexes: Map< string, number >;
+}
+
+interface SetPreviewIndexAction {
+ type: 'SET_PREVIEW_INDEX';
+ payload: {
+ resultsId: string;
+ index: number;
+ };
+}
+
+type ActionPayload = SetPreviewIndexAction;
+
+export interface ActionCreators extends Record< string, ActionCreator > {
+ setPreviewIndex: ( resultsId: string, index: number ) => SetPreviewIndexAction;
+}
+
+export interface Selectors {
+ getPreviewIndex: ( resultsId: string ) => number;
+}
+
+const actionCreators: ActionCreators = {
+ setPreviewIndex( resultsId: string, index: number ): SetPreviewIndexAction {
+ return {
+ type: 'SET_PREVIEW_INDEX',
+ payload: { resultsId, index },
+ };
+ },
+};
+
+const selectors = {
+ getPreviewIndex( state: State = INITIAL_STATE, resultsId: string ): number {
+ return state.previewIndexes?.get( resultsId ) ?? 0;
+ },
+};
+
+const INITIAL_STATE: State = {
+ previewIndexes: new Map(),
+};
+
+const remoteDataBlocksStoreConfig: ReduxStoreConfig< State, ActionCreators, typeof selectors > = {
+ actions: actionCreators,
+ reducer: ( state: State = INITIAL_STATE, action: ActionPayload ) => {
+ switch ( action.type ) {
+ case 'SET_PREVIEW_INDEX':
+ return {
+ ...state,
+ previewIndexes: new Map( state.previewIndexes ).set(
+ action.payload.resultsId,
+ action.payload.index
+ ),
+ };
+ }
+ },
+ selectors,
+};
+
+const store = createReduxStore( STORE_NAME, remoteDataBlocksStoreConfig );
+
+( register as ( storeDescriptor: StoreDescriptor ) => void )( store );
diff --git a/src/utils/block-binding.ts b/src/utils/block-binding.ts
index a282094a9..9cc37a111 100644
--- a/src/utils/block-binding.ts
+++ b/src/utils/block-binding.ts
@@ -1,64 +1,56 @@
import { cloneBlock } from '@wordpress/blocks';
import { BLOCK_BINDING_SOURCE } from '@/config/constants';
-import { getRemoteDataResultValue } from '@/utils/remote-data';
import { getClassName } from '@/utils/string';
-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.
+ * a pattern preview or a template preview. Template previews may be previewing
+ * a specific result from the result set, so a preview index can be provided.
*/
export function cloneBlockForPreview(
block: BlockInstance< RemoteDataInnerBlockAttributes >,
result: RemoteDataApiResult,
- remoteDataBlockName: string
+ remoteDataBlockName: string,
+ previewIndex?: number
): BlockInstance {
const newInnerBlocks = block.innerBlocks?.map( innerBlock =>
- cloneBlockForPreview( innerBlock, result, remoteDataBlockName )
- );
-
- const mismatchedAttributes = getMismatchedAttributes(
- block.attributes,
- [ result ],
- remoteDataBlockName
+ cloneBlockForPreview( innerBlock, result, remoteDataBlockName, previewIndex )
);
- return cloneBlock( block, mismatchedAttributes, newInnerBlocks );
-}
-
-function getAttributeValue( attributes: unknown, key: string | undefined | null ): string {
- if ( ! key || ! isObjectWithStringKeys( attributes ) ) {
- return '';
- }
-
- // This .toString() call is important to handle RichTextData objects. We may
- // set the attribute value as a string, but once loaded by the editor, it will
- // be a RichTextData object. Currently, .toString() proxies to .toHTMLString():
- //
- // https://github.com/WordPress/gutenberg/blob/7bca2fadddde7b2b2f62823b8a4b81378f117412/packages/rich-text/src/create.js#L157
- return attributes[ key ]?.toString() ?? '';
-}
-
-function getExpectedAttributeValue(
- result?: RemoteDataApiResult,
- args?: RemoteDataBlockBindingArgs
-): string | null {
- if ( ! args?.field || ! result?.result?.[ args.field ] ) {
- return null;
- }
-
- // See comment on toString() in getAttributeValue.
- let expectedValue = getRemoteDataResultValue( result, args.field );
- if ( args.label ) {
- const labelClass = getClassName( 'block-label' );
- expectedValue = `${ args.label } ${ expectedValue }`;
+ let previewArgs = {};
+ if ( undefined !== previewIndex ) {
+ previewArgs = { isPreview: true, previewIndex };
}
- return expectedValue;
+ const clonedAttributes = {
+ ...block.attributes,
+ metadata: {
+ ...block.attributes.metadata,
+ bindings: {
+ ...block.attributes.metadata?.bindings,
+ ...Object.fromEntries(
+ getBoundAttributeEntries( block.attributes, remoteDataBlockName ).map(
+ ( [ target, binding ] ) => [
+ target,
+ {
+ ...binding,
+ args: {
+ ...binding.args,
+ ...previewArgs,
+ },
+ },
+ ]
+ )
+ ),
+ },
+ },
+ };
+
+ return cloneBlock( block, clonedAttributes, newInnerBlocks );
}
export function getBoundAttributeEntries(
@@ -88,24 +80,6 @@ export function getBoundBlockClassName(
return Array.from( classNames.values() ).filter( Boolean ).join( ' ' );
}
-export function getMismatchedAttributes(
- attributes: RemoteDataInnerBlockAttributes,
- results: RemoteDataApiResult[],
- remoteDataBlockName: string,
- index = 0
-): Partial< RemoteDataInnerBlockAttributes > {
- return Object.fromEntries(
- getBoundAttributeEntries( attributes, remoteDataBlockName )
- .map( ( [ target, binding ] ) => [
- target,
- getExpectedAttributeValue( results[ index ], binding.args ),
- ] )
- .filter(
- ( [ target, value ] ) => null !== value && value !== getAttributeValue( attributes, target )
- )
- ) as Partial< RemoteDataInnerBlockAttributes >;
-}
-
/**
* Recursively determine if a block or its inner blocks have any block bindings.
*/
diff --git a/src/utils/type-narrowing.ts b/src/utils/type-narrowing.ts
deleted file mode 100644
index 32edb84a9..000000000
--- a/src/utils/type-narrowing.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export function isObjectWithStringKeys( value: unknown ): value is Record< string, unknown > {
- return typeof value === 'object' && value !== null && ! Array.isArray( value );
-}
-
-export function removeNullValuesFromObject< ValueType >(
- obj: Record< string, ValueType | null >
-): Record< string, ValueType > {
- return Object.fromEntries< ValueType >(
- Object.entries( obj ).filter( ( entry ): entry is [ string, ValueType ] => entry[ 1 ] !== null )
- );
-}
diff --git a/tests/src/block-editor/filters/withBlockBinding.test.tsx b/tests/src/block-editor/filters/withBlockBinding.test.tsx
deleted file mode 100644
index 209a74262..000000000
--- a/tests/src/block-editor/filters/withBlockBinding.test.tsx
+++ /dev/null
@@ -1,284 +0,0 @@
-import { cleanup, render, screen } from '@testing-library/react';
-import { useSelect } from '@wordpress/data';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-
-import { withBlockBinding } from '@/block-editor/filters/withBlockBinding';
-import { REMOTE_DATA_CONTEXT_KEY } from '@/blocks/remote-data-container/config/constants';
-import { BLOCK_BINDING_SOURCE, PATTERN_OVERRIDES_CONTEXT_KEY } from '@/config/constants';
-import { createRemoteDataResults as createResults } from '@/utils/remote-data';
-
-vi.mock( '@/blocks/remote-data-container/utils/tracks', () => ( {
- sendTracksEvent: vi.fn(),
-} ) );
-
-// Minimal mocking for WordPress dependencies
-vi.mock( '@wordpress/block-editor', () => ( {
- InspectorControls: ( { children }: { children: React.ReactNode } ) => (
- { children }
- ),
- store: {},
-} ) );
-
-vi.mock( '@wordpress/components', () => ( {
- PanelBody: ( { children, title }: { children: React.ReactNode; title: string } ) => (
-
- { children }
-
- ),
-} ) );
-
-vi.mock( '@wordpress/data' );
-
-vi.mock( '@/hooks/useEditedPostAttribute', () => ( {
- useEditedPostAttribute: () => ( {
- postType: '',
- } ),
-} ) );
-
-describe( 'withBlockBinding', () => {
- const hasMultiSelection = vi.fn().mockReturnValue( false );
- const MockBlockEdit = vi.fn( () => );
- const WrappedComponent = withBlockBinding( MockBlockEdit );
- const testBlockConfig: LocalizedBlockData = {
- config: {
- 'test/block': {
- availableBindings: { field1: { name: 'Field 1', type: 'string' } },
- availableOverrides: [],
- dataSourceType: 'test-source',
- name: 'test/block',
- patterns: { default: 'test/block/pattern' },
- selectors: [],
- settings: {
- category: 'widget',
- title: 'Test block',
- },
- },
- },
- rest_url: 'http://example.com/wp-json',
- };
-
- beforeEach( () => {
- vi.useFakeTimers();
- vi.mocked( useSelect ).mockImplementation( () => ( { hasMultiSelection } ) );
- window.REMOTE_DATA_BLOCKS = testBlockConfig;
- } );
-
- afterEach( () => {
- vi.useRealTimers();
- hasMultiSelection.mockClear();
- MockBlockEdit.mockClear();
- cleanup();
- } );
-
- it( 'renders original BlockEdit when no remote data is available', () => {
- render(
- {} }
- clientId="test-client-id"
- isSelected={ false }
- className=""
- />
- );
-
- expect( hasMultiSelection ).not.toHaveBeenCalled();
- expect( screen.getByTestId( 'mock-block-edit' ) ).toBeDefined();
- expect( screen.queryByTestId( 'inspector-controls' ) ).toBeNull();
- } );
-
- it( 'renders original BlockEdit when multiple blocks are selected', () => {
- hasMultiSelection.mockReturnValueOnce( true );
-
- const remoteData = {
- blockName: 'test/block',
- results: createResults( [ { field1: 'value1' } ] ),
- };
-
- render(
- {} }
- clientId="test-client-id"
- isSelected={ false }
- className=""
- />
- );
-
- expect( hasMultiSelection ).toHaveBeenCalledTimes( 1 );
- expect( screen.getByTestId( 'mock-block-edit' ) ).toBeDefined();
- expect( screen.queryByTestId( 'inspector-controls' ) ).toBeNull();
- } );
-
- it( 'renders BoundBlockEdit when remote data is available', async () => {
- const remoteData = {
- blockName: 'test/block',
- results: createResults( [ { field1: 'value1' } ] ),
- };
- render(
- {} }
- clientId="test-client-id"
- isSelected={ false }
- className=""
- />
- );
-
- await vi.runAllTimersAsync();
-
- expect( hasMultiSelection ).toHaveBeenCalledTimes( 1 );
- expect( screen.getByTestId( 'mock-block-edit' ) ).toBeDefined();
- expect( screen.getByTestId( 'inspector-controls' ) ).toBeDefined();
- expect( screen.getByTestId( 'panel-body' ) ).toBeDefined();
- } );
-
- it( 'does not render BoundBlockEdit for synced pattern without enabled overrides', () => {
- const remoteData = {
- blockName: 'test/block',
- results: createResults( [ { field1: 'value1' } ] ),
- };
- render(
- {} }
- clientId="test-client-id"
- isSelected={ false }
- className=""
- />
- );
-
- expect( hasMultiSelection ).toHaveBeenCalledTimes( 1 );
- expect( screen.getByTestId( 'mock-block-edit' ) ).toBeDefined();
- expect( screen.queryByTestId( 'inspector-controls' ) ).toBeNull();
- } );
-
- it( 'updates attributes when mismatches are found', () => {
- const mockSetAttributes = vi.fn();
- const props = {
- attributes: {
- content: 'Old Title',
- metadata: {
- bindings: {
- content: {
- source: BLOCK_BINDING_SOURCE,
- args: { block: 'test/block', field: 'title' },
- },
- },
- },
- },
- context: {
- [ REMOTE_DATA_CONTEXT_KEY ]: {
- blockName: 'test/block',
- results: createResults( [ { title: 'New Title' } ] ),
- },
- },
- name: 'test/block',
- setAttributes: mockSetAttributes,
- clientId: 'test-client-id',
- isSelected: false,
- className: '',
- };
-
- render( );
-
- expect( MockBlockEdit ).toHaveBeenCalledTimes( 1 );
- expect( MockBlockEdit ).toHaveBeenCalledWith(
- {
- ...props,
- attributes: { ...props.attributes, content: 'New Title' },
- },
- {}
- );
- expect( hasMultiSelection ).toHaveBeenCalledTimes( 1 );
- expect( mockSetAttributes ).not.toHaveBeenCalled();
- } );
-
- it( 'does not update attributes when no mismatches are found', () => {
- const mockSetAttributes = vi.fn();
- const props = {
- attributes: {
- content: 'Matching Title',
- metadata: {
- bindings: {
- content: {
- source: BLOCK_BINDING_SOURCE,
- args: { block: 'test/block', field: 'title' },
- },
- },
- },
- },
- context: {
- [ REMOTE_DATA_CONTEXT_KEY ]: {
- blockName: 'test/block',
- results: createResults( [ { title: 'Matching Title' } ] ),
- },
- },
- name: 'test/block',
- setAttributes: mockSetAttributes,
- clientId: 'test-client-id',
- isSelected: false,
- className: '',
- };
-
- render( );
-
- expect( MockBlockEdit ).toHaveBeenCalledTimes( 1 );
- expect( MockBlockEdit ).toHaveBeenCalledWith( props, {} );
- expect( hasMultiSelection ).toHaveBeenCalledTimes( 1 );
- expect( mockSetAttributes ).not.toHaveBeenCalled();
- } );
-
- it( 'updates attributes even when binding ui is hidden', () => {
- const mockSetAttributes = vi.fn();
- const props = {
- attributes: {
- content: 'Old Title',
- metadata: {
- bindings: {
- content: {
- source: BLOCK_BINDING_SOURCE,
- args: { block: 'test/block', field: 'title' },
- },
- },
- },
- },
- context: {
- [ REMOTE_DATA_CONTEXT_KEY ]: {
- blockName: 'test/block',
- results: createResults( [ { title: 'New Title' } ] ),
- },
- },
- name: 'test/block',
- setAttributes: mockSetAttributes,
- clientId: 'test-client-id',
- isSelected: false,
- className: '',
- };
-
- hasMultiSelection.mockReturnValueOnce( true );
-
- render( );
-
- expect( MockBlockEdit ).toHaveBeenCalledTimes( 1 );
- expect( MockBlockEdit ).toHaveBeenCalledWith(
- {
- ...props,
- attributes: { ...props.attributes, content: 'New Title' },
- },
- {}
- );
- expect( hasMultiSelection ).toHaveBeenCalledTimes( 1 );
- expect( mockSetAttributes ).not.toHaveBeenCalled();
- } );
-} );
diff --git a/tests/src/blocks/remote-data-template/components/loop-template/LoopTemplate.test.tsx b/tests/src/blocks/remote-data-template/components/loop-template/LoopTemplate.test.tsx
index 765392761..4259b7fdc 100644
--- a/tests/src/blocks/remote-data-template/components/loop-template/LoopTemplate.test.tsx
+++ b/tests/src/blocks/remote-data-template/components/loop-template/LoopTemplate.test.tsx
@@ -1,8 +1,13 @@
import { cleanup, render } from '@testing-library/react';
-import { afterEach, describe, expect, it } from 'vitest';
+import { afterEach, describe, expect, it, vi } from 'vitest';
import { LoopTemplate } from '@/blocks/remote-data-template/components/loop-template/LoopTemplate';
+vi.mock( '@wordpress/data', () => ( {
+ useDispatch: vi.fn( () => ( { setPreviewIndex: vi.fn() } ) ),
+ useSelect: vi.fn(),
+} ) );
+
describe( 'LoopTemplate', () => {
const mockGetInnerBlocks = () => [];
const mockRemoteData: RemoteData = {
@@ -32,7 +37,7 @@ describe( 'LoopTemplate', () => {
afterEach( cleanup );
- it( 'renders a list when there are results', () => {
+ it( 'renders a list when there are more than one result', () => {
const { container } = render(
);
diff --git a/tests/src/utils/block-binding.test.ts b/tests/src/utils/block-binding.test.ts
index 4f52df9d4..f65c1fcf0 100644
--- a/tests/src/utils/block-binding.test.ts
+++ b/tests/src/utils/block-binding.test.ts
@@ -1,8 +1,7 @@
import { describe, expect, it } from 'vitest';
import { BLOCK_BINDING_SOURCE } from '@/config/constants';
-import { getBoundAttributeEntries, getMismatchedAttributes } from '@/utils/block-binding';
-import { createRemoteDataResults as createResults } from '@/utils/remote-data';
+import { getBoundAttributeEntries } from '@/utils/block-binding';
describe( 'block-binding utils', () => {
describe( 'getBoundAttributeEntries', () => {
@@ -35,138 +34,4 @@ describe( 'block-binding utils', () => {
expect( result ).toEqual( [] );
} );
} );
-
- describe( 'getMismatchedAttributes', () => {
- it( 'should return mismatched attributes', () => {
- const block = 'test/block';
- const attributes: RemoteDataInnerBlockAttributes = {
- content: 'Old content',
- url: 'https://old-url.com',
- alt: 'Old alt',
- metadata: {
- bindings: {
- content: { source: BLOCK_BINDING_SOURCE, args: { block, field: 'title' } },
- url: { source: BLOCK_BINDING_SOURCE, args: { block, field: 'link' } },
- },
- },
- };
-
- const results = createResults( [ { title: 'New content', link: 'https://new-url.com' } ] );
-
- const result = getMismatchedAttributes( attributes, results, block );
-
- expect( result ).toEqual( {
- content: 'New content',
- url: 'https://new-url.com',
- } );
- } );
-
- it( 'should return an empty object when no mismatches are found', () => {
- const block = 'test/block';
- const attributes: RemoteDataInnerBlockAttributes = {
- content: 'Title Current content',
- url: 'https://current-url.com',
- metadata: {
- bindings: {
- content: {
- source: BLOCK_BINDING_SOURCE,
- args: { block, field: 'title', label: 'Title' },
- },
- url: { source: BLOCK_BINDING_SOURCE, args: { block, field: 'link' } },
- },
- },
- };
-
- const results = createResults( [
- { title: 'Current content', link: 'https://current-url.com' },
- ] );
-
- const result = getMismatchedAttributes( attributes, results, block );
-
- expect( result ).toEqual( {} );
- } );
-
- it( 'should handle missing results', () => {
- const block = 'test/block';
- const attributes: RemoteDataInnerBlockAttributes = {
- content: 'Old content',
- url: 'https://old-url.com',
- metadata: {
- bindings: {
- content: { source: BLOCK_BINDING_SOURCE, args: { block, field: 'title' } },
- url: { source: BLOCK_BINDING_SOURCE, args: { block, field: 'link' } },
- },
- },
- };
-
- const results = createResults( [ { title: 'New content' } ] );
-
- const result = getMismatchedAttributes( attributes, results, block );
-
- expect( result ).toEqual( {
- content: 'New content',
- } );
- } );
-
- it( 'should handle missing label', () => {
- const block = 'test/block';
- const attributes: RemoteDataInnerBlockAttributes = {
- content: 'My Title',
- metadata: {
- bindings: {
- content: {
- source: BLOCK_BINDING_SOURCE,
- args: { block, field: 'title', label: 'Title' },
- },
- },
- },
- };
-
- const results = createResults( [ { title: 'My Title' } ] );
-
- const result = getMismatchedAttributes( attributes, results, block );
-
- expect( result ).toEqual( {
- content: 'Title My Title',
- } );
- } );
-
- it( 'should handle mismatched types', () => {
- const block = 'test/block';
- const attributes: RemoteDataInnerBlockAttributes = {
- content: '123',
- metadata: {
- bindings: {
- content: { source: BLOCK_BINDING_SOURCE, args: { block, field: 'a_field' } },
- },
- },
- };
-
- const results = createResults( [ { a_field: 123 } ] );
-
- const result = getMismatchedAttributes( attributes, results, block );
-
- expect( result ).toEqual( {} );
- } );
-
- it( 'should handle convert mismatched attributes to string', () => {
- const block = 'test/block';
- const attributes: RemoteDataInnerBlockAttributes = {
- content: '123',
- metadata: {
- bindings: {
- content: { source: BLOCK_BINDING_SOURCE, args: { block, field: 'a_field' } },
- },
- },
- };
-
- const results = createResults( [ { a_field: 1234 } ] );
-
- const result = getMismatchedAttributes( attributes, results, block );
-
- expect( result ).toEqual( {
- content: '1234',
- } );
- } );
- } );
} );
diff --git a/tests/src/utils/type-narrowing.test.ts b/tests/src/utils/type-narrowing.test.ts
deleted file mode 100644
index d6fbee405..000000000
--- a/tests/src/utils/type-narrowing.test.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { isObjectWithStringKeys, removeNullValuesFromObject } from '@/utils/type-narrowing';
-
-describe( 'type-narrowing utils', () => {
- describe( 'isObjectWithStringKeys', () => {
- it( 'should return true for objects with string keys', () => {
- expect( isObjectWithStringKeys( { a: 1, b: 'two' } ) ).toBe( true );
- expect( isObjectWithStringKeys( { 'key-with-dash': true } ) ).toBe( true );
- expect( isObjectWithStringKeys( {} ) ).toBe( true );
- } );
-
- it( 'should return false for arrays', () => {
- expect( isObjectWithStringKeys( [] ) ).toBe( false );
- expect( isObjectWithStringKeys( [ 1, 2, 3 ] ) ).toBe( false );
- } );
-
- it( 'should return false for null', () => {
- expect( isObjectWithStringKeys( null ) ).toBe( false );
- } );
-
- it( 'should return false for primitive types', () => {
- expect( isObjectWithStringKeys( 'string' ) ).toBe( false );
- expect( isObjectWithStringKeys( 123 ) ).toBe( false );
- expect( isObjectWithStringKeys( true ) ).toBe( false );
- expect( isObjectWithStringKeys( undefined ) ).toBe( false );
- } );
-
- it( 'should return false for functions', () => {
- expect( isObjectWithStringKeys( () => {} ) ).toBe( false );
- } );
-
- it( 'should return true for objects with symbol keys', () => {
- const obj = { [ Symbol( 'test' ) ]: 'value' };
- expect( isObjectWithStringKeys( obj ) ).toBe( true );
- } );
- } );
-
- describe( 'removeNullValuesFromObject', () => {
- it( 'should remove null values from object', () => {
- const obj = {
- aaa: 1,
- bbb: null,
- ccc: 'three',
- ddd: null,
- };
-
- expect( removeNullValuesFromObject( obj ) ).toEqual( { aaa: 1, ccc: 'three' } );
- } );
- } );
-} );
diff --git a/types/remote-data.d.ts b/types/remote-data.d.ts
index 8364b6ca1..ce1d877c3 100644
--- a/types/remote-data.d.ts
+++ b/types/remote-data.d.ts
@@ -68,6 +68,8 @@ interface RemoteDataBlockBindingArgs {
block: string;
field: string;
label?: string;
+ isPreview?: boolean;
+ previewIndex?: number;
}
interface RemoteDataBlockBinding {
diff --git a/types/wordpress__block-editor/index.d.ts b/types/wordpress__block-editor/index.d.ts
index d3f4df97b..9fed2ca2d 100644
--- a/types/wordpress__block-editor/index.d.ts
+++ b/types/wordpress__block-editor/index.d.ts
@@ -9,6 +9,12 @@ import { ReactElement } from '@wordpress/element/build-types/serialize';
declare module '@wordpress/block-editor' {
function BlockContextProvider( props: { children: ReactElement; value: object } ): JSX.Element;
+ function useBlockBindingsUtils( clientId?: string ): {
+ updateBlockBindings: (
+ bindings: Record< string, { args: object; source: string } | undefined >
+ ) => void;
+ };
+
function useBlockEditContext(): {
clientId: string;
[ key: string ]: unknown;
@@ -47,6 +53,7 @@ declare module '@wordpress/block-editor' {
getBlocksByClientId: < T extends BlockAttributes >( clientId: string ) => BlockInstance< T >[];
getBlocksByName: ( name: string ) => string[];
getPatternsByBlockTypes: ( name: string | string[], clientId?: string ) => BlockPattern[];
+ getSelectedBlock: < T extends BlockAttributes >() => BlockInstance< T > | undefined;
getSettings: () => EditorSettings;
hasMultiSelection: () => boolean;
}
diff --git a/types/wordpress__blocks/index.d.ts b/types/wordpress__blocks/index.d.ts
index 38bad373d..746a1ff39 100644
--- a/types/wordpress__blocks/index.d.ts
+++ b/types/wordpress__blocks/index.d.ts
@@ -9,16 +9,30 @@ import type { Block, BlockEditProps as BlockEditPropsOriginal } from '@wordpress
* The types provided by @wordpress/blocks are incomplete.
*/
-interface GetValuesPayload< Context, Values > {
- bindings: Values;
- clientId: string;
+interface ContextSelectPayload< Context > {
context: Context;
- select: ( store: BlockEditorStoreDescriptor ) => BlockEditorStoreSelectors;
+ select: < Selectors >( store: StoreDescriptor ) => Selectors;
+}
+
+interface GetValuesPayload< Context, Binding > extends ContextSelectPayload< Context > {
+ bindings: Record< string, Binding >;
+ clientId: string;
}
-interface SetValuesPayload< Context, Values > extends GetValuesPayload< Context, Values > {
+interface SetValuesPayload< Context, Binding > extends GetValuesPayload< Context, Binding > {
dispatch: ( store: BlockEditorStoreDescriptor ) => BlockEditorStoreActions;
- values: Values;
+}
+
+interface BaseBinding {
+ args: object;
+ source: string;
+}
+
+interface BaseItem {
+ key?: string;
+ label: string;
+ type: string;
+ value?: string;
}
// Properly allow simplified block registration calls when register_block_type() is already called server-side.
@@ -35,18 +49,39 @@ declare module '@wordpress/blocks' {
name: string;
}
- interface BlockBindingsSource< Context = Record< string, unknown >, Values = unknown > {
- canUserEditValue?: ( payload: GetValuesPayload< Context, Values > ) => boolean;
- getValues?: ( payload: GetValuesPayload< Context, Values > ) => Values;
+ interface BlockBindingsSource<
+ Context = Record< string, unknown >,
+ Binding extends BaseBinding,
+ Values = Record< string, unknown >
+ > {
+ canUserEditValue?: ( payload: ContextSelectPayload< Context > ) => boolean;
+ editorUI?: < Item extends BaseItem >(
+ payload: ContextSelectPayload< Context >
+ ) =>
+ | {
+ mode: 'dropdown';
+ data: Item[];
+ getArgs?: ( payload: ItemCallbackPayload< Item, Binding > ) => Binding[ 'args' ];
+ isSelected?: ( payload: ItemCallbackPayload< Item, Binding > ) => boolean;
+ }
+ | {
+ mode: 'modal';
+ data: Item[];
+ renderModalContent: ( payload: { attribute: string } ) => void;
+ }
+ | {};
+ getValues?: ( payload: GetValuesPayload< Context, Binding > ) => Values;
label?: string;
name: string;
- setValues?: ( payload: SetValuesPayload< Context, Values > ) => void;
+ setValues?: ( payload: SetValuesPayload< Context, Binding > ) => void;
usesContext?: string[];
}
- function registerBlockBindingsSource< Context, Values >(
- source: BlockBindingsSource< Context, Values >
- ): void;
+ function registerBlockBindingsSource<
+ Context,
+ Binding extends BaseBinding,
+ Values = Record< string, unknown >
+ >( source: BlockBindingsSource< Context, Binding, Values > ): void;
export function registerBlockType< TAttributes extends Record< string, any > = {} >(
name: string,
diff --git a/webpack.config.js b/webpack.config.js
index 8a8f90324..05ce4141b 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -5,6 +5,7 @@ const additionalScripts = {
'dataviews/index': './src/dataviews/index',
'pattern-editor/index': './src/pattern-editor/index',
'settings/index': './src/settings/index',
+ 'store/index': './src/store/index',
};
const { modernize, moduleConfig, scriptConfig } = require( './webpack.utils' );