From e3561037b4ab08de3112fec64fb66ad6a9d54b51 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Mon, 16 Oct 2023 10:45:56 +0200 Subject: [PATCH 01/22] Add capabilities settings to pattern categories so authors can add categories to their patterns. Also set API call to `view` context so contributors can see user pattern categories --- lib/compat/wordpress-6.4/block-patterns.php | 4 ++++ packages/core-data/src/resolvers.js | 1 + 2 files changed, 5 insertions(+) diff --git a/lib/compat/wordpress-6.4/block-patterns.php b/lib/compat/wordpress-6.4/block-patterns.php index 922dea910b47a..c7ba2ff64d1a2 100644 --- a/lib/compat/wordpress-6.4/block-patterns.php +++ b/lib/compat/wordpress-6.4/block-patterns.php @@ -30,6 +30,10 @@ function gutenberg_register_taxonomy_patterns() { 'show_in_nav_menus' => false, 'show_in_rest' => true, 'show_admin_column' => true, + 'capabilities' => array( + 'edit_terms' => 'publish_posts', + 'assign_terms' => 'publish_posts', + ), ); register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), $args ); } diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 07e9cd98cb5ec..5fc7cd14f35c0 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -651,6 +651,7 @@ export const getUserPatternCategories = { per_page: -1, _fields: 'id,name,description,slug', + context: 'view', } ); From 0627b4b63eda9608d24e52b8bffe7c933c2c1066 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Mon, 16 Oct 2023 11:31:10 +0200 Subject: [PATCH 02/22] Fix linting error --- lib/compat/wordpress-6.4/block-patterns.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.4/block-patterns.php b/lib/compat/wordpress-6.4/block-patterns.php index c7ba2ff64d1a2..b3894ffdd6626 100644 --- a/lib/compat/wordpress-6.4/block-patterns.php +++ b/lib/compat/wordpress-6.4/block-patterns.php @@ -30,7 +30,7 @@ function gutenberg_register_taxonomy_patterns() { 'show_in_nav_menus' => false, 'show_in_rest' => true, 'show_admin_column' => true, - 'capabilities' => array( + 'capabilities' => array( 'edit_terms' => 'publish_posts', 'assign_terms' => 'publish_posts', ), From df14aca82f99521653e7c6f0f57a413f00d5cbd5 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Mon, 16 Oct 2023 18:39:35 +0200 Subject: [PATCH 03/22] We don't need assign terms as we are not assigning these terms to posts --- lib/compat/wordpress-6.4/block-patterns.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/compat/wordpress-6.4/block-patterns.php b/lib/compat/wordpress-6.4/block-patterns.php index b3894ffdd6626..97177f649e620 100644 --- a/lib/compat/wordpress-6.4/block-patterns.php +++ b/lib/compat/wordpress-6.4/block-patterns.php @@ -32,7 +32,6 @@ function gutenberg_register_taxonomy_patterns() { 'show_admin_column' => true, 'capabilities' => array( 'edit_terms' => 'publish_posts', - 'assign_terms' => 'publish_posts', ), ); register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), $args ); From 906bc9e1ad06f2f59f93f714ee7f134b5dcf8dd8 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Tue, 17 Oct 2023 09:29:53 +0200 Subject: [PATCH 04/22] Remove additional capabilities as the defaults work --- lib/compat/wordpress-6.4/block-patterns.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/compat/wordpress-6.4/block-patterns.php b/lib/compat/wordpress-6.4/block-patterns.php index 97177f649e620..922dea910b47a 100644 --- a/lib/compat/wordpress-6.4/block-patterns.php +++ b/lib/compat/wordpress-6.4/block-patterns.php @@ -30,9 +30,6 @@ function gutenberg_register_taxonomy_patterns() { 'show_in_nav_menus' => false, 'show_in_rest' => true, 'show_admin_column' => true, - 'capabilities' => array( - 'edit_terms' => 'publish_posts', - ), ); register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), $args ); } From c562738fd0c2dc08f9e7b11247a1d85ea972e0ab Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 17 Oct 2023 16:42:47 +0800 Subject: [PATCH 05/22] Try adding a pattern categories rest controller to handle the unique permission requirements --- lib/compat/wordpress-6.4/block-patterns.php | 1 + ...erg-rest-pattern-categories-controller.php | 45 +++++++++++++++++++ lib/load.php | 1 + 3 files changed, 47 insertions(+) create mode 100644 lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php diff --git a/lib/compat/wordpress-6.4/block-patterns.php b/lib/compat/wordpress-6.4/block-patterns.php index 922dea910b47a..2d48716a7465c 100644 --- a/lib/compat/wordpress-6.4/block-patterns.php +++ b/lib/compat/wordpress-6.4/block-patterns.php @@ -30,6 +30,7 @@ function gutenberg_register_taxonomy_patterns() { 'show_in_nav_menus' => false, 'show_in_rest' => true, 'show_admin_column' => true, + 'rest_controller_class' => 'Gutenberg_REST_Pattern_Categories_Controller', ); register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), $args ); } diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php new file mode 100644 index 0000000000000..e14301319f52c --- /dev/null +++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php @@ -0,0 +1,45 @@ +check_is_taxonomy_allowed( $this->taxonomy ) ) { + return false; + } + + $taxonomy_obj = get_taxonomy( $this->taxonomy ); + + // Patterns categories are a flat hierarchy (like tags), but work more like post categories in terms of permissions. + if ( ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) { + return new WP_Error( + 'rest_cannot_create', + __( 'Sorry, you are not allowed to create terms in this taxonomy.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } +} diff --git a/lib/load.php b/lib/load.php index 13636b2587130..cfde63fc7dcbc 100644 --- a/lib/load.php +++ b/lib/load.php @@ -53,6 +53,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.4 compat. require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php'; require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php'; + require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php'; require_once __DIR__ . '/compat/wordpress-6.4/rest-api.php'; require_once __DIR__ . '/compat/wordpress-6.4/theme-previews.php'; From bd8ff09188f868b1d49aea2ff130663e9024d074 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Tue, 17 Oct 2023 12:05:36 +0200 Subject: [PATCH 06/22] Don't allow adding of categories if user doesn't have capability, just show current available categories --- .../src/components/category-selector.js | 38 ++++++---- .../src/components/create-pattern-modal.js | 71 +++++++++++-------- 2 files changed, 68 insertions(+), 41 deletions(-) diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js index 7f00350e278ec..ab9594f35abe1 100644 --- a/packages/patterns/src/components/category-selector.js +++ b/packages/patterns/src/components/category-selector.js @@ -3,7 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { useMemo, useState } from '@wordpress/element'; -import { FormTokenField } from '@wordpress/components'; +import { FormTokenField, SelectControl } from '@wordpress/components'; import { useDebounce } from '@wordpress/compose'; import { decodeEntities } from '@wordpress/html-entities'; @@ -17,7 +17,9 @@ export default function CategorySelector( { categoryTerms, onChange, categoryMap, + canAddCategories, } ) { + const categoryOptions = Array.from( categoryMap.values() ); const [ search, setSearch ] = useState( '' ); const debouncedSearch = useDebounce( setSearch, 500 ); @@ -51,16 +53,28 @@ export default function CategorySelector( { } return ( - + <> + { canAddCategories && ( + + ) } + { ! canAddCategories && categoryOptions.length > 0 && ( + onChange( terms ) } + label={ __( 'Categories' ) } + /> + ) } + ); } diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index 22d20fd037265..b1da7166aefa0 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -48,41 +48,46 @@ export default function CreatePatternModal( { const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore ); const { createErrorNotice } = useDispatch( noticesStore ); - const { corePatternCategories, userPatternCategories } = useSelect( - ( select ) => { - const { getUserPatternCategories, getBlockPatternCategories } = - select( coreStore ); + const { corePatternCategories, userPatternCategories, canAddCategories } = + useSelect( ( select ) => { + const { + getUserPatternCategories, + getBlockPatternCategories, + canUser, + } = select( coreStore ); return { corePatternCategories: getBlockPatternCategories(), userPatternCategories: getUserPatternCategories(), + canAddCategories: canUser( 'create', 'wp_pattern_category' ), }; - } - ); + } ); const categoryMap = useMemo( () => { // Merge the user and core pattern categories and remove any duplicates. const uniqueCategories = new Map(); - [ ...userPatternCategories, ...corePatternCategories ].forEach( - ( category ) => { - if ( - ! uniqueCategories.has( category.label ) && - // There are two core categories with `Post` label so explicitly remove the one with - // the `query` slug to avoid any confusion. - category.name !== 'query' - ) { - // We need to store the name separately as this is used as the slug in the - // taxonomy and may vary from the label. - uniqueCategories.set( category.label, { - label: category.label, - value: category.label, - name: category.name, - } ); - } + [ + ...userPatternCategories, + ...( canAddCategories ? corePatternCategories : [] ), + ].forEach( ( category ) => { + if ( + ! uniqueCategories.has( category.label ) && + // There are two core categories with `Post` label so explicitly remove the one with + // the `query` slug to avoid any confusion. + category.name !== 'query' + ) { + // We need to store the name separately as this is used as the slug in the + // taxonomy and may vary from the label. + uniqueCategories.set( category.label, { + label: category.label, + value: category.label, + name: category.name, + id: category.id, + } ); } - ); + } ); return uniqueCategories; - }, [ userPatternCategories, corePatternCategories ] ); + }, [ userPatternCategories, corePatternCategories, canAddCategories ] ); async function onCreate( patternTitle, sync ) { if ( ! title || isSaving ) { @@ -91,11 +96,18 @@ export default function CreatePatternModal( { try { setIsSaving( true ); - const categories = await Promise.all( - categoryTerms.map( ( termName ) => - findOrCreateTerm( termName ) - ) - ); + let categories; + if ( canAddCategories ) { + categories = await Promise.all( + categoryTerms.map( ( termName ) => + findOrCreateTerm( termName ) + ) + ); + } else { + categories = categoryTerms.map( + ( term ) => categoryMap.get( term ).id + ); + } const newPattern = await createPattern( patternTitle, @@ -177,6 +189,7 @@ export default function CreatePatternModal( { categoryTerms={ categoryTerms } onChange={ setCategoryTerms } categoryMap={ categoryMap } + canAddCategories={ canAddCategories } /> Date: Tue, 17 Oct 2023 13:07:14 +0200 Subject: [PATCH 07/22] Fix linting issues --- lib/compat/wordpress-6.4/block-patterns.php | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/compat/wordpress-6.4/block-patterns.php b/lib/compat/wordpress-6.4/block-patterns.php index 2d48716a7465c..bbb910ff400d0 100644 --- a/lib/compat/wordpress-6.4/block-patterns.php +++ b/lib/compat/wordpress-6.4/block-patterns.php @@ -16,20 +16,20 @@ */ function gutenberg_register_taxonomy_patterns() { $args = array( - 'public' => true, - 'publicly_queryable' => false, - 'hierarchical' => false, - 'labels' => array( + 'public' => true, + 'publicly_queryable' => false, + 'hierarchical' => false, + 'labels' => array( 'name' => _x( 'Pattern Categories', 'taxonomy general name' ), 'singular_name' => _x( 'Pattern Category', 'taxonomy singular name' ), ), - 'query_var' => false, - 'rewrite' => false, - 'show_ui' => true, - '_builtin' => true, - 'show_in_nav_menus' => false, - 'show_in_rest' => true, - 'show_admin_column' => true, + 'query_var' => false, + 'rewrite' => false, + 'show_ui' => true, + '_builtin' => true, + 'show_in_nav_menus' => false, + 'show_in_rest' => true, + 'show_admin_column' => true, 'rest_controller_class' => 'Gutenberg_REST_Pattern_Categories_Controller', ); register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), $args ); From 8fc53393b78d6c35a2b9cda631af5db8e3af5123 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Tue, 17 Oct 2023 13:30:06 +0200 Subject: [PATCH 08/22] Fix linting issues --- .../class-gutenberg-rest-pattern-categories-controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php index e14301319f52c..5e47a9f4846f1 100644 --- a/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php +++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php @@ -19,12 +19,12 @@ class Gutenberg_REST_Pattern_Categories_Controller extends WP_REST_Terms_Control * Make pattern categories behave more like a hierarchical taxonomy in terms of permissions. * Check the edit_terms cap to see whether term creation is possible. * - * @since 6.4.0 + * @since 6.4.0 * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ - public function create_item_permissions_check( $request ) { + public function create_item_permissions_check() { if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) { return false; } From 0422988add61942b390dadaee1a396236aa4b2d6 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Tue, 17 Oct 2023 13:41:41 +0200 Subject: [PATCH 09/22] More appeasing of the linting gods --- .../class-gutenberg-rest-pattern-categories-controller.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php index 5e47a9f4846f1..b996d34b1dc74 100644 --- a/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php +++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php @@ -21,7 +21,6 @@ class Gutenberg_REST_Pattern_Categories_Controller extends WP_REST_Terms_Control * * @since 6.4.0 * - * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function create_item_permissions_check() { From 6e82662b3bbb80f3fc257ad96beef8dcb5f6752d Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Tue, 17 Oct 2023 13:53:44 +0200 Subject: [PATCH 10/22] Third time lucky with phpcs --- .../class-gutenberg-rest-pattern-categories-controller.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php index b996d34b1dc74..e249d67e8acaa 100644 --- a/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php +++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php @@ -21,9 +21,10 @@ class Gutenberg_REST_Pattern_Categories_Controller extends WP_REST_Terms_Control * * @since 6.4.0 * + * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ - public function create_item_permissions_check() { + public function create_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) { return false; } From f8b3edd22c4fc13b31a3ba04f0ff5c83a56a2717 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Wed, 18 Oct 2023 11:02:08 +0200 Subject: [PATCH 11/22] Use a list of checkboxes instead of a multiselect for category selection for authors --- .../src/components/category-selector.js | 53 ++++++++++++++++--- packages/patterns/src/components/style.scss | 26 +++++++++ 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js index ab9594f35abe1..0336bb312a947 100644 --- a/packages/patterns/src/components/category-selector.js +++ b/packages/patterns/src/components/category-selector.js @@ -3,7 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { useMemo, useState } from '@wordpress/element'; -import { FormTokenField, SelectControl } from '@wordpress/components'; +import { FormTokenField, CheckboxControl } from '@wordpress/components'; import { useDebounce } from '@wordpress/compose'; import { decodeEntities } from '@wordpress/html-entities'; @@ -52,6 +52,38 @@ export default function CategorySelector( { onChange( uniqueTerms ); } + const renderTerms = ( renderedTerms ) => { + return renderedTerms.map( ( category ) => { + return ( +
+ { + if ( categoryTerms.includes( category.label ) ) { + onChange( + categoryTerms.filter( + ( categoryTerm ) => + categoryTerm !== category.label + ) + ); + } else { + onChange( [ + ...categoryTerms, + category.label, + ] ); + } + } } + label={ decodeEntities( category.label ) } + /> +
+ ); + } ); + }; + return ( <> { canAddCategories && ( @@ -68,12 +100,19 @@ export default function CategorySelector( { /> ) } { ! canAddCategories && categoryOptions.length > 0 && ( - onChange( terms ) } - label={ __( 'Categories' ) } - /> + <> +
+ { __( 'Categories' ) } +
+
+ { renderTerms( categoryOptions ) } +
+ ) } ); diff --git a/packages/patterns/src/components/style.scss b/packages/patterns/src/components/style.scss index e786952004495..b29a124d4e231 100644 --- a/packages/patterns/src/components/style.scss +++ b/packages/patterns/src/components/style.scss @@ -34,3 +34,29 @@ // Override the default 1px margin-x. margin: 0; } + +.patterns-menu-items__convert-modal__terms-label { + font-size: 11px; + font-weight: 500; + line-height: 1.4; + text-transform: uppercase; + display: inline-block; + padding: 0; +} + +.patterns-menu-items__convert-modal__terms-list { + max-height: 6rem; + overflow: auto; + border: 1px solid $gray-600; + // Extra left padding prevents checkbox focus borders from being cut off. + padding-left: $border-width * 4 + $border-width-focus-fallback; + margin-top: -$border-width * 4 - $border-width-focus-fallback; + padding-top: $border-width * 4 + $border-width-focus-fallback; + + .patterns-menu-items__convert-modal__terms-choice { + margin-bottom: $grid-unit-10; + &:last-child { + margin-bottom: $grid-unit-05; + } + } +} From aff2d017a9457c1e9a17cecbbf6940694558872e Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Wed, 18 Oct 2023 11:27:40 +0200 Subject: [PATCH 12/22] Switch to using BaseControl to get the label --- .../src/components/category-selector.js | 31 ++++++++++++------- packages/patterns/src/components/style.scss | 1 - 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js index 0336bb312a947..9c773232352b8 100644 --- a/packages/patterns/src/components/category-selector.js +++ b/packages/patterns/src/components/category-selector.js @@ -3,7 +3,11 @@ */ import { __ } from '@wordpress/i18n'; import { useMemo, useState } from '@wordpress/element'; -import { FormTokenField, CheckboxControl } from '@wordpress/components'; +import { + FormTokenField, + CheckboxControl, + BaseControl, +} from '@wordpress/components'; import { useDebounce } from '@wordpress/compose'; import { decodeEntities } from '@wordpress/html-entities'; @@ -101,17 +105,20 @@ export default function CategorySelector( { ) } { ! canAddCategories && categoryOptions.length > 0 && ( <> -
- { __( 'Categories' ) } -
-
- { renderTerms( categoryOptions ) } -
+ + + { __( 'Categories' ) } + + +
+ { renderTerms( categoryOptions ) } +
+
) } diff --git a/packages/patterns/src/components/style.scss b/packages/patterns/src/components/style.scss index b29a124d4e231..286d4fa682a45 100644 --- a/packages/patterns/src/components/style.scss +++ b/packages/patterns/src/components/style.scss @@ -50,7 +50,6 @@ border: 1px solid $gray-600; // Extra left padding prevents checkbox focus borders from being cut off. padding-left: $border-width * 4 + $border-width-focus-fallback; - margin-top: -$border-width * 4 - $border-width-focus-fallback; padding-top: $border-width * 4 + $border-width-focus-fallback; .patterns-menu-items__convert-modal__terms-choice { From ca568f01ebc6c933325f0fd3bfddd1f52c6a85a9 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Wed, 18 Oct 2023 15:01:17 +0200 Subject: [PATCH 13/22] Abstract out the author category select so it can be shared by the post editor --- .../src/components/category-editor.js | 99 +++++++++++++++ .../src/components/category-selector.js | 117 ++++-------------- .../src/components/create-pattern-modal.js | 4 +- 3 files changed, 122 insertions(+), 98 deletions(-) create mode 100644 packages/patterns/src/components/category-editor.js diff --git a/packages/patterns/src/components/category-editor.js b/packages/patterns/src/components/category-editor.js new file mode 100644 index 0000000000000..b761f8b1ab745 --- /dev/null +++ b/packages/patterns/src/components/category-editor.js @@ -0,0 +1,99 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useMemo, useState } from '@wordpress/element'; +import { FormTokenField } from '@wordpress/components'; +import { useDebounce } from '@wordpress/compose'; +import { decodeEntities } from '@wordpress/html-entities'; + +/** + * Internal dependencies + */ +import CategorySelector from './category-selector'; + +const unescapeString = ( arg ) => { + return decodeEntities( arg ); +}; + +export const CATEGORY_SLUG = 'wp_pattern_category'; + +export default function CategoryEditor( { + categoryTerms, + onChange, + categoryMap, + canAddCategories, +} ) { + const categoryOptions = Array.from( categoryMap.values() ); + const [ search, setSearch ] = useState( '' ); + const debouncedSearch = useDebounce( setSearch, 500 ); + + const suggestions = useMemo( () => { + return Array.from( categoryMap.values() ) + .map( ( category ) => unescapeString( category.label ) ) + .filter( ( category ) => { + if ( search !== '' ) { + return category + .toLowerCase() + .includes( search.toLowerCase() ); + } + return true; + } ) + .sort( ( a, b ) => a.localeCompare( b ) ); + }, [ search, categoryMap ] ); + + function handleChange( termNames ) { + const uniqueTerms = termNames.reduce( ( terms, newTerm ) => { + if ( + ! terms.some( + ( term ) => term.toLowerCase() === newTerm.toLowerCase() + ) + ) { + terms.push( newTerm ); + } + return terms; + }, [] ); + + onChange( uniqueTerms ); + } + const isCategorySelected = ( selectedCategory ) => + categoryTerms.includes( selectedCategory.label ); + + const onCategorySelectChange = ( selectedCategory ) => { + if ( categoryTerms.includes( selectedCategory.label ) ) { + onChange( + categoryTerms.filter( + ( categoryTerm ) => categoryTerm !== selectedCategory.label + ) + ); + } else { + onChange( [ ...categoryTerms, selectedCategory.label ] ); + } + }; + + return ( + <> + { canAddCategories && ( + + ) } + { ! canAddCategories && categoryOptions.length > 0 && ( + + ) } + + ); +} diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js index 9c773232352b8..6ee80a7d4e508 100644 --- a/packages/patterns/src/components/category-selector.js +++ b/packages/patterns/src/components/category-selector.js @@ -2,60 +2,18 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useMemo, useState } from '@wordpress/element'; -import { - FormTokenField, - CheckboxControl, - BaseControl, -} from '@wordpress/components'; -import { useDebounce } from '@wordpress/compose'; -import { decodeEntities } from '@wordpress/html-entities'; -const unescapeString = ( arg ) => { - return decodeEntities( arg ); -}; +import { CheckboxControl, BaseControl } from '@wordpress/components'; + +import { decodeEntities } from '@wordpress/html-entities'; export const CATEGORY_SLUG = 'wp_pattern_category'; export default function CategorySelector( { - categoryTerms, onChange, - categoryMap, - canAddCategories, + isCategorySelected, + categoryOptions, } ) { - const categoryOptions = Array.from( categoryMap.values() ); - const [ search, setSearch ] = useState( '' ); - const debouncedSearch = useDebounce( setSearch, 500 ); - - const suggestions = useMemo( () => { - return Array.from( categoryMap.values() ) - .map( ( category ) => unescapeString( category.label ) ) - .filter( ( category ) => { - if ( search !== '' ) { - return category - .toLowerCase() - .includes( search.toLowerCase() ); - } - return true; - } ) - .sort( ( a, b ) => a.localeCompare( b ) ); - }, [ search, categoryMap ] ); - - function handleChange( termNames ) { - const uniqueTerms = termNames.reduce( ( terms, newTerm ) => { - if ( - ! terms.some( - ( term ) => term.toLowerCase() === newTerm.toLowerCase() - ) - ) { - terms.push( newTerm ); - } - return terms; - }, [] ); - - onChange( uniqueTerms ); - } - const renderTerms = ( renderedTerms ) => { return renderedTerms.map( ( category ) => { return ( @@ -65,22 +23,8 @@ export default function CategorySelector( { > { - if ( categoryTerms.includes( category.label ) ) { - onChange( - categoryTerms.filter( - ( categoryTerm ) => - categoryTerm !== category.label - ) - ); - } else { - onChange( [ - ...categoryTerms, - category.label, - ] ); - } - } } + checked={ isCategorySelected( category ) } + onChange={ () => onChange( category ) } label={ decodeEntities( category.label ) } /> @@ -89,38 +33,19 @@ export default function CategorySelector( { }; return ( - <> - { canAddCategories && ( - - ) } - { ! canAddCategories && categoryOptions.length > 0 && ( - <> - - - { __( 'Categories' ) } - - -
- { renderTerms( categoryOptions ) } -
-
- - ) } - + + + { __( 'Categories' ) } + + +
+ { renderTerms( categoryOptions ) } +
+
); } diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index b1da7166aefa0..67953ecdee668 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -24,7 +24,7 @@ import { PATTERN_DEFAULT_CATEGORY, PATTERN_SYNC_TYPES } from '../constants'; * Internal dependencies */ import { store as patternsStore } from '../store'; -import CategorySelector, { CATEGORY_SLUG } from './category-selector'; +import CategoryEditor, { CATEGORY_SLUG } from './category-editor'; import { unlock } from '../lock-unlock'; export default function CreatePatternModal( { @@ -185,7 +185,7 @@ export default function CreatePatternModal( { placeholder={ __( 'My pattern' ) } className="patterns-create-modal__name-input" /> - Date: Wed, 18 Oct 2023 17:21:46 +0200 Subject: [PATCH 14/22] Share the new category selector with the post editor --- packages/editor/src/components/index.js | 1 + .../pattern-categories-selector.js | 124 ++++++++++++++++++ .../src/components/category-selector.js | 14 +- .../rename-pattern-category-modal.js | 2 +- packages/patterns/src/private-apis.js | 3 + 5 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 packages/editor/src/components/post-taxonomies/pattern-categories-selector.js diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 39b562806c109..99de8c83c6fbe 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -62,6 +62,7 @@ export { export { default as PostTaxonomies } from './post-taxonomies'; export { FlatTermSelector as PostTaxonomiesFlatTermSelector } from './post-taxonomies/flat-term-selector'; export { HierarchicalTermSelector as PostTaxonomiesHierarchicalTermSelector } from './post-taxonomies/hierarchical-term-selector'; +export { PatternCategoriesSelector as PostPatternCategoriesSelector } from './post-taxonomies/pattern-categories-selector'; export { default as PostTaxonomiesCheck } from './post-taxonomies/check'; export { default as PostTextEditor } from './post-text-editor'; export { default as PostTitle } from './post-title'; diff --git a/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js b/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js new file mode 100644 index 0000000000000..2343e5d7fe53c --- /dev/null +++ b/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js @@ -0,0 +1,124 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { addFilter } from '@wordpress/hooks'; +import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { store as editorStore } from '../../store'; + +const { CategorySelector } = unlock( patternsPrivateApis ); + +const EMPTY_ARRAY = []; + +const DEFAULT_QUERY = { + per_page: -1, + orderby: 'name', + order: 'asc', + _fields: 'id,name,parent', + context: 'view', +}; + +/* + * Pattern categories are a flat taxonomy but do not allow Author users and below to create + * new categories, so this selector overrides the default flat taxonomy selector for + * wp_block post types and users without 'create' capability for wp_pattern_category. + */ +export function PatternCategoriesSelector( { slug } ) { + const { hasAssignAction, terms, availableTerms, taxonomy, loading } = + useSelect( + ( select ) => { + const { getCurrentPost, getEditedPostAttribute } = + select( editorStore ); + const { getTaxonomy, getEntityRecords, isResolving } = + select( coreStore ); + const _taxonomy = getTaxonomy( slug ); + const post = getCurrentPost(); + + return { + hasAssignAction: _taxonomy + ? post._links?.[ + 'wp:action-assign-' + _taxonomy.rest_base + ] ?? false + : false, + terms: _taxonomy + ? getEditedPostAttribute( _taxonomy.rest_base ) + : EMPTY_ARRAY, + loading: isResolving( 'getEntityRecords', [ + 'taxonomy', + slug, + DEFAULT_QUERY, + ] ), + availableTerms: + getEntityRecords( 'taxonomy', slug, DEFAULT_QUERY ) || + EMPTY_ARRAY, + taxonomy: _taxonomy, + }; + }, + [ slug ] + ); + + const { editPost } = useDispatch( editorStore ); + + if ( ! hasAssignAction || loading ) { + return null; + } + + /** + * Update terms for post. + * + * @param {number[]} termIds Term ids. + */ + const onUpdateTerms = ( termIds ) => { + editPost( { [ taxonomy.rest_base ]: termIds } ); + }; + + const onChange = ( term ) => { + const hasTerm = terms.includes( term.id ); + const newTerms = hasTerm + ? terms.filter( ( id ) => id !== term.id ) + : [ ...terms, term.id ]; + onUpdateTerms( newTerms ); + }; + + const isCategorySelected = ( term ) => terms.includes( term.id ); + + const categoryOptions = availableTerms.map( ( term ) => ( { + ...term, + label: term.name, + } ) ); + + return ( + + ); +} + +export default function patternCategorySelector( OriginalComponent ) { + return function ( props ) { + const canAddCategories = useSelect( ( select ) => { + const { canUser } = select( coreStore ); + return canUser( 'create', 'wp_pattern_category' ); + } ); + if ( props.slug === 'wp_pattern_category' && ! canAddCategories ) { + return ; + } + + return ; + }; +} + +addFilter( + 'editor.PostTaxonomyType', + 'core/pattern-category-selector', + patternCategorySelector +); diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js index 6ee80a7d4e508..3ea7fd8466419 100644 --- a/packages/patterns/src/components/category-selector.js +++ b/packages/patterns/src/components/category-selector.js @@ -2,17 +2,14 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; - import { CheckboxControl, BaseControl } from '@wordpress/components'; - import { decodeEntities } from '@wordpress/html-entities'; -export const CATEGORY_SLUG = 'wp_pattern_category'; - export default function CategorySelector( { onChange, isCategorySelected, categoryOptions, + showLabel = true, } ) { const renderTerms = ( renderedTerms ) => { return renderedTerms.map( ( category ) => { @@ -34,10 +31,11 @@ export default function CategorySelector( { return ( - - { __( 'Categories' ) } - - + { showLabel && ( + + { __( 'Categories' ) } + + ) }
Date: Wed, 18 Oct 2023 18:08:06 +0200 Subject: [PATCH 15/22] Fix bug with selection not appearing if no categories selected --- .../components/post-taxonomies/pattern-categories-selector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js b/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js index 2343e5d7fe53c..2fa0d53b6b6ea 100644 --- a/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js +++ b/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js @@ -65,7 +65,7 @@ export function PatternCategoriesSelector( { slug } ) { const { editPost } = useDispatch( editorStore ); - if ( ! hasAssignAction || loading ) { + if ( ! hasAssignAction || loading || availableTerms.length === 0 ) { return null; } From 9e3e9bcda235ae1c54e5773cb2ff94fd08129f47 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Thu, 19 Oct 2023 09:50:26 +0200 Subject: [PATCH 16/22] remove unnecessary comment --- .../post-taxonomies/pattern-categories-selector.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js b/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js index 2fa0d53b6b6ea..ac6a60aa00934 100644 --- a/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js +++ b/packages/editor/src/components/post-taxonomies/pattern-categories-selector.js @@ -69,11 +69,6 @@ export function PatternCategoriesSelector( { slug } ) { return null; } - /** - * Update terms for post. - * - * @param {number[]} termIds Term ids. - */ const onUpdateTerms = ( termIds ) => { editPost( { [ taxonomy.rest_base ]: termIds } ); }; From d8c40300352e467c2edef816d9e7a6d0cc89503b Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Thu, 19 Oct 2023 10:26:34 +0200 Subject: [PATCH 17/22] Fix padding of category select list --- packages/patterns/src/components/style.scss | 30 ++++++++++----------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/patterns/src/components/style.scss b/packages/patterns/src/components/style.scss index 286d4fa682a45..409c587c2d678 100644 --- a/packages/patterns/src/components/style.scss +++ b/packages/patterns/src/components/style.scss @@ -26,6 +26,20 @@ border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; } + + .patterns-menu-items__convert-modal__terms-list { + max-height: 6rem; + overflow: auto; + border: 1px solid $gray-600; + padding: 8px; + + .patterns-menu-items__convert-modal__terms-choice { + margin-bottom: $grid-unit-10; + &:last-child { + margin-bottom: 2px; + } + } + } } .patterns-create-modal__name-input input[type="text"] { @@ -43,19 +57,3 @@ display: inline-block; padding: 0; } - -.patterns-menu-items__convert-modal__terms-list { - max-height: 6rem; - overflow: auto; - border: 1px solid $gray-600; - // Extra left padding prevents checkbox focus borders from being cut off. - padding-left: $border-width * 4 + $border-width-focus-fallback; - padding-top: $border-width * 4 + $border-width-focus-fallback; - - .patterns-menu-items__convert-modal__terms-choice { - margin-bottom: $grid-unit-10; - &:last-child { - margin-bottom: $grid-unit-05; - } - } -} From c803d764ef3bd1fb2d769476b88a65beeb42d9bd Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Thu, 19 Oct 2023 10:32:07 +0200 Subject: [PATCH 18/22] Update class names to reflect fact that selector not always in model --- .../src/components/category-selector.js | 4 +-- packages/patterns/src/components/style.scss | 28 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js index 3ea7fd8466419..84dae97e232f8 100644 --- a/packages/patterns/src/components/category-selector.js +++ b/packages/patterns/src/components/category-selector.js @@ -16,7 +16,7 @@ export default function CategorySelector( { return (
) }
Date: Thu, 19 Oct 2023 17:26:00 +0200 Subject: [PATCH 19/22] Fix the taxonomy permissions on the wp_block rest endpoint --- lib/compat/wordpress-6.4/blocks.php | 24 ++++++ ...class-gutenberg-rest-blocks-controller.php | 76 +++++++++++++++++++ lib/load.php | 1 + 3 files changed, 101 insertions(+) create mode 100644 lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller.php diff --git a/lib/compat/wordpress-6.4/blocks.php b/lib/compat/wordpress-6.4/blocks.php index 74fa9253e45d5..073302dbab65f 100644 --- a/lib/compat/wordpress-6.4/blocks.php +++ b/lib/compat/wordpress-6.4/blocks.php @@ -21,3 +21,27 @@ function gutenberg_add_custom_capabilities_to_wp_block( $args ) { return $args; } add_filter( 'register_wp_block_post_type_args', 'gutenberg_add_custom_capabilities_to_wp_block', 10, 1 ); + +/** + * Updates the wp_block REST enpoint in order to modify the wp_pattern_category action + * links that are returned because as although the taxonomy is flat Author level users + * are only allowed to assign categories. + * + * Note: This should be removed when the minimum required WP version is >= 6.4. + * + * @see https://github.com/WordPress/gutenberg/pull/55379 + * + * @param array $args Register post type args. + * @param string $post_type The post type string. + * + * @return array Register post type args. + */ +function gutenberg_update_patterns_block_rest_controller_class( $args, $post_type ) { + if ( 'wp_block' === $post_type ) { + $args['rest_controller_class'] = 'Gutenberg_REST_Blocks_Controller_6_4'; + } + + return $args; +} + +add_filter( 'register_post_type_args', 'gutenberg_update_patterns_block_rest_controller_class', 11, 2 ); diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller.php new file mode 100644 index 0000000000000..62a77effa73ff --- /dev/null +++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller.php @@ -0,0 +1,76 @@ +post_type ); + + if ( 'attachment' !== $this->post_type && current_user_can( $post_type->cap->publish_posts ) ) { + $rels[] = 'https://api.w.org/action-publish'; + } + + if ( current_user_can( 'unfiltered_html' ) ) { + $rels[] = 'https://api.w.org/action-unfiltered-html'; + } + + if ( 'post' === $post_type->name ) { + if ( current_user_can( $post_type->cap->edit_others_posts ) && current_user_can( $post_type->cap->publish_posts ) ) { + $rels[] = 'https://api.w.org/action-sticky'; + } + } + + if ( post_type_supports( $post_type->name, 'author' ) ) { + if ( current_user_can( $post_type->cap->edit_others_posts ) ) { + $rels[] = 'https://api.w.org/action-assign-author'; + } + } + + $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + + foreach ( $taxonomies as $tax ) { + $tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name; + + if ( current_user_can( $tax->cap->edit_terms ) ) { + $rels[] = 'https://api.w.org/action-create-' . $tax_base; + } + + if ( current_user_can( $tax->cap->assign_terms ) ) { + $rels[] = 'https://api.w.org/action-assign-' . $tax_base; + } + } + + return $rels; + } +} diff --git a/lib/load.php b/lib/load.php index cfde63fc7dcbc..e963fd79d89ed 100644 --- a/lib/load.php +++ b/lib/load.php @@ -53,6 +53,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.4 compat. require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php'; require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php'; + require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller.php'; require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php'; require_once __DIR__ . '/compat/wordpress-6.4/rest-api.php'; require_once __DIR__ . '/compat/wordpress-6.4/theme-previews.php'; From 0069a964bd8d0a3d7793c5dc48e491093de1ff30 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Thu, 19 Oct 2023 17:36:30 +0200 Subject: [PATCH 20/22] Fix linting errors --- ...ller.php => class-gutenberg-rest-blocks-controller-6-4.php} | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename lib/compat/wordpress-6.4/{class-gutenberg-rest-blocks-controller.php => class-gutenberg-rest-blocks-controller-6-4.php} (96%) diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php similarity index 96% rename from lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller.php rename to lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php index 62a77effa73ff..bc91492e26979 100644 --- a/lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller.php +++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php @@ -28,7 +28,6 @@ class Gutenberg_REST_Blocks_Controller_6_4 extends Gutenberg_REST_Blocks_Control * @return array List of link relations. */ protected function get_available_actions( $post, $request ) { - if ( 'edit' !== $request['context'] ) { return array(); } @@ -60,7 +59,7 @@ protected function get_available_actions( $post, $request ) { $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); foreach ( $taxonomies as $tax ) { - $tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name; + $tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name; if ( current_user_can( $tax->cap->edit_terms ) ) { $rels[] = 'https://api.w.org/action-create-' . $tax_base; From e02d1e1dce331bc490bcd207d0d2ea295e97b7cf Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Thu, 19 Oct 2023 17:39:24 +0200 Subject: [PATCH 21/22] Fix file load naming --- lib/load.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/load.php b/lib/load.php index e963fd79d89ed..2d1ae1550a3df 100644 --- a/lib/load.php +++ b/lib/load.php @@ -53,7 +53,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.4 compat. require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php'; require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php'; - require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller.php'; + require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php'; require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php'; require_once __DIR__ . '/compat/wordpress-6.4/rest-api.php'; require_once __DIR__ . '/compat/wordpress-6.4/theme-previews.php'; From 2311222dd2eab32ca1efc436764a1aab6a28a7e5 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Fri, 20 Oct 2023 08:52:47 +0200 Subject: [PATCH 22/22] Fix up pointless {} --- packages/patterns/src/components/category-editor.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/patterns/src/components/category-editor.js b/packages/patterns/src/components/category-editor.js index b761f8b1ab745..a394013af333b 100644 --- a/packages/patterns/src/components/category-editor.js +++ b/packages/patterns/src/components/category-editor.js @@ -12,9 +12,7 @@ import { decodeEntities } from '@wordpress/html-entities'; */ import CategorySelector from './category-selector'; -const unescapeString = ( arg ) => { - return decodeEntities( arg ); -}; +const unescapeString = ( arg ) => decodeEntities( arg ); export const CATEGORY_SLUG = 'wp_pattern_category';