Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment/meta post type api with context #65076

Draft
wants to merge 17 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 48 additions & 38 deletions packages/block-editor/src/hooks/block-bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,64 +182,74 @@ 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();

const filteredBindings = { ...bindings };
Object.keys( filteredBindings ).forEach( ( key ) => {
if (
! canBindAttribute( blockName, key ) ||
filteredBindings[ key ].source === 'core/pattern-overrides'
) {
delete filteredBindings[ key ];
}
} );

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 ];
// While this hook doesn't directly call any selectors, `useSelect` is
// used purposely here to ensure `getFieldsList` is updated whenever
// there are attribute updates.
// `source.getFieldsList` may also call a selector via `registry.select`.
const { fieldsList } = useSelect( () => {
if ( ! bindableAttributes || bindableAttributes.length === 0 ) {
return {};
}
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 };
}
}
const sourceList = getFieldsList( {
registry,
context,
} );
// Only add source if the list is not empty.
if ( sourceList ) {
fieldsList[ sourceName ] = { ...sourceList };
}
}
}
);
// Remove empty sources.
);
return { fieldsList: _fieldsList };
}, [ 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 (
! canBindAttribute( blockName, key ) ||
filteredBindings[ key ].source === 'core/pattern-overrides'
) {
delete filteredBindings[ key ];
}
} );

// Lock the UI when the user can't update bindings or there are no fields to connect to.
// Lock the UI when the experiment is not enabled or there are no fields to connect to.
const readOnly =
! canUpdateBlockBindings || ! Object.keys( fieldsList ).length;

Expand Down
1 change: 1 addition & 0 deletions packages/core-data/src/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ async function loadPostTypeEntities() {
baseURLParams: { context: 'edit' },
name,
label: postType.name,
meta: postType.meta,
transientEdits: {
blocks: true,
selection: true,
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 @@ -976,3 +976,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 ( { select, dispatch } ) => {
try {
const {
rest_namespace: restNamespace = 'wp/v2',
rest_base: restBase,
} = select.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
56 changes: 53 additions & 3 deletions packages/editor/src/bindings/post-meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { store as coreDataStore } from '@wordpress/core-data';
* Internal dependencies
*/
import { store as editorStore } from '../store';
import { unlock } from '../lock-unlock';

export default {
name: 'core/post-meta',
Expand Down Expand Up @@ -82,19 +83,68 @@ export default {
return true;
},
getFieldsList( { registry, context } ) {
const metaFields = registry
.select( coreDataStore )
.getEditedEntityRecord(
let metaFields = {};
const {
type,
is_custom: isCustom,
slug,
} = registry.select( editorStore ).getCurrentPost();
const { getPostTypes, getEditedEntityRecord } =
registry.select( coreDataStore );

const { getRegisteredPostMeta } = unlock(
registry.select( coreDataStore )
);

// Inherit the postType from the slug if it is a template.
if ( ! context?.postType && type === 'wp_template' ) {
// Get the 'kind' from the start of the slug.
// Use 'post' as the default.
let postType = 'post';
const isGlobalTemplate = isCustom || slug === 'index';
if ( ! isGlobalTemplate ) {
const [ kind ] = slug.split( '-' );
if ( kind === 'page' ) {
postType = 'page';
} else if ( kind === 'single' ) {
const postTypes =
getPostTypes( { per_page: -1 } )?.map(
( entity ) => entity.slug
) || [];

// Infer the post type from the slug.
// TODO: Review, as it may not have a post type. http://localhost:8888/wp-admin/site-editor.php?canvas=edit
const match = slug.match(
`^single-(${ postTypes.join( '|' ) })(?:-.+)?$`
);
postType = match ? match[ 1 ] : 'post';
}
}
const fields = getRegisteredPostMeta( postType );

// Populate the `metaFields` object with the default values.
Object.entries( fields || {} ).forEach( ( [ key, props ] ) => {
// If the template is global, skip the fields with a subtype.
// TODO: Add subtype to schema to be able to filter.
if ( isGlobalTemplate && props.subtype ) {
return;
}
metaFields[ key ] = props.default;
} );
} else {
metaFields = getEditedEntityRecord(
'postType',
context?.postType,
context?.postId
).meta;
}

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
50 changes: 40 additions & 10 deletions packages/editor/src/components/provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
import { useEffect, useLayoutEffect, useMemo } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { EntityProvider, useEntityBlockEditor } from '@wordpress/core-data';
import {
EntityProvider,
useEntityBlockEditor,
store as coreStore,
} from '@wordpress/core-data';
import {
BlockEditorProvider,
BlockContextProvider,
Expand Down Expand Up @@ -48,7 +52,6 @@ const noop = () => {};
*/
const NON_CONTEXTUAL_POST_TYPES = [
'wp_block',
'wp_template',
'wp_navigation',
'wp_template_part',
];
Expand Down Expand Up @@ -161,31 +164,57 @@ export const ExperimentalEditorProvider = withRegistryProvider(
BlockEditorProviderComponent = ExperimentalBlockEditorProvider,
__unstableTemplate: template,
} ) => {
const { editorSettings, selection, isReady, mode } = useSelect(
( select ) => {
const { editorSettings, selection, isReady, mode, postTypes } =
useSelect( ( select ) => {
const {
getEditorSettings,
getEditorSelection,
getRenderingMode,
__unstableIsEditorReady,
} = select( editorStore );
const { getPostTypes } = select( coreStore );

return {
editorSettings: getEditorSettings(),
isReady: __unstableIsEditorReady(),
mode: getRenderingMode(),
selection: getEditorSelection(),
postTypes:
getPostTypes( { per_page: -1 } )?.map(
( entity ) => entity.slug
) || [],
};
},
[]
);
}, [] );
const shouldRenderTemplate = !! template && mode !== 'post-only';
const rootLevelPost = shouldRenderTemplate ? template : post;
const defaultBlockContext = useMemo( () => {
const postContext =
const postContext = {};
// If it is a template, try to inherit the post type from the slug.
if ( post.type === 'wp_template' ) {
if ( ! post.is_custom ) {
const [ kind ] = post.slug.split( '-' );
switch ( kind ) {
case 'page':
postContext.postType = 'page';
break;
case 'single':
// Infer the post type from the slug.
const match = post.slug.match(
`^single-(${ postTypes.join( '|' ) })(?:-.+)?$`
);
if ( match ) {
postContext.postType = match[ 1 ];
}
break;
}
}
} else if (
! NON_CONTEXTUAL_POST_TYPES.includes( rootLevelPost.type ) ||
shouldRenderTemplate
? { postId: post.id, postType: post.type }
: {};
) {
postContext.postId = post.id;
postContext.postType = post.type;
}

return {
...postContext,
Expand All @@ -200,6 +229,7 @@ export const ExperimentalEditorProvider = withRegistryProvider(
post.type,
rootLevelPost.type,
rootLevelPost.slug,
postTypes,
] );
const { id, type } = rootLevelPost;
const blockEditorSettings = useBlockEditorSettings(
Expand Down
Loading