Merge branch 'trunk' into spike/mini-cart-iapi
This commit is contained in:
commit
928793b30a
|
@ -59,7 +59,7 @@ For our Woo extension, we'll be appending our field right at the end with `wooco
|
|||
Let's get started with creating a new class which will hold the code for the field. Add a new file with the name `class-product-fields.php` to the `/includes/admin/` folder. Within the class, we add our namespace, an abort if anyone tries to call the file directly and a \_\_construct method which calls the `hooks()` method:
|
||||
|
||||
```php
|
||||
<?php
|
||||
<?php
|
||||
|
||||
namespace WooProductField\Admin;
|
||||
|
||||
|
@ -68,7 +68,7 @@ defined( 'ABSPATH' ) || exit;
|
|||
class ProductFields {
|
||||
|
||||
public function __construct() {
|
||||
$this->hooks();
|
||||
$this->hooks();
|
||||
}
|
||||
|
||||
private function hooks() {}
|
||||
|
@ -93,19 +93,19 @@ With the class set up and being called, we can create a function to add the cust
|
|||
```php
|
||||
public function add_field() {
|
||||
global $product_object;
|
||||
?>
|
||||
<div class="inventory_new_stock_information options_group show_if_simple show_if_variable">
|
||||
<?php woocommerce_wp_text_input(
|
||||
?>
|
||||
<div class="inventory_new_stock_information options_group show_if_simple show_if_variable">
|
||||
<?php woocommerce_wp_text_input(
|
||||
array(
|
||||
'id' => '_new_stock_information',
|
||||
'label' => __( 'New Stock', 'woo_product_field' ),
|
||||
'description' => __( 'Information shown in store', 'woo_product_field' ),
|
||||
'desc_tip' => true,
|
||||
'value' => $product_object->get_meta( '_new_stock_information' )
|
||||
'id' => '_new_stock_information',
|
||||
'label' => __( 'New Stock', 'woo_product_field' ),
|
||||
'description' => __( 'Information shown in store', 'woo_product_field' ),
|
||||
'desc_tip' => true,
|
||||
'value' => $product_object->get_meta( '_new_stock_information' )
|
||||
)
|
||||
); ?>
|
||||
</div>
|
||||
<?php
|
||||
); ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -119,8 +119,8 @@ Now that we have our field, we need to save it. For this, we can hook into wooco
|
|||
public function save_field( $post_id, $post ) {
|
||||
if ( isset( $_POST['_new_stock_information'] ) ) {
|
||||
$product = wc_get_product( intval( $post_id ) );
|
||||
$product->update_meta_data( '_new_stock_information', sanitize_text_field( $_POST['_new_stock_information'] ) );
|
||||
$product->save_meta_data();
|
||||
$product->update_meta_data( '_new_stock_information', sanitize_text_field( $_POST['_new_stock_information'] ) );
|
||||
$product->save_meta_data();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -142,12 +142,12 @@ If we add data and save the product, then the new meta data is inserted into the
|
|||
|
||||
At this point you have a working extension that saves a custom field for a product as product meta.
|
||||
Showing the field in the store
|
||||
If we want to display the new field in our store, then we can do this with the `get_meta()` method of the Woo product class: `$product->get_meta( '\_new_stock_information' )`
|
||||
If we want to display the new field in our store, then we can do this with the `get_meta()` method of the Woo product class: `$product->get_meta( '\_new_stock_information' )`
|
||||
|
||||
Let's get started by creating a new file /includes/class-product.php. You may have noticed that this is outside the `/admin/` folder as this code will run in the front. So when we set up the class, we also adjust the namespace accordingly:
|
||||
|
||||
```php
|
||||
<?php
|
||||
<?php
|
||||
|
||||
namespace WooProductField;
|
||||
|
||||
|
@ -155,7 +155,7 @@ defined( 'ABSPATH' ) || exit;
|
|||
|
||||
class Product {
|
||||
public function __construct() {
|
||||
$this->hooks();
|
||||
$this->hooks();
|
||||
}
|
||||
|
||||
private function hooks() { }
|
||||
|
@ -190,9 +190,9 @@ In our function we output the stock information with the [appropriate escape fun
|
|||
```php
|
||||
public function add_stock_info() {
|
||||
global $product;
|
||||
?>
|
||||
<p><?php echo esc_html( $product->get_meta( '_new_stock_information' ) ); ?> </p>
|
||||
<?php
|
||||
?>
|
||||
<p><?php echo esc_html( $product->get_meta( '_new_stock_information' ) ); ?> </p>
|
||||
<?php
|
||||
|
||||
}
|
||||
```
|
||||
|
@ -223,14 +223,14 @@ The setup is very similar to simple products, the main difference is that we nee
|
|||
|
||||
```php
|
||||
public function add_variation_field( $loop, $variation_data, $variation ) {
|
||||
$variation_product = wc_get_product( $variation->ID );
|
||||
$variation_product = wc_get_product( $variation->ID );
|
||||
|
||||
woocommerce_wp_text_input(
|
||||
array(
|
||||
'id' => '\_new_stock_information' . '[' . $loop . ']',
|
||||
'label' => \_\_( 'New Stock Information', 'woo_product_field' ),
|
||||
'wrapper_class' => 'form-row form-row-full',
|
||||
'value' => $variation_product->get_meta( '\_new_stock_information' )
|
||||
'id' => '\_new_stock_information' . '[' . $loop . ']',
|
||||
'label' => \_\_( 'New Stock Information', 'woo_product_field' ),
|
||||
'wrapper_class' => 'form-row form-row-full',
|
||||
'value' => $variation_product->get_meta( '\_new_stock_information' )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -242,8 +242,8 @@ For saving we use:
|
|||
public function save_variation_field( $variation_id, $i ) {
|
||||
if ( isset( $_POST['_new_stock_information'][$i] ) ) {
|
||||
$variation_product = wc_get_product( $variation_id );
|
||||
$variation_product->update_meta_data( '_new_stock_information', sanitize_text_field( $_POST['_new_stock_information'][$i] ) );
|
||||
$variation_product->save_meta_data();
|
||||
$variation_product->update_meta_data( '_new_stock_information', sanitize_text_field( $_POST['_new_stock_information'][$i] ) );
|
||||
$variation_product->save_meta_data();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1109,16 +1109,16 @@ The contents of this block will display when there are no products found.
|
|||
- **Supports:** align, color (background, gradients, link, text), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~
|
||||
- **Attributes:**
|
||||
|
||||
## Product Filter (Experimental) - woocommerce/product-filter
|
||||
## Product Filters (Experimental) - woocommerce/product-filters
|
||||
|
||||
A block that adds product filters to the product collection.
|
||||
Let shoppers filter products displayed on the page.
|
||||
|
||||
- **Name:** woocommerce/product-filter
|
||||
- **Name:** woocommerce/product-filters
|
||||
- **Category:** woocommerce
|
||||
- **Ancestor:** woocommerce/product-filters
|
||||
- **Ancestor:**
|
||||
- **Parent:**
|
||||
- **Supports:** ~~html~~, ~~inserter~~, ~~reusable~~
|
||||
- **Attributes:** attributeId, filterType, heading, isPreview
|
||||
- **Supports:** align, color (background, text), interactivity, layout (allowJustification, allowOrientation, allowVerticalAlignment, default, ~~allowInheriting~~), spacing (blockGap), typography (fontSize, textAlign), ~~inserter~~, ~~multiple~~
|
||||
- **Attributes:** overlay, overlayButtonStyle, overlayIcon, overlayIconSize
|
||||
|
||||
## Filter Options - woocommerce/product-filter-active
|
||||
|
||||
|
@ -1153,50 +1153,6 @@ Allows shoppers to reset this filter.
|
|||
- **Supports:** interactivity, ~~inserter~~
|
||||
- **Attributes:**
|
||||
|
||||
## Filter Options - woocommerce/product-filter-price
|
||||
|
||||
Enable customers to filter the product collection by choosing a price range.
|
||||
|
||||
- **Name:** woocommerce/product-filter-price
|
||||
- **Category:** woocommerce
|
||||
- **Ancestor:** woocommerce/product-filter
|
||||
- **Parent:**
|
||||
- **Supports:** interactivity, ~~inserter~~
|
||||
- **Attributes:** inlineInput, showInputFields
|
||||
|
||||
## Filter Options - woocommerce/product-filter-rating
|
||||
|
||||
Enable customers to filter the product collection by rating.
|
||||
|
||||
- **Name:** woocommerce/product-filter-rating
|
||||
- **Category:** woocommerce
|
||||
- **Ancestor:** woocommerce/product-filter
|
||||
- **Parent:**
|
||||
- **Supports:** color (text, ~~background~~), interactivity, ~~inserter~~
|
||||
- **Attributes:** className, displayStyle, isPreview, selectType, showCounts
|
||||
|
||||
## Filter Options - woocommerce/product-filter-stock-status
|
||||
|
||||
Enable customers to filter the product collection by stock status.
|
||||
|
||||
- **Name:** woocommerce/product-filter-stock-status
|
||||
- **Category:** woocommerce
|
||||
- **Ancestor:** woocommerce/product-filter
|
||||
- **Parent:**
|
||||
- **Supports:** color (text, ~~background~~), interactivity, ~~html~~, ~~inserter~~, ~~multiple~~
|
||||
- **Attributes:** className, displayStyle, isPreview, selectType, showCounts
|
||||
|
||||
## Product Filters (Experimental) - woocommerce/product-filters
|
||||
|
||||
Let shoppers filter products displayed on the page.
|
||||
|
||||
- **Name:** woocommerce/product-filters
|
||||
- **Category:** woocommerce
|
||||
- **Ancestor:**
|
||||
- **Parent:**
|
||||
- **Supports:** align, color (background, text), interactivity, layout (allowJustification, allowOrientation, allowVerticalAlignment, default, ~~allowInheriting~~), spacing (blockGap), typography (fontSize, textAlign), ~~inserter~~, ~~multiple~~
|
||||
- **Attributes:** overlay, overlayButtonStyle, overlayIcon, overlayIconSize
|
||||
|
||||
## Product Filters Overlay (Experimental) - woocommerce/product-filters-overlay
|
||||
|
||||
Display product filters in an overlay on top of a page.
|
||||
|
@ -1219,6 +1175,50 @@ Display overlay navigation controls.
|
|||
- **Supports:** align (center, left, right), color (background, text), layout (default, ~~allowEditing~~), position (sticky), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~inserter~~
|
||||
- **Attributes:** align, buttonStyle, iconSize, navigationStyle, overlayMode, style, triggerType
|
||||
|
||||
## Filter Options - woocommerce/product-filter-price
|
||||
|
||||
Enable customers to filter the product collection by choosing a price range.
|
||||
|
||||
- **Name:** woocommerce/product-filter-price
|
||||
- **Category:** woocommerce
|
||||
- **Ancestor:** woocommerce/product-filter
|
||||
- **Parent:**
|
||||
- **Supports:** interactivity, ~~inserter~~
|
||||
- **Attributes:** inlineInput, showInputFields
|
||||
|
||||
## Product Filter (Experimental) - woocommerce/product-filter
|
||||
|
||||
A block that adds product filters to the product collection.
|
||||
|
||||
- **Name:** woocommerce/product-filter
|
||||
- **Category:** woocommerce
|
||||
- **Ancestor:** woocommerce/product-filters
|
||||
- **Parent:**
|
||||
- **Supports:** inserter, ~~html~~, ~~reusable~~
|
||||
- **Attributes:** attributeId, filterType, heading, isPreview
|
||||
|
||||
## Filter Options - woocommerce/product-filter-rating
|
||||
|
||||
Enable customers to filter the product collection by rating.
|
||||
|
||||
- **Name:** woocommerce/product-filter-rating
|
||||
- **Category:** woocommerce
|
||||
- **Ancestor:** woocommerce/product-filter
|
||||
- **Parent:**
|
||||
- **Supports:** color (text, ~~background~~), interactivity, ~~inserter~~
|
||||
- **Attributes:** className, displayStyle, isPreview, selectType, showCounts
|
||||
|
||||
## Filter Options - woocommerce/product-filter-stock-status
|
||||
|
||||
Enable customers to filter the product collection by stock status.
|
||||
|
||||
- **Name:** woocommerce/product-filter-stock-status
|
||||
- **Category:** woocommerce
|
||||
- **Ancestor:** woocommerce/product-filter
|
||||
- **Parent:**
|
||||
- **Supports:** color (text, ~~background~~), interactivity, ~~html~~, ~~inserter~~
|
||||
- **Attributes:** className, displayStyle, isPreview, selectType, showCounts
|
||||
|
||||
## Product Gallery (Beta) - woocommerce/product-gallery
|
||||
|
||||
Showcase your products relevant images and media.
|
||||
|
|
|
@ -14,17 +14,17 @@ if ( ! function_exists( 'YOUR_PREFIX_login_message' ) ) {
|
|||
*/
|
||||
function YOUR_PREFIX_login_message() {
|
||||
if ( get_option( 'woocommerce_enable_myaccount_registration' ) == 'yes' ) {
|
||||
?>
|
||||
<div class="woocommerce-info">
|
||||
<p><?php _e( 'Returning customers login. New users register for next time so you can:', 'YOUR-TEXTDOMAIN' ); ?></p>
|
||||
<ul>
|
||||
<li><?php _e( 'View your order history', 'YOUR-TEXTDOMAIN' ); ?></li>
|
||||
<li><?php _e( 'Check on your orders', 'YOUR-TEXTDOMAIN' ); ?></li>
|
||||
<li><?php _e( 'Edit your addresses', 'YOUR-TEXTDOMAIN' ); ?></li>
|
||||
<li><?php _e( 'Change your password', 'YOUR-TEXTDOMAIN' ); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php
|
||||
?>
|
||||
<div class="woocommerce-info">
|
||||
<p><?php _e( 'Returning customers login. New users register for next time so you can:', 'YOUR-TEXTDOMAIN' ); ?></p>
|
||||
<ul>
|
||||
<li><?php _e( 'View your order history', 'YOUR-TEXTDOMAIN' ); ?></li>
|
||||
<li><?php _e( 'Check on your orders', 'YOUR-TEXTDOMAIN' ); ?></li>
|
||||
<li><?php _e( 'Edit your addresses', 'YOUR-TEXTDOMAIN' ); ?></li>
|
||||
<li><?php _e( 'Change your password', 'YOUR-TEXTDOMAIN' ); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
add_action( 'woocommerce_before_customer_login_form', 'YOUR_PREFIX_login_message' );
|
||||
|
|
|
@ -38,7 +38,7 @@ function big_apple_get_tax_class( $tax_class, $product ) {
|
|||
|
||||
Some merchants may require different tax rates to be applied based on a customer role to accommodate for wholesale status or tax exemption.
|
||||
|
||||
To enable this functionality, add the following snippet to your child theme's functions.php file or via a code snippet plugin. In this snippet, users with “administrator” capabilities will be assigned the **Zero rate tax class**. Adjust it according to your requirements.
|
||||
To enable this functionality, add the following snippet to your child theme's functions.php file or via a code snippet plugin. In this snippet, users with "administrator" capabilities will be assigned the **Zero rate tax class**. Adjust it according to your requirements.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
|
|
@ -127,3 +127,9 @@ These instructions presume you're currently have your `docs/` branch open and yo
|
|||
If you are a non-technical contributor who isn't experienced with command line tools, we're still happy to receive your contributions. If you're unable to include an updated manifest, please ensure that you mention this in your pull request's description.
|
||||
|
||||
If you're a technical contributor who is able to regenerate the manifest, we request that you do so where possible.
|
||||
|
||||
## Caveats
|
||||
|
||||
* Emojis are not supported.
|
||||
* Avoid copy pasting content from editors such as Google docs. For example, quotation-mark characters in these editors may not translate properly when ingested by our plugin.
|
||||
* You may want to references HTML like content, however, our plugin is set up to strip non-allowlisted HTML elements. You may use an [HTML named references](https://developer.mozilla.org/en-US/docs/Glossary/Character_reference) to create an HTML like tag, by using the less-than (<) and greater-than (>) symbol named references.
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
"post_title": "Blocks reference",
|
||||
"menu_title": "Blocks Reference",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/building-a-woo-store/block-references.md",
|
||||
"hash": "329f17097ce67074a915d7814b2363e8b9e908910c1f7b196c8f4fd8594cc55c",
|
||||
"hash": "9bbd3555641a70a0d7c24c818323a9270e437a6446998de9a6506e0c2ed6ddf5",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/building-a-woo-store/block-references.md",
|
||||
"id": "1fbe91d7fa4fafaf35f0297e4cee1e7958756aed"
|
||||
},
|
||||
|
@ -83,7 +83,7 @@
|
|||
"menu_title": "Add Custom Fields to Products",
|
||||
"tags": "how-to",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md",
|
||||
"hash": "59ef97ed2053dedfa5e7c658d5c7fa470d8110afc9b84584cb32b58389bf5687",
|
||||
"hash": "fe8cf43940f5166bf69f102aa4643cbe32415b1167d6b6d8968d434a4d113879",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md",
|
||||
"id": "64b686dcd5fdd4842be2fc570108231d5a8bfc1b"
|
||||
}
|
||||
|
@ -414,7 +414,7 @@
|
|||
"menu_title": "Configuring special tax scenarios",
|
||||
"tags": "code-snippet, tax",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/configuring_special_tax_scenarios.md",
|
||||
"hash": "acce5111eb917b7381d9bdcb260c72870e89d723195b86522050032741c5107c",
|
||||
"hash": "c2459568db70b97d9a901c83eecb7737746499fe03cc84133aab3cf981b5c96a",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/configuring_special_tax_scenarios.md",
|
||||
"id": "a8ab8b6734ba2ac5af7c6653635d15548abdab2a"
|
||||
},
|
||||
|
@ -439,7 +439,7 @@
|
|||
"post_title": "Add a message above the login / register form",
|
||||
"tags": "code-snippet",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/before-login--register-form.md",
|
||||
"hash": "49f0d942364ea6815e799972894406cb0963164adfb6c373222dcf7fb0ee6fb9",
|
||||
"hash": "dd3d61cd1f99d459956167041718e6a25693901a810d4bde47613d5ea2595ca3",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/before-login--register-form.md",
|
||||
"id": "26ea2036f16952c8c965f2ab38ab214e421aa615"
|
||||
},
|
||||
|
@ -594,7 +594,7 @@
|
|||
"post_title": "Contributing Technical Documentation",
|
||||
"menu_title": "Contributing Docs",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/contributing-docs/contributing-docs.md",
|
||||
"hash": "ee2eed4bc33ccbc4a84a2c73ee503f2df5a92183013e3f703bb439edab0a3fe3",
|
||||
"hash": "6733ba23f82a6e784ef10e2862aa3afd8a61e32181e157f67ccabfc8354aa989",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/contributing-docs/contributing-docs.md",
|
||||
"id": "71c1a72bfd4d5ae6aa656d4264b1bf3beb6eca1c"
|
||||
}
|
||||
|
@ -727,7 +727,7 @@
|
|||
"menu_title": "Implement merchant onboarding",
|
||||
"tags": "how-to",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/handling-merchant-onboarding.md",
|
||||
"hash": "a34e2c637ac47adb1dd2e7b2510fb3eb7ff517b8f31e9cfa6fe4d3920f2567e7",
|
||||
"hash": "c73e3c5015e6cda3be9ebd2d5fbda590ac9fa599e5fb02163c971c01060970ad",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/handling-merchant-onboarding.md",
|
||||
"id": "89fe15dc232379f546852822230c334d3d940b93"
|
||||
},
|
||||
|
@ -939,7 +939,7 @@
|
|||
"menu_title": "Enable HPOS for large stores",
|
||||
"tags": "how-to",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/high-performance-order-storage/guide-large-store.md",
|
||||
"hash": "8bcae74d27e3a4ee9a902719c7e8d5aec4a4d82d7c14acd8665a72b9d4758181",
|
||||
"hash": "1e144ac0d0a7c869093533bf94a1f218a42930a3f3edcbcfdd1210448243a992",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/high-performance-order-storage/guide-large-store.md",
|
||||
"id": "b6156ac7b77d75022867e9ebb968bc9c1c35f0da"
|
||||
},
|
||||
|
@ -1033,7 +1033,7 @@
|
|||
"menu_title": "Performance best practices",
|
||||
"tags": "reference",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/performance/performance-best-practices.md",
|
||||
"hash": "5af1f4e4085e85a1693390f40e238cbd6a4a0b7d5d304afdda935c34fed97c64",
|
||||
"hash": "3c49b5553e99b64ecdbdad565866479f7af5e474dbbcc9aa36d711c02d8b0906",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/performance/performance-best-practices.md",
|
||||
"id": "35bda1cd7068d6179a9e46cca8d7dc2694d0df96"
|
||||
}
|
||||
|
@ -1050,7 +1050,7 @@
|
|||
"menu_title": "Registering custom collections",
|
||||
"tags": "how-to",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-collection-block/register-product-collection.md",
|
||||
"hash": "e3df65c5eec52e4bb797e34c040dbb8f820ea6571e9ce50b1d518e95ca6cb169",
|
||||
"hash": "27c321bed35524d74019e015f5eed6cdca7e6c2efe0bc89ffdd2b9b5d43c47e8",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-collection-block/register-product-collection.md",
|
||||
"id": "3bf26fc7c56ae6e6a56e1171f750f5204fcfcece"
|
||||
},
|
||||
|
@ -1059,7 +1059,7 @@
|
|||
"menu_title": "DOM Events",
|
||||
"tags": "how-to",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-collection-block/dom-events.md",
|
||||
"hash": "fbad20bc55cc569161e80478c0789db3c34cf35513e669554af36db1de967a26",
|
||||
"hash": "59a4b49eb146774d33229bc60ab7d8f74381493f6e7089ca8f0e2d0eb433a7a4",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-collection-block/dom-events.md",
|
||||
"id": "c8d247b91472740075871e6b57a9583d893ac650"
|
||||
}
|
||||
|
@ -1451,7 +1451,7 @@
|
|||
{
|
||||
"post_title": "Template structure & Overriding templates via a theme",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/theme-development/template-structure.md",
|
||||
"hash": "ff781eff7998ea93723f644bddd4f6da6f73c635bcfc3cd46950f03a8b83b26c",
|
||||
"hash": "a82d885c8395385dc51c976b2b51eec88cdcce72bf905fda78f97d77533ce079",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/theme-development/template-structure.md",
|
||||
"id": "34bfebec9fc45e680976814928a7b8a1778af14e"
|
||||
},
|
||||
|
@ -1554,7 +1554,7 @@
|
|||
"post_title": "WooCommerce Extension Guidelines - Navigation",
|
||||
"menu_title": "Navigation",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/ux-guidelines-extensions/navigation.md",
|
||||
"hash": "f14cbd3750451934d173ec43aa996067699bdd8fc05f5c34097d1967a79145db",
|
||||
"hash": "9c5313a31ebe26909a2cfaaa0bcfcdc9d5a0855ac0ddb9808832524be313ebb6",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/ux-guidelines-extensions/navigation.md",
|
||||
"id": "9922abbcea787a91b3360f49fa53d5402a604e6b"
|
||||
},
|
||||
|
@ -1804,5 +1804,5 @@
|
|||
"categories": []
|
||||
}
|
||||
],
|
||||
"hash": "2ecf48b6181dae0526b3858df889bce4e6ee425e10f2c43d151771d845c5a948"
|
||||
"hash": "12e9abfbcdbeae7dd5cc12dc3af3818332f272cb4b3ad12993cc010299009013"
|
||||
}
|
|
@ -26,7 +26,7 @@ Setup tasks appear on the WooCommerce Admin home screen and prompt a merchant to
|
|||
To register your task as an extended task list item, you'll need to start by creating a new PHP class that extends the Task class. This class will define the properties and behavior of your custom task.
|
||||
|
||||
```php
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* Custom task example.
|
||||
*
|
||||
|
@ -113,37 +113,37 @@ import {
|
|||
} from '@woocommerce/onboarding';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
|
||||
const Task = ( { onComplete, task, query } ) => {
|
||||
const Task = ( { onComplete, task, query } ) => {
|
||||
// Implement your task UI/feature here.
|
||||
return <div></div>;
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
registerPlugin( 'add-task-content', {
|
||||
render: () => (
|
||||
<WooOnboardingTask id="my-task">
|
||||
{ ( { onComplete, query, task } ) => (
|
||||
<Task onComplete={ onComplete } task={ task } query={ query } />
|
||||
render: () => (
|
||||
<WooOnboardingTask id="my-task">
|
||||
{ ( { onComplete, query, task } ) => (
|
||||
<Task onComplete={ onComplete } task={ task } query={ query } />
|
||||
) }
|
||||
</WooOnboardingTask>
|
||||
</WooOnboardingTask>
|
||||
),
|
||||
} );
|
||||
|
||||
registerPlugin( 'add-task-list-item', {
|
||||
scope: 'woocommerce-tasks',
|
||||
render: () => (
|
||||
<WooOnboardingTaskListItem id="my-task">
|
||||
{ ( { defaultTaskItem: DefaultTaskItem } ) => (
|
||||
render: () => (
|
||||
<WooOnboardingTaskListItem id="my-task">
|
||||
{ ( { defaultTaskItem: DefaultTaskItem } ) => (
|
||||
// Add a custom wrapper around the default task item.
|
||||
<div
|
||||
<div
|
||||
className="woocommerce-custom-tasklist-item"
|
||||
style={ {
|
||||
border: '1px solid red',
|
||||
} }
|
||||
>
|
||||
<DefaultTaskItem />
|
||||
</div>
|
||||
>
|
||||
<DefaultTaskItem />
|
||||
</div>
|
||||
) }
|
||||
</WooOnboardingTaskListItem>
|
||||
</WooOnboardingTaskListItem>
|
||||
),
|
||||
} );
|
||||
```
|
||||
|
@ -168,37 +168,37 @@ import { registerPlugin } from '@wordpress/plugins';
|
|||
Next, we create a [functional component](https://reactjs.org/docs/components-and-props.html) that returns our task card. The intermixed JavaScript/HTML syntax we're using here is called JSX. If you're unfamiliar with it, you can [read more about it in the React docs](https://reactjs.org/docs/introducing-jsx.html).
|
||||
|
||||
```js
|
||||
const Task = ( { onComplete, task } ) => {
|
||||
const Task = ( { onComplete, task } ) => {
|
||||
const { actionTask } = useDispatch( ONBOARDING_STORE_NAME );
|
||||
const { isActioned } = task;
|
||||
|
||||
return (
|
||||
<Card className="woocommerce-task-card">
|
||||
<CardBody>
|
||||
<Card className="woocommerce-task-card">
|
||||
<CardBody>
|
||||
{ __(
|
||||
"This task's completion status is dependent on being actioned. The action button below will action this task, while the complete button will optimistically complete the task in the task list and redirect back to the task list. Note that in this example, the task must be actioned for completion to persist.",
|
||||
'plugin-domain'
|
||||
) }{ ' ' }
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
{ __( 'Task actioned status: ', 'plugin-domain' ) }{ ' ' }
|
||||
{ isActioned ? 'actioned' : 'not actioned' }
|
||||
<br />
|
||||
<br />
|
||||
<div>
|
||||
<button
|
||||
onClick={ () => {
|
||||
<br />
|
||||
<br />
|
||||
<div>
|
||||
<button
|
||||
onClick={ () => {
|
||||
actionTask( 'my-task' );
|
||||
} }
|
||||
>
|
||||
>
|
||||
{ __( 'Action task', 'plugin-domain' ) }
|
||||
</button>
|
||||
<button onClick={ onComplete }>
|
||||
</button>
|
||||
<button onClick={ onComplete }>
|
||||
{ __( 'Complete', 'plugin-domain' ) }
|
||||
</button>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</button>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
@ -211,14 +211,14 @@ Next, we register the Task component as a plugin named "add-task-content" using
|
|||
|
||||
```js
|
||||
registerPlugin( 'add-task-content', {
|
||||
render: () => (
|
||||
render: () => (
|
||||
|
||||
{ ( {
|
||||
onComplete,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
query,
|
||||
task,
|
||||
} ) => }
|
||||
} ) => }
|
||||
|
||||
),
|
||||
scope: 'woocommerce-tasks',
|
||||
|
@ -232,20 +232,20 @@ Finally, we register another plugin named "my-task-list-item-plugin." This plugi
|
|||
```js
|
||||
registerPlugin( 'my-task-list-item-plugin', {
|
||||
scope: 'woocommerce-tasks',
|
||||
render: () => (
|
||||
<WooOnboardingTaskListItem id="my-task">
|
||||
{ ( { defaultTaskItem: DefaultTaskItem } ) => (
|
||||
render: () => (
|
||||
<WooOnboardingTaskListItem id="my-task">
|
||||
{ ( { defaultTaskItem: DefaultTaskItem } ) => (
|
||||
// Add a custom wrapper around the default task item.
|
||||
<div
|
||||
<div
|
||||
className="woocommerce-custom-tasklist-item"
|
||||
style={ {
|
||||
border: '1px solid red',
|
||||
} }
|
||||
>
|
||||
<DefaultTaskItem />
|
||||
</div>
|
||||
>
|
||||
<DefaultTaskItem />
|
||||
</div>
|
||||
) }
|
||||
</WooOnboardingTaskListItem>
|
||||
</WooOnboardingTaskListItem>
|
||||
),
|
||||
} );
|
||||
```
|
||||
|
@ -344,7 +344,7 @@ import { addFilter } from '@wordpress/hooks';
|
|||
addFilter(
|
||||
'woocommerce_admin_homescreen_quicklinks',
|
||||
'my-extension',
|
||||
( quickLinks ) => {
|
||||
( quickLinks ) => {
|
||||
return [
|
||||
...quickLinks,
|
||||
{
|
||||
|
@ -374,7 +374,7 @@ Despite being a part of the new React-powered admin experience in WooCommerce, A
|
|||
The recommended approach for using Admin Notes is to encapsulate your note within its own class that uses the [NoteTraits](https://github.com/woocommerce/woocommerce-admin/blob/831c9ff13a862f22cf53d3ae676daeabbefe90ad/src/Notes/NoteTraits.php) trait included with WooCommerce Admin. Below is a simple example of what this might look like:
|
||||
|
||||
```php
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* Simple note provider
|
||||
*
|
||||
|
@ -423,10 +423,10 @@ class ExampleNote {
|
|||
$note = new Automattic\WooCommerce\Admin\Notes\Note();
|
||||
|
||||
// Set our note's title.
|
||||
$note->set_title( 'Getting Started' );
|
||||
$note->set_title( 'Getting Started' );
|
||||
|
||||
// Set our note's content.
|
||||
$note->set_content(
|
||||
$note->set_content(
|
||||
sprintf(
|
||||
'Extension activated on %s.', $activated_time_formatted
|
||||
)
|
||||
|
@ -436,41 +436,41 @@ class ExampleNote {
|
|||
// You can use this property to re-localize notes on the fly, but
|
||||
// that is just one use. You can store other data here too. This
|
||||
// is backed by a longtext column in the database.
|
||||
$note->set_content_data( (object) array(
|
||||
'getting_started' => true,
|
||||
'activated' => $activated_time,
|
||||
'activated_formatted' => $activated_time_formatted
|
||||
$note->set_content_data( (object) array(
|
||||
'getting_started' => true,
|
||||
'activated' => $activated_time,
|
||||
'activated_formatted' => $activated_time_formatted
|
||||
) );
|
||||
|
||||
// Set the type of the note. Note types are defined as enum-style
|
||||
// constants in the Note class. Available note types are:
|
||||
// error, warning, update, info, marketing.
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
|
||||
// Set the type of layout the note uses. Supported layout types are:
|
||||
// 'banner', 'plain', 'thumbnail'
|
||||
$note->set_layout( 'plain' );
|
||||
$note->set_layout( 'plain' );
|
||||
|
||||
// Set the image for the note. This property renders as the src
|
||||
// attribute for an img tag, so use a string here.
|
||||
$note->set_image( '' );
|
||||
$note->set_image( '' );
|
||||
|
||||
|
||||
// Set the note name and source. You should store your extension's
|
||||
// name (slug) in the source property of the note. You can use
|
||||
// the name property of the note to support multiple sub-types of
|
||||
// notes. This also gives you a handy way of namespacing your notes.
|
||||
$note->set_source( 'inbox-note-example');
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_source( 'inbox-note-example');
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
|
||||
// Add action buttons to the note. A note can support 0, 1, or 2 actions.
|
||||
// The first parameter is the action name, which can be used for event handling.
|
||||
// The second parameter renders as the label for the button.
|
||||
// The third parameter is an optional URL for actions that require navigation.
|
||||
$note->add_action(
|
||||
$note->add_action(
|
||||
'settings', 'Open Settings', '?page=wc-settings&tab=general'
|
||||
);
|
||||
$note->add_action(
|
||||
$note->add_action(
|
||||
'learn_more', 'Learn More', 'https://example.com'
|
||||
);
|
||||
|
||||
|
@ -559,12 +559,12 @@ Next, we'll instantiate a new `Note` object.
|
|||
|
||||
Once we have an instance of the Note class, we can work with its API to set its properties, starting with its title.
|
||||
|
||||
`$note->set_title( 'Getting Started' );`
|
||||
`$note->set_title( 'Getting Started' );`
|
||||
|
||||
Then we'll use some of the timestamp data we collected above to set the note's content.
|
||||
|
||||
```php
|
||||
$note->set_content(
|
||||
$note->set_content(
|
||||
sprintf(
|
||||
'Extension activated on %s.', $activated_time_formatted
|
||||
)
|
||||
|
@ -574,41 +574,41 @@ $note->set_content(
|
|||
In addition to regular content, notes also support structured content using the `content_data` property. You can use this property to re-localize notes on the fly, but that is just one use case. You can store other data here too. This is backed by a `longtext` column in the database.
|
||||
|
||||
```php
|
||||
$note->set_content_data( (object) array(
|
||||
'getting_started' => true,
|
||||
'activated' => $activated_time,
|
||||
'activated_formatted' => $activated_time_formatted
|
||||
$note->set_content_data( (object) array(
|
||||
'getting_started' => true,
|
||||
'activated' => $activated_time,
|
||||
'activated_formatted' => $activated_time_formatted
|
||||
) );
|
||||
```
|
||||
|
||||
Next, we'll set the note's `type` property. Note types are defined as enum-style class constants in the `Note` class. Available note types are _error_, _warning_, _update_, _info_, and _marketing_. When selecting a note type, be aware that the _error_ and _update_ result in the note being shown as a Store Alert, not in the Inbox. It's best to avoid using these types of notes unless you absolutely need to.
|
||||
|
||||
`$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );`
|
||||
`$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );`
|
||||
|
||||
Admin Notes also support a few different layouts. You can specify `banner`, `plain`, or `thumbnail` as the layout. If you're interested in seeing the different layouts in action, take a look at [this simple plugin](https://gist.github.com/octaedro/864315edaf9c6a2a6de71d297be1ed88) that you can install to experiment with them.
|
||||
|
||||
We'll choose `plain` as our layout, but it's also the default, so we could leave this property alone and the effect would be the same.
|
||||
|
||||
`$note->set_layout( 'plain' );`
|
||||
`$note->set_layout( 'plain' );`
|
||||
|
||||
If you have an image that you want to add to your Admin Note, you can specify it using the `set_image` function. This property ultimately renders as the `src` attribute on an `img` tag, so use a string here.
|
||||
|
||||
`$note->set_image( '' );`
|
||||
`$note->set_image( '' );`
|
||||
|
||||
Next, we'll set the values for our Admin Note's `name` and `source` properties. As a best practice, you should store your extension's name (i.e. its slug) in the `source` property of the note. You can use the `name` property to support multiple sub-types of notes. This gives you a handy way of namespacing your notes and managing them at both a high and low level.
|
||||
|
||||
```php
|
||||
$note->set_source( 'inbox-note-example');
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_source( 'inbox-note-example');
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
```
|
||||
|
||||
Admin Notes can support 0, 1, or 2 actions (buttons). You can use these actions to capture events that trigger asynchronous processes or help the merchant navigate to a particular view to complete a step, or even simply to provide an external link for further information. The `add_action()` function takes up to three arguments. The first is the action name, which can be used for event handling, the second renders as a label for the action's button, and the third is an optional URL for actions that require navigation.
|
||||
|
||||
```php
|
||||
$note->add_action(
|
||||
$note->add_action(
|
||||
'settings', 'Open Settings', '?page=wc-settings&tab=general'
|
||||
);
|
||||
$note->add_action(
|
||||
$note->add_action(
|
||||
'learn_more', 'Learn More', 'https://example.com'
|
||||
);
|
||||
```
|
||||
|
|
|
@ -87,7 +87,7 @@ Also, we didn't see a noticeable negative performance impact when keeping synchr
|
|||
|
||||
### Switch to HPOS as authoritative
|
||||
|
||||
It's time to switch to HPOS. Go to **WooCommerce > Settings > Advanced > Features** and set HPOS to be authoritative (select “**Use the WooCommerce orders tables**").
|
||||
It's time to switch to HPOS. Go to **WooCommerce > Settings > Advanced > Features** and set HPOS to be authoritative (select "**Use the WooCommerce orders tables**").
|
||||
|
||||
As mentioned above, don't turn off synchronization yet. If there are any issues, the system can be instantaneously reverted to the posts table, resulting in no downtime.
|
||||
|
||||
|
@ -107,7 +107,7 @@ We disable sync on read first because it demands more resources. If your site is
|
|||
|
||||
### Switch off sync on write
|
||||
|
||||
If everything is working as expected, you can disable sync on write as well. Given sync on read was already disabled, you can disable sync altogether from the settings. As usual, go to **WooCommerce > Settings > Advanced > Features**, and uncheck **“Enable compatibility mode"**.
|
||||
If everything is working as expected, you can disable sync on write as well. Given sync on read was already disabled, you can disable sync altogether from the settings. As usual, go to **WooCommerce > Settings > Advanced > Features**, and uncheck **"Enable compatibility mode"**.
|
||||
|
||||
On our high-volume site, we fully disabled sync after 1 week. We still run some manual synchronization (via `wp wc cot sync`) periodically so that we have the opportunity to fall back to posts immediately should anything happen.
|
||||
|
||||
|
@ -118,11 +118,11 @@ Now with synchronization fully disabled, test out various critical flows, check
|
|||
### Review: Phase 3 Checklist
|
||||
|
||||
1. [ ] Plan to be online and monitoring your live site for a period of time.
|
||||
2. [ ] Enable synchronization with posts set as authoritative: in **WooCommerce > Settings > Advanced > Features** > select “**Use the WordPress posts tables**".
|
||||
2. [ ] Enable synchronization with posts set as authoritative: in **WooCommerce > Settings > Advanced > Features** > select "**Use the WordPress posts tables**".
|
||||
3. [ ] Start migration via CLI using the `wp wc cot sync` command.
|
||||
4. [ ] Monitor for errors during migration; halt or resume as necessary.
|
||||
5. [ ] Verify migrated data integrity using the verify command `wp wc cot verify_cot_data`.
|
||||
6. [ ] Enable synchronization with HPOS set as authoritative: in **WooCommerce > Settings > Advanced > Features** > select “Use the **WooCommerce orders tables**".
|
||||
6. [ ] Enable synchronization with HPOS set as authoritative: in **WooCommerce > Settings > Advanced > Features** > select "Use the **WooCommerce orders tables**".
|
||||
7. [ ] Test all critical flows, perform checkouts with multiple payment methods, and verify order data accuracy.
|
||||
8. [ ] Monitor support tickets for any issues.
|
||||
9. [ ] Disable synchronization on read using the provided snippet: `add_filter( 'woocommerce_hpos_enable_sync_on_read', '__return_false' );`
|
||||
|
|
|
@ -20,7 +20,7 @@ For WooCommerce extensions, performance optimization means ensuring that your co
|
|||
|
||||
## Benchmarking Performance
|
||||
|
||||
Setting clear performance benchmarks is essential for development and continuous improvement of WooCommerce extensions. A recommended performance standard is achieving a Chrome Core Web Vitals "Performance" score of 90 or above on Woo Express, using tools like the [Chrome Lighthouse](https://developer.chrome.com/docs/lighthouse/overview/).
|
||||
Setting clear performance benchmarks is essential for development and continuous improvement of WooCommerce extensions. A recommended performance standard is achieving a Chrome Core Web Vitals "Performance" score of 90 or above on a simple Woo site, using tools like the [Chrome Lighthouse](https://developer.chrome.com/docs/lighthouse/overview/).
|
||||
|
||||
### Using Accessible Tools for Benchmarking
|
||||
|
||||
|
|
|
@ -10,13 +10,13 @@ tags: how-to
|
|||
|
||||
This event is triggered when Product Collection block was rendered or re-rendered (e.g. due to page change).
|
||||
|
||||
### `detail` parameters
|
||||
### `wc-blocks_product_list_rendered` - `detail` parameters
|
||||
|
||||
| Parameter | Type | Default value | Description |
|
||||
| ------------------ | ------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `collection` | string | `undefined` | Collection type. It's `undefined` for "create your own" collections as the type is not specified. For other Core collections it can be one of type: `woocommerce/product-collection/best-sellers`, `woocommerce/product-collection/featured`, `woocommerce/product-collection/new-arrivals`, `woocommerce/product-collection/on-sale`, `woocommerce/product-collection/top-rated`. For custom collections it will hold their name. |
|
||||
|
||||
### Example usage
|
||||
### `wc-blocks_product_list_rendered` - Example usage
|
||||
|
||||
```javascript
|
||||
window.document.addEventListener(
|
||||
|
@ -27,3 +27,27 @@ window.document.addEventListener(
|
|||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Event: `wc-blocks_viewed_product`
|
||||
|
||||
This event is triggered when some blocks are clicked in order to view product (redirect to product page).
|
||||
|
||||
### `wc-blocks_viewed_product` - `detail` parameters
|
||||
|
||||
| Parameter | Type | Default value | Description |
|
||||
| ------------------ | ------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `collection` | string | `undefined` | Collection type. It's `undefined` for "create your own" collections as the type is not specified. For other Core collections it can be one of type: `woocommerce/product-collection/best-sellers`, `woocommerce/product-collection/featured`, `woocommerce/product-collection/new-arrivals`, `woocommerce/product-collection/on-sale`, `woocommerce/product-collection/top-rated`. For custom collections it will hold their name. |
|
||||
| `productId` | number | | Product ID |
|
||||
|
||||
### `wc-blocks_viewed_product` Example usage
|
||||
|
||||
```javascript
|
||||
window.document.addEventListener(
|
||||
'wc-blocks_viewed_product',
|
||||
( e ) => {
|
||||
const { collection, productId } = e.detail;
|
||||
console.log( collection ) // -> collection name, e.g. "woocommerce/product-collection/featured" or undefined for default one
|
||||
console.log( productId ) // -> product ID, e.g. 34
|
||||
}
|
||||
);
|
||||
```
|
||||
|
|
|
@ -6,20 +6,20 @@ tags: how-to
|
|||
|
||||
# Register Product Collection
|
||||
|
||||
The `__experimentalRegisterProductCollection` function is part of the `@woocommerce/blocks-registry` package. This function allows third party developers to register a new collection. This function accepts most of the arguments that are accepted by [Block Variation](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/#defining-a-block-variation).
|
||||
The `__experimentalRegisterProductCollection` function is part of the `@woocommerce/blocks-registry` package. This function allows third party developers to register a new collection. This function accepts most of the arguments that are accepted by [Block Variation](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/#defining-a-block-variation).
|
||||
|
||||
> [!WARNING]
|
||||
> It's experimental and may change in the future. Please use it with caution.
|
||||
|
||||
**There are two ways to use this function:**
|
||||
|
||||
1. Using `@woocommerce/dependency-extraction-webpack-plugin` in a Webpack configuration: This will allow you to import the function from the package & use it in your code. For example:
|
||||
1. Using `@woocommerce/dependency-extraction-webpack-plugin` in a Webpack configuration: This will allow you to import the function from the package & use it in your code. For example:
|
||||
|
||||
```tsx
|
||||
import { __experimentalRegisterProductCollection } from "@woocommerce/blocks-registry";
|
||||
```
|
||||
|
||||
2. Using the global `wc` object: This will allow you to use the function using the global JS object without importing it. For example:
|
||||
2. Using the global `wc` object: This will allow you to use the function using the global JS object without importing it. For example:
|
||||
|
||||
```tsx
|
||||
wc.wcBlocksRegistry.__experimentalRegisterProductCollection({
|
||||
|
@ -113,7 +113,7 @@ __experimentalRegisterProductCollection({
|
|||
});
|
||||
```
|
||||
|
||||
As you can see in the example above, we are registering a new collection with the name `woocommerce/product-collection/my-custom-collection` & title `My Custom Collection`. Here is screenshot of how it will look like:
|
||||
As you can see in the example above, we are registering a new collection with the name `woocommerce/product-collection/my-custom-collection` & title `My Custom Collection`. Here is screenshot of how it will look like:
|
||||
![image](https://github.com/woocommerce/woocommerce/assets/16707866/7fddbc02-a6cd-494e-b2f4-13dd5ef9cf96)
|
||||
|
||||
### Example 2: Register a new collection with a preview
|
||||
|
|
|
@ -22,7 +22,7 @@ Below is video walkthrough showing how one may go about updating the template fi
|
|||
|
||||
## Template list
|
||||
|
||||
The various template files on your WooCommerce site can be found via an FTP client or your hosts file manager, in `/wp-content/plugins/woocommerce/templates/`. Below are links to the current and earlier versions of the WooCommerce template files on Github, where you can view the code exactly as it appears in those files:
|
||||
The various template files on your WooCommerce site can be found via an FTP client or your hosts file manager, in `/wp-content/plugins/woocommerce/templates/`. Below are links to the current and earlier versions of the WooCommerce template files on Github, where you can view the code exactly as it appears in those files:
|
||||
|
||||
| Latest Version | Files |
|
||||
| -------------- | ----- |
|
||||
|
@ -96,7 +96,7 @@ Below are the links to the files of all major previous WooCommerce versions:
|
|||
|
||||
## Changing Templates via Hooks
|
||||
|
||||
When you open a template file, you will notice they all contain _hooks_ that allow you to add/move content without needing to edit template files themselves. Hooks are a way for one piece of code to interact/modify another piece of code at specific, pre-defined spots. This method allows implementing a code snippet that “hooks” into a particular a theme location. It avoids upgrade issues, as the template files can be left completely untouched and doesn't require a child theme to be configured.
|
||||
When you open a template file, you will notice they all contain _hooks_ that allow you to add/move content without needing to edit template files themselves. Hooks are a way for one piece of code to interact/modify another piece of code at specific, pre-defined spots. This method allows implementing a code snippet that "hooks" into a particular a theme location. It avoids upgrade issues, as the template files can be left completely untouched and doesn't require a child theme to be configured.
|
||||
|
||||
Let's take a look at [/wp-content/plugins/woocommerce/templates/emails/admin-new-order.php](https://github.com/woocommerce/woocommerce/blob/8.9.0/plugins/woocommerce/templates/emails/admin-new-order.php) and see what a hook looks like. Starting on line 30, we see the following code, which is responsible for producing the order details section of the New Order email.
|
||||
|
||||
|
@ -137,7 +137,7 @@ The copied file will now override the WooCommerce default template file, so you
|
|||
|
||||
---
|
||||
|
||||
**Note** A (desirable) side-effect of your templates being upgrade-safe is that WooCommerce core templates will update, but your custom overrides will not. You may occassionally see notices in your System Status report that says, e.g. “version 3.5.0 is out of date. The core version is 3.7.0″. Should that happen, follow the Fixing Outdated WooCommerce Templates guide to bring them in line.
|
||||
**Note** A (desirable) side-effect of your templates being upgrade-safe is that WooCommerce core templates will update, but your custom overrides will not. You may occassionally see notices in your System Status report that says, e.g. "version 3.5.0 is out of date. The core version is 3.7.0". Should that happen, follow the Fixing Outdated WooCommerce Templates guide to bring them in line.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -7,10 +7,7 @@ menu_title: Navigation
|
|||
|
||||
Examples:
|
||||
|
||||
- If your extension is extending a component within WooCommerce, it should live within either the Extensions navigation drawer (in Woo Express stores), or directly within that category's section.
|
||||
|
||||
Extensions drawer (Woo Express)
|
||||
![Navigation extensions drawer](https://developer.woocommerce.com/docs/wp-content/uploads/sites/3/2024/01/Image-1224x572-1.png)
|
||||
- If your extension is extending a component within WooCommerce, it should live directly within that category's section.
|
||||
|
||||
![Navigation category](https://developer.woocommerce.com/docs/wp-content/uploads/sites/3/2024/01/Image-1242x764-1.png)
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add DataForms panel to the experimental product data views list.
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo } from '@wordpress/element';
|
||||
import { edit } from '@wordpress/icons';
|
||||
import { privateApis as routerPrivateApis } from '@wordpress/router';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Product } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { unlock } from '../../lock-unlock';
|
||||
|
||||
const { useHistory, useLocation } = unlock( routerPrivateApis );
|
||||
|
||||
export const useEditProductAction = ( { postType }: { postType: string } ) => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
return useMemo(
|
||||
() => ( {
|
||||
id: 'edit-product',
|
||||
label: __( 'Edit', 'woocommerce' ),
|
||||
isPrimary: true,
|
||||
icon: edit,
|
||||
supportsBulk: true,
|
||||
isEligible( product: Product ) {
|
||||
if ( product.status === 'trash' ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
callback( items: Product[] ) {
|
||||
const product = items[ 0 ];
|
||||
history.push( {
|
||||
...location.params,
|
||||
postId: product.id,
|
||||
postType,
|
||||
quickEdit: true,
|
||||
} );
|
||||
},
|
||||
} ),
|
||||
[ history, location.params ]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { DataForm, isItemValid } from '@wordpress/dataviews';
|
||||
import type { Form } from '@wordpress/dataviews';
|
||||
import { createElement, useState, useMemo } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
// @ts-expect-error missing types.
|
||||
__experimentalHeading as Heading,
|
||||
// @ts-expect-error missing types.
|
||||
__experimentalText as Text,
|
||||
// @ts-expect-error missing types.
|
||||
__experimentalHStack as HStack,
|
||||
// @ts-expect-error missing types.
|
||||
__experimentalVStack as VStack,
|
||||
FlexItem,
|
||||
Button,
|
||||
} from '@wordpress/components';
|
||||
// @ts-expect-error missing types.
|
||||
// eslint-disable-next-line @woocommerce/dependency-group
|
||||
import { privateApis as editorPrivateApis } from '@wordpress/editor';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { unlock } from '../../lock-unlock';
|
||||
import { productFields } from '../product-list/fields';
|
||||
|
||||
const { NavigableRegion } = unlock( editorPrivateApis );
|
||||
|
||||
const form: Form = {
|
||||
type: 'panel',
|
||||
fields: [ 'name', 'status' ],
|
||||
};
|
||||
|
||||
type ProductEditProps = {
|
||||
subTitle?: string;
|
||||
className?: string;
|
||||
hideTitleFromUI?: boolean;
|
||||
actions?: React.JSX.Element;
|
||||
postType: string;
|
||||
postId: string;
|
||||
};
|
||||
|
||||
export default function ProductEdit( {
|
||||
subTitle,
|
||||
actions,
|
||||
className,
|
||||
hideTitleFromUI = true,
|
||||
postType,
|
||||
postId = '',
|
||||
}: ProductEditProps ) {
|
||||
const classes = classNames( 'edit-product-page', className, {
|
||||
'is-empty': ! postId,
|
||||
} );
|
||||
const ids = useMemo( () => postId.split( ',' ), [ postId ] );
|
||||
const { initialEdits } = useSelect(
|
||||
( select ) => {
|
||||
return {
|
||||
initialEdits:
|
||||
ids.length === 1
|
||||
? select( 'wc/admin/products' ).getProduct( ids[ 0 ] )
|
||||
: null,
|
||||
};
|
||||
},
|
||||
[ postType, ids ]
|
||||
);
|
||||
const [ edits, setEdits ] = useState( {} );
|
||||
const itemWithEdits = useMemo( () => {
|
||||
return {
|
||||
...initialEdits,
|
||||
...edits,
|
||||
};
|
||||
}, [ initialEdits, edits ] );
|
||||
const isUpdateDisabled = ! isItemValid(
|
||||
itemWithEdits,
|
||||
productFields,
|
||||
form
|
||||
);
|
||||
|
||||
const onSubmit = async ( event: Event ) => {
|
||||
event.preventDefault();
|
||||
|
||||
if ( ! isItemValid( itemWithEdits, productFields, form ) ) {
|
||||
return;
|
||||
}
|
||||
// Empty save.
|
||||
|
||||
setEdits( {} );
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigableRegion
|
||||
className={ classes }
|
||||
ariaLabel={ __( 'Product Edit', 'woocommerce' ) }
|
||||
>
|
||||
<div className="edit-product-content">
|
||||
{ ! hideTitleFromUI && (
|
||||
<VStack
|
||||
className="edit-site-page-header"
|
||||
as="header"
|
||||
spacing={ 0 }
|
||||
>
|
||||
<HStack className="edit-site-page-header__page-title">
|
||||
<Heading
|
||||
as="h2"
|
||||
level={ 3 }
|
||||
weight={ 500 }
|
||||
className="edit-site-page-header__title"
|
||||
truncate
|
||||
>
|
||||
{ __( 'Product Edit', 'woocommerce' ) }
|
||||
</Heading>
|
||||
<FlexItem className="edit-site-page-header__actions">
|
||||
{ actions }
|
||||
</FlexItem>
|
||||
</HStack>
|
||||
{ subTitle && (
|
||||
<Text
|
||||
variant="muted"
|
||||
as="p"
|
||||
className="edit-site-page-header__sub-title"
|
||||
>
|
||||
{ subTitle }
|
||||
</Text>
|
||||
) }
|
||||
</VStack>
|
||||
) }
|
||||
{ ! postId && (
|
||||
<p>{ __( 'Select a product to edit', 'woocommerce' ) }</p>
|
||||
) }
|
||||
{ postId && (
|
||||
<VStack spacing={ 4 } as="form" onSubmit={ onSubmit }>
|
||||
<DataForm
|
||||
data={ itemWithEdits }
|
||||
fields={ productFields }
|
||||
form={ form }
|
||||
onChange={ setEdits }
|
||||
/>
|
||||
<FlexItem>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
// @ts-expect-error missing type.
|
||||
accessibleWhenDisabled
|
||||
disabled={ isUpdateDisabled }
|
||||
__next40pxDefaultSize
|
||||
>
|
||||
{ __( 'Update', 'woocommerce' ) }
|
||||
</Button>
|
||||
</FlexItem>
|
||||
</VStack>
|
||||
) }
|
||||
</div>
|
||||
</NavigableRegion>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
.edit-product-page {
|
||||
padding: $grid-unit-30;
|
||||
background: #fff;
|
||||
color: #2f2f2f;
|
||||
container: edit-site-page/inline-size;
|
||||
height: 100%;
|
||||
transition: width .2s ease-out;
|
||||
|
||||
.edit-product-content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.is-empty .edit-product-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement, Fragment } from '@wordpress/element';
|
||||
import { Product } from '@woocommerce/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Field } from '@wordpress/dataviews';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { OPERATOR_IS } from '../constants';
|
||||
|
||||
const STATUSES = [
|
||||
{ value: 'draft', label: __( 'Draft', 'woocommerce' ) },
|
||||
{ value: 'future', label: __( 'Scheduled', 'woocommerce' ) },
|
||||
{ value: 'private', label: __( 'Private', 'woocommerce' ) },
|
||||
{ value: 'publish', label: __( 'Published', 'woocommerce' ) },
|
||||
{ value: 'trash', label: __( 'Trash', 'woocommerce' ) },
|
||||
];
|
||||
|
||||
/**
|
||||
* TODO: auto convert some of the product editor blocks ( from the blocks directory ) to this format.
|
||||
* The edit function should work relatively well with the edit from the blocks, the only difference is that the blocks rely on getEntityProp to get the value
|
||||
*/
|
||||
export const productFields: Field< Product >[] = [
|
||||
{
|
||||
id: 'name',
|
||||
label: __( 'Name', 'woocommerce' ),
|
||||
enableHiding: false,
|
||||
type: 'text',
|
||||
render: function nameRender( { item }: { item: Product } ) {
|
||||
return <>{ item.name }</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sku',
|
||||
label: __( 'SKU', 'woocommerce' ),
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
render: ( { item }: { item: Product } ) => {
|
||||
return <>{ item.sku }</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'date',
|
||||
label: __( 'Date', 'woocommerce' ),
|
||||
render: ( { item }: { item: Product } ) => {
|
||||
return <time>{ item.date_created }</time>;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __( 'Status', 'woocommerce' ),
|
||||
id: 'status',
|
||||
getValue: ( { item }: { item: Product } ) =>
|
||||
STATUSES.find( ( { value } ) => value === item.status )?.label ??
|
||||
item.status,
|
||||
elements: STATUSES,
|
||||
filterBy: {
|
||||
operators: [ OPERATOR_IS ],
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
|
@ -1,17 +1,19 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Action, DataViews, View } from '@wordpress/dataviews';
|
||||
import { DataViews, View } from '@wordpress/dataviews';
|
||||
import {
|
||||
createElement,
|
||||
useState,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
Fragment,
|
||||
} from '@wordpress/element';
|
||||
import { Product, ProductQuery } from '@woocommerce/data';
|
||||
import { drawerRight } from '@wordpress/icons';
|
||||
import { privateApis as routerPrivateApis } from '@wordpress/router';
|
||||
import { store as coreStore } from '@wordpress/core-data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import classNames from 'classnames';
|
||||
|
@ -39,63 +41,13 @@ import {
|
|||
useDefaultViews,
|
||||
defaultLayouts,
|
||||
} from '../sidebar-dataviews/default-views';
|
||||
import { LAYOUT_LIST, OPERATOR_IS } from '../constants';
|
||||
import { LAYOUT_LIST } from '../constants';
|
||||
import { productFields } from './fields';
|
||||
import { useEditProductAction } from '../dataviews-actions';
|
||||
|
||||
const { NavigableRegion } = unlock( editorPrivateApis );
|
||||
const { NavigableRegion, usePostActions } = unlock( editorPrivateApis );
|
||||
const { useHistory, useLocation } = unlock( routerPrivateApis );
|
||||
|
||||
const STATUSES = [
|
||||
{ value: 'draft', label: __( 'Draft', 'woocommerce' ) },
|
||||
{ value: 'future', label: __( 'Scheduled', 'woocommerce' ) },
|
||||
{ value: 'private', label: __( 'Private', 'woocommerce' ) },
|
||||
{ value: 'publish', label: __( 'Published', 'woocommerce' ) },
|
||||
{ value: 'trash', label: __( 'Trash', 'woocommerce' ) },
|
||||
];
|
||||
|
||||
/**
|
||||
* TODO: auto convert some of the product editor blocks ( from the blocks directory ) to this format.
|
||||
* The edit function should work relatively well with the edit from the blocks, the only difference is that the blocks rely on getEntityProp to get the value
|
||||
*/
|
||||
const fields = [
|
||||
{
|
||||
id: 'name',
|
||||
label: __( 'Name', 'woocommerce' ),
|
||||
enableHiding: false,
|
||||
type: 'text',
|
||||
render: function nameRender( { item }: { item: Product } ) {
|
||||
return item.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sku',
|
||||
label: __( 'SKU', 'woocommerce' ),
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
render: ( { item }: { item: Product } ) => {
|
||||
return item.sku;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'date',
|
||||
label: __( 'Date', 'woocommerce' ),
|
||||
render: ( { item }: { item: Product } ) => {
|
||||
return <time>{ item.date_created }</time>;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __( 'Status', 'woocommerce' ),
|
||||
id: 'status',
|
||||
getValue: ( { item }: { item: Product } ) =>
|
||||
STATUSES.find( ( { value } ) => value === item.status )?.label ??
|
||||
item.status,
|
||||
elements: STATUSES,
|
||||
filterBy: {
|
||||
operators: [ OPERATOR_IS ],
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
export type ProductListProps = {
|
||||
subTitle?: string;
|
||||
className?: string;
|
||||
|
@ -105,7 +57,6 @@ export type ProductListProps = {
|
|||
|
||||
const PAGE_SIZE = 25;
|
||||
const EMPTY_ARRAY: Product[] = [];
|
||||
const EMPTY_ACTIONS_ARRAY: Action< Product >[] = [];
|
||||
|
||||
const getDefaultView = (
|
||||
defaultViews: Array< { slug: string; view: View } >,
|
||||
|
@ -265,6 +216,33 @@ export default function ProductList( {
|
|||
[ totalCount, view.perPage ]
|
||||
);
|
||||
|
||||
const { labels, canCreateRecord } = useSelect(
|
||||
( select ) => {
|
||||
const { getPostType, canUser } = select( coreStore );
|
||||
const postTypeData:
|
||||
| { labels: Record< string, string > }
|
||||
| undefined = getPostType( postType );
|
||||
return {
|
||||
labels: postTypeData?.labels,
|
||||
canCreateRecord: canUser( 'create', {
|
||||
kind: 'postType',
|
||||
name: postType,
|
||||
} ),
|
||||
};
|
||||
},
|
||||
[ postType ]
|
||||
);
|
||||
|
||||
const postTypeActions = usePostActions( {
|
||||
postType,
|
||||
context: 'list',
|
||||
} );
|
||||
const editAction = useEditProductAction( { postType } );
|
||||
const actions = useMemo(
|
||||
() => [ editAction, ...postTypeActions ],
|
||||
[ postTypeActions, editAction ]
|
||||
);
|
||||
|
||||
const classes = classNames( 'edit-site-page', className );
|
||||
|
||||
return (
|
||||
|
@ -290,7 +268,18 @@ export default function ProductList( {
|
|||
{ __( 'Products', 'woocommerce' ) }
|
||||
</Heading>
|
||||
<FlexItem className="edit-site-page-header__actions">
|
||||
{ /* { actions } */ }
|
||||
{ labels?.add_new_item && canCreateRecord && (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={ true }
|
||||
// @ts-expect-error missing type.
|
||||
__next40pxDefaultSize
|
||||
>
|
||||
{ labels.add_new_item }
|
||||
</Button>
|
||||
</>
|
||||
) }
|
||||
</FlexItem>
|
||||
</HStack>
|
||||
{ subTitle && (
|
||||
|
@ -307,12 +296,11 @@ export default function ProductList( {
|
|||
<DataViews
|
||||
key={ activeView + isCustom }
|
||||
paginationInfo={ paginationInfo }
|
||||
// @ts-expect-error types seem rather strict for this still.
|
||||
fields={ fields }
|
||||
actions={ EMPTY_ACTIONS_ARRAY }
|
||||
fields={ productFields }
|
||||
data={ records || EMPTY_ARRAY }
|
||||
isLoading={ isLoading }
|
||||
view={ view }
|
||||
actions={ actions }
|
||||
onChangeView={ setView }
|
||||
onChangeSelection={ onChangeSelection }
|
||||
getItemId={ getItemId }
|
||||
|
|
|
@ -9,6 +9,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router';
|
|||
*/
|
||||
import { unlock } from '../lock-unlock';
|
||||
import ProductList from './product-list';
|
||||
import ProductEdit from './product-edit';
|
||||
import DataViewsSidebarContent from './sidebar-dataviews';
|
||||
import SidebarNavigationScreen from './sidebar-navigation-screen';
|
||||
|
||||
|
@ -32,7 +33,13 @@ export type Route = {
|
|||
|
||||
export default function useLayoutAreas() {
|
||||
const { params = {} } = useLocation();
|
||||
const { postType = 'product', layout = 'table', canvas } = params;
|
||||
const {
|
||||
postType = 'product',
|
||||
layout = 'table',
|
||||
canvas,
|
||||
quickEdit: showQuickEdit,
|
||||
postId,
|
||||
} = params;
|
||||
// Products list.
|
||||
if ( [ 'product' ].includes( postType ) ) {
|
||||
const isListLayout = layout === 'list' || ! layout;
|
||||
|
@ -49,9 +56,13 @@ export default function useLayoutAreas() {
|
|||
content: <ProductList />,
|
||||
preview: false,
|
||||
mobile: <ProductList postType={ postType } />,
|
||||
edit: showQuickEdit && (
|
||||
<ProductEdit postType={ postType } postId={ postId } />
|
||||
),
|
||||
},
|
||||
widths: {
|
||||
content: isListLayout ? 380 : undefined,
|
||||
edit: showQuickEdit && ! isListLayout ? 380 : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -41,3 +41,4 @@ body.js.is-fullscreen-mode {
|
|||
}
|
||||
|
||||
@import "products-app/sidebar-dataviews/style.scss";
|
||||
@import "products-app/product-edit/style.scss";
|
||||
|
|
|
@ -50,7 +50,7 @@ const tabs: Tabs = {
|
|||
},
|
||||
extensions: {
|
||||
name: 'extensions',
|
||||
title: __( 'Browse', 'woocommerce' ),
|
||||
title: __( 'Extensions', 'woocommerce' ),
|
||||
showUpdateCount: false,
|
||||
updateCount: 0,
|
||||
},
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
Scripts located in this directory are meant to be loaded on wp-admin pages outside the context of WooCommerce Admin, such as the post editor. Adding the script name to `wpAdminScripts` in the Webpack config will automatically build these scripts.
|
||||
# WP Admin Scripts
|
||||
|
||||
Scripts must be manually enqueued with any necessary dependencies. For example, `onboarding-homepage-notice` uses the WooCommerce navigation package:
|
||||
Scripts located in this directory are meant to be loaded on wp-admin pages outside the context of WooCommerce Admin, such as the post editor.
|
||||
|
||||
Each subdirectory of this directory is automatically added as an entrypoint in the [webpack build script](../../../../plugins/woocommerce-admin/webpack.config.js#L71) of WooCommerce Admin. When they are built, each one results in a pair of `<subdirectory-name>.asset.php` and `<subdirectory-name>.js` file in the `build\wp-admin-scripts\` path, but they will not be automatically loaded on every PHP page.
|
||||
|
||||
The `<subdirectory-name>.asset.php` file contains a list of automatically detected dependencies generated by the Dependency Extraction Webpack Plugin, so JS dependencies are not required to be manually enqueued as long as the `WCAdminAssets::register_script()` function is used to enqueue the script.
|
||||
|
||||
As an example, the `payment-method-promotions` wp-admin-scripts has generated `payment-method-promotions.asset.php` with the contents below:
|
||||
|
||||
`<?php return array('dependencies' => array('react', 'wc-components', 'wc-experimental', 'wc-settings', 'wc-store-data', 'wc-tracks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => 'e64b1e69145febe07734');`
|
||||
|
||||
And is registered as a script like so: [`plugins/woocommerce/src/Internal/Admin/WCPayPromotion/Init.php`](../../../../plugins/woocommerce/src/Internal/Admin/WCPayPromotion/Init.php#L179),
|
||||
|
||||
`WCAdminAssets::register_script( 'wp-admin-scripts', 'payment-method-promotions', true );`
|
||||
|
||||
`wp_enqueue_script( 'onboarding-homepage-notice', Loader::get_url( 'wp-scripts/onboarding-homepage-notice.js' ), array( 'wc-navigation' ) );`
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
const { get } = require( 'lodash' );
|
||||
const path = require( 'path' );
|
||||
const fs = require( 'fs' );
|
||||
const CopyWebpackPlugin = require( 'copy-webpack-plugin' );
|
||||
const CustomTemplatedPathPlugin = require( '@wordpress/custom-templated-path-webpack-plugin' );
|
||||
const BundleAnalyzerPlugin =
|
||||
|
@ -25,7 +26,22 @@ const WC_ADMIN_PHASE = process.env.WC_ADMIN_PHASE || 'development';
|
|||
const isHot = Boolean( process.env.HOT );
|
||||
const isProduction = NODE_ENV === 'production';
|
||||
|
||||
const getSubdirectoriesAt = ( searchPath ) => {
|
||||
const dir = path.resolve( searchPath );
|
||||
return fs
|
||||
.readdirSync( dir, { withFileTypes: true } )
|
||||
.filter( ( entry ) => entry.isDirectory() )
|
||||
.map( ( entry ) => entry.name );
|
||||
};
|
||||
|
||||
const WC_ADMIN_PACKAGES_DIR = '../../packages/js';
|
||||
const WP_ADMIN_SCRIPTS_DIR = './client/wp-admin-scripts';
|
||||
|
||||
// wpAdminScripts are loaded on wp-admin pages outside the context of WooCommerce Admin
|
||||
// See ./client/wp-admin-scripts/README.md for more details
|
||||
const wpAdminScripts = getSubdirectoriesAt( WP_ADMIN_SCRIPTS_DIR ); // automatically include all subdirs
|
||||
const wcAdminPackages = [
|
||||
// we use a whitelist for this instead of dynamically generating it because not all folders are packages meant for consumption
|
||||
'admin-layout',
|
||||
'components',
|
||||
'csv-export',
|
||||
|
@ -44,49 +60,16 @@ const wcAdminPackages = [
|
|||
'product-editor',
|
||||
'remote-logging',
|
||||
];
|
||||
// wpAdminScripts are loaded on wp-admin pages outside the context of WooCommerce Admin
|
||||
// See ./client/wp-admin-scripts/README.md for more details
|
||||
const wpAdminScripts = [
|
||||
'marketing-coupons',
|
||||
'onboarding-homepage-notice',
|
||||
'onboarding-product-notice',
|
||||
'onboarding-product-import-notice',
|
||||
'onboarding-tax-notice',
|
||||
'print-shipping-label-banner',
|
||||
'beta-features-tracking-modal',
|
||||
'payment-method-promotions',
|
||||
'onboarding-load-sample-products-notice',
|
||||
'product-tracking',
|
||||
'add-term-tracking',
|
||||
'attributes-tracking',
|
||||
'category-tracking',
|
||||
'tags-tracking',
|
||||
'product-tour',
|
||||
'wc-addons-tour',
|
||||
'settings-tracking',
|
||||
'order-tracking',
|
||||
'product-import-tracking',
|
||||
'variable-product-tour',
|
||||
'product-category-metabox',
|
||||
'shipping-settings-region-picker',
|
||||
'command-palette',
|
||||
'command-palette-analytics',
|
||||
'woo-connect-notice',
|
||||
'woo-plugin-update-connect-notice',
|
||||
'woo-enable-autorenew',
|
||||
'woo-renew-subscription',
|
||||
'woo-subscriptions-notice',
|
||||
'woo-product-usage-notice',
|
||||
];
|
||||
|
||||
const getEntryPoints = () => {
|
||||
const entryPoints = {
|
||||
app: './client/index.js',
|
||||
};
|
||||
wcAdminPackages.forEach( ( name ) => {
|
||||
entryPoints[ name ] = `../../packages/js/${ name }`;
|
||||
entryPoints[ name ] = `${ WC_ADMIN_PACKAGES_DIR }/${ name }`;
|
||||
} );
|
||||
wpAdminScripts.forEach( ( name ) => {
|
||||
entryPoints[ name ] = `./client/wp-admin-scripts/${ name }`;
|
||||
entryPoints[ name ] = `${ WP_ADMIN_SCRIPTS_DIR }/${ name }`;
|
||||
} );
|
||||
return entryPoints;
|
||||
};
|
||||
|
|
|
@ -70,6 +70,17 @@ export const triggerProductListRenderedEvent = ( payload: {
|
|||
} );
|
||||
};
|
||||
|
||||
export const triggerViewedProductEvent = ( payload: {
|
||||
collection?: CoreCollectionNames | string;
|
||||
productId: number;
|
||||
} ): void => {
|
||||
dispatchEvent( 'wc-blocks_viewed_product', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: payload,
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that listens to a jQuery event and dispatches a native JS event.
|
||||
* Useful to convert WC Core events into events that can be read by blocks.
|
||||
|
|
|
@ -8,7 +8,10 @@ import {
|
|||
getElement,
|
||||
getContext,
|
||||
} from '@woocommerce/interactivity';
|
||||
import { triggerProductListRenderedEvent } from '@woocommerce/base-utils';
|
||||
import {
|
||||
triggerProductListRenderedEvent,
|
||||
triggerViewedProductEvent,
|
||||
} from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -17,6 +20,8 @@ import { CoreCollectionNames } from './types';
|
|||
import './style.scss';
|
||||
|
||||
export type ProductCollectionStoreContext = {
|
||||
// Available on the <li/> product element and deeper
|
||||
productId?: number;
|
||||
isPrefetchNextOrPreviousLink: boolean;
|
||||
animation: 'start' | 'finish';
|
||||
accessibilityMessage: string;
|
||||
|
@ -164,6 +169,14 @@ const productCollectionStore = {
|
|||
yield prefetch( ref.href );
|
||||
}
|
||||
},
|
||||
*viewProduct() {
|
||||
const { collection, productId } =
|
||||
getContext< ProductCollectionStoreContext >();
|
||||
|
||||
if ( productId ) {
|
||||
triggerViewedProductEvent( { collection, productId } );
|
||||
}
|
||||
},
|
||||
},
|
||||
callbacks: {
|
||||
/**
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"supports": {
|
||||
"html": false,
|
||||
"reusable": false,
|
||||
"inserter": false
|
||||
"inserter": true
|
||||
},
|
||||
"ancestor": [
|
||||
"woocommerce/product-filters"
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"supports": {
|
||||
"interactivity": true,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"inserter": false,
|
||||
"color": {
|
||||
"text": true,
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { test as base, expect } from '@woocommerce/e2e-utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import ProductCollectionPage from './product-collection.page';
|
||||
|
||||
const test = base.extend< { pageObject: ProductCollectionPage } >( {
|
||||
pageObject: async ( { page, admin, editor }, use ) => {
|
||||
const pageObject = new ProductCollectionPage( {
|
||||
page,
|
||||
admin,
|
||||
editor,
|
||||
} );
|
||||
await use( pageObject );
|
||||
},
|
||||
} );
|
||||
|
||||
test.describe( 'Product Collection - extensibility JS events', () => {
|
||||
test( 'emits wc-blocks_product_list_rendered event on init and on page change', async ( {
|
||||
pageObject,
|
||||
page,
|
||||
} ) => {
|
||||
await pageObject.createNewPostAndInsertBlock();
|
||||
|
||||
await page.addInitScript( () => {
|
||||
let eventFired = 0;
|
||||
window.document.addEventListener(
|
||||
'wc-blocks_product_list_rendered',
|
||||
( e ) => {
|
||||
const { collection } = e.detail;
|
||||
window.eventPayload = collection;
|
||||
window.eventFired = ++eventFired;
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
await pageObject.publishAndGoToFrontend();
|
||||
|
||||
await expect
|
||||
.poll( async () => await page.evaluate( 'window.eventPayload' ) )
|
||||
.toBe( undefined );
|
||||
await expect
|
||||
.poll( async () => await page.evaluate( 'window.eventFired' ) )
|
||||
.toBe( 1 );
|
||||
|
||||
await page.getByRole( 'link', { name: 'Next Page' } ).click();
|
||||
|
||||
await expect
|
||||
.poll( async () => await page.evaluate( 'window.eventFired' ) )
|
||||
.toBe( 2 );
|
||||
} );
|
||||
|
||||
test( 'emits one wc-blocks_product_list_rendered event per block', async ( {
|
||||
pageObject,
|
||||
page,
|
||||
} ) => {
|
||||
// Adding three blocks in total
|
||||
await pageObject.createNewPostAndInsertBlock();
|
||||
await pageObject.insertProductCollection();
|
||||
await pageObject.chooseCollectionInPost();
|
||||
await pageObject.insertProductCollection();
|
||||
await pageObject.chooseCollectionInPost();
|
||||
|
||||
await page.addInitScript( () => {
|
||||
let eventFired = 0;
|
||||
window.document.addEventListener(
|
||||
'wc-blocks_product_list_rendered',
|
||||
() => {
|
||||
window.eventFired = ++eventFired;
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
await pageObject.publishAndGoToFrontend();
|
||||
|
||||
await expect
|
||||
.poll( async () => await page.evaluate( 'window.eventFired' ) )
|
||||
.toBe( 3 );
|
||||
} );
|
||||
|
||||
test.describe( 'wc-blocks_viewed_product is emitted', () => {
|
||||
let promise: Promise< { productId?: number; collection?: string } >;
|
||||
|
||||
test.beforeEach( async ( { page, pageObject } ) => {
|
||||
await pageObject.createNewPostAndInsertBlock( 'featured' );
|
||||
|
||||
promise = new Promise( ( resolve ) => {
|
||||
void page.exposeFunction( 'resolvePayload', resolve );
|
||||
void page.addInitScript( () => {
|
||||
window.document.addEventListener(
|
||||
'wc-blocks_viewed_product',
|
||||
( e ) => {
|
||||
window.resolvePayload( e.detail );
|
||||
}
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
await pageObject.publishAndGoToFrontend();
|
||||
} );
|
||||
|
||||
test( 'when Product Image is clicked', async ( { page } ) => {
|
||||
await page
|
||||
.locator( '[data-block-name="woocommerce/product-image"]' )
|
||||
.nth( 0 )
|
||||
.click();
|
||||
|
||||
const { collection, productId } = await promise;
|
||||
expect( collection ).toEqual(
|
||||
'woocommerce/product-collection/featured'
|
||||
);
|
||||
expect( productId ).toEqual( expect.any( Number ) );
|
||||
} );
|
||||
|
||||
test( 'when Product Title is clicked', async ( { page } ) => {
|
||||
await page.locator( '.wp-block-post-title' ).nth( 0 ).click();
|
||||
|
||||
const { collection, productId } = await promise;
|
||||
expect( collection ).toEqual(
|
||||
'woocommerce/product-collection/featured'
|
||||
);
|
||||
expect( productId ).toEqual( expect.any( Number ) );
|
||||
} );
|
||||
|
||||
test( 'when Add to Cart Anchor is clicked', async ( { page } ) => {
|
||||
await page.getByLabel( 'Select options for “V-Neck T-' ).click();
|
||||
|
||||
const { collection, productId } = await promise;
|
||||
expect( collection ).toEqual(
|
||||
'woocommerce/product-collection/featured'
|
||||
);
|
||||
expect( productId ).toEqual( expect.any( Number ) );
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -901,72 +901,6 @@ test.describe( 'Product Collection', () => {
|
|||
await expect( products ).toHaveText( expectedProducts );
|
||||
} );
|
||||
} );
|
||||
|
||||
test.describe( 'Extensibility - JS events', () => {
|
||||
test( 'emits wc-blocks_product_list_rendered event on init and on page change', async ( {
|
||||
pageObject,
|
||||
page,
|
||||
} ) => {
|
||||
await pageObject.createNewPostAndInsertBlock();
|
||||
|
||||
await page.addInitScript( () => {
|
||||
let eventFired = 0;
|
||||
window.document.addEventListener(
|
||||
'wc-blocks_product_list_rendered',
|
||||
( e ) => {
|
||||
const { collection } = e.detail;
|
||||
window.eventPayload = collection;
|
||||
window.eventFired = ++eventFired;
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
await pageObject.publishAndGoToFrontend();
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => await page.evaluate( 'window.eventPayload' )
|
||||
)
|
||||
.toBe( undefined );
|
||||
await expect
|
||||
.poll( async () => await page.evaluate( 'window.eventFired' ) )
|
||||
.toBe( 1 );
|
||||
|
||||
await page.getByRole( 'link', { name: 'Next Page' } ).click();
|
||||
|
||||
await expect
|
||||
.poll( async () => await page.evaluate( 'window.eventFired' ) )
|
||||
.toBe( 2 );
|
||||
} );
|
||||
|
||||
test( 'emits one wc-blocks_product_list_rendered event per block', async ( {
|
||||
pageObject,
|
||||
page,
|
||||
} ) => {
|
||||
// Adding three blocks in total
|
||||
await pageObject.createNewPostAndInsertBlock();
|
||||
await pageObject.insertProductCollection();
|
||||
await pageObject.chooseCollectionInPost();
|
||||
await pageObject.insertProductCollection();
|
||||
await pageObject.chooseCollectionInPost();
|
||||
|
||||
await page.addInitScript( () => {
|
||||
let eventFired = 0;
|
||||
window.document.addEventListener(
|
||||
'wc-blocks_product_list_rendered',
|
||||
() => {
|
||||
window.eventFired = ++eventFired;
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
await pageObject.publishAndGoToFrontend();
|
||||
|
||||
await expect
|
||||
.poll( async () => await page.evaluate( 'window.eventFired' ) )
|
||||
.toBe( 3 );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
test.describe( 'Testing "usesReference" argument in "registerProductCollection"', () => {
|
||||
|
|
|
@ -1,7 +1,19 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { test, expect } from '@woocommerce/e2e-utils';
|
||||
import { test, expect, BlockData } from '@woocommerce/e2e-utils';
|
||||
|
||||
const blockData: BlockData = {
|
||||
name: 'Product Filters (Experimental)',
|
||||
slug: 'woocommerce/product-filters',
|
||||
mainClass: '.wp-block-woocommerce-product-filters',
|
||||
selectors: {
|
||||
editor: {
|
||||
block: '.wp-block-woocommerce-product-filters',
|
||||
},
|
||||
frontend: {},
|
||||
},
|
||||
};
|
||||
|
||||
test.describe( 'Product Filters Template Part', () => {
|
||||
test.beforeEach( async ( { admin, requestUtils } ) => {
|
||||
|
@ -31,8 +43,50 @@ test.describe( 'Product Filters Template Part', () => {
|
|||
|
||||
test( 'should render the Product Filters block', async ( { editor } ) => {
|
||||
const productFiltersBlock = editor.canvas.getByLabel(
|
||||
'Block: Product Filters (Experimental)'
|
||||
`Block: ${ blockData.name }`
|
||||
);
|
||||
await expect( productFiltersBlock ).toBeVisible();
|
||||
} );
|
||||
|
||||
test( 'Filters > can be added multiple times', async ( { editor } ) => {
|
||||
const block = editor.canvas.getByLabel( `Block: ${ blockData.name }` );
|
||||
await expect( block ).toBeVisible();
|
||||
|
||||
const searchTerms = [
|
||||
'Status (Experimental)',
|
||||
'Price (Experimental)',
|
||||
'Rating (Experimental)',
|
||||
'Attribute (Experimental)',
|
||||
'Active (Experimental)',
|
||||
];
|
||||
|
||||
for ( const filter of searchTerms ) {
|
||||
await editor.selectBlocks( blockData.selectors.editor.block );
|
||||
|
||||
const addBlock = block.getByRole( 'button', {
|
||||
name: 'Add block',
|
||||
} );
|
||||
|
||||
await addBlock.click();
|
||||
|
||||
await editor.page.getByPlaceholder( 'Search' ).fill( filter );
|
||||
|
||||
const searchResult = editor.page.getByRole( 'option', {
|
||||
name: filter,
|
||||
} );
|
||||
await expect( searchResult ).toBeVisible();
|
||||
|
||||
await searchResult.click();
|
||||
|
||||
let _locator = `[aria-label="Block: ${ filter }"]`;
|
||||
|
||||
// We need to treat the attributes filter different because
|
||||
// the variation of the block label depends on the product attribute.
|
||||
if ( filter === 'Attribute (Experimental)' ) {
|
||||
_locator = '.wp-block-woocommerce-product-filter-attribute';
|
||||
}
|
||||
|
||||
await expect( editor.canvas.locator( _locator ) ).toHaveCount( 2 );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Update /merchant tests (second five files), so they are passing against Pressable env.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
Comment: Allow multiple instances of the product filters
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
Comment: This reverts an existing PR.
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
Comment: Change label `Browse` to `Extensions`
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Move Pressable and WPCOM e2e runss to release-checks instead of daily-checks
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Update /merchant tests so they are passing against Pressable env.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: enhancement
|
||||
|
||||
Add filters for custom orderby query params for WC Analytics
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Product Elements send a JS event when user attempts to view a product
|
|
@ -1,4 +0,0 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Added more paths to remote logger query param whitelist
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Updated webpack build script for wc admin so that the wp-admin-scripts are dynamically fetched from the fs instead of a hard list
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Update /shopper tests (second part), so they are passing against Pressable env.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
|
||||
Provide screen readers with product sorting status message.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
|
||||
Add endpoint title to the document title.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Make Admin\API\Reports\Customers\DataStore::anonymize_customer accept an order instance as a parameter to ensure compatibility with the `woocommerce_privacy_remove_order_personal_data` hook.
|
|
@ -1,4 +0,0 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Add check to ensure themes API is safe
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Only log PTK error if no action has been scheduled
|
|
@ -1,4 +0,0 @@
|
|||
Significance: minor
|
||||
Type: enhancement
|
||||
|
||||
Add query params masking to remote logger
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Put site visibility badge behind the launch your store feature flag
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: performance
|
||||
|
||||
Only run woocommerce_admin_shared_settings filter on admin requests
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Update docs
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Replace retired theme Nokul with Gizmo in the CYS experience
|
|
@ -1,4 +0,0 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Update WooCommerce Shipping Promo Banner to install the latest version of WooCommerce Shipping instead of WCS&T.
|
|
@ -148,7 +148,14 @@ jQuery( function ( $ ) {
|
|||
} );
|
||||
});
|
||||
|
||||
document.addEventListener( 'DOMContentLoaded' , function() {
|
||||
/**
|
||||
* Focus on the first notice element on the page.
|
||||
*
|
||||
* Populated live regions don't always are announced by screen readers.
|
||||
* This function focus on the first notice message with the role="alert"
|
||||
* attribute to make sure it's announced.
|
||||
*/
|
||||
function focus_populate_live_region() {
|
||||
var noticeClasses = [ 'woocommerce-message', 'woocommerce-error', 'wc-block-components-notice-banner' ];
|
||||
var noticeSelectors = noticeClasses.map( function( className ) {
|
||||
return '.' + className + '[role="alert"]';
|
||||
|
@ -168,4 +175,28 @@ document.addEventListener( 'DOMContentLoaded' , function() {
|
|||
firstNotice.focus();
|
||||
clearTimeout( delayFocusNoticeId );
|
||||
}, 500 );
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the sorted by live region.
|
||||
*/
|
||||
function refresh_sorted_by_live_region () {
|
||||
var sorted_by_live_region = document.querySelector( '.woocommerce-result-count[data-is-sorted-by="true"]' );
|
||||
|
||||
if ( sorted_by_live_region ) {
|
||||
var text = sorted_by_live_region.innerHTML;
|
||||
|
||||
var sorted_by_live_region_id = setTimeout( function() {
|
||||
sorted_by_live_region.innerHTML = '';
|
||||
sorted_by_live_region.innerHTML = text;
|
||||
clearTimeout( sorted_by_live_region_id );
|
||||
}, 1000 );
|
||||
}
|
||||
}
|
||||
|
||||
function on_document_ready() {
|
||||
focus_populate_live_region();
|
||||
refresh_sorted_by_live_region();
|
||||
}
|
||||
|
||||
document.addEventListener( 'DOMContentLoaded' , on_document_ready );
|
||||
|
|
|
@ -357,7 +357,7 @@ class WC_Privacy_Erasers {
|
|||
* Allow extensions to remove their own personal data for this order.
|
||||
*
|
||||
* @since 3.4.0
|
||||
* @param WC_Order $order A customer object.
|
||||
* @param WC_Order $order Order instance.
|
||||
*/
|
||||
do_action( 'woocommerce_privacy_remove_order_personal_data', $order );
|
||||
}
|
||||
|
|
|
@ -33,6 +33,36 @@ function wc_page_endpoint_title( $title ) {
|
|||
|
||||
add_filter( 'the_title', 'wc_page_endpoint_title' );
|
||||
|
||||
/**
|
||||
* Replace the title part of the document title.
|
||||
*
|
||||
* @param array $title {
|
||||
* The document title parts.
|
||||
*
|
||||
* @type string $title Title of the viewed page.
|
||||
* @type string $page Optional. Page number if paginated.
|
||||
* @type string $tagline Optional. Site description when on home page.
|
||||
* @type string $site Optional. Site title when not on home page.
|
||||
* }
|
||||
* @return array
|
||||
*/
|
||||
function wc_page_endpoint_document_title_parts( $title ) {
|
||||
global $wp_query;
|
||||
|
||||
if ( ! is_null( $wp_query ) && ! is_admin() && is_main_query() && is_page() && is_wc_endpoint_url() ) {
|
||||
$endpoint = WC()->query->get_current_endpoint();
|
||||
$action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$endpoint_title = WC()->query->get_endpoint_title( $endpoint, $action );
|
||||
$title['title'] = $endpoint_title ? $endpoint_title : $title['title'];
|
||||
|
||||
remove_filter( 'document_title_parts', 'wc_page_endpoint_document_title_parts' );
|
||||
}
|
||||
|
||||
return $title;
|
||||
}
|
||||
|
||||
add_filter( 'document_title_parts', 'wc_page_endpoint_document_title_parts' );
|
||||
|
||||
/**
|
||||
* Retrieve page ids - used for myaccount, edit_address, shop, cart, checkout, pay, view_order, terms. returns -1 if no page is found.
|
||||
*
|
||||
|
|
|
@ -1480,10 +1480,48 @@ if ( ! function_exists( 'woocommerce_result_count' ) ) {
|
|||
if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) {
|
||||
return;
|
||||
}
|
||||
$args = array(
|
||||
'total' => wc_get_loop_prop( 'total' ),
|
||||
'per_page' => wc_get_loop_prop( 'per_page' ),
|
||||
'current' => wc_get_loop_prop( 'current_page' ),
|
||||
|
||||
/**
|
||||
* Filters the default orderby option.
|
||||
*
|
||||
* @since 1.6.4
|
||||
*
|
||||
* @param string $default_orderby The default orderby option.
|
||||
*/
|
||||
$default_orderby = apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', '' ) );
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$orderby = isset( $_GET['orderby'] ) ? wc_clean( wp_unslash( $_GET['orderby'] ) ) : $default_orderby;
|
||||
|
||||
// If products follow the default order this doesn't need to be informed.
|
||||
$orderby = 'menu_order' === $orderby ? '' : $orderby;
|
||||
|
||||
$orderby = is_string( $orderby ) ? $orderby : '';
|
||||
|
||||
/**
|
||||
* Filters ordered by messages.
|
||||
*
|
||||
* @since 9.3.0
|
||||
*
|
||||
* @param array $orderedby_messages The list of messages per orderby key.
|
||||
*/
|
||||
$catalog_orderedby_options = apply_filters(
|
||||
'woocommerce_catalog_orderedby',
|
||||
array(
|
||||
'menu_order' => __( 'Default sorting', 'woocommerce' ),
|
||||
'popularity' => __( 'Sorted by popularity', 'woocommerce' ),
|
||||
'rating' => __( 'Sorted by average rating', 'woocommerce' ),
|
||||
'date' => __( 'Sorted by latest', 'woocommerce' ),
|
||||
'price' => __( 'Sorted by price: low to high', 'woocommerce' ),
|
||||
'price-desc' => __( 'Sorted by price: high to low', 'woocommerce' ),
|
||||
)
|
||||
);
|
||||
$orderedby = isset( $catalog_orderedby_options[ $orderby ] ) ? $catalog_orderedby_options[ $orderby ] : '';
|
||||
$orderedby = is_string( $orderedby ) ? $orderedby : '';
|
||||
$args = array(
|
||||
'total' => wc_get_loop_prop( 'total' ),
|
||||
'per_page' => wc_get_loop_prop( 'per_page' ),
|
||||
'current' => wc_get_loop_prop( 'current_page' ),
|
||||
'orderedby' => $orderedby,
|
||||
);
|
||||
|
||||
wc_get_template( 'loop/result-count.php', $args );
|
||||
|
|
|
@ -560,7 +560,7 @@
|
|||
"shardingArguments": [],
|
||||
"changes": [],
|
||||
"events": [
|
||||
"daily-checks",
|
||||
"release-checks",
|
||||
"on-demand"
|
||||
],
|
||||
"report": {
|
||||
|
@ -576,7 +576,7 @@
|
|||
"shardingArguments": [],
|
||||
"changes": [],
|
||||
"events": [
|
||||
"daily-checks",
|
||||
"release-checks",
|
||||
"on-demand"
|
||||
],
|
||||
"report": {
|
||||
|
|
|
@ -349,36 +349,36 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
|||
'link_url' => add_query_arg( $in_app_purchase_params, 'https://woocommerce.com/products/luminate/' ),
|
||||
),
|
||||
array(
|
||||
'name' => 'Nokul',
|
||||
'name' => 'Gizmo',
|
||||
/* translators: %d: price */
|
||||
'price' => sprintf( __( '$%d/year', 'woocommerce' ), 79 ),
|
||||
'is_free' => false,
|
||||
'color_palettes' => array(
|
||||
array(
|
||||
'title' => 'Foreground and background',
|
||||
'primary' => '#000000',
|
||||
'secondary' => '#f1eee2',
|
||||
'title' => 'Primary',
|
||||
'primary' => '#ff5833',
|
||||
'secondary' => '#ff5833',
|
||||
),
|
||||
array(
|
||||
'title' => 'Foreground and secondary',
|
||||
'primary' => '#000000',
|
||||
'secondary' => '#999999',
|
||||
'title' => 'Foreground',
|
||||
'primary' => '#111111',
|
||||
'secondary' => '#111111',
|
||||
),
|
||||
array(
|
||||
'title' => 'Foreground and accent',
|
||||
'primary' => '#000000',
|
||||
'secondary' => '#d82f16',
|
||||
'title' => 'Background',
|
||||
'primary' => '#FFFFFF',
|
||||
'secondary' => '#FFFFFF',
|
||||
),
|
||||
array(
|
||||
'title' => 'Primary and background',
|
||||
'primary' => '#d9d0bf',
|
||||
'secondary' => '#f1eee2',
|
||||
'title' => 'Base',
|
||||
'primary' => '#595959',
|
||||
'secondary' => '#595959',
|
||||
),
|
||||
),
|
||||
'total_palettes' => 6,
|
||||
'slug' => 'nokul',
|
||||
'thumbnail_url' => 'https://woocommerce.com/wp-content/uploads/2022/11/Product-logo.jpg',
|
||||
'link_url' => add_query_arg( $in_app_purchase_params, 'https://woocommerce.com/products/nokul/' ),
|
||||
'total_palettes' => 10,
|
||||
'slug' => 'gizmo',
|
||||
'thumbnail_url' => 'https://woocommerce.com/wp-content/uploads/2022/11/gizmo-regular-card-product-logo.jpg?w=900',
|
||||
'link_url' => add_query_arg( $in_app_purchase_params, 'https://woocommerce.com/products/gizmo/' ),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
@ -171,13 +171,15 @@ class Controller extends GenericController implements ExportableInterface {
|
|||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params['orderby']['default'] = 'category_id';
|
||||
$params['orderby']['enum'] = array(
|
||||
'category_id',
|
||||
'items_sold',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'products_count',
|
||||
'category',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'category_id',
|
||||
'items_sold',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'products_count',
|
||||
'category',
|
||||
)
|
||||
);
|
||||
$params['interval'] = array(
|
||||
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
|
||||
|
|
|
@ -184,11 +184,13 @@ class Controller extends GenericController implements ExportableInterface {
|
|||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params['orderby']['default'] = 'coupon_id';
|
||||
$params['orderby']['enum'] = array(
|
||||
'coupon_id',
|
||||
'code',
|
||||
'amount',
|
||||
'orders_count',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'coupon_id',
|
||||
'code',
|
||||
'amount',
|
||||
'orders_count',
|
||||
)
|
||||
);
|
||||
$params['coupons'] = array(
|
||||
'description' => __( 'Limit result set to coupons assigned specific coupon IDs.', 'woocommerce' ),
|
||||
|
|
|
@ -142,11 +142,13 @@ class Controller extends GenericStatsController {
|
|||
*/
|
||||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params['orderby']['enum'] = array(
|
||||
'date',
|
||||
'amount',
|
||||
'coupons_count',
|
||||
'orders_count',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'date',
|
||||
'amount',
|
||||
'coupons_count',
|
||||
'orders_count',
|
||||
)
|
||||
);
|
||||
$params['coupons'] = array(
|
||||
'description' => __( 'Limit result set to coupons assigned specific coupon IDs.', 'woocommerce' ),
|
||||
|
|
|
@ -304,18 +304,20 @@ class Controller extends GenericController implements ExportableInterface {
|
|||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['orderby']['default'] = 'date_registered';
|
||||
$params['orderby']['enum'] = array(
|
||||
'username',
|
||||
'name',
|
||||
'country',
|
||||
'city',
|
||||
'state',
|
||||
'postcode',
|
||||
'date_registered',
|
||||
'date_last_active',
|
||||
'orders_count',
|
||||
'total_spend',
|
||||
'avg_order_value',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'username',
|
||||
'name',
|
||||
'country',
|
||||
'city',
|
||||
'state',
|
||||
'postcode',
|
||||
'date_registered',
|
||||
'date_last_active',
|
||||
'orders_count',
|
||||
'total_spend',
|
||||
'avg_order_value',
|
||||
)
|
||||
);
|
||||
$params['match'] = array(
|
||||
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
|
||||
|
|
|
@ -913,14 +913,18 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
* Anonymize the customer data for a single order.
|
||||
*
|
||||
* @internal
|
||||
* @param int $order_id Order id.
|
||||
* @param int|WC_Order $order Order instance or ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function anonymize_customer( $order_id ) {
|
||||
public static function anonymize_customer( $order ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! is_object( $order ) ) {
|
||||
$order = wc_get_order( absint( $order ) );
|
||||
}
|
||||
|
||||
$customer_id = $wpdb->get_var(
|
||||
$wpdb->prepare( "SELECT customer_id FROM {$wpdb->prefix}wc_order_stats WHERE order_id = %d", $order_id )
|
||||
$wpdb->prepare( "SELECT customer_id FROM {$wpdb->prefix}wc_order_stats WHERE order_id = %d", $order->get_id() )
|
||||
);
|
||||
|
||||
if ( ! $customer_id ) {
|
||||
|
|
|
@ -219,9 +219,11 @@ class Controller extends GenericController implements ExportableInterface {
|
|||
*/
|
||||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params['orderby']['enum'] = array(
|
||||
'date',
|
||||
'product',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'date',
|
||||
'product',
|
||||
)
|
||||
);
|
||||
$params['match'] = array(
|
||||
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: products, orders, username, ip_address.', 'woocommerce' ),
|
||||
|
|
|
@ -196,9 +196,11 @@ class Controller extends GenericStatsController {
|
|||
*/
|
||||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params['orderby']['enum'] = array(
|
||||
'date',
|
||||
'download_count',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'date',
|
||||
'download_count',
|
||||
)
|
||||
);
|
||||
$params['match'] = array(
|
||||
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
|
||||
|
|
|
@ -257,4 +257,31 @@ abstract class GenericController extends \WC_REST_Reports_Controller {
|
|||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a filter for custom orderby enum.
|
||||
*
|
||||
* @param array $orderby_enum An array of orderby enum options.
|
||||
*
|
||||
* @return array An array of filtered orderby enum options.
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
protected function apply_custom_orderby_filters( $orderby_enum ) {
|
||||
/**
|
||||
* Filter orderby query parameter enum.
|
||||
*
|
||||
* There was an initial concern about potential SQL injection with the custom orderby.
|
||||
* However, testing shows it is safely blocked by validation in the controller,
|
||||
* which results in an "Invalid parameter(s): orderby" error.
|
||||
*
|
||||
* Additionally, it's the responsibility of the merchant/developer to ensure the custom orderby is valid,
|
||||
* or a WordPress database error will occur for unknown columns.
|
||||
*
|
||||
* @since 9.4.0
|
||||
*
|
||||
* @param array $orderby_enum The orderby query parameter enum.
|
||||
*/
|
||||
return apply_filters( "woocommerce_analytics_orderby_enum_{$this->rest_base}", $orderby_enum );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -233,10 +233,12 @@ class Controller extends GenericController implements ExportableInterface {
|
|||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params['per_page']['minimum'] = 0;
|
||||
$params['orderby']['enum'] = array(
|
||||
'date',
|
||||
'num_items_sold',
|
||||
'net_total',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'date',
|
||||
'num_items_sold',
|
||||
'net_total',
|
||||
)
|
||||
);
|
||||
$params['product_includes'] = array(
|
||||
'description' => __( 'Limit result set to items that have the specified product(s) assigned.', 'woocommerce' ),
|
||||
|
|
|
@ -202,11 +202,13 @@ class Controller extends GenericStatsController {
|
|||
*/
|
||||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params['orderby']['enum'] = array(
|
||||
'date',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'avg_order_value',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'date',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'avg_order_value',
|
||||
)
|
||||
);
|
||||
$params['match'] = array(
|
||||
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
|
||||
|
|
|
@ -231,14 +231,16 @@ class Controller extends GenericController implements ExportableInterface {
|
|||
*/
|
||||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params['orderby']['enum'] = array(
|
||||
'date',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'items_sold',
|
||||
'product_name',
|
||||
'variations',
|
||||
'sku',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'date',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'items_sold',
|
||||
'product_name',
|
||||
'variations',
|
||||
'sku',
|
||||
)
|
||||
);
|
||||
$params['categories'] = array(
|
||||
'description' => __( 'Limit result to items from the specified categories.', 'woocommerce' ),
|
||||
|
|
|
@ -197,16 +197,18 @@ class Controller extends GenericStatsController {
|
|||
*/
|
||||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params['orderby']['enum'] = array(
|
||||
'date',
|
||||
'net_revenue',
|
||||
'coupons',
|
||||
'refunds',
|
||||
'shipping',
|
||||
'taxes',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'items_sold',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'date',
|
||||
'net_revenue',
|
||||
'coupons',
|
||||
'refunds',
|
||||
'shipping',
|
||||
'taxes',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'items_sold',
|
||||
)
|
||||
);
|
||||
$params['categories'] = array(
|
||||
'description' => __( 'Limit result to items from the specified categories.', 'woocommerce' ),
|
||||
|
|
|
@ -223,17 +223,19 @@ class Controller extends GenericStatsController implements ExportableInterface {
|
|||
*/
|
||||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params['orderby']['enum'] = array(
|
||||
'date',
|
||||
'total_sales',
|
||||
'coupons',
|
||||
'refunds',
|
||||
'shipping',
|
||||
'taxes',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'items_sold',
|
||||
'gross_sales',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'date',
|
||||
'total_sales',
|
||||
'coupons',
|
||||
'refunds',
|
||||
'shipping',
|
||||
'taxes',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'items_sold',
|
||||
'gross_sales',
|
||||
)
|
||||
);
|
||||
$params['segmentby'] = array(
|
||||
'description' => __( 'Segment the response by additional constraint.', 'woocommerce' ),
|
||||
|
|
|
@ -443,14 +443,16 @@ class Controller extends GenericController implements ExportableInterface {
|
|||
);
|
||||
$params['order']['default'] = 'asc';
|
||||
$params['orderby']['default'] = 'stock_status';
|
||||
$params['orderby']['enum'] = array(
|
||||
'stock_status',
|
||||
'stock_quantity',
|
||||
'date',
|
||||
'id',
|
||||
'include',
|
||||
'title',
|
||||
'sku',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'stock_status',
|
||||
'stock_quantity',
|
||||
'date',
|
||||
'id',
|
||||
'include',
|
||||
'title',
|
||||
'sku',
|
||||
)
|
||||
);
|
||||
$params['parent'] = array(
|
||||
'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ),
|
||||
|
|
|
@ -195,15 +195,17 @@ class Controller extends GenericController implements ExportableInterface {
|
|||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params['orderby']['default'] = 'tax_rate_id';
|
||||
$params['orderby']['enum'] = array(
|
||||
'name',
|
||||
'tax_rate_id',
|
||||
'tax_code',
|
||||
'rate',
|
||||
'order_tax',
|
||||
'total_tax',
|
||||
'shipping_tax',
|
||||
'orders_count',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'name',
|
||||
'tax_rate_id',
|
||||
'tax_code',
|
||||
'rate',
|
||||
'order_tax',
|
||||
'total_tax',
|
||||
'shipping_tax',
|
||||
'orders_count',
|
||||
)
|
||||
);
|
||||
$params['taxes'] = array(
|
||||
'description' => __( 'Limit result set to items assigned one or more tax rates.', 'woocommerce' ),
|
||||
|
|
|
@ -186,12 +186,14 @@ class Controller extends GenericStatsController {
|
|||
*/
|
||||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params['orderby']['enum'] = array(
|
||||
'date',
|
||||
'items_sold',
|
||||
'total_sales',
|
||||
'orders_count',
|
||||
'products_count',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'date',
|
||||
'items_sold',
|
||||
'total_sales',
|
||||
'orders_count',
|
||||
'products_count',
|
||||
)
|
||||
);
|
||||
$params['taxes'] = array(
|
||||
'description' => __( 'Limit result set to all items that have the specified term assigned in the taxes taxonomy.', 'woocommerce' ),
|
||||
|
|
|
@ -246,12 +246,14 @@ class Controller extends GenericController implements ExportableInterface {
|
|||
*/
|
||||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
$params['orderby']['enum'] = array(
|
||||
'date',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'items_sold',
|
||||
'sku',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'date',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'items_sold',
|
||||
'sku',
|
||||
)
|
||||
);
|
||||
$params['match'] = array(
|
||||
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
|
||||
|
|
|
@ -210,16 +210,18 @@ class Controller extends GenericStatsController {
|
|||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['orderby']['enum'] = array(
|
||||
'date',
|
||||
'net_revenue',
|
||||
'coupons',
|
||||
'refunds',
|
||||
'shipping',
|
||||
'taxes',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'items_sold',
|
||||
$params['orderby']['enum'] = $this->apply_custom_orderby_filters(
|
||||
array(
|
||||
'date',
|
||||
'net_revenue',
|
||||
'coupons',
|
||||
'refunds',
|
||||
'shipping',
|
||||
'taxes',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'items_sold',
|
||||
)
|
||||
);
|
||||
$params['category_includes'] = array(
|
||||
'description' => __( 'Limit result to items from the specified categories.', 'woocommerce' ),
|
||||
|
|
|
@ -141,9 +141,14 @@ class BlockPatterns {
|
|||
|
||||
$patterns = $this->ptk_patterns_store->get_patterns();
|
||||
if ( empty( $patterns ) ) {
|
||||
wc_get_logger()->warning(
|
||||
__( 'Empty patterns received from the PTK Pattern Store', 'woocommerce' ),
|
||||
);
|
||||
// By only logging when patterns are empty and no fetch is scheduled,
|
||||
// we ensure that warnings are only generated in genuinely problematic situations,
|
||||
// such as when the pattern fetching mechanism has failed entirely.
|
||||
if ( ! as_has_scheduled_action( 'fetch_patterns' ) ) {
|
||||
wc_get_logger()->warning(
|
||||
__( 'Empty patterns received from the PTK Pattern Store', 'woocommerce' ),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -176,6 +176,10 @@ class ProductButton extends AbstractBlock {
|
|||
data-wc-class--loading="context.isLoading"
|
||||
';
|
||||
|
||||
$anchor_directive = '
|
||||
data-wc-on--click="woocommerce/product-collection::actions.viewProduct"
|
||||
';
|
||||
|
||||
$span_button_directives = '
|
||||
data-wc-text="state.addToCartText"
|
||||
data-wc-class--wc-block-slide-in="state.slideInAnimation"
|
||||
|
@ -219,7 +223,7 @@ class ProductButton extends AbstractBlock {
|
|||
'{attributes}' => isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '',
|
||||
'{add_to_cart_text}' => esc_html( $initial_product_text ),
|
||||
'{div_directives}' => $is_ajax_button ? $div_directives : '',
|
||||
'{button_directives}' => $is_ajax_button ? $button_directives : '',
|
||||
'{button_directives}' => $is_ajax_button ? $button_directives : $anchor_directive,
|
||||
'{span_button_directives}' => $is_ajax_button ? $span_button_directives : '',
|
||||
'{view_cart_html}' => $is_ajax_button ? $this->get_view_cart_html() : '',
|
||||
)
|
||||
|
|
|
@ -117,6 +117,7 @@ class ProductCollection extends AbstractBlock {
|
|||
// Interactivity API: Add navigation directives to the product collection block.
|
||||
add_filter( 'render_block_woocommerce/product-collection', array( $this, 'handle_rendering' ), 10, 2 );
|
||||
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
|
||||
add_filter( 'render_block_core/post-title', array( $this, 'add_product_title_click_event_directives' ), 10, 3 );
|
||||
|
||||
add_filter( 'posts_clauses', array( $this, 'add_price_range_filter_posts_clauses' ), 10, 2 );
|
||||
|
||||
|
@ -408,6 +409,36 @@ class ProductCollection extends AbstractBlock {
|
|||
return $block_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add interactivity to the Product Title block within Product Collection.
|
||||
* This enables the triggering of a custom event when the product title is clicked.
|
||||
*
|
||||
* @param string $block_content The block content.
|
||||
* @param array $block The full block, including name and attributes.
|
||||
* @param \WP_Block $instance The block instance.
|
||||
* @return string Modified block content with added interactivity.
|
||||
*/
|
||||
public function add_product_title_click_event_directives( $block_content, $block, $instance ) {
|
||||
$namespace = $instance->attributes['__woocommerceNamespace'] ?? '';
|
||||
$is_product_title_block = 'woocommerce/product-collection/product-title' === $namespace;
|
||||
$is_link = $instance->attributes['isLink'] ?? false;
|
||||
|
||||
// Only proceed if the block is a Product Title (Post Title variation) block.
|
||||
if ( $is_product_title_block && $is_link ) {
|
||||
$p = new \WP_HTML_Tag_Processor( $block_content );
|
||||
$p->next_tag( array( 'class_name' => 'wp-block-post-title' ) );
|
||||
$is_anchor = $p->next_tag( array( 'tag_name' => 'a' ) );
|
||||
|
||||
if ( $is_anchor ) {
|
||||
$p->set_attribute( 'data-wc-on--click', 'woocommerce/product-collection::actions.viewProduct' );
|
||||
|
||||
$block_content = $p->get_updated_html();
|
||||
}
|
||||
}
|
||||
|
||||
return $block_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process pagination links within the block content.
|
||||
*
|
||||
|
|
|
@ -125,12 +125,15 @@ class ProductImage extends AbstractBlock {
|
|||
private function render_anchor( $product, $on_sale_badge, $product_image, $attributes ) {
|
||||
$product_permalink = $product->get_permalink();
|
||||
|
||||
$pointer_events = false === $attributes['showProductLink'] ? 'pointer-events: none;' : '';
|
||||
$is_link = true === $attributes['showProductLink'];
|
||||
$pointer_events = $is_link ? '' : 'pointer-events: none;';
|
||||
$directive = $is_link ? 'data-wc-on--click="woocommerce/product-collection::actions.viewProduct"' : '';
|
||||
|
||||
return sprintf(
|
||||
'<a href="%1$s" style="%2$s">%3$s %4$s</a>',
|
||||
'<a href="%1$s" style="%2$s" %3$s>%4$s %5$s</a>',
|
||||
$product_permalink,
|
||||
$pointer_events,
|
||||
$directive,
|
||||
$on_sale_badge,
|
||||
$product_image
|
||||
);
|
||||
|
|
|
@ -85,6 +85,7 @@ class ProductTemplate extends AbstractBlock {
|
|||
|
||||
// Get an instance of the current Post Template block.
|
||||
$block_instance = $block->parsed_block;
|
||||
$product_id = get_the_ID();
|
||||
|
||||
// Set the block name to one that does not correspond to an existing registered block.
|
||||
// This ensures that for the inner instances of the Post Template block, we do not render any block supports.
|
||||
|
@ -97,14 +98,39 @@ class ProductTemplate extends AbstractBlock {
|
|||
$block_instance,
|
||||
array(
|
||||
'postType' => get_post_type(),
|
||||
'postId' => get_the_ID(),
|
||||
'postId' => $product_id,
|
||||
)
|
||||
)
|
||||
)->render( array( 'dynamic' => false ) );
|
||||
|
||||
$interactive = array(
|
||||
'namespace' => 'woocommerce/product-collection',
|
||||
);
|
||||
|
||||
$context = array(
|
||||
'productId' => $product_id,
|
||||
);
|
||||
|
||||
$li_directives = '
|
||||
data-wc-interactive=\'' . wp_json_encode( $interactive, JSON_NUMERIC_CHECK | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) . '\'
|
||||
data-wc-context=\'' . wp_json_encode( $context, JSON_NUMERIC_CHECK | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) . '\'
|
||||
data-wc-key="product-item-' . $product_id . '"
|
||||
';
|
||||
|
||||
// Wrap the render inner blocks in a `li` element with the appropriate post classes.
|
||||
$post_classes = implode( ' ', get_post_class( 'wc-block-product' ) );
|
||||
$content .= '<li data-wc-key="product-item-' . get_the_ID() . '" class="' . esc_attr( $post_classes ) . '">' . $block_content . '</li>';
|
||||
$content .= strtr(
|
||||
'<li class="{classes}"
|
||||
{li_directives}
|
||||
>
|
||||
{content}
|
||||
</li>',
|
||||
array(
|
||||
'{classes}' => esc_attr( $post_classes ),
|
||||
'{li_directives}' => $li_directives,
|
||||
'{content}' => $block_content,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -54,14 +54,18 @@ class WCAdminSharedSettings {
|
|||
* @return void
|
||||
*/
|
||||
public function on_woocommerce_blocks_loaded() {
|
||||
// Ensure we only add admin settings on the admin.
|
||||
if ( ! is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( class_exists( '\Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry' ) ) {
|
||||
\Automattic\WooCommerce\Blocks\Package::container()->get( \Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry::class )->add(
|
||||
$this->settings_prefix,
|
||||
function() {
|
||||
function () {
|
||||
return apply_filters( 'woocommerce_admin_shared_settings', array() );
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ declare( strict_types = 1 );
|
|||
|
||||
namespace Automattic\WooCommerce\Internal\ComingSoon;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
|
||||
/**
|
||||
* Adds hooks to add a badge to the WordPress admin bar showing site visibility.
|
||||
*/
|
||||
|
@ -27,6 +29,11 @@ class ComingSoonAdminBarBadge {
|
|||
* @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
|
||||
*/
|
||||
public function site_visibility_badge( $wp_admin_bar ) {
|
||||
// Early exit if LYS feature is disabled.
|
||||
if ( ! Features::is_enabled( 'launch-your-store' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$labels = array(
|
||||
'coming-soon' => __( 'Coming soon', 'woocommerce' ),
|
||||
'store-coming-soon' => __( 'Store coming soon', 'woocommerce' ),
|
||||
|
@ -60,6 +67,11 @@ class ComingSoonAdminBarBadge {
|
|||
* @internal
|
||||
*/
|
||||
public function output_css() {
|
||||
// Early exit if LYS feature is disabled.
|
||||
if ( ! Features::is_enabled( 'launch-your-store' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_admin_bar_showing() ) {
|
||||
echo '<style>
|
||||
#wpadminbar .quicklinks #wp-admin-bar-woocommerce-site-visibility-badge {
|
||||
|
|
|
@ -22,7 +22,7 @@ Well-defined schema also provides a layer of security, as it enables us to valid
|
|||
When defining schema, take note of the [WordPress REST API handbook](https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/) which documents available properties and types, as well as the [JSON schema standard](http://json-schema.org/). In addition to this:
|
||||
|
||||
- Properties should use snake_case 🐍
|
||||
- Ambiguous terms should be avoided, and property names should try to use understandable language, rather than “WooCommerce” terminology or setting names
|
||||
- Ambiguous terms should be avoided, and property names should try to use understandable language, rather than "WooCommerce" terminology or setting names
|
||||
- Properties should be defined using US English, but the descriptions of fields should be localized
|
||||
- Multiple types are permitted, for example, using a `null` type if a value is not applicable
|
||||
- `sanitize_callback` and `validate_callback` are encouraged where possible to ensure data is received in the correct format before processing requests
|
||||
|
|
|
@ -14,26 +14,28 @@
|
|||
*
|
||||
* @see https://woocommerce.com/document/template-structure/
|
||||
* @package WooCommerce\Templates
|
||||
* @version 3.7.0
|
||||
* @version 9.4.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<p class="woocommerce-result-count">
|
||||
<p class="woocommerce-result-count" <?php echo ( empty( $orderedby ) || 1 === intval( $total ) ) ? '' : 'role="alert" aria-relevant="all" data-is-sorted-by="true"'; ?>>
|
||||
<?php
|
||||
// phpcs:disable WordPress.Security
|
||||
if ( 1 === intval( $total ) ) {
|
||||
_e( 'Showing the single result', 'woocommerce' );
|
||||
} elseif ( $total <= $per_page || -1 === $per_page ) {
|
||||
/* translators: %d: total results */
|
||||
printf( _n( 'Showing all %d result', 'Showing all %d results', $total, 'woocommerce' ), $total );
|
||||
$orderedby_placeholder = empty( $orderedby ) ? '%2$s' : '<span class="screen-reader-text">%2$s</span>';
|
||||
/* translators: 1: total results 2: sorted by */
|
||||
printf( _n( 'Showing all %1$d result', 'Showing all %1$d results', $total, 'woocommerce' ) . $orderedby_placeholder, $total, esc_html( $orderedby ) );
|
||||
} else {
|
||||
$first = ( $per_page * $current ) - $per_page + 1;
|
||||
$last = min( $total, $per_page * $current );
|
||||
/* translators: 1: first result 2: last result 3: total results */
|
||||
printf( _nx( 'Showing %1$d–%2$d of %3$d result', 'Showing %1$d–%2$d of %3$d results', $total, 'with first and last result', 'woocommerce' ), $first, $last, $total );
|
||||
$first = ( $per_page * $current ) - $per_page + 1;
|
||||
$last = min( $total, $per_page * $current );
|
||||
$orderedby_placeholder = empty( $orderedby ) ? '%4$s' : '<span class="screen-reader-text">%4$s</span>';
|
||||
/* translators: 1: first result 2: last result 3: total results 4: sorted by */
|
||||
printf( _nx( 'Showing %1$d–%2$d of %3$d result', 'Showing %1$d–%2$d of %3$d results', $total, 'with first and last result', 'woocommerce' ) . $orderedby_placeholder, $first, $last, $total, esc_html( $orderedby ) );
|
||||
}
|
||||
// phpcs:enable WordPress.Security
|
||||
?>
|
||||
|
|
|
@ -16,11 +16,48 @@ config = {
|
|||
'**/admin-marketing/**/*.spec.js',
|
||||
'**/admin-tasks/**/*.spec.js',
|
||||
'**/customize-store/**/*.spec.js',
|
||||
'**/merchant/create-page.spec.js',
|
||||
'**/merchant/create-post.spec.js',
|
||||
'**/merchant/create-restricted-coupons.spec.js',
|
||||
'**/merchant/create-shipping-classes.spec.js',
|
||||
'**/merchant/create-shipping-zones.spec.js',
|
||||
'**/merchant/command-palette.spec.js',
|
||||
'**/merchant/create-cart-block.spec.js',
|
||||
'**/merchant/create-checkout-block.spec.js',
|
||||
'**/merchant/create-coupon.spec.js',
|
||||
'**/merchant/create-order.spec.js',
|
||||
'**/merchant/create-woocommerce-blocks.spec.js',
|
||||
'**/merchant/create-woocommerce-patterns.spec.js',
|
||||
'**/merchant/customer-list.spec.js',
|
||||
'**/merchant/customer-payment-page.spec.js',
|
||||
'**/merchant/launch-your-store.spec.js',
|
||||
'**/merchant/lost-password.spec.js',
|
||||
'**/merchant/order-bulk-edit.spec.js',
|
||||
'**/merchant/order-coupon.spec.js',
|
||||
'**/merchant/order-edit.spec.js',
|
||||
'**/merchant/order-emails.spec.js',
|
||||
'**/merchant/order-refund.spec.js',
|
||||
'**/merchant/order-search.spec.js',
|
||||
'**/merchant/order-status-filter.spec.js',
|
||||
'**/merchant/page-loads.spec.js',
|
||||
'**/merchant/product-create-simple.spec.js',
|
||||
'**/shopper/checkout-create-account.spec.js',
|
||||
'**/shopper/checkout-login.spec.js',
|
||||
'**/shopper/checkout.spec.js',
|
||||
'**/shopper/dashboard-access.spec.js',
|
||||
'**/shopper/mini-cart.spec.js',
|
||||
'**/shopper/my-account-addresses.spec.js',
|
||||
'**/shopper/my-account-create-account.spec.js',
|
||||
'**/shopper/my-account-downloads.spec.js',
|
||||
'**/shopper/my-account-pay-order.spec.js',
|
||||
'**/shopper/my-account.spec.js',
|
||||
'**/shopper/order-email-receiving.spec.js',
|
||||
'**/shopper/product-grouped.spec.js',
|
||||
'**/shopper/product-simple.spec.js',
|
||||
'**/shopper/product-tags-attributes.spec.js',
|
||||
'**/shopper/product-variable.spec.js',
|
||||
'**/shopper/shop-search-browse-sort.spec.js',
|
||||
'**/shopper/shop-title-after-deletion.spec.js',
|
||||
],
|
||||
grepInvert: /@skip-on-default-pressable/,
|
||||
},
|
||||
|
|
|
@ -7,7 +7,27 @@ config = {
|
|||
{
|
||||
name: 'default wpcom',
|
||||
use: { ...devices[ 'Desktop Chrome' ] },
|
||||
testMatch: '**basic.spec.js',
|
||||
testMatch: [
|
||||
'**/basic.spec.js',
|
||||
'**/shopper/checkout-create-account.spec.js',
|
||||
'**/shopper/checkout-login.spec.js',
|
||||
'**/shopper/checkout.spec.js',
|
||||
'**/shopper/dashboard-access.spec.js',
|
||||
'**/shopper/mini-cart.spec.js',
|
||||
'**/shopper/my-account-addresses.spec.js',
|
||||
'**/shopper/my-account-create-account.spec.js',
|
||||
'**/shopper/my-account-downloads.spec.js',
|
||||
'**/shopper/my-account-pay-order.spec.js',
|
||||
'**/shopper/my-account.spec.js',
|
||||
'**/shopper/order-email-receiving.spec.js',
|
||||
'**/shopper/product-grouped.spec.js',
|
||||
'**/shopper/product-simple.spec.js',
|
||||
'**/shopper/product-tags-attributes.spec.js',
|
||||
'**/shopper/product-variable.spec.js',
|
||||
'**/shopper/shop-search-browse-sort.spec.js',
|
||||
'**/shopper/shop-title-after-deletion.spec.js',
|
||||
],
|
||||
grepInvert: /@skip-on-default-wpcom/,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -74,15 +74,24 @@ const test = baseTest.extend( {
|
|||
coupon: async ( { api }, use ) => {
|
||||
const coupon = {};
|
||||
await use( coupon );
|
||||
await api.delete( `coupons/${ coupon.id }`, { force: true } );
|
||||
await api
|
||||
.delete( `coupons/${ coupon.id }`, { force: true } )
|
||||
.then( ( response ) => {
|
||||
console.log( 'Delete successful:', response.data );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
console.log( 'Error response data:', error.response.data );
|
||||
throw new Error( error.response.data );
|
||||
} );
|
||||
},
|
||||
|
||||
product: async ( { api }, use ) => {
|
||||
let product = {};
|
||||
const productName = `Product ${ Date.now() }`;
|
||||
|
||||
await api
|
||||
.post( 'products', {
|
||||
name: 'Product',
|
||||
name: productName,
|
||||
regular_price: '100',
|
||||
} )
|
||||
.then( ( response ) => {
|
||||
|
|
|
@ -52,7 +52,7 @@ const test = baseTest.extend( {
|
|||
|
||||
test.describe(
|
||||
'Add WooCommerce Blocks Into Page',
|
||||
{ tag: [ '@gutenberg', '@services' ] },
|
||||
{ tag: [ '@gutenberg', '@services', '@skip-on-default-pressable' ] },
|
||||
() => {
|
||||
test.beforeAll( async ( { api } ) => {
|
||||
// add product attribute
|
||||
|
|
|
@ -28,7 +28,7 @@ const test = baseTest.extend( {
|
|||
|
||||
test.describe(
|
||||
'Add WooCommerce Patterns Into Page',
|
||||
{ tag: [ '@gutenberg', '@services' ] },
|
||||
{ tag: [ '@gutenberg', '@services', '@skip-on-default-pressable' ] },
|
||||
() => {
|
||||
test( 'can insert WooCommerce patterns into page', async ( {
|
||||
page,
|
||||
|
|
|
@ -83,144 +83,157 @@ test.describe( 'Merchant > Customer List', { tag: '@services' }, () => {
|
|||
await context.route( '**/users/**', ( route ) => route.abort() );
|
||||
} );
|
||||
|
||||
test( 'Merchant can view a list of all customers, filter and download', async ( {
|
||||
page,
|
||||
customers,
|
||||
} ) => {
|
||||
await test.step( 'Go to the customers reports page', async () => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
'**/wp-json/wc-analytics/reports/customers?orderby**'
|
||||
);
|
||||
await page.goto(
|
||||
'/wp-admin/admin.php?page=wc-admin&path=%2Fcustomers'
|
||||
);
|
||||
await responsePromise;
|
||||
} );
|
||||
test(
|
||||
'Merchant can view a list of all customers, filter and download',
|
||||
{ tag: '@skip-on-default-pressable' },
|
||||
async ( { page, customers } ) => {
|
||||
await test.step( 'Go to the customers reports page', async () => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
'**/wp-json/wc-analytics/reports/customers?orderby**'
|
||||
);
|
||||
await page.goto(
|
||||
'/wp-admin/admin.php?page=wc-admin&path=%2Fcustomers'
|
||||
);
|
||||
await responsePromise;
|
||||
} );
|
||||
|
||||
// may have more than 3 customers due to guest orders
|
||||
// await test.step( 'Check that 3 customers are displayed', async () => {
|
||||
// await expect(
|
||||
// page.getByText( '3customers0Average orders$0.' )
|
||||
// ).toBeVisible();
|
||||
// } );
|
||||
// may have more than 3 customers due to guest orders
|
||||
// await test.step( 'Check that 3 customers are displayed', async () => {
|
||||
// await expect(
|
||||
// page.getByText( '3customers0Average orders$0.' )
|
||||
// ).toBeVisible();
|
||||
// } );
|
||||
|
||||
await test.step( 'Check that the customers are displayed in the list', async () => {
|
||||
for ( const customer of customers ) {
|
||||
await expect(
|
||||
page.getByRole( 'link', { name: customer.email } )
|
||||
).toBeVisible();
|
||||
}
|
||||
} );
|
||||
await test.step( 'Check that the customers are displayed in the list', async () => {
|
||||
for ( const customer of customers ) {
|
||||
await expect(
|
||||
page.getByRole( 'link', { name: customer.email } )
|
||||
).toBeVisible();
|
||||
}
|
||||
} );
|
||||
|
||||
await test.step( 'Check that the customer list can be filtered by first name', async () => {
|
||||
let x = 1;
|
||||
for ( const customer of customers ) {
|
||||
await test.step( 'Check that the customer list can be filtered by first name', async () => {
|
||||
let x = 1;
|
||||
for ( const customer of customers ) {
|
||||
await page
|
||||
.getByRole( 'combobox', {
|
||||
expanded: false,
|
||||
disabled: false,
|
||||
} )
|
||||
.click();
|
||||
await page
|
||||
.getByRole( 'combobox', {
|
||||
expanded: false,
|
||||
disabled: false,
|
||||
} )
|
||||
.pressSequentially(
|
||||
`${ customer.first_name } ${ customer.last_name }`
|
||||
);
|
||||
await page
|
||||
.getByRole( 'option', {
|
||||
name: `All customers with names that include ${ customer.first_name } ${ customer.last_name }`,
|
||||
exact: true,
|
||||
} )
|
||||
.waitFor();
|
||||
await page
|
||||
.getByRole( 'option', {
|
||||
name: `${ customer.first_name } ${ customer.last_name }`,
|
||||
exact: true,
|
||||
} )
|
||||
.waitFor();
|
||||
await page
|
||||
.getByRole( 'option', {
|
||||
name: `All customers with names that include ${ customer.first_name } ${ customer.last_name }`,
|
||||
exact: true,
|
||||
} )
|
||||
.click( { delay: 300 } );
|
||||
await expect(
|
||||
page.getByRole( 'link', { name: customer.email } )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( `${ x }customer` )
|
||||
).toBeVisible();
|
||||
x++;
|
||||
}
|
||||
await page.getByRole( 'button', { name: 'Clear all' } ).click();
|
||||
} );
|
||||
|
||||
await test.step( 'Hide and display columns', async () => {
|
||||
await page
|
||||
.getByRole( 'combobox', {
|
||||
expanded: false,
|
||||
disabled: false,
|
||||
.getByRole( 'button', {
|
||||
name: 'Choose which values to display',
|
||||
} )
|
||||
.click();
|
||||
// hide a few columns
|
||||
await page.getByRole( 'menu' ).getByText( 'Username' ).click();
|
||||
await page
|
||||
.getByRole( 'combobox', {
|
||||
expanded: false,
|
||||
disabled: false,
|
||||
} )
|
||||
.pressSequentially(
|
||||
`${ customer.first_name } ${ customer.last_name }`
|
||||
);
|
||||
.getByRole( 'menu' )
|
||||
.getByText( 'Last active' )
|
||||
.click();
|
||||
await page
|
||||
.getByRole( 'option', {
|
||||
name: `All customers with names that include ${ customer.first_name } ${ customer.last_name }`,
|
||||
exact: true,
|
||||
} )
|
||||
.waitFor();
|
||||
await page
|
||||
.getByRole( 'option', {
|
||||
name: `${ customer.first_name } ${ customer.last_name }`,
|
||||
exact: true,
|
||||
} )
|
||||
.waitFor();
|
||||
await page
|
||||
.getByRole( 'option', {
|
||||
name: `All customers with names that include ${ customer.first_name } ${ customer.last_name }`,
|
||||
exact: true,
|
||||
} )
|
||||
.click( { delay: 300 } );
|
||||
.getByRole( 'menu' )
|
||||
.getByText( 'Total spend' )
|
||||
.click();
|
||||
|
||||
// click to close the menu
|
||||
await page.getByText( 'Show:' ).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole( 'link', { name: customer.email } )
|
||||
page.getByRole( 'columnheader', { name: 'Username' } )
|
||||
).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole( 'columnheader', { name: 'Last active' } )
|
||||
).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole( 'columnheader', { name: 'Total spend' } )
|
||||
).toBeHidden();
|
||||
|
||||
// show the columns again
|
||||
await page
|
||||
.getByRole( 'button', {
|
||||
name: 'Choose which values to display',
|
||||
} )
|
||||
.click();
|
||||
await page.getByRole( 'menu' ).getByText( 'Username' ).click();
|
||||
await page
|
||||
.getByRole( 'menu' )
|
||||
.getByText( 'Last active' )
|
||||
.click();
|
||||
await page
|
||||
.getByRole( 'menu' )
|
||||
.getByText( 'Total spend' )
|
||||
.click();
|
||||
|
||||
// click to close the menu
|
||||
await page.getByText( 'Show:' ).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole( 'columnheader', { name: 'Username' } )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( `${ x }customer` )
|
||||
page.getByRole( 'columnheader', { name: 'Last active' } )
|
||||
).toBeVisible();
|
||||
x++;
|
||||
}
|
||||
await page.getByRole( 'button', { name: 'Clear all' } ).click();
|
||||
} );
|
||||
await expect(
|
||||
page.getByRole( 'columnheader', { name: 'Total spend' } )
|
||||
).toBeVisible();
|
||||
} );
|
||||
|
||||
await test.step( 'Hide and display columns', async () => {
|
||||
await page
|
||||
.getByRole( 'button', {
|
||||
name: 'Choose which values to display',
|
||||
} )
|
||||
.click();
|
||||
// hide a few columns
|
||||
await page.getByRole( 'menu' ).getByText( 'Username' ).click();
|
||||
await page.getByRole( 'menu' ).getByText( 'Last active' ).click();
|
||||
await page.getByRole( 'menu' ).getByText( 'Total spend' ).click();
|
||||
await test.step( 'Download the customer list', async () => {
|
||||
const downloadPromise = page.waitForEvent( 'download' );
|
||||
await page.getByRole( 'button', { name: 'Download' } ).click();
|
||||
const download = await downloadPromise;
|
||||
|
||||
// click to close the menu
|
||||
await page.getByText( 'Show:' ).click();
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String( today.getMonth() + 1 ).padStart( 2, '0' );
|
||||
const day = String( today.getDate() ).padStart( 2, '0' );
|
||||
|
||||
await expect(
|
||||
page.getByRole( 'columnheader', { name: 'Username' } )
|
||||
).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole( 'columnheader', { name: 'Last active' } )
|
||||
).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole( 'columnheader', { name: 'Total spend' } )
|
||||
).toBeHidden();
|
||||
const filename = `customers_${ year }-${ month }-${ day }_orderby-date-last-active_order-desc_page-wc-admin_path--customers.csv`;
|
||||
|
||||
// show the columns again
|
||||
await page
|
||||
.getByRole( 'button', {
|
||||
name: 'Choose which values to display',
|
||||
} )
|
||||
.click();
|
||||
await page.getByRole( 'menu' ).getByText( 'Username' ).click();
|
||||
await page.getByRole( 'menu' ).getByText( 'Last active' ).click();
|
||||
await page.getByRole( 'menu' ).getByText( 'Total spend' ).click();
|
||||
|
||||
// click to close the menu
|
||||
await page.getByText( 'Show:' ).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole( 'columnheader', { name: 'Username' } )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole( 'columnheader', { name: 'Last active' } )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole( 'columnheader', { name: 'Total spend' } )
|
||||
).toBeVisible();
|
||||
} );
|
||||
|
||||
await test.step( 'Download the customer list', async () => {
|
||||
const downloadPromise = page.waitForEvent( 'download' );
|
||||
await page.getByRole( 'button', { name: 'Download' } ).click();
|
||||
const download = await downloadPromise;
|
||||
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String( today.getMonth() + 1 ).padStart( 2, '0' );
|
||||
const day = String( today.getDate() ).padStart( 2, '0' );
|
||||
|
||||
const filename = `customers_${ year }-${ month }-${ day }_orderby-date-last-active_order-desc_page-wc-admin_path--customers.csv`;
|
||||
|
||||
await expect( download.suggestedFilename() ).toBe( filename );
|
||||
} );
|
||||
} );
|
||||
await expect( download.suggestedFilename() ).toBe( filename );
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
test( 'Merchant can view a single customer', async ( {
|
||||
page,
|
||||
|
|
|
@ -71,53 +71,63 @@ test.describe(
|
|||
} );
|
||||
} );
|
||||
|
||||
test( 'can receive new order email', async ( { page, baseURL } ) => {
|
||||
// New order emails are sent automatically when we create a simple order. Verify that we get these.
|
||||
// Need to create a new order for this test because we clear logs before each run.
|
||||
const api = new wcApi( {
|
||||
url: baseURL,
|
||||
consumerKey: process.env.CONSUMER_KEY,
|
||||
consumerSecret: process.env.CONSUMER_SECRET,
|
||||
version: 'wc/v3',
|
||||
} );
|
||||
await api
|
||||
.post( 'orders', {
|
||||
status: 'processing',
|
||||
billing: customerBilling,
|
||||
} )
|
||||
.then( ( response ) => {
|
||||
newOrderId = response.data.id;
|
||||
test(
|
||||
'can receive new order email',
|
||||
{ tag: '@skip-on-default-pressable' },
|
||||
async ( { page, baseURL } ) => {
|
||||
// New order emails are sent automatically when we create a simple order. Verify that we get these.
|
||||
// Need to create a new order for this test because we clear logs before each run.
|
||||
const api = new wcApi( {
|
||||
url: baseURL,
|
||||
consumerKey: process.env.CONSUMER_KEY,
|
||||
consumerSecret: process.env.CONSUMER_SECRET,
|
||||
version: 'wc/v3',
|
||||
} );
|
||||
// search to narrow it down to just the messages we want
|
||||
await page.goto(
|
||||
`/wp-admin/tools.php?page=wpml_plugin_log&s=${ encodeURIComponent(
|
||||
customerBilling.email
|
||||
) }`
|
||||
);
|
||||
await expect(
|
||||
page.locator( 'td.column-receiver >> nth=1' )
|
||||
).toContainText( admin.email );
|
||||
await expect(
|
||||
page.locator( 'td.column-subject >> nth=1' )
|
||||
).toContainText( `[${ storeName }]: New order #${ newOrderId }` );
|
||||
await api
|
||||
.post( 'orders', {
|
||||
status: 'processing',
|
||||
billing: customerBilling,
|
||||
} )
|
||||
.then( ( response ) => {
|
||||
newOrderId = response.data.id;
|
||||
} );
|
||||
// search to narrow it down to just the messages we want
|
||||
await page.goto(
|
||||
`/wp-admin/tools.php?page=wpml_plugin_log&s=${ encodeURIComponent(
|
||||
customerBilling.email
|
||||
) }`
|
||||
);
|
||||
await expect(
|
||||
page.locator( 'td.column-receiver >> nth=1' )
|
||||
).toContainText( admin.email );
|
||||
await expect(
|
||||
page.locator( 'td.column-subject >> nth=1' )
|
||||
).toContainText(
|
||||
`[${ storeName }]: New order #${ newOrderId }`
|
||||
);
|
||||
|
||||
// look at order email contents
|
||||
await page
|
||||
.getByRole( 'button', { name: 'View log' } )
|
||||
.last()
|
||||
.click();
|
||||
// look at order email contents
|
||||
await page
|
||||
.getByRole( 'button', { name: 'View log' } )
|
||||
.last()
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByText( 'Receiver wordpress@example.com' )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( 'Subject [WooCommerce Core E2E' )
|
||||
).toBeVisible();
|
||||
await page.getByRole( 'link', { name: 'json' } ).click();
|
||||
await expect(
|
||||
page.locator( '#wp-mail-logging-modal-content-body-content' )
|
||||
).toContainText( 'You’ve received the following order from :' );
|
||||
} );
|
||||
await expect(
|
||||
page.getByText( 'Receiver wordpress@example.com' )
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText( 'Subject [WooCommerce Core E2E' )
|
||||
).toBeVisible();
|
||||
await page.getByRole( 'link', { name: 'json' } ).click();
|
||||
await expect(
|
||||
page.locator(
|
||||
'#wp-mail-logging-modal-content-body-content'
|
||||
)
|
||||
).toContainText(
|
||||
'You’ve received the following order from :'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
test( 'can receive completed email', async ( { page, baseURL } ) => {
|
||||
// Completed order emails are sent automatically when an order's payment is completed.
|
||||
|
|
|
@ -6,6 +6,7 @@ const statusColumnTextSelector = 'mark.order-status > span';
|
|||
|
||||
// Define order statuses to filter against
|
||||
const orderStatus = [
|
||||
[ 'All', 'all' ],
|
||||
[ 'Pending payment', 'wc-pending' ],
|
||||
[ 'Processing', 'wc-processing' ],
|
||||
[ 'On hold', 'wc-on-hold' ],
|
||||
|
@ -55,19 +56,6 @@ test.describe(
|
|||
await api.post( 'orders/batch', { delete: [ ...orderBatchId ] } );
|
||||
} );
|
||||
|
||||
test( 'should filter by All', async ( { page } ) => {
|
||||
await page.goto( '/wp-admin/admin.php?page=wc-orders' );
|
||||
|
||||
await page.locator( 'li.all > a' ).click();
|
||||
// because tests are running in parallel, we can't know how many orders there
|
||||
// are beyond the ones we created here.
|
||||
for ( let i = 0; i < orderStatus.length; i++ ) {
|
||||
const statusTag = 'text=' + orderStatus[ i ][ 0 ];
|
||||
const countElements = await page.locator( statusTag ).count();
|
||||
await expect( countElements ).toBeGreaterThan( 0 );
|
||||
}
|
||||
} );
|
||||
|
||||
for ( let i = 0; i < orderStatus.length; i++ ) {
|
||||
test( `should filter by ${ orderStatus[ i ][ 0 ] }`, async ( {
|
||||
page,
|
||||
|
@ -75,6 +63,9 @@ test.describe(
|
|||
await page.goto( '/wp-admin/admin.php?page=wc-orders' );
|
||||
|
||||
await page.locator( `li.${ orderStatus[ i ][ 1 ] }` ).click();
|
||||
await expect(
|
||||
page.locator( `li.${ orderStatus[ i ][ 1 ] } > a.current` )
|
||||
).toBeVisible();
|
||||
const countElements = await page
|
||||
.locator( statusColumnTextSelector )
|
||||
.count();
|
||||
|
|
|
@ -72,43 +72,51 @@ async function runComingSoonTests( themeContext = '' ) {
|
|||
} );
|
||||
}
|
||||
|
||||
test.describe( 'Launch Your Store front end - logged out', () => {
|
||||
test.afterAll( async ( { baseURL } ) => {
|
||||
try {
|
||||
await setOption(
|
||||
request,
|
||||
baseURL,
|
||||
'woocommerce_coming_soon',
|
||||
'no'
|
||||
test.describe(
|
||||
'Launch Your Store front end - logged out',
|
||||
{ tag: [ '@skip-on-default-wpcom', '@skip-on-default-pressable' ] },
|
||||
() => {
|
||||
test.afterAll( async ( { baseURL } ) => {
|
||||
try {
|
||||
await setOption(
|
||||
request,
|
||||
baseURL,
|
||||
'woocommerce_coming_soon',
|
||||
'no'
|
||||
);
|
||||
} catch ( error ) {
|
||||
console.log( error );
|
||||
}
|
||||
} );
|
||||
|
||||
test.describe( 'Block Theme (Twenty Twenty Four)', () => {
|
||||
test.beforeAll( async () => {
|
||||
await activateTheme( 'twentytwentyfour' );
|
||||
} );
|
||||
|
||||
test.afterAll( async () => {
|
||||
// Reset theme to the default.
|
||||
await activateTheme( DEFAULT_THEME );
|
||||
} );
|
||||
|
||||
runComingSoonTests( test.step, test.use );
|
||||
} );
|
||||
|
||||
test.describe( 'Classic Theme (Storefront)', () => {
|
||||
test.beforeAll( async () => {
|
||||
await activateTheme( 'storefront' );
|
||||
} );
|
||||
|
||||
test.afterAll( async () => {
|
||||
// Reset theme to the default.
|
||||
await activateTheme( DEFAULT_THEME );
|
||||
} );
|
||||
|
||||
runComingSoonTests(
|
||||
test.step,
|
||||
test.use,
|
||||
'Classic Theme (Storefront)'
|
||||
);
|
||||
} catch ( error ) {
|
||||
console.log( error );
|
||||
}
|
||||
} );
|
||||
|
||||
test.describe( 'Block Theme (Twenty Twenty Four)', () => {
|
||||
test.beforeAll( async () => {
|
||||
await activateTheme( 'twentytwentyfour' );
|
||||
} );
|
||||
|
||||
test.afterAll( async () => {
|
||||
// Reset theme to the default.
|
||||
await activateTheme( DEFAULT_THEME );
|
||||
} );
|
||||
|
||||
runComingSoonTests( test.step, test.use );
|
||||
} );
|
||||
|
||||
test.describe( 'Classic Theme (Storefront)', () => {
|
||||
test.beforeAll( async () => {
|
||||
await activateTheme( 'storefront' );
|
||||
} );
|
||||
|
||||
test.afterAll( async () => {
|
||||
// Reset theme to the default.
|
||||
await activateTheme( DEFAULT_THEME );
|
||||
} );
|
||||
|
||||
runComingSoonTests( test.step, test.use, 'Classic Theme (Storefront)' );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
|
|
@ -24,7 +24,14 @@ const test = baseTest.extend( {
|
|||
|
||||
test.describe(
|
||||
'Filter items in the shop by product price',
|
||||
{ tag: [ '@payments', '@services' ] },
|
||||
{
|
||||
tag: [
|
||||
'@payments',
|
||||
'@services',
|
||||
'@skip-on-default-wpcom',
|
||||
'@skip-on-default-pressable',
|
||||
],
|
||||
},
|
||||
() => {
|
||||
test.beforeAll( async ( { api } ) => {
|
||||
// add products
|
||||
|
|
|
@ -30,6 +30,18 @@ test.describe(
|
|||
await page
|
||||
.getByText( '1 page restored from the Trash.' )
|
||||
.waitFor( { state: 'visible' } );
|
||||
await page.goto( 'wp-admin/edit.php?post_type=page' );
|
||||
await page.getByRole( 'cell', { name: '“Shop” (Edit)' } ).hover();
|
||||
await page
|
||||
.getByLabel( 'Quick edit “Shop” inline' )
|
||||
.click( { force: true } );
|
||||
await page
|
||||
.getByLabel( 'Status Published Pending' )
|
||||
.selectOption( 'publish', { exact: true } );
|
||||
await page.getByRole( 'button', { name: 'Update' } ).click();
|
||||
await page
|
||||
.locator( '#a11y-speak-polite', { hasText: 'Changes saved.' } )
|
||||
.waitFor();
|
||||
} );
|
||||
|
||||
test( 'Check the title of the shop page after the page has been deleted', async ( {
|
||||
|
|
|
@ -6,7 +6,13 @@ const test = baseTest.extend( {
|
|||
|
||||
test(
|
||||
'logged-in customer can comment on a post',
|
||||
{ tag: [ '@non-critical' ] },
|
||||
{
|
||||
tag: [
|
||||
'@non-critical',
|
||||
'@skip-on-default-wpcom',
|
||||
'@skip-on-default-pressable',
|
||||
],
|
||||
},
|
||||
async ( { page } ) => {
|
||||
await page.goto( 'hello-world/' );
|
||||
await expect(
|
||||
|
|
|
@ -45,6 +45,7 @@ const goToPageEditor = async ( { page } ) => {
|
|||
);
|
||||
await page.goto( 'wp-admin/post-new.php?post_type=page' );
|
||||
await disableWelcomeModal( { page } );
|
||||
await closeChoosePatternModal( { page } );
|
||||
await responsePromise;
|
||||
};
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue