diff --git a/backport-changelog/6.8/8063.md b/backport-changelog/6.8/8063.md new file mode 100644 index 00000000000000..10cc8c2a2f8c67 --- /dev/null +++ b/backport-changelog/6.8/8063.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/8063 + +* https://github.com/WordPress/gutenberg/pull/67125 diff --git a/lib/compat/wordpress-6.8/class-gutenberg-rest-static-templates-controller.php b/lib/compat/wordpress-6.8/class-gutenberg-rest-static-templates-controller.php new file mode 100644 index 00000000000000..f22db34e63921b --- /dev/null +++ b/lib/compat/wordpress-6.8/class-gutenberg-rest-static-templates-controller.php @@ -0,0 +1,125 @@ +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%s%s)', + $this->rest_base, + /* + * Matches theme's directory: `/themes///` or `/themes//`. + * 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 ); + + $templates = array(); + foreach ( $query_result as $template ) { + $item = $this->prepare_item_for_response( $template, $request ); + $item->data['type'] = '_wp_static_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_static_template'; + return rest_ensure_response( $item ); + } +} diff --git a/lib/compat/wordpress-6.8/class-gutenberg-rest-templates-controller.php b/lib/compat/wordpress-6.8/class-gutenberg-rest-templates-controller.php new file mode 100644 index 00000000000000..53dbb6ca50c39c --- /dev/null +++ b/lib/compat/wordpress-6.8/class-gutenberg-rest-templates-controller.php @@ -0,0 +1,16 @@ +rest_base = 'templates'; + $controller = new Gutenberg_REST_Templates_Controller_6_7( '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_static_template'] = clone $wp_post_types['wp_template']; + $wp_post_types['_wp_static_template']->name = '_wp_static_template'; + $wp_post_types['_wp_static_template']->rest_base = '_wp_static_template'; + $wp_post_types['_wp_static_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', + '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 ) { + if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) { + $active_templates = 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 +add_action( 'rest_pre_insert_wp_template', 'gutenberg_set_active_template_theme', 10, 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 ); +} diff --git a/lib/load.php b/lib/load.php index 371f9c54e5fc4a..326a9057814f2b 100644 --- a/lib/load.php +++ b/lib/load.php @@ -42,6 +42,9 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.7/rest-api.php'; // WordPress 6.8 compat. + require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-static-templates-controller.php'; + require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-templates-controller.php'; + require __DIR__ . '/compat/wordpress-6.8/template-activate.php'; require __DIR__ . '/compat/wordpress-6.8/block-comments.php'; require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-comment-controller-6-8.php'; require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-post-types-controller-6-8.php'; diff --git a/packages/core-commands/src/site-editor-navigation-commands.js b/packages/core-commands/src/site-editor-navigation-commands.js index c1b12a84d4d61a..478ee372279c1b 100644 --- a/packages/core-commands/src/site-editor-navigation-commands.js +++ b/packages/core-commands/src/site-editor-navigation-commands.js @@ -32,6 +32,7 @@ const icons = { post, page, wp_template: layout, + _wp_static_template: layout, wp_template_part: symbolFilled, }; @@ -169,7 +170,7 @@ const getNavigationCommandLoaderPerTemplate = ( templateType ) => return { isBlockBasedTheme: select( coreStore ).getCurrentTheme()?.is_block_theme, - canCreateTemplate: select( coreStore ).canUser( 'create', { + canCreateTemplate: select( coreStore ).canUser( 'read', { kind: 'postType', name: templateType, } ), @@ -420,6 +421,10 @@ export function useSiteEditorNavigationCommands() { name: 'core/edit-site/navigate-templates', hook: getNavigationCommandLoaderPerTemplate( 'wp_template' ), } ); + useCommandLoader( { + name: 'core/edit-site/navigate-templates', + hook: getNavigationCommandLoaderPerTemplate( '_wp_static_template' ), + } ); useCommandLoader( { name: 'core/edit-site/navigate-template-parts', hook: getNavigationCommandLoaderPerTemplate( 'wp_template_part' ), diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 13cbba39e11765..b47adfe8e982ca 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -92,6 +92,16 @@ export function receiveEntityRecords( edits, meta ) { + // If we receive an auto-draft template, pretend it's already published. + if ( kind === 'postType' && name === 'wp_template' ) { + records = ( Array.isArray( records ) ? records : [ records ] ).map( + ( record ) => + record.status === 'auto-draft' + ? { ...record, status: 'publish' } + : record + ); + } + // Auto drafts should not have titles, but some plugins rely on them so we can't filter this // on the server. if ( kind === 'postType' ) { @@ -362,7 +372,7 @@ export const deleteEntityRecord = */ export const editEntityRecord = ( kind, name, recordId, edits, options = {} ) => - ( { select, dispatch } ) => { + async ( { select, dispatch, resolveSelect } ) => { const entityConfig = select.getEntityConfig( kind, name ); if ( ! entityConfig ) { throw new Error( @@ -424,6 +434,33 @@ export const editEntityRecord = ], options.isCached ); + // Temporary solution until we find the right UX: when the user + // modifies a template, we automatically set it active. + // It can be unchecked in multi-entity saving. + // This is to keep the current behaviour where templates are + // immediately active. + if ( + ! options.isCached && + kind === 'postType' && + name === 'wp_template' + ) { + const site = await resolveSelect.getEntityRecord( + 'root', + 'site' + ); + await dispatch.editEntityRecord( + 'root', + 'site', + undefined, + { + active_templates: { + ...site.active_templates, + [ record.slug ]: record.id, + }, + }, + { isCached: true } + ); + } } dispatch( { type: 'EDIT_ENTITY_RECORD', @@ -673,6 +710,11 @@ export const saveEntityRecord = ), }; } + // Unless there is no persisted record, set the status to + // publish. + if ( name === 'wp_template' && persistedRecord ) { + edits.status = 'publish'; + } updatedRecord = await __unstableFetch( { path, method: recordId ? 'PUT' : 'POST', diff --git a/packages/core-data/src/private-actions.js b/packages/core-data/src/private-actions.js index df76d2693e54f3..b11ef2cf080451 100644 --- a/packages/core-data/src/private-actions.js +++ b/packages/core-data/src/private-actions.js @@ -14,3 +14,7 @@ export function receiveRegisteredPostMeta( postType, registeredPostMeta ) { registeredPostMeta, }; } + +export function receiveTemplateAutoDraftId( target, id ) { + return { type: 'RECEIVE_TEMPLATE_AUTO_DRAFT_ID', target, id }; +} diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index fb0401509694ef..ffeb0b386ab508 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -279,3 +279,10 @@ export const getTemplateId = createRegistrySelector( } ); } ); + +export function getTemplateAutoDraftId( + state: State, + staticTemplateId: string +) { + return state.templateAutoDraftId[ staticTemplateId ]; +} diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 9748355fc5caf6..34070eafe1ebb3 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -647,6 +647,12 @@ export function registeredPostMeta( state = {}, action ) { return state; } +export function templateAutoDraftId( state = {}, action ) { + return action.type === 'RECEIVE_TEMPLATE_AUTO_DRAFT_ID' + ? { ...state, [ action.target ]: action.id } + : state; +} + export default combineReducers( { terms, users, @@ -669,4 +675,5 @@ export default combineReducers( { navigationFallbackId, defaultTemplates, registeredPostMeta, + templateAutoDraftId, } ); diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 4f101035b10130..8b26a3a375f1f2 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -66,6 +66,18 @@ export const getCurrentUser = export const getEntityRecord = ( kind, name, key = '', query ) => async ( { select, dispatch, registry, resolveSelect } ) => { + // For back-compat, we allow querying for static templates through + // wp_template. + if ( + kind === 'postType' && + name === 'wp_template' && + typeof key === 'string' && + // __experimentalGetDirtyEntityRecords always calls getEntityRecord + // with a string key, so we need that it's not a numeric ID. + ! /^\d+$/.test( key ) + ) { + name = '_wp_static_template'; + } const configs = await resolveSelect.getEntitiesConfig( kind ); const entityConfig = configs.find( ( config ) => config.name === name && config.kind === kind @@ -211,6 +223,30 @@ export const getEntityRecord = } }; +export const getTemplateAutoDraftId = + ( staticTemplateId ) => + async ( { resolveSelect, dispatch } ) => { + const record = await resolveSelect.getEntityRecord( + 'postType', + '_wp_static_template', + staticTemplateId + ); + const autoDraft = await dispatch.saveEntityRecord( + 'postType', + 'wp_template', + { + ...record, + id: undefined, + type: 'wp_template', + status: 'auto-draft', + } + ); + await dispatch.receiveTemplateAutoDraftId( + staticTemplateId, + autoDraft.id + ); + }; + /** * Requests an entity's record from the REST API. */ @@ -799,23 +835,35 @@ export const getDefaultTemplateId = // Wait for the the entities config to be loaded, otherwise receiving // the template as an entity will not work. await resolveSelect.getEntitiesConfig( 'postType' ); + const id = template?.wp_id || template?.id; // Endpoint may return an empty object if no template is found. - if ( template?.id ) { + if ( id ) { + template.id = id; + template.type = + typeof id === 'string' ? '_wp_static_template' : 'wp_template'; registry.batch( () => { - dispatch.receiveDefaultTemplateId( query, template.id ); - dispatch.receiveEntityRecords( 'postType', 'wp_template', [ + dispatch.receiveDefaultTemplateId( query, id ); + dispatch.receiveEntityRecords( 'postType', template.type, [ template, ] ); // Avoid further network requests. dispatch.finishResolution( 'getEntityRecord', [ 'postType', - 'wp_template', - template.id, + template.type, + id, ] ); } ); } }; +getDefaultTemplateId.shouldInvalidate = ( action ) => { + return ( + action.type === 'EDIT_ENTITY_RECORD' && + action.kind === 'root' && + action.name === 'site' + ); +}; + /** * Requests an entity's revisions from the REST API. * diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 7f4b0d38846468..096ef6f3b9aaa7 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -48,6 +48,7 @@ export interface State { userPatternCategories: Array< UserPatternCategory >; defaultTemplates: Record< string, string >; registeredPostMeta: Record< string, Object >; + templateAutoDraftId: Record< string, number | null >; } type EntityRecordKey = string | number; @@ -353,6 +354,18 @@ export const getEntityRecord = createSelector( key?: EntityRecordKey, query?: GetRecordsHttpQuery ): EntityRecord | undefined => { + // For back-compat, we allow querying for static templates through + // wp_template. + if ( + kind === 'postType' && + name === 'wp_template' && + typeof key === 'string' && + // __experimentalGetDirtyEntityRecords always calls getEntityRecord + // with a string key, so we need that it's not a numeric ID. + ! /^\d+$/.test( key ) + ) { + name = '_wp_static_template'; + } const queriedState = state.entities.records?.[ kind ]?.[ name ]?.queriedData; if ( ! queriedState ) { diff --git a/packages/core-data/src/test/actions.js b/packages/core-data/src/test/actions.js index 8438d03293c164..4e35a6dcd8ecab 100644 --- a/packages/core-data/src/test/actions.js +++ b/packages/core-data/src/test/actions.js @@ -38,14 +38,14 @@ describe( 'editEntityRecord', () => { const select = { getEntityConfig: jest.fn(), }; - const fulfillment = () => + const fulfillment = async () => editEntityRecord( entityConfig.kind, entityConfig.name, entityConfig.id, {} )( { select } ); - expect( fulfillment ).toThrow( + await expect( fulfillment ).rejects.toThrow( `The entity being edited (${ entityConfig.kind }, ${ entityConfig.name }) does not have a loaded config.` ); expect( select.getEntityConfig ).toHaveBeenCalledTimes( 1 ); diff --git a/packages/dataviews/src/dataviews-layouts/grid/index.tsx b/packages/dataviews/src/dataviews-layouts/grid/index.tsx index e8f8a46002ebdf..a91b6c55575c3e 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/grid/index.tsx @@ -177,12 +177,18 @@ function GridItem< Item >( { justify="flex-start" > { badgeFields.map( ( field ) => { + const out = ( + + ); + if ( field.__returnsBadge ) { + return out; + } return ( - + { out } ); } ) } diff --git a/packages/dataviews/src/filter-and-sort-data-view.ts b/packages/dataviews/src/filter-and-sort-data-view.ts index da2e9915d515ba..9b649c029c3047 100644 --- a/packages/dataviews/src/filter-and-sort-data-view.ts +++ b/packages/dataviews/src/filter-and-sort-data-view.ts @@ -77,7 +77,10 @@ export function filterSortAndPaginate< Item >( return filter.value.some( ( filterValue: any ) => fieldValue.includes( filterValue ) ); - } else if ( typeof fieldValue === 'string' ) { + } else if ( + typeof fieldValue === 'string' || + typeof fieldValue === 'number' + ) { return filter.value.includes( fieldValue ); } return false; diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 820f75364df204..0614f7724779cc 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -170,6 +170,7 @@ export type NormalizedField< Item > = Field< Item > & { isValid: ( item: Item, context?: ValidationContext ) => boolean; enableHiding: boolean; enableSorting: boolean; + __returnsBadge?: boolean; }; /** diff --git a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts index 21bf56b578b57d..c5186a6e9978e6 100644 --- a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts +++ b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts @@ -8,6 +8,7 @@ interface SiteEditorOptions { postType?: string; path?: string; canvas?: string; + activeView?: string; showWelcomeGuide?: boolean; } @@ -21,7 +22,7 @@ export async function visitSiteEditor( this: Admin, options: SiteEditorOptions = {} ) { - const { postId, postType, path, canvas } = options; + const { postId, postType, path, canvas, activeView } = options; const query = new URLSearchParams(); if ( postId ) { @@ -36,6 +37,9 @@ export async function visitSiteEditor( if ( canvas ) { query.set( 'canvas', canvas ); } + if ( activeView ) { + query.set( 'activeView', activeView ); + } await this.visitAdminPage( 'site-editor.php', query.toString() ); diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index acc71afe1db0a8..4465d5106d45a6 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -86,6 +86,7 @@ const DESIGN_POST_TYPES = [ 'wp_template_part', 'wp_block', 'wp_navigation', + '_wp_static_template', ]; function useEditorStyles( ...additionalStyles ) { @@ -445,6 +446,17 @@ function Layout( { ); useMetaBoxInitialization( hasActiveMetaboxes ); + const editableResolvedTemplateId = useSelect( + ( select ) => { + if ( typeof templateId !== 'string' ) { + return templateId; + } + return unlock( select( coreStore ) ).getTemplateAutoDraftId( + templateId + ); + }, + [ templateId ] + ); const [ paddingAppenderRef, paddingStyle ] = usePaddingAppender( enablePaddingAppender ); @@ -569,7 +581,7 @@ function Layout( { initialEdits={ initialEdits } postType={ currentPostType } postId={ currentPostId } - templateId={ templateId } + templateId={ editableResolvedTemplateId } className={ className } styles={ styles } forceIsDirty={ hasActiveMetaboxes } diff --git a/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js b/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js index 69f1925c7b0e44..7cfb133d45fadd 100644 --- a/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js +++ b/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js @@ -166,9 +166,7 @@ function SuggestionList( { entityForSuggestions, onSelect } ) { } function AddCustomTemplateModalContent( { onSelect, entityForSuggestions } ) { - const [ showSearchEntities, setShowSearchEntities ] = useState( - entityForSuggestions.hasGeneralTemplate - ); + const [ showSearchEntities, setShowSearchEntities ] = useState( false ); return ( slug - ); const missingDefaultTemplates = ( defaultTemplateTypes || [] ).filter( - ( template ) => - DEFAULT_TEMPLATE_SLUGS.includes( template.slug ) && - ! existingTemplateSlugs.includes( template.slug ) + ( template ) => DEFAULT_TEMPLATE_SLUGS.includes( template.slug ) ); const onClickMenuItem = ( _entityForSuggestions ) => { onClick?.(); diff --git a/packages/edit-site/src/components/add-new-template/utils.js b/packages/edit-site/src/components/add-new-template/utils.js index 759f3f478cadaf..83316fb8ee3c4f 100644 --- a/packages/edit-site/src/components/add-new-template/utils.js +++ b/packages/edit-site/src/components/add-new-template/utils.js @@ -180,7 +180,6 @@ export function usePostTypeArchiveMenuItems() { export const usePostTypeMenuItems = ( onClickMenuItem ) => { const publicPostTypes = usePublicPostTypes(); - const existingTemplates = useExistingTemplates(); const defaultTemplateTypes = useDefaultTemplateTypes(); // We need to keep track of naming conflicts. If a conflict // occurs, we need to add slug. @@ -220,9 +219,6 @@ export const usePostTypeMenuItems = ( onClickMenuItem ) => { [ publicPostTypes ] ); const postTypesInfo = useEntitiesInfo( 'postType', templatePrefixes ); - const existingTemplateSlugs = ( existingTemplates || [] ).map( - ( { slug } ) => slug - ); const menuItems = ( publicPostTypes || [] ).reduce( ( accumulator, postType ) => { const { slug, labels, icon } = postType; @@ -233,8 +229,6 @@ export const usePostTypeMenuItems = ( onClickMenuItem ) => { const defaultTemplateType = defaultTemplateTypes?.find( ( { slug: _slug } ) => _slug === generalTemplateSlug ); - const hasGeneralTemplate = - existingTemplateSlugs?.includes( generalTemplateSlug ); const _needsUniqueIdentifier = needsUniqueIdentifier( postType ); let menuItemTitle = labels.template_name || @@ -312,14 +306,12 @@ export const usePostTypeMenuItems = ( onClickMenuItem ) => { }, }, labels, - hasGeneralTemplate, template, } ); }; } - // We don't need to add the menu item if there are no - // entities and the general template exists. - if ( ! hasGeneralTemplate || hasEntities ) { + // We don't need to add the menu item if there are no entities. + if ( hasEntities ) { accumulator.push( menuItem ); } return accumulator; @@ -546,7 +538,11 @@ export function useAuthorMenuItem( onClickMenuItem ) { getSpecificTemplate: ( suggestion ) => { const templateSlug = `author-${ suggestion.slug }`; return { - title: templateSlug, + title: sprintf( + // translators: %s: Name of the author e.g: "Admin". + __( 'Author: %s' ), + suggestion.name + ), slug: templateSlug, templatePrefix: 'author', }; @@ -568,91 +564,6 @@ export function useAuthorMenuItem( onClickMenuItem ) { } } -/** - * Helper hook that filters all the existing templates by the given - * object with the entity's slug as key and the template prefix as value. - * - * Example: - * `existingTemplates` is: [ { slug: 'tag-apple' }, { slug: 'page-about' }, { slug: 'tag' } ] - * `templatePrefixes` is: { post_tag: 'tag' } - * It will return: { post_tag: ['apple'] } - * - * Note: We append the `-` to the given template prefix in this function for our checks. - * - * @param {Record} templatePrefixes An object with the entity's slug as key and the template prefix as value. - * @return {Record} An object with the entity's slug as key and an array with the existing template slugs as value. - */ -const useExistingTemplateSlugs = ( templatePrefixes ) => { - const existingTemplates = useExistingTemplates(); - const existingSlugs = useMemo( () => { - return Object.entries( templatePrefixes || {} ).reduce( - ( accumulator, [ slug, prefix ] ) => { - const slugsWithTemplates = ( existingTemplates || [] ).reduce( - ( _accumulator, existingTemplate ) => { - const _prefix = `${ prefix }-`; - if ( existingTemplate.slug.startsWith( _prefix ) ) { - _accumulator.push( - existingTemplate.slug.substring( - _prefix.length - ) - ); - } - return _accumulator; - }, - [] - ); - if ( slugsWithTemplates.length ) { - accumulator[ slug ] = slugsWithTemplates; - } - return accumulator; - }, - {} - ); - }, [ templatePrefixes, existingTemplates ] ); - return existingSlugs; -}; - -/** - * Helper hook that finds the existing records with an associated template, - * as they need to be excluded from the template suggestions. - * - * @param {string} entityName The entity's name. - * @param {Record} templatePrefixes An object with the entity's slug as key and the template prefix as value. - * @param {Record} additionalQueryParameters An object with the entity's slug as key and additional query parameters as value. - * @return {Record} An object with the entity's slug as key and the existing records as value. - */ -const useTemplatesToExclude = ( - entityName, - templatePrefixes, - additionalQueryParameters = {} -) => { - const slugsToExcludePerEntity = - useExistingTemplateSlugs( templatePrefixes ); - const recordsToExcludePerEntity = useSelect( - ( select ) => { - return Object.entries( slugsToExcludePerEntity || {} ).reduce( - ( accumulator, [ slug, slugsWithTemplates ] ) => { - const entitiesWithTemplates = select( - coreStore - ).getEntityRecords( entityName, slug, { - _fields: 'id', - context: 'view', - slug: slugsWithTemplates, - ...additionalQueryParameters[ slug ], - } ); - if ( entitiesWithTemplates?.length ) { - accumulator[ slug ] = entitiesWithTemplates; - } - return accumulator; - }, - {} - ); - }, - [ slugsToExcludePerEntity ] - ); - return recordsToExcludePerEntity; -}; - /** * Helper hook that returns information about an entity having * records that we can create a specific template for. @@ -673,26 +584,16 @@ const useEntitiesInfo = ( templatePrefixes, additionalQueryParameters = EMPTY_OBJECT ) => { - const recordsToExcludePerEntity = useTemplatesToExclude( - entityName, - templatePrefixes, - additionalQueryParameters - ); const entitiesHasRecords = useSelect( ( select ) => { return Object.keys( templatePrefixes || {} ).reduce( ( accumulator, slug ) => { - const existingEntitiesIds = - recordsToExcludePerEntity?.[ slug ]?.map( - ( { id } ) => id - ) || []; accumulator[ slug ] = !! select( coreStore ).getEntityRecords( entityName, slug, { per_page: 1, _fields: 'id', context: 'view', - exclude: existingEntitiesIds, ...additionalQueryParameters[ slug ], } )?.length; return accumulator; @@ -700,28 +601,18 @@ const useEntitiesInfo = ( {} ); }, - [ - templatePrefixes, - recordsToExcludePerEntity, - entityName, - additionalQueryParameters, - ] + [ templatePrefixes, entityName, additionalQueryParameters ] ); const entitiesInfo = useMemo( () => { return Object.keys( templatePrefixes || {} ).reduce( ( accumulator, slug ) => { - const existingEntitiesIds = - recordsToExcludePerEntity?.[ slug ]?.map( - ( { id } ) => id - ) || []; accumulator[ slug ] = { hasEntities: entitiesHasRecords[ slug ], - existingEntitiesIds, }; return accumulator; }, {} ); - }, [ templatePrefixes, recordsToExcludePerEntity, entitiesHasRecords ] ); + }, [ templatePrefixes, entitiesHasRecords ] ); return entitiesInfo; }; diff --git a/packages/edit-site/src/components/dataviews-actions/index.js b/packages/edit-site/src/components/dataviews-actions/index.js index 0a7b20c712c820..b1e227c3407ad2 100644 --- a/packages/edit-site/src/components/dataviews-actions/index.js +++ b/packages/edit-site/src/components/dataviews-actions/index.js @@ -5,6 +5,8 @@ import { __ } from '@wordpress/i18n'; import { edit } from '@wordpress/icons'; import { useMemo } from '@wordpress/element'; import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies @@ -14,6 +16,51 @@ import { unlock } from '../../lock-unlock'; const { useHistory } = unlock( routerPrivateApis ); +export const useSetActiveTemplateAction = () => { + const { getEntityRecord } = useSelect( coreStore ); + const { editEntityRecord, saveEditedEntityRecord } = + useDispatch( coreStore ); + return useMemo( + () => ( { + id: 'set-active-template', + label( items ) { + return items.some( ( item ) => item._isActive ) + ? __( 'Deactivate' ) + : __( 'Activate' ); + }, + isPrimary: true, + icon: edit, + isEligible( item ) { + return ! ( item.slug === 'index' && item.source === 'theme' ); + }, + async callback( items ) { + const deactivate = items.some( ( item ) => item._isActive ); + // current active templates + const activeTemplates = { + ...( ( await getEntityRecord( 'root', 'site' ) + .active_templates ) ?? {} ), + }; + for ( const item of items ) { + if ( deactivate ) { + if ( item.source === 'theme' ) { + activeTemplates[ item.slug ] = false; + } else { + delete activeTemplates[ item.slug ]; + } + } else { + activeTemplates[ item.slug ] = item.id; + } + } + await editEntityRecord( 'root', 'site', undefined, { + active_templates: activeTemplates, + } ); + await saveEditedEntityRecord( 'root', 'site' ); + }, + } ), + [ editEntityRecord, saveEditedEntityRecord, getEntityRecord ] + ); +}; + export const useEditPostAction = () => { const history = useHistory(); return useMemo( diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index ad88ee07e2150f..33a599373d86ae 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -109,6 +109,7 @@ function getNavigationPath( location, postType ) { 'template-part-item', 'page-item', 'template-item', + 'static-template-item', 'post-item', ].includes( name ) ) { diff --git a/packages/edit-site/src/components/editor/use-resolve-edited-entity.js b/packages/edit-site/src/components/editor/use-resolve-edited-entity.js index 8da076f9f00b71..26896091589123 100644 --- a/packages/edit-site/src/components/editor/use-resolve-edited-entity.js +++ b/packages/edit-site/src/components/editor/use-resolve-edited-entity.js @@ -25,13 +25,12 @@ const postTypesWithoutParentTemplate = [ TEMPLATE_PART_POST_TYPE, NAVIGATION_POST_TYPE, PATTERN_TYPES.user, + '_wp_static_template', ]; const authorizedPostTypes = [ 'page', 'post' ]; -export function useResolveEditedEntity() { - const { name, params = {}, query } = useLocation(); - const { postId = query?.postId } = params; // Fallback to query param for postId for list view routes. +function getPostType( name, postId ) { let postType; if ( name === 'navigation-item' ) { postType = NAVIGATION_POST_TYPE; @@ -39,19 +38,48 @@ export function useResolveEditedEntity() { postType = PATTERN_TYPES.user; } else if ( name === 'template-part-item' ) { postType = TEMPLATE_PART_POST_TYPE; - } else if ( name === 'template-item' || name === 'templates' ) { + } else if ( name === 'templates' ) { + postType = /^\d+$/.test( postId ) + ? TEMPLATE_POST_TYPE + : '_wp_static_template'; + } else if ( name === 'template-item' ) { postType = TEMPLATE_POST_TYPE; + } else if ( name === 'static-template-item' ) { + postType = '_wp_static_template'; } else if ( name === 'page-item' || name === 'pages' ) { postType = 'page'; } else if ( name === 'post-item' || name === 'posts' ) { postType = 'post'; } + return postType; +} + +export function useResolveEditedEntity() { + const { name, params = {}, query } = useLocation(); + const { postId: _postId = query?.postId } = params; // Fallback to query param for postId for list view routes. + const _postType = getPostType( name, _postId ) ?? query?.postType; + const homePage = useSelect( ( select ) => { const { getHomePage } = unlock( select( coreDataStore ) ); return getHomePage(); }, [] ); + const [ postType, postId ] = useSelect( + ( select ) => { + if ( _postType !== '_wp_static_template' ) { + return [ _postType, _postId ]; + } + return [ + TEMPLATE_POST_TYPE, + unlock( select( coreDataStore ) ).getTemplateAutoDraftId( + _postId + ), + ]; + }, + [ _postType, _postId ] + ); + /** * This is a hook that recreates the logic to resolve a template for a given WordPress postID postTypeId * in order to match the frontend as closely as possible in the site editor. @@ -98,6 +126,18 @@ export function useResolveEditedEntity() { [ homePage, postId, postType ] ); + const editableResolvedTemplateId = useSelect( + ( select ) => { + if ( typeof resolvedTemplateId !== 'string' ) { + return resolvedTemplateId; + } + return unlock( select( coreDataStore ) ).getTemplateAutoDraftId( + resolvedTemplateId + ); + }, + [ resolvedTemplateId ] + ); + const context = useMemo( () => { if ( postTypesWithoutParentTemplate.includes( postType ) && postId ) { return {}; @@ -121,9 +161,9 @@ export function useResolveEditedEntity() { if ( !! homePage ) { return { - isReady: resolvedTemplateId !== undefined, + isReady: editableResolvedTemplateId !== undefined, postType: TEMPLATE_POST_TYPE, - postId: resolvedTemplateId, + postId: editableResolvedTemplateId, context, }; } diff --git a/packages/edit-site/src/components/page-templates/fields.js b/packages/edit-site/src/components/page-templates/fields.js index 97b427690901a6..e966c9272a75d3 100644 --- a/packages/edit-site/src/components/page-templates/fields.js +++ b/packages/edit-site/src/components/page-templates/fields.js @@ -6,8 +6,12 @@ import clsx from 'clsx'; /** * WordPress dependencies */ -import { Icon, __experimentalHStack as HStack } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { + Icon, + __experimentalHStack as HStack, + privateApis as componentsPrivateApis, +} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; import { useState, useMemo } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; import { parse } from '@wordpress/blocks'; @@ -21,10 +25,12 @@ import { EditorProvider } from '@wordpress/editor'; * Internal dependencies */ import { useAddedBy } from './hooks'; +import { useDefaultTemplateTypes } from '../add-new-template/utils'; import usePatternSettings from '../page-patterns/use-pattern-settings'; import { unlock } from '../../lock-unlock'; const { useGlobalStyle } = unlock( blockEditorPrivateApis ); +const { Badge } = unlock( componentsPrivateApis ); function PreviewField( { item } ) { const settings = usePatternSettings(); @@ -113,6 +119,38 @@ function AuthorField( { item } ) { export const authorField = { label: __( 'Author' ), id: 'author', - getValue: ( { item } ) => item.author_text, + getValue: ( { item } ) => item.author_text ?? item.author, render: AuthorField, }; + +export const activeField = { + label: __( 'Active' ), + id: 'active', + getValue: ( { item } ) => item._isActive, + __returnsBadge: true, + render: function Render( { item } ) { + const isActive = item._isActive; + return ( + + { isActive ? __( 'Active' ) : __( 'Inactive' ) } + + ); + }, +}; + +export const slugField = { + label: __( 'Template Type' ), + id: 'slug', + getValue: ( { item } ) => item.slug, + render: function Render( { item } ) { + const defaultTemplateTypes = useDefaultTemplateTypes(); + const defaultTemplateType = defaultTemplateTypes.find( + ( type ) => type.slug === item.slug + ); + return ( + defaultTemplateType?.title || + // translators: %s is the slug of a custom template. + sprintf( __( 'Custom: %s' ), item.slug ) + ); + }, +}; diff --git a/packages/edit-site/src/components/page-templates/hooks.js b/packages/edit-site/src/components/page-templates/hooks.js index 3cf2d78a59ee99..e02a7f7059566b 100644 --- a/packages/edit-site/src/components/page-templates/hooks.js +++ b/packages/edit-site/src/components/page-templates/hooks.js @@ -89,7 +89,7 @@ export function useAddedBy( postType, postId ) { type: 'user', icon: authorIcon, imageUrl: user?.avatar_urls?.[ 48 ], - text: authorText, + text: authorText ?? user?.name, isCustomized: false, }; } diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index e0e04e0f5da924..2038c9005d00d7 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -3,12 +3,17 @@ */ import { __ } from '@wordpress/i18n'; import { useState, useMemo, useCallback, useEffect } from '@wordpress/element'; -import { privateApis as corePrivateApis } from '@wordpress/core-data'; +import { + privateApis as corePrivateApis, + store as coreStore, +} from '@wordpress/core-data'; import { DataViews, filterSortAndPaginate } from '@wordpress/dataviews'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { templateTitleField } from '@wordpress/fields'; import { addQueryArgs } from '@wordpress/url'; +import { useSelect } from '@wordpress/data'; +import { useEvent } from '@wordpress/compose'; /** * Internal dependencies @@ -23,16 +28,23 @@ import { LAYOUT_LIST, } from '../../utils/constants'; import { unlock } from '../../lock-unlock'; -import { useEditPostAction } from '../dataviews-actions'; -import { authorField, descriptionField, previewField } from './fields'; -import { useEvent } from '@wordpress/compose'; +import { + useEditPostAction, + useSetActiveTemplateAction, +} from '../dataviews-actions'; +import { + authorField, + descriptionField, + previewField, + activeField, + slugField, +} from './fields'; +import { useDefaultTemplateTypes } from '../add-new-template/utils'; const { usePostActions } = unlock( editorPrivateApis ); const { useHistory, useLocation } = unlock( routerPrivateApis ); const { useEntityRecordsWithPermissions } = unlock( corePrivateApis ); -const EMPTY_ARRAY = []; - const defaultLayouts = { [ LAYOUT_TABLE ]: { showMedia: false, @@ -46,9 +58,15 @@ const defaultLayouts = { }, [ LAYOUT_GRID ]: { showMedia: true, + layout: { + badgeFields: [ 'active', 'slug' ], + }, }, [ LAYOUT_LIST ]: { showMedia: false, + layout: { + badgeFields: [ 'active', 'slug' ], + }, }, }; @@ -64,31 +82,29 @@ const DEFAULT_VIEW = { titleField: 'title', descriptionField: 'description', mediaField: 'preview', - fields: [ 'author' ], + fields: [ 'author', 'active', 'slug' ], filters: [], ...defaultLayouts[ LAYOUT_GRID ], }; export default function PageTemplates() { const { path, query } = useLocation(); - const { activeView = 'all', layout, postId } = query; + const { activeView = 'active', layout, postId } = query; const [ selection, setSelection ] = useState( [ postId ] ); - const defaultView = useMemo( () => { const usedType = layout ?? DEFAULT_VIEW.type; return { ...DEFAULT_VIEW, type: usedType, - filters: - activeView !== 'all' - ? [ - { - field: 'author', - operator: 'isAny', - value: [ activeView ], - }, - ] - : [], + filters: ! [ 'active', 'user' ].includes( activeView ) + ? [ + { + field: 'author', + operator: 'isAny', + value: [ activeView ], + }, + ] + : [], ...defaultLayouts[ usedType ], }; }, [ layout, activeView ] ); @@ -106,23 +122,123 @@ export default function PageTemplates() { useEffect( () => { setView( ( currentView ) => ( { ...currentView, - filters: - activeView !== 'all' - ? [ - { - field: 'author', - operator: OPERATOR_IS_ANY, - value: [ activeView ], - }, - ] - : [], + filters: ! [ 'active', 'user' ].includes( activeView ) + ? [ + { + field: 'author', + operator: OPERATOR_IS_ANY, + value: [ activeView ], + }, + ] + : [], } ) ); }, [ setView, activeView ] ); - const { records, isResolving: isLoadingData } = + const activeTemplatesOption = useSelect( + ( select ) => + select( coreStore ).getEntityRecord( 'root', 'site' ) + ?.active_templates + ); + const defaultTemplateTypes = useDefaultTemplateTypes(); + // Todo: this will have to be better so that we're not fetching all the + // records all the time. Active templates query will need to move server + // side. + const { records: userRecords, isResolving: isLoadingUserRecords } = useEntityRecordsWithPermissions( 'postType', TEMPLATE_POST_TYPE, { per_page: -1, } ); + const { records: staticRecords, isResolving: isLoadingStaticData } = + useEntityRecordsWithPermissions( 'postType', '_wp_static_template', { + per_page: -1, + } ); + + const activeTemplates = useMemo( () => { + const _active = [ ...staticRecords ]; + if ( activeTemplatesOption ) { + for ( const activeSlug in activeTemplatesOption ) { + const activeId = activeTemplatesOption[ activeSlug ]; + if ( activeId === false ) { + // Remove the template from the array. + const index = _active.findIndex( + ( template ) => template.slug === activeSlug + ); + if ( index !== -1 ) { + _active.splice( index, 1 ); + } + } else { + // Replace the template in the array. + const template = userRecords.find( + ( { id } ) => id === activeId + ); + if ( template ) { + const index = _active.findIndex( + ( { slug } ) => slug === template.slug + ); + if ( index !== -1 ) { + _active[ index ] = template; + } else { + _active.push( template ); + } + } + } + } + } + const defaultSlugs = defaultTemplateTypes.map( ( type ) => type.slug ); + return _active.filter( ( template ) => + defaultSlugs.includes( template.slug ) + ); + }, [ + defaultTemplateTypes, + userRecords, + staticRecords, + activeTemplatesOption, + ] ); + + let _records; + let isLoadingData; + if ( activeView === 'active' ) { + _records = activeTemplates; + isLoadingData = isLoadingUserRecords || isLoadingStaticData; + } else if ( activeView === 'user' ) { + _records = userRecords; + isLoadingData = isLoadingUserRecords; + } else { + _records = staticRecords; + isLoadingData = isLoadingStaticData; + } + + const records = useMemo( () => { + return _records.map( ( record ) => ( { + ...record, + _isActive: + typeof record.id === 'string' + ? activeTemplatesOption[ record.slug ] === record.id || + activeTemplatesOption[ record.slug ] === undefined + : Object.values( activeTemplatesOption ).includes( + record.id + ), + } ) ); + }, [ _records, activeTemplatesOption ] ); + + const users = useSelect( + ( select ) => { + const { getUser } = select( coreStore ); + return records.reduce( ( acc, record ) => { + if ( record.author_text ) { + if ( ! acc[ record.author_text ] ) { + acc[ record.author_text ] = record.author_text; + } + } else if ( record.author ) { + if ( ! acc[ record.author ] ) { + acc[ record.author ] = getUser( record.author ); + } + } + return acc; + }, {} ); + }, + [ records ] + ); + const history = useHistory(); const onChangeSelection = useCallback( ( items ) => { @@ -138,32 +254,29 @@ export default function PageTemplates() { [ history, path, view?.type ] ); - const authors = useMemo( () => { - if ( ! records ) { - return EMPTY_ARRAY; - } - const authorsSet = new Set(); - records.forEach( ( template ) => { - authorsSet.add( template.author_text ); - } ); - return Array.from( authorsSet ).map( ( author ) => ( { - value: author, - label: author, - } ) ); - }, [ records ] ); - - const fields = useMemo( - () => [ + const fields = useMemo( () => { + const _fields = [ previewField, templateTitleField, descriptionField, - { + activeField, + slugField, + ]; + if ( [ 'active', 'user' ].includes( activeView ) ) { + const elements = []; + for ( const author in users ) { + elements.push( { + value: users[ author ]?.id ?? author, + label: users[ author ]?.name ?? author, + } ); + } + _fields.push( { ...authorField, - elements: authors, - }, - ], - [ authors ] - ); + elements, + } ); + } + return _fields; + }, [ users, activeView ] ); const { data, paginationInfo } = useMemo( () => { return filterSortAndPaginate( records, view, fields ); @@ -174,9 +287,13 @@ export default function PageTemplates() { context: 'list', } ); const editAction = useEditPostAction(); + const setActiveTemplateAction = useSetActiveTemplateAction(); const actions = useMemo( - () => [ editAction, ...postTypeActions ], - [ postTypeActions, editAction ] + () => + activeView === 'user' + ? [ setActiveTemplateAction, editAction, ...postTypeActions ] + : [ setActiveTemplateAction, ...postTypeActions ], + [ postTypeActions, setActiveTemplateAction, editAction, activeView ] ); const onChangeView = useEvent( ( newView ) => { @@ -207,8 +324,10 @@ export default function PageTemplates() { onChangeView={ onChangeView } onChangeSelection={ onChangeSelection } isItemClickable={ () => true } - onClickItem={ ( { id } ) => { - history.navigate( `/wp_template/${ id }?canvas=edit` ); + onClickItem={ ( item ) => { + history.navigate( + `/${ item.type }/${ item.id }?canvas=edit` + ); } } selection={ selection } defaultLayouts={ defaultLayouts } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js index 7920d49a43c8cd..0221113b7154a7 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js @@ -14,7 +14,6 @@ import { addQueryArgs } from '@wordpress/url'; import SidebarNavigationItem from '../sidebar-navigation-item'; import { useAddedBy } from '../page-templates/hooks'; import { layout } from '@wordpress/icons'; -import { TEMPLATE_POST_TYPE } from '../../utils/constants'; import { unlock } from '../../lock-unlock'; const { useLocation } = unlock( routerPrivateApis ); @@ -37,9 +36,9 @@ function TemplateDataviewItem( { template, isActive } ) { export default function DataviewsTemplatesSidebarContent() { const { - query: { activeView = 'all' }, + query: { activeView = 'active' }, } = useLocation(); - const { records } = useEntityRecords( 'postType', TEMPLATE_POST_TYPE, { + const { records } = useEntityRecords( 'postType', '_wp_static_template', { per_page: -1, } ); const firstItemPerAuthorText = useMemo( () => { @@ -61,9 +60,16 @@ export default function DataviewsTemplatesSidebarContent() { - { __( 'All templates' ) } + { __( 'Active templates' ) } + + + { __( 'Custom templates' ) } { firstItemPerAuthorText.map( ( template ) => { return ( diff --git a/packages/edit-site/src/components/site-editor-routes/index.js b/packages/edit-site/src/components/site-editor-routes/index.js index 232cb640cff264..24e3424d0eae13 100644 --- a/packages/edit-site/src/components/site-editor-routes/index.js +++ b/packages/edit-site/src/components/site-editor-routes/index.js @@ -17,7 +17,7 @@ import { patternsRoute } from './patterns'; import { patternItemRoute } from './pattern-item'; import { templatePartItemRoute } from './template-part-item'; import { templatesRoute } from './templates'; -import { templateItemRoute } from './template-item'; +import { templateItemRoute, staticTemplateItemRoute } from './template-item'; import { pagesRoute } from './pages'; import { pageItemRoute } from './page-item'; import { stylebookRoute } from './stylebook'; @@ -26,6 +26,7 @@ const routes = [ pageItemRoute, pagesRoute, templateItemRoute, + staticTemplateItemRoute, templatesRoute, templatePartItemRoute, patternItemRoute, diff --git a/packages/edit-site/src/components/site-editor-routes/template-item.js b/packages/edit-site/src/components/site-editor-routes/template-item.js index 22726f6a5ac430..38a140daf16b28 100644 --- a/packages/edit-site/src/components/site-editor-routes/template-item.js +++ b/packages/edit-site/src/components/site-editor-routes/template-item.js @@ -13,3 +13,13 @@ export const templateItemRoute = { preview: , }, }; + +export const staticTemplateItemRoute = { + name: 'static-template-item', + path: '/_wp_static_template/*postId', + areas: { + sidebar: , + mobile: , + preview: , + }, +}; diff --git a/packages/editor/src/components/post-excerpt/panel.js b/packages/editor/src/components/post-excerpt/panel.js index d4f2b27126c7c1..c38c162e72c950 100644 --- a/packages/editor/src/components/post-excerpt/panel.js +++ b/packages/editor/src/components/post-excerpt/panel.js @@ -109,6 +109,7 @@ function PrivateExcerpt() { getCurrentPostId, getEditedPostAttribute, isEditorPanelEnabled, + __experimentalGetDefaultTemplateType, } = select( editorStore ); const postType = getCurrentPostType(); const isTemplateOrTemplatePart = [ @@ -131,13 +132,17 @@ function PrivateExcerpt() { postType, getCurrentPostId() ); + const fallback = isTemplateOrTemplatePart + ? __experimentalGetDefaultTemplateType( template.slug ) + .description + : undefined; // For post types that use excerpt as description, we do not abide // by the `isEnabled` panel flag in order to render them as text. const _shouldRender = isEditorPanelEnabled( PANEL_NAME ) || _shouldBeUsedAsDescription; return { - excerpt: getEditedPostAttribute( _usedAttribute ), + excerpt: getEditedPostAttribute( _usedAttribute ) ?? fallback, shouldRender: _shouldRender, shouldBeUsedAsDescription: _shouldBeUsedAsDescription, // If we should render, allow editing for all post types that are not used as description. diff --git a/packages/editor/src/components/post-template/hooks.js b/packages/editor/src/components/post-template/hooks.js index c9668cd1443335..ca4d641bf42487 100644 --- a/packages/editor/src/components/post-template/hooks.js +++ b/packages/editor/src/components/post-template/hooks.js @@ -48,14 +48,30 @@ export function useAllowSwitchingTemplates() { } function useTemplates( postType ) { - return useSelect( - ( select ) => - select( coreStore ).getEntityRecords( 'postType', 'wp_template', { - per_page: -1, - post_type: postType, - } ), + // To do: create a new selector to checks if templates exist at all instead + // of and unbound request. In the modal, the user templates should be + // paginated and we should not make an unbound request. + const { staticTemplates, templates } = useSelect( + ( select ) => { + return { + staticTemplates: select( coreStore ).getEntityRecords( + 'postType', + '_wp_static_template', + { per_page: -1, post_type: postType } + ), + templates: select( coreStore ).getEntityRecords( + 'postType', + 'wp_template', + { per_page: -1, post_type: postType } + ), + }; + }, [ postType ] ); + return useMemo( + () => [ ...( staticTemplates || [] ), ...( templates || [] ) ], + [ staticTemplates, templates ] + ); } export function useAvailableTemplates( postType ) { @@ -67,7 +83,7 @@ export function useAvailableTemplates( postType ) { allowSwitchingTemplate && templates?.filter( ( template ) => - template.is_custom && + ( template.is_custom || template.type === 'wp_template' ) && template.slug !== currentTemplateSlug && !! template.content.raw // Skip empty templates. ), diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 1259eae623de93..bd105b21ae420f 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -214,7 +214,10 @@ export const ExperimentalEditorProvider = withRegistryProvider( const defaultBlockContext = useMemo( () => { const postContext = {}; // If it is a template, try to inherit the post type from the name. - if ( post.type === 'wp_template' ) { + if ( + post.type === 'wp_template' || + post.type === '_wp_static_template' + ) { if ( post.slug === 'page' ) { postContext.postType = 'page'; } else if ( post.slug === 'single' ) { diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index 2119b52756e964..d718afafc8b7ea 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -137,7 +137,7 @@ export const registerPostTypeSchema = : undefined, // @ts-ignore globalThis.IS_GUTENBERG_PLUGIN - ? ! [ 'wp_template', 'wp_block', 'wp_template_part' ].includes( + ? ! [ 'wp_block', 'wp_template_part' ].includes( postTypeConfig.slug ) && canCreate && diff --git a/packages/fields/src/actions/duplicate-post.tsx b/packages/fields/src/actions/duplicate-post.tsx index 5f079b5132c600..c14715e2e385f2 100644 --- a/packages/fields/src/actions/duplicate-post.tsx +++ b/packages/fields/src/actions/duplicate-post.tsx @@ -55,10 +55,14 @@ const duplicatePost: Action< BasePost > = { return; } + const isTemplate = + item.type === 'wp_template' || + item.type === '_wp_static_template'; + const newItemOject = { - status: 'draft', + status: isTemplate ? 'publish' : 'draft', title: item.title, - slug: item.title || __( 'No title' ), + slug: isTemplate ? item.slug : item.title || __( 'No title' ), comment_status: item.comment_status, content: typeof item.content === 'string' @@ -97,7 +101,9 @@ const duplicatePost: Action< BasePost > = { try { const newItem = await saveEntityRecord( 'postType', - item.type, + item.type === '_wp_static_template' + ? 'wp_template' + : item.type, newItemOject, { throwOnError: true } ); diff --git a/packages/fields/src/actions/rename-post.tsx b/packages/fields/src/actions/rename-post.tsx index be0e69db26cd80..c8b0202eee8b7f 100644 --- a/packages/fields/src/actions/rename-post.tsx +++ b/packages/fields/src/actions/rename-post.tsx @@ -21,12 +21,7 @@ import { store as noticesStore } from '@wordpress/notices'; */ import { unlock } from '../lock-unlock'; -import { - getItemTitle, - isTemplateRemovable, - isTemplate, - isTemplatePart, -} from './utils'; +import { getItemTitle, isTemplatePart } from './utils'; import type { CoreDataError, PostWithPermissions } from '../types'; // Patterns. @@ -42,7 +37,6 @@ const renamePost: Action< PostWithPermissions > = { // Templates, template parts and patterns have special checks for renaming. if ( ! [ - 'wp_template', 'wp_template_part', ...Object.values( PATTERN_TYPES ), ].includes( post.type ) @@ -50,15 +44,6 @@ const renamePost: Action< PostWithPermissions > = { return post.permissions?.update; } - // In the case of templates, we can only rename custom templates. - if ( isTemplate( post ) ) { - return ( - isTemplateRemovable( post ) && - post.is_custom && - post.permissions?.update - ); - } - if ( isTemplatePart( post ) ) { return ( post.source === 'custom' && diff --git a/packages/fields/src/actions/trash-post.tsx b/packages/fields/src/actions/trash-post.tsx index c0227996b5e866..d0ddf0730fb16d 100644 --- a/packages/fields/src/actions/trash-post.tsx +++ b/packages/fields/src/actions/trash-post.tsx @@ -18,7 +18,7 @@ import type { Action } from '@wordpress/dataviews'; /** * Internal dependencies */ -import { getItemTitle, isTemplateOrTemplatePart } from './utils'; +import { getItemTitle } from './utils'; import type { CoreDataError, PostWithPermissions } from '../types'; const trashPost: Action< PostWithPermissions > = { @@ -27,7 +27,7 @@ const trashPost: Action< PostWithPermissions > = { isPrimary: true, icon: trash, isEligible( item ) { - if ( isTemplateOrTemplatePart( item ) || item.type === 'wp_block' ) { + if ( item.type === 'wp_template_part' || item.type === 'wp_block' ) { return false; } diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index 79cb01038da23c..263eea58a66209 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -1013,7 +1013,7 @@ test.describe( 'Image - Site editor', () => { test.beforeEach( async ( { admin } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); } ); diff --git a/test/e2e/specs/editor/blocks/post-comments-form.spec.js b/test/e2e/specs/editor/blocks/post-comments-form.spec.js index db75771dc09154..ac6e3dd95fb423 100644 --- a/test/e2e/specs/editor/blocks/post-comments-form.spec.js +++ b/test/e2e/specs/editor/blocks/post-comments-form.spec.js @@ -33,7 +33,7 @@ test.describe( 'Comments Form', () => { // Navigate to "Singular" post template await admin.visitSiteEditor( { postId: 'emptytheme//singular', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); diff --git a/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js index 318707e22f098d..1f389a4155be46 100644 --- a/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js +++ b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js @@ -25,7 +25,7 @@ test.describe( 'Post Meta source', () => { test.beforeEach( async ( { admin, editor } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/block-bindings//single-movie', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); await editor.openDocumentSettingsSidebar(); @@ -283,7 +283,7 @@ test.describe( 'Post Meta source', () => { test.beforeEach( async ( { admin, editor } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/block-bindings//custom-template', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); await editor.openDocumentSettingsSidebar(); diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js index 7d2be84187ef61..caf4af5f41c383 100644 --- a/test/e2e/specs/editor/various/pattern-overrides.spec.js +++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js @@ -263,7 +263,7 @@ test.describe( 'Pattern Overrides', () => { await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); diff --git a/test/e2e/specs/editor/various/write-design-mode.spec.js b/test/e2e/specs/editor/various/write-design-mode.spec.js index fb3e231e6fff60..ecf2ccc41843fd 100644 --- a/test/e2e/specs/editor/various/write-design-mode.spec.js +++ b/test/e2e/specs/editor/various/write-design-mode.spec.js @@ -13,7 +13,7 @@ test.describe( 'Write/Design mode', () => { } ); await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); } ); diff --git a/test/e2e/specs/site-editor/block-removal.spec.js b/test/e2e/specs/site-editor/block-removal.spec.js index 7fc547c19e59e3..9eb8053049a2b0 100644 --- a/test/e2e/specs/site-editor/block-removal.spec.js +++ b/test/e2e/specs/site-editor/block-removal.spec.js @@ -15,7 +15,7 @@ test.describe( 'Site editor block removal prompt', () => { test.beforeEach( async ( { admin } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); } ); diff --git a/test/e2e/specs/site-editor/browser-history.spec.js b/test/e2e/specs/site-editor/browser-history.spec.js index c3eb2ac5e3a2fa..7a07dbff220cd2 100644 --- a/test/e2e/specs/site-editor/browser-history.spec.js +++ b/test/e2e/specs/site-editor/browser-history.spec.js @@ -23,7 +23,7 @@ test.describe( 'Site editor browser history', () => { .locator( '.fields-field__title', { hasText: 'Index' } ) .click(); await expect( page ).toHaveURL( - '/wp-admin/site-editor.php?p=%2Fwp_template%2Femptytheme%2F%2Findex&canvas=edit' + '/wp-admin/site-editor.php?p=%2F_wp_static_template%2Femptytheme%2F%2Findex&canvas=edit' ); // Navigate back to the template list diff --git a/test/e2e/specs/site-editor/font-library.spec.js b/test/e2e/specs/site-editor/font-library.spec.js index 1824257df12fd3..4962521fdfb8ff 100644 --- a/test/e2e/specs/site-editor/font-library.spec.js +++ b/test/e2e/specs/site-editor/font-library.spec.js @@ -12,7 +12,7 @@ test.describe( 'Font Library', () => { test.beforeEach( async ( { admin } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); } ); @@ -57,7 +57,7 @@ test.describe( 'Font Library', () => { test.beforeEach( async ( { admin } ) => { await admin.visitSiteEditor( { postId: 'twentytwentythree//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); } ); @@ -137,7 +137,7 @@ test.describe( 'Font Library', () => { test.beforeEach( async ( { admin } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); } ); @@ -219,7 +219,7 @@ test.describe( 'Font Library', () => { test.beforeEach( async ( { admin } ) => { await admin.visitSiteEditor( { postId: 'twentytwentyfour//home', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); } ); diff --git a/test/e2e/specs/site-editor/global-styles-sidebar.spec.js b/test/e2e/specs/site-editor/global-styles-sidebar.spec.js index 7f1b818df4ce0a..ac99f9b521e5e7 100644 --- a/test/e2e/specs/site-editor/global-styles-sidebar.spec.js +++ b/test/e2e/specs/site-editor/global-styles-sidebar.spec.js @@ -15,7 +15,7 @@ test.describe( 'Global styles sidebar', () => { test.beforeEach( async ( { admin } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); } ); diff --git a/test/e2e/specs/site-editor/iframe-rendering.spec.js b/test/e2e/specs/site-editor/iframe-rendering.spec.js index 9c25ef504637e4..b01ffeb0ad19c2 100644 --- a/test/e2e/specs/site-editor/iframe-rendering.spec.js +++ b/test/e2e/specs/site-editor/iframe-rendering.spec.js @@ -18,7 +18,7 @@ test.describe( 'Site editor iframe rendering mode', () => { } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); const compatMode = await editor.canvas diff --git a/test/e2e/specs/site-editor/multi-entity-saving.spec.js b/test/e2e/specs/site-editor/multi-entity-saving.spec.js index cbc3bfde457a14..5f3caa386d7e39 100644 --- a/test/e2e/specs/site-editor/multi-entity-saving.spec.js +++ b/test/e2e/specs/site-editor/multi-entity-saving.spec.js @@ -21,7 +21,7 @@ test.describe( 'Site Editor - Multi-entity save flow', () => { test.beforeEach( async ( { admin } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); } ); @@ -45,9 +45,7 @@ test.describe( 'Site Editor - Multi-entity save flow', () => { .getByRole( 'button', { name: 'Open save panel' } ) ).toBeVisible(); - await editor.saveSiteEditorEntities( { - isOnlyCurrentEntityDirty: true, - } ); + await editor.saveSiteEditorEntities(); const saveButton = page .getByRole( 'region', { name: 'Editor top bar' } ) .getByRole( 'button', { name: 'Save' } ); @@ -76,9 +74,7 @@ test.describe( 'Site Editor - Multi-entity save flow', () => { // Change font size. await fontSizePicker.getByRole( 'radio', { name: 'Small' } ).click(); - await editor.saveSiteEditorEntities( { - isOnlyCurrentEntityDirty: true, - } ); + await editor.saveSiteEditorEntities(); // Change font size again. await fontSizePicker.getByRole( 'radio', { name: 'Medium' } ).click(); diff --git a/test/e2e/specs/site-editor/new-templates-list.spec.js b/test/e2e/specs/site-editor/new-templates-list.spec.js index d26306a6c8e3b5..58f6eff154e416 100644 --- a/test/e2e/specs/site-editor/new-templates-list.spec.js +++ b/test/e2e/specs/site-editor/new-templates-list.spec.js @@ -40,11 +40,14 @@ test.describe( 'Templates', () => { } ); test( 'Filtering', async ( { requestUtils, admin, page } ) => { - await requestUtils.createTemplate( 'wp_template', { + const template = await requestUtils.createTemplate( 'wp_template', { slug: 'date', title: 'Date Archives', content: 'hi', } ); + await requestUtils.updateSiteSettings( { + active_templates: { date: template.wp_id }, + } ); await admin.visitSiteEditor( { postType: 'wp_template' } ); // Global search. await page.getByRole( 'searchbox', { name: 'Search' } ).fill( 'tag' ); @@ -54,7 +57,7 @@ test.describe( 'Templates', () => { await page .getByRole( 'button', { name: 'Reset search', exact: true } ) .click(); - await expect( titles ).toHaveCount( 6 ); + await expect( titles ).toHaveCount( 5 ); // Filter by author. await page.getByRole( 'button', { name: 'Add filter' } ).click(); diff --git a/test/e2e/specs/site-editor/pages.spec.js b/test/e2e/specs/site-editor/pages.spec.js index 37b164e85a5973..79f097b8eb8da0 100644 --- a/test/e2e/specs/site-editor/pages.spec.js +++ b/test/e2e/specs/site-editor/pages.spec.js @@ -265,9 +265,7 @@ test.describe( 'Pages', () => { await page .locator( '.block-editor-block-patterns-list__list-item' ) .click(); - await editor.saveSiteEditorEntities( { - isOnlyCurrentEntityDirty: true, - } ); + await editor.saveSiteEditorEntities(); await admin.visitSiteEditor(); // Create new page that has the default template so as to swap it. diff --git a/test/e2e/specs/site-editor/preload.spec.js b/test/e2e/specs/site-editor/preload.spec.js index e731e932e30523..7b9bfc1b3925a4 100644 --- a/test/e2e/specs/site-editor/preload.spec.js +++ b/test/e2e/specs/site-editor/preload.spec.js @@ -31,7 +31,9 @@ test.describe( 'Preload', () => { const urlObject = new URL( url ); const restRoute = urlObject.searchParams.get( 'rest_route' ); if ( restRoute ) { - requests.push( restRoute ); + urlObject.searchParams.delete( 'rest_route' ); + urlObject.searchParams.delete( '_locale' ); + requests.push( restRoute + urlObject.search ); } else { requests.push( url ); } @@ -47,6 +49,12 @@ test.describe( 'Preload', () => { // There are two separate settings OPTIONS requests. We should fix // so the one for canUser and getEntityRecord are reused. '/wp/v2/settings', + // There's an issue where preloaded data does not invalidate, + // preloading had to be disabled for now. + '/wp/v2/templates/lookup?slug=front-page', + '/wp/v2/wp_template', + // This is the auto-draft template. + expect.stringMatching( /\/wp\/v2\/wp_template\/\d+\?context=edit/ ), // Seems to be coming from `enableComplementaryArea`. '/wp/v2/users/me', ] ); diff --git a/test/e2e/specs/site-editor/push-to-global-styles.spec.js b/test/e2e/specs/site-editor/push-to-global-styles.spec.js index 3e30f764811b1f..178b01e7dc394d 100644 --- a/test/e2e/specs/site-editor/push-to-global-styles.spec.js +++ b/test/e2e/specs/site-editor/push-to-global-styles.spec.js @@ -15,7 +15,7 @@ test.describe( 'Push to Global Styles button', () => { test.beforeEach( async ( { admin } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); } ); diff --git a/test/e2e/specs/site-editor/settings-sidebar.spec.js b/test/e2e/specs/site-editor/settings-sidebar.spec.js index 87e9023401109d..e086276c3d4564 100644 --- a/test/e2e/specs/site-editor/settings-sidebar.spec.js +++ b/test/e2e/specs/site-editor/settings-sidebar.spec.js @@ -15,7 +15,7 @@ test.describe( 'Settings sidebar', () => { test.beforeEach( async ( { admin } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); } ); @@ -63,7 +63,7 @@ test.describe( 'Settings sidebar', () => { await admin.visitSiteEditor( { postId: 'emptytheme//singular', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); diff --git a/test/e2e/specs/site-editor/site-editor-export.spec.js b/test/e2e/specs/site-editor/site-editor-export.spec.js index a0a56c18089cc2..db85a4c066f04d 100644 --- a/test/e2e/specs/site-editor/site-editor-export.spec.js +++ b/test/e2e/specs/site-editor/site-editor-export.spec.js @@ -22,7 +22,7 @@ test.describe( 'Site Editor Templates Export', () => { } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); await page diff --git a/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js b/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js index a0cc0af5463aed..a50897977f704b 100644 --- a/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js +++ b/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js @@ -44,7 +44,7 @@ test.describe( 'Site editor url navigation', () => { .click(); await page.getByRole( 'option', { name: 'Demo' } ).click(); await expect( page ).toHaveURL( - '/wp-admin/site-editor.php?p=%2Fwp_template%2Femptytheme%2F%2Fsingle-post-demo&canvas=edit' + /wp-admin\/site-editor\.php\?p=%2Fwp_template%2F\d+&canvas=edit/ ); } ); diff --git a/test/e2e/specs/site-editor/style-variations.spec.js b/test/e2e/specs/site-editor/style-variations.spec.js index 9c4243b0d171f6..949dd873710039 100644 --- a/test/e2e/specs/site-editor/style-variations.spec.js +++ b/test/e2e/specs/site-editor/style-variations.spec.js @@ -36,7 +36,7 @@ test.describe( 'Global styles variations', () => { } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/style-variations//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); @@ -72,7 +72,7 @@ test.describe( 'Global styles variations', () => { } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/style-variations//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); await siteEditorStyleVariations.browseStyles(); @@ -108,7 +108,7 @@ test.describe( 'Global styles variations', () => { } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/style-variations//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); await siteEditorStyleVariations.browseStyles(); @@ -150,7 +150,7 @@ test.describe( 'Global styles variations', () => { } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/style-variations//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); await siteEditorStyleVariations.browseStyles(); @@ -180,7 +180,7 @@ test.describe( 'Global styles variations', () => { } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/style-variations//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); await siteEditorStyleVariations.browseStyles(); diff --git a/test/e2e/specs/site-editor/template-activate.spec.js b/test/e2e/specs/site-editor/template-activate.spec.js new file mode 100644 index 00000000000000..7dbc15d4349296 --- /dev/null +++ b/test/e2e/specs/site-editor/template-activate.spec.js @@ -0,0 +1,127 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Template Activate', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'emptytheme' ); + await requestUtils.deleteAllTemplates( 'wp_template' ); + await requestUtils.deleteAllTemplates( 'wp_template_part' ); + } ); + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deleteAllTemplates( 'wp_template' ); + await requestUtils.deleteAllTemplates( 'wp_template_part' ); + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + test.beforeEach( async ( { admin, requestUtils } ) => { + await requestUtils.deleteAllTemplates( 'wp_template' ); + await admin.visitSiteEditor( { postType: 'wp_template' } ); + } ); + + test( 'should duplicate and activate', async ( { + page, + admin, + editor, + } ) => { + // Inside the grid cell, find the button with the text "Actions" + const index = page.locator( + '.dataviews-view-grid__card:has-text("Index")' + ); + let actionsButton = index.getByRole( 'button', { name: 'Actions' } ); + await actionsButton.click(); + + const duplicateButton = page.getByRole( 'menuitem', { + name: 'Duplicate', + } ); + await duplicateButton.click(); + + await page.keyboard.press( 'Enter' ); + + // Wait for the snackbar message. + await page.waitForSelector( '.components-snackbar__content' ); + + await admin.visitSiteEditor( { + postType: 'wp_template', + activeView: 'user', + } ); + + const indexCopy = page.locator( + '.dataviews-view-grid__card:has-text("Index (Copy)")' + ); + + expect( await indexCopy.textContent() ).toContain( 'Inactive' ); + + actionsButton = indexCopy.getByRole( 'button', { + name: 'Actions', + } ); + await actionsButton.click(); + + const activateButton = page.getByRole( 'menuitem', { + name: 'Activate', + } ); + await activateButton.click(); + + await page.waitForSelector( + '.dataviews-view-grid__badge-fields .is-success' + ); + + await page.getByLabel( 'Index (Copy)', { exact: true } ).click(); + + await expect( editor.canvas.getByText( 'gutenberg' ) ).toBeVisible(); + + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Copied from Index.' }, + } ); + + await editor.saveSiteEditorEntities( { + isOnlyCurrentEntityDirty: true, + } ); + + // Visit the front end. + const previewButton = page.getByRole( 'button', { + name: 'View', + exact: true, + } ); + + await previewButton.click(); + + const previewMenuItem = page.getByRole( 'menuitem', { + name: 'View site', + } ); + + const [ previewPage ] = await Promise.all( [ + page.context().waitForEvent( 'page' ), + previewMenuItem.click(), + ] ); + + await expect( previewPage.locator( 'body' ) ).toContainText( + 'Copied from Index.' + ); + + await page.bringToFront(); + + await page.getByRole( 'button', { name: 'Open Navigation' } ).click(); + + await actionsButton.click(); + + const deactivateButton = page.getByRole( 'menuitem', { + name: 'Deactivate', + } ); + await deactivateButton.click(); + + await expect( + page.locator( + '.dataviews-view-grid__card:has-text("Index (Copy)") .is-success' + ) + ).toBeHidden(); + + await previewPage.bringToFront(); + await previewPage.reload(); + + await expect( previewPage.locator( 'body' ) ).not.toContainText( + 'Copied from Index.' + ); + } ); +} ); diff --git a/test/e2e/specs/site-editor/template-registration.spec.js b/test/e2e/specs/site-editor/template-registration.spec.js index 2960367fc32ef1..4c138a0432542d 100644 --- a/test/e2e/specs/site-editor/template-registration.spec.js +++ b/test/e2e/specs/site-editor/template-registration.spec.js @@ -41,6 +41,7 @@ test.describe( 'Block template registration', () => { // Verify template is listed in the Site Editor. await admin.visitSiteEditor( { postType: 'wp_template', + activeView: 'Gutenberg', } ); await blockTemplateRegistrationUtils.searchForTemplate( 'Plugin Template' @@ -49,7 +50,6 @@ test.describe( 'Block template registration', () => { await expect( page.getByText( 'A template registered by a plugin.' ) ).toBeVisible(); - await expect( page.getByText( 'AuthorGutenberg' ) ).toBeVisible(); // Verify the template contents are rendered in the editor. await page.getByText( 'Plugin Template' ).click(); @@ -62,19 +62,18 @@ test.describe( 'Block template registration', () => { name: 'core/paragraph', attributes: { content: 'User-edited template' }, } ); - await editor.saveSiteEditorEntities( { - isOnlyCurrentEntityDirty: true, - } ); + await editor.saveSiteEditorEntities(); await page.goto( '/?cat=1' ); await expect( page.getByText( 'User-edited template' ) ).toBeVisible(); // Verify template can be reset. await admin.visitSiteEditor( { postType: 'wp_template', + activeView: 'user', } ); const resetNotice = page .getByLabel( 'Dismiss this notice' ) - .getByText( `"Plugin Template" reset.` ); + .getByText( `"Plugin Template" moved to the trash.` ); const savedButton = page.getByRole( 'button', { name: 'Saved', } ); @@ -83,8 +82,8 @@ test.describe( 'Block template registration', () => { ); const searchResults = page.getByLabel( 'Actions' ); await searchResults.first().click(); - await page.getByRole( 'menuitem', { name: 'Reset' } ).click(); - await page.getByRole( 'button', { name: 'Reset' } ).click(); + await page.getByRole( 'menuitem', { name: 'Move to trash' } ).click(); + await page.getByRole( 'button', { name: 'Trash' } ).click(); await expect( resetNotice ).toBeVisible(); await expect( savedButton ).toBeVisible(); @@ -154,6 +153,7 @@ test.describe( 'Block template registration', () => { // Verify the plugin-registered template doesn't appear in the Site Editor. await admin.visitSiteEditor( { postType: 'wp_template', + activeView: 'Emptytheme', } ); await blockTemplateRegistrationUtils.searchForTemplate( 'Custom' ); await expect( @@ -165,8 +165,6 @@ test.describe( 'Block template registration', () => { 'A custom template registered by a plugin and overridden by a theme.' ) ).toBeVisible(); - // Verify the theme template shows the theme name as the author. - await expect( page.getByText( 'AuthorEmptytheme' ) ).toBeVisible(); } ); test( 'templates can be deleted if the registered plugin is deactivated', async ( { @@ -179,6 +177,7 @@ test.describe( 'Block template registration', () => { // Make an edit to the template. await admin.visitSiteEditor( { postType: 'wp_template', + activeView: 'Gutenberg', } ); await blockTemplateRegistrationUtils.searchForTemplate( 'Plugin Template' @@ -191,9 +190,7 @@ test.describe( 'Block template registration', () => { name: 'core/paragraph', attributes: { content: 'User-customized template' }, } ); - await editor.saveSiteEditorEntities( { - isOnlyCurrentEntityDirty: true, - } ); + await editor.saveSiteEditorEntities(); // Deactivate plugin. await requestUtils.deactivatePlugin( @@ -203,10 +200,11 @@ test.describe( 'Block template registration', () => { // Verify template can be deleted. await admin.visitSiteEditor( { postType: 'wp_template', + activeView: 'user', } ); const deletedNotice = page .getByLabel( 'Dismiss this notice' ) - .getByText( `"Plugin Template" deleted.` ); + .getByText( `"Plugin Template" moved to the trash.` ); const savedButton = page.getByRole( 'button', { name: 'Saved', } ); @@ -215,8 +213,8 @@ test.describe( 'Block template registration', () => { ); const searchResults = page.getByLabel( 'Actions' ); await searchResults.first().click(); - await page.getByRole( 'menuitem', { name: 'Delete' } ).click(); - await page.getByRole( 'button', { name: 'Delete' } ).click(); + await page.getByRole( 'menuitem', { name: 'Move to trash' } ).click(); + await page.getByRole( 'button', { name: 'Trash' } ).click(); await expect( deletedNotice ).toBeVisible(); await expect( savedButton ).toBeVisible(); @@ -287,9 +285,7 @@ test.describe( 'Block template registration', () => { name: 'core/paragraph', attributes: { content: 'Author template customized by the user.' }, } ); - await editor.saveSiteEditorEntities( { - isOnlyCurrentEntityDirty: true, - } ); + await editor.saveSiteEditorEntities(); await requestUtils.activatePlugin( 'gutenberg-test-block-template-registration' @@ -313,32 +309,26 @@ test.describe( 'Block template registration', () => { ); await expect( page.getByText( 'Plugin Author Template' ) ).toBeHidden(); + await admin.visitSiteEditor( { + postType: 'wp_template', + activeView: 'user', + } ); + // Reset the user-modified template. const resetNotice = page .getByLabel( 'Dismiss this notice' ) - .getByText( `"Author: Admin" reset.` ); + .getByText( `"Author: Admin" moved to the trash.` ); await page.getByPlaceholder( 'Search' ).fill( 'Author: admin' ); await page .locator( '.fields-field__title', { hasText: 'Author: Admin' } ) .click(); const actions = page.getByLabel( 'Actions' ); await actions.first().click(); - await page.getByRole( 'menuitem', { name: 'Reset' } ).click(); - await page.getByRole( 'button', { name: 'Reset' } ).click(); + await page.getByRole( 'menuitem', { name: 'Move to trash' } ).click(); + await page.getByRole( 'button', { name: 'Trash' } ).click(); await expect( resetNotice ).toBeVisible(); - // Verify the template registered by the plugin is applied in the editor... - await expect( - editor.canvas.getByText( 'Author template customized by the user.' ) - ).toBeHidden(); - await expect( - editor.canvas.getByText( - 'This is a plugin-registered author template.' - ) - ).toBeVisible(); - - // ... and the frontend. await page.goto( '?author=1' ); await expect( page.getByText( 'Author template customized by the user.' ) @@ -363,6 +353,6 @@ class BlockTemplateRegistrationUtils { await this.page.getByPlaceholder( 'Search' ).fill( searchTerm ); await expect .poll( async () => await searchResults.count() ) - .toBeLessThan( initialSearchResultsCount ); + .toBeLessThanOrEqual( initialSearchResultsCount ); } } diff --git a/test/e2e/specs/site-editor/template-revert.spec.js b/test/e2e/specs/site-editor/template-revert.spec.js deleted file mode 100644 index 50a5598f400ebd..00000000000000 --- a/test/e2e/specs/site-editor/template-revert.spec.js +++ /dev/null @@ -1,257 +0,0 @@ -/** - * WordPress dependencies - */ -const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); - -test.use( { - templateRevertUtils: async ( { editor, page }, use ) => { - await use( new TemplateRevertUtils( { editor, page } ) ); - }, -} ); - -test.describe( 'Template Revert', () => { - test.beforeAll( async ( { requestUtils } ) => { - await requestUtils.activateTheme( 'emptytheme' ); - await requestUtils.deleteAllTemplates( 'wp_template' ); - await requestUtils.deleteAllTemplates( 'wp_template_part' ); - } ); - test.afterAll( async ( { requestUtils } ) => { - await requestUtils.deleteAllTemplates( 'wp_template' ); - await requestUtils.deleteAllTemplates( 'wp_template_part' ); - await requestUtils.activateTheme( 'twentytwentyone' ); - } ); - test.beforeEach( async ( { admin, requestUtils } ) => { - await requestUtils.deleteAllTemplates( 'wp_template' ); - await admin.visitSiteEditor( { canvas: 'edit' } ); - } ); - - test( 'should delete the template after saving the reverted template', async ( { - editor, - page, - templateRevertUtils, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { content: 'Test' }, - } ); - await editor.saveSiteEditorEntities( { - isOnlyCurrentEntityDirty: true, - } ); - await templateRevertUtils.revertTemplate(); - - const isTemplateTabVisible = await page - .locator( - 'role=region[name="Editor settings"i] >> role=button[name="Template"i]' - ) - .isVisible(); - if ( isTemplateTabVisible ) { - await page.click( - 'role=region[name="Editor settings"i] >> role=button[name="Template"i]' - ); - } - - // The revert button isn't visible anymore. - await expect( - page.locator( - 'role=region[name="Editor settings"i] >> role=button[name="Actions"i]' - ) - ).toBeDisabled(); - } ); - - test( 'should show the original content after revert', async ( { - editor, - templateRevertUtils, - } ) => { - const contentBefore = - await templateRevertUtils.getCurrentSiteEditorContent(); - - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { content: 'Test' }, - } ); - await editor.saveSiteEditorEntities( { - isOnlyCurrentEntityDirty: true, - } ); - await templateRevertUtils.revertTemplate(); - - const contentAfter = - await templateRevertUtils.getCurrentSiteEditorContent(); - expect( contentAfter ).toEqual( contentBefore ); - } ); - - test( 'should show the original content after revert and page reload', async ( { - admin, - editor, - templateRevertUtils, - } ) => { - const contentBefore = - await templateRevertUtils.getCurrentSiteEditorContent(); - - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { content: 'Test' }, - } ); - await editor.saveSiteEditorEntities( { - isOnlyCurrentEntityDirty: true, - } ); - await templateRevertUtils.revertTemplate(); - await admin.visitSiteEditor(); - - const contentAfter = - await templateRevertUtils.getCurrentSiteEditorContent(); - expect( contentAfter ).toEqual( contentBefore ); - } ); - - test( 'should show the edited content after revert and clicking undo in the header toolbar', async ( { - editor, - page, - templateRevertUtils, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { content: 'Test' }, - } ); - await editor.saveSiteEditorEntities( { - isOnlyCurrentEntityDirty: true, - } ); - const contentBefore = - await templateRevertUtils.getCurrentSiteEditorContent(); - - // Revert template and check state. - await templateRevertUtils.revertTemplate(); - const contentAfterSave = - await templateRevertUtils.getCurrentSiteEditorContent(); - expect( contentAfterSave ).not.toEqual( contentBefore ); - - // Undo revert by clicking header button and check state again. - await page.click( - 'role=region[name="Editor top bar"i] >> role=button[name="Undo"i]' - ); - const contentAfterUndo = - await templateRevertUtils.getCurrentSiteEditorContent(); - expect( contentAfterUndo ).toEqual( contentBefore ); - } ); - - test( 'should show the original content after revert, clicking undo then redo in the header toolbar', async ( { - editor, - page, - templateRevertUtils, - } ) => { - const contentBefore = - await templateRevertUtils.getCurrentSiteEditorContent(); - - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { content: 'Test' }, - } ); - await editor.saveSiteEditorEntities( { - isOnlyCurrentEntityDirty: true, - } ); - await templateRevertUtils.revertTemplate(); - await page.click( - 'role=region[name="Editor top bar"i] >> role=button[name="Undo"i]' - ); - - const contentAfterUndo = - await templateRevertUtils.getCurrentSiteEditorContent(); - expect( contentAfterUndo ).not.toEqual( contentBefore ); - - await page.click( - 'role=region[name="Editor top bar"i] >> role=button[name="Redo"i]' - ); - - const contentAfterRedo = - await templateRevertUtils.getCurrentSiteEditorContent(); - expect( contentAfterRedo ).toEqual( contentBefore ); - } ); - - test( 'should show the edited content after revert, clicking undo in the header toolbar, save and reload', async ( { - admin, - editor, - page, - templateRevertUtils, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { content: 'Test' }, - } ); - await editor.saveSiteEditorEntities( { - isOnlyCurrentEntityDirty: true, - } ); - await page - .getByRole( 'button', { name: 'Dismiss this notice' } ) - .getByText( /(updated|published)\./ ) - .click(); - const contentBefore = - await templateRevertUtils.getCurrentSiteEditorContent(); - - await templateRevertUtils.revertTemplate(); - - await page.click( - 'role=region[name="Editor top bar"i] >> role=button[name="Undo"i]' - ); - await editor.saveSiteEditorEntities( { - isOnlyCurrentEntityDirty: true, - } ); - await admin.visitSiteEditor(); - - const contentAfter = - await templateRevertUtils.getCurrentSiteEditorContent(); - expect( contentAfter ).toEqual( contentBefore ); - } ); -} ); - -class TemplateRevertUtils { - constructor( { editor, page } ) { - this.editor = editor; - this.page = page; - } - - async revertTemplate() { - await this.editor.openDocumentSettingsSidebar(); - const isTemplateTabVisible = await this.page - .locator( - 'role=region[name="Editor settings"i] >> role=tab[name="Template"i]' - ) - .isVisible(); - if ( isTemplateTabVisible ) { - await this.page.click( - 'role=region[name="Editor settings"i] >> role=tab[name="Template"i]' - ); - } - await this.page.click( - 'role=region[name="Editor settings"i] >> role=button[name="Actions"i]' - ); - await this.page.click( 'role=menuitem[name=/Reset/i]' ); - await this.page.getByRole( 'button', { name: 'Reset' } ).click(); - await this.page.waitForSelector( - 'role=button[name="Dismiss this notice"i] >> text=/ reset./' - ); - } - - async getCurrentSiteEditorContent() { - return this.page.evaluate( () => { - const postId = window.wp.data - .select( 'core/editor' ) - .getCurrentPostId(); - const postType = window.wp.data - .select( 'core/editor' ) - .getCurrentPostType(); - const record = window.wp.data - .select( 'core' ) - .getEditedEntityRecord( 'postType', postType, postId ); - if ( record ) { - if ( typeof record.content === 'function' ) { - return record.content( record ); - } else if ( record.blocks ) { - return window.wp.blocks.__unstableSerializeAndClean( - record.blocks - ); - } else if ( record.content ) { - return record.content; - } - } - return ''; - } ); - } -} diff --git a/test/e2e/specs/site-editor/title.spec.js b/test/e2e/specs/site-editor/title.spec.js index 8f6c5252c9f41b..75ae0ffa9efe23 100644 --- a/test/e2e/specs/site-editor/title.spec.js +++ b/test/e2e/specs/site-editor/title.spec.js @@ -19,7 +19,7 @@ test.describe( 'Site editor title', () => { // Navigate to a template. await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); const title = page diff --git a/test/e2e/specs/site-editor/zoom-out.spec.js b/test/e2e/specs/site-editor/zoom-out.spec.js index 493b566671f8be..0151d724daca57 100644 --- a/test/e2e/specs/site-editor/zoom-out.spec.js +++ b/test/e2e/specs/site-editor/zoom-out.spec.js @@ -90,7 +90,7 @@ test.describe( 'Zoom Out', () => { test.beforeEach( async ( { admin } ) => { await admin.visitSiteEditor( { postId: 'twentytwentyfour//index', - postType: 'wp_template', + postType: '_wp_static_template', canvas: 'edit', } ); } ); diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index 5a0c7f0e952116..892dd5889622ab 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -238,7 +238,10 @@ test.describe( 'Site Editor Performance', () => { await metrics.startTracing(); await page - .getByText( 'Single Posts', { exact: true } ) + .getByRole( 'button', { + name: 'Single Posts', + exact: true, + } ) .click( { force: true } ); await metrics.stopTracing(); @@ -289,11 +292,7 @@ test.describe( 'Site Editor Performance', () => { for ( let i = 1; i <= samples; i++ ) { // We want to start from a fresh state each time, without // queries or patterns already cached. - await admin.visitSiteEditor( { - postId: 'twentytwentyfour//home', - postType: 'wp_template', - canvas: 'edit', - } ); + await admin.visitSiteEditor( { canvas: 'edit' } ); await editor.openDocumentSettingsSidebar(); /*