From 520ac3a63e5b4cab3b4e02d6eb5920e59732c727 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/17] 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 36c5e35e8fa5e5568d83602a115564dfb210c492 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/17] 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 4de8396d4c13b..908ab7920fbeb 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 a981a88b92f99a079be3c793f96da78ce4ec4bc6 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/17] 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 908ab7920fbeb..70f73c8072d3c 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 521201ed483bf6b1aa6c91177db0e49145be76d4 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Fri, 2 Aug 2024 11:02:22 +0200 Subject: [PATCH 04/17] 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 ced672b1b32d272b134270abb090c68fb20d3ad5 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Fri, 2 Aug 2024 11:11:41 +0200 Subject: [PATCH 05/17] 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 133db902284ddefc55ffe76b74de4e7b3fe3b9c8 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 13 Aug 2024 11:14:33 +0200 Subject: [PATCH 06/17] 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 e2c2df2b9f32ff1bd9c50631eaeab84fab8317d1 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 13 Aug 2024 14:24:10 +0200 Subject: [PATCH 07/17] 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 748898b4d4a69da2ba7c2dbc851e9b86831be336 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 13 Aug 2024 14:24:38 +0200 Subject: [PATCH 08/17] 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 b62ac07bda56d01a532fbdfeb0da412610534bd0 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 13 Aug 2024 14:30:22 +0200 Subject: [PATCH 09/17] 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 70f73c8072d3c..048eda86c898f 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 055930491ef985371a5124660e76b5f1cbdfd5ac Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 14 Aug 2024 12:49:12 +0200 Subject: [PATCH 10/17] 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 048eda86c898f..d2b05a19a91e5 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 a22d0564baece7eb3ea93f78db52d580e5c1dd4e 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/17] 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 d2b05a19a91e5..46045265be0fc 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 0c0abc302ccf660984fe2769f56aa1a2c1908ff7 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/17] 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 e3168a0301500bfa670e9c6d0e7a4bad1eeadec6 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 2 Sep 2024 17:31:52 +0200 Subject: [PATCH 13/17] Try: Expose registered meta fields in schema --- lib/compat/wordpress-6.7/rest-api.php | 38 ----- .../block-editor/src/hooks/block-bindings.js | 86 +++++----- packages/core-data/src/entities.js | 155 ++++++++++-------- packages/editor/src/bindings/post-meta.js | 62 +++---- 4 files changed, 161 insertions(+), 180 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..4efa375a132aa 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -182,21 +182,10 @@ function EditableBlockBindingsPanelItems( { export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { const registry = useRegistry(); const blockContext = useContext( BlockContext ); - const { bindings } = metadata || {}; const { removeAllBlockBindings } = useBlockBindingsUtils(); const bindableAttributes = getBindableAttributes( blockName ); const dropdownMenuProps = useToolsPanelDropdownMenuProps(); - const filteredBindings = { ...bindings }; - Object.keys( filteredBindings ).forEach( ( key ) => { - if ( - ! canBindAttribute( blockName, key ) || - filteredBindings[ key ].source === 'core/pattern-overrides' - ) { - delete filteredBindings[ key ]; - } - } ); - const { canUpdateBlockBindings } = useSelect( ( select ) => { return { canUpdateBlockBindings: @@ -204,42 +193,63 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { }; }, [] ); - if ( ! bindableAttributes || bindableAttributes.length === 0 ) { - return null; - } - - const fieldsList = {}; - const { getBlockBindingsSources } = unlock( blocksPrivateApis ); - const registeredSources = getBlockBindingsSources(); - Object.entries( registeredSources ).forEach( - ( [ sourceName, { getFieldsList, usesContext } ] ) => { - if ( getFieldsList ) { - // Populate context. - const context = {}; - if ( usesContext?.length ) { - for ( const key of usesContext ) { - context[ key ] = blockContext[ key ]; + // While this hook doesn't directly call any selectors, `useSelect` is + // used purposely here to ensure `getFieldsList` is updated whenever + // there are attribute updates. + // `source.getFieldsList` may also call a selector via `registry.select`. + const { fieldsList } = useSelect( () => { + if ( ! bindableAttributes || bindableAttributes.length === 0 ) { + return {}; + } + const _fieldsList = {}; + const { getBlockBindingsSources } = unlock( blocksPrivateApis ); + const registeredSources = getBlockBindingsSources(); + Object.entries( registeredSources ).forEach( + ( [ sourceName, { getFieldsList, usesContext } ] ) => { + if ( getFieldsList ) { + // Populate context. + const context = {}; + if ( usesContext?.length ) { + for ( const key of usesContext ) { + context[ key ] = blockContext[ key ]; + } + } + const sourceList = getFieldsList( { + registry, + context, + } ); + // Only add source if the list is not empty. + if ( sourceList ) { + _fieldsList[ sourceName ] = { ...sourceList }; } - } - const sourceList = getFieldsList( { - registry, - context, - } ); - // Only add source if the list is not empty. - if ( sourceList ) { - fieldsList[ sourceName ] = { ...sourceList }; } } - } - ); - // Remove empty sources. + ); + return { fieldsList: _fieldsList }; + }, [ blockContext, bindableAttributes, registry ] ); + // Return early if there are no bindable attributes. + if ( ! bindableAttributes || bindableAttributes.length === 0 ) { + return null; + } + // Remove empty sources from the list of fields. Object.entries( fieldsList ).forEach( ( [ key, value ] ) => { if ( ! Object.keys( value ).length ) { delete fieldsList[ key ]; } } ); + // Filter bindings to only show bindable attributes and remove pattern overrides. + const { bindings } = metadata || {}; + const filteredBindings = { ...bindings }; + Object.keys( filteredBindings ).forEach( ( key ) => { + if ( + ! canBindAttribute( blockName, key ) || + filteredBindings[ key ].source === 'core/pattern-overrides' + ) { + delete filteredBindings[ key ]; + } + } ); - // Lock the UI when the user can't update bindings or there are no fields to connect to. + // Lock the UI when the experiment is not enabled or there are no fields to connect to. const readOnly = ! canUpdateBlockBindings || ! Object.keys( fieldsList ).length; 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 46045265be0fc..35f042e0ec915 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 fbde61492902d06ff03eaf92730fc668afd59d1c Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 3 Sep 2024 17:32:57 +0200 Subject: [PATCH 14/17] 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 d1aaf0b447cfe..ebb2258a9a6bc 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -976,3 +976,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 35f042e0ec915..a0872abd2ec19 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 dbd66f627fab6b95f36d21f4eb4f863183799799 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/17] 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 ebb2258a9a6bc..86aed8461c34d 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -986,9 +986,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 36a08ae6de3fae9d579f2d798714cd0445dbcc05 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/17] 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 a0872abd2ec19..488085a14cda5 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 4b3f87ee319c368598b390d595eb18f6eeebd53a Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 4 Sep 2024 17:05:41 +0200 Subject: [PATCH 17/17] Inherit post type from template slug --- .../editor/src/components/provider/index.js | 50 +++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index aaf25621d3324..654fecf2d3ecf 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -4,7 +4,11 @@ import { useEffect, useLayoutEffect, useMemo } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { EntityProvider, useEntityBlockEditor } from '@wordpress/core-data'; +import { + EntityProvider, + useEntityBlockEditor, + store as coreStore, +} from '@wordpress/core-data'; import { BlockEditorProvider, BlockContextProvider, @@ -48,7 +52,6 @@ const noop = () => {}; */ const NON_CONTEXTUAL_POST_TYPES = [ 'wp_block', - 'wp_template', 'wp_navigation', 'wp_template_part', ]; @@ -161,31 +164,57 @@ export const ExperimentalEditorProvider = withRegistryProvider( BlockEditorProviderComponent = ExperimentalBlockEditorProvider, __unstableTemplate: template, } ) => { - const { editorSettings, selection, isReady, mode } = useSelect( - ( select ) => { + const { editorSettings, selection, isReady, mode, postTypes } = + useSelect( ( select ) => { const { getEditorSettings, getEditorSelection, getRenderingMode, __unstableIsEditorReady, } = select( editorStore ); + const { getPostTypes } = select( coreStore ); + return { editorSettings: getEditorSettings(), isReady: __unstableIsEditorReady(), mode: getRenderingMode(), selection: getEditorSelection(), + postTypes: + getPostTypes( { per_page: -1 } )?.map( + ( entity ) => entity.slug + ) || [], }; - }, - [] - ); + }, [] ); const shouldRenderTemplate = !! template && mode !== 'post-only'; const rootLevelPost = shouldRenderTemplate ? template : post; const defaultBlockContext = useMemo( () => { - const postContext = + const postContext = {}; + // If it is a template, try to inherit the post type from the slug. + if ( post.type === 'wp_template' ) { + if ( ! post.is_custom ) { + const [ kind ] = post.slug.split( '-' ); + switch ( kind ) { + case 'page': + postContext.postType = 'page'; + break; + case 'single': + // Infer the post type from the slug. + const match = post.slug.match( + `^single-(${ postTypes.join( '|' ) })(?:-.+)?$` + ); + if ( match ) { + postContext.postType = match[ 1 ]; + } + break; + } + } + } else if ( ! NON_CONTEXTUAL_POST_TYPES.includes( rootLevelPost.type ) || shouldRenderTemplate - ? { postId: post.id, postType: post.type } - : {}; + ) { + postContext.postId = post.id; + postContext.postType = post.type; + } return { ...postContext, @@ -200,6 +229,7 @@ export const ExperimentalEditorProvider = withRegistryProvider( post.type, rootLevelPost.type, rootLevelPost.slug, + postTypes, ] ); const { id, type } = rootLevelPost; const blockEditorSettings = useBlockEditorSettings(