From 12f355efa5dc2227bb8bebfb0b16b7c25029d245 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:05:05 +0200 Subject: [PATCH 01/42] Initial commit. Add meta field to post types. --- lib/compat/wordpress-6.7/rest-api.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index c5e2927198da0..011019cda34b4 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -114,3 +114,27 @@ function gutenberg_override_default_rest_server() { return 'Gutenberg_REST_Server'; } add_filter( 'wp_rest_server_class', 'gutenberg_override_default_rest_server', 1 ); + +if ( ! function_exists( 'gutenberg_register_wp_rest_post_types_meta_fields' ) ) { + /** + * Adds `template` and `template_lock` fields to WP_REST_Post_Types_Controller class. + */ + function gutenberg_register_wp_rest_post_types_meta_fields() { + register_rest_field( + 'type', + 'meta', + array( + 'get_callback' => function ( $item ) { + return get_registered_meta_keys( $item['slug'] ); + }, + 'schema' => array( + 'type' => 'array', + 'description' => __( 'Meta Keys', 'gutenberg' ), + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + ) + ); + } +} +add_action( 'rest_api_init', 'gutenberg_register_wp_rest_post_types_meta_fields' ); From 9ba93b030b6e53b21eab2a8f2e028cfee4d21c2a Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:05:05 +0200 Subject: [PATCH 02/42] Add post meta --- packages/core-data/src/entities.js | 1 + packages/editor/src/bindings/post-meta.js | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index 8d09402087cf9..05f7f55ef759b 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -304,6 +304,7 @@ async function loadPostTypeEntities() { baseURLParams: { context: 'edit' }, name, label: postType.name, + meta: postType.meta, transientEdits: { blocks: true, selection: true, diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 0562c1f7adf07..092e013d36774 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -105,8 +105,21 @@ export default { getFieldsList( { registry, context } ) { const metaFields = getMetadata( registry, context ); + const fields = registry + .select( coreDataStore ) + .getEntityRecord( 'root', 'postType', 'post' ); + if ( ! metaFields || ! Object.keys( metaFields ).length ) { - return null; + if ( ! fields?.meta ) { + return null; + } + const metaDefaults = {}; + for ( const key in fields.meta ) { + if ( fields.meta.hasOwnProperty( key ) ) { + metaDefaults[ key ] = fields.meta[ key ].default; + } + } + return metaDefaults; } // Remove footnotes or private keys from the list of fields. From 364dc27d89d83f49053d6c7f99c7a3926e37c043 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:05:05 +0200 Subject: [PATCH 03/42] Add todos --- packages/editor/src/bindings/post-meta.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 092e013d36774..ebe63fcdea1f3 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -105,8 +105,10 @@ export default { getFieldsList( { registry, context } ) { const metaFields = getMetadata( registry, context ); + // TODO: Fields returns undefined on the first click. const fields = registry .select( coreDataStore ) + // TODO: Last item 'post' should not be hardcoded. .getEntityRecord( 'root', 'postType', 'post' ); if ( ! metaFields || ! Object.keys( metaFields ).length ) { From d3491d664c7b3a2716479c3100d4ad01ea1aea80 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:05:05 +0200 Subject: [PATCH 04/42] Add fields in all postType --- lib/compat/wordpress-6.7/rest-api.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index 011019cda34b4..f0ef56c40a1e5 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -125,7 +125,9 @@ function gutenberg_register_wp_rest_post_types_meta_fields() { 'meta', array( 'get_callback' => function ( $item ) { - return get_registered_meta_keys( $item['slug'] ); + $default_fields = get_registered_meta_keys( 'post' ); + $post_type_fields = get_registered_meta_keys( 'post', $item['slug'] ); + return array_merge( $default_fields, $post_type_fields ); }, 'schema' => array( 'type' => 'array', From 19cb1f9788a0f1ab9fe40017a854f21380f723f8 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:06:34 +0200 Subject: [PATCH 05/42] WIP: Add first version to link templates and entities --- packages/core-data/src/private-selectors.ts | 244 ++++++++++++++++++++ 1 file changed, 244 insertions(+) diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index b2f6fa7def985..db674b40efa4c 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -11,6 +11,11 @@ import { STORE_NAME } from './name'; type EntityRecordKey = string | number; +type TemplateQuery = { + slug: string; + is_custom?: boolean; +}; + /** * Returns the previous edit from the current undo offset * for the entity records edits history, if any. @@ -105,3 +110,242 @@ export function getEntityRecordPermissions( export function getRegisteredPostMeta( state: State, postType: string ) { return state.registeredPostMeta?.[ postType ] ?? {}; } +export const getRelatedEditedEntityRecordsByTemplate = createRegistrySelector( + ( select ) => + createSelector( + ( state: State, template: TemplateQuery ) => { + /* + * Get the relationship between the template slug and the entity records. + * Similar to how the Template Hierarchy works: https://developer.wordpress.org/themes/basics/template-hierarchy/ + * + * It returns the specific entity record if it is specified and if not it returns the root entity record. + * + * These are the possible slugs and the related entities. + * + * ARCHIVES + * + * archive: Any taxonomy, author, and date. + * author: Author archives. + * author-{user-slug}: Specific author. + * category: Taxonomy "category" archives. + * category-{category-slug}: Specific category. + * date: Post archive for a specific date. + * tag: Taxonomy "post_tag" archives. + * tag-{tag-slug}: Specific tag. + * taxonomy-{tax-slug}: Specific taxonomy archives. + * taxonomy-{tax-slug}-{item-slug}: Specific item of a specific taxonomy. + * + * POST TYPES + * + * index: Any post type. + * page: Post type "page". + * page-{page-slug}: Specific "page". + * single: Post type "post". + * single-post: Post type "post". + * single-post-{post-slug}: Specific "post". + * single-{cpt-slug}: Specific post type. + * single-{cpt-slug}-{item-slug}: Specific item of a specific post type. + * + * SPECIAL CASES + * + * home: Latest posts as either the site homepage or as the "Posts page". + * front-page: Homepage whether it is set to display latest posts or a static page. Overrides `home`. + * 404: Displays when a visitor views a non-existent page. + * search: Displays when a visitor performs a search on the website. + * + * Custom templates apply to posts, pages, or custom post types. + */ + + // TODO: Review archive-{post-type} and attachment. + // TODO: Await somehow for getEntityRecords calls. + + const { slug, is_custom: isCustom } = template; + + // Custom templates. + if ( isCustom ) { + // Return all post types. + return select( STORE_NAME ).getEntityRecords( + 'root', + 'postType' + ); + } + + // Homepage templates. + if ( slug === 'home' || slug === 'front-page' ) { + // TODO: Review how to get the page on front. + const { page_on_front: pageOnFront } = select( + STORE_NAME + ).getEntityRecord( 'root', 'site' ); + + if ( pageOnFront === 0 ) { + // Homepage displays latest posts. + const postsEntity = select( + STORE_NAME + ).getEntityRecord( 'root', 'postType', 'posts' ); + return postsEntity ? [ postsEntity ] : undefined; + } + // Homepage displays a static page. + const pageEntity = select( STORE_NAME ).getEntityRecord( + 'postType', + 'page', + pageOnFront + ); + return pageEntity ? [ pageEntity ] : undefined; + } + + // Special cases. + // TODO: Review what to return in these cases. + if ( slug === 'date' || slug === '404' || slug === 'search' ) { + return; + } + + // First item corresponds to the type. + // eslint-disable-next-line @wordpress/no-unused-vars-before-return + const [ type, ...slugParts ] = slug.split( '-' ); + + // Author archives. + // TODO: Review what to return in these cases. + if ( type === 'author' ) { + return; + } + + // Build the query. + let kind, entitySlug, itemSlug; + + // Get the `kind`. + switch ( type ) { + case 'archive': + case 'taxonomy': + case 'category': + case 'tag': + kind = 'taxonomy'; + break; + + case 'index': + case 'single': + case 'page': + kind = 'postType'; + break; + } + + // Generate the `entitySlug` and `itemSlug`. + switch ( type ) { + case 'category': + entitySlug = 'category'; + if ( slugParts.length ) { + itemSlug = slugParts.join( '-' ); + } + break; + + case 'tag': + entitySlug = 'post_tag'; + if ( slugParts.length ) { + itemSlug = slugParts.join( '-' ); + } + break; + + case 'page': + entitySlug = 'page'; + if ( slugParts.length ) { + itemSlug = slugParts.join( '-' ); + } + break; + + case 'taxonomy': + case 'single': + /* + * Extract entitySlug and itemSlug from the slugParts. + * Slugs can contain dashes. + * + * taxonomy-{tax-slug} + * taxonomy-{tax-slug}-{item-slug} + * single + * single-{cpt-slug} + * single-{cpt-slug}-{item-slug} + */ + if ( ! slugParts.length ) { + if ( type === 'single' ) { + entitySlug = 'post'; + } + break; + } + let firstSlug = ''; + for ( let i = 0; i < slugParts.length; i++ ) { + if ( firstSlug === '' ) { + firstSlug = slugParts[ i ]; + } else { + firstSlug += `-${ slugParts[ i ] }`; + } + + // Check if the current combination is an existing taxonomy or post type. + // TODO: Check better way to get defined taxonomies or post types. + const existingPostTypes = Object.keys( + state.entities.records.postType + ); + const existingTaxonomies = Object.keys( + state.entities.records.taxonomy + ); + if ( + existingTaxonomies.includes( firstSlug ) || + existingPostTypes.includes( firstSlug ) + ) { + entitySlug = firstSlug; + const remainingParts = slugParts.slice( i + 1 ); + if ( remainingParts.length ) { + itemSlug = remainingParts.join( '-' ); + } + break; + } + } + break; + } + + if ( ! entitySlug ) { + /* + * archive + * index + */ + return select( STORE_NAME ).getEntityRecords( + 'root', + kind + ); + } + + if ( ! itemSlug ) { + /* + * category + * tag + * taxonomy-{tax-slug} + * page + * single + * single-{cpt-slug} + */ + + // It seems it is not possible to filter by slug in `getEntityRecords`. + const rootEntity = select( STORE_NAME ).getEntityRecord( + 'root', + kind, + entitySlug + ); + return rootEntity ? [ rootEntity ] : undefined; + } + + /* + * category-{category-slug} + * tag-{tag-slug} + * page-{page-slug} + * taxonomy-{tax-slug}-{item-slug} + * single-{cpt-slug}-{item-slug} + */ + return select( STORE_NAME ).getEntityRecords( + kind, + entitySlug, + { + slug: itemSlug, + } + ); + }, + // TODO: Review what to include here. + ( state ) => [ state.entities.records ] + ) +); From 1e8c3bf72424fc132a2fe13f9afa4271e973f78c Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:06:57 +0200 Subject: [PATCH 06/42] Revert "WIP: Add first version to link templates and entities" This reverts commit a43e39194f25d39e69426b15a2b9036022f301d3. --- packages/core-data/src/private-selectors.ts | 256 -------------------- 1 file changed, 256 deletions(-) diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index db674b40efa4c..841f4ee2ef460 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -11,11 +11,6 @@ import { STORE_NAME } from './name'; type EntityRecordKey = string | number; -type TemplateQuery = { - slug: string; - is_custom?: boolean; -}; - /** * Returns the previous edit from the current undo offset * for the entity records edits history, if any. @@ -98,254 +93,3 @@ 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 ] ?? {}; -} -export const getRelatedEditedEntityRecordsByTemplate = createRegistrySelector( - ( select ) => - createSelector( - ( state: State, template: TemplateQuery ) => { - /* - * Get the relationship between the template slug and the entity records. - * Similar to how the Template Hierarchy works: https://developer.wordpress.org/themes/basics/template-hierarchy/ - * - * It returns the specific entity record if it is specified and if not it returns the root entity record. - * - * These are the possible slugs and the related entities. - * - * ARCHIVES - * - * archive: Any taxonomy, author, and date. - * author: Author archives. - * author-{user-slug}: Specific author. - * category: Taxonomy "category" archives. - * category-{category-slug}: Specific category. - * date: Post archive for a specific date. - * tag: Taxonomy "post_tag" archives. - * tag-{tag-slug}: Specific tag. - * taxonomy-{tax-slug}: Specific taxonomy archives. - * taxonomy-{tax-slug}-{item-slug}: Specific item of a specific taxonomy. - * - * POST TYPES - * - * index: Any post type. - * page: Post type "page". - * page-{page-slug}: Specific "page". - * single: Post type "post". - * single-post: Post type "post". - * single-post-{post-slug}: Specific "post". - * single-{cpt-slug}: Specific post type. - * single-{cpt-slug}-{item-slug}: Specific item of a specific post type. - * - * SPECIAL CASES - * - * home: Latest posts as either the site homepage or as the "Posts page". - * front-page: Homepage whether it is set to display latest posts or a static page. Overrides `home`. - * 404: Displays when a visitor views a non-existent page. - * search: Displays when a visitor performs a search on the website. - * - * Custom templates apply to posts, pages, or custom post types. - */ - - // TODO: Review archive-{post-type} and attachment. - // TODO: Await somehow for getEntityRecords calls. - - const { slug, is_custom: isCustom } = template; - - // Custom templates. - if ( isCustom ) { - // Return all post types. - return select( STORE_NAME ).getEntityRecords( - 'root', - 'postType' - ); - } - - // Homepage templates. - if ( slug === 'home' || slug === 'front-page' ) { - // TODO: Review how to get the page on front. - const { page_on_front: pageOnFront } = select( - STORE_NAME - ).getEntityRecord( 'root', 'site' ); - - if ( pageOnFront === 0 ) { - // Homepage displays latest posts. - const postsEntity = select( - STORE_NAME - ).getEntityRecord( 'root', 'postType', 'posts' ); - return postsEntity ? [ postsEntity ] : undefined; - } - // Homepage displays a static page. - const pageEntity = select( STORE_NAME ).getEntityRecord( - 'postType', - 'page', - pageOnFront - ); - return pageEntity ? [ pageEntity ] : undefined; - } - - // Special cases. - // TODO: Review what to return in these cases. - if ( slug === 'date' || slug === '404' || slug === 'search' ) { - return; - } - - // First item corresponds to the type. - // eslint-disable-next-line @wordpress/no-unused-vars-before-return - const [ type, ...slugParts ] = slug.split( '-' ); - - // Author archives. - // TODO: Review what to return in these cases. - if ( type === 'author' ) { - return; - } - - // Build the query. - let kind, entitySlug, itemSlug; - - // Get the `kind`. - switch ( type ) { - case 'archive': - case 'taxonomy': - case 'category': - case 'tag': - kind = 'taxonomy'; - break; - - case 'index': - case 'single': - case 'page': - kind = 'postType'; - break; - } - - // Generate the `entitySlug` and `itemSlug`. - switch ( type ) { - case 'category': - entitySlug = 'category'; - if ( slugParts.length ) { - itemSlug = slugParts.join( '-' ); - } - break; - - case 'tag': - entitySlug = 'post_tag'; - if ( slugParts.length ) { - itemSlug = slugParts.join( '-' ); - } - break; - - case 'page': - entitySlug = 'page'; - if ( slugParts.length ) { - itemSlug = slugParts.join( '-' ); - } - break; - - case 'taxonomy': - case 'single': - /* - * Extract entitySlug and itemSlug from the slugParts. - * Slugs can contain dashes. - * - * taxonomy-{tax-slug} - * taxonomy-{tax-slug}-{item-slug} - * single - * single-{cpt-slug} - * single-{cpt-slug}-{item-slug} - */ - if ( ! slugParts.length ) { - if ( type === 'single' ) { - entitySlug = 'post'; - } - break; - } - let firstSlug = ''; - for ( let i = 0; i < slugParts.length; i++ ) { - if ( firstSlug === '' ) { - firstSlug = slugParts[ i ]; - } else { - firstSlug += `-${ slugParts[ i ] }`; - } - - // Check if the current combination is an existing taxonomy or post type. - // TODO: Check better way to get defined taxonomies or post types. - const existingPostTypes = Object.keys( - state.entities.records.postType - ); - const existingTaxonomies = Object.keys( - state.entities.records.taxonomy - ); - if ( - existingTaxonomies.includes( firstSlug ) || - existingPostTypes.includes( firstSlug ) - ) { - entitySlug = firstSlug; - const remainingParts = slugParts.slice( i + 1 ); - if ( remainingParts.length ) { - itemSlug = remainingParts.join( '-' ); - } - break; - } - } - break; - } - - if ( ! entitySlug ) { - /* - * archive - * index - */ - return select( STORE_NAME ).getEntityRecords( - 'root', - kind - ); - } - - if ( ! itemSlug ) { - /* - * category - * tag - * taxonomy-{tax-slug} - * page - * single - * single-{cpt-slug} - */ - - // It seems it is not possible to filter by slug in `getEntityRecords`. - const rootEntity = select( STORE_NAME ).getEntityRecord( - 'root', - kind, - entitySlug - ); - return rootEntity ? [ rootEntity ] : undefined; - } - - /* - * category-{category-slug} - * tag-{tag-slug} - * page-{page-slug} - * taxonomy-{tax-slug}-{item-slug} - * single-{cpt-slug}-{item-slug} - */ - return select( STORE_NAME ).getEntityRecords( - kind, - entitySlug, - { - slug: itemSlug, - } - ); - }, - // TODO: Review what to include here. - ( state ) => [ state.entities.records ] - ) -); From f8f18fc6c205fd4fb6d21da8e284f0593cad10c8 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:06:57 +0200 Subject: [PATCH 07/42] Only expose public fields --- lib/compat/wordpress-6.7/rest-api.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index f0ef56c40a1e5..e296c0f3ff883 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -125,9 +125,19 @@ function gutenberg_register_wp_rest_post_types_meta_fields() { 'meta', array( 'get_callback' => function ( $item ) { - $default_fields = get_registered_meta_keys( 'post' ); + $public_fields = array(); + $global_fields = get_registered_meta_keys( 'post' ); $post_type_fields = get_registered_meta_keys( 'post', $item['slug'] ); - return array_merge( $default_fields, $post_type_fields ); + foreach ( array_merge( $global_fields, $post_type_fields ) as $key => $properties ) { + // Only expose fields with `show_in_rest` set to true. + if ( $properties['show_in_rest'] ) { + $public_fields[ $key ] = array( + 'default' => $properties['default'] ?? '', + 'description' => $properties['description'], + ); + } + } + return $public_fields; }, 'schema' => array( 'type' => 'array', From 82006cf8c1138c96918c43b2f27363eefd15ad75 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:06:57 +0200 Subject: [PATCH 08/42] Add subtype to meta properties --- lib/compat/wordpress-6.7/rest-api.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index e296c0f3ff883..8c7263ffae548 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -134,6 +134,8 @@ function gutenberg_register_wp_rest_post_types_meta_fields() { $public_fields[ $key ] = array( 'default' => $properties['default'] ?? '', 'description' => $properties['description'], + // Add property to indicate if it is specific to this post type. + 'subtype' => array_key_exists( $key, $post_type_fields ) ? $item['slug'] : null, ); } } From ae6037ed942f90abbe971fb82ecd572715af8473 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:07:11 +0200 Subject: [PATCH 09/42] Render the appropriate fields depending on the postType in templates --- packages/editor/src/bindings/post-meta.js | 70 ++++++++++++++++++----- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index ebe63fcdea1f3..24e52b675efe3 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -103,25 +103,65 @@ export default { return true; }, getFieldsList( { registry, context } ) { - const metaFields = getMetadata( registry, context ); + let metaFields = {}; + const { + type, + is_custom: isCustom, + slug, + } = registry.select( editorStore ).getCurrentPost(); + const { getPostTypes, getEntityRecord, getEditedEntityRecord } = + registry.select( coreDataStore ); - // TODO: Fields returns undefined on the first click. - const fields = registry - .select( coreDataStore ) - // TODO: Last item 'post' should not be hardcoded. - .getEntityRecord( 'root', 'postType', 'post' ); + // If it is a template, use the default values. + if ( type === 'wp_template' ) { + let postType; + let isGlobalTemplate = false; + // Get the 'kind' from the start of the slug. + const [ kind ] = slug.split( '-' ); + if ( isCustom || slug === 'index' ) { + isGlobalTemplate = true; + // Use 'post' as the default. + postType = 'post'; + } else if ( kind === 'page' ) { + postType = 'page'; + } else if ( kind === 'single' ) { + const postTypes = + getPostTypes( { per_page: -1 } )?.map( + ( entity ) => entity.slug + ) || []; - if ( ! metaFields || ! Object.keys( metaFields ).length ) { - if ( ! fields?.meta ) { - return null; + // Infer the post type from the slug. + const match = slug.match( + `^single-(${ postTypes.join( '|' ) })(?:-.+)?$` + ); + postType = match ? match[ 1 ] : 'post'; } - const metaDefaults = {}; - for ( const key in fields.meta ) { - if ( fields.meta.hasOwnProperty( key ) ) { - metaDefaults[ key ] = fields.meta[ key ].default; + + // TODO: Fields returns undefined on the first click. + const fields = getEntityRecord( + 'root', + 'postType', + postType + )?.meta; + + // 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. + if ( isGlobalTemplate && props.subtype ) { + return; } - } - return metaDefaults; + 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. From faa713f48895015958632cee688312aa4e0993d6 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:07:11 +0200 Subject: [PATCH 10/42] Use context postType when available --- packages/editor/src/bindings/post-meta.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 24e52b675efe3..cf329133f264e 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -113,7 +113,7 @@ export default { registry.select( coreDataStore ); // If it is a template, use the default values. - if ( type === 'wp_template' ) { + if ( ! context?.postType && type === 'wp_template' ) { let postType; let isGlobalTemplate = false; // Get the 'kind' from the start of the slug. From 8706071dc6a309e67414af565478b65ed0abbfb4 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:07:11 +0200 Subject: [PATCH 11/42] Fetch the data on render, preventing one click needed --- lib/compat/wordpress-6.7/rest-api.php | 4 ++-- packages/editor/src/bindings/post-meta.js | 29 +++++++++++++++-------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index 8c7263ffae548..cf385767ef3ea 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -129,8 +129,8 @@ function gutenberg_register_wp_rest_post_types_meta_fields() { $global_fields = get_registered_meta_keys( 'post' ); $post_type_fields = get_registered_meta_keys( 'post', $item['slug'] ); foreach ( array_merge( $global_fields, $post_type_fields ) as $key => $properties ) { - // Only expose fields with `show_in_rest` set to true. - if ( $properties['show_in_rest'] ) { + // Only expose fields with `show_in_rest` set to true. Not protected meta. Not footnotes. + if ( $properties['show_in_rest'] && ! is_protected_meta( $key ) && $key !== 'footnotes' ) { $public_fields[ $key ] = array( 'default' => $properties['default'] ?? '', 'description' => $properties['description'], diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index cf329133f264e..1fd04362acda1 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { store as coreDataStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -102,19 +103,33 @@ export default { return true; }, - getFieldsList( { registry, context } ) { + getFieldsList: function GetFieldsList( { registry, context } ) { let metaFields = {}; const { type, is_custom: isCustom, slug, } = registry.select( editorStore ).getCurrentPost(); - const { getPostTypes, getEntityRecord, getEditedEntityRecord } = + const { getPostTypes, getEditedEntityRecord } = registry.select( coreDataStore ); + let postType = context?.postType; + + // useSelect prevents needing a blockBindingsPanel render to fetch the data. + const fields = useSelect( + ( select ) => { + const entityRecord = select( coreDataStore ).getEntityRecord( + 'root', + 'postType', + postType + ); + return entityRecord?.meta; + }, + [ postType ] + ); + // If it is a template, use the default values. if ( ! context?.postType && type === 'wp_template' ) { - let postType; let isGlobalTemplate = false; // Get the 'kind' from the start of the slug. const [ kind ] = slug.split( '-' ); @@ -131,19 +146,13 @@ export default { ) || []; // 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'; } - // TODO: Fields returns undefined on the first click. - const fields = getEntityRecord( - 'root', - 'postType', - postType - )?.meta; - // 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. From 75936290edbcee87cd3906ea790152e22da459fa Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:07:11 +0200 Subject: [PATCH 12/42] Yoda conditions.. --- lib/compat/wordpress-6.7/rest-api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index cf385767ef3ea..67e95493620a1 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -130,7 +130,7 @@ function gutenberg_register_wp_rest_post_types_meta_fields() { $post_type_fields = get_registered_meta_keys( 'post', $item['slug'] ); foreach ( array_merge( $global_fields, $post_type_fields ) as $key => $properties ) { // Only expose fields with `show_in_rest` set to true. Not protected meta. Not footnotes. - if ( $properties['show_in_rest'] && ! is_protected_meta( $key ) && $key !== 'footnotes' ) { + if ( $properties['show_in_rest'] && ! is_protected_meta( $key ) && 'footnotes' !== $key ) { $public_fields[ $key ] = array( 'default' => $properties['default'] ?? '', 'description' => $properties['description'], From 6758ec10ca949f35eaea82417689e1e21841a3f7 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:07:24 +0200 Subject: [PATCH 13/42] Try: Expose registered meta fields in schema --- lib/compat/wordpress-6.7/rest-api.php | 38 ----- .../block-editor/src/hooks/block-bindings.js | 112 ++++++++----- packages/core-data/src/entities.js | 155 ++++++++++-------- packages/editor/src/bindings/post-meta.js | 64 +++----- 4 files changed, 186 insertions(+), 183 deletions(-) diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index 67e95493620a1..c5e2927198da0 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -114,41 +114,3 @@ function gutenberg_override_default_rest_server() { return 'Gutenberg_REST_Server'; } add_filter( 'wp_rest_server_class', 'gutenberg_override_default_rest_server', 1 ); - -if ( ! function_exists( 'gutenberg_register_wp_rest_post_types_meta_fields' ) ) { - /** - * Adds `template` and `template_lock` fields to WP_REST_Post_Types_Controller class. - */ - function gutenberg_register_wp_rest_post_types_meta_fields() { - register_rest_field( - 'type', - 'meta', - array( - 'get_callback' => function ( $item ) { - $public_fields = array(); - $global_fields = get_registered_meta_keys( 'post' ); - $post_type_fields = get_registered_meta_keys( 'post', $item['slug'] ); - foreach ( array_merge( $global_fields, $post_type_fields ) as $key => $properties ) { - // Only expose fields with `show_in_rest` set to true. Not protected meta. Not footnotes. - if ( $properties['show_in_rest'] && ! is_protected_meta( $key ) && 'footnotes' !== $key ) { - $public_fields[ $key ] = array( - 'default' => $properties['default'] ?? '', - 'description' => $properties['description'], - // Add property to indicate if it is specific to this post type. - 'subtype' => array_key_exists( $key, $post_type_fields ) ? $item['slug'] : null, - ); - } - } - return $public_fields; - }, - 'schema' => array( - 'type' => 'array', - 'description' => __( 'Meta Keys', 'gutenberg' ), - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - ) - ); - } -} -add_action( 'rest_api_init', 'gutenberg_register_wp_rest_post_types_meta_fields' ); diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js index a0bd8820d36c5..13768b6445f02 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -188,50 +188,40 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { const bindableAttributes = getBindableAttributes( blockName ); const dropdownMenuProps = useToolsPanelDropdownMenuProps(); - // `useSelect` is used purposely here to ensure `getFieldsList` - // is updated whenever there are updates in block context. + // 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 = {}; - 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 }; + 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 }; + } } - ); - return { - fieldsList: - Object.values( _fieldsList ).length > 0 - ? _fieldsList - : EMPTY_OBJECT, - canUpdateBlockBindings: - select( blockEditorStore ).getSettings() - .canUpdateBlockBindings, - }; - }, - [ blockContext, bindableAttributes, registry ] - ); + } + ); + return { fieldsList: _fieldsList }; + }, [ blockContext, bindableAttributes, registry ] ); // Return early if there are no bindable attributes. if ( ! bindableAttributes || bindableAttributes.length === 0 ) { return null; @@ -254,7 +244,49 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { } } ); + const { canUpdateBlockBindings } = useSelect( ( select ) => { + return { + canUpdateBlockBindings: + select( blockEditorStore ).getSettings().canUpdateBlockBindings, + }; + }, [] ); + + if ( ! bindableAttributes || bindableAttributes.length === 0 ) { + return null; + } + + 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. + // Lock the UI when the experiment is not enabled or there are no fields to connect to. const readOnly = ! canUpdateBlockBindings || ! Object.keys( fieldsList ).length; diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index 05f7f55ef759b..9603ea5ce8ef4 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -293,76 +293,99 @@ async function loadPostTypeEntities() { const postTypes = await apiFetch( { path: '/wp/v2/types?context=view', } ); - return Object.entries( postTypes ?? {} ).map( ( [ name, postType ] ) => { - const isTemplate = [ 'wp_template', 'wp_template_part' ].includes( - name - ); - const namespace = postType?.rest_namespace ?? 'wp/v2'; - return { - kind: 'postType', - baseURL: `/${ namespace }/${ postType.rest_base }`, - baseURLParams: { context: 'edit' }, - name, - label: postType.name, - meta: postType.meta, - transientEdits: { - blocks: true, - selection: true, - }, - mergedEdits: { meta: true }, - rawAttributes: POST_RAW_ATTRIBUTES, - getTitle: ( record ) => - record?.title?.rendered || - record?.title || - ( isTemplate - ? capitalCase( record.slug ?? '' ) - : String( record.id ) ), - __unstablePrePersist: isTemplate ? undefined : prePersistPostType, - __unstable_rest_base: postType.rest_base, - syncConfig: { - fetch: async ( id ) => { - return apiFetch( { - path: `/${ namespace }/${ postType.rest_base }/${ id }?context=edit`, - } ); + const entities = Object.entries( postTypes ?? {} ).map( + async ( [ name, postType ] ) => { + const isTemplate = [ 'wp_template', 'wp_template_part' ].includes( + name + ); + const namespace = postType?.rest_namespace ?? 'wp/v2'; + // If meta is not present, fetch it. + const registeredMeta = + postType.meta || + ( + await apiFetch( { + path: '/wp/v2/' + postType?.rest_base + '?context=edit', + method: 'OPTIONS', + } ) + )?.schema?.properties?.meta?.properties; + return { + kind: 'postType', + baseURL: `/${ namespace }/${ postType.rest_base }`, + baseURLParams: { context: 'edit' }, + name, + label: postType.name, + meta: registeredMeta, + transientEdits: { + blocks: true, + selection: true, }, - applyChangesToDoc: ( doc, changes ) => { - const document = doc.getMap( 'document' ); - - Object.entries( changes ).forEach( ( [ key, value ] ) => { - if ( typeof value !== 'function' ) { - if ( key === 'blocks' ) { - if ( ! serialisableBlocksCache.has( value ) ) { - serialisableBlocksCache.set( - value, - makeBlocksSerializable( value ) - ); + mergedEdits: { meta: true }, + rawAttributes: POST_RAW_ATTRIBUTES, + getTitle: ( record ) => + record?.title?.rendered || + record?.title || + ( isTemplate + ? capitalCase( record.slug ?? '' ) + : String( record.id ) ), + __unstablePrePersist: isTemplate + ? undefined + : prePersistPostType, + __unstable_rest_base: postType.rest_base, + syncConfig: { + fetch: async ( id ) => { + return apiFetch( { + path: `/${ namespace }/${ postType.rest_base }/${ id }?context=edit`, + } ); + }, + applyChangesToDoc: ( doc, changes ) => { + const document = doc.getMap( 'document' ); + + Object.entries( changes ).forEach( + ( [ key, value ] ) => { + if ( typeof value !== 'function' ) { + if ( key === 'blocks' ) { + if ( + ! serialisableBlocksCache.has( + value + ) + ) { + serialisableBlocksCache.set( + value, + makeBlocksSerializable( value ) + ); + } + + value = + serialisableBlocksCache.get( + value + ); + } + + if ( document.get( key ) !== value ) { + document.set( key, value ); + } } - - value = serialisableBlocksCache.get( value ); - } - - if ( document.get( key ) !== value ) { - document.set( key, value ); } - } - } ); + ); + }, + fromCRDTDoc: ( doc ) => { + return doc.getMap( 'document' ).toJSON(); + }, }, - fromCRDTDoc: ( doc ) => { - return doc.getMap( 'document' ).toJSON(); - }, - }, - syncObjectType: 'postType/' + postType.name, - getSyncObjectId: ( id ) => id, - supportsPagination: true, - getRevisionsUrl: ( parentId, revisionId ) => - `/${ namespace }/${ - postType.rest_base - }/${ parentId }/revisions${ - revisionId ? '/' + revisionId : '' - }`, - revisionKey: isTemplate ? 'wp_id' : DEFAULT_ENTITY_KEY, - }; - } ); + syncObjectType: 'postType/' + postType.name, + getSyncObjectId: ( id ) => id, + supportsPagination: true, + getRevisionsUrl: ( parentId, revisionId ) => + `/${ namespace }/${ + postType.rest_base + }/${ parentId }/revisions${ + revisionId ? '/' + revisionId : '' + }`, + revisionKey: isTemplate ? 'wp_id' : DEFAULT_ENTITY_KEY, + }; + } + ); + return await Promise.all( entities ); } /** diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 1fd04362acda1..7423ead3abd39 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -2,7 +2,6 @@ * WordPress dependencies */ import { store as coreDataStore } from '@wordpress/core-data'; -import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -103,59 +102,46 @@ export default { return true; }, - getFieldsList: function GetFieldsList( { registry, context } ) { + getFieldsList( { registry, context } ) { let metaFields = {}; const { type, is_custom: isCustom, slug, } = registry.select( editorStore ).getCurrentPost(); - const { getPostTypes, getEditedEntityRecord } = + const { getEntityConfig, getPostTypes, getEditedEntityRecord } = registry.select( coreDataStore ); - let postType = context?.postType; - - // useSelect prevents needing a blockBindingsPanel render to fetch the data. - const fields = useSelect( - ( select ) => { - const entityRecord = select( coreDataStore ).getEntityRecord( - 'root', - 'postType', - postType - ); - return entityRecord?.meta; - }, - [ postType ] - ); - - // If it is a template, use the default values. + // Inherit the postType from the slug if it is a template. if ( ! context?.postType && type === 'wp_template' ) { - let isGlobalTemplate = false; // Get the 'kind' from the start of the slug. - const [ kind ] = slug.split( '-' ); - if ( isCustom || slug === 'index' ) { - isGlobalTemplate = true; - // Use 'post' as the default. - postType = 'post'; - } else 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'; + // 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 = getEntityConfig( 'postType', postType )?.meta; // 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; } From 35127e0ca22503e5025be61f08c55b252bf75c19 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:07:44 +0200 Subject: [PATCH 14/42] Try: Create a resolver to get registered post meta --- docs/reference-guides/data/data-core.md | 26 ++++ packages/core-data/README.md | 26 ++++ packages/core-data/src/actions.js | 17 +++ packages/core-data/src/entities.js | 155 +++++++++------------- packages/core-data/src/resolvers.js | 9 +- packages/core-data/src/selectors.ts | 12 ++ packages/editor/src/bindings/post-meta.js | 4 +- 7 files changed, 152 insertions(+), 97 deletions(-) diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index 474207aa20460..f5f1544bc8c18 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -521,6 +521,19 @@ _Returns_ - A value whose reference will change only when an edit occurs. +### getRegisteredPostMeta + +Returns the registered post meta fields for a given post type. + +_Parameters_ + +- _state_ `State`: Data state. +- _postType_ `string`: Post type. + +_Returns_ + +- Registered post meta fields. + ### getRevision Returns a single, specific revision of a parent entity. @@ -838,6 +851,19 @@ _Returns_ - `Object`: Action object. +### receiveRegisteredPostMeta + +Returns an action object used in signalling that the registered post meta fields for a post type have been received. + +_Parameters_ + +- _postType_ `string`: Post type slug. +- _registeredPostMeta_ `Object`: Registered post meta. + +_Returns_ + +- `Object`: Action object. + ### receiveRevisions Action triggered to receive revision items. diff --git a/packages/core-data/README.md b/packages/core-data/README.md index 079f95ddbfc7a..b27bae0832f81 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -248,6 +248,19 @@ _Returns_ - `Object`: Action object. +### receiveRegisteredPostMeta + +Returns an action object used in signalling that the registered post meta fields for a post type have been received. + +_Parameters_ + +- _postType_ `string`: Post type slug. +- _registeredPostMeta_ `Object`: Registered post meta. + +_Returns_ + +- `Object`: Action object. + ### receiveRevisions Action triggered to receive revision items. @@ -743,6 +756,19 @@ _Returns_ - A value whose reference will change only when an edit occurs. +### getRegisteredPostMeta + +Returns the registered post meta fields for a given post type. + +_Parameters_ + +- _state_ `State`: Data state. +- _postType_ `string`: Post type. + +_Returns_ + +- Registered post meta fields. + ### getRevision Returns a single, specific revision of a parent entity. diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index e83ad02828cfe..11f2b152f0fd9 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -995,3 +995,20 @@ export const receiveRevisions = invalidateCache, } ); }; + +/** + * 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, + }; +} diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index 9603ea5ce8ef4..05f7f55ef759b 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -293,99 +293,76 @@ async function loadPostTypeEntities() { const postTypes = await apiFetch( { path: '/wp/v2/types?context=view', } ); - const entities = Object.entries( postTypes ?? {} ).map( - async ( [ name, postType ] ) => { - const isTemplate = [ 'wp_template', 'wp_template_part' ].includes( - name - ); - const namespace = postType?.rest_namespace ?? 'wp/v2'; - // If meta is not present, fetch it. - const registeredMeta = - postType.meta || - ( - await apiFetch( { - path: '/wp/v2/' + postType?.rest_base + '?context=edit', - method: 'OPTIONS', - } ) - )?.schema?.properties?.meta?.properties; - return { - kind: 'postType', - baseURL: `/${ namespace }/${ postType.rest_base }`, - baseURLParams: { context: 'edit' }, - name, - label: postType.name, - meta: registeredMeta, - transientEdits: { - blocks: true, - selection: true, + return Object.entries( postTypes ?? {} ).map( ( [ name, postType ] ) => { + const isTemplate = [ 'wp_template', 'wp_template_part' ].includes( + name + ); + const namespace = postType?.rest_namespace ?? 'wp/v2'; + return { + kind: 'postType', + baseURL: `/${ namespace }/${ postType.rest_base }`, + baseURLParams: { context: 'edit' }, + name, + label: postType.name, + meta: postType.meta, + transientEdits: { + blocks: true, + selection: true, + }, + mergedEdits: { meta: true }, + rawAttributes: POST_RAW_ATTRIBUTES, + getTitle: ( record ) => + record?.title?.rendered || + record?.title || + ( isTemplate + ? capitalCase( record.slug ?? '' ) + : String( record.id ) ), + __unstablePrePersist: isTemplate ? undefined : prePersistPostType, + __unstable_rest_base: postType.rest_base, + syncConfig: { + fetch: async ( id ) => { + return apiFetch( { + path: `/${ namespace }/${ postType.rest_base }/${ id }?context=edit`, + } ); }, - mergedEdits: { meta: true }, - rawAttributes: POST_RAW_ATTRIBUTES, - getTitle: ( record ) => - record?.title?.rendered || - record?.title || - ( isTemplate - ? capitalCase( record.slug ?? '' ) - : String( record.id ) ), - __unstablePrePersist: isTemplate - ? undefined - : prePersistPostType, - __unstable_rest_base: postType.rest_base, - syncConfig: { - fetch: async ( id ) => { - return apiFetch( { - path: `/${ namespace }/${ postType.rest_base }/${ id }?context=edit`, - } ); - }, - applyChangesToDoc: ( doc, changes ) => { - const document = doc.getMap( 'document' ); - - Object.entries( changes ).forEach( - ( [ key, value ] ) => { - if ( typeof value !== 'function' ) { - if ( key === 'blocks' ) { - if ( - ! serialisableBlocksCache.has( - value - ) - ) { - serialisableBlocksCache.set( - value, - makeBlocksSerializable( value ) - ); - } - - value = - serialisableBlocksCache.get( - value - ); - } - - if ( document.get( key ) !== value ) { - document.set( key, value ); - } + applyChangesToDoc: ( doc, changes ) => { + const document = doc.getMap( 'document' ); + + Object.entries( changes ).forEach( ( [ key, value ] ) => { + if ( typeof value !== 'function' ) { + if ( key === 'blocks' ) { + if ( ! serialisableBlocksCache.has( value ) ) { + serialisableBlocksCache.set( + value, + makeBlocksSerializable( value ) + ); } + + value = serialisableBlocksCache.get( value ); + } + + if ( document.get( key ) !== value ) { + document.set( key, value ); } - ); - }, - fromCRDTDoc: ( doc ) => { - return doc.getMap( 'document' ).toJSON(); - }, + } + } ); }, - syncObjectType: 'postType/' + postType.name, - getSyncObjectId: ( id ) => id, - supportsPagination: true, - getRevisionsUrl: ( parentId, revisionId ) => - `/${ namespace }/${ - postType.rest_base - }/${ parentId }/revisions${ - revisionId ? '/' + revisionId : '' - }`, - revisionKey: isTemplate ? 'wp_id' : DEFAULT_ENTITY_KEY, - }; - } - ); - return await Promise.all( entities ); + fromCRDTDoc: ( doc ) => { + return doc.getMap( 'document' ).toJSON(); + }, + }, + syncObjectType: 'postType/' + postType.name, + getSyncObjectId: ( id ) => id, + supportsPagination: true, + getRevisionsUrl: ( parentId, revisionId ) => + `/${ namespace }/${ + postType.rest_base + }/${ parentId }/revisions${ + revisionId ? '/' + revisionId : '' + }`, + revisionKey: isTemplate ? 'wp_id' : DEFAULT_ENTITY_KEY, + }; + } ); } /** diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index ce8c2db7a53b4..6810307f7d8a4 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -992,14 +992,11 @@ export const getRevision = */ export const getRegisteredPostMeta = ( postType ) => - async ( { dispatch, resolveSelect } ) => { + async ( { select, dispatch } ) => { try { - const { - rest_namespace: restNamespace = 'wp/v2', - rest_base: restBase, - } = ( await resolveSelect.getPostType( postType ) ) || {}; + const restBase = select.getPostType( postType )?.rest_base; const options = await apiFetch( { - path: `${ restNamespace }/${ restBase }/?context=edit`, + path: `wp/v2/${ restBase }/?context=edit`, method: 'OPTIONS', } ); dispatch.receiveRegisteredPostMeta( diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index ba22723f951f4..45726a053ac71 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -1527,3 +1527,15 @@ export const getRevision = createSelector( ]; } ); + +/** + * 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 ] ?? {}; +} diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 7423ead3abd39..aba30b2bc1199 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -109,7 +109,7 @@ export default { is_custom: isCustom, slug, } = registry.select( editorStore ).getCurrentPost(); - const { getEntityConfig, getPostTypes, getEditedEntityRecord } = + const { getRegisteredPostMeta, getPostTypes, getEditedEntityRecord } = registry.select( coreDataStore ); // Inherit the postType from the slug if it is a template. @@ -136,7 +136,7 @@ export default { postType = match ? match[ 1 ] : 'post'; } } - const fields = getEntityConfig( 'postType', postType )?.meta; + const fields = getRegisteredPostMeta( postType ); // Populate the `metaFields` object with the default values. Object.entries( fields || {} ).forEach( ( [ key, props ] ) => { From ef6c64bd08a380a25d5e06cfa62c5b25fe13fd9b Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:07:44 +0200 Subject: [PATCH 15/42] Use rest namespace --- packages/core-data/src/resolvers.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 6810307f7d8a4..86919fed32578 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -994,9 +994,12 @@ export const getRegisteredPostMeta = ( postType ) => async ( { select, dispatch } ) => { try { - const restBase = select.getPostType( postType )?.rest_base; + const { + rest_namespace: restNamespace = 'wp/v2', + rest_base: restBase, + } = select.getPostType( postType ) || {}; const options = await apiFetch( { - path: `wp/v2/${ restBase }/?context=edit`, + path: `${ restNamespace }/${ restBase }/?context=edit`, method: 'OPTIONS', } ); dispatch.receiveRegisteredPostMeta( From 0330ddfcb6a396fd19c0d4548ebbad60a34294d8 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:08:08 +0200 Subject: [PATCH 16/42] Move actions and selectors to private. --- docs/reference-guides/data/data-core.md | 26 ----------------- packages/core-data/README.md | 26 ----------------- packages/core-data/src/actions.js | 17 ----------- packages/core-data/src/private-selectors.ts | 12 ++++++++ packages/core-data/src/selectors.ts | 12 -------- packages/editor/src/bindings/post-meta.js | 31 ++++----------------- 6 files changed, 17 insertions(+), 107 deletions(-) diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index f5f1544bc8c18..474207aa20460 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -521,19 +521,6 @@ _Returns_ - A value whose reference will change only when an edit occurs. -### getRegisteredPostMeta - -Returns the registered post meta fields for a given post type. - -_Parameters_ - -- _state_ `State`: Data state. -- _postType_ `string`: Post type. - -_Returns_ - -- Registered post meta fields. - ### getRevision Returns a single, specific revision of a parent entity. @@ -851,19 +838,6 @@ _Returns_ - `Object`: Action object. -### receiveRegisteredPostMeta - -Returns an action object used in signalling that the registered post meta fields for a post type have been received. - -_Parameters_ - -- _postType_ `string`: Post type slug. -- _registeredPostMeta_ `Object`: Registered post meta. - -_Returns_ - -- `Object`: Action object. - ### receiveRevisions Action triggered to receive revision items. diff --git a/packages/core-data/README.md b/packages/core-data/README.md index b27bae0832f81..079f95ddbfc7a 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -248,19 +248,6 @@ _Returns_ - `Object`: Action object. -### receiveRegisteredPostMeta - -Returns an action object used in signalling that the registered post meta fields for a post type have been received. - -_Parameters_ - -- _postType_ `string`: Post type slug. -- _registeredPostMeta_ `Object`: Registered post meta. - -_Returns_ - -- `Object`: Action object. - ### receiveRevisions Action triggered to receive revision items. @@ -756,19 +743,6 @@ _Returns_ - A value whose reference will change only when an edit occurs. -### getRegisteredPostMeta - -Returns the registered post meta fields for a given post type. - -_Parameters_ - -- _state_ `State`: Data state. -- _postType_ `string`: Post type. - -_Returns_ - -- Registered post meta fields. - ### getRevision Returns a single, specific revision of a parent entity. diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 11f2b152f0fd9..e83ad02828cfe 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -995,20 +995,3 @@ export const receiveRevisions = invalidateCache, } ); }; - -/** - * 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, - }; -} diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index 841f4ee2ef460..b2f6fa7def985 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -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 ] ?? {}; +} diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 45726a053ac71..ba22723f951f4 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -1527,15 +1527,3 @@ export const getRevision = createSelector( ]; } ); - -/** - * 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 ] ?? {}; -} diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index aba30b2bc1199..b46121ab55dcc 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -9,31 +9,6 @@ import { store as coreDataStore } from '@wordpress/core-data'; 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 } ) { @@ -109,9 +84,13 @@ export default { is_custom: isCustom, slug, } = registry.select( editorStore ).getCurrentPost(); - const { getRegisteredPostMeta, getPostTypes, getEditedEntityRecord } = + 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. From 53ac96da5dfd1bcea9e56f4e85f61cd7fdcb25f2 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:08:08 +0200 Subject: [PATCH 17/42] Merge useSelect --- .../block-editor/src/hooks/block-bindings.js | 75 +++++++++---------- packages/core-data/src/entities.js | 1 - 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js index 13768b6445f02..94e6649e51a55 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -188,40 +188,47 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { const bindableAttributes = getBindableAttributes( blockName ); const dropdownMenuProps = useToolsPanelDropdownMenuProps(); - // While this hook doesn't directly call any selectors, `useSelect` is - // used purposely here to ensure `getFieldsList` is updated whenever - // there are attribute updates. + // `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 { fieldsList, canUpdateBlockBindings } = useSelect( + ( select ) => { + 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 }; } } - } - ); - return { fieldsList: _fieldsList }; - }, [ blockContext, bindableAttributes, registry ] ); + ); + return { + fieldsList: _fieldsList, + canUpdateBlockBindings: + select( blockEditorStore ).getSettings() + .canUpdateBlockBindings, + }; + }, + [ blockContext, bindableAttributes, registry ] + ); // Return early if there are no bindable attributes. if ( ! bindableAttributes || bindableAttributes.length === 0 ) { return null; @@ -244,13 +251,6 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { } } ); - const { canUpdateBlockBindings } = useSelect( ( select ) => { - return { - canUpdateBlockBindings: - select( blockEditorStore ).getSettings().canUpdateBlockBindings, - }; - }, [] ); - if ( ! bindableAttributes || bindableAttributes.length === 0 ) { return null; } @@ -286,7 +286,6 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { } ); // 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; diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index 05f7f55ef759b..8d09402087cf9 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -304,7 +304,6 @@ async function loadPostTypeEntities() { baseURLParams: { context: 'edit' }, name, label: postType.name, - meta: postType.meta, transientEdits: { blocks: true, selection: true, From c41877b9d27fff27029d20b41281a1c14dc7843e Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:08:08 +0200 Subject: [PATCH 18/42] Fix duplicated --- .../block-editor/src/hooks/block-bindings.js | 34 ------------------- packages/core-data/src/resolvers.js | 4 +-- 2 files changed, 2 insertions(+), 36 deletions(-) diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js index 94e6649e51a55..299cae89a7383 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -251,40 +251,6 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { } } ); - if ( ! bindableAttributes || bindableAttributes.length === 0 ) { - return null; - } - - 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; diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 86919fed32578..ce8c2db7a53b4 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -992,12 +992,12 @@ export const getRevision = */ export const getRegisteredPostMeta = ( postType ) => - async ( { select, dispatch } ) => { + async ( { dispatch, resolveSelect } ) => { try { const { rest_namespace: restNamespace = 'wp/v2', rest_base: restBase, - } = select.getPostType( postType ) || {}; + } = ( await resolveSelect.getPostType( postType ) ) || {}; const options = await apiFetch( { path: `${ restNamespace }/${ restBase }/?context=edit`, method: 'OPTIONS', From bf7ab98d0a8bd62989a691bb1cad220b23f60d08 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:08 +0200 Subject: [PATCH 19/42] Add object_subtype to schema --- lib/compat/wordpress-6.7/rest-api.php | 32 +++++++++++++++++++++++ packages/editor/src/bindings/post-meta.js | 3 +-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index c5e2927198da0..d1a2abe08f8db 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -114,3 +114,35 @@ function gutenberg_override_default_rest_server() { return 'Gutenberg_REST_Server'; } add_filter( 'wp_rest_server_class', 'gutenberg_override_default_rest_server', 1 ); + +/** + * Add `object_subtype` to schema. + * + * @param array $args Array of arguments for registering meta. + * @return array Modified arguments array including `label`. + */ +function gutenberg_add_object_subtype_to_schema( $args ) { + // Don't update schema when label isn't provided. + if ( ! isset( $args['object_subtype'] ) ) { + return $args; + } + + $schema = array( 'object_subtype' => $args['object_subtype'] ); + if ( ! is_array( $args['show_in_rest'] ) ) { + $args['show_in_rest'] = array( + 'schema' => $schema, + ); + return $args; + } + + if ( ! empty( $args['show_in_rest']['schema'] ) ) { + $args['show_in_rest']['schema'] = array_merge( $args['show_in_rest']['schema'], $schema ); + } else { + $args['show_in_rest']['schema'] = $schema; + } + + return $args; +} + +// Priority must be lower than 10 to ensure the label is not removed. +add_filter( 'register_meta_args', 'gutenberg_add_object_subtype_to_schema', 5, 1 ); diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index b46121ab55dcc..0c07db3c2baa0 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -120,8 +120,7 @@ export default { // 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 ) { + if ( isGlobalTemplate && props.object_subtype ) { return; } metaFields[ key ] = props.default; From da336befd11f14dc31b1ebb912f8b17dde4a1cfa Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:08:08 +0200 Subject: [PATCH 20/42] Update docs to object_subtype --- lib/compat/wordpress-6.7/rest-api.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index d1a2abe08f8db..4317c7db453b1 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -119,10 +119,10 @@ function gutenberg_override_default_rest_server() { * Add `object_subtype` to schema. * * @param array $args Array of arguments for registering meta. - * @return array Modified arguments array including `label`. + * @return array Modified arguments array including `object_subtype`. */ function gutenberg_add_object_subtype_to_schema( $args ) { - // Don't update schema when label isn't provided. + // Don't update schema when object_subtype isn't provided. if ( ! isset( $args['object_subtype'] ) ) { return $args; } @@ -144,5 +144,5 @@ function gutenberg_add_object_subtype_to_schema( $args ) { return $args; } -// Priority must be lower than 10 to ensure the label is not removed. +// Priority must be lower than 10 to ensure the object_subtype is not removed. add_filter( 'register_meta_args', 'gutenberg_add_object_subtype_to_schema', 5, 1 ); From 0734e027c2a1b64cd47369aad92c88daeda113d5 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:08:08 +0200 Subject: [PATCH 21/42] Add explanatory comment --- packages/editor/src/bindings/post-meta.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 0c07db3c2baa0..1fb6af1f73f90 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -96,6 +96,7 @@ export default { // Get the 'kind' from the start of the slug. // Use 'post' as the default. let postType = 'post'; + // A global template can be used with any post type. const isGlobalTemplate = isCustom || slug === 'index'; if ( ! isGlobalTemplate ) { const [ kind ] = slug.split( '-' ); From d76d0a4f07d6beb8e31e6b598cf2d7c66ab4d3de Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 22/42] 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 Co-authored-by: cbravobernal Co-authored-by: gziolo Co-authored-by: mtias --- packages/editor/src/bindings/post-meta.js | 117 +++++++++--------- .../editor/various/block-bindings.spec.js | 8 +- 2 files changed, 65 insertions(+), 60 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 1fb6af1f73f90..87efbe1687cda 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -9,6 +9,65 @@ import { store as coreDataStore } from '@wordpress/core-data'; import { store as editorStore } from '../store'; import { unlock } from '../lock-unlock'; +function getMetadata( registry, context ) { + 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. + if ( isGlobalTemplate && props.object_subtype ) { + return; + } + 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 } ) { @@ -18,7 +77,7 @@ export default { for ( const [ attributeName, source ] of Object.entries( bindings ) ) { // Use the key if the value is not set. newValues[ attributeName ] = - metaFields?.[ source.args.key ] ?? source.args.key; + metaFields?.[ source.args.key ] || source.args.key; } return newValues; }, @@ -78,61 +137,7 @@ export default { return true; }, getFieldsList( { registry, context } ) { - 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'; - // A global template can be used with any post type. - 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. - if ( isGlobalTemplate && props.object_subtype ) { - return; - } - metaFields[ key ] = props.default; - } ); - } else { - metaFields = getEditedEntityRecord( - 'postType', - context?.postType, - context?.postId - ).meta; - } + const metaFields = getMetadata( registry, context ); if ( ! metaFields || ! Object.keys( metaFields ).length ) { return null; diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index c556c469698eb..f21e12e75df20 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -62,7 +62,7 @@ test.describe( 'Block bindings', () => { name: 'Block: Paragraph', } ); await expect( paragraphBlock ).toHaveText( - 'text_custom_field' + 'Value of the text custom field' ); } ); @@ -922,7 +922,7 @@ test.describe( 'Block bindings', () => { .getByRole( 'tabpanel', { name: 'Settings' } ) .getByLabel( 'Alternative text' ) .inputValue(); - expect( altValue ).toBe( 'text_custom_field' ); + expect( altValue ).toBe( 'Value of the text custom field' ); // Title input is enabled and with the original value. await page @@ -1064,7 +1064,7 @@ test.describe( 'Block bindings', () => { .getByRole( 'tabpanel', { name: 'Settings' } ) .getByLabel( 'Title attribute' ) .inputValue(); - expect( titleValue ).toBe( 'text_custom_field' ); + expect( titleValue ).toBe( 'Value of the text custom field' ); } ); test( 'should disable title input when title is bound to an undefined source', async ( { @@ -1183,7 +1183,7 @@ test.describe( 'Block bindings', () => { .getByRole( 'tabpanel', { name: 'Settings' } ) .getByLabel( 'Alternative text' ) .inputValue(); - expect( altValue ).toBe( 'text_custom_field' ); + expect( altValue ).toBe( 'Value of the text custom field' ); // Title input is enabled and with the original value. await page From ca45424d77e06f509723de828ea135f6387f5443 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 23/42] Try removing all object subtype --- lib/compat/wordpress-6.7/rest-api.php | 32 ----------------------- packages/editor/src/bindings/post-meta.js | 8 +----- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index 4317c7db453b1..c5e2927198da0 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -114,35 +114,3 @@ function gutenberg_override_default_rest_server() { return 'Gutenberg_REST_Server'; } add_filter( 'wp_rest_server_class', 'gutenberg_override_default_rest_server', 1 ); - -/** - * Add `object_subtype` to schema. - * - * @param array $args Array of arguments for registering meta. - * @return array Modified arguments array including `object_subtype`. - */ -function gutenberg_add_object_subtype_to_schema( $args ) { - // Don't update schema when object_subtype isn't provided. - if ( ! isset( $args['object_subtype'] ) ) { - return $args; - } - - $schema = array( 'object_subtype' => $args['object_subtype'] ); - if ( ! is_array( $args['show_in_rest'] ) ) { - $args['show_in_rest'] = array( - 'schema' => $schema, - ); - return $args; - } - - if ( ! empty( $args['show_in_rest']['schema'] ) ) { - $args['show_in_rest']['schema'] = array_merge( $args['show_in_rest']['schema'], $schema ); - } else { - $args['show_in_rest']['schema'] = $schema; - } - - return $args; -} - -// Priority must be lower than 10 to ensure the object_subtype is not removed. -add_filter( 'register_meta_args', 'gutenberg_add_object_subtype_to_schema', 5, 1 ); diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 87efbe1687cda..3cd8879de13f0 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -22,7 +22,6 @@ function getMetadata( registry, context ) { 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. @@ -34,11 +33,11 @@ function getMetadata( registry, context ) { if ( kind === 'page' ) { postType = 'page'; } else if ( kind === 'single' ) { + // Get postTypes is returning []. 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( @@ -48,13 +47,8 @@ function getMetadata( registry, context ) { } } 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. - if ( isGlobalTemplate && props.object_subtype ) { - return; - } metaFields[ key ] = props.default; } ); } else { From b911680edc02453a1d6e61dcf69db9bce668815a Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 24/42] Fix e2e --- packages/editor/src/bindings/post-meta.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 3cd8879de13f0..6dc0af8631e02 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -71,7 +71,7 @@ export default { for ( const [ attributeName, source ] of Object.entries( bindings ) ) { // Use the key if the value is not set. newValues[ attributeName ] = - metaFields?.[ source.args.key ] || source.args.key; + metaFields?.[ source.args.key ] ?? source.args.key; } return newValues; }, From 44048d726a97f5403e48ae24247bb21ce7887cbe Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 25/42] Update code --- packages/editor/src/bindings/post-meta.js | 35 +++-------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 6dc0af8631e02..e9b460de9ff19 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -11,42 +11,15 @@ import { unlock } from '../lock-unlock'; function getMetadata( registry, context ) { let metaFields = {}; - const { - type, - is_custom: isCustom, - slug, - } = registry.select( editorStore ).getCurrentPost(); - const { getPostTypes, getEditedEntityRecord } = - registry.select( coreDataStore ); + const { type } = registry.select( editorStore ).getCurrentPost(); + const { 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' ) { - // Get postTypes is returning []. - 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 ); + if ( type === 'wp_template' ) { + const fields = getRegisteredPostMeta( context?.postType || 'post' ); // Populate the `metaFields` object with the default values. Object.entries( fields || {} ).forEach( ( [ key, props ] ) => { metaFields[ key ] = props.default; From 17e0bd6bbc5d597b543ac2ecea185600b969578e Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 26/42] Fix `useSelect` warning --- packages/block-editor/src/hooks/block-bindings.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js index 299cae89a7383..a0bd8820d36c5 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -189,14 +189,14 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { const dropdownMenuProps = useToolsPanelDropdownMenuProps(); // `useSelect` is used purposely here to ensure `getFieldsList` - // is updated whenever there are attribute updates. + // 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 {}; + return EMPTY_OBJECT; } - const _fieldsList = {}; const { getBlockBindingsSources } = unlock( blocksPrivateApis ); const registeredSources = getBlockBindingsSources(); Object.entries( registeredSources ).forEach( @@ -221,7 +221,10 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { } ); return { - fieldsList: _fieldsList, + fieldsList: + Object.values( _fieldsList ).length > 0 + ? _fieldsList + : EMPTY_OBJECT, canUpdateBlockBindings: select( blockEditorStore ).getSettings() .canUpdateBlockBindings, From 1d331036daf16de5e787f9fabdc9c6d075daf062 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 27/42] Remove old comment --- packages/editor/src/bindings/post-meta.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index e9b460de9ff19..f4faebeaa727a 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -13,11 +13,10 @@ function getMetadata( registry, context ) { let metaFields = {}; const { type } = registry.select( editorStore ).getCurrentPost(); const { getEditedEntityRecord } = registry.select( coreDataStore ); - const { getRegisteredPostMeta } = unlock( registry.select( coreDataStore ) ); - // Inherit the postType from the slug if it is a template. + if ( type === 'wp_template' ) { const fields = getRegisteredPostMeta( context?.postType || 'post' ); // Populate the `metaFields` object with the default values. From e17fde7daea5db1292cf65e86689ccd613703b55 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 28/42] Remove support for generic templates --- packages/editor/src/bindings/post-meta.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index f4faebeaa727a..0562c1f7adf07 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -18,7 +18,7 @@ function getMetadata( registry, context ) { ); if ( type === 'wp_template' ) { - const fields = getRegisteredPostMeta( context?.postType || 'post' ); + const fields = getRegisteredPostMeta( context?.postType ); // Populate the `metaFields` object with the default values. Object.entries( fields || {} ).forEach( ( [ key, props ] ) => { metaFields[ key ] = props.default; From 782a1234956b4dba49005d36a1cb36cbc384f759 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 29/42] Revert changes to e2e tests --- test/e2e/specs/editor/various/block-bindings.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index f21e12e75df20..c556c469698eb 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -62,7 +62,7 @@ test.describe( 'Block bindings', () => { name: 'Block: Paragraph', } ); await expect( paragraphBlock ).toHaveText( - 'Value of the text custom field' + 'text_custom_field' ); } ); @@ -922,7 +922,7 @@ test.describe( 'Block bindings', () => { .getByRole( 'tabpanel', { name: 'Settings' } ) .getByLabel( 'Alternative text' ) .inputValue(); - expect( altValue ).toBe( 'Value of the text custom field' ); + expect( altValue ).toBe( 'text_custom_field' ); // Title input is enabled and with the original value. await page @@ -1064,7 +1064,7 @@ test.describe( 'Block bindings', () => { .getByRole( 'tabpanel', { name: 'Settings' } ) .getByLabel( 'Title attribute' ) .inputValue(); - expect( titleValue ).toBe( 'Value of the text custom field' ); + expect( titleValue ).toBe( 'text_custom_field' ); } ); test( 'should disable title input when title is bound to an undefined source', async ( { @@ -1183,7 +1183,7 @@ test.describe( 'Block bindings', () => { .getByRole( 'tabpanel', { name: 'Settings' } ) .getByLabel( 'Alternative text' ) .inputValue(); - expect( altValue ).toBe( 'Value of the text custom field' ); + expect( altValue ).toBe( 'text_custom_field' ); // Title input is enabled and with the original value. await page From 344cbf78fd19b56a03e91425a5b8884401580ef8 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 30/42] Change the value returned by `getFieldsList` to include label --- packages/editor/src/bindings/post-meta.js | 31 ++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 0562c1f7adf07..88087e83213f8 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -38,12 +38,18 @@ export default { name: 'core/post-meta', getValues( { registry, context, bindings } ) { const metaFields = getMetadata( registry, context ); + const { getRegisteredPostMeta } = unlock( + registry.select( coreDataStore ) + ); + const registeredFields = getRegisteredPostMeta( context?.postType ); const newValues = {}; for ( const [ attributeName, source ] of Object.entries( bindings ) ) { // Use the key if the value is not set. newValues[ attributeName ] = - metaFields?.[ source.args.key ] ?? source.args.key; + metaFields?.[ source.args.key ] ?? + registeredFields?.[ source.args.key ]?.label ?? + source.args.key; } return newValues; }, @@ -104,17 +110,30 @@ export default { }, getFieldsList( { registry, context } ) { const metaFields = getMetadata( registry, context ); + const { getRegisteredPostMeta } = unlock( + registry.select( coreDataStore ) + ); + const registeredFields = getRegisteredPostMeta( postType ); 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 ) !== '_' - ) + Object.entries( metaFields ) + // Remove footnotes or private keys from the list of fields. + .filter( + ( [ key ] ) => + key !== 'footnotes' && key.charAt( 0 ) !== '_' + ) + // Return object with label and value. + .map( ( [ key, value ] ) => [ + key, + { + label: registeredFields?.[ key ]?.label || key, + value, + }, + ] ) ); }, }; From 4914c8f0a66d2d12ce0abc6652332f3d99a9bfa5 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 31/42] Use label in bindings panel --- .../block-editor/src/hooks/block-bindings.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js index a0bd8820d36c5..55ac1ea1e603c 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -61,7 +61,7 @@ function BlockBindingsPanelDropdown( { fieldsList, attribute, binding } ) { { registeredSources[ name ].label } ) } - { Object.entries( fields ).map( ( [ key, value ] ) => ( + { Object.entries( fields ).map( ( [ key, args ] ) => ( @@ -77,10 +77,10 @@ function BlockBindingsPanelDropdown( { fieldsList, attribute, binding } ) { checked={ key === currentKey } > - { key } + { args?.label } - { value } + { args?.value } ) ) } @@ -94,7 +94,7 @@ function BlockBindingsPanelDropdown( { fieldsList, attribute, binding } ) { ); } -function BlockBindingsAttribute( { attribute, binding } ) { +function BlockBindingsAttribute( { attribute, binding, fieldsList } ) { const { source: sourceName, args } = binding || {}; const sourceProps = unlock( blocksPrivateApis ).getBlockBindingsSource( sourceName ); @@ -110,14 +110,17 @@ function BlockBindingsAttribute( { attribute, binding } ) { > { isSourceInvalid ? __( 'Invalid source' ) - : args?.key || sourceProps?.label || sourceName } + : fieldsList?.[ sourceName ]?.[ args?.key ]?.label || + args?.key || + sourceProps?.label || + sourceName } ) } ); } -function ReadOnlyBlockBindingsPanelItems( { bindings } ) { +function ReadOnlyBlockBindingsPanelItems( { bindings, fieldsList } ) { return ( <> { Object.entries( bindings ).map( ( [ attribute, binding ] ) => ( @@ -125,6 +128,7 @@ function ReadOnlyBlockBindingsPanelItems( { bindings } ) { ) ) } @@ -164,6 +168,7 @@ function EditableBlockBindingsPanelItems( { } @@ -276,6 +281,7 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { { readOnly ? ( ) : ( Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 32/42] Use label in rich text placeholders --- .../src/components/rich-text/index.js | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 732b8dbf2c089..2f19eff4aaca2 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -125,6 +125,7 @@ export function RichTextWrapper( const { clientId, isSelected: isBlockSelected, name: blockName } = context; const blockBindings = context[ blockBindingsKey ]; const blockContext = useContext( BlockContext ); + const registry = useRegistry(); const selector = ( select ) => { // Avoid subscribing to the block editor store if the block is not // selected. @@ -178,6 +179,10 @@ export function RichTextWrapper( const blockBindingsSource = getBlockBindingsSource( relatedBinding.source ); + const fieldsList = blockBindingsSource?.getFieldsList?.( { + registry, + context: blockContext, + } ); const _disableBoundBlock = ! blockBindingsSource?.canUserEditValue?.( { @@ -186,12 +191,17 @@ export function RichTextWrapper( args: relatedBinding.args, } ); + const bindingKey = + fieldsList?.[ relatedBinding?.args?.key ]?.label ?? + relatedBinding?.args?.key ?? + blockBindingsSource?.label; + const _bindingsPlaceholder = _disableBoundBlock - ? relatedBinding?.args?.key || blockBindingsSource?.label + ? bindingKey : sprintf( /* translators: %s: source label or key */ __( 'Add %s' ), - relatedBinding?.args?.key || blockBindingsSource?.label + bindingKey ); return { @@ -201,7 +211,14 @@ export function RichTextWrapper( _bindingsPlaceholder, }; }, - [ blockBindings, identifier, blockName, blockContext, adjustedValue ] + [ + blockBindings, + identifier, + blockName, + blockContext, + registry, + adjustedValue, + ] ); const shouldDisableEditing = readOnly || disableBoundBlock; @@ -371,7 +388,6 @@ export function RichTextWrapper( element.focus(); } - const registry = useRegistry(); const TagName = tagName; return ( <> From a23dd37f284dd34eadf55175c6b99129eadc28b7 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 33/42] Add filter to include `label` --- lib/compat/wordpress-6.7/block-bindings.php | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/compat/wordpress-6.7/block-bindings.php b/lib/compat/wordpress-6.7/block-bindings.php index 9e82c1843f35a..9ff6083b1c4c8 100644 --- a/lib/compat/wordpress-6.7/block-bindings.php +++ b/lib/compat/wordpress-6.7/block-bindings.php @@ -53,3 +53,31 @@ function gutenberg_add_can_update_block_bindings_editor_setting( $editor_setting } add_filter( 'block_editor_settings_all', 'gutenberg_add_can_update_block_bindings_editor_setting', 10 ); + +/** + * Add `label` to `register_meta`. + * + * @param array $args Array of arguments for registering meta. + * @return array Modified arguments array including `label`. + */ +function gutenberg_update_meta_args_with_label( $args ) { + // Default to empty string. + $schema = array( 'title' => isset( $args['label'] ) ? $args['label'] : '' ); + if ( ! is_array( $args['show_in_rest'] ) ) { + $args['show_in_rest'] = array( + 'schema' => $schema, + ); + return $args; + } + + if ( ! empty( $args['show_in_rest']['schema'] ) ) { + $args['show_in_rest']['schema'] = array_merge( $args['show_in_rest']['schema'], $schema ); + } else { + $args['show_in_rest']['schema'] = $schema; + } + + return $args; +} + +// Priority must be lower than 10 to ensure the label is not removed. +add_filter( 'register_meta_args', 'gutenberg_update_meta_args_with_label', 5, 1 ); From a4436b0f74da5efa78ee0d8facd727a45b0451d0 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 34/42] Use title instead of label in schema --- packages/editor/src/bindings/post-meta.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 88087e83213f8..47cd96512ebaa 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -130,7 +130,7 @@ export default { .map( ( [ key, value ] ) => [ key, { - label: registeredFields?.[ key ]?.label || key, + label: registeredFields?.[ key ]?.title || key, value, }, ] ) From d5eb2203bd404ef41e453a3644fb2fcdbd834375 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 35/42] Add safety check --- lib/compat/wordpress-6.7/block-bindings.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/compat/wordpress-6.7/block-bindings.php b/lib/compat/wordpress-6.7/block-bindings.php index 9ff6083b1c4c8..1aaaf7bd3cb1b 100644 --- a/lib/compat/wordpress-6.7/block-bindings.php +++ b/lib/compat/wordpress-6.7/block-bindings.php @@ -61,8 +61,12 @@ function gutenberg_add_can_update_block_bindings_editor_setting( $editor_setting * @return array Modified arguments array including `label`. */ function gutenberg_update_meta_args_with_label( $args ) { - // Default to empty string. - $schema = array( 'title' => isset( $args['label'] ) ? $args['label'] : '' ); + // Don't update schema when label isn't provided. + if ( ! isset( $args['label'] ) ) { + return $args; + } + + $schema = array( 'title' => $args['label'] ); if ( ! is_array( $args['show_in_rest'] ) ) { $args['show_in_rest'] = array( 'schema' => $schema, From 0a92891b6c80a4f3fc9f6fd7c05d6cb09da5628f Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 36/42] Adapt branch after rebase --- packages/editor/src/bindings/post-meta.js | 29 +++++++++++------------ 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 47cd96512ebaa..100c8802696fe 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -9,20 +9,18 @@ import { store as coreDataStore } from '@wordpress/core-data'; import { store as editorStore } from '../store'; import { unlock } from '../lock-unlock'; -function getMetadata( registry, context ) { +function getMetadata( registry, context, registeredFields ) { 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; - } ); + Object.entries( registeredFields || {} ).forEach( + ( [ key, props ] ) => { + metaFields[ key ] = props.default; + } + ); } else { metaFields = getEditedEntityRecord( 'postType', @@ -37,19 +35,20 @@ function getMetadata( registry, context ) { export default { name: 'core/post-meta', getValues( { registry, context, bindings } ) { - const metaFields = getMetadata( registry, context ); const { getRegisteredPostMeta } = unlock( registry.select( coreDataStore ) ); const registeredFields = getRegisteredPostMeta( context?.postType ); + const metaFields = getMetadata( registry, context, registeredFields ); const newValues = {}; for ( const [ attributeName, source ] of Object.entries( bindings ) ) { - // Use the key if the value is not set. + // Use the value, the field label, or the field key. + const metaKey = source.args.key; newValues[ attributeName ] = - metaFields?.[ source.args.key ] ?? - registeredFields?.[ source.args.key ]?.label ?? - source.args.key; + metaFields?.[ metaKey ] ?? + registeredFields?.[ metaKey ]?.label ?? + metaKey; } return newValues; }, @@ -109,11 +108,11 @@ export default { return true; }, getFieldsList( { registry, context } ) { - const metaFields = getMetadata( registry, context ); const { getRegisteredPostMeta } = unlock( registry.select( coreDataStore ) ); - const registeredFields = getRegisteredPostMeta( postType ); + const registeredFields = getRegisteredPostMeta( context?.postType ); + const metaFields = getMetadata( registry, context, registeredFields ); if ( ! metaFields || ! Object.keys( metaFields ).length ) { return null; From 0a0ee3f4f7ff3f33f7d64f2937610d387dc4adc6 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 37/42] Remove extra spaces --- lib/compat/wordpress-6.7/block-bindings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.7/block-bindings.php b/lib/compat/wordpress-6.7/block-bindings.php index 1aaaf7bd3cb1b..a8f68c0f0f04e 100644 --- a/lib/compat/wordpress-6.7/block-bindings.php +++ b/lib/compat/wordpress-6.7/block-bindings.php @@ -57,7 +57,7 @@ function gutenberg_add_can_update_block_bindings_editor_setting( $editor_setting /** * Add `label` to `register_meta`. * - * @param array $args Array of arguments for registering meta. + * @param array $args Array of arguments for registering meta. * @return array Modified arguments array including `label`. */ function gutenberg_update_meta_args_with_label( $args ) { From f0896760629f55a62f957a76e7c1ba4a91b6f3a4 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 38/42] Don't rely on key outside of post meta --- packages/block-editor/src/components/rich-text/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 2f19eff4aaca2..5cc88266536a5 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -193,7 +193,6 @@ export function RichTextWrapper( const bindingKey = fieldsList?.[ relatedBinding?.args?.key ]?.label ?? - relatedBinding?.args?.key ?? blockBindingsSource?.label; const _bindingsPlaceholder = _disableBoundBlock From 6288d349f857958f6c5117412b11e66e0ec1e742 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 39/42] Remove key from bindings component --- packages/block-editor/src/hooks/block-bindings.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js index 55ac1ea1e603c..33284b4cd27fd 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -111,7 +111,6 @@ function BlockBindingsAttribute( { attribute, binding, fieldsList } ) { { isSourceInvalid ? __( 'Invalid source' ) : fieldsList?.[ sourceName ]?.[ args?.key ]?.label || - args?.key || sourceProps?.label || sourceName } From efa3b5d93cc9e3b4e8038068c9347dca8eea83ed Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:08:20 +0200 Subject: [PATCH 40/42] Read title instead of label --- packages/editor/src/bindings/post-meta.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 100c8802696fe..20013bd8f246b 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -18,7 +18,9 @@ function getMetadata( registry, context, registeredFields ) { // Populate the `metaFields` object with the default values. Object.entries( registeredFields || {} ).forEach( ( [ key, props ] ) => { - metaFields[ key ] = props.default; + if ( props.default ) { + metaFields[ key ] = props.default; + } } ); } else { @@ -47,7 +49,7 @@ export default { const metaKey = source.args.key; newValues[ attributeName ] = metaFields?.[ metaKey ] ?? - registeredFields?.[ metaKey ]?.label ?? + registeredFields?.[ metaKey ]?.title ?? metaKey; } return newValues; From 23908f7c520ff594820fd3ffdb8f057fd7625f9e Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 23:12:48 +0200 Subject: [PATCH 41/42] Add backport to changelog --- backport-changelog/6.7/7298.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 backport-changelog/6.7/7298.md diff --git a/backport-changelog/6.7/7298.md b/backport-changelog/6.7/7298.md new file mode 100644 index 0000000000000..4c01ef5d4f46e --- /dev/null +++ b/backport-changelog/6.7/7298.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7298 + +* https://github.com/WordPress/gutenberg/pull/65099 \ No newline at end of file From d724026a292a79c63c0beb6fdea701139ab38d5f Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 18 Sep 2024 12:39:42 +0200 Subject: [PATCH 42/42] Update translator comment --- packages/block-editor/src/components/rich-text/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 5cc88266536a5..387f388b8fdad 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -198,7 +198,7 @@ export function RichTextWrapper( const _bindingsPlaceholder = _disableBoundBlock ? bindingKey : sprintf( - /* translators: %s: source label or key */ + /* translators: %s: connected field label or source label */ __( 'Add %s' ), bindingKey );