diff --git a/lib/compat/wordpress-6.3/block-patterns.php b/lib/compat/wordpress-6.3/block-patterns.php new file mode 100644 index 00000000000000..98c3ada8a6ae3a --- /dev/null +++ b/lib/compat/wordpress-6.3/block-patterns.php @@ -0,0 +1,159 @@ += 6.3. + * + * @see https://github.com/WordPress/gutenberg/pull/51144 + * + * @return void + */ +function gutenberg_register_taxonomy_patterns() { + $labels = array( + 'name' => _x( 'Patterns', 'taxonomy general name' ), + 'singular_name' => _x( 'Pattern', 'taxonomy singular name' ), + 'search_items' => __( 'Search Patterns' ), + 'all_items' => __( 'All Pattern Categories' ), + 'edit_item' => __( 'Edit Pattern Category' ), + 'update_item' => __( 'Update Pattern Category' ), + 'add_new_item' => __( 'Add New Pattern Category' ), + 'new_item_name' => __( 'New Pattern Category Name' ), + 'menu_name' => __( 'Pattern' ), + ); + $args = array( + 'hierarchical' => false, + 'labels' => $labels, + 'show_ui' => true, + 'show_in_menu' => true, + 'show_in_nav_menus' => true, + 'show_admin_column' => true, + 'query_var' => true, + 'show_in_rest' => true, + 'rewrite' => array( 'slug' => 'wp_pattern' ), + ); + register_taxonomy( 'wp_pattern', array( 'wp_block' ), $args ); +} +add_action( 'init', 'gutenberg_register_taxonomy_patterns' ); + +/** + * Add categories to new wp_patterns taxonomy for WP 6.3. + * + * @package gutenberg + */ +function gutenberg_register_wp_patterns_taxonomy_categories() { + $categories = array( + array( + 'slug' => 'banner', + 'label' => _x( 'Banners', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Patterns used for adding banners', 'gutenberg' ), + ), + array( + 'slug' => 'buttons', + 'label' => _x( 'Buttons', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Patterns that contain buttons and call to actions.', 'gutenberg' ), + ), + array( + 'slug' => 'columns', + 'label' => _x( 'Columns', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Multi-column patterns with more complex layouts.', 'gutenberg' ), + ), + array( + 'slug' => 'text', + 'label' => _x( 'Text', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Patterns containing mostly text.', 'gutenberg' ), + ), + array( + 'slug' => 'query', + 'label' => _x( 'Posts', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Display your latest posts in lists, grids or other layouts.', 'gutenberg' ), + ), + array( + 'slug' => 'featured', + 'label' => _x( 'Featured', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'A set of high quality curated patterns.', 'gutenberg' ), + ), + + // Register new core block pattern categories. + array( + 'slug' => 'call-to-action', + 'label' => _x( 'Call to Action', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Sections whose purpose is to trigger a specific action.', 'gutenberg' ), + ), + array( + 'slug' => 'team', + 'label' => _x( 'Team', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'A variety of designs to display your team members.', 'gutenberg' ), + ), + array( + 'slug' => 'testimonials', + 'label' => _x( 'Testimonials', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Share reviews and feedback about your brand/business.', 'gutenberg' ), + ), + array( + 'slug' => 'services', + 'label' => _x( 'Services', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Briefly describe what your business does and how you can help.', 'gutenberg' ), + ), + array( + 'slug' => 'contact', + 'label' => _x( 'Contact', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Display your contact information.', 'gutenberg' ), + ), + array( + 'slug' => 'about', + 'label' => _x( 'About', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Introduce yourself.', 'gutenberg' ), + ), + array( + 'slug' => 'portfolio', + 'label' => _x( 'Portfolio', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Showcase your latest work.', 'gutenberg' ), + ), + array( + 'slug' => 'gallery', + 'label' => _x( 'Gallery', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Different layouts for displaying images.', 'gutenberg' ), + ), + array( + 'slug' => 'media', + 'label' => _x( 'Media', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Different layouts containing video or audio.', 'gutenberg' ), + ), + array( + 'slug' => 'posts', + 'label' => _x( 'Posts', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Display your latest posts in lists, grids or other layouts.', 'gutenberg' ), + ), + // Site building pattern categories. + array( + 'slug' => 'footer', + 'label' => _x( 'Footers', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'A variety of footer designs displaying information and site navigation.', 'gutenberg' ), + ), + array( + 'slug' => 'header', + 'label' => _x( 'Headers', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'A variety of header designs displaying your site title and navigation.', 'gutenberg' ), + ), + ); + + foreach ( $categories as $category ) { + if ( empty( term_exists( $category['slug'], 'wp_pattern' ) ) ) { + wp_insert_term( + $category['label'], + 'wp_pattern', + array( + 'slug' => $category['slug'], + 'description' => $category['description'], + ) + ); + } + } +} +add_action( 'init', 'gutenberg_register_wp_patterns_taxonomy_categories', 20 ); diff --git a/lib/compat/wordpress-6.3/blocks.php b/lib/compat/wordpress-6.3/blocks.php index 5f017997f52b58..6c98e14c816069 100644 --- a/lib/compat/wordpress-6.3/blocks.php +++ b/lib/compat/wordpress-6.3/blocks.php @@ -26,3 +26,80 @@ function gutenberg_add_selectors_property_to_block_type_settings( $settings, $me return $settings; } add_filter( 'block_type_metadata_settings', 'gutenberg_add_selectors_property_to_block_type_settings', 10, 2 ); + +/** + * Adds custom fields support to the wp_block post type so a partial and unsynced option can be added. + * + * Note: This should be removed when the minimum required WP version is >= 6.3. + * + * @see https://github.com/WordPress/gutenberg/pull/51144 + * + * @param array $args Register post type args. + * @param string $post_type The post type string. + * + * @return array Register post type args. + */ +function gutenberg_add_custom_fields_to_wp_block( $args, $post_type ) { + + if ( 'wp_block' === $post_type ) { + array_push( $args['supports'], 'custom-fields' ); + } + + return $args; +} +add_filter( 'register_post_type_args', 'gutenberg_add_custom_fields_to_wp_block', 10, 2 ); + +/** + * Adds wp_block_sync_status and wp_block_category_name meta fields to the wp_block post type so a partial and unsynced option can be added. + * + * Note: This should be removed when the minimum required WP version is >= 6.3. + * + * @see https://github.com/WordPress/gutenberg/pull/51144 + * + * @return void + */ +function gutenberg_wp_block_register_post_meta() { + $post_type = 'wp_block'; + register_post_meta( + $post_type, + 'wp_block', + array( + 'auth_callback' => function() { + return current_user_can( 'edit_posts' ); + }, + 'sanitize_callback' => 'gutenberg_wp_block_sanitize_post_meta', + 'single' => true, + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'sync_status' => array( + 'type' => 'string', + ), + 'slug' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); +} +/** + * Sanitizes the array of wp_block post meta categories array. + * + * Note: This should be removed when the minimum required WP version is >= 6.3. + * + * @see https://github.com/WordPress/gutenberg/pull/51144 + * + * @param array $meta_value Array of values to sanitize. + * + * @return array Sanitized array of values. + */ +function gutenberg_wp_block_sanitize_post_meta( $meta_value ) { + $meta_value['sync_status'] = sanitize_text_field( $meta_value['sync_status'] ); + $meta_value['slug'] = sanitize_text_field( $meta_value['slug'] ); + return $meta_value; +} +add_action( 'init', 'gutenberg_wp_block_register_post_meta' ); diff --git a/lib/load.php b/lib/load.php index b0ed333185ef6a..c62be5408c299c 100644 --- a/lib/load.php +++ b/lib/load.php @@ -52,6 +52,7 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/compat/wordpress-6.3/navigation-block-preloading.php'; require_once __DIR__ . '/compat/wordpress-6.3/link-template.php'; require_once __DIR__ . '/compat/wordpress-6.3/behaviors.php'; + require_once __DIR__ . '/compat/wordpress-6.3/block-patterns.php'; // Experimental. if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) { diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index fcba45450ea5e9..94586f6d60894d 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -29,6 +29,7 @@ import { } from '@wordpress/block-editor'; import { store as reusableBlocksStore } from '@wordpress/reusable-blocks'; import { ungroup } from '@wordpress/icons'; +import { cloneBlock } from '@wordpress/blocks'; export default function ReusableBlockEdit( { attributes: { ref }, clientId } ) { const hasAlreadyRendered = useHasRecursion( ref ); @@ -54,11 +55,25 @@ export default function ReusableBlockEdit( { attributes: { ref }, clientId } ) { const { __experimentalConvertBlockToStatic: convertBlockToStatic } = useDispatch( reusableBlocksStore ); + const { replaceBlocks } = useDispatch( blockEditorStore ); + const [ blocks, onInput, onChange ] = useEntityBlockEditor( 'postType', 'wp_block', { id: ref } ); + + if ( + hasResolved && + record?.meta?.wp_block?.sync_status === 'notSynced' && + blocks?.length > 0 + ) { + replaceBlocks( + clientId, + blocks.map( ( block ) => cloneBlock( block ) ) + ); + } + const [ title, setTitle ] = useEntityProp( 'postType', 'wp_block', diff --git a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-convert-button.js b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-convert-button.js index 18824b892544a8..cb1e8716cc3a2c 100644 --- a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-convert-button.js +++ b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-convert-button.js @@ -14,12 +14,13 @@ import { TextControl, __experimentalHStack as HStack, __experimentalVStack as VStack, + SelectControl, } from '@wordpress/components'; import { symbol } from '@wordpress/icons'; import { useDispatch, useSelect } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; -import { store as coreStore } from '@wordpress/core-data'; +import { store as coreStore, useEntityRecords } from '@wordpress/core-data'; /** * Internal dependencies @@ -38,11 +39,22 @@ export default function ReusableBlockConvertButton( { clientIds, rootClientId, } ) { + const query = { per_page: -1, hide_empty: false, context: 'view' }; + + const { records: categories } = useEntityRecords( + 'taxonomy', + 'wp_pattern', + query + ); + const [ isModalOpen, setIsModalOpen ] = useState( false ); + const [ blockType, setBlockType ] = useState( 'reusable' ); const [ title, setTitle ] = useState( '' ); - const canConvert = useSelect( + const [ categoryId, setCategoryId ] = useState( '' ); + const { canConvert } = useSelect( ( select ) => { const { canUser } = select( coreStore ); + const { getBlocksByClientId, canInsertBlockType } = select( blockEditorStore ); @@ -75,7 +87,9 @@ export default function ReusableBlockConvertButton( { // Hide when current doesn't have permission to do that. !! canUser( 'create', 'blocks' ); - return _canConvert; + return { + canConvert: _canConvert, + }; }, [ clientIds ] ); @@ -88,22 +102,52 @@ export default function ReusableBlockConvertButton( { const onConvert = useCallback( async function ( reusableBlockTitle ) { try { - await convertBlocksToReusable( clientIds, reusableBlockTitle ); - createSuccessNotice( __( 'Reusable block created.' ), { - type: 'snackbar', - } ); + await convertBlocksToReusable( + clientIds, + reusableBlockTitle, + blockType, + categoryId + ); + createSuccessNotice( + sprintf( + // translators: %s: Type of block (i.e. Reusable or Pattern). + __( '%s created.' ), + blockType === 'reusable' + ? __( 'Reusable block' ) + : __( 'Pattern' ) + ), + { + type: 'snackbar', + } + ); } catch ( error ) { createErrorNotice( error.message, { type: 'snackbar', } ); } }, - [ clientIds ] + [ + convertBlocksToReusable, + clientIds, + blockType, + categoryId, + createSuccessNotice, + createErrorNotice, + ] ); if ( ! canConvert ) { return null; } + const patternCategories = categories === null ? [] : categories; + const categoryOptions = patternCategories + .map( ( category ) => ( { + label: category.name, + value: category.id, + } ) ) + .concat( [ + { value: '', label: __( 'Select a category' ), disabled: true }, + ] ); return ( @@ -112,14 +156,30 @@ export default function ReusableBlockConvertButton( { { + setBlockType( 'reusable' ); setIsModalOpen( true ); } } > { __( 'Create Reusable block' ) } + { + setBlockType( 'pattern' ); + setIsModalOpen( true ); + } } + > + { __( 'Create a Pattern' ) } + { isModalOpen && ( { setIsModalOpen( false ); setTitle( '' ); @@ -142,6 +202,15 @@ export default function ReusableBlockConvertButton( { value={ title } onChange={ setTitle } /> + { blockType === 'pattern' && ( + + ) }