Skip to content

Commit

Permalink
Block bindings: Bring bindings UI in Site Editor (#64072)
Browse files Browse the repository at this point in the history
* Initial commit. Add meta field to post types.

* Add post meta

* Add todos

* Add fields in all postType

* WIP: Add first version to link templates and entities

* Revert "WIP: Add first version to link templates and entities"

This reverts commit a43e391.

* Only expose public fields

* Add subtype to meta properties

* Render the appropriate fields depending on the postType in templates

* Use context postType when available

* Fetch the data on render, preventing one click needed

* Yoda conditions..

* Try: Expose registered meta fields in schema

* Try: Create a resolver to get registered post meta

* Use rest namespace

* Move actions and selectors to private.

* Add unlocking and import

* Merge useSelect

* Fix duplicated

* Add object_subtype to schema

* Update docs to object_subtype

* Add explanatory comment

* Block Bindings: Use default values in connected custom fields in templates (#65128)

* Abstract `getMetadata`  and use it in `getValues`

* Adapt e2e tests

* Update e2e

---------

Co-authored-by: SantosGuillamot <[email protected]>
Co-authored-by: cbravobernal <[email protected]>
Co-authored-by: gziolo <[email protected]>
Co-authored-by: mtias <[email protected]>

* Try removing all object subtype

* Fix e2e

* Update code

* Fix `useSelect` warning

* Remove old comment

* Remove support for generic templates

* Revert changes to e2e tests

---------

Co-authored-by: mtias <[email protected]>
Co-authored-by: cbravobernal <[email protected]>
Co-authored-by: SantosGuillamot <[email protected]>
Co-authored-by: Mamaduka <[email protected]>
Co-authored-by: gziolo <[email protected]>
Co-authored-by: youknowriad <[email protected]>
Co-authored-by: tyxla <[email protected]>
Co-authored-by: TimothyBJacobs <[email protected]>
Co-authored-by: artemiomorales <[email protected]>
Co-authored-by: spacedmonkey <[email protected]>
  • Loading branch information
11 people committed Sep 17, 2024
1 parent 636710b commit 893181e
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 76 deletions.
101 changes: 58 additions & 43 deletions packages/block-editor/src/hooks/block-bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { store as blockEditorStore } from '../store';

const { DropdownMenuV2 } = unlock( componentsPrivateApis );

const EMPTY_OBJECT = {};

const useToolsPanelDropdownMenuProps = () => {
const isMobile = useViewportMatch( 'medium', '<' );
return ! isMobile
Expand Down Expand Up @@ -182,11 +184,66 @@ function EditableBlockBindingsPanelItems( {
export const BlockBindingsPanel = ( { name: blockName, metadata } ) => {
const registry = useRegistry();
const blockContext = useContext( BlockContext );
const { bindings } = metadata || {};
const { removeAllBlockBindings } = useBlockBindingsUtils();
const bindableAttributes = getBindableAttributes( blockName );
const dropdownMenuProps = useToolsPanelDropdownMenuProps();

// `useSelect` is used purposely here to ensure `getFieldsList`
// is updated whenever there are updates in block context.
// `source.getFieldsList` may also call a selector via `registry.select`.
const _fieldsList = {};
const { fieldsList, canUpdateBlockBindings } = useSelect(
( select ) => {
if ( ! bindableAttributes || bindableAttributes.length === 0 ) {
return EMPTY_OBJECT;
}
const { getBlockBindingsSources } = unlock( blocksPrivateApis );
const registeredSources = getBlockBindingsSources();
Object.entries( registeredSources ).forEach(
( [ sourceName, { getFieldsList, usesContext } ] ) => {
if ( getFieldsList ) {
// Populate context.
const context = {};
if ( usesContext?.length ) {
for ( const key of usesContext ) {
context[ key ] = blockContext[ key ];
}
}
const sourceList = getFieldsList( {
registry,
context,
} );
// Only add source if the list is not empty.
if ( sourceList ) {
_fieldsList[ sourceName ] = { ...sourceList };
}
}
}
);
return {
fieldsList:
Object.values( _fieldsList ).length > 0
? _fieldsList
: EMPTY_OBJECT,
canUpdateBlockBindings:
select( blockEditorStore ).getSettings()
.canUpdateBlockBindings,
};
},
[ blockContext, bindableAttributes, registry ]
);
// Return early if there are no bindable attributes.
if ( ! bindableAttributes || bindableAttributes.length === 0 ) {
return null;
}
// Remove empty sources from the list of fields.
Object.entries( fieldsList ).forEach( ( [ key, value ] ) => {
if ( ! Object.keys( value ).length ) {
delete fieldsList[ key ];
}
} );
// Filter bindings to only show bindable attributes and remove pattern overrides.
const { bindings } = metadata || {};
const filteredBindings = { ...bindings };
Object.keys( filteredBindings ).forEach( ( key ) => {
if (
Expand All @@ -197,48 +254,6 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => {
}
} );

const { canUpdateBlockBindings } = useSelect( ( select ) => {
return {
canUpdateBlockBindings:
select( blockEditorStore ).getSettings().canUpdateBlockBindings,
};
}, [] );

if ( ! bindableAttributes || bindableAttributes.length === 0 ) {
return null;
}

const fieldsList = {};
const { getBlockBindingsSources } = unlock( blocksPrivateApis );
const registeredSources = getBlockBindingsSources();
Object.entries( registeredSources ).forEach(
( [ sourceName, { getFieldsList, usesContext } ] ) => {
if ( getFieldsList ) {
// Populate context.
const context = {};
if ( usesContext?.length ) {
for ( const key of usesContext ) {
context[ key ] = blockContext[ key ];
}
}
const sourceList = getFieldsList( {
registry,
context,
} );
// Only add source if the list is not empty.
if ( sourceList ) {
fieldsList[ sourceName ] = { ...sourceList };
}
}
}
);
// Remove empty sources.
Object.entries( fieldsList ).forEach( ( [ key, value ] ) => {
if ( ! Object.keys( value ).length ) {
delete fieldsList[ key ];
}
} );

// Lock the UI when the user can't update bindings or there are no fields to connect to.
const readOnly =
! canUpdateBlockBindings || ! Object.keys( fieldsList ).length;
Expand Down
2 changes: 2 additions & 0 deletions packages/core-data/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import reducer from './reducer';
import * as selectors from './selectors';
import * as privateSelectors from './private-selectors';
import * as actions from './actions';
import * as privateActions from './private-actions';
import * as resolvers from './resolvers';
import createLocksActions from './locks/actions';
import {
Expand Down Expand Up @@ -79,6 +80,7 @@ const storeConfig = () => ( {
*/
export const store = createReduxStore( STORE_NAME, storeConfig() );
unlock( store ).registerPrivateSelectors( privateSelectors );
unlock( store ).registerPrivateActions( privateActions );
register( store ); // Register store after unlocking private selectors to allow resolvers to use them.

export { default as EntityProvider } from './entity-provider';
Expand Down
16 changes: 16 additions & 0 deletions packages/core-data/src/private-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Returns an action object used in signalling that the registered post meta
* fields for a post type have been received.
*
* @param {string} postType Post type slug.
* @param {Object} registeredPostMeta Registered post meta.
*
* @return {Object} Action object.
*/
export function receiveRegisteredPostMeta( postType, registeredPostMeta ) {
return {
type: 'RECEIVE_REGISTERED_POST_META',
postType,
registeredPostMeta,
};
}
12 changes: 12 additions & 0 deletions packages/core-data/src/private-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,15 @@ export function getEntityRecordPermissions(
) {
return getEntityRecordsPermissions( state, kind, name, id )[ 0 ];
}

/**
* Returns the registered post meta fields for a given post type.
*
* @param state Data state.
* @param postType Post type.
*
* @return Registered post meta fields.
*/
export function getRegisteredPostMeta( state: State, postType: string ) {
return state.registeredPostMeta?.[ postType ] ?? {};
}
20 changes: 20 additions & 0 deletions packages/core-data/src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,25 @@ export function defaultTemplates( state = {}, action ) {
return state;
}

/**
* Reducer returning an object of registered post meta.
*
* @param {Object} state Current state.
* @param {Object} action Dispatched action.
*
* @return {Object} Updated state.
*/
export function registeredPostMeta( state = {}, action ) {
switch ( action.type ) {
case 'RECEIVE_REGISTERED_POST_META':
return {
...state,
[ action.postType ]: action.registeredPostMeta,
};
}
return state;
}

export default combineReducers( {
terms,
users,
Expand All @@ -649,4 +668,5 @@ export default combineReducers( {
userPatternCategories,
navigationFallbackId,
defaultTemplates,
registeredPostMeta,
} );
26 changes: 26 additions & 0 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -984,3 +984,29 @@ export const getRevision =
dispatch.receiveRevisions( kind, name, recordKey, record, query );
}
};

/**
* Requests a specific post type options from the REST API.
*
* @param {string} postType Post type slug.
*/
export const getRegisteredPostMeta =
( postType ) =>
async ( { dispatch, resolveSelect } ) => {
try {
const {
rest_namespace: restNamespace = 'wp/v2',
rest_base: restBase,
} = ( await resolveSelect.getPostType( postType ) ) || {};
const options = await apiFetch( {
path: `${ restNamespace }/${ restBase }/?context=edit`,
method: 'OPTIONS',
} );
dispatch.receiveRegisteredPostMeta(
postType,
options?.schema?.properties?.meta?.properties
);
} catch {
dispatch.receiveRegisteredPostMeta( postType, false );
}
};
1 change: 1 addition & 0 deletions packages/core-data/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface State {
navigationFallbackId: EntityRecordKey;
userPatternCategories: Array< UserPatternCategory >;
defaultTemplates: Record< string, string >;
registeredPostMeta: Record< string, { postType: string } >;
}

type EntityRecordKey = string | number;
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e-tests/plugins/block-bindings.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function gutenberg_test_block_bindings_registration() {
'show_in_rest' => true,
'type' => 'string',
'single' => true,
'default' => 'Value of the text_custom_field',
'default' => 'Value of the text custom field',
)
);
register_meta(
Expand Down
46 changes: 31 additions & 15 deletions packages/editor/src/bindings/post-meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,43 @@ import { store as coreDataStore } from '@wordpress/core-data';
* Internal dependencies
*/
import { store as editorStore } from '../store';
import { unlock } from '../lock-unlock';

function getMetadata( registry, context ) {
let metaFields = {};
const { type } = registry.select( editorStore ).getCurrentPost();
const { getEditedEntityRecord } = registry.select( coreDataStore );
const { getRegisteredPostMeta } = unlock(
registry.select( coreDataStore )
);

if ( type === 'wp_template' ) {
const fields = getRegisteredPostMeta( context?.postType );
// Populate the `metaFields` object with the default values.
Object.entries( fields || {} ).forEach( ( [ key, props ] ) => {
metaFields[ key ] = props.default;
} );
} else {
metaFields = getEditedEntityRecord(
'postType',
context?.postType,
context?.postId
).meta;
}

return metaFields;
}

export default {
name: 'core/post-meta',
getValues( { registry, context, bindings } ) {
const meta = registry
.select( coreDataStore )
.getEditedEntityRecord(
'postType',
context?.postType,
context?.postId
)?.meta;
const metaFields = getMetadata( registry, context );

const newValues = {};
for ( const [ attributeName, source ] of Object.entries( bindings ) ) {
// Use the key if the value is not set.
newValues[ attributeName ] =
meta?.[ source.args.key ] ?? source.args.key;
metaFields?.[ source.args.key ] ?? source.args.key;
}
return newValues;
},
Expand Down Expand Up @@ -82,19 +103,14 @@ export default {
return true;
},
getFieldsList( { registry, context } ) {
const metaFields = registry
.select( coreDataStore )
.getEditedEntityRecord(
'postType',
context?.postType,
context?.postId
).meta;
const metaFields = getMetadata( registry, context );

if ( ! metaFields || ! Object.keys( metaFields ).length ) {
return null;
}

// Remove footnotes or private keys from the list of fields.
// TODO: Remove this once we retrieve the fields from 'types' endpoint in post or page editor.
return Object.fromEntries(
Object.entries( metaFields ).filter(
( [ key ] ) => key !== 'footnotes' && key.charAt( 0 ) !== '_'
Expand Down
Loading

0 comments on commit 893181e

Please sign in to comment.