-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Allow template duplication + concept of active templates #67125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c1c5111
1c55ea9
07fcf91
75f8056
e35f93c
af44163
64873fa
adc86c7
0d3d192
bce1d83
a3e799b
4a569e4
b849489
cf33029
d9d4c16
f1941b3
e8beeea
daa1412
fa55d1e
fa8761d
435b22c
5d7e2a9
31041bb
c4e30f4
487c124
efa4291
f03a90d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| https://github.com/WordPress/wordpress-develop/pull/8063 | ||
|
|
||
| * https://github.com/WordPress/gutenberg/pull/67125 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| <?php | ||
|
|
||
| class Gutenberg_REST_Static_Templates_Controller extends WP_REST_Templates_Controller { | ||
| public function register_routes() { | ||
| // Lists all templates. | ||
| register_rest_route( | ||
| $this->namespace, | ||
| '/' . $this->rest_base, | ||
| array( | ||
| array( | ||
| 'methods' => WP_REST_Server::READABLE, | ||
| 'callback' => array( $this, 'get_items' ), | ||
| 'permission_callback' => array( $this, 'get_items_permissions_check' ), | ||
| 'args' => $this->get_collection_params(), | ||
| ), | ||
| 'schema' => array( $this, 'get_public_item_schema' ), | ||
| ) | ||
| ); | ||
|
|
||
| // Lists/updates a single template based on the given id. | ||
| register_rest_route( | ||
| $this->namespace, | ||
| // The route. | ||
| sprintf( | ||
| '/%s/(?P<id>%s%s)', | ||
| $this->rest_base, | ||
| /* | ||
| * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`. | ||
| * Excludes invalid directory name characters: `/:<>*?"|`. | ||
| */ | ||
| '([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', | ||
| // Matches the template name. | ||
| '[\/\w%-]+' | ||
| ), | ||
| array( | ||
| 'args' => array( | ||
| 'id' => array( | ||
| 'description' => __( 'The id of a template' ), | ||
| 'type' => 'string', | ||
| 'sanitize_callback' => array( $this, '_sanitize_template_id' ), | ||
| ), | ||
| ), | ||
| array( | ||
| 'methods' => WP_REST_Server::READABLE, | ||
| 'callback' => array( $this, 'get_item' ), | ||
| 'permission_callback' => array( $this, 'get_item_permissions_check' ), | ||
| 'args' => array( | ||
| 'context' => $this->get_context_param( array( 'default' => 'view' ) ), | ||
| ), | ||
| ), | ||
| 'schema' => array( $this, 'get_public_item_schema' ), | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| public function get_item_schema() { | ||
| $schema = parent::get_item_schema(); | ||
| $schema['properties']['is_custom'] = array( | ||
| 'description' => __( 'Whether a template is a custom template.' ), | ||
| 'type' => 'bool', | ||
| 'context' => array( 'embed', 'view', 'edit' ), | ||
| 'readonly' => true, | ||
| ); | ||
| $schema['properties']['plugin'] = array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'Plugin that registered the template.' ), | ||
| 'readonly' => true, | ||
| 'context' => array( 'view', 'edit', 'embed' ), | ||
| ); | ||
| return $schema; | ||
| } | ||
|
|
||
| public function get_items( $request ) { | ||
| $query = array(); | ||
| if ( isset( $request['area'] ) ) { | ||
| $query['area'] = $request['area']; | ||
| } | ||
| if ( isset( $request['post_type'] ) ) { | ||
| $query['post_type'] = $request['post_type']; | ||
| } | ||
| $template_files = _get_block_templates_files( 'wp_template', $query ); | ||
| $query_result = array(); | ||
| foreach ( $template_files as $template_file ) { | ||
| $query_result[] = _build_block_template_result_from_file( $template_file, 'wp_template' ); | ||
| } | ||
|
|
||
| // Add templates registered in the template registry. Filtering out the ones which have a theme file. | ||
| $registered_templates = WP_Block_Templates_Registry::get_instance()->get_by_query( $query ); | ||
| $matching_registered_templates = array_filter( | ||
| $registered_templates, | ||
| function ( $registered_template ) use ( $template_files ) { | ||
| foreach ( $template_files as $template_file ) { | ||
| if ( $template_file['slug'] === $registered_template->slug ) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| ); | ||
|
|
||
| $query_result = array_merge( $query_result, $matching_registered_templates ); | ||
|
|
||
| /** | ||
| * Filters the array of queried block templates array after they've been fetched. | ||
| * | ||
| * @since 5.9.0 | ||
| * | ||
| * @param WP_Block_Template[] $query_result Array of found block templates. | ||
| * @param array $query { | ||
| * Arguments to retrieve templates. All arguments are optional. | ||
| * | ||
| * @type string[] $slug__in List of slugs to include. | ||
| * @type int $wp_id Post ID of customized template. | ||
| * @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only). | ||
| * @type string $post_type Post type to get the templates for. | ||
| * } | ||
| * @param string $template_type wp_template or wp_template_part. | ||
| */ | ||
| $query_result = apply_filters( 'get_block_templates', $query_result, $query, 'wp_template' ); | ||
|
|
||
| $templates = array(); | ||
| foreach ( $query_result as $template ) { | ||
| $item = $this->prepare_item_for_response( $template, $request ); | ||
| $item->data['type'] = 'wp_registered_template'; | ||
| $templates[] = $this->prepare_response_for_collection( $item ); | ||
| } | ||
|
|
||
| return rest_ensure_response( $templates ); | ||
| } | ||
|
|
||
| public function get_item( $request ) { | ||
| $template = get_block_file_template( $request['id'], 'wp_template' ); | ||
|
|
||
| if ( ! $template ) { | ||
| return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) ); | ||
| } | ||
|
|
||
| $item = $this->prepare_item_for_response( $template, $request ); | ||
| // adjust the template type here instead | ||
| $item->data['type'] = 'wp_registered_template'; | ||
| return rest_ensure_response( $item ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <?php | ||
|
|
||
| class Gutenberg_REST_Templates_Controller extends WP_REST_Posts_Controller { | ||
| protected function handle_status_param( $status, $request ) { | ||
| if ( 'auto-draft' === $status ) { | ||
| return $status; | ||
| } | ||
| return parent::handle_status_param( $status, $request ); | ||
| } | ||
| protected function add_additional_fields_schema( $schema ) { | ||
| $schema = parent::add_additional_fields_schema( $schema ); | ||
|
|
||
| $schema['properties']['status']['enum'][] = 'auto-draft'; | ||
| return $schema; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * Preload necessary resources for the editors. | ||
| * | ||
| * @param array $paths REST API paths to preload. | ||
| * @param WP_Block_Editor_Context $context Current block editor context | ||
| * | ||
| * @return array Filtered preload paths. | ||
| */ | ||
| function gutenberg_block_editor_preload_paths_6_9( $paths, $context ) { | ||
| if ( 'core/edit-site' === $context->name ) { | ||
| // Only prefetch for the root. If we preload it for all pages and it's not used | ||
| // it won't be possible to invalidate. | ||
| // To do: perhaps purge all preloaded paths when client side navigating. | ||
| if ( '/' !== $_GET['p'] ) { | ||
| $paths = array_filter( | ||
| $paths, | ||
| function ( $path ) { | ||
| return '/wp/v2/templates/lookup?slug=front-page' !== $path && '/wp/v2/templates/lookup?slug=home' !== $path; | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| $paths[] = '/wp/v2/wp_registered_template?context=edit'; | ||
| } | ||
|
|
||
| return $paths; | ||
| } | ||
| add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_6_9', 10, 2 ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| <?php | ||
|
|
||
| // How does this work? | ||
| // 1. For wp_template, we remove the custom templates controller, so it becomes | ||
| // a normal posts endpoint, modified slightly to allow auto-drafts. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is the right thing to do but I'm a bit concerned about potential breaking changes.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The part about changing it to a normal posts endpoint? That's valid. The alternative would be to create a different CPT or endpoint 🤔
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm just wondering if the endpoint is going to continue working the exact same way or not? Do we have some fallbacks in place when previous "slug ids" are used?. I think a good summary of the endpoints changes would be good.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Came here to ask the same 🙇
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add a fallback for a slug id in a follow-up as discussed. Right there should be a summary. The main change is that whatever imitation of the post type endpoint there was for |
||
| add_filter( 'register_post_type_args', 'gutenberg_modify_wp_template_post_type_args', 10, 2 ); | ||
| function gutenberg_modify_wp_template_post_type_args( $args, $post_type ) { | ||
| if ( 'wp_template' === $post_type ) { | ||
| $args['rest_base'] = 'wp_template'; | ||
| $args['rest_controller_class'] = 'Gutenberg_REST_Templates_Controller'; | ||
| $args['autosave_rest_controller_class'] = null; | ||
| $args['revisions_rest_controller_class'] = null; | ||
| } | ||
| return $args; | ||
| } | ||
|
|
||
| // 2. We maintain the routes for /templates and /templates/lookup. I think we'll | ||
| // need to deprecate /templates eventually, but we'll still want to be able | ||
| // to lookup the active template for a specific slug, and probably get a list | ||
| // of all _active_ templates. For that we can keep /lookup. | ||
| add_action( 'rest_api_init', 'gutenberg_maintain_templates_routes' ); | ||
| function gutenberg_maintain_templates_routes() { | ||
| // This should later be changed in core so we don't need initialise | ||
| // WP_REST_Templates_Controller with a post type. | ||
| global $wp_post_types; | ||
| $wp_post_types['wp_template']->rest_base = 'templates'; | ||
| $controller = new WP_REST_Templates_Controller( 'wp_template' ); | ||
| $wp_post_types['wp_template']->rest_base = 'wp_template'; | ||
| $controller->register_routes(); | ||
| } | ||
|
|
||
| // 3. We need a route to get that raw static templates from themes and plugins. | ||
| // I registered this as a post type route because right now the | ||
| // EditorProvider assumes templates are posts. | ||
| add_action( 'init', 'gutenberg_setup_static_template' ); | ||
| function gutenberg_setup_static_template() { | ||
| global $wp_post_types; | ||
| $wp_post_types['wp_registered_template'] = clone $wp_post_types['wp_template']; | ||
| $wp_post_types['wp_registered_template']->name = 'wp_registered_template'; | ||
| $wp_post_types['wp_registered_template']->rest_base = 'wp_registered_template'; | ||
| $wp_post_types['wp_registered_template']->rest_controller_class = 'Gutenberg_REST_Static_Templates_Controller'; | ||
|
|
||
| register_setting( | ||
| 'reading', | ||
| 'active_templates', | ||
| array( | ||
| 'type' => 'object', | ||
| 'show_in_rest' => array( | ||
| 'schema' => array( | ||
| 'type' => 'object', | ||
| // properties can be integers or false (deactivated). | ||
| 'additionalProperties' => true, | ||
| ), | ||
| ), | ||
| 'default' => array(), | ||
| 'label' => 'Active Templates', | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| add_filter( 'pre_wp_unique_post_slug', 'gutenberg_allow_template_slugs_to_be_duplicated', 10, 5 ); | ||
| function gutenberg_allow_template_slugs_to_be_duplicated( $override, $slug, $post_id, $post_status, $post_type ) { | ||
| return 'wp_template' === $post_type ? $slug : $override; | ||
| } | ||
|
|
||
| add_filter( 'pre_get_block_templates', 'gutenberg_pre_get_block_templates', 10, 3 ); | ||
| function gutenberg_pre_get_block_templates( $output, $query, $template_type ) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we have some comments just to clarify what these overrides do.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was removed btw, the filtering is way less hacky |
||
| if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) { | ||
| $active_templates = (array) get_option( 'active_templates', array() ); | ||
| $slugs = $query['slug__in']; | ||
| $output = array(); | ||
| foreach ( $slugs as $slug ) { | ||
| if ( isset( $active_templates[ $slug ] ) ) { | ||
| if ( false !== $active_templates[ $slug ] ) { | ||
| $post = get_post( $active_templates[ $slug ] ); | ||
| if ( $post && 'publish' === $post->post_status ) { | ||
| $output[] = _build_block_template_result_from_post( $post ); | ||
| } | ||
| } else { | ||
| // Deactivated template, fall back to next slug. | ||
| $output[] = array(); | ||
| } | ||
| } | ||
| } | ||
| if ( empty( $output ) ) { | ||
| $output = null; | ||
| } | ||
| } | ||
| return $output; | ||
| } | ||
|
|
||
| // Whenever templates are queried by slug, never return any user templates. | ||
| // We are handling that in gutenberg_pre_get_block_templates. | ||
| function gutenberg_remove_tax_query_for_templates( $query ) { | ||
| if ( isset( $query->query['post_type'] ) && 'wp_template' === $query->query['post_type'] ) { | ||
| // We don't have templates with this status, that's the point. We want | ||
| // this query to not return any user templates. | ||
| $query->set( 'post_status', array( 'pending' ) ); | ||
| } | ||
| } | ||
|
|
||
| add_filter( 'pre_get_block_templates', 'gutenberg_tax_pre_get_block_templates', 10, 3 ); | ||
| function gutenberg_tax_pre_get_block_templates( $output, $query, $template_type ) { | ||
| // Do not remove the tax query when querying for a specific slug. | ||
| if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) { | ||
| add_action( 'pre_get_posts', 'gutenberg_remove_tax_query_for_templates' ); | ||
| } | ||
| return $output; | ||
| } | ||
|
|
||
| add_filter( 'get_block_templates', 'gutenberg_tax_get_block_templates', 10, 3 ); | ||
| function gutenberg_tax_get_block_templates( $output, $query, $template_type ) { | ||
| if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) { | ||
| remove_action( 'pre_get_posts', 'gutenberg_remove_tax_query_for_templates' ); | ||
| } | ||
| return $output; | ||
| } | ||
|
|
||
| // We need to set the theme for the template when it's created. See: | ||
| // https://github.com/WordPress/wordpress-develop/blob/b2c8d8d2c8754cab5286b06efb4c11e2b6aa92d5/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php#L571-L578 | ||
| // Priority 9 so it runs before default hooks like | ||
| // `inject_ignored_hooked_blocks_metadata_attributes`. | ||
| add_action( 'rest_pre_insert_wp_template', 'gutenberg_set_active_template_theme', 9, 2 ); | ||
| function gutenberg_set_active_template_theme( $changes, $request ) { | ||
| $template = $request['id'] ? get_block_template( $request['id'], 'wp_template' ) : null; | ||
| if ( $template ) { | ||
| return $changes; | ||
| } | ||
| $changes->tax_input = array( | ||
| 'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : get_stylesheet(), | ||
| ); | ||
| return $changes; | ||
| } | ||
|
|
||
| // Migrate existing "edited" templates. By existing, it means that the template | ||
| // is active. | ||
| add_action( 'init', 'gutenberg_migrate_existing_templates' ); | ||
| function gutenberg_migrate_existing_templates() { | ||
| $active_templates = get_option( 'active_templates' ); | ||
|
|
||
| if ( $active_templates ) { | ||
| return; | ||
| } | ||
|
|
||
| // Query all templates in the database. See `get_block_templates`. | ||
| $wp_query_args = array( | ||
| 'post_status' => 'publish', | ||
| 'post_type' => 'wp_template', | ||
| 'posts_per_page' => -1, | ||
| 'no_found_rows' => true, | ||
| 'lazy_load_term_meta' => false, | ||
| 'tax_query' => array( | ||
| array( | ||
| 'taxonomy' => 'wp_theme', | ||
| 'field' => 'name', | ||
| 'terms' => get_stylesheet(), | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| $template_query = new WP_Query( $wp_query_args ); | ||
| $active_templates = array(); | ||
|
|
||
| foreach ( $template_query->posts as $post ) { | ||
| $active_templates[ $post->post_name ] = $post->ID; | ||
| } | ||
|
|
||
| update_option( 'active_templates', $active_templates ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the changes here compared to the regular template endpoint that we have before. This is only for "theme" and "plugin" templates right, aka "registered templates"?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's correct. We shouldn't include the edits/user templates that the current one has
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will all have to be reconciled in the core back port PR