From 70094bedcabb341263d9c923aea25ee9524639ac Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Mon, 29 Jul 2024 23:49:37 +0200 Subject: [PATCH 01/30] 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 862f4d108a7b389c21b6d7de25ea6ba28120a608 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:58:51 +0200 Subject: [PATCH 02/30] 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 7618ba6c36023..0b8a210cf68e8 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -90,8 +90,21 @@ export default { context?.postId ).meta; + 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 9b9d0b4cf846bdfcbfc57540b46ddca5d95d284e Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:27:50 +0200 Subject: [PATCH 03/30] 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 0b8a210cf68e8..b1676564592e8 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -90,8 +90,10 @@ export default { context?.postId ).meta; + // 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 ff8ebc52d9fbc5362489422bcd6be47f3e2232f6 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Fri, 2 Aug 2024 11:02:22 +0200 Subject: [PATCH 04/30] 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 233bf9748597d8c58360e37d8b4301d199a6c9c2 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Fri, 2 Aug 2024 11:11:41 +0200 Subject: [PATCH 05/30] WIP: Add first version to link templates and entities --- packages/core-data/src/private-selectors.ts | 245 ++++++++++++++++++++ 1 file changed, 245 insertions(+) diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index 841f4ee2ef460..a60a9e7685310 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. @@ -93,3 +98,243 @@ export function getEntityRecordPermissions( ) { return getEntityRecordsPermissions( state, kind, name, id )[ 0 ]; } + +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 9e3e1e8487b8e3dd8c3f3f5333fd86085bbd296a Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 13 Aug 2024 11:14:33 +0200 Subject: [PATCH 06/30] Revert "WIP: Add first version to link templates and entities" This reverts commit a43e39194f25d39e69426b15a2b9036022f301d3. --- packages/core-data/src/private-selectors.ts | 245 -------------------- 1 file changed, 245 deletions(-) diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index a60a9e7685310..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,243 +93,3 @@ export function getEntityRecordPermissions( ) { return getEntityRecordsPermissions( state, kind, name, id )[ 0 ]; } - -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 6ded461f1589aaf0a0c7d129ee6afb2c3fa95f12 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 13 Aug 2024 14:24:10 +0200 Subject: [PATCH 07/30] 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 8a69d400029df65301cb748b1d9a4f37341a3a86 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 13 Aug 2024 14:24:38 +0200 Subject: [PATCH 08/30] 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 6c32b80f2436cfda72ae4df500dd797060cf6e75 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 13 Aug 2024 14:30:22 +0200 Subject: [PATCH 09/30] Render the appropriate fields depending on the postType in templates --- packages/editor/src/bindings/post-meta.js | 72 +++++++++++++++++------ 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index b1676564592e8..5437c50cf29bc 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -82,31 +82,65 @@ export default { return true; }, getFieldsList( { registry, context } ) { - const metaFields = registry - .select( coreDataStore ) - .getEditedEntityRecord( + let metaFields = {}; + const { + type, + is_custom: isCustom, + slug, + } = registry.select( editorStore ).getCurrentPost(); + const { getPostTypes, getEntityRecord, getEditedEntityRecord } = + registry.select( coreDataStore ); + + // 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 + ) || []; + + // Infer the post type from the slug. + 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. + if ( isGlobalTemplate && props.subtype ) { + return; + } + metaFields[ key ] = props.default; + } ); + } else { + metaFields = getEditedEntityRecord( 'postType', context?.postType, context?.postId ).meta; - - // 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 ) { - 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; + return null; } // Remove footnotes or private keys from the list of fields. From e85aac7f29d77317265402eb14077ea901881dcd Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 14 Aug 2024 12:49:12 +0200 Subject: [PATCH 10/30] 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 5437c50cf29bc..5ee52b702e5e8 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -92,7 +92,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 304c28e6972ad407b3b3aea884cd1f603f246680 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:27:41 +0200 Subject: [PATCH 11/30] 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 | 30 +++++++++++++++-------- 2 files changed, 22 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 5ee52b702e5e8..e9bff70e347d3 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 @@ -81,19 +82,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( '-' ); @@ -110,19 +125,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. @@ -144,6 +153,7 @@ export default { } // 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 ) !== '_' From fac01f9a86934445ee785e2820b7c2c9269ffba3 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:54:04 +0200 Subject: [PATCH 12/30] 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 a4b3dc4e9c832b9a09584d3fa1446427a2ec849e Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 2 Sep 2024 17:31:52 +0200 Subject: [PATCH 13/30] Try: Expose registered meta fields in schema --- lib/compat/wordpress-6.7/rest-api.php | 38 ----- .../block-editor/src/hooks/block-bindings.js | 49 +++++- packages/core-data/src/entities.js | 155 ++++++++++-------- packages/editor/src/bindings/post-meta.js | 62 +++---- 4 files changed, 160 insertions(+), 144 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 3b59de2238b96..eeb74f0f51eee 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -182,11 +182,56 @@ function EditableBlockBindingsPanelItems( { export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { const registry = useRegistry(); const blockContext = useContext( BlockContext ); - const { bindings } = metadata || {}; const { removeAllBlockBindings } = useBlockBindingsUtils(); const bindableAttributes = getBindableAttributes( blockName ); const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + // While this hook doesn't directly call any selectors, `useSelect` is + // used purposely here to ensure `getFieldsList` is updated whenever + // there are attribute updates. + // `source.getFieldsList` may also call a selector via `registry.select`. + const { fieldsList } = useSelect( () => { + if ( ! bindableAttributes || bindableAttributes.length === 0 ) { + return {}; + } + const _fieldsList = {}; + const { getBlockBindingsSources } = unlock( blocksPrivateApis ); + const registeredSources = getBlockBindingsSources(); + Object.entries( registeredSources ).forEach( + ( [ sourceName, { getFieldsList, usesContext } ] ) => { + if ( getFieldsList ) { + // Populate context. + const context = {}; + if ( usesContext?.length ) { + for ( const key of usesContext ) { + context[ key ] = blockContext[ key ]; + } + } + const sourceList = getFieldsList( { + registry, + context, + } ); + // Only add source if the list is not empty. + if ( sourceList ) { + _fieldsList[ sourceName ] = { ...sourceList }; + } + } + } + ); + return { fieldsList: _fieldsList }; + }, [ blockContext, bindableAttributes, registry ] ); + // Return early if there are no bindable attributes. + if ( ! bindableAttributes || bindableAttributes.length === 0 ) { + return null; + } + // Remove empty sources from the list of fields. + Object.entries( fieldsList ).forEach( ( [ key, value ] ) => { + if ( ! Object.keys( value ).length ) { + delete fieldsList[ key ]; + } + } ); + // Filter bindings to only show bindable attributes and remove pattern overrides. + const { bindings } = metadata || {}; const filteredBindings = { ...bindings }; Object.keys( filteredBindings ).forEach( ( key ) => { if ( @@ -208,7 +253,6 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { return null; } - const fieldsList = {}; const { getBlockBindingsSources } = unlock( blocksPrivateApis ); const registeredSources = getBlockBindingsSources(); Object.entries( registeredSources ).forEach( @@ -240,6 +284,7 @@ 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..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 e9bff70e347d3..7288f8e4d09bf 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 @@ -82,59 +81,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 - ) || []; + // 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'; + // 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 851ae0d2cd6f177ed3584c3ed6d96fe25391bf99 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 3 Sep 2024 17:32:57 +0200 Subject: [PATCH 14/30] 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/reducer.js | 20 +++ packages/core-data/src/resolvers.js | 23 ++++ packages/core-data/src/selectors.ts | 13 ++ packages/editor/src/bindings/post-meta.js | 4 +- 8 files changed, 193 insertions(+), 91 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/reducer.js b/packages/core-data/src/reducer.js index 97a8cc5904153..9748355fc5caf 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -628,6 +628,25 @@ export function defaultTemplates( state = {}, action ) { return state; } +/** + * Reducer returning an object of registered post meta. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function registeredPostMeta( state = {}, action ) { + switch ( action.type ) { + case 'RECEIVE_REGISTERED_POST_META': + return { + ...state, + [ action.postType ]: action.registeredPostMeta, + }; + } + return state; +} + export default combineReducers( { terms, users, @@ -649,4 +668,5 @@ export default combineReducers( { userPatternCategories, navigationFallbackId, defaultTemplates, + registeredPostMeta, } ); diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 9229673903623..6810307f7d8a4 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -984,3 +984,26 @@ export const getRevision = dispatch.receiveRevisions( kind, name, recordKey, record, query ); } }; + +/** + * Requests a specific post type options from the REST API. + * + * @param {string} postType Post type slug. + */ +export const getRegisteredPostMeta = + ( postType ) => + async ( { select, dispatch } ) => { + try { + const restBase = select.getPostType( postType )?.rest_base; + const options = await apiFetch( { + path: `wp/v2/${ restBase }/?context=edit`, + method: 'OPTIONS', + } ); + dispatch.receiveRegisteredPostMeta( + postType, + options?.schema?.properties?.meta?.properties + ); + } catch { + dispatch.receiveRegisteredPostMeta( postType, false ); + } + }; diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index aeec14782ce4f..45726a053ac71 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -47,6 +47,7 @@ export interface State { navigationFallbackId: EntityRecordKey; userPatternCategories: Array< UserPatternCategory >; defaultTemplates: Record< string, string >; + registeredPostMeta: Record< string, { postType: string } >; } type EntityRecordKey = string | number; @@ -1526,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 7288f8e4d09bf..6fd088ce83516 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -88,7 +88,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. @@ -115,7 +115,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 9c11f15e60fb5d64f091460587501b91ed55a7eb Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:31:46 +0200 Subject: [PATCH 15/30] 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 01afd0dc07e1b43f25165356120b0c0fa3c2b32d Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Wed, 4 Sep 2024 18:00:37 +0200 Subject: [PATCH 16/30] 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-actions.js | 16 +++++++++++++ packages/core-data/src/private-selectors.ts | 12 ++++++++++ packages/core-data/src/selectors.ts | 12 ---------- packages/editor/src/bindings/post-meta.js | 7 +++++- 7 files changed, 34 insertions(+), 82 deletions(-) create mode 100644 packages/core-data/src/private-actions.js 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-actions.js b/packages/core-data/src/private-actions.js new file mode 100644 index 0000000000000..df76d2693e54f --- /dev/null +++ b/packages/core-data/src/private-actions.js @@ -0,0 +1,16 @@ +/** + * Returns an action object used in signalling that the registered post meta + * fields for a post type have been received. + * + * @param {string} postType Post type slug. + * @param {Object} registeredPostMeta Registered post meta. + * + * @return {Object} Action object. + */ +export function receiveRegisteredPostMeta( postType, registeredPostMeta ) { + return { + type: 'RECEIVE_REGISTERED_POST_META', + postType, + registeredPostMeta, + }; +} 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 6fd088ce83516..db5a25b41ca55 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -7,6 +7,7 @@ import { store as coreDataStore } from '@wordpress/core-data'; * Internal dependencies */ import { store as editorStore } from '../store'; +import { unlock } from '../lock-unlock'; export default { name: 'core/post-meta', @@ -88,9 +89,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 e49be2624f25617aada68960dda4cbeb105e010a Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Thu, 5 Sep 2024 09:32:06 +0200 Subject: [PATCH 17/30] Add unlocking and import --- packages/core-data/src/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js index ad6adec0203c5..99507a914f377 100644 --- a/packages/core-data/src/index.js +++ b/packages/core-data/src/index.js @@ -10,6 +10,7 @@ import reducer from './reducer'; import * as selectors from './selectors'; import * as privateSelectors from './private-selectors'; import * as actions from './actions'; +import * as privateActions from './private-actions'; import * as resolvers from './resolvers'; import createLocksActions from './locks/actions'; import { @@ -79,6 +80,7 @@ const storeConfig = () => ( { */ export const store = createReduxStore( STORE_NAME, storeConfig() ); unlock( store ).registerPrivateSelectors( privateSelectors ); +unlock( store ).registerPrivateActions( privateActions ); register( store ); // Register store after unlocking private selectors to allow resolvers to use them. export { default as EntityProvider } from './entity-provider'; From 32128049cc93aeb27bfab1b0df9d01f6407fe16a Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:55:00 +0200 Subject: [PATCH 18/30] 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 eeb74f0f51eee..3c5925d0524ac 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -186,40 +186,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; @@ -242,13 +249,6 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { } } ); - const { canUpdateBlockBindings } = useSelect( ( select ) => { - return { - canUpdateBlockBindings: - select( blockEditorStore ).getSettings().canUpdateBlockBindings, - }; - }, [] ); - if ( ! bindableAttributes || bindableAttributes.length === 0 ) { return null; } @@ -284,7 +284,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 116fc092082fe12ac2efd8e6246710ba6f5e3ed0 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:55:20 +0200 Subject: [PATCH 19/30] 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 3c5925d0524ac..ec7348a981d96 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -249,40 +249,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 66ab07df3994c7174be1020f8dbc1d5e0fbc00a3 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Fri, 6 Sep 2024 11:03:31 +0200 Subject: [PATCH 20/30] 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 db5a25b41ca55..cd7a14119ebff 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -125,8 +125,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 56cd505b63f4b625c4c7742f3ddd1263059b399b Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:16:05 +0200 Subject: [PATCH 21/30] 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 25196ff66bd0f9db4cce3d1ce34cae43e7641fa6 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Fri, 6 Sep 2024 18:33:17 +0200 Subject: [PATCH 22/30] 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 cd7a14119ebff..887df2528fc74 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -101,6 +101,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 6cb6a38b2f70c58eb326c39b12ce9f94a31421a5 Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Tue, 10 Sep 2024 10:53:27 +0200 Subject: [PATCH 23/30] 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/e2e-tests/plugins/block-bindings.php | 2 +- packages/editor/src/bindings/post-meta.js | 126 +++++++++--------- .../editor/various/block-bindings.spec.js | 42 +++--- 3 files changed, 85 insertions(+), 85 deletions(-) diff --git a/packages/e2e-tests/plugins/block-bindings.php b/packages/e2e-tests/plugins/block-bindings.php index 143feb240ac2e..8951255d516bf 100644 --- a/packages/e2e-tests/plugins/block-bindings.php +++ b/packages/e2e-tests/plugins/block-bindings.php @@ -28,7 +28,7 @@ function gutenberg_test_block_bindings_registration() { 'show_in_rest' => true, 'type' => 'string', 'single' => true, - 'default' => 'Value of the text_custom_field', + 'default' => 'Value of the text custom field', ) ); register_meta( diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 887df2528fc74..87efbe1687cda 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -9,21 +9,75 @@ 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 } ) { - const meta = registry - .select( coreDataStore ) - .getEditedEntityRecord( - 'postType', - context?.postType, - context?.postId - )?.meta; + const metaFields = getMetadata( registry, context ); + const newValues = {}; for ( const [ attributeName, source ] of Object.entries( bindings ) ) { // Use the key if the value is not set. newValues[ attributeName ] = - meta?.[ source.args.key ] ?? source.args.key; + metaFields?.[ source.args.key ] || source.args.key; } return newValues; }, @@ -83,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 6e36b6ad5dd33..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 @@ -1231,14 +1231,14 @@ test.describe( 'Block bindings', () => { name: 'Block: Paragraph', } ); await expect( paragraphBlock ).toHaveText( - 'Value of the text_custom_field' + 'Value of the text custom field' ); // Check the frontend shows the value of the custom field. const previewPage = await editor.openPreviewPage(); await expect( previewPage.locator( '#paragraph-binding' ) - ).toHaveText( 'Value of the text_custom_field' ); + ).toHaveText( 'Value of the text custom field' ); } ); test( "should show the value of the key when custom field doesn't exist", async ( { @@ -1400,7 +1400,7 @@ test.describe( 'Block bindings', () => { .locator( '[data-type="core/paragraph"]' ) .all(); await expect( initialParagraph ).toHaveText( - 'Value of the text_custom_field' + 'Value of the text custom field' ); await expect( newEmptyParagraph ).toHaveText( '' ); await expect( newEmptyParagraph ).toBeEditable(); @@ -1510,7 +1510,7 @@ test.describe( 'Block bindings', () => { name: 'Block: Paragraph', } ); await expect( paragraphBlock ).toHaveText( - 'Value of the text_custom_field' + 'Value of the text custom field' ); } ); } ); @@ -1538,14 +1538,14 @@ test.describe( 'Block bindings', () => { name: 'Block: Heading', } ); await expect( headingBlock ).toHaveText( - 'Value of the text_custom_field' + 'Value of the text custom field' ); // Check the frontend shows the value of the custom field. const previewPage = await editor.openPreviewPage(); await expect( previewPage.locator( '#heading-binding' ) - ).toHaveText( 'Value of the text_custom_field' ); + ).toHaveText( 'Value of the text custom field' ); } ); test( 'should add empty paragraph block when pressing enter', async ( { @@ -1584,7 +1584,7 @@ test.describe( 'Block bindings', () => { 'core/heading' ); await expect( initialHeading ).toHaveText( - 'Value of the text_custom_field' + 'Value of the text custom field' ); // Second block should be an empty paragraph block. await expect( newEmptyParagraph ).toHaveAttribute( @@ -1642,7 +1642,7 @@ test.describe( 'Block bindings', () => { .getByRole( 'textbox' ); await buttonBlock.click(); await expect( buttonBlock ).toHaveText( - 'Value of the text_custom_field' + 'Value of the text custom field' ); // Check the frontend shows the value of the custom field. @@ -1651,7 +1651,7 @@ test.describe( 'Block bindings', () => { '#button-text-binding a' ); await expect( buttonDom ).toHaveText( - 'Value of the text_custom_field' + 'Value of the text custom field' ); await expect( buttonDom ).toHaveAttribute( 'href', @@ -1731,7 +1731,7 @@ test.describe( 'Block bindings', () => { '#button-multiple-bindings a' ); await expect( buttonDom ).toHaveText( - 'Value of the text_custom_field' + 'Value of the text custom field' ); await expect( buttonDom ).toHaveAttribute( 'href', @@ -1778,7 +1778,7 @@ test.describe( 'Block bindings', () => { .all(); // First block should be the original block. await expect( initialButton ).toHaveText( - 'Value of the text_custom_field' + 'Value of the text custom field' ); // Second block should be an empty paragraph block. await expect( newEmptyButton ).toHaveText( '' ); @@ -1943,7 +1943,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( 'Value of the text custom field' ); // Check the frontend uses the value of the custom field. const previewPage = await editor.openPreviewPage(); @@ -1956,7 +1956,7 @@ test.describe( 'Block bindings', () => { ); await expect( imageDom ).toHaveAttribute( 'alt', - 'Value of the text_custom_field' + 'Value of the text custom field' ); await expect( imageDom ).toHaveAttribute( 'title', @@ -2013,7 +2013,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( 'Value of the text custom field' ); // Check the frontend uses the value of the custom field. const previewPage = await editor.openPreviewPage(); @@ -2030,7 +2030,7 @@ test.describe( 'Block bindings', () => { ); await expect( imageDom ).toHaveAttribute( 'title', - 'Value of the text_custom_field' + 'Value of the text custom field' ); } ); @@ -2077,7 +2077,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( 'Value of the text custom field' ); // Title input should have the original value. const advancedButton = page @@ -2107,7 +2107,7 @@ test.describe( 'Block bindings', () => { ); await expect( imageDom ).toHaveAttribute( 'alt', - 'Value of the text_custom_field' + 'Value of the text custom field' ); await expect( imageDom ).toHaveAttribute( 'title', From 0144e343d169eaa9864e0639f796000800492c60 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:35:04 +0200 Subject: [PATCH 24/30] 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 4cac9bde0f3fbfc0a953ac00c099e54300fd4c69 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:57:45 +0200 Subject: [PATCH 25/30] 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 af79c2b2f9e71f23628c6c68d0c0321c09bf218b Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Wed, 11 Sep 2024 12:54:21 +0200 Subject: [PATCH 26/30] 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 782dd99252e8bbbe335d453c82effacfc465f819 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 16 Sep 2024 14:03:52 +0200 Subject: [PATCH 27/30] Fix `useSelect` warning --- packages/block-editor/src/hooks/block-bindings.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js index ec7348a981d96..c59c365d9d6eb 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -31,6 +31,8 @@ import { store as blockEditorStore } from '../store'; const { DropdownMenuV2 } = unlock( componentsPrivateApis ); +const EMPTY_OBJECT = {}; + const useToolsPanelDropdownMenuProps = () => { const isMobile = useViewportMatch( 'medium', '<' ); return ! isMobile @@ -187,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( @@ -219,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 b4a11c01b729a0840dbabd51645cefaef852212f Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 11:47:03 +0200 Subject: [PATCH 28/30] 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 42ef0c3ed65ca86c79eee0c9a9f706625e0c3e7a Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 12:25:48 +0200 Subject: [PATCH 29/30] 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 deb3e2de84970efdfb770977e2bbe3820183dcae Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 17 Sep 2024 12:51:49 +0200 Subject: [PATCH 30/30] 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