From cbdd5f9ff7c5a6239ffdb93679028b9b647944ff Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 17 Jan 2024 13:39:21 +0100 Subject: [PATCH 01/12] Add sources registration mechanism and methods --- .../block-editor/src/store/private-actions.js | 10 ++++++++++ .../block-editor/src/store/private-selectors.js | 8 ++++++++ packages/block-editor/src/store/reducer.js | 16 ++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index a31455a0b7e7b..528b1dd4c31cb 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -240,6 +240,16 @@ export function clearBlockRemovalPrompt() { }; } +export function registerBlockBindingsSource( source ) { + return { + type: 'REGISTER_BLOCK_BINDINGS_SOURCE', + sourceName: source.name, + sourceLabel: source.label, + sourceComponent: source.component, + useSource: source.useSource, + }; +} + /** * Returns an action object used to set up any rules that a block editor may * provide in order to prevent a user from accidentally removing certain diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index d31a710fd94fe..253ec8d546064 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -170,6 +170,14 @@ export function getStyleOverrides( state ) { return state.styleOverrides; } +export function getAllBlockBindingsSources( state ) { + return state.blockBindingsSources; +} + +export function getBlockBindingsSource( state, sourceName ) { + return state?.blockBindingsSources?.[ sourceName ]; +} + /** @typedef {import('./actions').InserterMediaCategory} InserterMediaCategory */ /** * Returns the registered inserter media categories through the public API. diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index fa6c8942e66ad..c49307ee6234b 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1513,6 +1513,21 @@ function removalPromptData( state = false, action ) { return state; } +function blockBindingsSources( state = {}, action ) { + if ( action.type === 'REGISTER_BLOCK_BINDINGS_SOURCE' ) { + return { + ...state, + [ action.sourceName ]: { + label: action.sourceLabel, + component: action.sourceComponent, + useSource: action.useSource, + }, + }; + } + + return state; +} + /** * Reducer returning any rules that a block editor may provide in order to * prevent a user from accidentally removing certain blocks. These rules are @@ -2044,6 +2059,7 @@ const combinedReducers = combineReducers( { blockEditingModes, styleOverrides, removalPromptData, + blockBindingsSources, blockRemovalRules, openedBlockSettingsMenu, registeredInserterMediaCategories, From ac49c33f1f992efcb6bcb409e71472bf26f3ff5b Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 17 Jan 2024 17:14:47 +0100 Subject: [PATCH 02/12] Add initial UI for block bindings sources --- packages/block-editor/src/hooks/bindings.js | 110 ++++++++++++++++++++ packages/block-editor/src/hooks/index.js | 2 + 2 files changed, 112 insertions(+) create mode 100644 packages/block-editor/src/hooks/bindings.js diff --git a/packages/block-editor/src/hooks/bindings.js b/packages/block-editor/src/hooks/bindings.js new file mode 100644 index 0000000000000..6c5ec118ecadb --- /dev/null +++ b/packages/block-editor/src/hooks/bindings.js @@ -0,0 +1,110 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { + Button, + privateApis as componentsPrivateApis, +} from '@wordpress/components'; +import { plugins as pluginsIcon } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { BlockControls } from '../components'; +import { store as blockEditorStore } from '../store'; +import { unlock } from '../lock-unlock'; + +const { + DropdownMenuV2: DropdownMenu, + DropdownMenuItemV2: DropdownMenuItem, + DropdownMenuItemLabelV2: DropdownMenuItemLabel, +} = unlock( componentsPrivateApis ); + +const BLOCK_BINDINGS_ALLOWED_BLOCKS = { + 'core/paragraph': [ 'content' ], + 'core/heading': [ 'content' ], + 'core/image': [ 'url', 'title' ], + 'core/button': [ 'url', 'text' ], +}; + +function BlockBindingsUI( { name: blockName, clientId } ) { + const { attributes, sources } = useSelect( ( select ) => { + return { + attributes: + select( blockEditorStore ).getBlockAttributes( clientId ), + sources: unlock( + select( blockEditorStore ) + ).getAllBlockBindingsSources(), + }; + }, [] ); + + return ( + <> + + + } + > + { /* Iterate over block attributes */ } + { BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].map( + ( attribute ) => { + return ( + + + { attribute } + + + } + key={ attribute } + > + { /* Iterate over sources */ } + { Object.entries( sources ).map( + ( [ sourceName, source ] ) => { + return ( + + + { source.label } + + + } + key={ sourceName } + > + { source.component() } + + ); + } + ) } + + ); + } + ) } + + + + ); +} + +export default { + edit: BlockBindingsUI, + hasSupport() { + return true; + }, +}; diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index f17c0a22166e4..8bf060e54d265 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -11,6 +11,7 @@ import align from './align'; import './lock'; import anchor from './anchor'; import ariaLabel from './aria-label'; +import bindings from './bindings'; import customClassName from './custom-class-name'; import './generated-class-name'; import style from './style'; @@ -32,6 +33,7 @@ createBlockEditFilter( [ align, anchor, + bindings, customClassName, style, duotone, From e1dbd6a7e7bc15e06161497a3e56881ef69c97e2 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 18 Jan 2024 10:12:02 +0100 Subject: [PATCH 03/12] Add context to all blocks --- packages/block-editor/src/hooks/bindings.js | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/block-editor/src/hooks/bindings.js b/packages/block-editor/src/hooks/bindings.js index 6c5ec118ecadb..5f687f7f1ba09 100644 --- a/packages/block-editor/src/hooks/bindings.js +++ b/packages/block-editor/src/hooks/bindings.js @@ -6,6 +6,7 @@ import { Button, privateApis as componentsPrivateApis, } from '@wordpress/components'; +import { addFilter } from '@wordpress/hooks'; import { plugins as pluginsIcon } from '@wordpress/icons'; /** @@ -108,3 +109,25 @@ export default { return true; }, }; + +if ( window.__experimentalBlockBindings ) { + addFilter( + 'blocks.registerBlockType', + 'core/block-bindings-ui', + ( settings, name ) => { + if ( ! ( name in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) { + return settings; + } + const contextItems = [ 'postId', 'postType', 'queryId' ]; + const usesContextArray = settings.usesContext; + const oldUsesContextArray = new Set( usesContextArray ); + contextItems.forEach( ( item ) => { + if ( ! oldUsesContextArray.has( item ) ) { + usesContextArray.push( item ); + } + } ); + settings.usesContext = usesContextArray; + return settings; + } + ); +} From 4fee41a489176c6449d18e6d3a116aaf6771f061 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 18 Jan 2024 13:37:47 +0100 Subject: [PATCH 04/12] Pass props and attribute to source component --- packages/block-editor/src/hooks/bindings.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/hooks/bindings.js b/packages/block-editor/src/hooks/bindings.js index 5f687f7f1ba09..f8fce039d60b1 100644 --- a/packages/block-editor/src/hooks/bindings.js +++ b/packages/block-editor/src/hooks/bindings.js @@ -29,7 +29,8 @@ const BLOCK_BINDINGS_ALLOWED_BLOCKS = { 'core/button': [ 'url', 'text' ], }; -function BlockBindingsUI( { name: blockName, clientId } ) { +function BlockBindingsUI( props ) { + const { name: blockName, clientId } = props; const { attributes, sources } = useSelect( ( select ) => { return { attributes: @@ -88,7 +89,10 @@ function BlockBindingsUI( { name: blockName, clientId } ) { } key={ sourceName } > - { source.component() } + { source.component( + props, + attribute + ) } ); } @@ -105,6 +109,7 @@ function BlockBindingsUI( { name: blockName, clientId } ) { export default { edit: BlockBindingsUI, + attributeKeys: [ 'metadata' ], hasSupport() { return true; }, From 26115f260cc9c58789b8eb7ef697e306a001c07a Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 18 Jan 2024 14:30:11 +0100 Subject: [PATCH 05/12] Don't add bindings if block is not in the list --- packages/block-editor/src/hooks/bindings.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/block-editor/src/hooks/bindings.js b/packages/block-editor/src/hooks/bindings.js index f8fce039d60b1..a24b71e52da82 100644 --- a/packages/block-editor/src/hooks/bindings.js +++ b/packages/block-editor/src/hooks/bindings.js @@ -40,6 +40,9 @@ function BlockBindingsUI( props ) { ).getAllBlockBindingsSources(), }; }, [] ); + if ( ! ( blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) { + return null; + } return ( <> From cc72a8f15ed6f50f95e64a8a2b18033c32e75852 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 18 Jan 2024 14:33:39 +0100 Subject: [PATCH 06/12] WIP: Add temporary updateBindingsAttr util --- packages/block-editor/README.md | 12 +++++ packages/block-editor/src/utils/index.js | 1 + .../src/utils/update-bindings-attribute.js | 50 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 packages/block-editor/src/utils/update-bindings-attribute.js diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 5917ac235505c..d087023741953 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -812,6 +812,18 @@ _Properties_ Ensures that the text selection keeps the same vertical distance from the viewport during keyboard events within this component. The vertical distance can vary. It is the last clicked or scrolled to position. +### updateBlockBindingsAttribute + +Helper to update the bindings attribute used by the Block Bindings API. + +_Parameters_ + +- _blockAttributes_ `Object`: - The original block attributes. +- _setAttributes_ `Function`: - setAttributes function to modify the bindings property. +- _attributeName_ `string`: - The attribute in the bindings object to update. +- _sourceName_ `string`: - The source name added to the bindings property. +- _sourceAttributes_ `string`: - The source attributes added to the bindings property. + ### URLInput _Related_ diff --git a/packages/block-editor/src/utils/index.js b/packages/block-editor/src/utils/index.js index ee3b2692b369a..488e27d12da52 100644 --- a/packages/block-editor/src/utils/index.js +++ b/packages/block-editor/src/utils/index.js @@ -1,3 +1,4 @@ export { default as transformStyles } from './transform-styles'; export * from './block-variation-transforms'; export { default as getPxFromCssUnit } from './get-px-from-css-unit'; +export * from './update-bindings-attribute'; diff --git a/packages/block-editor/src/utils/update-bindings-attribute.js b/packages/block-editor/src/utils/update-bindings-attribute.js new file mode 100644 index 0000000000000..a5ee0671848e4 --- /dev/null +++ b/packages/block-editor/src/utils/update-bindings-attribute.js @@ -0,0 +1,50 @@ +/** + * Helper to update the bindings attribute used by the Block Bindings API. + * + * @param {Object} blockAttributes - The original block attributes. + * @param {Function} setAttributes - setAttributes function to modify the bindings property. + * @param {string} attributeName - The attribute in the bindings object to update. + * @param {string} sourceName - The source name added to the bindings property. + * @param {string} sourceAttributes - The source attributes added to the bindings property. + */ +export const updateBlockBindingsAttribute = ( + blockAttributes, + setAttributes, + attributeName, + sourceName, + sourceAttributes +) => { + // TODO: Review if we can create a React Hook for this. + + let updatedBindings = {}; + // // If no sourceName is provided, remove the attribute from the bindings. + if ( sourceName === null ) { + if ( ! blockAttributes?.metadata.bindings ) { + return blockAttributes?.metadata; + } + + updatedBindings = { + ...blockAttributes?.metadata?.bindings, + [ attributeName ]: undefined, + }; + if ( Object.keys( updatedBindings ).length === 1 ) { + updatedBindings = undefined; + } + } else { + updatedBindings = { + ...blockAttributes?.metadata?.bindings, + [ attributeName ]: { + source: { name: sourceName, attributes: sourceAttributes }, + }, + }; + } + + setAttributes( { + metadata: { + ...blockAttributes.metadata, + bindings: updatedBindings, + }, + } ); + + return blockAttributes.metadata; +}; From fde9f76b54fa9e73cf12614c645bfffaa7539796 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 18 Jan 2024 15:38:15 +0100 Subject: [PATCH 07/12] WIP: Hook to understand and update bindings attribute --- packages/block-editor/src/hooks/index.js | 1 + .../src/hooks/use-bindings-attributes.js | 101 ++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 packages/block-editor/src/hooks/use-bindings-attributes.js diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 8bf060e54d265..6a6383911b0c5 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -28,6 +28,7 @@ import contentLockUI from './content-lock-ui'; import './metadata'; import blockHooks from './block-hooks'; import blockRenaming from './block-renaming'; +import './use-bindings-attributes'; createBlockEditFilter( [ diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js new file mode 100644 index 0000000000000..11417101ad7a2 --- /dev/null +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -0,0 +1,101 @@ +/** + * WordPress dependencies + */ +import { createHigherOrderComponent } from '@wordpress/compose'; +import { useRegistry, useSelect } from '@wordpress/data'; +import { addFilter } from '@wordpress/hooks'; +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../store'; +import { useBlockEditContext } from '../components/block-edit/context'; +import { unlock } from '../lock-unlock'; + +/** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */ +/** @typedef {import('@wordpress/blocks').WPBlockSettings} WPBlockSettings */ + +/** + * Given a binding of block attributes, returns a higher order component that + * overrides its `attributes` and `setAttributes` props to sync any changes needed. + * + * @return {WPHigherOrderComponent} Higher-order component. + */ +const createEditFunctionWithBindingsAttribute = () => + createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const { clientId } = useBlockEditContext(); + + const { + getBlockBindingsSource, + getBlockAttributes, + updateBlockAttributes, + } = useSelect( ( select ) => { + return { + getBlockBindingsSource: unlock( select( blockEditorStore ) ) + .getBlockBindingsSource, + getBlockAttributes: + select( blockEditorStore ).getBlockAttributes, + updateBlockAttributes: + select( blockEditorStore ).updateBlockAttributes, + }; + }, [] ); + + const updatedAttributes = getBlockAttributes( clientId ); + if ( updatedAttributes?.metadata?.bindings ) { + Object.entries( updatedAttributes.metadata.bindings ).forEach( + ( [ attributeName, settings ] ) => { + const source = getBlockBindingsSource( + settings.source.name + ); + + if ( source ) { + // Second argument (`setValue`) will be used to update the value in the future. + const [ value ] = source.useSource( + props, + settings.source.attributes + ); + updatedAttributes[ attributeName ] = value; + } + } + ); + } + + const registry = useRegistry(); + + return ( + <> + + registry.batch( () => + updateBlockAttributes( blockId, newAttributes ) + ) + } + { ...props } + /> + + ); + }, + 'useBoundAttributes' + ); + +/** + * Filters a registered block's settings to enhance a block's `edit` component + * to upgrade bound attributes. + * + * @param {WPBlockSettings} settings Registered block settings. + * + * @return {WPBlockSettings} Filtered block settings. + */ +function shimAttributeSource( settings ) { + settings.edit = createEditFunctionWithBindingsAttribute()( settings.edit ); + + return settings; +} + +addFilter( + 'blocks.registerBlockType', + 'core/editor/custom-sources-backwards-compatibility/shim-attribute-source', + shimAttributeSource +); From 6a19c4218d5c9a071207de4c5d4fb51d9ea6b356 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 18 Jan 2024 15:39:44 +0100 Subject: [PATCH 08/12] Pass context to the `createBlockEditFilter` util --- packages/block-editor/src/hooks/utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index cd660c85826c2..57bb6b8648716 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -449,6 +449,7 @@ export function createBlockEditFilter( features ) { name={ props.name } isSelected={ props.isSelected } clientId={ props.clientId } + context={ props.context } setAttributes={ props.setAttributes } __unstableParentLayout={ props.__unstableParentLayout From 62b050073e51e0b654d38746bb27cf5cf979ffdb Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 18 Jan 2024 15:42:45 +0100 Subject: [PATCH 09/12] WIP: Register post meta source --- packages/editor/package.json | 2 +- packages/editor/src/bindings/index.js | 13 +++ packages/editor/src/bindings/post-meta.js | 100 ++++++++++++++++++++++ packages/editor/src/index.js | 1 + 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 packages/editor/src/bindings/index.js create mode 100644 packages/editor/src/bindings/post-meta.js diff --git a/packages/editor/package.json b/packages/editor/package.json index b974c7443851f..63c81cbd5cc7f 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -27,7 +27,7 @@ "sideEffects": [ "build-style/**", "src/**/*.scss", - "{src,build,build-module}/{index.js,store/index.js,hooks/**}" + "{src,build,build-module}/{index.js,store/index.js,hooks/**,bindings/**}" ], "dependencies": { "@babel/runtime": "^7.16.0", diff --git a/packages/editor/src/bindings/index.js b/packages/editor/src/bindings/index.js new file mode 100644 index 0000000000000..8a883e8904a71 --- /dev/null +++ b/packages/editor/src/bindings/index.js @@ -0,0 +1,13 @@ +/** + * WordPress dependencies + */ +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { dispatch } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { unlock } from '../lock-unlock'; +import postMeta from './post-meta'; + +const { registerBlockBindingsSource } = unlock( dispatch( blockEditorStore ) ); +registerBlockBindingsSource( postMeta ); diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js new file mode 100644 index 0000000000000..6bc2c1172560c --- /dev/null +++ b/packages/editor/src/bindings/post-meta.js @@ -0,0 +1,100 @@ +/** + * WordPress dependencies + */ +import { updateBlockBindingsAttribute } from '@wordpress/block-editor'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { useEntityProp, store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { unlock } from '../lock-unlock'; +import { store as editorStore } from '../store'; + +function PostMetaComponent( props, selectedAttribute ) { + const { + DropdownMenuGroupV2: DropdownMenuGroup, + DropdownMenuItemLabelV2: DropdownMenuItemLabel, + DropdownMenuCheckboxItemV2: DropdownMenuCheckboxItem, + } = unlock( componentsPrivateApis ); + const { context, metadata, setAttributes } = props; + // TODO: Expose the post meta in the `types` REST API endpoint and use that. + const data = useSelect( + ( select ) => { + const postId = context.postId + ? context.postId + : select( editorStore ).getCurrentPostId(); + const postType = context.postType + ? context.postType + : select( editorStore ).getCurrentPostType(); + const { getEntityRecord } = select( coreStore ); + return getEntityRecord( 'postType', postType, postId ); + }, + [ context.postId, context.postType ] + ); + if ( ! data || ! data.meta ) { + return null; + } + + // Prettify the name until the label is available in the REST API endpoint. + const keyToLabel = ( key ) => { + return key + .split( '_' ) + .map( ( word ) => word.charAt( 0 ).toUpperCase() + word.slice( 1 ) ) + .join( ' ' ); + }; + + return ( + + { Object.keys( data.meta ).map( ( key ) => { + return ( + { + updateBlockBindingsAttribute( + { metadata }, + setAttributes, + selectedAttribute, + 'post_meta', + { value: key } + ); + } } + > + + { keyToLabel( key ) } + + + ); + } ) } + + ); +} + +export default { + name: 'post_meta', + label: 'Post Meta', + component: PostMetaComponent, + useSource( props, sourceAttributes ) { + const { context } = props; + const { value: metaKey } = sourceAttributes; + const [ meta, setMeta ] = useEntityProp( + 'postType', + context.postType, + 'meta', + context.postId + ); + const metaValue = meta[ metaKey ]; + const updateMetaValue = ( newValue ) => { + setMeta( { ...meta, [ metaKey ]: newValue } ); + }; + return [ metaValue, updateMetaValue ]; + }, +}; diff --git a/packages/editor/src/index.js b/packages/editor/src/index.js index 05c04b8232907..3f6d7a78d837c 100644 --- a/packages/editor/src/index.js +++ b/packages/editor/src/index.js @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import './bindings'; import './hooks'; export { storeConfig, store } from './store'; From 166bf5206e6ee573d4d3e388aecdfe67f0871213 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 18 Jan 2024 16:25:08 +0100 Subject: [PATCH 10/12] Revert "Pass context to the `createBlockEditFilter` util" This reverts commit 6a19c4218d5c9a071207de4c5d4fb51d9ea6b356. --- packages/block-editor/src/hooks/utils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index 57bb6b8648716..cd660c85826c2 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -449,7 +449,6 @@ export function createBlockEditFilter( features ) { name={ props.name } isSelected={ props.isSelected } clientId={ props.clientId } - context={ props.context } setAttributes={ props.setAttributes } __unstableParentLayout={ props.__unstableParentLayout From 842b03095cadce7cba644546193a0970549a5930 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 18 Jan 2024 16:46:53 +0100 Subject: [PATCH 11/12] Pass `blockContext` to the bindings components --- packages/block-editor/src/hooks/bindings.js | 26 +++++++++++++-------- packages/editor/src/bindings/post-meta.js | 14 +++++------ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/block-editor/src/hooks/bindings.js b/packages/block-editor/src/hooks/bindings.js index a24b71e52da82..3b01a6b3b91fb 100644 --- a/packages/block-editor/src/hooks/bindings.js +++ b/packages/block-editor/src/hooks/bindings.js @@ -6,15 +6,16 @@ import { Button, privateApis as componentsPrivateApis, } from '@wordpress/components'; +import { useContext } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; import { plugins as pluginsIcon } from '@wordpress/icons'; - /** * Internal dependencies */ import { BlockControls } from '../components'; import { store as blockEditorStore } from '../store'; import { unlock } from '../lock-unlock'; +import BlockContext from '../components/block-context'; const { DropdownMenuV2: DropdownMenu, @@ -31,15 +32,19 @@ const BLOCK_BINDINGS_ALLOWED_BLOCKS = { function BlockBindingsUI( props ) { const { name: blockName, clientId } = props; - const { attributes, sources } = useSelect( ( select ) => { - return { - attributes: - select( blockEditorStore ).getBlockAttributes( clientId ), - sources: unlock( - select( blockEditorStore ) - ).getAllBlockBindingsSources(), - }; - }, [] ); + const { attributes, sources } = useSelect( + ( select ) => { + return { + attributes: + select( blockEditorStore ).getBlockAttributes( clientId ), + sources: unlock( + select( blockEditorStore ) + ).getAllBlockBindingsSources(), + }; + }, + [ clientId ] + ); + const blockContext = useContext( BlockContext ); if ( ! ( blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) { return null; } @@ -94,6 +99,7 @@ function BlockBindingsUI( props ) { > { source.component( props, + blockContext, attribute ) } diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 6bc2c1172560c..453a223fe8686 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -11,26 +11,26 @@ import { useSelect } from '@wordpress/data'; import { unlock } from '../lock-unlock'; import { store as editorStore } from '../store'; -function PostMetaComponent( props, selectedAttribute ) { +function PostMetaComponent( props, blockContext, selectedAttribute ) { const { DropdownMenuGroupV2: DropdownMenuGroup, DropdownMenuItemLabelV2: DropdownMenuItemLabel, DropdownMenuCheckboxItemV2: DropdownMenuCheckboxItem, } = unlock( componentsPrivateApis ); - const { context, metadata, setAttributes } = props; + const { metadata, setAttributes } = props; // TODO: Expose the post meta in the `types` REST API endpoint and use that. const data = useSelect( ( select ) => { - const postId = context.postId - ? context.postId + const postId = blockContext.postId + ? blockContext.postId : select( editorStore ).getCurrentPostId(); - const postType = context.postType - ? context.postType + const postType = blockContext.postType + ? blockContext.postType : select( editorStore ).getCurrentPostType(); const { getEntityRecord } = select( coreStore ); return getEntityRecord( 'postType', postType, postId ); }, - [ context.postId, context.postType ] + [ blockContext.postId, blockContext.postType ] ); if ( ! data || ! data.meta ) { return null; From ef5c81ea6899f195c9ebbf45e3298abdc999806f Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Fri, 19 Jan 2024 13:32:47 +0100 Subject: [PATCH 12/12] WIP: Adapt post meta source to work with templates --- .../src/hooks/use-bindings-attributes.js | 28 +++- packages/editor/src/bindings/post-meta.js | 127 ++++++++++++++---- 2 files changed, 127 insertions(+), 28 deletions(-) diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index 11417101ad7a2..f3a670cc1883e 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -20,6 +20,14 @@ import { unlock } from '../lock-unlock'; * * @return {WPHigherOrderComponent} Higher-order component. */ + +const BLOCK_BINDINGS_ALLOWED_BLOCKS = { + 'core/paragraph': [ 'content' ], + 'core/heading': [ 'content' ], + 'core/image': [ 'url', 'title' ], + 'core/button': [ 'url', 'text' ], +}; + const createEditFunctionWithBindingsAttribute = () => createHigherOrderComponent( ( BlockEdit ) => ( props ) => { @@ -49,12 +57,23 @@ const createEditFunctionWithBindingsAttribute = () => ); if ( source ) { - // Second argument (`setValue`) will be used to update the value in the future. - const [ value ] = source.useSource( + // Second argument (`updateMetaValue`) will be used to update the value in the future. + const { + placeholder, + useValue: [ metaValue = null ] = [], + } = source.useSource( props, settings.source.attributes ); - updatedAttributes[ attributeName ] = value; + + if ( placeholder ) { + updatedAttributes.placeholder = placeholder; + updatedAttributes[ attributeName ] = null; + } + + if ( metaValue ) { + updatedAttributes[ attributeName ] = metaValue; + } } } ); @@ -89,6 +108,9 @@ const createEditFunctionWithBindingsAttribute = () => * @return {WPBlockSettings} Filtered block settings. */ function shimAttributeSource( settings ) { + if ( ! ( settings.name in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) { + return settings; + } settings.edit = createEditFunctionWithBindingsAttribute()( settings.edit ); return settings; diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 453a223fe8686..7b3338295ed61 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -4,13 +4,24 @@ import { updateBlockBindingsAttribute } from '@wordpress/block-editor'; import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { useEntityProp, store as coreStore } from '@wordpress/core-data'; -import { useSelect } from '@wordpress/data'; +import { select } from '@wordpress/data'; /** * Internal dependencies */ import { unlock } from '../lock-unlock'; import { store as editorStore } from '../store'; +const { getCurrentPostId, getCurrentPostType } = select( editorStore ); +const { getEntityRecord, getEntityRecords } = select( coreStore ); + +// Prettify the name until the label is available in the REST API endpoint. +const keyToLabel = ( key ) => { + return key + .split( '_' ) + .map( ( word ) => word.charAt( 0 ).toUpperCase() + word.slice( 1 ) ) + .join( ' ' ); +}; + function PostMetaComponent( props, blockContext, selectedAttribute ) { const { DropdownMenuGroupV2: DropdownMenuGroup, @@ -18,31 +29,90 @@ function PostMetaComponent( props, blockContext, selectedAttribute ) { DropdownMenuCheckboxItemV2: DropdownMenuCheckboxItem, } = unlock( componentsPrivateApis ); const { metadata, setAttributes } = props; - // TODO: Expose the post meta in the `types` REST API endpoint and use that. - const data = useSelect( - ( select ) => { - const postId = blockContext.postId - ? blockContext.postId - : select( editorStore ).getCurrentPostId(); - const postType = blockContext.postType - ? blockContext.postType - : select( editorStore ).getCurrentPostType(); - const { getEntityRecord } = select( coreStore ); - return getEntityRecord( 'postType', postType, postId ); - }, - [ blockContext.postId, blockContext.postType ] - ); - if ( ! data || ! data.meta ) { - return null; + + // Get list of post_meta fields depending on the context. + const postId = blockContext.postId + ? blockContext.postId + : getCurrentPostId(); + const postType = blockContext.postType + ? blockContext.postType + : getCurrentPostType(); + + let data = {}; + if ( postType === 'wp_template' ) { + const { slug: templateSlug } = getEntityRecord( + 'postType', + 'wp_template', + postId + ); + + // Get the post type from the template slug. + + // Match "page-{slug}". + const pagePattern = /^page(?:-(.+))?$/; + // Match "single-{postType}-{slug}". + const postPattern = /^single-([^-]+)(?:-(.+))?$/; + // Match "wp-custom-template-{slug}". + const customTemplatePattern = /^wp-custom-template-(.+)$/; + // If it doesn't match any of the accepted patterns, return. + if ( + ! templateSlug !== 'index' && + ! templateSlug !== 'page' && + ! pagePattern.test( templateSlug ) && + ! postPattern.test( templateSlug ) && + ! customTemplatePattern.test( templateSlug ) + ) { + data = null; + } + + let records = []; + // If it is an index or a generic page template, return any page. + if ( templateSlug === 'index' || templateSlug === 'page' ) { + records = getEntityRecords( 'postType', 'page', { + per_page: 1, + } ); + } + + // If it is specific page template, return that one. + if ( pagePattern.test( templateSlug ) ) { + records = getEntityRecords( 'postType', 'page', { + slug: templateSlug.match( pagePattern )[ 1 ], + } ); + } + + // If it is post/cpt template. + if ( postPattern.test( templateSlug ) ) { + const [ , entityPostType, entitySlug ] = + templateSlug.match( postPattern ); + + // If it is a specific post. + if ( entitySlug ) { + records = getEntityRecords( 'postType', entityPostType, { + slug: entitySlug, + } ); + } else { + // If it is a generic template, return any post. + records = getEntityRecords( 'postType', entityPostType, { + per_page: 1, + } ); + } + } + + // If it is a custom template, get the fields from any page. + if ( customTemplatePattern.test( templateSlug ) || ! records ) { + records = getEntityRecords( 'postType', 'page', { + per_page: 1, + } ); + } + + data = records?.[ 0 ]; + } else { + data = getEntityRecord( 'postType', postType, postId ); } - // Prettify the name until the label is available in the REST API endpoint. - const keyToLabel = ( key ) => { - return key - .split( '_' ) - .map( ( word ) => word.charAt( 0 ).toUpperCase() + word.slice( 1 ) ) - .join( ' ' ); - }; + if ( ! data || ! data?.meta ) { + return null; + } return ( @@ -85,16 +155,23 @@ export default { useSource( props, sourceAttributes ) { const { context } = props; const { value: metaKey } = sourceAttributes; + const postType = context.postType + ? context.postType + : getCurrentPostType(); const [ meta, setMeta ] = useEntityProp( 'postType', context.postType, 'meta', context.postId ); + + if ( postType === 'wp_template' ) { + return { placeholder: keyToLabel( metaKey ) }; + } const metaValue = meta[ metaKey ]; const updateMetaValue = ( newValue ) => { setMeta( { ...meta, [ metaKey ]: newValue } ); }; - return [ metaValue, updateMetaValue ]; + return { useValue: [ metaValue, updateMetaValue ] }; }, };