Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Fallback to customized Product Archive template if no better Category or Tag templates are present #5563

Closed
60 changes: 52 additions & 8 deletions src/BlockTemplatesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ protected function init() {
add_action( 'template_redirect', array( $this, 'render_block_template' ) );
add_filter( 'pre_get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
add_filter( 'get_block_templates', array( $this, 'add_block_templates' ), 10, 3 );
add_filter( 'taxonomy_template_hierarchy', array( BlockTemplateUtils::class, 'adjust_template_hierarchy' ), 10, 1 );
}

/**
Expand Down Expand Up @@ -113,8 +114,27 @@ public function get_block_file_template( $template, $id, $template_type ) {
return $template_built;
}

// Hand back over to Gutenberg if we can't find a template.
return $template;
$slugs = array( $slug );

if ( BlockTemplateUtils::template_is_eligible_for_product_archive_fallback( $slug ) ) {
$slugs[] = 'archive-product';
}

$available_templates = $this->get_block_templates_from_woocommerce(
$slugs,
$this->get_block_templates_from_db( $slugs, $template_type ),
$template_type
);

if ( is_array( $available_templates ) && count( $available_templates ) > 0 ) {
$existing_template = $available_templates[0];

return $existing_template instanceof \WP_Block_Template
? $existing_template
: BlockTemplateUtils::build_template_result_from_file( $existing_template, $existing_template->type );
} else {
return $template;
}
}

/**
Expand All @@ -130,8 +150,20 @@ public function add_block_templates( $query_result, $query, $template_type ) {
return $query_result;
}

$post_type = isset( $query['post_type'] ) ? $query['post_type'] : '';
$slugs = isset( $query['slug__in'] ) ? $query['slug__in'] : array();
$post_type = isset( $query['post_type'] ) ? $query['post_type'] : '';
$slugs = isset( $query['slug__in'] ) ? $query['slug__in'] : array();

if (
count(
array_filter(
$slugs,
array( BlockTemplateUtils::class, 'template_is_eligible_for_product_archive_fallback' )
)
) > 0
) {
$slugs[] = 'archive-product';
}

$template_files = $this->get_block_templates( $slugs, $template_type );

// @todo: Add apply_filters to _gutenberg_get_template_files() in Gutenberg to prevent duplication of logic.
Expand Down Expand Up @@ -261,11 +293,23 @@ function ( $template ) use ( $template_slug ) {
continue;
}

// If the theme has an archive-product.html template, but not a taxonomy-product_cat.html template let's use the themes archive-product.html template.
// Category and tags template are eligible to fallback to the Product Archive if available.
if ( BlockTemplateUtils::template_is_eligible_for_product_archive_fallback( $template_slug ) ) {
$template_file = BlockTemplateUtils::get_theme_template_path( 'archive-product' );
$templates[] = BlockTemplateUtils::create_new_block_template_object( $template_file, $template_type, $template_slug, true );
continue;
$customized_archive_template_index = array_search( 'archive-product', array_column( $already_found_templates, 'slug' ), true );

// If the `archive-product` has been customized by the user, and the theme does *not* have a more
// specific appropriate template, we clone the customized `archive-product`.
if ( false !== $customized_archive_template_index ) {
$customized_archive_template = $already_found_templates[ $customized_archive_template_index ];
$templates[] = BlockTemplateUtils::clone_template_with_new_slug( $customized_archive_template, $template_slug );
continue;

// If `archive-product` has not been customized, and theme has `archive-product`, we fallback to that.
} elseif ( BlockTemplateUtils::theme_has_template( 'archive-product' ) ) {
$template_file = BlockTemplateUtils::get_theme_template_path( 'archive-product' );
$templates[] = BlockTemplateUtils::create_new_block_template_object( $template_file, $template_type, $template_slug, true );
continue;
}
}

// At this point the template only exists in the Blocks filesystem and has not been saved in the DB,
Expand Down
59 changes: 57 additions & 2 deletions src/Utils/BlockTemplateUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,7 @@ public static function template_is_eligible_for_product_archive_fallback( $templ
$eligible_for_fallbacks = array( 'taxonomy-product_cat', 'taxonomy-product_tag' );

return in_array( $template_slug, $eligible_for_fallbacks, true )
&& ! self::theme_has_template( $template_slug )
&& self::theme_has_template( 'archive-product' );
&& ! self::theme_has_template( $template_slug );
}

/**
Expand Down Expand Up @@ -516,4 +515,60 @@ function( $template ) use ( $customised_template_slugs ) {
)
);
}

/**
* Clones a template and assigns a new slug and id
*
* @param object $template The template object to clone.
* @param string $slug The new slug to assign.
*
* @return object The cloned template.
*/
public static function clone_template_with_new_slug( $template, $slug ) {
$clone = clone $template;

$clone->slug = $slug;
$clone->title = self::convert_slug_to_title( $slug );

$template_name_parts = explode( '//', $clone->id );

if ( count( $template_name_parts ) > 1 ) {
$clone->id = $template_name_parts[0] . '//' . $slug;
} else {
$clone->id = $slug;
}

return $clone;
}

/**
* Adds the `archive-product` within the hierarchy of templates for taxonomies
*
* This is hooked into {@see 'taxonomy_template_hierarchy'} to add our fallback logic
* to the template hierarchy. Otherwise, the least specific template being considered
* for categories and tags, would be `taxonomy`. In our case, for eligible templates,
* we want to use `archive-product` too.
*
* @param string[] $template_hierarchy An ordered array of template names.
*
* @return string[] The hierarchy with `archive-product` added if is eligible.
*/
public static function adjust_template_hierarchy( $template_hierarchy ) {
$slugs = array_map(
'gutenberg_strip_template_file_suffix',
$template_hierarchy
);

if (
count(
array_filter(
$slugs,
array( __CLASS__, 'template_is_eligible_for_product_archive_fallback' )
)
) > 0 ) {
$template_hierarchy[] = 'archive-product.php';
}

return $template_hierarchy;
}
}
16 changes: 1 addition & 15 deletions tests/e2e/specs/backend/site-editing-templates.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
canvas,
deleteAllTemplates,
getCurrentSiteEditorContent,
insertBlock,
} from '@wordpress/e2e-test-utils';
import {
getNormalPagePermalink,
Expand All @@ -17,23 +16,10 @@ import {
getAllTemplates,
goToTemplateEditor,
goToTemplatesList,
saveTemplate,
useTheme,
visitTemplateAndAddCustomParagraph,
} from '../../utils';

async function visitTemplateAndAddCustomParagraph(
templateSlug,
customText = CUSTOMIZED_STRING
) {
await goToTemplateEditor( {
postId: `woocommerce/woocommerce//${ templateSlug }`,
} );

await insertBlock( 'Paragraph' );
await page.keyboard.type( customText );
await saveTemplate();
}

function blockSelector( id ) {
return `[data-type="${ id }"]`;
}
Expand Down
122 changes: 122 additions & 0 deletions tests/e2e/specs/backend/store-editing-template-fallbacks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { canvas, deleteAllTemplates } from '@wordpress/e2e-test-utils';
import {
BASE_URL,
DEFAULT_TIMEOUT,
getAllTemplates,
goToTemplateEditor,
goToTemplatesList,
SELECTORS,
useTheme,
visitTemplateAndAddCustomParagraph,
} from '../../utils';

function defaultTemplateProps( templateTitle ) {
return {
templateTitle,
addedBy: THEME_ID,
hasActions: false,
};
}

async function visitTemplateAndEditFirstParagraphBlock(
templateSlug,
newText
) {
await goToTemplateEditor( {
postId: `${ THEME_ID }//${ templateSlug }`,
} );

const BLOCK_SELECTOR = SELECTORS.blocks.byType( 'core/paragraph' );

await page.waitForSelector( BLOCK_SELECTOR, DEFAULT_TIMEOUT );
await page.$eval( BLOCK_SELECTOR, ( $el ) => ( $el.innerHTML = '' ) );
await page.type( BLOCK_SELECTOR, newText );
await saveTemplate();
}

const THEME_PARSED_ID = 'Theme with Woo Templates';
const THEME_ID = 'theme-with-woo-templates';

describe( 'Store Editing template fallbacks', () => {
useTheme( THEME_ID );

beforeAll( async () => {
await deleteAllTemplates( 'wp_template' );
await deleteAllTemplates( 'wp_template_part' );
} );

it( 'should use theme-provided `archive-product` template for missing category template', async () => {
const EXPECTED_TEMPLATE = defaultTemplateProps(
'Products by Category'
);

await goToTemplatesList();

const templates = await getAllTemplates();

try {
expect( templates ).toContainEqual( EXPECTED_TEMPLATE );
} catch ( ok ) {
expect( templates ).toContainEqual( {
...EXPECTED_TEMPLATE,
addedBy: THEME_PARSED_ID,
} );
}
} );

it( 'should use the same edits applied to the `archive-product` to the eligible templates', async () => {
const CUSTOMIZED_STRING = 'My awesome customization';

await visitTemplateAndAddCustomParagraph( 'archive-product', {
prefix: THEME_ID,
} );

await page.goto(
new URL( '/product-category/uncategorized', BASE_URL )
);

await expect( page ).toMatchElement( 'p', {
text: CUSTOMIZED_STRING,
timeout: DEFAULT_TIMEOUT,
} );
} );

it( 'should not use the edits on the `archive-product` for an eligible template if the theme provides it', async () => {
const CUSTOMIZED_STRING = 'My awesome customization';

await page.goto( new URL( '/product-tag/newest', BASE_URL ) );

await expect( page ).not.toMatchElement( 'p', {
text: CUSTOMIZED_STRING,
timeout: DEFAULT_TIMEOUT,
} );
} );

it.only( 'should use the edits applied specifically to the “cloned” category template over the ones to `archive-product`', async () => {
const CUSTOMIZED_STRING = 'My custom product category template';
// Note: `taxonomy-product_cat` is not provided by `theme-with-woo-templates` (or should not be).
// Instead, it is cloned at runtime and appears to the user as it is there, unless something overrides
// it in the hierarchy. As such, it is editable on its own.
await visitTemplateAndEditFirstParagraphBlock(
'taxonomy-product_cat',
CUSTOMIZED_STRING
);

await page.goto(
new URL( '/product-category/uncategorized', BASE_URL )
);

await expect( page ).toMatchElement( 'p', {
text: CUSTOMIZED_STRING,
timeout: DEFAULT_TIMEOUT,
} );
} );

it.todo(
'should use the original edits on `archive-product` even when a connected child template was modified'
);

it.todo(
'should correctly clear all customizations from “cloned” templates as well'
);
} );
33 changes: 32 additions & 1 deletion tests/e2e/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
visitAdminPage,
pressKeyWithModifier,
searchForBlock as searchForFSEBlock,
insertBlock,
} from '@wordpress/e2e-test-utils';
import { addQueryArgs } from '@wordpress/url';
import { WP_ADMIN_DASHBOARD } from '@woocommerce/e2e-utils';
Expand Down Expand Up @@ -41,7 +42,11 @@ export const GUTENBERG_EDITOR_CONTEXT =
process.env.GUTENBERG_EDITOR_CONTEXT || 'core';
export const DEFAULT_TIMEOUT = 30000;

