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.9/class-gutenberg-rest-static-templates-controller.php b/lib/compat/wordpress-6.9/class-gutenberg-rest-static-templates-controller.php new file mode 100644 index 00000000000000..5a12b5def492d4 --- /dev/null +++ b/lib/compat/wordpress-6.9/class-gutenberg-rest-static-templates-controller.php @@ -0,0 +1,143 @@ +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 ); + + /** + * Filters the array of queried block templates array after they've been fetched. + * + * @since 5.9.0 + * + * @param WP_Block_Template[] $query_result Array of found block templates. + * @param array $query { + * Arguments to retrieve templates. All arguments are optional. + * + * @type string[] $slug__in List of slugs to include. + * @type int $wp_id Post ID of customized template. + * @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only). + * @type string $post_type Post type to get the templates for. + * } + * @param string $template_type wp_template or wp_template_part. + */ + $query_result = apply_filters( 'get_block_templates', $query_result, $query, 'wp_template' ); + + $templates = array(); + foreach ( $query_result as $template ) { + $item = $this->prepare_item_for_response( $template, $request ); + $item->data['type'] = 'wp_registered_template'; + $templates[] = $this->prepare_response_for_collection( $item ); + } + + return rest_ensure_response( $templates ); + } + + public function get_item( $request ) { + $template = get_block_file_template( $request['id'], 'wp_template' ); + + if ( ! $template ) { + return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) ); + } + + $item = $this->prepare_item_for_response( $template, $request ); + // adjust the template type here instead + $item->data['type'] = 'wp_registered_template'; + return rest_ensure_response( $item ); + } +} diff --git a/lib/compat/wordpress-6.9/class-gutenberg-rest-templates-controller.php b/lib/compat/wordpress-6.9/class-gutenberg-rest-templates-controller.php new file mode 100644 index 00000000000000..53dbb6ca50c39c --- /dev/null +++ b/lib/compat/wordpress-6.9/class-gutenberg-rest-templates-controller.php @@ -0,0 +1,16 @@ +name ) { + // Only prefetch for the root. If we preload it for all pages and it's not used + // it won't be possible to invalidate. + // To do: perhaps purge all preloaded paths when client side navigating. + if ( '/' !== $_GET['p'] ) { + $paths = array_filter( + $paths, + function ( $path ) { + return '/wp/v2/templates/lookup?slug=front-page' !== $path && '/wp/v2/templates/lookup?slug=home' !== $path; + } + ); + } + + $paths[] = '/wp/v2/wp_registered_template?context=edit'; + } + + return $paths; +} +add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_6_9', 10, 2 ); diff --git a/lib/compat/wordpress-6.9/template-activate.php b/lib/compat/wordpress-6.9/template-activate.php new file mode 100644 index 00000000000000..936620b268e245 --- /dev/null +++ b/lib/compat/wordpress-6.9/template-activate.php @@ -0,0 +1,169 @@ +rest_base = 'templates'; + $controller = new WP_REST_Templates_Controller( 'wp_template' ); + $wp_post_types['wp_template']->rest_base = 'wp_template'; + $controller->register_routes(); +} + +// 3. We need a route to get that raw static templates from themes and plugins. +// I registered this as a post type route because right now the +// EditorProvider assumes templates are posts. +add_action( 'init', 'gutenberg_setup_static_template' ); +function gutenberg_setup_static_template() { + global $wp_post_types; + $wp_post_types['wp_registered_template'] = clone $wp_post_types['wp_template']; + $wp_post_types['wp_registered_template']->name = 'wp_registered_template'; + $wp_post_types['wp_registered_template']->rest_base = 'wp_registered_template'; + $wp_post_types['wp_registered_template']->rest_controller_class = 'Gutenberg_REST_Static_Templates_Controller'; + + register_setting( + 'reading', + 'active_templates', + array( + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + // properties can be integers or false (deactivated). + 'additionalProperties' => true, + ), + ), + 'default' => array(), + 'label' => 'Active Templates', + ) + ); +} + +add_filter( 'pre_wp_unique_post_slug', 'gutenberg_allow_template_slugs_to_be_duplicated', 10, 5 ); +function gutenberg_allow_template_slugs_to_be_duplicated( $override, $slug, $post_id, $post_status, $post_type ) { + return 'wp_template' === $post_type ? $slug : $override; +} + +add_filter( 'pre_get_block_templates', 'gutenberg_pre_get_block_templates', 10, 3 ); +function gutenberg_pre_get_block_templates( $output, $query, $template_type ) { + if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) { + $active_templates = (array) get_option( 'active_templates', array() ); + $slugs = $query['slug__in']; + $output = array(); + foreach ( $slugs as $slug ) { + if ( isset( $active_templates[ $slug ] ) ) { + if ( false !== $active_templates[ $slug ] ) { + $post = get_post( $active_templates[ $slug ] ); + if ( $post && 'publish' === $post->post_status ) { + $output[] = _build_block_template_result_from_post( $post ); + } + } else { + // Deactivated template, fall back to next slug. + $output[] = array(); + } + } + } + if ( empty( $output ) ) { + $output = null; + } + } + return $output; +} + +// Whenever templates are queried by slug, never return any user templates. +// We are handling that in gutenberg_pre_get_block_templates. +function gutenberg_remove_tax_query_for_templates( $query ) { + if ( isset( $query->query['post_type'] ) && 'wp_template' === $query->query['post_type'] ) { + // We don't have templates with this status, that's the point. We want + // this query to not return any user templates. + $query->set( 'post_status', array( 'pending' ) ); + } +} + +add_filter( 'pre_get_block_templates', 'gutenberg_tax_pre_get_block_templates', 10, 3 ); +function gutenberg_tax_pre_get_block_templates( $output, $query, $template_type ) { + // Do not remove the tax query when querying for a specific slug. + if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) { + add_action( 'pre_get_posts', 'gutenberg_remove_tax_query_for_templates' ); + } + return $output; +} + +add_filter( 'get_block_templates', 'gutenberg_tax_get_block_templates', 10, 3 ); +function gutenberg_tax_get_block_templates( $output, $query, $template_type ) { + if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) { + remove_action( 'pre_get_posts', 'gutenberg_remove_tax_query_for_templates' ); + } + return $output; +} + +// We need to set the theme for the template when it's created. See: +// https://github.com/WordPress/wordpress-develop/blob/b2c8d8d2c8754cab5286b06efb4c11e2b6aa92d5/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php#L571-L578 +// Priority 9 so it runs before default hooks like +// `inject_ignored_hooked_blocks_metadata_attributes`. +add_action( 'rest_pre_insert_wp_template', 'gutenberg_set_active_template_theme', 9, 2 ); +function gutenberg_set_active_template_theme( $changes, $request ) { + $template = $request['id'] ? get_block_template( $request['id'], 'wp_template' ) : null; + if ( $template ) { + return $changes; + } + $changes->tax_input = array( + 'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : get_stylesheet(), + ); + return $changes; +} + +// Migrate existing "edited" templates. By existing, it means that the template +// is active. +add_action( 'init', 'gutenberg_migrate_existing_templates' ); +function gutenberg_migrate_existing_templates() { + $active_templates = get_option( 'active_templates' ); + + if ( $active_templates ) { + return; + } + + // Query all templates in the database. See `get_block_templates`. + $wp_query_args = array( + 'post_status' => 'publish', + 'post_type' => 'wp_template', + 'posts_per_page' => -1, + 'no_found_rows' => true, + 'lazy_load_term_meta' => false, + 'tax_query' => array( + array( + 'taxonomy' => 'wp_theme', + 'field' => 'name', + 'terms' => get_stylesheet(), + ), + ), + ); + + $template_query = new WP_Query( $wp_query_args ); + $active_templates = array(); + + foreach ( $template_query->posts as $post ) { + $active_templates[ $post->post_name ] = $post->ID; + } + + update_option( 'active_templates', $active_templates ); +} diff --git a/lib/load.php b/lib/load.php index 1c77fc8c78600d..193259a9acbc80 100644 --- a/lib/load.php +++ b/lib/load.php @@ -39,6 +39,9 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.8/rest-api.php'; // WordPress 6.9 compat. + require __DIR__ . '/compat/wordpress-6.9/class-gutenberg-rest-static-templates-controller.php'; + require __DIR__ . '/compat/wordpress-6.9/class-gutenberg-rest-templates-controller.php'; + require __DIR__ . '/compat/wordpress-6.9/template-activate.php'; require __DIR__ . '/compat/wordpress-6.9/block-bindings.php'; require __DIR__ . '/compat/wordpress-6.9/post-data-block-bindings.php'; require __DIR__ . '/compat/wordpress-6.9/term-data-block-bindings.php'; @@ -84,6 +87,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.9 compat. require __DIR__ . '/compat/wordpress-6.9/customizer-preview-custom-css.php'; require __DIR__ . '/compat/wordpress-6.9/command-palette.php'; +require __DIR__ . '/compat/wordpress-6.9/preload.php'; // Experimental features. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; diff --git a/packages/core-commands/src/site-editor-navigation-commands.js b/packages/core-commands/src/site-editor-navigation-commands.js index 3bfd4b6209afff..8439b3cb1ced56 100644 --- a/packages/core-commands/src/site-editor-navigation-commands.js +++ b/packages/core-commands/src/site-editor-navigation-commands.js @@ -33,6 +33,7 @@ const icons = { post, page, wp_template: layout, + wp_registered_template: layout, wp_template_part: symbolFilled, }; @@ -170,7 +171,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, } ), @@ -469,6 +470,10 @@ export function useSiteEditorNavigationCommands() { name: 'core/edit-site/navigate-templates', hook: getNavigationCommandLoaderPerTemplate( 'wp_template' ), } ); + useCommandLoader( { + name: 'core/edit-site/navigate-templates', + hook: getNavigationCommandLoaderPerTemplate( 'wp_registered_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 f6b245bb525198..07cd87a89cd183 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -93,6 +93,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' ) { @@ -364,7 +374,7 @@ export const deleteEntityRecord = */ export const editEntityRecord = ( kind, name, recordId, edits, options = {} ) => - ( { select, dispatch } ) => { + async ( { select, dispatch, resolveSelect } ) => { logEntityDeprecation( kind, name, 'editEntityRecord' ); const entityConfig = select.getEntityConfig( kind, name ); if ( ! entityConfig ) { @@ -427,6 +437,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', @@ -677,6 +714,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/entities.js b/packages/core-data/src/entities.js index 722d118e230c3c..14ec31b3a86240 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -374,7 +374,7 @@ async function loadPostTypeEntities() { }/${ parentId }/revisions${ revisionId ? '/' + revisionId : '' }`, - revisionKey: isTemplate ? 'wp_id' : DEFAULT_ENTITY_KEY, + revisionKey: DEFAULT_ENTITY_KEY, }; } ); } diff --git a/packages/core-data/src/private-actions.js b/packages/core-data/src/private-actions.js index 29af65ba137bf6..0ed6e9748b552a 100644 --- a/packages/core-data/src/private-actions.js +++ b/packages/core-data/src/private-actions.js @@ -132,3 +132,7 @@ export const editMediaEntity = dispatch.__unstableReleaseStoreLock( lock ); } }; + +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 6acbe493913a89..0b3e67e7ce3eed 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -163,6 +163,9 @@ export const getHomePage = createRegistrySelector( ( select ) => return { postType: 'wp_template', postId: frontPageTemplateId }; }, ( state ) => [ + // Even though getDefaultTemplateId.shouldInvalidate returns true when root/site changes, + // it doesn't seem to invalidate this cache, I'm not sure why. + getEntityRecord( state, 'root', 'site' ), getEntityRecord( state, 'root', '__unstableBase' ), getDefaultTemplateId( state, { slug: 'front-page', @@ -265,3 +268,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 2d53bf5762702b..051dc884a3f079 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -628,6 +628,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( { users, currentTheme, @@ -648,4 +654,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 f3b0d620d40510..a56388091f6a13 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_registered_template'; + } const configs = await resolveSelect.getEntitiesConfig( kind ); const entityConfig = configs.find( ( config ) => config.name === name && config.kind === kind @@ -202,6 +214,30 @@ export const getEntityRecord = } }; +export const getTemplateAutoDraftId = + ( staticTemplateId ) => + async ( { resolveSelect, dispatch } ) => { + const record = await resolveSelect.getEntityRecord( + 'postType', + 'wp_registered_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. */ @@ -821,23 +857,37 @@ 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_registered_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 68ab56580564fa..a50c86905562db 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -49,6 +49,7 @@ export interface State { userPatternCategories: Array< UserPatternCategory >; defaultTemplates: Record< string, string >; registeredPostMeta: Record< string, Object >; + templateAutoDraftId: Record< string, number | null >; } type EntityRecordKey = string | number; @@ -358,6 +359,18 @@ export const getEntityRecord = createSelector( ): EntityRecord | undefined => { logEntityDeprecation( kind, name, 'getEntityRecord' ); + // 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_registered_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/e2e-test-utils-playwright/src/admin/visit-site-editor.ts b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts index bc6e3e23d078b5..670792a8f95bea 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 c4ab06d2d1d306..50866435b3c93b 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -81,6 +81,7 @@ const DESIGN_POST_TYPES = [ 'wp_template_part', 'wp_block', 'wp_navigation', + 'wp_registered_template', ]; function useEditorStyles( ...additionalStyles ) { @@ -468,6 +469,17 @@ function Layout( { useMetaBoxInitialization( hasActiveMetaboxes && hasResolvedMode ); + const editableResolvedTemplateId = useSelect( + ( select ) => { + if ( typeof templateId !== 'string' ) { + return templateId; + } + return unlock( select( coreStore ) ).getTemplateAutoDraftId( + templateId + ); + }, + [ templateId ] + ); const [ paddingAppenderRef, paddingStyle ] = usePaddingAppender( enablePaddingAppender ); @@ -591,7 +603,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 7a846985c8c6a6..4929ace507af8e 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 @@ -172,9 +172,7 @@ function AddCustomTemplateModalContent( { onBack, containerRef, } ) { - const [ showSearchEntities, setShowSearchEntities ] = useState( - entityForSuggestions.hasGeneralTemplate - ); + const [ showSearchEntities, setShowSearchEntities ] = useState(); // Focus on the first focusable element when the modal opens. // We handle focus management in the parent modal, just need to focus on the first focusable element. diff --git a/packages/edit-site/src/components/add-new-template/index.js b/packages/edit-site/src/components/add-new-template/index.js index aa36fa2e59540f..754cce67898400 100644 --- a/packages/edit-site/src/components/add-new-template/index.js +++ b/packages/edit-site/src/components/add-new-template/index.js @@ -53,7 +53,6 @@ import { TEMPLATE_POST_TYPE } from '../../utils/constants'; */ import AddCustomTemplateModalContent from './add-custom-template-modal-content'; import { - useExistingTemplates, useDefaultTemplateTypes, useTaxonomiesMenuItems, usePostTypeMenuItems, @@ -387,15 +386,9 @@ function NewTemplate() { } function useMissingTemplates( setEntityForSuggestions, onClick ) { - const existingTemplates = useExistingTemplates(); const defaultTemplateTypes = useDefaultTemplateTypes(); - const existingTemplateSlugs = ( existingTemplates || [] ).map( - ( { slug } ) => 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 e9f3961bee5a39..c7bd9ecb11729b 100644 --- a/packages/edit-site/src/components/add-new-template/utils.js +++ b/packages/edit-site/src/components/add-new-template/utils.js @@ -189,7 +189,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. @@ -229,9 +228,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; @@ -242,8 +238,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 || @@ -321,14 +315,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; @@ -555,7 +547,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', }; @@ -577,91 +573,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. @@ -682,26 +593,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; @@ -709,28 +610,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..64083f36bc4218 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,55 @@ 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; + } + } + // To do: figure out why the REST API deletes the option when + // it's set to an empty object. That would trigger the migration + // function, which will make all templates in the database active. + activeTemplates.__preventCollapse = 0; + 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 3a4828d93e911c..decbf0e7f971a3 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -110,6 +110,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..8261d8e761787c 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_registered_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_registered_template'; + } else if ( name === 'template-item' ) { postType = TEMPLATE_POST_TYPE; + } else if ( name === 'static-template-item' ) { + postType = 'wp_registered_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_registered_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, }; } @@ -141,7 +181,14 @@ export function useSyncDeprecatedEntityIntoState( { useEffect( () => { if ( isReady ) { - setEditedEntity( postType, postId, context ); + // setEditedEntity expects a string (because the postId used to be + // the template slug, even for edited templates). Now the postId can + // be a number (either because it's an auto-draft or edited + // template). Passing a number could break plugins doing things like + // `id.includes`. It would be way more complex to keep passing the + // template slug, while also being incorrect, so the easiest + // solution is to cast the postId to a string. + setEditedEntity( postType, String( postId ), context ); } }, [ isReady, postType, postId, context, setEditedEntity ] ); } diff --git a/packages/edit-site/src/components/page-templates/fields.js b/packages/edit-site/src/components/page-templates/fields.js index d532cbc43fac76..04aba955e07b27 100644 --- a/packages/edit-site/src/components/page-templates/fields.js +++ b/packages/edit-site/src/components/page-templates/fields.js @@ -6,7 +6,11 @@ import clsx from 'clsx'; /** * WordPress dependencies */ -import { Icon, __experimentalHStack as HStack } from '@wordpress/components'; +import { + Icon, + __experimentalHStack as HStack, + privateApis as componentsPrivateApis, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useState, useMemo } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; @@ -16,15 +20,40 @@ import { privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { EditorProvider } from '@wordpress/editor'; +import { privateApis as corePrivateApis } from '@wordpress/core-data'; /** * 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 ); +const { useEntityRecordsWithPermissions } = unlock( corePrivateApis ); + +function useAllDefaultTemplateTypes() { + const defaultTemplateTypes = useDefaultTemplateTypes(); + const { records: staticRecords } = useEntityRecordsWithPermissions( + 'postType', + 'wp_registered_template', + { per_page: -1 } + ); + return [ + ...defaultTemplateTypes, + ...staticRecords + ?.filter( ( record ) => ! record.is_custom ) + .map( ( record ) => { + return { + slug: record.slug, + title: record.title.rendered, + description: record.description, + }; + } ), + ]; +} function PreviewField( { item } ) { const settings = usePatternSettings(); @@ -68,8 +97,14 @@ export const previewField = { export const descriptionField = { label: __( 'Description' ), id: 'description', - render: ( { item } ) => { - return item.description && decodeEntities( item.description ); + render: function RenderDescription( { item } ) { + const defaultTemplateTypes = useAllDefaultTemplateTypes(); + const defaultTemplateType = defaultTemplateTypes.find( + ( type ) => type.slug === item.slug + ); + return item.description + ? decodeEntities( item.description ) + : defaultTemplateType?.description; }, enableSorting: false, enableGlobalSearch: true, @@ -107,6 +142,37 @@ 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: __( 'Status' ), + id: 'active', + getValue: ( { item } ) => item._isActive, + 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 = useAllDefaultTemplateTypes(); + const defaultTemplateType = defaultTemplateTypes.find( + ( type ) => type.slug === item.slug + ); + return ( + defaultTemplateType?.title || + // translators: %s is the slug of a custom template. + __( 'Custom' ) + ); + }, +}; diff --git a/packages/edit-site/src/components/page-templates/hooks.js b/packages/edit-site/src/components/page-templates/hooks.js index 99b05dacf8e1e0..fa84728795bc0d 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 033370f4ec449c..22b7bded399d17 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -3,11 +3,16 @@ */ 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 { addQueryArgs } from '@wordpress/url'; +import { useSelect } from '@wordpress/data'; +import { useEvent } from '@wordpress/compose'; /** * Internal dependencies @@ -22,16 +27,22 @@ 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'; const { usePostActions, templateTitleField } = unlock( editorPrivateApis ); const { useHistory, useLocation } = unlock( routerPrivateApis ); const { useEntityRecordsWithPermissions } = unlock( corePrivateApis ); -const EMPTY_ARRAY = []; - const defaultLayouts = { [ LAYOUT_TABLE ]: { showMedia: false, @@ -56,31 +67,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 ] ); @@ -98,23 +107,116 @@ 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 + ); + // 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_registered_template', { + per_page: -1, + } ); + + const activeTemplates = useMemo( () => { + const _active = [ ...staticRecords ].filter( + ( record ) => ! record.is_custom + ); + 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 ); + } + } + } + } + } + return _active; + }, [ 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 ) => { @@ -130,32 +232,27 @@ 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, - { - ...authorField, - elements: authors, - }, - ], - [ authors ] - ); + activeField, + slugField, + ]; + const elements = []; + for ( const author in users ) { + elements.push( { + value: users[ author ]?.id ?? author, + label: users[ author ]?.name ?? author, + } ); + } + _fields.push( { + ...authorField, + elements, + } ); + return _fields; + }, [ users ] ); const { data, paginationInfo } = useMemo( () => { return filterSortAndPaginate( records, view, fields ); @@ -166,9 +263,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 ) => { @@ -199,8 +300,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..b7fde2d056b2f3 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,11 +36,15 @@ function TemplateDataviewItem( { template, isActive } ) { export default function DataviewsTemplatesSidebarContent() { const { - query: { activeView = 'all' }, + query: { activeView = 'active' }, } = useLocation(); - const { records } = useEntityRecords( 'postType', TEMPLATE_POST_TYPE, { - per_page: -1, - } ); + const { records } = useEntityRecords( + 'postType', + 'wp_registered_template', + { + per_page: -1, + } + ); const firstItemPerAuthorText = useMemo( () => { const firstItemPerAuthor = records?.reduce( ( acc, template ) => { const author = template.author_text; @@ -61,9 +64,16 @@ export default function DataviewsTemplatesSidebarContent() { + { __( 'Active templates' ) } + + - { __( 'All 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 fe2210ae6e5e64..f1ab32b5e48d73 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'; @@ -27,6 +27,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 7711495b214773..55c557a2b47a77 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 @@ -5,33 +5,41 @@ import Editor from '../editor'; import SidebarNavigationScreenTemplatesBrowse from '../sidebar-navigation-screen-templates-browse'; import SidebarNavigationScreenUnsupported from '../sidebar-navigation-screen-unsupported'; +const areas = { + sidebar( { siteData } ) { + const isBlockTheme = siteData.currentTheme?.is_block_theme; + return isBlockTheme ? ( + + ) : ( + + ); + }, + mobile( { siteData } ) { + const isBlockTheme = siteData.currentTheme?.is_block_theme; + return isBlockTheme ? ( + + ) : ( + + ); + }, + preview( { siteData } ) { + const isBlockTheme = siteData.currentTheme?.is_block_theme; + return isBlockTheme ? ( + + ) : ( + + ); + }, +}; + export const templateItemRoute = { name: 'template-item', path: '/wp_template/*postId', - areas: { - sidebar( { siteData } ) { - const isBlockTheme = siteData.currentTheme?.is_block_theme; - return isBlockTheme ? ( - - ) : ( - - ); - }, - mobile( { siteData } ) { - const isBlockTheme = siteData.currentTheme?.is_block_theme; - return isBlockTheme ? ( - - ) : ( - - ); - }, - preview( { siteData } ) { - const isBlockTheme = siteData.currentTheme?.is_block_theme; - return isBlockTheme ? ( - - ) : ( - - ); - }, - }, + areas, +}; + +export const staticTemplateItemRoute = { + name: 'static-template-item', + path: '/wp_registered_template/*postId', + areas, }; 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 1aa82a828573f0..78ecb17c17c6f8 100644 --- a/packages/editor/src/components/post-template/hooks.js +++ b/packages/editor/src/components/post-template/hooks.js @@ -52,14 +52,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_registered_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 ) { @@ -71,7 +87,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 f934aa9fde90fa..55db84b93fcde1 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -220,7 +220,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_registered_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 82c2c8911c7c96..831d138734aa60 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -138,7 +138,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 012a4f5ed50661..78c17fe25dd101 100644 --- a/packages/fields/src/actions/duplicate-post.tsx +++ b/packages/fields/src/actions/duplicate-post.tsx @@ -56,10 +56,14 @@ const duplicatePost: Action< BasePost > = { return; } + const isTemplate = + item.type === 'wp_template' || + item.type === 'wp_registered_template'; + const newItemObject = { - 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' @@ -98,7 +102,9 @@ const duplicatePost: Action< BasePost > = { try { const newItem = await saveEntityRecord( 'postType', - item.type, + item.type === 'wp_registered_template' + ? 'wp_template' + : item.type, newItemObject, { throwOnError: true } ); diff --git a/packages/fields/src/actions/rename-post.tsx b/packages/fields/src/actions/rename-post.tsx index 5c79e07da0c8a5..4d0f6b27b30293 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. @@ -43,7 +38,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 ) @@ -51,15 +45,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 d110d73670314d..ff10ca983f77cb 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 585f1fce0ad056..31795981357da1 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -1046,7 +1046,7 @@ test.describe( 'Image - Site editor', () => { test.beforeEach( async ( { admin } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: 'wp_registered_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..96da063ba382a7 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_registered_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..0794a8fe5d34fe 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_registered_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_registered_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 7610e559c3ac03..925a9434f0950f 100644 --- a/test/e2e/specs/editor/various/pattern-overrides.spec.js +++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js @@ -253,7 +253,7 @@ test.describe( 'Pattern Overrides', () => { await admin.visitSiteEditor( { postId: 'emptytheme//index', - postType: 'wp_template', + postType: 'wp_registered_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 1135207583b152..d18033962ed4b9 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_registered_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..8dca99849d70f2 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_registered_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..f5b73f110f43b1 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=%2Fwp_registered_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..63dc5d8e509d46 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_registered_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_registered_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_registered_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_registered_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..754e71813129a9 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_registered_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..9306b3e27ca1d8 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_registered_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..3f427c00d31241 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_registered_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 7f70bc6da58973..84e25cdd9a0b94 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 b6f9f49aedeb7c..de96395c7ed6c0 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 ); } @@ -44,8 +46,15 @@ test.describe( 'Preload', () => { // To do: these should all be removed or preloaded. expect( requests ).toEqual( [ + // I'm not quite sure why this is needed, because it is preloaded. + // It might be that there's a request that invalides the resolver + // and then triggers a new request. + '/wp/v2/templates/lookup?slug=front-page', // Seems to be coming from `enableComplementaryArea`. '/wp/v2/users/me', + '/wp/v2/wp_template', + // This is the auto-draft template. + expect.stringMatching( /\/wp\/v2\/wp_template\/\d+\?context=edit/ ), // There are two separate settings OPTIONS requests. We should fix // so the one for canUser and getEntityRecord are reused. '/wp/v2/settings', 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..778829c17b99cd 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_registered_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..099222c131077b 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_registered_template', canvas: 'edit', } ); } ); @@ -63,7 +63,7 @@ test.describe( 'Settings sidebar', () => { await admin.visitSiteEditor( { postId: 'emptytheme//singular', - postType: 'wp_template', + postType: 'wp_registered_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..aa6a472204a6db 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_registered_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 580b1060227156..df6408ce24e75f 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 53de08717226a4..9a718119fb6e94 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_registered_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_registered_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_registered_template', canvas: 'edit', } ); await siteEditorStyleVariations.browseStyles(); @@ -144,7 +144,7 @@ test.describe( 'Global styles variations', () => { } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/style-variations//index', - postType: 'wp_template', + postType: 'wp_registered_template', canvas: 'edit', } ); await siteEditorStyleVariations.browseStyles(); @@ -174,7 +174,7 @@ test.describe( 'Global styles variations', () => { } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/style-variations//index', - postType: 'wp_template', + postType: 'wp_registered_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..0f33d8a7fce248 --- /dev/null +++ b/test/e2e/specs/site-editor/template-activate.spec.js @@ -0,0 +1,129 @@ +/** + * 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__field-value .is-success' + ); + + await page + .getByRole( 'button', { name: 'Index (Copy)', exact: true } ) + .first() + .click(); + + await expect( editor.canvas.getByText( 'gutenberg' ) ).toBeVisible(); + + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Copied from Index.' }, + } ); + + await editor.saveSiteEditorEntities( { + isOnlyCurrentEntityDirty: true, + } ); + + await page + .getByRole( 'button', { + name: 'View', + exact: true, + } ) + .click(); + + const [ previewPage ] = await Promise.all( [ + page.context().waitForEvent( 'page' ), + page + .getByRole( 'menuitem', { + name: 'View site', + } ) + .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 4f35d81737ae29..30ded7e834f8e3 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..61ce0b20570a3d 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_registered_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 8986215e0b222b..28ccba2004a33c 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_registered_template', canvas: 'edit', } ); } ); diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index 2c070acb69d3a8..cd462ec2c636cd 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(); /*