From 2ba5e3afdb78dc9c2103df54f17b7abbe16d60d2 Mon Sep 17 00:00:00 2001 From: Sarah Norris <1645628+mikachan@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:14:09 +0100 Subject: [PATCH] Try: Add synced patterns to theme on save (#675) * Add savePatterns option * Add basic add_patterns_to_theme function * Switch order of save options * Add prepare_pattern_for_export function * Copy media to theme filesystem * Fix pattern slug * Add replace_local_pattern_references * Tidy up pattern_from_wp_block * Add pattern categories * Default to empty string for categories list * Potentially save pattern sync status * Refactor PHP content of pattern_from_template Co-Authored-By: Elliott Richmond * Refactor PHP content of pattern_from_wp_block Co-Authored-By: Elliott Richmond * Delete synced patterns after adding to theme Co-Authored-By: Elliott Richmond * Refactor and add error handling * Update save patterns option description * Remove pattern- prefix * Update option description * Redirect to patterns page if editing a pattern * Add check for preference.savePatterns * Update templates that reference the pattern --------- Co-authored-by: Elliott Richmond --- includes/class-create-block-theme-api.php | 8 + includes/create-theme/theme-patterns.php | 175 ++++++++++++++++++++-- includes/create-theme/theme-templates.php | 13 +- src/editor-sidebar/save-panel.js | 55 +++++-- 4 files changed, 226 insertions(+), 25 deletions(-) diff --git a/includes/class-create-block-theme-api.php b/includes/class-create-block-theme-api.php index 1593ebe0..9b9dcb4e 100644 --- a/includes/class-create-block-theme-api.php +++ b/includes/class-create-block-theme-api.php @@ -394,6 +394,14 @@ function rest_save_theme( $request ) { CBT_Theme_Styles::clear_user_styles_customizations(); } + if ( isset( $options['savePatterns'] ) && true === $options['savePatterns'] ) { + $response = CBT_Theme_Patterns::add_patterns_to_theme( $options ); + + if ( is_wp_error( $response ) ) { + return $response; + } + } + wp_get_theme()->cache_delete(); return new WP_REST_Response( diff --git a/includes/create-theme/theme-patterns.php b/includes/create-theme/theme-patterns.php index 23224c2d..0f9ee96f 100644 --- a/includes/create-theme/theme-patterns.php +++ b/includes/create-theme/theme-patterns.php @@ -4,23 +4,47 @@ class CBT_Theme_Patterns { public static function pattern_from_template( $template, $new_slug = null ) { $theme_slug = $new_slug ? $new_slug : wp_get_theme()->get( 'TextDomain' ); $pattern_slug = $theme_slug . '/' . $template->slug; - $pattern_content = ( - 'slug . ' - * Slug: ' . $pattern_slug . ' - * Categories: hidden - * Inserter: no - */ -?> -' . $template->content - ); + $pattern_content = <<slug} + * Slug: {$pattern_slug} + * Categories: hidden + * Inserter: no + */ + ?> + {$template->content} + PHP; + return array( 'slug' => $pattern_slug, 'content' => $pattern_content, ); } + public static function pattern_from_wp_block( $pattern_post ) { + $pattern = new stdClass(); + $pattern->id = $pattern_post->ID; + $pattern->title = $pattern_post->post_title; + $pattern->name = sanitize_title_with_dashes( $pattern_post->post_title ); + $pattern->slug = wp_get_theme()->get( 'TextDomain' ) . '/' . $pattern->name; + $pattern_category_list = get_the_terms( $pattern->id, 'wp_pattern_category' ); + $pattern->categories = ! empty( $pattern_category_list ) ? join( ', ', wp_list_pluck( $pattern_category_list, 'name' ) ) : ''; + $pattern->sync_status = get_post_meta( $pattern->id, 'wp_pattern_sync_status', true ); + $pattern->content = <<title} + * Slug: {$pattern->slug} + * Categories: {$pattern->categories} + */ + ?> + {$pattern_post->post_content} + PHP; + + return $pattern; + } + public static function escape_alt_for_pattern( $html ) { if ( empty( $html ) ) { return $html; @@ -47,4 +71,133 @@ public static function create_pattern_link( $attributes ) { $attributes_json = json_encode( $block_attributes, JSON_UNESCAPED_SLASHES ); return ''; } + + public static function replace_local_pattern_references( $pattern ) { + // Find any references to pattern in templates + $templates_to_update = array(); + $args = array( + 'post_type' => array( 'wp_template', 'wp_template_part' ), + 'posts_per_page' => -1, + 's' => 'wp:block {"ref":' . $pattern->id . '}', + ); + $find_pattern_refs = new WP_Query( $args ); + if ( $find_pattern_refs->have_posts() ) { + foreach ( $find_pattern_refs->posts as $post ) { + $slug = $post->post_name; + array_push( $templates_to_update, $slug ); + } + } + $templates_to_update = array_unique( $templates_to_update ); + + // Only update templates that reference the pattern + CBT_Theme_Templates::add_templates_to_local( 'all', null, null, $options, $templates_to_update ); + + // List all template and pattern files in the theme + $base_dir = get_stylesheet_directory(); + $patterns = glob( $base_dir . DIRECTORY_SEPARATOR . 'patterns' . DIRECTORY_SEPARATOR . '*.php' ); + $templates = glob( $base_dir . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . '*.html' ); + $template_parts = glob( $base_dir . DIRECTORY_SEPARATOR . 'template-parts' . DIRECTORY_SEPARATOR . '*.html' ); + + // Replace references to the local patterns in the theme + foreach ( array_merge( $patterns, $templates, $template_parts ) as $file ) { + $file_content = file_get_contents( $file ); + $file_content = str_replace( 'wp:block {"ref":' . $pattern->id . '}', 'wp:pattern {"slug":"' . $pattern->slug . '"}', $file_content ); + file_put_contents( $file, $file_content ); + } + + CBT_Theme_Templates::clear_user_templates_customizations(); + CBT_Theme_Templates::clear_user_template_parts_customizations(); + } + + public static function prepare_pattern_for_export( $pattern, $options = null ) { + if ( ! $options ) { + $options = array( + 'localizeText' => false, + 'removeNavRefs' => true, + 'localizeImages' => true, + ); + } + + $pattern = CBT_Theme_Templates::eliminate_environment_specific_content( $pattern, $options ); + + if ( array_key_exists( 'localizeText', $options ) && $options['localizeText'] ) { + $pattern = CBT_Theme_Templates::escape_text_in_template( $pattern ); + } + + if ( array_key_exists( 'localizeImages', $options ) && $options['localizeImages'] ) { + $pattern = CBT_Theme_Media::make_template_images_local( $pattern ); + + // Write the media assets if there are any + if ( $pattern->media ) { + CBT_Theme_Media::add_media_to_local( $pattern->media ); + } + } + + return $pattern; + } + + /** + * Copy the local patterns as well as any media to the theme filesystem. + */ + public static function add_patterns_to_theme( $options = null ) { + $base_dir = get_stylesheet_directory(); + $patterns_dir = $base_dir . DIRECTORY_SEPARATOR . 'patterns'; + + $pattern_query = new WP_Query( + array( + 'post_type' => 'wp_block', + 'posts_per_page' => -1, + ) + ); + + if ( $pattern_query->have_posts() ) { + // If there is no patterns folder, create it. + if ( ! is_dir( $patterns_dir ) ) { + wp_mkdir_p( $patterns_dir ); + } + + foreach ( $pattern_query->posts as $pattern ) { + $pattern = self::pattern_from_wp_block( $pattern ); + $pattern = self::prepare_pattern_for_export( $pattern, $options ); + $pattern_exists = false; + + // Check pattern is synced before adding to theme. + if ( 'unsynced' !== $pattern->sync_status ) { + // Check pattern name doesn't already exist before creating the file. + $existing_patterns = glob( $patterns_dir . DIRECTORY_SEPARATOR . '*.php' ); + foreach ( $existing_patterns as $existing_pattern ) { + if ( strpos( $existing_pattern, $pattern->name . '.php' ) !== false ) { + $pattern_exists = true; + } + } + + if ( $pattern_exists ) { + return new WP_Error( + 'pattern_already_exists', + sprintf( + /* Translators: Pattern name. */ + __( + 'A pattern with this name already exists: "%s".', + 'create-block-theme' + ), + $pattern->name + ) + ); + } + + // Create the pattern file. + $pattern_file = $patterns_dir . $pattern->name . '.php'; + file_put_contents( + $patterns_dir . DIRECTORY_SEPARATOR . $pattern->name . '.php', + $pattern->content + ); + + self::replace_local_pattern_references( $pattern ); + + // Remove it from the database to ensure that these patterns are loaded from the theme. + wp_delete_post( $pattern->id, true ); + } + } + } + } } diff --git a/includes/create-theme/theme-templates.php b/includes/create-theme/theme-templates.php index 7b1ea1bf..7837b124 100644 --- a/includes/create-theme/theme-templates.php +++ b/includes/create-theme/theme-templates.php @@ -10,12 +10,13 @@ class CBT_Theme_Templates { * based on the given export_type. * * @param string $export_type The type of export to perform. 'all', 'current', or 'user'. + * @param array $templates_to_export List of specific templates to export. * @return object An object containing the templates and parts that should be exported. */ - public static function get_theme_templates( $export_type ) { + public static function get_theme_templates( $export_type, $templates_to_export = null ) { - $templates = get_block_templates(); - $template_parts = get_block_templates( array(), 'wp_template_part' ); + $templates = get_block_templates( array( 'slug__in' => $templates_to_export ) ); + $template_parts = get_block_templates( array( 'slug__in' => $templates_to_export ), 'wp_template_part' ); $exported_templates = array(); $exported_parts = array(); @@ -195,10 +196,12 @@ public static function prepare_template_for_export( $template, $slug = null, $op * @param string $export_type The type of export to perform. 'all', 'current', or 'user'. * @param string $path The path to the theme folder. If null it is assumed to be the current theme. * @param string $slug The slug of the theme. If null it is assumed to be the current theme. + * @param array $options An array of options to use when exporting the templates. + * @param array $templates_to_export List of specific templates to export. If null it will be fetched. */ - public static function add_templates_to_local( $export_type, $path = null, $slug = null, $options = null ) { + public static function add_templates_to_local( $export_type, $path = null, $slug = null, $options = null, $templates_to_export = null ) { - $theme_templates = self::get_theme_templates( $export_type ); + $theme_templates = self::get_theme_templates( $export_type, $templates_to_export ); $template_folders = get_block_theme_folders(); $base_dir = $path ? $path : get_stylesheet_directory(); diff --git a/src/editor-sidebar/save-panel.js b/src/editor-sidebar/save-panel.js index 116d04a9..8266f91e 100644 --- a/src/editor-sidebar/save-panel.js +++ b/src/editor-sidebar/save-panel.js @@ -34,6 +34,7 @@ export const SaveThemePanel = () => { saveTemplates: _preference?.saveTemplates ?? true, processOnlySavedTemplates: _preference?.processOnlySavedTemplates ?? true, + savePatterns: _preference?.savePatterns ?? true, saveFonts: _preference?.saveFonts ?? true, removeNavRefs: _preference?.removeNavRefs ?? false, localizeText: _preference?.localizeText ?? false, @@ -68,7 +69,22 @@ export const SaveThemePanel = () => { 'create-block-theme' ) ); - window.location.reload(); + + const searchParams = new URLSearchParams( + window?.location?.search + ); + // If user is editing a pattern and savePatterns is true, redirect back to the patterns page. + if ( + preference.savePatterns && + searchParams.get( 'postType' ) === 'wp_block' && + searchParams.get( 'postId' ) + ) { + window.location = + '/wp-admin/site-editor.php?postType=wp_block'; + } else { + // If user is not editing a pattern, reload the editor. + window.location.reload(); + } } ) .catch( ( error ) => { const errorMessage = @@ -135,27 +151,44 @@ export const SaveThemePanel = () => { handleTogglePreference( 'processOnlySavedTemplates' ) } /> + handleTogglePreference( 'savePatterns' ) } + /> handleTogglePreference( 'localizeText' ) } /> handleTogglePreference( 'localizeImages' ) @@ -170,9 +203,13 @@ export const SaveThemePanel = () => { 'Remove Navigation Refs from the theme returning your navigation to the default state.', 'create-block-theme' ) } - disabled={ ! preference.saveTemplates } + disabled={ + ! preference.saveTemplates && ! preference.savePatterns + } checked={ - preference.saveTemplates && preference.removeNavRefs + ( preference.saveTemplates || + preference.savePatterns ) && + preference.removeNavRefs } onChange={ () => handleTogglePreference( 'removeNavRefs' ) } />