const SELECTORS = {
export const SELECTORS = {
blocks: {
byType: ( blockType, index = 0 ) =>
`[data-type="${ blockType }"]:nth-of-type(${ index + 1 })`,
},
canvas: 'iframe[name="editor-canvas"]',
inserter: {
search:
Expand Down Expand Up @@ -431,3 +436,29 @@ export const createCoupon = async ( coupon ) => {

return createdCoupon;
};

/**
* Visits a template and adds a paragraph
*
* Useful util for simple template customization testing.
*
* @param {string} templateSlug The slug for the template to customize
* @param {Record<string, string>?} opts
* @param {string} opts.customText The custom text to add to the paragraph
* @param {string} opts.prefix The prefix for the template to customize
*/
export async function visitTemplateAndAddCustomParagraph(
templateSlug,
{ customText, prefix }
) {
customText = customText || 'My awesome customization';
prefix = prefix || 'woocommerce/woocommerce';

await goToTemplateEditor( {
postId: `${ prefix }//${ templateSlug }`,
} );

await insertBlock( 'Paragraph' );
await page.keyboard.type( customText );
await saveTemplate();
}

This file was deleted.

2 changes: 1 addition & 1 deletion tests/mocks/theme-with-woo-templates/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ Requires PHP: 5.6
Version: 1.0
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: themewithwootemplates
Text Domain: theme-with-woo-templates
*/