plugin updates

This commit is contained in:
Tony Volpe
2024-09-05 11:04:01 -04:00
parent ed6b060261
commit 50cd64dd3d
925 changed files with 16918 additions and 13003 deletions

View File

@@ -0,0 +1,673 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks\AIContent;
/**
* Patterns Dictionary class.
*/
class PatternsDictionary {
/**
* Returns the patterns' dictionary.
*
* @return array[]
*/
public static function get() {
return [
[
'name' => 'Banner',
'slug' => 'woocommerce-blocks/banner',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Up to 60% off', 'woocommerce' ),
'ai_prompt' => __( 'A four words title advertising the sale', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Holiday Sale', 'woocommerce' ),
'ai_prompt' => __( 'A two words label with the sale name', 'woocommerce' ),
],
[
'default' => __( 'Get your favorite vinyl at record-breaking prices.', 'woocommerce' ),
'ai_prompt' => __( 'The main description of the sale with at least 65 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop vinyl records', 'woocommerce' ),
'ai_prompt' => __( 'A 3 words button text to go to the sale page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Discount Banner',
'slug' => 'woocommerce-blocks/discount-banner',
'content' => [
'descriptions' => [
[
'default' => __( 'Select products', 'woocommerce' ),
'ai_prompt' => __( 'A two words description of the products on sale', 'woocommerce' ),
],
],
],
],
[
'name' => 'Discount Banner with Image',
'slug' => 'woocommerce-blocks/discount-banner-with-image',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'descriptions' => [
[
'default' => __( 'Select products', 'woocommerce' ),
'ai_prompt' => __( 'A two words description of the products on sale', 'woocommerce' ),
],
],
],
],
[
'name' => 'Featured Category Focus',
'slug' => 'woocommerce-blocks/featured-category-focus',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Black and white high-quality prints', 'woocommerce' ),
'ai_prompt' => __( 'The four words title of the featured category related to the following image description: [image.0]', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop prints', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the featured category', 'woocommerce' ),
],
],
],
],
[
'name' => 'Featured Category Triple',
'slug' => 'woocommerce-blocks/featured-category-triple',
'images_total' => 3,
'images_format' => 'portrait',
'content' => [
'titles' => [
[
'default' => __( 'Home decor', 'woocommerce' ),
'ai_prompt' => __( 'A one-word graphic title that encapsulates the essence of the business, inspired by the following image description: [image.0] and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image', 'woocommerce' ),
],
[
'default' => __( 'Retro photography', 'woocommerce' ),
'ai_prompt' => __( 'A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: [image.1] and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image', 'woocommerce' ),
],
[
'default' => __( 'Handmade gifts', 'woocommerce' ),
'ai_prompt' => __( 'A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: [image.2] and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image', 'woocommerce' ),
],
],
],
],
[
'name' => 'Featured Products: Fresh & Tasty',
'slug' => 'woocommerce-blocks/featured-products-fresh-and-tasty',
'images_total' => 4,
'images_format' => 'portrait',
'content' => [
'titles' => [
[
'default' => __( 'Fresh & tasty goods', 'woocommerce' ),
'ai_prompt' => __( 'The title of the featured products with at least 20 characters', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Sweet Organic Lemons', 'woocommerce' ),
'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.0]', 'woocommerce' ),
],
[
'default' => __( 'Fresh Organic Tomatoes', 'woocommerce' ),
'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.1]', 'woocommerce' ),
],
[
'default' => __( 'Fresh Lettuce (Washed)', 'woocommerce' ),
'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.2]', 'woocommerce' ),
],
[
'default' => __( 'Russet Organic Potatoes', 'woocommerce' ),
'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.3]', 'woocommerce' ),
],
],
],
],
[
'name' => 'Hero Product 3 Split',
'slug' => 'woocommerce-blocks/hero-product-3-split',
'images_total' => 1,
'images_format' => 'portrait',
'content' => [
'titles' => [
[
'default' => __( 'Timeless elegance', 'woocommerce' ),
'ai_prompt' => __( 'Write a two words title for advertising the store', 'woocommerce' ),
],
[
'default' => __( 'Durable glass', 'woocommerce' ),
'ai_prompt' => __( 'Write a two words title for advertising the store', 'woocommerce' ),
],
[
'default' => __( 'Versatile charm', 'woocommerce' ),
'ai_prompt' => __( 'Write a two words title for advertising the store', 'woocommerce' ),
],
[
'default' => __( 'New: Retro Glass Jug', 'woocommerce' ),
'ai_prompt' => __( 'Write a title with less than 20 characters for advertising the store', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Elevate your table with a 330ml Retro Glass Jug, blending classic design and durable hardened glass.', 'woocommerce' ),
'ai_prompt' => __( 'Write a text with approximately 130 characters, to describe a product the business is selling', 'woocommerce' ),
],
[
'default' => __( 'Crafted from resilient thick glass, this jug ensures lasting quality, making it perfect for everyday use with a touch of vintage charm.', 'woocommerce' ),
'ai_prompt' => __( 'Write a text with approximately 130 characters, to describe a product the business is selling', 'woocommerce' ),
],
[
'default' => __( "The Retro Glass Jug's classic silhouette effortlessly complements any setting, making it the ideal choice for serving beverages with style and flair.", 'woocommerce' ),
'ai_prompt' => __( 'Write a long text, with at least 130 characters, to describe a product the business is selling', 'woocommerce' ),
],
],
],
],
[
'name' => 'Hero Product Chessboard',
'slug' => 'woocommerce-blocks/hero-product-chessboard',
'images_total' => 2,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Quality Materials', 'woocommerce' ),
'ai_prompt' => __( 'A two words title describing the first displayed product feature', 'woocommerce' ),
],
[
'default' => __( 'Unique design', 'woocommerce' ),
'ai_prompt' => __( 'A two words title describing the second displayed product feature', 'woocommerce' ),
],
[
'default' => __( 'Make your house feel like home', 'woocommerce' ),
'ai_prompt' => __( 'A two words title describing the fourth displayed product feature', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'We use only the highest-quality materials in our products, ensuring that they look great and last for years to come.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the product feature with at least 115 characters', 'woocommerce' ),
],
[
'default' => __( 'From bold prints to intricate details, our products are a perfect combination of style and function.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the product feature with at least 115 characters', 'woocommerce' ),
],
[
'default' => __( 'Add a touch of charm and coziness this holiday season with a wide selection of hand-picked decorations — from minimalist vases to designer furniture.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the product feature with at least 115 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop home decor', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the product page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Hero Product Split',
'slug' => 'woocommerce-blocks/hero-product-split',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Keep dry with 50% off rain jackets', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the product the store is selling with at least 35 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Just Arrived Full Hero',
'slug' => 'woocommerce-blocks/just-arrived-full-hero',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Sound like no other', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 10 characters', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Experience your music like never before with our latest generation of hi-fidelity headphones.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the product collection with at least 35 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop now', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the product collection page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Collection Banner',
'slug' => 'woocommerce-blocks/product-collection-banner',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Brand New for the Holidays', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 25 characters related to the following image description: [image.0]', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Check out our brand new collection of holiday products and find the right gift for anyone.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the product collection with at least 90 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Collections Featured Collection',
'slug' => 'woocommerce-blocks/product-collections-featured-collection',
'content' => [
'titles' => [
[
'default' => "This week's popular products",
'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 30 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Collections Featured Collections',
'slug' => 'woocommerce-blocks/product-collections-featured-collections',
'images_total' => 4,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Tech gifts under $100', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the product collection with at least 20 characters related to the following image descriptions: [image.0], [image.1]', 'woocommerce' ),
],
[
'default' => __( 'For the gamers', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the product collection with at least 15 characters related to the following image descriptions: [image.2], [image.3]', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop tech', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the product collection page', 'woocommerce' ),
],
[
'default' => __( 'Shop games', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the product collection page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Collections Newest Arrivals',
'slug' => 'woocommerce-blocks/product-collections-newest-arrivals',
'content' => [
'titles' => [
[
'default' => __( 'Our newest arrivals', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 20 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'More new products', 'woocommerce' ),
'ai_prompt' => __( 'The button text to go to the product collection page with at least 15 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Collection 4 Columns',
'slug' => 'woocommerce-blocks/product-collection-4-columns',
'content' => [
'titles' => [
[
'default' => __( 'Staff picks', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 20 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Collection 5 Columns',
'slug' => 'woocommerce-blocks/product-collection-5-columns',
'content' => [
'titles' => [
[
'default' => __( 'Our latest and greatest', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase with that advertises the product collection with at least 20 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Gallery',
'slug' => 'woocommerce-blocks/product-query-product-gallery',
'content' => [
'titles' => [
[
'default' => __( 'Bestsellers', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the featured products with at least 10 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Featured Products 2 Columns',
'slug' => 'woocommerce-blocks/featured-products-2-cols',
'content' => [
'titles' => [
[
'default' => __( 'Fan favorites', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the featured products with at least 10 characters', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Get ready to start the season right. All the fan favorites in one place at the best price.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the featured products with at least 90 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop All', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the featured products page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Hero 2 Column 2 Row',
'slug' => 'woocommerce-blocks/product-hero-2-col-2-row',
'images_total' => 2,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'The Eden Jacket', 'woocommerce' ),
'ai_prompt' => __( 'A three words title that advertises a product related to the following image description: [image.0]', 'woocommerce' ),
],
[
'default' => __( '100% Woolen', 'woocommerce' ),
'ai_prompt' => __( 'A two words title that advertises a product feature', 'woocommerce' ),
],
[
'default' => __( 'Fits your wardrobe', 'woocommerce' ),
'ai_prompt' => __( 'A three words title that advertises a product feature', 'woocommerce' ),
],
[
'default' => __( 'Versatile', 'woocommerce' ),
'ai_prompt' => __( 'An one word title that advertises a product feature', 'woocommerce' ),
],
[
'default' => __( 'Normal Fit', 'woocommerce' ),
'ai_prompt' => __( 'A two words title that advertises a product feature', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Perfect for any look featuring a mid-rise, relax fitting silhouette.', 'woocommerce' ),
'ai_prompt' => __( 'The description of a product with at least 65 characters related to the following image: [image.0]', 'woocommerce' ),
],
[
'default' => __( 'Reflect your fashionable style.', 'woocommerce' ),
'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ),
],
[
'default' => __( 'Half tuck into your pants or layer over.', 'woocommerce' ),
'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ),
],
[
'default' => __( 'Button-down front for any type of mood or look.', 'woocommerce' ),
'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ),
],
[
'default' => __( '42% Cupro 34% Linen 24% Viscose', 'woocommerce' ),
'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'View product', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the product page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Shop by Price',
'slug' => 'woocommerce-blocks/shop-by-price',
'content' => [
'titles' => [
[
'default' => __( 'Outdoor Furniture & Accessories', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the first product collection with at least 30 characters', 'woocommerce' ),
],
[
'default' => __( 'Summer Dinning', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the second product collection with at least 20 characters', 'woocommerce' ),
],
[
'default' => "Women's Styles",
'ai_prompt' => __( 'An impact phrase that advertises the third product collection with at least 20 characters', 'woocommerce' ),
],
[
'default' => "Kids' Styles",
'ai_prompt' => __( 'An impact phrase that advertises the fourth product collection with at least 20 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Small Discount Banner with Image',
'slug' => 'woocommerce-blocks/small-discount-banner-with-image',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Chairs', 'woocommerce' ),
'ai_prompt' => __( 'A single word that advertises the product and is related to the following image description: [image.0]', 'woocommerce' ),
],
],
],
],
[
'name' => 'Social: Follow us on social media',
'slug' => 'woocommerce-blocks/social-follow-us-in-social-media',
'images_total' => 4,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Stay in the loop', 'woocommerce' ),
'ai_prompt' => __( 'A phrase that advertises the social media accounts of the store with at least 25 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Alternating Image and Text',
'slug' => 'woocommerce-blocks/alt-image-and-text',
'images_total' => 2,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Our products', 'woocommerce' ),
'ai_prompt' => __( 'A two words impact phrase that advertises the products', 'woocommerce' ),
],
[
'default' => __( 'Sustainable blends, stylish accessories', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the products with at least 40 characters and related to the following image description: [image.0]', 'woocommerce' ),
],
[
'default' => __( 'About us', 'woocommerce' ),
'ai_prompt' => __( 'A two words impact phrase that advertises the brand', 'woocommerce' ),
],
[
'default' => __( 'Committed to a greener lifestyle', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the brand with at least 50 characters related to the following image description: [image.1]', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Indulge in the finest organic coffee beans, teas, and hand-picked accessories, all locally sourced and sustainable for a mindful lifestyle.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the products with at least 180 characters', 'woocommerce' ),
],
[
'default' => "Our passion is crafting mindful moments with locally sourced, organic, and sustainable products. We're more than a store; we're your path to a community-driven, eco-friendly lifestyle that embraces premium quality.",
'ai_prompt' => __( 'A description of the products with at least 180 characters', 'woocommerce' ),
],
[
'default' => __( 'Locally sourced ingredients', 'woocommerce' ),
'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ),
],
[
'default' => __( 'Premium organic blends', 'woocommerce' ),
'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ),
],
[
'default' => __( 'Hand-picked accessories', 'woocommerce' ),
'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ),
],
[
'default' => __( 'Sustainable business practices', 'woocommerce' ),
'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Meet us', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the product page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Testimonials 3 Columns',
'slug' => 'woocommerce-blocks/testimonials-3-columns',
'content' => [
'titles' => [
[
'default' => __( 'Eclectic finds, ethical delights', 'woocommerce' ),
'ai_prompt' => __( 'Write a short title advertising a testimonial from a customer', 'woocommerce' ),
],
[
'default' => __( 'Sip, Shop, Savor', 'woocommerce' ),
'ai_prompt' => __( 'Write a short title advertising a testimonial from a customer', 'woocommerce' ),
],
[
'default' => __( 'LOCAL LOVE', 'woocommerce' ),
'ai_prompt' => __( 'Write a short title advertising a testimonial from a customer', 'woocommerce' ),
],
[
'default' => __( 'What our customers say', 'woocommerce' ),
'ai_prompt' => __( 'Write just 4 words to advertise testimonials from customers', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Transformed my daily routine with unique, eco-friendly treasures. Exceptional quality and service. Proud to support a store that aligns with my values.', 'woocommerce' ),
'ai_prompt' => __( 'Write the testimonial from a customer with approximately 150 characters', 'woocommerce' ),
],
[
'default' => __( 'The organic coffee beans are a revelation. Each sip feels like a journey. Beautifully crafted accessories add a touch of elegance to my home.', 'woocommerce' ),
'ai_prompt' => __( 'Write the testimonial from a customer with approximately 150 characters', 'woocommerce' ),
],
[
'default' => __( 'From sustainably sourced teas to chic vases, this store is a treasure trove. Love knowing my purchases contribute to a greener planet.', 'woocommerce' ),
'ai_prompt' => __( 'Write the testimonial from a customer with approximately 150 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Testimonials Single',
'slug' => 'woocommerce-blocks/testimonials-single',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'A brewtiful experience :-)', 'woocommerce' ),
'ai_prompt' => __( 'A two words title that advertises the testimonial', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Exceptional flavors, sustainable choices. The carefully curated collection of coffee pots and accessories turned my kitchen into a haven of style and taste.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the testimonial with at least 225 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Featured Category Cover Image',
'slug' => 'woocommerce-blocks/featured-category-cover-image',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Sit back and relax', 'woocommerce' ),
'ai_prompt' => __( 'A description for a product with at least 20 characters', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'With a wide range of designer chairs to elevate your living space.', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the products with at least 55 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop chairs', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the shop page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Collection: Featured Products 5 Columns',
'slug' => 'woocommerce-blocks/product-collection-featured-products-5-columns',
'content' => [
'titles' => [
[
'default' => __( 'Shop new arrivals', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the newest additions to the store with at least 20 characters', 'woocommerce' ),
],
],
],
],
];
}
}

View File

@@ -63,7 +63,7 @@ class PatternsHelper {
/**
* Upsert the patterns AI data.
*
* @param array $patterns_dictionary The patterns dictionary.
* @param array $patterns_dictionary The patterns' dictionary.
*
* @return WP_Error|null
*/
@@ -92,18 +92,12 @@ class PatternsHelper {
* @return array|WP_Error Returns pattern dictionary or WP_Error on failure.
*/
public static function get_patterns_dictionary( $pattern_slug = null ) {
$patterns_dictionary_file = plugin_dir_path( __FILE__ ) . 'dictionary.json';
$default_patterns_dictionary = PatternsDictionary::get();
if ( ! file_exists( $patterns_dictionary_file ) ) {
if ( empty( $default_patterns_dictionary ) ) {
return new WP_Error( 'missing_patterns_dictionary', __( 'The patterns dictionary is missing.', 'woocommerce' ) );
}
$default_patterns_dictionary = wp_json_file_decode( $patterns_dictionary_file, array( 'associative' => true ) );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return new WP_Error( 'json_decode_error', __( 'Error decoding JSON.', 'woocommerce' ) );
}
$patterns_dictionary = '';
$ai_connection_allowed = get_option( 'woocommerce_blocks_allow_ai_connection' );

View File

@@ -402,13 +402,13 @@ class UpdatePatterns {
* @return mixed|WP_Error|null
*/
public static function get_patterns_dictionary() {
$patterns_dictionary = plugin_dir_path( __FILE__ ) . 'dictionary.json';
$patterns_dictionary = PatternsDictionary::get();
if ( ! file_exists( $patterns_dictionary ) ) {
if ( empty( $patterns_dictionary ) ) {
return new WP_Error( 'missing_patterns_dictionary', __( 'The patterns dictionary is missing.', 'woocommerce' ) );
}
return wp_json_file_decode( $patterns_dictionary, array( 'associative' => true ) );
return $patterns_dictionary;
}
/**

View File

@@ -26,7 +26,7 @@ class UpdateProducts {
'price' => 249,
],
[
'title' => 'Black and White Summer Portrait',
'title' => 'Black and White',
'image' => 'assets/images/pattern-placeholders/white-black-black-and-white-photograph-monochrome-photography.jpg',
'description' => 'This 24" x 30" high-quality print just exudes summer. Hang it on the wall and forget about the world outside.',
'price' => 115,
@@ -113,7 +113,7 @@ class UpdateProducts {
$products_to_create = max( 0, 6 - $real_products_count - $dummy_products_count );
while ( $products_to_create > 0 ) {
$this->create_new_product( self::DUMMY_PRODUCTS[ $products_to_create - 1 ] );
$products_to_create--;
--$products_to_create;
}
// Identify dummy products that need to have their content updated.
@@ -327,7 +327,7 @@ class UpdateProducts {
public function assign_ai_selected_images_to_dummy_products( $dummy_products_to_update, $ai_selected_images ) {
$products_information_list = [];
$dummy_products_count = count( $dummy_products_to_update );
for ( $i = 0; $i < $dummy_products_count; $i ++ ) {
for ( $i = 0; $i < $dummy_products_count; $i++ ) {
$image_src = $ai_selected_images[ $i ]['URL'] ?? '';
if ( wc_is_valid_url( $image_src ) ) {
@@ -396,7 +396,7 @@ class UpdateProducts {
$ai_request_retries = 0;
$success = false;
while ( $ai_request_retries < 5 && ! $success ) {
$ai_request_retries ++;
++$ai_request_retries;
$ai_response = $ai_connection->fetch_ai_response( $token, $formatted_prompt, 30 );
if ( is_wp_error( $ai_response ) ) {
continue;
@@ -464,7 +464,7 @@ class UpdateProducts {
$this->product_update( $product, $product_image_id, self::DUMMY_PRODUCTS[ $i ]['title'], self::DUMMY_PRODUCTS[ $i ]['description'], self::DUMMY_PRODUCTS[ $i ]['price'] );
$i++;
++$i;
}
}

View File

@@ -1,656 +0,0 @@
[
{
"name": "Banner",
"slug": "woocommerce-blocks/banner",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Up to 60% off",
"ai_prompt": "A four words title advertising the sale"
}
],
"descriptions": [
{
"default": "Holiday Sale",
"ai_prompt": "A two words label with the sale name"
},
{
"default": "Get your favorite vinyl at record-breaking prices.",
"ai_prompt": "The main description of the sale with at least 65 characters"
}
],
"buttons": [
{
"default": "Shop vinyl records",
"ai_prompt": "A 3 words button text to go to the sale page"
}
]
}
},
{
"name": "Discount Banner",
"slug": "woocommerce-blocks/discount-banner",
"content": {
"descriptions": [
{
"default": "Select products",
"ai_prompt": "A two words description of the products on sale"
}
]
}
},
{
"name": "Discount Banner with Image",
"slug": "woocommerce-blocks/discount-banner-with-image",
"images_total": 1,
"images_format": "landscape",
"content": {
"descriptions": [
{
"default": "Select products",
"ai_prompt": "A two words description of the products on sale"
}
]
}
},
{
"name": "Featured Category Focus",
"slug": "woocommerce-blocks/featured-category-focus",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Black and white high-quality prints",
"ai_prompt": "The four words title of the featured category related to the following image description: {image.0}"
}
],
"buttons": [
{
"default": "Shop prints",
"ai_prompt": "A two words button text to go to the featured category"
}
]
}
},
{
"name": "Featured Category Triple",
"slug": "woocommerce-blocks/featured-category-triple",
"images_total": 3,
"images_format": "portrait",
"content": {
"titles": [
{
"default": "Home decor",
"ai_prompt": "A one-word graphic title that encapsulates the essence of the business, inspired by the following image description: {image.0} and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image"
},
{
"default": "Retro photography",
"ai_prompt": "A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: {image.1} and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image"
},
{
"default": "Handmade gifts",
"ai_prompt": "A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: {image.2} and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image"
}
]
}
},
{
"name": "Featured Products: Fresh & Tasty",
"slug": "woocommerce-blocks/featured-products-fresh-and-tasty",
"images_total": 4,
"images_format": "portrait",
"content": {
"titles": [
{
"default": "Fresh & tasty goods",
"ai_prompt": "The title of the featured products with at least 20 characters"
}
],
"descriptions": [
{
"default": "Sweet Organic Lemons",
"ai_prompt": "The three words description of the featured product related to the following image description: {image.0}"
},
{
"default": "Fresh Organic Tomatoes",
"ai_prompt": "The three words description of the featured product related to the following image description: {image.1}"
},
{
"default": "Fresh Lettuce (Washed)",
"ai_prompt": "The three words description of the featured product related to the following image description: {image.2}"
},
{
"default": "Russet Organic Potatoes",
"ai_prompt": "The three words description of the featured product related to the following image description: {image.3}"
}
]
}
},
{
"name": "Hero Product 3 Split",
"slug": "woocommerce-blocks/hero-product-3-split",
"images_total": 1,
"images_format": "portrait",
"content": {
"titles": [
{
"default": "Timeless elegance",
"ai_prompt": "Write a two words title for advertising the store"
},
{
"default": "Durable glass",
"ai_prompt": "Write a two words title for advertising the store"
},
{
"default": "Versatile charm",
"ai_prompt": "Write a two words title for advertising the store"
},
{
"default": "New: Retro Glass Jug",
"ai_prompt": "Write a title with less than 20 characters for advertising the store"
}
],
"descriptions": [
{
"default": "Elevate your table with a 330ml Retro Glass Jug, blending classic design and durable hardened glass.",
"ai_prompt": "Write a text with approximately 130 characters, to describe a product the business is selling"
},
{
"default": "Crafted from resilient thick glass, this jug ensures lasting quality, making it perfect for everyday use with a touch of vintage charm.",
"ai_prompt": "Write a text with approximately 130 characters, to describe a product the business is selling"
},
{
"default": "The Retro Glass Jug's classic silhouette effortlessly complements any setting, making it the ideal choice for serving beverages with style and flair.",
"ai_prompt": "Write a long text, with at least 130 characters, to describe a product the business is selling"
}
]
}
},
{
"name": "Hero Product Chessboard",
"slug": "woocommerce-blocks/hero-product-chessboard",
"images_total": 2,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Quality Materials",
"ai_prompt": "A two words title describing the first displayed product feature"
},
{
"default": "Unique design",
"ai_prompt": "A two words title describing the second displayed product feature"
},
{
"default": "Make your house feel like home",
"ai_prompt": "A two words title describing the fourth displayed product feature"
}
],
"descriptions": [
{
"default": "We use only the highest-quality materials in our products, ensuring that they look great and last for years to come.",
"ai_prompt": "A description of the product feature with at least 115 characters"
},
{
"default": "From bold prints to intricate details, our products are a perfect combination of style and function.",
"ai_prompt": "A description of the product feature with at least 115 characters"
},
{
"default": "Add a touch of charm and coziness this holiday season with a wide selection of hand-picked decorations — from minimalist vases to designer furniture.",
"ai_prompt": "A description of the product feature with at least 115 characters"
}
],
"buttons": [
{
"default": "Shop home decor",
"ai_prompt": "A two words button text to go to the product page"
}
]
}
},
{
"name": "Hero Product Split",
"slug": "woocommerce-blocks/hero-product-split",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Keep dry with 50% off rain jackets",
"ai_prompt": "An impact phrase that advertises the product the store is selling with at least 35 characters"
}
]
}
},
{
"name": "Just Arrived Full Hero",
"slug": "woocommerce-blocks/just-arrived-full-hero",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Sound like no other",
"ai_prompt": "An impact phrase that advertises the displayed product collection with at least 10 characters"
}
],
"descriptions": [
{
"default": "Experience your music like never before with our latest generation of hi-fidelity headphones.",
"ai_prompt": "A description of the product collection with at least 35 characters"
}
],
"buttons": [
{
"default": "Shop now",
"ai_prompt": "A two words button text to go to the product collection page"
}
]
}
},
{
"name": "Product Collection Banner",
"slug": "woocommerce-blocks/product-collection-banner",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Brand New for the Holidays",
"ai_prompt": "An impact phrase that advertises the displayed product collection with at least 25 characters related to the following image description: {image.0}"
}
],
"descriptions": [
{
"default": "Check out our brand new collection of holiday products and find the right gift for anyone.",
"ai_prompt": "A description of the product collection with at least 90 characters"
}
]
}
},
{
"name": "Product Collections Featured Collection",
"slug": "woocommerce-blocks/product-collections-featured-collection",
"content": {
"titles": [
{
"default": "This week's popular products",
"ai_prompt": "An impact phrase that advertises the displayed product collection with at least 30 characters"
}
]
}
},
{
"name": "Product Collections Featured Collections",
"slug": "woocommerce-blocks/product-collections-featured-collections",
"images_total": 4,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Tech gifts under $100",
"ai_prompt": "An impact phrase that advertises the product collection with at least 20 characters related to the following image descriptions: {image.0}, {image.1}"
},
{
"default": "For the gamers",
"ai_prompt": "An impact phrase that advertises the product collection with at least 15 characters related to the following image descriptions: {image.2}, {image.3}"
}
],
"buttons": [
{
"default": "Shop tech",
"ai_prompt": "A two words button text to go to the product collection page"
},
{
"default": "Shop games",
"ai_prompt": "A two words button text to go to the product collection page"
}
]
}
},
{
"name": "Product Collections Newest Arrivals",
"slug": "woocommerce-blocks/product-collections-newest-arrivals",
"content": {
"titles": [
{
"default": "Our newest arrivals",
"ai_prompt": "An impact phrase that advertises the displayed product collection with at least 20 characters"
}
],
"buttons": [
{
"default": "More new products",
"ai_prompt": "The button text to go to the product collection page with at least 15 characters"
}
]
}
},
{
"name": "Product Collection 4 Columns",
"slug": "woocommerce-blocks/product-collection-4-columns",
"content": {
"titles": [
{
"default": "Staff picks",
"ai_prompt": "An impact phrase that advertises the displayed product collection with at least 20 characters"
}
]
}
},
{
"name": "Product Collection 5 Columns",
"slug": "woocommerce-blocks/product-collection-5-columns",
"content": {
"titles": [
{
"default": "Our latest and greatest",
"ai_prompt": "An impact phrase with that advertises the product collection with at least 20 characters"
}
]
}
},
{
"name": "Product Gallery",
"slug": "woocommerce-blocks/product-query-product-gallery",
"content": {
"titles": [
{
"default": "Bestsellers",
"ai_prompt": "An impact phrase that advertises the featured products with at least 10 characters"
}
]
}
},
{
"name": "Featured Products 2 Columns",
"slug": "woocommerce-blocks/featured-products-2-cols",
"content": {
"titles": [
{
"default": "Fan favorites",
"ai_prompt": "An impact phrase that advertises the featured products with at least 10 characters"
}
],
"descriptions": [
{
"default": "Get ready to start the season right. All the fan favorites in one place at the best price.",
"ai_prompt": "A description of the featured products with at least 90 characters"
}
],
"buttons": [
{
"default": "Shop All",
"ai_prompt": "A two words button text to go to the featured products page"
}
]
}
},
{
"name": "Product Hero 2 Column 2 Row",
"slug": "woocommerce-blocks/product-hero-2-col-2-row",
"images_total": 2,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "The Eden Jacket",
"ai_prompt": "A three words title that advertises a product related to the following image description: {image.0}"
},
{
"default": "100% Woolen",
"ai_prompt": "A two words title that advertises a product feature"
},
{
"default": "Fits your wardrobe",
"ai_prompt": "A three words title that advertises a product feature"
},
{
"default": "Versatile",
"ai_prompt": "An one word title that advertises a product feature"
},
{
"default": "Normal Fit",
"ai_prompt": "A two words title that advertises a product feature"
}
],
"descriptions": [
{
"default": "Perfect for any look featuring a mid-rise, relax fitting silhouette.",
"ai_prompt": "The description of a product with at least 65 characters related to the following image: {image.0}"
},
{
"default": "Reflect your fashionable style.",
"ai_prompt": "The description of a product feature with at least 30 characters"
},
{
"default": "Half tuck into your pants or layer over.",
"ai_prompt": "The description of a product feature with at least 30 characters"
},
{
"default": "Button-down front for any type of mood or look.",
"ai_prompt": "The description of a product feature with at least 30 characters"
},
{
"default": "42% Cupro 34% Linen 24% Viscose",
"ai_prompt": "The description of a product feature with at least 30 characters"
}
],
"buttons": [
{
"default": "View product",
"ai_prompt": "A two words button text to go to the product page"
}
]
}
},
{
"name": "Shop by Price",
"slug": "woocommerce-blocks/shop-by-price",
"content": {
"titles": [
{
"default": "Outdoor Furniture & Accessories",
"ai_prompt": "An impact phrase that advertises the first product collection with at least 30 characters"
},
{
"default": "Summer Dinning",
"ai_prompt": "An impact phrase that advertises the second product collection with at least 20 characters"
},
{
"default": "Women's Styles",
"ai_prompt": "An impact phrase that advertises the third product collection with at least 20 characters"
},
{
"default": "Kids' Styles",
"ai_prompt": "An impact phrase that advertises the fourth product collection with at least 20 characters"
}
]
}
},
{
"name": "Small Discount Banner with Image",
"slug": "woocommerce-blocks/small-discount-banner-with-image",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Chairs",
"ai_prompt": "A single word that advertises the product and is related to the following image description: {image.0}"
}
]
}
},
{
"name": "Social: Follow us on social media",
"slug": "woocommerce-blocks/social-follow-us-in-social-media",
"images_total": 4,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Stay in the loop",
"ai_prompt": "A phrase that advertises the social media accounts of the store with at least 25 characters"
}
]
}
},
{
"name": "Alternating Image and Text",
"slug": "woocommerce-blocks/alt-image-and-text",
"images_total": 2,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Our products",
"ai_prompt": "A two words impact phrase that advertises the products"
},
{
"default": "Sustainable blends, stylish accessories",
"ai_prompt": "An impact phrase that advertises the products with at least 40 characters and related to the following image description: {image.0}"
},
{
"default": "About us",
"ai_prompt": "A two words impact phrase that advertises the brand"
},
{
"default": "Committed to a greener lifestyle",
"ai_prompt": "An impact phrase that advertises the brand with at least 50 characters related to the following image description: {image.1}"
}
],
"descriptions": [
{
"default": "Indulge in the finest organic coffee beans, teas, and hand-picked accessories, all locally sourced and sustainable for a mindful lifestyle.",
"ai_prompt": "A description of the products with at least 180 characters"
},
{
"default": "Our passion is crafting mindful moments with locally sourced, organic, and sustainable products. We're more than a store; we're your path to a community-driven, eco-friendly lifestyle that embraces premium quality.",
"ai_prompt": "A description of the products with at least 180 characters"
},
{
"default": "Locally sourced ingredients",
"ai_prompt": "A three word description of the products"
},
{
"default": "Premium organic blends",
"ai_prompt": "A three word description of the products"
},
{
"default": "Hand-picked accessories",
"ai_prompt": "A three word description of the products"
},
{
"default": "Sustainable business practices",
"ai_prompt": "A three word description of the products"
}
],
"buttons": [
{
"default": "Meet us",
"ai_prompt": "A two words button text to go to the product page"
}
]
}
},
{
"name": "Testimonials 3 Columns",
"slug": "woocommerce-blocks/testimonials-3-columns",
"content": {
"titles": [
{
"default": "Eclectic finds, ethical delights",
"ai_prompt": "Write a short title advertising a testimonial from a customer"
},
{
"default": "Sip, Shop, Savor",
"ai_prompt": "Write a short title advertising a testimonial from a customer"
},
{
"default": "LOCAL LOVE",
"ai_prompt": "Write a short title advertising a testimonial from a customer"
},
{
"default": "What our customers say",
"ai_prompt": "Write just 4 words to advertise testimonials from customers"
}
],
"descriptions": [
{
"default": "Transformed my daily routine with unique, eco-friendly treasures. Exceptional quality and service. Proud to support a store that aligns with my values.",
"ai_prompt": "Write the testimonial from a customer with approximately 150 characters"
},
{
"default": "The organic coffee beans are a revelation. Each sip feels like a journey. Beautifully crafted accessories add a touch of elegance to my home.",
"ai_prompt": "Write the testimonial from a customer with approximately 150 characters"
},
{
"default": "From sustainably sourced teas to chic vases, this store is a treasure trove. Love knowing my purchases contribute to a greener planet.",
"ai_prompt": "Write the testimonial from a customer with approximately 150 characters"
}
]
}
},
{
"name": "Testimonials Single",
"slug": "woocommerce-blocks/testimonials-single",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "A brewtiful experience :-)",
"ai_prompt": "A two words title that advertises the testimonial"
}
],
"descriptions": [
{
"default": "Exceptional flavors, sustainable choices. The carefully curated collection of coffee pots and accessories turned my kitchen into a haven of style and taste.",
"ai_prompt": "A description of the testimonial with at least 225 characters"
}
]
}
},
{
"name": "Featured Category Cover Image",
"slug": "woocommerce-blocks/featured-category-cover-image",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Sit back and relax",
"ai_prompt": "A description for a product with at least 20 characters"
}
],
"descriptions": [
{
"default": "With a wide range of designer chairs to elevate your living space.",
"ai_prompt": "An impact phrase that advertises the products with at least 55 characters"
}
],
"buttons": [
{
"default": "Shop chairs",
"ai_prompt": "A two words button text to go to the shop page"
}
]
}
},
{
"name": "Product Collection: Featured Products 5 Columns",
"slug": "woocommerce-blocks/product-collection-featured-products-5-columns",
"content": {
"titles": [
{
"default": "Shop new arrivals",
"ai_prompt": "An impact phrase that advertises the newest additions to the store with at least 20 characters"
}
]
}
}
]

View File

@@ -40,6 +40,7 @@ final class AssetsController {
add_action( 'admin_enqueue_scripts', array( $this, 'update_block_style_dependencies' ), 20 );
add_action( 'wp_enqueue_scripts', array( $this, 'update_block_settings_dependencies' ), 100 );
add_action( 'admin_enqueue_scripts', array( $this, 'update_block_settings_dependencies' ), 100 );
add_filter( 'js_do_concat', array( $this, 'skip_boost_minification_for_cart_checkout' ), 10, 2 );
}
/**
@@ -62,9 +63,14 @@ final class AssetsController {
// The price package is shared externally so has no blocks prefix.
$this->api->register_script( 'wc-price-format', 'assets/client/blocks/price-format.js', array(), false );
$this->api->register_script( 'wc-blocks-vendors-frontend', $this->api->get_block_asset_build_path( 'wc-blocks-vendors-frontend' ), array(), false );
$this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js', array( 'wc-blocks-vendors-frontend' ) );
$this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js', array( 'wc-blocks-vendors-frontend' ) );
// Vendor scripts for blocks frontends (not including cart and checkout).
$this->api->register_script( 'wc-blocks-frontend-vendors', $this->api->get_block_asset_build_path( 'wc-blocks-frontend-vendors-frontend' ), array(), true );
// Cart and checkout frontend scripts.
$this->api->register_script( 'wc-cart-checkout-vendors', $this->api->get_block_asset_build_path( 'wc-cart-checkout-vendors-frontend' ), array(), true );
$this->api->register_script( 'wc-cart-checkout-base', $this->api->get_block_asset_build_path( 'wc-cart-checkout-base-frontend' ), array(), true );
$this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js' );
$this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js' );
// Register the interactivity components here for now.
$this->api->register_script( 'wc-interactivity-dropdown', 'assets/client/blocks/wc-interactivity-dropdown.js', array() );
@@ -253,6 +259,23 @@ final class AssetsController {
return $src;
}
/**
* Skip Jetpack Boost minification on older versions of Jetpack Boost where it causes issues.
*
* @param mixed $do_concat Whether to concatenate the script or not.
* @param mixed $handle The script handle.
* @return mixed
*/
public function skip_boost_minification_for_cart_checkout( $do_concat, $handle ) {
$boost_is_outdated = defined( 'JETPACK_BOOST_VERSION' ) && version_compare( JETPACK_BOOST_VERSION, '3.4.2', '<' );
$scripts_to_ignore = [
'wc-cart-checkout-vendors',
'wc-cart-checkout-base',
];
return $boost_is_outdated && in_array( $handle, $scripts_to_ignore, true ) ? false : $do_concat;
}
/**
* Add body classes to the frontend and within admin.
*

View File

@@ -34,6 +34,8 @@ use WP_Error;
* @internal
*/
class BlockPatterns {
const CATEGORIES_PREFIXES = [ '_woo_', '_dotcom_imported_' ];
/**
* Path to the patterns' directory.
*
@@ -145,6 +147,8 @@ class BlockPatterns {
return;
}
$patterns = $this->parse_categories( $patterns );
foreach ( $patterns as $pattern ) {
$pattern['slug'] = $pattern['name'];
$pattern['content'] = $pattern['html'];
@@ -152,4 +156,33 @@ class BlockPatterns {
$this->pattern_registry->register_block_pattern( $pattern['ID'], $pattern, $this->dictionary );
}
}
/**
* Parse prefixed categories from the PTK patterns into the actual WooCommerce categories.
*
* @param array $patterns The patterns to parse.
* @return array The parsed patterns.
*/
private function parse_categories( array $patterns ) {
return array_map(
function ( $pattern ) {
$pattern['categories'] = array_map(
function ( $category ) {
foreach ( self::CATEGORIES_PREFIXES as $prefix ) {
if ( strpos( $category['title'], $prefix ) !== false ) {
$parsed_category = str_replace( $prefix, '', $category['title'] );
$parsed_category = str_replace( '_', ' ', $parsed_category );
$category['title'] = ucfirst( $parsed_category );
}
}
return $category;
},
$pattern['categories']
);
return $pattern;
},
$patterns
);
}
}

View File

@@ -28,76 +28,9 @@ class BlockTemplatesController {
add_filter( 'get_block_template', array( $this, 'add_block_template_details' ), 10, 3 );
add_filter( 'get_block_templates', array( $this, 'add_block_templates' ), 10, 3 );
add_filter( 'taxonomy_template_hierarchy', array( $this, 'add_archive_product_to_eligible_for_fallback_templates' ), 10, 1 );
add_action( 'after_switch_theme', array( $this, 'check_should_use_blockified_product_grid_templates' ), 10, 2 );
if ( wc_current_theme_is_fse_theme() ) {
// By default, the Template Part Block only supports template parts that are in the current theme directory.
// This render_callback wrapper allows us to add support for plugin-housed template parts.
add_filter(
'block_type_metadata_settings',
function ( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/template-part' === $metadata['name'] &&
in_array( $settings['render_callback'], array( 'render_block_core_template_part', 'gutenberg_render_block_core_template_part' ), true )
) {
$settings['render_callback'] = array( $this, 'render_woocommerce_template_part' );
}
return $settings;
},
10,
2
);
// Prevents shortcodes in templates having their HTML content broken by wpautop.
// @see https://core.trac.wordpress.org/ticket/58366 for more info.
add_filter(
'block_type_metadata_settings',
function ( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/shortcode' === $metadata['name']
) {
$settings['original_render_callback'] = $settings['render_callback'];
$settings['render_callback'] = function ( $attributes, $content ) use ( $settings ) {
// The shortcode has already been rendered, so look for the cart/checkout HTML.
if ( strstr( $content, 'woocommerce-cart-form' ) || strstr( $content, 'wc-empty-cart-message' ) || strstr( $content, 'woocommerce-checkout-form' ) ) {
// Return early before wpautop runs again.
return $content;
}
$render_callback = $settings['original_render_callback'];
return $render_callback( $attributes, $content );
};
}
return $settings;
},
10,
2
);
/**
* Prevents the pages that are assigned as cart/checkout from showing the "template" selector in the page-editor.
* We want to avoid this flow and point users towards the site editor instead.
*/
add_action(
'current_screen',
function () {
if ( ! is_admin() ) {
return;
}
$current_screen = get_current_screen();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $current_screen && 'page' === $current_screen->id && ! empty( $_GET['post'] ) && in_array( absint( $_GET['post'] ), array( wc_get_page_id( 'cart' ), wc_get_page_id( 'checkout' ) ), true ) ) {
wp_add_inline_style( 'wc-blocks-editor-style', '.edit-post-post-template { display: none; }' );
}
},
10
);
}
add_filter( 'block_type_metadata_settings', array( $this, 'add_plugin_templates_parts_support' ), 10, 2 );
add_filter( 'block_type_metadata_settings', array( $this, 'prevent_shortcodes_html_breakage' ), 10, 2 );
add_action( 'current_screen', array( $this, 'hide_template_selector_in_cart_checkout_pages' ), 10 );
}
/**
@@ -214,22 +147,69 @@ class BlockTemplatesController {
}
/**
* Checks the old and current themes and determines if the "wc_blocks_use_blockified_product_grid_block_as_template"
* option need to be updated accordingly.
* By default, the Template Part Block only supports template parts that are in the current theme directory.
* This render_callback wrapper allows us to add support for plugin-housed template parts.
*
* @param array $settings Array of determined settings for registering a block type.
* @param array $metadata Metadata provided for registering a block type.
*/
public function add_plugin_templates_parts_support( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/template-part' === $metadata['name'] &&
in_array( $settings['render_callback'], array( 'render_block_core_template_part', 'gutenberg_render_block_core_template_part' ), true )
) {
$settings['render_callback'] = array( $this, 'render_woocommerce_template_part' );
}
return $settings;
}
/**
* Prevents shortcodes in templates having their HTML content broken by wpautop.
*
* @see https://core.trac.wordpress.org/ticket/58366 for more info.
*
* @param array $settings Array of determined settings for registering a block type.
* @param array $metadata Metadata provided for registering a block type.
*/
public function prevent_shortcodes_html_breakage( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/shortcode' === $metadata['name']
) {
$settings['original_render_callback'] = $settings['render_callback'];
$settings['render_callback'] = function ( $attributes, $content ) use ( $settings ) {
// The shortcode has already been rendered, so look for the cart/checkout HTML.
if ( strstr( $content, 'woocommerce-cart-form' ) || strstr( $content, 'wc-empty-cart-message' ) || strstr( $content, 'woocommerce-checkout-form' ) ) {
// Return early before wpautop runs again.
return $content;
}
$render_callback = $settings['original_render_callback'];
return $render_callback( $attributes, $content );
};
}
return $settings;
}
/**
* Prevents the pages that are assigned as Cart/Checkout from showing the "template" selector in the page-editor.
* We want to avoid this flow and point users towards the Site Editor instead.
*
* @param string $old_name Old theme name.
* @param \WP_Theme $old_theme Instance of the old theme.
* @return void
*/
public function check_should_use_blockified_product_grid_templates( $old_name, $old_theme ) {
if ( ! wc_current_theme_is_fse_theme() ) {
update_option( Options::WC_BLOCK_USE_BLOCKIFIED_PRODUCT_GRID_BLOCK_AS_TEMPLATE, wc_bool_to_string( false ) );
public function hide_template_selector_in_cart_checkout_pages() {
if ( ! is_admin() ) {
return;
}
if ( ! $old_theme->is_block_theme() && wc_current_theme_is_fse_theme() ) {
update_option( Options::WC_BLOCK_USE_BLOCKIFIED_PRODUCT_GRID_BLOCK_AS_TEMPLATE, wc_bool_to_string( true ) );
return;
$current_screen = get_current_screen();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $current_screen && 'page' === $current_screen->id && ! empty( $_GET['post'] ) && in_array( absint( $_GET['post'] ), array( wc_get_page_id( 'cart' ), wc_get_page_id( 'checkout' ) ), true ) ) {
wp_add_inline_style( 'wc-blocks-editor-style', '.edit-post-post-template { display: none; }' );
}
}
@@ -355,16 +335,26 @@ class BlockTemplatesController {
continue;
}
$is_not_custom = false === array_search(
$possible_template_ids = [
$theme_slug . '//' . $template_file->slug,
array_column( $query_result, 'id' ),
true
);
$theme_slug . '//' . BlockTemplateUtils::DIRECTORY_NAMES['TEMPLATE_PARTS'] . '/' . $template_file->slug,
$theme_slug . '//' . BlockTemplateUtils::DIRECTORY_NAMES['DEPRECATED_TEMPLATE_PARTS'] . '/' . $template_file->slug,
];
$is_custom = false;
$query_result_template_ids = array_column( $query_result, 'id' );
foreach ( $possible_template_ids as $template_id ) {
if ( in_array( $template_id, $query_result_template_ids, true ) ) {
$is_custom = true;
break;
}
}
$fits_slug_query =
! isset( $query['slug__in'] ) || in_array( $template_file->slug, $query['slug__in'], true );
$fits_area_query =
! isset( $query['area'] ) || ( property_exists( $template_file, 'area' ) && $template_file->area === $query['area'] );
$should_include = $is_not_custom && $fits_slug_query && $fits_area_query;
$should_include = ! $is_custom && $fits_slug_query && $fits_area_query;
if ( $should_include ) {
$template = BlockTemplateUtils::build_template_result_from_file( $template_file, $template_type );
$query_result[] = $template;
@@ -469,14 +459,6 @@ class BlockTemplatesController {
continue;
}
// At this point the template only exists in the Blocks filesystem, if is a taxonomy-product_cat/tag/attribute.html template
// let's use the archive-product.html template from Blocks.
if ( BlockTemplateUtils::template_is_eligible_for_product_archive_fallback( $template_slug ) ) {
$template_file = $this->get_template_path_from_woocommerce( ProductCatalogTemplate::SLUG );
$templates[] = BlockTemplateUtils::create_new_block_template_object( $template_file, $template_type, $template_slug, false );
continue;
}
// At this point the template only exists in the Blocks filesystem and has not been saved in the DB,
// or superseded by the theme.
$templates[] = BlockTemplateUtils::create_new_block_template_object( $template_file, $template_type, $template_slug );
@@ -500,18 +482,6 @@ class BlockTemplatesController {
return array_merge( $templates_from_db, $templates_from_woo );
}
/**
* Returns the path of a template on the Blocks template folder.
*
* @param string $template_slug Block template slug e.g. single-product.
* @param string $template_type wp_template or wp_template_part.
*
* @return string
*/
public function get_template_path_from_woocommerce( $template_slug, $template_type = 'wp_template' ) {
return BlockTemplateUtils::get_templates_directory( $template_type ) . '/' . $template_slug . '.html';
}
/**
* Checks whether a block template with that name exists in Woo Blocks
*

View File

@@ -19,6 +19,7 @@ use Automattic\WooCommerce\Blocks\Templates\ProductCategoryTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductTagTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductSearchResultsTemplate;
use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductFiltersTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductFiltersOverlayTemplate;
/**
@@ -59,10 +60,14 @@ class BlockTemplatesRegistry {
}
if ( BlockTemplateUtils::supports_block_templates( 'wp_template_part' ) ) {
$template_parts = array(
MiniCartTemplate::SLUG => new MiniCartTemplate(),
CheckoutHeaderTemplate::SLUG => new CheckoutHeaderTemplate(),
ProductFiltersOverlayTemplate::SLUG => new ProductFiltersOverlayTemplate(),
MiniCartTemplate::SLUG => new MiniCartTemplate(),
CheckoutHeaderTemplate::SLUG => new CheckoutHeaderTemplate(),
);
if ( Features::is_enabled( 'experimental-blocks' ) ) {
$template_parts[ ProductFiltersTemplate::SLUG ] = new ProductFiltersTemplate();
$template_parts[ ProductFiltersOverlayTemplate::SLUG ] = new ProductFiltersOverlayTemplate();
}
} else {
$template_parts = array();
}

View File

@@ -36,9 +36,9 @@ class Checkout extends AbstractBlock {
// This prevents the page redirecting when the cart is empty. This is so the editor still loads the page preview.
add_filter(
'woocommerce_checkout_redirect_empty_cart',
function( $return ) {
function ( $redirect_empty_cart ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return isset( $_GET['_wp-find-template'] ) ? false : $return;
return isset( $_GET['_wp-find-template'] ) ? false : $redirect_empty_cart;
}
);
@@ -94,10 +94,17 @@ class Checkout extends AbstractBlock {
* @return array|string
*/
protected function get_block_type_script( $key = null ) {
$dependencies = [];
// Load password strength meter script asynchronously if needed.
if ( ! is_user_logged_in() && 'no' === get_option( 'woocommerce_registration_generate_password' ) ) {
$dependencies[] = 'zxcvbn-async';
}
$script = [
'handle' => 'wc-' . $this->block_name . '-block-frontend',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ),
'dependencies' => [],
'dependencies' => $dependencies,
];
return $key ? $script[ $key ] : $script;
}
@@ -354,6 +361,7 @@ class Checkout extends AbstractBlock {
$this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ) );
$this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ) );
$this->asset_data_registry->add( 'forcedBillingAddress', 'billing_only' === get_option( 'woocommerce_ship_to_destination' ) );
$this->asset_data_registry->add( 'generatePassword', filter_var( get_option( 'woocommerce_registration_generate_password' ), FILTER_VALIDATE_BOOLEAN ) );
$this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled() );
$this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled() );
$this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled() );
@@ -377,7 +385,7 @@ class Checkout extends AbstractBlock {
$shipping_methods = WC()->shipping()->get_shipping_methods();
$formatted_shipping_methods = array_reduce(
$shipping_methods,
function( $acc, $method ) {
function ( $acc, $method ) {
if ( in_array( $method->id, LocalPickupUtils::get_local_pickup_method_ids(), true ) ) {
return $acc;
}
@@ -405,7 +413,7 @@ class Checkout extends AbstractBlock {
$payment_methods = $this->get_enabled_payment_gateways();
$formatted_payment_methods = array_reduce(
$payment_methods,
function( $acc, $method ) {
function ( $acc, $method ) {
$acc[] = [
'id' => $method->id,
'title' => $method->method_title,
@@ -427,7 +435,7 @@ class Checkout extends AbstractBlock {
$all_plugins = \get_plugins(); // Note that `get_compatible_plugins_for_feature` calls `get_plugins` internally, so this is already in cache.
$incompatible_extensions = array_reduce(
$declared_extensions['incompatible'],
function( $acc, $item ) use ( $all_plugins ) {
function ( $acc, $item ) use ( $all_plugins ) {
$plugin = $all_plugins[ $item ] ?? null;
$plugin_id = $plugin['TextDomain'] ?? dirname( $item, 2 );
$plugin_name = $plugin['Name'] ?? $plugin_id;
@@ -465,7 +473,7 @@ class Checkout extends AbstractBlock {
$payment_gateways = WC()->payment_gateways->payment_gateways();
return array_filter(
$payment_gateways,
function( $payment_gateway ) {
function ( $payment_gateway ) {
return 'yes' === $payment_gateway->enabled;
}
);
@@ -500,7 +508,7 @@ class Checkout extends AbstractBlock {
$payment_methods[ $payment_method_group ] = array_values(
array_filter(
$saved_payment_methods,
function( $saved_payment_method ) use ( $payment_gateways ) {
function ( $saved_payment_method ) use ( $payment_gateways ) {
return in_array( $saved_payment_method['method']['gateway'], array_keys( $payment_gateways ), true );
}
)

View File

@@ -21,12 +21,24 @@ class ComingSoon extends AbstractBlock {
}
/**
* Get the frontend style handle for this block type.
* Enqueue frontend assets for this block, just in time for rendering.
*
* @return null
* @internal This prevents the block script being enqueued on all pages. It is only enqueued as needed. Note that
* we intentionally do not pass 'script' to register_block_type.
*
* @param array $attributes Any attributes that currently are available from the block.
* @param string $content The block content.
* @param WP_Block $block The block object.
*/
protected function get_block_type_style() {
return null;
protected function enqueue_assets( array $attributes, $content, $block ) {
parent::enqueue_assets( $attributes, $content, $block );
if ( isset( $attributes['color'] ) ) {
wp_add_inline_style(
'wc-blocks-style',
':root{--woocommerce-coming-soon-color: ' . esc_html( $attributes['color'] ) . '}'
);
}
}
/**

View File

@@ -11,9 +11,10 @@ use Automattic\WooCommerce\Blocks\Utils\BlockHooksTrait;
class CustomerAccount extends AbstractBlock {
use BlockHooksTrait;
const TEXT_ONLY = 'text_only';
const ICON_ONLY = 'icon_only';
const DISPLAY_ALT = 'alt';
const TEXT_ONLY = 'text_only';
const ICON_ONLY = 'icon_only';
const DISPLAY_ALT = 'alt';
const DISPLAY_LINE = 'line';
/**
* Block name.
@@ -33,6 +34,7 @@ class CustomerAccount extends AbstractBlock {
'anchor' => 'core/navigation',
'area' => 'header',
'callback' => 'should_unhook_block',
'version' => '8.4.0',
),
);
@@ -65,6 +67,7 @@ class CustomerAccount extends AbstractBlock {
*/
public function modify_hooked_block_attributes( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ) {
$parsed_hooked_block['attrs']['displayStyle'] = 'icon_only';
$parsed_hooked_block['attrs']['iconStyle'] = 'line';
/*
* The Mini Cart block (which is hooked into the header) has a margin of 0.5em on the left side.
@@ -116,16 +119,26 @@ class CustomerAccount extends AbstractBlock {
$account_link = get_option( 'woocommerce_myaccount_page_id' ) ? wc_get_account_endpoint_url( 'dashboard' ) : wp_login_url();
$allowed_svg = array(
'svg' => array(
'svg' => array(
'class' => true,
'xmlns' => true,
'width' => true,
'height' => true,
'viewbox' => true,
),
'path' => array(
'd' => true,
'fill' => true,
'path' => array(
'd' => true,
'fill' => true,
'fill-rule' => true,
'clip-rule' => true,
),
'circle' => array(
'cx' => true,
'cy' => true,
'r' => true,
'stroke' => true,
'stroke-width' => true,
'fill' => true,
),
);
@@ -153,8 +166,27 @@ class CustomerAccount extends AbstractBlock {
return '';
}
if ( self::DISPLAY_LINE === $attributes['iconStyle'] ) {
return '<svg class="' . $attributes['iconClass'] . '" viewBox="5 5 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle
cx="16"
cy="10.5"
r="3.5"
stroke="currentColor"
stroke-width="2"
fill="none"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.5 18.5H20.5C21.8807 18.5 23 19.6193 23 21V25.5H25V21C25 18.5147 22.9853 16.5 20.5 16.5H11.5C9.01472 16.5 7 18.5147 7 21V25.5H9V21C9 19.6193 10.1193 18.5 11.5 18.5Z"
fill="currentColor"
/>
</svg>';
}
if ( self::DISPLAY_ALT === $attributes['iconStyle'] ) {
return '<svg class="' . $attributes['iconClass'] . '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" width="18" height="18">
return '<svg class="' . $attributes['iconClass'] . '" xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 19 19" width="18" height="18">
<path
d="M9 0C4.03579 0 0 4.03579 0 9C0 13.9642 4.03579 18 9 18C13.9642 18 18 13.9642 18 9C18 4.03579 13.9642 0 9
0ZM9 4.32C10.5347 4.32 11.7664 5.57056 11.7664 7.08638C11.7664 8.62109 10.5158 9.85277 9 9.85277C7.4653
@@ -166,7 +198,7 @@ class CustomerAccount extends AbstractBlock {
</svg>';
}
return '<svg class="' . $attributes['iconClass'] . '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
return '<svg class="' . $attributes['iconClass'] . '" xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 17 17" width="16" height="16">
<path
d="M8.00009 8.34785C10.3096 8.34785 12.1819 6.47909 12.1819 4.17393C12.1819 1.86876 10.3096 0 8.00009 0C5.69055
0 3.81824 1.86876 3.81824 4.17393C3.81824 6.47909 5.69055 8.34785 8.00009 8.34785ZM0.333496 15.6522C0.333496

View File

@@ -65,6 +65,7 @@ class MiniCart extends AbstractBlock {
'position' => 'after',
'anchor' => 'core/navigation',
'area' => 'header',
'version' => '8.4.0',
),
);
@@ -459,7 +460,7 @@ class MiniCart extends AbstractBlock {
// It is not necessary to load the Mini-Cart Block on Cart and Checkout page.
return '<div class="' . esc_attr( $wrapper_classes ) . '" style="visibility:hidden" aria-hidden="true">
<button class="wc-block-mini-cart__button" disabled>' . $button_html . '</button>
<button class="wc-block-mini-cart__button" disabled aria-label="' . __( 'Cart', 'woocommerce' ) . '">' . $button_html . '</button>
</div>';
}
@@ -487,7 +488,7 @@ class MiniCart extends AbstractBlock {
}
return '<div class="' . esc_attr( $wrapper_classes ) . '" style="' . esc_attr( $wrapper_styles ) . '">
<button class="wc-block-mini-cart__button">' . $button_html . '</button>
<button class="wc-block-mini-cart__button" aria-label="' . __( 'Cart', 'woocommerce' ) . '">' . $button_html . '</button>
<div class="is-loading wc-block-components-drawer__screen-overlay wc-block-components-drawer__screen-overlay--is-hidden" aria-hidden="true">
<div class="wc-block-mini-cart__drawer wc-block-components-drawer">
<div class="wc-block-components-drawer__content">
@@ -546,7 +547,7 @@ class MiniCart extends AbstractBlock {
'tax_label' => $tax_label,
'display_cart_prices_including_tax' => false,
);
};
}
return array(
'tax_label' => '',

View File

@@ -40,13 +40,27 @@ class Status extends AbstractOrderConfirmationBlock {
return '';
}
$account_notice = $this->render_account_notice( $order );
if ( $account_notice ) {
$block = sprintf(
'<div class="wc-block-order-confirmation-status-notices %1$s">%2$s</div>',
esc_attr( trim( $classname ) ),
$account_notice
) . $block;
}
$additional_content = $this->render_confirmation_notice( $order );
return $additional_content ? $block . sprintf(
'<div class="wc-block-order-confirmation-status-description %1$s">%2$s</div>',
esc_attr( trim( $classname ) ),
$additional_content
) : $block;
if ( $additional_content ) {
$block = $block . sprintf(
'<div class="wc-block-order-confirmation-status-description %1$s">%2$s</div>',
esc_attr( trim( $classname ) ),
$additional_content
);
}
return $block;
}
/**
@@ -142,6 +156,33 @@ class Status extends AbstractOrderConfirmationBlock {
return '<p>' . esc_html__( 'Please check your email for the order confirmation.', 'woocommerce' ) . '</p>';
}
/**
* If the user associated with the order needs to set a password (new account) show a notice.
*
* @param \WC_Order|null $order Order object.
* @return string
*/
protected function render_account_notice( $order = null ) {
if ( $order && $order->get_customer_id() && 'store-api' === $order->get_created_via() ) {
$nag = get_user_option( 'default_password_nag', $order->get_customer_id() );
$generate = filter_var( get_option( 'woocommerce_registration_generate_password', false ), FILTER_VALIDATE_BOOLEAN );
if ( $nag && $generate ) {
return wc_print_notice(
sprintf(
// translators: %s: site name.
__( 'Your account with %s has been successfully created. We emailed you a link to set your account password.', 'woocommerce' ),
esc_html( wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) )
),
'notice',
array(),
true
);
}
}
return '';
}
/**
* If the order is invalid or there is no permission to view the details, tell the user to check email or log-in.
*

View File

@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
use WP_Query;
use WC_Tax;
@@ -76,6 +77,9 @@ class ProductCollection extends AbstractBlock {
// Extend allowed `collection_params` for the REST API.
add_filter( 'rest_product_collection_params', array( $this, 'extend_rest_query_allowed_params' ), 10, 1 );
// Provide location context into block's context.
add_filter( 'render_block_context', array( $this, 'provide_location_context_for_inner_blocks' ), 11, 1 );
// Interactivity API: Add navigation directives to the product collection block.
add_filter( 'render_block_woocommerce/product-collection', array( $this, 'enhance_product_collection_with_interactivity' ), 10, 2 );
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
@@ -86,6 +90,71 @@ class ProductCollection extends AbstractBlock {
add_filter( 'render_block_data', array( $this, 'disable_enhanced_pagination' ), 10, 1 );
}
/**
* Provides the location context to each inner block of the product collection block.
* Hint: Only blocks using the 'query' context will be affected.
*
* The sourceData structure depends on the context type as follows:
* - site: [ ]
* - order: [ 'orderId' => int ]
* - cart: [ 'productIds' => int[] ]
* - archive: [ 'taxonomy' => string, 'termId' => int ]
* - product: [ 'productId' => int ]
*
* @example array(
* 'type' => 'product',
* 'sourceData' => array( 'productId' => 123 ),
* )
*
* @param array $context The block context.
* @return array $context {
* The block context including the product collection location context.
*
* @type array $productCollectionLocation {
* @type string $type The context type. Possible values are 'site', 'order', 'cart', 'archive', 'product'.
* @type array $sourceData The context source data. Can be the product ID of the viewed product, the order ID of the current order viewed, etc. See structure above for more details.
* }
* }
*/
public function provide_location_context_for_inner_blocks( $context ) {
// Run only on frontend.
// This is needed to avoid SSR renders while in editor. @see https://github.com/woocommerce/woocommerce/issues/45181.
if ( is_admin() || \WC()->is_rest_api_request() ) {
return $context;
}
// Target only product collection's inner blocks that use the 'query' context.
if ( ! isset( $context['query'] ) || ! isset( $context['query']['isProductCollectionBlock'] ) || ! $context['query']['isProductCollectionBlock'] ) {
return $context;
}
$is_in_single_product = isset( $context['singleProduct'] ) && ! empty( $context['postId'] );
$context['productCollectionLocation'] = $is_in_single_product ? array(
'type' => 'product',
'sourceData' => array(
'productId' => absint( $context['postId'] ),
),
) : $this->get_location_context();
return $context;
}
/**
* Get the global location context.
* Serve as a runtime cache for the location context.
*
* @see ProductCollectionUtils::parse_frontend_location_context()
*
* @return array The location context.
*/
private function get_location_context() {
static $location_context = null;
if ( null === $location_context ) {
$location_context = ProductCollectionUtils::parse_frontend_location_context();
}
return $location_context;
}
/**
* Enhances the Product Collection block with client-side pagination.
*
@@ -451,10 +520,14 @@ class ProductCollection extends AbstractBlock {
}
$block_context_query = $block->context['query'];
// phpcs:ignore WordPress.DB.SlowDBQuery
$block_context_query['tax_query'] = ! empty( $query['tax_query'] ) ? $query['tax_query'] : array();
$is_exclude_applied_filters = ! ( $block->context['query']['inherit'] ?? false );
$inherit = $block->context['query']['inherit'] ?? false;
$filterable = $block->context['query']['filterable'] ?? false;
$is_exclude_applied_filters = ! ( $inherit || $filterable );
return $this->get_final_frontend_query( $block_context_query, $page, $is_exclude_applied_filters );
}

View File

@@ -39,8 +39,6 @@ class ProductFilters extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
return <<<HTML
<div>Product Filters</div>
HTML;
return $content;
}
}

View File

@@ -1,6 +1,9 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
* ProductFiltersOverlay class.
*/
@@ -30,10 +33,49 @@ class ProductFiltersOverlay extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
ob_start();
printf( '<div>%s</div>', esc_html__( 'Filters Overlay', 'woocommerce' ) );
$html = ob_get_clean();
return $content;
}
return $html;
/**
* Extra data passed through from server to client for block.
*
* @param array $attributes Any attributes that currently are available from the block.
* Note, this will be empty in the editor context when the block is
* not in the post content on editor load.
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
$template_part_edit_uri = '';
if (
current_user_can( 'edit_theme_options' ) &&
( wc_current_theme_is_fse_theme() || current_theme_supports( 'block-template-parts' ) )
) {
$theme_slug = BlockTemplateUtils::theme_has_template_part( 'product-filters-overlay' ) ? wp_get_theme()->get_stylesheet() : BlockTemplateUtils::PLUGIN_SLUG;
$site_editor_uri = add_query_arg(
array(
'canvas' => 'edit',
'path' => '/template-parts/single',
),
admin_url( 'site-editor.php' )
);
$template_part_edit_uri = esc_url_raw(
add_query_arg(
array(
'postId' => sprintf( '%s//%s', $theme_slug, 'product-filters-overlay' ),
'postType' => 'wp_template_part',
),
$site_editor_uri
)
);
}
$this->asset_data_registry->add(
'templatePartProductFiltersOverlayEditUri',
$template_part_edit_uri
);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* ProductFilters class.
*/
class ProductFiltersOverlayNavigation extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-filters-overlay-navigation';
}

View File

@@ -48,6 +48,8 @@ class ProductTemplate extends AbstractBlock {
$classnames = '';
if ( isset( $block->context['displayLayout'] ) && isset( $block->context['query'] ) ) {
$classnames = 'is-product-collection-layout-' . $block->context['displayLayout']['type'] . ' ';
if ( isset( $block->context['displayLayout']['type'] ) && 'flex' === $block->context['displayLayout']['type'] ) {
if ( isset( $block->context['displayLayout']['shrinkColumns'] ) && $block->context['displayLayout']['shrinkColumns'] ) {
$classnames = "wc-block-product-template__responsive columns-{$block->context['displayLayout']['columns']}";
@@ -56,6 +58,7 @@ class ProductTemplate extends AbstractBlock {
}
}
}
if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) {
$classnames .= ' has-link-color';
}

View File

@@ -31,6 +31,13 @@ final class BlockTypesController {
*/
protected $asset_data_registry;
/**
* Holds the registered blocks that have WooCommerce blocks as their parents.
*
* @var array List of registered blocks.
*/
private $registered_blocks_with_woocommerce_parents;
/**
* Constructor.
*
@@ -67,6 +74,45 @@ final class BlockTypesController {
);
}
/**
* Get registered blocks that have WooCommerce blocks as their parents. Adds the value to the
* `registered_blocks_with_woocommerce_parents` cache if `init` has been fired.
*
* @return array Registered blocks with WooCommerce blocks as parents.
*/
public function get_registered_blocks_with_woocommerce_parent() {
// If init has run and the cache is already set, return it.
if ( did_action( 'init' ) && ! empty( $this->registered_blocks_with_woocommerce_parents ) ) {
return $this->registered_blocks_with_woocommerce_parents;
}
$registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered();
if ( ! is_array( $registered_blocks ) ) {
return array();
}
$this->registered_blocks_with_woocommerce_parents = array_filter(
$registered_blocks,
function ( $block ) {
if ( empty( $block->parent ) ) {
return false;
}
if ( ! is_array( $block->parent ) ) {
$block->parent = array( $block->parent );
}
$woocommerce_blocks = array_filter(
$block->parent,
function ( $parent_block_name ) {
return 'woocommerce' === strtok( $parent_block_name, '/' );
}
);
return ! empty( $woocommerce_blocks );
}
);
return $this->registered_blocks_with_woocommerce_parents;
}
/**
* Check if the current post has a block with a specific attribute value.
*
@@ -132,14 +178,15 @@ final class BlockTypesController {
}
/**
* Add data- attributes to blocks when rendered if the block is under the woocommerce/ namespace.
* Check if a block should have data attributes appended on render. If it's in an allowed namespace, or the block
* has explicitly been added to the allowed block list, or if one of the block's parents is in the WooCommerce
* namespace it can have data attributes.
*
* @param string $content Block content.
* @param array $block Parsed block data.
* @return string
* @param string $block_name Name of the block to check.
*
* @return boolean
*/
public function add_data_attributes( $content, $block ) {
$block_name = $block['blockName'];
public function block_should_have_data_attributes( $block_name ) {
$block_namespace = strtok( $block_name ?? '', '/' );
/**
@@ -164,18 +211,44 @@ final class BlockTypesController {
*/
$allowed_blocks = (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_block', array() );
if ( ! in_array( $block_namespace, $allowed_namespaces, true ) && ! in_array( $block_name, $allowed_blocks, true ) ) {
$blocks_with_woo_parents = $this->get_registered_blocks_with_woocommerce_parent();
$block_has_woo_parent = in_array( $block_name, array_keys( $blocks_with_woo_parents ), true );
$in_allowed_namespace_list = in_array( $block_namespace, $allowed_namespaces, true );
$in_allowed_block_list = in_array( $block_name, $allowed_blocks, true );
return $block_has_woo_parent || $in_allowed_block_list || $in_allowed_namespace_list;
}
/**
* Add data- attributes to blocks when rendered if the block is under the woocommerce/ namespace.
*
* @param string $content Block content.
* @param array $block Parsed block data.
* @return string
*/
public function add_data_attributes( $content, $block ) {
$content = trim( $content );
if ( ! $this->block_should_have_data_attributes( $block['blockName'] ) ) {
return $content;
}
$attributes = (array) $block['attrs'];
$exclude_attributes = array( 'className', 'align' );
$escaped_data_attributes = array(
'data-block-name="' . esc_attr( $block['blockName'] ) . '"',
);
$attributes = (array) $block['attrs'];
$exclude_attributes = array( 'className', 'align' );
foreach ( $attributes as $key => $value ) {
if ( in_array( $key, $exclude_attributes, true ) ) {
$processor = new \WP_HTML_Tag_Processor( $content );
if (
false === $processor->next_token() ||
'DIV' !== $processor->get_token_name() ||
$processor->is_tag_closer()
) {
return $content;
}
foreach ( $attributes as $key => $value ) {
if ( ! is_string( $key ) || in_array( $key, $exclude_attributes, true ) ) {
continue;
}
if ( is_bool( $value ) ) {
@@ -184,10 +257,16 @@ final class BlockTypesController {
if ( ! is_scalar( $value ) ) {
$value = wp_json_encode( $value );
}
$escaped_data_attributes[] = 'data-' . esc_attr( strtolower( preg_replace( '/(?<!\ )[A-Z]/', '-$0', $key ) ) ) . '="' . esc_attr( $value ) . '"';
// For output consistency, we convert camelCase to kebab-case and output in lowercase.
$key = strtolower( preg_replace( '/(?<!^|\ )[A-Z]/', '-$0', $key ) );
$processor->set_attribute( "data-{$key}", $value );
}
return preg_replace( '/^<div /', '<div ' . implode( ' ', $escaped_data_attributes ) . ' ', trim( $content ) );
// Set this last to prevent user-input from overriding it.
$processor->set_attribute( 'data-block-name', $block['blockName'] );
return $processor->get_updated_html();
}
/**
@@ -327,6 +406,7 @@ final class BlockTypesController {
$block_types[] = 'ProductFilter';
$block_types[] = 'ProductFilters';
$block_types[] = 'ProductFiltersOverlay';
$block_types[] = 'ProductFiltersOverlayNavigation';
$block_types[] = 'ProductFilterStockStatus';
$block_types[] = 'ProductFilterPrice';
$block_types[] = 'ProductFilterAttribute';

View File

@@ -41,6 +41,8 @@ use Automattic\WooCommerce\Blocks\Shipping\ShippingController;
use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplateCompatibility;
use Automattic\WooCommerce\Blocks\Templates\ArchiveProductTemplatesCompatibility;
use Automattic\WooCommerce\Blocks\Domain\Services\OnboardingTasks\TasksController;
use Automattic\WooCommerce\Blocks\TemplateOptions;
/**
* Takes care of bootstrapping the plugin.
@@ -122,9 +124,23 @@ class Bootstrap {
0
);
$is_rest = wc()->is_rest_api_request();
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$is_store_api_request = $is_rest && ! empty( $_SERVER['REQUEST_URI'] ) && ( false !== strpos( $_SERVER['REQUEST_URI'], trailingslashit( rest_get_url_prefix() ) . 'wc/store/' ) );
// We need to initialize BlockTemplatesController and BlockTemplatesRegistry at the end of `after_setup_theme`
// so themes had the opportunity to declare support for template parts.
add_action(
'after_setup_theme',
function () {
$is_store_api_request = wc()->is_store_api_request();
if ( ! $is_store_api_request && ( wc_current_theme_is_fse_theme() || current_theme_supports( 'block-template-parts' ) ) ) {
$this->container->get( BlockTemplatesRegistry::class )->init();
$this->container->get( BlockTemplatesController::class )->init();
}
},
999
);
$is_rest = wc()->is_rest_api_request();
$is_store_api_request = wc()->is_store_api_request();
// Load and init assets.
$this->container->get( StoreApi::class )->init();
@@ -152,13 +168,12 @@ class Bootstrap {
$this->container->get( AIPatterns::class );
$this->container->get( BlockPatterns::class );
$this->container->get( BlockTypesController::class );
$this->container->get( BlockTemplatesRegistry::class )->init();
$this->container->get( BlockTemplatesController::class )->init();
$this->container->get( ClassicTemplatesCompatibility::class );
$this->container->get( ArchiveProductTemplatesCompatibility::class )->init();
$this->container->get( SingleProductTemplateCompatibility::class )->init();
$this->container->get( Notices::class )->init();
$this->container->get( PTKPatternsStore::class );
$this->container->get( TemplateOptions::class )->init();
}
$this->container->get( QueryFilters::class )->init();
@@ -254,18 +269,6 @@ class Bootstrap {
return new BlockTypesController( $asset_api, $asset_data_registry );
}
);
$this->container->register(
BlockTemplatesRegistry::class,
function () {
return new BlockTemplatesRegistry();
}
);
$this->container->register(
BlockTemplatesController::class,
function () {
return new BlockTemplatesController();
}
);
$this->container->register(
ClassicTemplatesCompatibility::class,
function ( Container $container ) {
@@ -350,6 +353,12 @@ class Bootstrap {
return new StoreApi();
}
);
$this->container->register(
TemplateOptions::class,
function () {
return new TemplateOptions();
}
);
// Maintains backwards compatibility with previous Store API namespace.
$this->container->register(
'Automattic\WooCommerce\Blocks\StoreApi\Formatters',
@@ -427,6 +436,18 @@ class Bootstrap {
return new QueryFilters();
}
);
$this->container->register(
BlockTemplatesRegistry::class,
function () {
return new BlockTemplatesRegistry();
}
);
$this->container->register(
BlockTemplatesController::class,
function () {
return new BlockTemplatesController();
}
);
}
/**

View File

@@ -13,13 +13,6 @@ use WP_Error;
*/
class CheckoutFields {
/**
* Core checkout fields.
*
* @var array
*/
private $core_fields;
/**
* Additional checkout fields.
*
@@ -91,144 +84,10 @@ class CheckoutFields {
*/
public function __construct( AssetDataRegistry $asset_data_registry ) {
$this->asset_data_registry = $asset_data_registry;
$this->core_fields = [
'email' => [
'label' => __( 'Email address', 'woocommerce' ),
'optionalLabel' => __(
'Email address (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'email',
'autocapitalize' => 'none',
'index' => 0,
],
'country' => [
'label' => __( 'Country/Region', 'woocommerce' ),
'optionalLabel' => __(
'Country/Region (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'country',
'index' => 1,
],
'first_name' => [
'label' => __( 'First name', 'woocommerce' ),
'optionalLabel' => __(
'First name (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'given-name',
'autocapitalize' => 'sentences',
'index' => 10,
],
'last_name' => [
'label' => __( 'Last name', 'woocommerce' ),
'optionalLabel' => __(
'Last name (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'family-name',
'autocapitalize' => 'sentences',
'index' => 20,
],
'company' => [
'label' => __( 'Company', 'woocommerce' ),
'optionalLabel' => __(
'Company (optional)',
'woocommerce'
),
'required' => false,
'hidden' => false,
'autocomplete' => 'organization',
'autocapitalize' => 'sentences',
'index' => 30,
],
'address_1' => [
'label' => __( 'Address', 'woocommerce' ),
'optionalLabel' => __(
'Address (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'address-line1',
'autocapitalize' => 'sentences',
'index' => 40,
],
'address_2' => [
'label' => __( 'Apartment, suite, etc.', 'woocommerce' ),
'optionalLabel' => __(
'Apartment, suite, etc. (optional)',
'woocommerce'
),
'required' => false,
'hidden' => false,
'autocomplete' => 'address-line2',
'autocapitalize' => 'sentences',
'index' => 50,
],
'city' => [
'label' => __( 'City', 'woocommerce' ),
'optionalLabel' => __(
'City (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'address-level2',
'autocapitalize' => 'sentences',
'index' => 70,
],
'state' => [
'label' => __( 'State/County', 'woocommerce' ),
'optionalLabel' => __(
'State/County (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'address-level1',
'autocapitalize' => 'sentences',
'index' => 80,
],
'postcode' => [
'label' => __( 'Postal code', 'woocommerce' ),
'optionalLabel' => __(
'Postal code (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'postal-code',
'autocapitalize' => 'characters',
'index' => 90,
],
'phone' => [
'label' => __( 'Phone', 'woocommerce' ),
'optionalLabel' => __(
'Phone (optional)',
'woocommerce'
),
'required' => false,
'hidden' => false,
'type' => 'tel',
'autocomplete' => 'tel',
'autocapitalize' => 'characters',
'index' => 100,
],
];
$this->fields_locations = [
// omit email from shipping and billing fields.
'address' => array_merge( \array_diff_key( array_keys( $this->core_fields ), array( 'email' ) ) ),
'address' => array_merge( \array_diff_key( $this->get_core_fields_keys(), array( 'email' ) ) ),
'contact' => array( 'email' ),
'order' => [],
];
@@ -527,17 +386,8 @@ class CheckoutFields {
$field_data['options'] = $cleaned_options;
// If the field is not required, inject an empty option at the start.
if ( isset( $field_data['required'] ) && false === $field_data['required'] && ! in_array( '', $added_values, true ) ) {
$field_data['options'] = array_merge(
[
[
'value' => '',
'label' => '',
],
],
$field_data['options']
);
if ( isset( $field_data['placeholder'] ) ) {
$field_data['placeholder'] = sanitize_text_field( $field_data['placeholder'] );
}
return $field_data;
@@ -620,13 +470,167 @@ class CheckoutFields {
);
}
/**
* Returns the keys of all core fields.
*
* @return array An array of field keys.
*/
public function get_core_fields_keys() {
return [
'email',
'country',
'first_name',
'last_name',
'company',
'address_1',
'address_2',
'city',
'state',
'postcode',
'phone',
];
}
/**
* Returns an array of all core fields.
*
* @return array An array of fields.
*/
public function get_core_fields() {
return $this->core_fields;
return [
'email' => [
'label' => __( 'Email address', 'woocommerce' ),
'optionalLabel' => __(
'Email address (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'email',
'autocapitalize' => 'none',
'index' => 0,
],
'country' => [
'label' => __( 'Country/Region', 'woocommerce' ),
'optionalLabel' => __(
'Country/Region (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'country',
'index' => 1,
],
'first_name' => [
'label' => __( 'First name', 'woocommerce' ),
'optionalLabel' => __(
'First name (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'given-name',
'autocapitalize' => 'sentences',
'index' => 10,
],
'last_name' => [
'label' => __( 'Last name', 'woocommerce' ),
'optionalLabel' => __(
'Last name (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'family-name',
'autocapitalize' => 'sentences',
'index' => 20,
],
'company' => [
'label' => __( 'Company', 'woocommerce' ),
'optionalLabel' => __(
'Company (optional)',
'woocommerce'
),
'required' => false,
'hidden' => false,
'autocomplete' => 'organization',
'autocapitalize' => 'sentences',
'index' => 30,
],
'address_1' => [
'label' => __( 'Address', 'woocommerce' ),
'optionalLabel' => __(
'Address (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'address-line1',
'autocapitalize' => 'sentences',
'index' => 40,
],
'address_2' => [
'label' => __( 'Apartment, suite, etc.', 'woocommerce' ),
'optionalLabel' => __(
'Apartment, suite, etc. (optional)',
'woocommerce'
),
'required' => false,
'hidden' => false,
'autocomplete' => 'address-line2',
'autocapitalize' => 'sentences',
'index' => 50,
],
'city' => [
'label' => __( 'City', 'woocommerce' ),
'optionalLabel' => __(
'City (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'address-level2',
'autocapitalize' => 'sentences',
'index' => 70,
],
'state' => [
'label' => __( 'State/County', 'woocommerce' ),
'optionalLabel' => __(
'State/County (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'address-level1',
'autocapitalize' => 'sentences',
'index' => 80,
],
'postcode' => [
'label' => __( 'Postal code', 'woocommerce' ),
'optionalLabel' => __(
'Postal code (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'postal-code',
'autocapitalize' => 'characters',
'index' => 90,
],
'phone' => [
'label' => __( 'Phone', 'woocommerce' ),
'optionalLabel' => __(
'Phone (optional)',
'woocommerce'
),
'required' => false,
'hidden' => false,
'type' => 'tel',
'autocomplete' => 'tel',
'autocapitalize' => 'characters',
'index' => 100,
],
];
}
/**

View File

@@ -43,7 +43,7 @@ class Notices {
public function init() {
add_action(
'after_setup_theme',
function() {
function () {
/**
* Allow classic theme developers to opt-in to using block notices.
*
@@ -104,13 +104,14 @@ class Notices {
* @return string
*/
public function get_notices_template( $template, $template_name, $args, $template_path, $default_path ) {
$directory = get_stylesheet_directory();
$file = $directory . '/woocommerce/' . $template_name;
if ( file_exists( $file ) ) {
return $file;
}
if ( in_array( $template_name, $this->notice_templates, true ) ) {
$directory = get_stylesheet_directory();
$file = $directory . '/woocommerce/' . $template_name;
if ( file_exists( $file ) ) {
return $file;
}
$template = $this->package->get_path( 'templates/block-' . $template_name );
wp_enqueue_style( 'wc-blocks-style' );
}

View File

@@ -11,10 +11,9 @@ use WP_Upgrader;
class PTKPatternsStore {
const TRANSIENT_NAME = 'ptk_patterns';
// Some patterns need to be excluded because they have dependencies which
// are not installed by default (like Jetpack). Otherwise, the user
// would see an error when trying to insert them in the editor.
const EXCLUDED_PATTERNS = array( '13923', '14781', '14779', '13666', '13664', '13660', '13588', '14922', '14880', '13596', '13967', '13958', '15050', '15027' );
const CATEGORY_MAPPING = array(
'testimonials' => 'reviews',
);
/**
* PatternsToolkit instance.
@@ -92,11 +91,13 @@ class PTKPatternsStore {
* @return void
*/
private function schedule_action_if_not_pending( $action ) {
if ( as_has_scheduled_action( $action ) ) {
$last_request = get_transient( 'last_fetch_patterns_request' );
if ( as_has_scheduled_action( $action ) || false !== $last_request ) {
return;
}
as_schedule_single_action( time(), $action );
set_transient( 'last_fetch_patterns_request', time(), HOUR_IN_SECONDS );
}
/**
@@ -117,26 +118,31 @@ class PTKPatternsStore {
}
/**
* Filter patterns to exclude those with the given IDs.
* Filter the patterns that have external dependencies.
*
* @param array $patterns The patterns to filter.
* @param array $pattern_ids The pattern IDs to exclude.
* @return array
*/
private function filter_patterns( array $patterns, array $pattern_ids ) {
return array_filter(
$patterns,
function ( $pattern ) use ( $pattern_ids ) {
if ( ! isset( $pattern['ID'] ) ) {
private function filter_patterns( array $patterns ) {
return array_values(
array_filter(
$patterns,
function ( $pattern ) {
if ( ! isset( $pattern['ID'] ) ) {
return true;
}
if ( isset( $pattern['post_type'] ) && 'wp_block' !== $pattern['post_type'] ) {
return false;
}
if ( $this->has_external_dependencies( $pattern ) ) {
return false;
}
return true;
}
if ( isset( $pattern['post_type'] ) && 'wp_block' !== $pattern['post_type'] ) {
return false;
}
return ! in_array( (string) $pattern['ID'], $pattern_ids, true );
}
)
);
}
@@ -181,22 +187,35 @@ class PTKPatternsStore {
$patterns = $this->ptk_client->fetch_patterns(
array(
'categories' => array( 'intro', 'about', 'services', 'testimonials' ),
// This is the site where the patterns are stored. Despite the 'wpcomstaging.com' domain suggesting a staging environment, this URL points to the production environment where stable versions of the patterns are maintained.
'site' => 'wooblockpatterns.wpcomstaging.com',
'categories' => array(
'_woo_intro',
'_woo_featured_selling',
'_woo_about',
'_woo_reviews',
'_woo_social_media',
'_woo_woocommerce',
'_dotcom_imported_intro',
'_dotcom_imported_about',
'_dotcom_imported_services',
'_dotcom_imported_reviews',
),
)
);
if ( is_wp_error( $patterns ) ) {
wc_get_logger()->warning(
sprintf(
// translators: %s is a generated error message.
__( 'Failed to get the patterns from the PTK: "%s"', 'woocommerce' ),
__( 'Failed to get WooCommerce patterns from the PTK: "%s"', 'woocommerce' ),
$patterns->get_error_message()
),
);
return;
}
$patterns = $this->filter_patterns( $patterns, self::EXCLUDED_PATTERNS );
$patterns = $this->filter_patterns( $patterns );
$patterns = $this->map_categories( $patterns );
set_transient( self::TRANSIENT_NAME, $patterns );
}
@@ -209,4 +228,51 @@ class PTKPatternsStore {
private function allowed_tracking_is_enabled(): bool {
return 'yes' === get_option( 'woocommerce_allow_tracking' );
}
/**
* Change the categories of the patterns to match the ones used in the CYS flow
*
* @param array $patterns The patterns to map categories for.
* @return array The patterns with the categories mapped.
*/
private function map_categories( array $patterns ) {
return array_map(
function ( $pattern ) {
if ( isset( $pattern['categories'] ) ) {
foreach ( $pattern['categories'] as $key => $category ) {
if ( isset( $category['slug'] ) && isset( self::CATEGORY_MAPPING[ $key ] ) ) {
$new_category = self::CATEGORY_MAPPING[ $key ];
unset( $pattern['categories'][ $key ] );
$pattern['categories'][ $new_category ]['slug'] = $new_category;
$pattern['categories'][ $new_category ]['title'] = ucfirst( $new_category );
}
}
}
return $pattern;
},
$patterns
);
}
/**
* Check if the pattern has external dependencies.
*
* @param array $pattern The pattern to check.
*
* @return bool
*/
private function has_external_dependencies( $pattern ) {
if ( ! isset( $pattern['dependencies'] ) || ! is_array( $pattern['dependencies'] ) ) {
return false;
}
foreach ( $pattern['dependencies'] as $dependency ) {
if ( 'woocommerce' !== $dependency ) {
return true;
}
}
return false;
}
}

View File

@@ -10,6 +10,30 @@ class PatternRegistry {
const SLUG_REGEX = '/^[A-z0-9\/_-]+$/';
const COMMA_SEPARATED_REGEX = '/[\s,]+/';
/**
* Associates pattern slugs with their localized labels for categorization.
* Each key represents a unique pattern slug, while the value is the localized label.
*
* @var array $category_labels
*/
private $category_labels;
/**
* Constructor.
*/
public function __construct() {
$this->category_labels = [
'woo-commerce' => __( 'WooCommerce', 'woocommerce' ),
'intro' => __( 'Intro', 'woocommerce' ),
'featured-selling' => __( 'Featured Selling', 'woocommerce' ),
'about' => __( 'About', 'woocommerce' ),
'social-media' => __( 'Social Media', 'woocommerce' ),
'services' => __( 'Services', 'woocommerce' ),
'reviews' => __( 'Reviews', 'woocommerce' ),
];
}
/**
* Register a block pattern.
*
@@ -166,10 +190,13 @@ class PatternRegistry {
$pattern_data['categories'][ $key ] = $category_slug;
$label = isset( $this->category_labels[ $category_slug ] ) ? $this->category_labels[ $category_slug ] : self::kebab_to_capital_case( $category_slug );
register_block_pattern_category(
$category_slug,
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
array( 'label' => __( $category, 'woocommerce' ) )
array(
'label' => $label,
),
);
}
}
@@ -194,4 +221,18 @@ class PatternRegistry {
return null;
}
/**
* Convert a kebab-case string to capital case.
*
* @param string $value The kebab-case string.
*
* @return string
*/
private static function kebab_to_capital_case( $value ) {
$string = str_replace( '-', ' ', $value );
$string = ucwords( $string );
return $string;
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Blocks\Options;
/**
* TemplateOptions class.
*
* @internal
*/
class TemplateOptions {
/**
* Initialization method.
*/
public function init() {
add_action( 'after_switch_theme', array( $this, 'check_should_use_blockified_product_grid_templates' ), 10, 2 );
}
/**
* Checks the old and current themes and determines if the "wc_blocks_use_blockified_product_grid_block_as_template"
* option need to be updated accordingly.
*
* @param string $old_name Old theme name.
* @param \WP_Theme $old_theme Instance of the old theme.
* @return void
*/
public function check_should_use_blockified_product_grid_templates( $old_name, $old_theme ) {
if ( ! $old_theme->is_block_theme() && wc_current_theme_is_fse_theme() ) {
$option_name = Options::WC_BLOCK_USE_BLOCKIFIED_PRODUCT_GRID_BLOCK_AS_TEMPLATE;
// We previously stored "yes" or "no" values. This will convert them to true or false.
$option_value = wc_string_to_bool( get_option( $option_name ) );
// We don't need to do anything if the option is already set to true.
if ( ! $option_value ) {
update_option( $option_name, true );
}
}
}
}

View File

@@ -20,14 +20,12 @@ class ProductFiltersOverlayTemplate extends AbstractTemplatePart {
*
* @var string
*/
public $template_area = 'product-filters-overlay';
public $template_area = 'uncategorized';
/**
* Initialization method.
*/
public function init() {
add_filter( 'default_wp_template_part_areas', array( $this, 'register_product_filters_overlay_template_part_area' ), 10, 1 );
}
public function init() {}
/**
* Returns the title of the template.
@@ -46,21 +44,4 @@ class ProductFiltersOverlayTemplate extends AbstractTemplatePart {
public function get_template_description() {
return __( 'Template used to display the Product Filters Overlay.', 'woocommerce' );
}
/**
* Add Filters Overlay to the default template part areas.
*
* @param array $default_area_definitions An array of supported area objects.
* @return array The supported template part areas including the Filters Overlay one.
*/
public function register_product_filters_overlay_template_part_area( $default_area_definitions ) {
$product_filters_overlay_template_part_area = array(
'area' => 'product-filters-overlay',
'label' => $this->get_template_title(),
'description' => $this->get_template_description(),
'icon' => 'filter',
'area_tag' => 'product-filters-overlay',
);
return array_merge( $default_area_definitions, array( $product_filters_overlay_template_part_area ) );
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
* ProductFiltersTemplate class.
*
* @internal
*/
class ProductFiltersTemplate extends AbstractTemplatePart {
/**
* The slug of the template.
*
* @var string
*/
const SLUG = 'product-filters';
/**
* The template part area where the template part belongs.
*
* @var string
*/
public $template_area = 'uncategorized';
/**
* Initialization method.
*/
public function init() {
add_filter( 'get_block_type_variations', array( $this, 'register_block_type_variation' ), 10, 2 );
}
/**
* Returns the title of the template.
*
* @return string
*/
public function get_template_title() {
return _x( 'Product Filters (Experimental)', 'Template name', 'woocommerce' );
}
/**
* Returns the description of the template.
*
* @return string
*/
public function get_template_description() {
return __(
'This is the template part for the product filters displayed on different pages across your store.',
'woocommerce'
);
}
/**
* Add variation for this template part to make it available in the block inserter.
*
* @param array $variations Array of registered variations for a block type.
* @param WP_Block_Type $block_type The full block type object.
*/
public function register_block_type_variation( $variations, $block_type ) {
if ( 'core/template-part' !== $block_type->name ) {
return $variations;
}
// If template part is modified, Core will pick it up and register a variation
// for it. Check if the variation already exists before adding it.
foreach ( $variations as $variation ) {
if ( ! empty( $variation['attributes']['slug'] ) && 'product-filters' === $variation['attributes']['slug'] ) {
return $variations;
}
}
$theme = 'woocommerce/woocommerce';
// Check if current theme overrides this template part.
if ( BlockTemplateUtils::theme_has_template_part( 'product-filters' ) ) {
$theme = wp_get_theme()->get( 'TextDomain' );
}
$variations[] = array(
'name' => 'file_' . self::SLUG,
'title' => $this->get_template_title(),
'description' => true,
'attributes' => array(
'slug' => self::SLUG,
'theme' => $theme,
'area' => $this->template_area,
),
'scope' => array( 'inserter' ),
'icon' => 'layout',
);
return $variations;
}
}

View File

@@ -17,13 +17,6 @@ class ProductSearchResultsTemplate extends AbstractTemplate {
*/
const SLUG = 'product-search-results';
/**
* The template used as a fallback if that one is customized.
*
* @var string
*/
public $fallback_template = ProductCatalogTemplate::SLUG;
/**
* Initialization method.
*/

View File

@@ -18,31 +18,34 @@ trait BlockHooksTrait {
* @return array An array of block slugs hooked into a given context.
*/
public function register_hooked_block( $hooked_blocks, $position, $anchor_block, $context ) {
/**
* If the block has no hook placements, return early.
*/
// If the block has no hook placements, return early.
if ( ! isset( $this->hooked_block_placements ) || empty( $this->hooked_block_placements ) ) {
return $hooked_blocks;
}
// Cache for active theme.
static $active_theme_name = null;
if ( is_null( $active_theme_name ) ) {
$active_theme_name = wp_get_theme()->get( 'Name' );
// Cache the block hooks version.
static $block_hooks_version = null;
if ( defined( 'WP_RUN_CORE_TESTS' ) || is_null( $block_hooks_version ) ) {
$block_hooks_version = get_option( 'woocommerce_hooked_blocks_version' );
}
/**
* A list of theme slugs to execute this with. This is a temporary
* measure until improvements to the Block Hooks API allow for exposing
* to all block themes.
*
* @since 8.4.0
*/
$theme_include_list = apply_filters( 'woocommerce_hooked_blocks_theme_include_list', array( 'Twenty Twenty-Four', 'Twenty Twenty-Three', 'Twenty Twenty-Two', 'Tsubaki', 'Zaino', 'Thriving Artist', 'Amulet', 'Tazza' ) );
// If block hooks are disabled or the version is not set, return early.
if ( 'no' === $block_hooks_version || false === $block_hooks_version ) {
return $hooked_blocks;
}
if ( $context && in_array( $active_theme_name, $theme_include_list, true ) ) {
foreach ( $this->hooked_block_placements as $placement ) {
// Valid placements are those that have no version specified,
// or have a version that is less than or equal to version specified in the woocommerce_hooked_blocks_version option.
$valid_placements = array_filter(
$this->hooked_block_placements,
function ( $placement ) use ( $block_hooks_version ) {
$placement_version = isset( $placement['version'] ) ? $placement['version'] : null;
return is_null( $placement_version ) || ! is_null( $placement_version ) && version_compare( $block_hooks_version, $placement_version, '>=' );
}
);
if ( $context && ! empty( $valid_placements ) ) {
foreach ( $valid_placements as $placement ) {
if ( $placement['position'] === $position && $placement['anchor'] === $anchor_block ) {
// If an area has been specified for this placement.

View File

@@ -316,9 +316,13 @@ class BlockTemplateUtils {
$wp_template_part_filenames = array(
'checkout-header.html',
'mini-cart.html',
'product-filters-overlay.html',
);
if ( Features::is_enabled( 'experimental-blocks' ) ) {
$wp_template_part_filenames[] = 'product-filters.html';
$wp_template_part_filenames[] = 'product-filters-overlay.html';
}
/*
* This may return the blockified directory for wp_templates.
* At the moment every template file has a corresponding blockified file.

View File

@@ -8,6 +8,7 @@ use WP_Query;
* {@internal This class and its methods are not intended for public use.}
*/
class ProductCollectionUtils {
/**
* Prepare and execute a query for the Product Collection block.
* This method is used by the Product Collection block and the No Results block.
@@ -78,6 +79,87 @@ class ProductCollectionUtils {
return self::remove_empty_array_recursive( $queries );
}
/**
* Parse WP Query's front-end context for the Product Collection block.
*
* The sourceData structure depends on the context type as follows:
* - site: [ ]
* - order: [ 'orderId' => int ]
* - cart: [ 'productIds' => int[] ]
* - archive: [ 'taxonomy' => string, 'termId' => int ]
* - product: [ 'productId' => int ]
*
* @return array $context {
* @type string $type The context type. Possible values are 'site', 'order', 'cart', 'archive', 'product'.
* @type array $sourceData The context source data. Can be the product ID of the viewed product, the order ID of the current order, etc.
* }
*/
public static function parse_frontend_location_context() {
global $wp_query;
// Default context.
// Hint: The Shop page uses the default context.
$type = 'site';
$source_data = array();
if ( ! ( $wp_query instanceof WP_Query ) ) {
return array(
'type' => $type,
'sourceData' => $source_data,
);
}
// As more areas are blockified, expected future contexts include:
// - is_checkout_pay_page().
// - is_view_order_page().
if ( is_order_received_page() ) {
$type = 'order';
$source_data = array( 'orderId' => absint( $wp_query->query_vars['order-received'] ) );
} elseif ( ( is_cart() || is_checkout() ) && isset( WC()->cart ) && is_a( WC()->cart, 'WC_Cart' ) ) {
$type = 'cart';
$items = array();
foreach ( WC()->cart->get_cart() as $cart_item ) {
if ( ! isset( $cart_item['product_id'] ) ) {
continue;
}
$items[] = absint( $cart_item['product_id'] );
}
$items = array_unique( array_filter( $items ) );
$source_data = array( 'productIds' => $items );
} elseif ( is_product_taxonomy() ) {
$source = $wp_query->get_queried_object();
$is_valid = is_a( $source, 'WP_Term' );
$taxonomy = $is_valid ? $source->taxonomy : '';
$term_id = $is_valid ? $source->term_id : '';
$type = 'archive';
$source_data = array(
'taxonomy' => wc_clean( $taxonomy ),
'termId' => absint( $term_id ),
);
} elseif ( is_product() ) {
$source = $wp_query->get_queried_object();
$product_id = is_a( $source, 'WP_Post' ) ? absint( $source->ID ) : 0;
$type = 'product';
$source_data = array( 'productId' => $product_id );
}
$context = array(
'type' => $type,
'sourceData' => $source_data,
);
return $context;
}
/**
* Remove falsy item from array, recursively.
*