Merge branch 'trunk' into 51232-fixed-fatal-error-call-to-member
This commit is contained in:
commit
e93d9d9823
|
@ -4,7 +4,7 @@ on:
|
||||||
- cron: '21 0 * * *'
|
- cron: '21 0 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions: { }
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
|
@ -36,6 +36,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
only-issue-labels: 'metric: flaky e2e test'
|
only-issue-labels: 'metric: flaky e2e test'
|
||||||
|
exempt-issue-labels: 'metric: skipped test'
|
||||||
days-before-stale: -1
|
days-before-stale: -1
|
||||||
days-before-close: -1
|
days-before-close: -1
|
||||||
days-before-issue-stale: 5
|
days-before-issue-stale: 5
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
= 9.3.2 2024-09-18 =
|
||||||
|
|
||||||
|
- Fix - Improve the product importer's handling of filepaths under Windows [#51456](https://github.com/woocommerce/woocommerce/pull/51456)
|
||||||
|
- Fix - Revert changes related to low stock product notifications [#51441](https://github.com/woocommerce/woocommerce/pull/51441)
|
||||||
|
- Fix - Resolve a bug where manually triggering `added_to_cart` event without a button element caused an Exception [#51449](https://github.com/woocommerce/woocommerce/pull/51449)
|
||||||
|
|
||||||
|
|
||||||
= 9.3.1 2024-09-12 =
|
= 9.3.1 2024-09-12 =
|
||||||
|
|
||||||
* Tweak - Disable remote logging feature by default [#51312](https://github.com/woocommerce/woocommerce/pull/51312)
|
* Tweak - Disable remote logging feature by default [#51312](https://github.com/woocommerce/woocommerce/pull/51312)
|
||||||
|
|
|
@ -254,4 +254,4 @@ Displaying the variation in the front store works a bit differently for variable
|
||||||
|
|
||||||
## How to find hooks?
|
## How to find hooks?
|
||||||
|
|
||||||
Everyone will have their own preferred way, but for me, the quickest way is to look in the WooCommere plugin code. The code for each data section can be found in `/woocommerce/includes/admin/meta-boxes/views`. To view how the inventory section is handled check the `html-product-data-inventory.php` file, and for variations take a look at `html-variation-admin.php`.
|
Everyone will have their own preferred way, but for me, the quickest way is to look in the WooCommerce plugin code. The code for each data section can be found in `/woocommerce/includes/admin/meta-boxes/views`. To view how the inventory section is handled check the `html-product-data-inventory.php` file, and for variations take a look at `html-variation-admin.php`.
|
||||||
|
|
|
@ -43,12 +43,16 @@ The options you feed the configuration instance should be an object in this shap
|
||||||
```js
|
```js
|
||||||
const options = {
|
const options = {
|
||||||
name: 'my_payment_method',
|
name: 'my_payment_method',
|
||||||
content: <div>A React node</div>,
|
title: 'My Mayment Method',
|
||||||
edit: <div>A React node</div>,
|
description: 'A setence or two about your payment method',
|
||||||
|
gatewayId: 'gateway-id',
|
||||||
|
content: <ReactNode />,
|
||||||
|
edit: <ReactNode />,
|
||||||
canMakePayment: () => true,
|
canMakePayment: () => true,
|
||||||
paymentMethodId: 'new_payment_method',
|
paymentMethodId: 'new_payment_method',
|
||||||
supports: {
|
supports: {
|
||||||
features: [],
|
features: [],
|
||||||
|
style: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
@ -59,6 +63,18 @@ Here's some more details on the configuration options:
|
||||||
|
|
||||||
This should be a unique string (wise to try to pick something unique for your gateway that wouldn't be used by another implementation) that is used as the identifier for the gateway client side. If `paymentMethodId` is not provided, `name` is used for `paymentMethodId` as well.
|
This should be a unique string (wise to try to pick something unique for your gateway that wouldn't be used by another implementation) that is used as the identifier for the gateway client side. If `paymentMethodId` is not provided, `name` is used for `paymentMethodId` as well.
|
||||||
|
|
||||||
|
#### `title` (optional)
|
||||||
|
|
||||||
|
This should be a human readable string with the name of your payment method. It should be sentence capitalised. It is displayed to the merchant in the editor when viewing the Checkout block to indicate which express payment methods are active. If it is not provided, the `name` will be used as the title.
|
||||||
|
|
||||||
|
#### `description` (optional)
|
||||||
|
|
||||||
|
This is one or two sentences maximum describing your payment gateway. It should be sentence capitalised. It is displayed to the merchant in the editor when viewing the Checkout block to indicate which express payment methods are active.
|
||||||
|
|
||||||
|
#### `gatewayId` (optional)
|
||||||
|
|
||||||
|
This is the ID of the Payment Gateway that your plugin registers server side, and which registers the express payment method. It is used to link your express payment method on the clinet, to a payment gateway defined on the server. It is used to direct the merchant to the right settings page within the editor. If this is not provided, the merchant will be redirected to the general Woo payment settings page.
|
||||||
|
|
||||||
#### `content` (required)
|
#### `content` (required)
|
||||||
|
|
||||||
This should be a React node that will output in the express payment method area when the block is rendered in the frontend. It will be cloned in the rendering process. When cloned, this React node will receive props passed in from the checkout payment method interface that will allow your component to interact with checkout data (more on [these props later](#props-fed-to-payment-method-nodes)).
|
This should be a React node that will output in the express payment method area when the block is rendered in the frontend. It will be cloned in the rendering process. When cloned, this React node will receive props passed in from the checkout payment method interface that will allow your component to interact with checkout data (more on [these props later](#props-fed-to-payment-method-nodes)).
|
||||||
|
@ -97,7 +113,11 @@ This is the only optional configuration object. The value of this property is wh
|
||||||
|
|
||||||
This is an array of payment features supported by the gateway. It is used to crosscheck if the payment method can be used for the content of the cart. By default payment methods should support at least `products` feature. If no value is provided then this assumes that `['products']` are supported.
|
This is an array of payment features supported by the gateway. It is used to crosscheck if the payment method can be used for the content of the cart. By default payment methods should support at least `products` feature. If no value is provided then this assumes that `['products']` are supported.
|
||||||
|
|
||||||
---
|
#### `supports:style`
|
||||||
|
|
||||||
|
This is an array of style variations supported by the express payment method. These are styles that are applied across all the active express payment buttons and can be controlled from the express payment block in the editor. Supported values for these are one of `['height', 'borderRadius']`.
|
||||||
|
|
||||||
|
![Express Checkout Uniform Styles](https://github.com/user-attachments/assets/f0f99f3f-dca7-42b0-8685-3b098a825020)
|
||||||
|
|
||||||
### Payment Methods - `registerPaymentMethod( options )`
|
### Payment Methods - `registerPaymentMethod( options )`
|
||||||
|
|
||||||
|
@ -140,7 +160,7 @@ The options you feed the configuration instance are the same as those for expres
|
||||||
A big part of the payment method integration is the interface that is exposed for payment methods to use via props when the node provided is cloned and rendered on block mount. While all the props are listed below, you can find more details about what the props reference, their types etc via the [typedefs described in this file](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/assets/js/types/type-defs/payment-method-interface.ts).
|
A big part of the payment method integration is the interface that is exposed for payment methods to use via props when the node provided is cloned and rendered on block mount. While all the props are listed below, you can find more details about what the props reference, their types etc via the [typedefs described in this file](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/assets/js/types/type-defs/payment-method-interface.ts).
|
||||||
|
|
||||||
| Property | Type | Description | Values |
|
| Property | Type | Description | Values |
|
||||||
| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `activePaymentMethod` | String | The slug of the current active payment method in the checkout. | - |
|
| `activePaymentMethod` | String | The slug of the current active payment method in the checkout. | - |
|
||||||
| `billing` | Object | Contains everything related to billing. | `billingAddress`, `cartTotal`, `currency`, `cartTotalItems`, `displayPricesIncludingTax`, `appliedCoupons`, `customerId` |
|
| `billing` | Object | Contains everything related to billing. | `billingAddress`, `cartTotal`, `currency`, `cartTotalItems`, `displayPricesIncludingTax`, `appliedCoupons`, `customerId` |
|
||||||
| `cartData` | Object | Data exposed from the cart including items, fees, and any registered extension data. Note that this data should be treated as immutable (should not be modified/mutated) or it will result in errors in your application. | `cartItems`, `cartFees`, `extensions` |
|
| `cartData` | Object | Data exposed from the cart including items, fees, and any registered extension data. Note that this data should be treated as immutable (should not be modified/mutated) or it will result in errors in your application. | `cartItems`, `cartFees`, `extensions` |
|
||||||
|
@ -151,7 +171,8 @@ A big part of the payment method integration is the interface that is exposed fo
|
||||||
| `onClick` | Function | **Provided to express payment methods** that should be triggered when the payment method button is clicked (which will signal to checkout the payment method has taken over payment processing) | - |
|
| `onClick` | Function | **Provided to express payment methods** that should be triggered when the payment method button is clicked (which will signal to checkout the payment method has taken over payment processing) | - |
|
||||||
| `onClose` | Function | **Provided to express payment methods** that should be triggered when the express payment method modal closes and control is returned to checkout. | - |
|
| `onClose` | Function | **Provided to express payment methods** that should be triggered when the express payment method modal closes and control is returned to checkout. | - |
|
||||||
| `onSubmit` | Function | Submits the checkout and begins processing | - |
|
| `onSubmit` | Function | Submits the checkout and begins processing | - |
|
||||||
| `paymentStatus` | Object | Various payment status helpers. Note, your payment method does not have to handle setting this status client side. Checkout will handle this via the responses your payment method gives from observers registered to [checkout event emitters](./checkout-flow-and-events.md). | `isPristine`, `isStarted`, `isProcessing`, `isFinished`, `hasError`, `hasFailed`, `isSuccessful` (see below for explanation) |
|
| `buttonAttributes` | Object | Styles set by the merchant that should be respected by all express payment buttons | `height, borderRadius` |
|
||||||
|
| `paymentStatus` | Object | Various payment status helpers. Note, your payment method does not have to handle setting this status client side. Checkout will handle this via the responses your payment method gives from observers registered to [checkout event emitters](./checkout-flow-and-events.md). | `isPristine`, `isStarted`, `isProcessing`, `isFinished`, `hasError`, `hasFailed`, `isSuccessful`(see below for explanation) |
|
||||||
| `setExpressPaymentError` | Function | Receives a string and allows express payment methods to set an error notice for the express payment area on demand. This can be necessary because some express payment method processing might happen outside of checkout events. | - |
|
| `setExpressPaymentError` | Function | Receives a string and allows express payment methods to set an error notice for the express payment area on demand. This can be necessary because some express payment method processing might happen outside of checkout events. | - |
|
||||||
| `shippingData` | Object | Contains all shipping related data (outside of the shipping status). | `shippingRates`, `shippingRatesLoading`, `selectedRates`, `setSelectedRates`, `isSelectingRate`, `shippingAddress`, `setShippingAddress`, and `needsShipping` |
|
| `shippingData` | Object | Contains all shipping related data (outside of the shipping status). | `shippingRates`, `shippingRatesLoading`, `selectedRates`, `setSelectedRates`, `isSelectingRate`, `shippingAddress`, `setShippingAddress`, and `needsShipping` |
|
||||||
| `shippingStatus` | Object | Various shipping status helpers. | <!-- markdownlint-disable-line no-inline-html --><ul><li>`shippingErrorStatus`: an object with various error statuses that might exist for shipping</li><li>`shippingErrorTypes`: an object containing all the possible types for shipping error status</li></ul> |
|
| `shippingStatus` | Object | Various shipping status helpers. | <!-- markdownlint-disable-line no-inline-html --><ul><li>`shippingErrorStatus`: an object with various error statuses that might exist for shipping</li><li>`shippingErrorTypes`: an object containing all the possible types for shipping error status</li></ul> |
|
||||||
|
@ -167,6 +188,29 @@ A big part of the payment method integration is the interface that is exposed fo
|
||||||
|
|
||||||
Any registered `savedTokenComponent` node will also receive a `token` prop which includes the id for the selected saved token in case your payment method needs to use it for some internal logic. However, keep in mind, this is just the id representing this token in the database (and the value of the radio input the shopper checked), not the actual customer payment token (since processing using that usually happens on the server for security).
|
Any registered `savedTokenComponent` node will also receive a `token` prop which includes the id for the selected saved token in case your payment method needs to use it for some internal logic. However, keep in mind, this is just the id representing this token in the database (and the value of the radio input the shopper checked), not the actual customer payment token (since processing using that usually happens on the server for security).
|
||||||
|
|
||||||
|
### Button Attributes for Express Payment Methods
|
||||||
|
|
||||||
|
This API provides a way to synchronise the look and feel of the express payment buttons for a coherent shopper experience. Express Payment Methods must prefer the values provided in the `buttonAttributes`, and use it's own configuration settings as backup when the buttons are rendered somewhere other than the Cart or Checkout block.
|
||||||
|
|
||||||
|
For example, in your button component, you would do something like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Get your extension specific settings and set defaults if not available
|
||||||
|
let {
|
||||||
|
borderRadius = '4',
|
||||||
|
height = '48',
|
||||||
|
} = getButtonSettingsFromConfig();
|
||||||
|
|
||||||
|
// In a cart & checkout block context, we receive `buttonAttributes` as a prop which overwrite the extension specific settings
|
||||||
|
if ( typeof buttonAttributes !== 'undefined' ) {
|
||||||
|
height = buttonAttributes.height;
|
||||||
|
borderRadius = buttonAttributes.borderRadius;
|
||||||
|
}
|
||||||
|
...
|
||||||
|
|
||||||
|
return <button style={height: `${height}px`, borderRadius: `${borderRadius}px`} />
|
||||||
|
```
|
||||||
|
|
||||||
## Server Side Integration
|
## Server Side Integration
|
||||||
|
|
||||||
### Processing Payment
|
### Processing Payment
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
"menu_title": "Add Custom Fields to Products",
|
"menu_title": "Add Custom Fields to Products",
|
||||||
"tags": "how-to",
|
"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",
|
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md",
|
||||||
"hash": "fe8cf43940f5166bf69f102aa4643cbe32415b1167d6b6d8968d434a4d113879",
|
"hash": "df61c93febc234fe0dbb4826a20ae120b153ab6f6c92d8778177fcac8d6696fe",
|
||||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md",
|
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/building-a-woo-store/adding-a-custom-field-to-variable-products.md",
|
||||||
"id": "64b686dcd5fdd4842be2fc570108231d5a8bfc1b"
|
"id": "64b686dcd5fdd4842be2fc570108231d5a8bfc1b"
|
||||||
}
|
}
|
||||||
|
@ -223,7 +223,7 @@
|
||||||
"menu_title": "Payment Method Integration",
|
"menu_title": "Payment Method Integration",
|
||||||
"tags": "reference",
|
"tags": "reference",
|
||||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md",
|
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md",
|
||||||
"hash": "138ffbf27e79ec8b35d2c46e87e3663c203d91fc9ba3f76c43f3cbe76258e5bf",
|
"hash": "015aae25bb331364c224fe8eb2b7675e4cbed0a9e6bee0dde5f5311388161b0a",
|
||||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md",
|
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/cart-and-checkout-blocks/checkout-payment-methods/payment-method-integration.md",
|
||||||
"id": "c9a763b6976ecf03aeb961577c17c31f1ac7c420",
|
"id": "c9a763b6976ecf03aeb961577c17c31f1ac7c420",
|
||||||
"links": {
|
"links": {
|
||||||
|
@ -1229,7 +1229,7 @@
|
||||||
"menu_title": "Core critical flows",
|
"menu_title": "Core critical flows",
|
||||||
"tags": "reference",
|
"tags": "reference",
|
||||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/core-critical-flows.md",
|
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/core-critical-flows.md",
|
||||||
"hash": "34109195216ebcb5b23e741391b9f355ba861777a5533d4ef1e341472cb5209e",
|
"hash": "c7122979df14f46646b3f1472ba071bc560b99e6462c5790a9aeaa3b4238ce15",
|
||||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/core-critical-flows.md",
|
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/core-critical-flows.md",
|
||||||
"id": "e561b46694dba223c38b87613ce4907e4e14333a"
|
"id": "e561b46694dba223c38b87613ce4907e4e14333a"
|
||||||
},
|
},
|
||||||
|
@ -1804,5 +1804,5 @@
|
||||||
"categories": []
|
"categories": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hash": "47dc2a7e213e1e9d83e93a85dafdf8c7b539cc5474b4166eb6398b734d150ff3"
|
"hash": "a88d9ea54465c8bbd820042a92df79cbd48943e785b418fcaa04d0c0e66116c0"
|
||||||
}
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Update @wordpress/dataviews package and fix class names for products dataviews app.
|
|
@ -56,7 +56,7 @@
|
||||||
"@wordpress/compose": "wp-6.0",
|
"@wordpress/compose": "wp-6.0",
|
||||||
"@wordpress/core-data": "wp-6.0",
|
"@wordpress/core-data": "wp-6.0",
|
||||||
"@wordpress/data": "wp-6.0",
|
"@wordpress/data": "wp-6.0",
|
||||||
"@wordpress/dataviews": "^4.2.0",
|
"@wordpress/dataviews": "^4.3.0",
|
||||||
"@wordpress/date": "wp-6.0",
|
"@wordpress/date": "wp-6.0",
|
||||||
"@wordpress/deprecated": "wp-6.0",
|
"@wordpress/deprecated": "wp-6.0",
|
||||||
"@wordpress/edit-post": "wp-6.0",
|
"@wordpress/edit-post": "wp-6.0",
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
@include wordpress-admin-schemes();
|
@include wordpress-admin-schemes();
|
||||||
|
|
||||||
.woocommerce_page_woocommerce-products-dashboard #wpadminbar,
|
.product_page_woocommerce-products-dashboard #wpadminbar,
|
||||||
.woocommerce_page_woocommerce-products-dashboard #adminmenumain {
|
.product_page_woocommerce-products-dashboard #adminmenumain {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce_page_woocommerce-products-dashboard #wpcontent {
|
.product_page_woocommerce-products-dashboard #wpcontent {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.woocommerce_page_woocommerce-products-dashboard #woocommerce-products-dashboard {
|
body.product_page_woocommerce-products-dashboard #woocommerce-products-dashboard {
|
||||||
@include wp-admin-reset("#woocommerce-products-dashboard");
|
@include wp-admin-reset("#woocommerce-products-dashboard");
|
||||||
@include reset;
|
@include reset;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
@ -40,7 +40,7 @@ body.js.is-fullscreen-mode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce_page_woocommerce-products-dashboard {
|
.product_page_woocommerce-products-dashboard {
|
||||||
@import "products-app/sidebar-dataviews/style.scss";
|
@import "products-app/sidebar-dataviews/style.scss";
|
||||||
@import "products-app/product-edit/style.scss";
|
@import "products-app/product-edit/style.scss";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
@import "../../stylesheets/_variables.scss";
|
@import "../../stylesheets/_variables.scss";
|
||||||
|
|
||||||
.woocommerce-marketplace__category-selector {
|
.woocommerce-marketplace__category-selector {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
margin: $grid-unit-20 0 0 0;
|
margin: 0;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-marketplace__category-item {
|
.woocommerce-marketplace__category-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
.components-dropdown {
|
.components-dropdown {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -50,7 +54,6 @@
|
||||||
|
|
||||||
.woocommerce-marketplace__category-selector--full-width {
|
.woocommerce-marketplace__category-selector--full-width {
|
||||||
display: none;
|
display: none;
|
||||||
margin-top: $grid-unit-15;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $break-medium) {
|
@media screen and (max-width: $break-medium) {
|
||||||
|
@ -122,3 +125,22 @@
|
||||||
background-color: $gray-900;
|
background-color: $gray-900;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.woocommerce-marketplace__category-navigation-button {
|
||||||
|
border: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-marketplace__category-navigation-button--prev {
|
||||||
|
background: linear-gradient(to right, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-marketplace__category-navigation-button--next {
|
||||||
|
background: linear-gradient(to left, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect } from '@wordpress/element';
|
import { useState, useEffect, useRef } from '@wordpress/element';
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
import { useQuery } from '@woocommerce/navigation';
|
import { useQuery } from '@woocommerce/navigation';
|
||||||
import clsx from 'clsx';
|
import { Icon } from '@wordpress/components';
|
||||||
|
import { useDebounce } from '@wordpress/compose';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import CategoryLink from './category-link';
|
import CategoryLink from './category-link';
|
||||||
import CategoryDropdown from './category-dropdown';
|
|
||||||
import { Category, CategoryAPIItem } from './types';
|
import { Category, CategoryAPIItem } from './types';
|
||||||
import { fetchCategories } from '../../utils/functions';
|
import { fetchCategories } from '../../utils/functions';
|
||||||
import './category-selector.scss';
|
|
||||||
import { ProductType } from '../product-list/types';
|
import { ProductType } from '../product-list/types';
|
||||||
|
import CategoryDropdown from './category-dropdown';
|
||||||
|
import './category-selector.scss';
|
||||||
|
|
||||||
const ALL_CATEGORIES_SLUGS = {
|
const ALL_CATEGORIES_SLUGS = {
|
||||||
[ ProductType.extension ]: '_all',
|
[ ProductType.extension ]: '_all',
|
||||||
|
@ -29,32 +30,21 @@ interface CategorySelectorProps {
|
||||||
export default function CategorySelector(
|
export default function CategorySelector(
|
||||||
props: CategorySelectorProps
|
props: CategorySelectorProps
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
const [ visibleItems, setVisibleItems ] = useState< Category[] >( [] );
|
|
||||||
const [ dropdownItems, setDropdownItems ] = useState< Category[] >( [] );
|
|
||||||
const [ selected, setSelected ] = useState< Category >();
|
const [ selected, setSelected ] = useState< Category >();
|
||||||
const [ isLoading, setIsLoading ] = useState( false );
|
const [ isLoading, setIsLoading ] = useState( false );
|
||||||
|
const [ categoriesToShow, setCategoriesToShow ] = useState< Category[] >(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const [ isOverflowing, setIsOverflowing ] = useState( false );
|
||||||
|
const [ scrollPosition, setScrollPosition ] = useState<
|
||||||
|
'start' | 'middle' | 'end'
|
||||||
|
>( 'start' );
|
||||||
|
|
||||||
|
const categorySelectorRef = useRef< HTMLUListElement >( null );
|
||||||
|
const selectedCategoryRef = useRef< HTMLLIElement >( null );
|
||||||
|
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
|
|
||||||
useEffect( () => {
|
|
||||||
// If no category is selected, show All as selected
|
|
||||||
let categoryToSearch = ALL_CATEGORIES_SLUGS[ props.type ];
|
|
||||||
|
|
||||||
if ( query.category ) {
|
|
||||||
categoryToSearch = query.category;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allCategories = visibleItems.concat( dropdownItems );
|
|
||||||
|
|
||||||
const selectedCategory = allCategories.find(
|
|
||||||
( category ) => category.slug === categoryToSearch
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( selectedCategory ) {
|
|
||||||
setSelected( selectedCategory );
|
|
||||||
}
|
|
||||||
}, [ query.category, props.type, visibleItems, dropdownItems ] );
|
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
setIsLoading( true );
|
setIsLoading( true );
|
||||||
|
|
||||||
|
@ -72,21 +62,125 @@ export default function CategorySelector(
|
||||||
return category.slug !== '_featured';
|
return category.slug !== '_featured';
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// Split array into two from 7th item
|
setCategoriesToShow( categories );
|
||||||
const visibleCategoryItems = categories.slice( 0, 7 );
|
|
||||||
const dropdownCategoryItems = categories.slice( 7 );
|
|
||||||
|
|
||||||
setVisibleItems( visibleCategoryItems );
|
|
||||||
setDropdownItems( dropdownCategoryItems );
|
|
||||||
} )
|
} )
|
||||||
.catch( () => {
|
.catch( () => {
|
||||||
setVisibleItems( [] );
|
setCategoriesToShow( [] );
|
||||||
setDropdownItems( [] );
|
|
||||||
} )
|
} )
|
||||||
.finally( () => {
|
.finally( () => {
|
||||||
setIsLoading( false );
|
setIsLoading( false );
|
||||||
} );
|
} );
|
||||||
}, [ props.type ] );
|
}, [ props.type, setCategoriesToShow ] );
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
// If no category is selected, show All as selected
|
||||||
|
let categoryToSearch = ALL_CATEGORIES_SLUGS[ props.type ];
|
||||||
|
|
||||||
|
if ( query.category ) {
|
||||||
|
categoryToSearch = query.category;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedCategory = categoriesToShow.find(
|
||||||
|
( category ) => category.slug === categoryToSearch
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( selectedCategory ) {
|
||||||
|
setSelected( selectedCategory );
|
||||||
|
}
|
||||||
|
}, [ query.category, props.type, categoriesToShow ] );
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
if ( selectedCategoryRef.current ) {
|
||||||
|
selectedCategoryRef.current.scrollIntoView( {
|
||||||
|
block: 'nearest',
|
||||||
|
inline: 'center',
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}, [ selected ] );
|
||||||
|
|
||||||
|
function checkOverflow() {
|
||||||
|
if (
|
||||||
|
categorySelectorRef.current &&
|
||||||
|
categorySelectorRef.current.parentElement?.scrollWidth
|
||||||
|
) {
|
||||||
|
const isContentOverflowing =
|
||||||
|
categorySelectorRef.current.scrollWidth >
|
||||||
|
categorySelectorRef.current.parentElement.scrollWidth;
|
||||||
|
|
||||||
|
setIsOverflowing( isContentOverflowing );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkScrollPosition() {
|
||||||
|
const ulElement = categorySelectorRef.current;
|
||||||
|
|
||||||
|
if ( ! ulElement ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { scrollLeft, scrollWidth, clientWidth } = ulElement;
|
||||||
|
|
||||||
|
if ( scrollLeft < 10 ) {
|
||||||
|
setScrollPosition( 'start' );
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( scrollLeft + clientWidth < scrollWidth ) {
|
||||||
|
setScrollPosition( 'middle' );
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( scrollLeft + clientWidth === scrollWidth ) {
|
||||||
|
setScrollPosition( 'end' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedCheckOverflow = useDebounce( checkOverflow, 300 );
|
||||||
|
const debouncedScrollPosition = useDebounce( checkScrollPosition, 100 );
|
||||||
|
|
||||||
|
function scrollCategories( scrollAmount: number ) {
|
||||||
|
if ( categorySelectorRef.current ) {
|
||||||
|
categorySelectorRef.current.scrollTo( {
|
||||||
|
left: categorySelectorRef.current.scrollLeft + scrollAmount,
|
||||||
|
behavior: 'smooth',
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToNextCategories() {
|
||||||
|
scrollCategories( 200 );
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToPrevCategories() {
|
||||||
|
scrollCategories( -200 );
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
window.addEventListener( 'resize', debouncedCheckOverflow );
|
||||||
|
|
||||||
|
const ulElement = categorySelectorRef.current;
|
||||||
|
|
||||||
|
if ( ulElement ) {
|
||||||
|
ulElement.addEventListener( 'scroll', debouncedScrollPosition );
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener( 'resize', debouncedCheckOverflow );
|
||||||
|
|
||||||
|
if ( ulElement ) {
|
||||||
|
ulElement.removeEventListener(
|
||||||
|
'scroll',
|
||||||
|
debouncedScrollPosition
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [ debouncedCheckOverflow, debouncedScrollPosition ] );
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
checkOverflow();
|
||||||
|
}, [ categoriesToShow ] );
|
||||||
|
|
||||||
function mobileCategoryDropdownLabel() {
|
function mobileCategoryDropdownLabel() {
|
||||||
const allCategoriesText = __( 'All Categories', 'woocommerce' );
|
const allCategoriesText = __( 'All Categories', 'woocommerce' );
|
||||||
|
@ -102,16 +196,6 @@ export default function CategorySelector(
|
||||||
return selected.label;
|
return selected.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSelectedInDropdown() {
|
|
||||||
if ( ! selected ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dropdownItems.find(
|
|
||||||
( category ) => category.slug === selected.slug
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( isLoading ) {
|
if ( isLoading ) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -131,50 +215,62 @@ export default function CategorySelector(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ul className="woocommerce-marketplace__category-selector">
|
<ul
|
||||||
{ visibleItems.map( ( category ) => (
|
className="woocommerce-marketplace__category-selector"
|
||||||
|
aria-label="Categories"
|
||||||
|
ref={ categorySelectorRef }
|
||||||
|
>
|
||||||
|
{ categoriesToShow.map( ( category ) => (
|
||||||
<li
|
<li
|
||||||
className="woocommerce-marketplace__category-item"
|
className="woocommerce-marketplace__category-item"
|
||||||
key={ category.slug }
|
key={ category.slug }
|
||||||
|
ref={
|
||||||
|
category.slug === selected?.slug
|
||||||
|
? selectedCategoryRef
|
||||||
|
: null
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<CategoryLink
|
<CategoryLink
|
||||||
{ ...category }
|
{ ...category }
|
||||||
selected={ category.slug === selected?.slug }
|
selected={ category.slug === selected?.slug }
|
||||||
|
aria-current={ category.slug === selected?.slug }
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
) ) }
|
) ) }
|
||||||
<li className="woocommerce-marketplace__category-item">
|
|
||||||
{ dropdownItems.length > 0 && (
|
|
||||||
<CategoryDropdown
|
|
||||||
type={ props.type }
|
|
||||||
label={ __( 'More', 'woocommerce' ) }
|
|
||||||
categories={ dropdownItems }
|
|
||||||
buttonClassName={ clsx(
|
|
||||||
'woocommerce-marketplace__category-item-button',
|
|
||||||
{
|
|
||||||
'woocommerce-marketplace__category-item-button--selected':
|
|
||||||
isSelectedInDropdown(),
|
|
||||||
}
|
|
||||||
) }
|
|
||||||
contentClassName="woocommerce-marketplace__category-item-content"
|
|
||||||
arrowIconSize={ 20 }
|
|
||||||
selected={ selected }
|
|
||||||
/>
|
|
||||||
) }
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div className="woocommerce-marketplace__category-selector--full-width">
|
<div className="woocommerce-marketplace__category-selector--full-width">
|
||||||
<CategoryDropdown
|
<CategoryDropdown
|
||||||
type={ props.type }
|
type={ props.type }
|
||||||
label={ mobileCategoryDropdownLabel() }
|
label={ mobileCategoryDropdownLabel() }
|
||||||
categories={ visibleItems.concat( dropdownItems ) }
|
categories={ categoriesToShow }
|
||||||
buttonClassName="woocommerce-marketplace__category-dropdown-button"
|
buttonClassName="woocommerce-marketplace__category-dropdown-button"
|
||||||
className="woocommerce-marketplace__category-dropdown"
|
className="woocommerce-marketplace__category-dropdown"
|
||||||
contentClassName="woocommerce-marketplace__category-dropdown-content"
|
contentClassName="woocommerce-marketplace__category-dropdown-content"
|
||||||
selected={ selected }
|
selected={ selected }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{ isOverflowing && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={ scrollToPrevCategories }
|
||||||
|
className="woocommerce-marketplace__category-navigation-button woocommerce-marketplace__category-navigation-button--prev"
|
||||||
|
hidden={ scrollPosition === 'start' }
|
||||||
|
aria-label="Scroll to previous categories"
|
||||||
|
tabIndex={ -1 }
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-left-alt2" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={ scrollToNextCategories }
|
||||||
|
className="woocommerce-marketplace__category-navigation-button woocommerce-marketplace__category-navigation-button--next"
|
||||||
|
hidden={ scrollPosition === 'end' }
|
||||||
|
aria-label="Scroll to next categories"
|
||||||
|
tabIndex={ -1 }
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-right-alt2" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) }
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const MARKETPLACE_SEARCH_API_PATH =
|
||||||
'/wp-json/wccom-extensions/1.0/search';
|
'/wp-json/wccom-extensions/1.0/search';
|
||||||
export const MARKETPLACE_CATEGORY_API_PATH =
|
export const MARKETPLACE_CATEGORY_API_PATH =
|
||||||
'/wp-json/wccom-extensions/1.0/categories';
|
'/wp-json/wccom-extensions/1.0/categories';
|
||||||
export const MARKETPLACE_ITEMS_PER_PAGE = 60;
|
export const MARKETPLACE_ITEMS_PER_PAGE = 60; // This should match the number of results returned by the API
|
||||||
export const MARKETPLACE_SEARCH_RESULTS_PER_PAGE = 8;
|
export const MARKETPLACE_SEARCH_RESULTS_PER_PAGE = 8;
|
||||||
export const MARKETPLACE_CART_PATH = MARKETPLACE_HOST + '/cart/';
|
export const MARKETPLACE_CART_PATH = MARKETPLACE_HOST + '/cart/';
|
||||||
export const MARKETPLACE_RENEW_SUBSCRIPTON_PATH =
|
export const MARKETPLACE_RENEW_SUBSCRIPTON_PATH =
|
||||||
|
|
|
@ -1,22 +1,29 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useContext, useEffect, useState } from '@wordpress/element';
|
import {
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
} from '@wordpress/element';
|
||||||
import { useQuery } from '@woocommerce/navigation';
|
import { useQuery } from '@woocommerce/navigation';
|
||||||
|
import { speak } from '@wordpress/a11y';
|
||||||
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import './content.scss';
|
import './content.scss';
|
||||||
import { Product, ProductType, SearchResultType } from '../product-list/types';
|
import { Product, ProductType } from '../product-list/types';
|
||||||
import { getAdminSetting } from '~/utils/admin-settings';
|
import { getAdminSetting } from '~/utils/admin-settings';
|
||||||
import Discover from '../discover/discover';
|
import Discover from '../discover/discover';
|
||||||
import Products from '../products/products';
|
import Products from '../products/products';
|
||||||
import SearchResults from '../search-results/search-results';
|
|
||||||
import MySubscriptions from '../my-subscriptions/my-subscriptions';
|
import MySubscriptions from '../my-subscriptions/my-subscriptions';
|
||||||
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
||||||
import { fetchSearchResults } from '../../utils/functions';
|
import { fetchSearchResults, getProductType } from '../../utils/functions';
|
||||||
import { SubscriptionsContextProvider } from '../../contexts/subscriptions-context';
|
import { SubscriptionsContextProvider } from '../../contexts/subscriptions-context';
|
||||||
|
import { SearchResultsCountType } from '../../contexts/types';
|
||||||
import {
|
import {
|
||||||
recordMarketplaceView,
|
recordMarketplaceView,
|
||||||
recordLegacyTabView,
|
recordLegacyTabView,
|
||||||
|
@ -26,79 +33,157 @@ import Promotions from '../promotions/promotions';
|
||||||
import ConnectNotice from '~/marketplace/components/connect-notice/connect-notice';
|
import ConnectNotice from '~/marketplace/components/connect-notice/connect-notice';
|
||||||
import PluginInstallNotice from '../woo-update-manager-plugin/plugin-install-notice';
|
import PluginInstallNotice from '../woo-update-manager-plugin/plugin-install-notice';
|
||||||
import SubscriptionsExpiredExpiringNotice from '~/marketplace/components/my-subscriptions/subscriptions-expired-expiring-notice';
|
import SubscriptionsExpiredExpiringNotice from '~/marketplace/components/my-subscriptions/subscriptions-expired-expiring-notice';
|
||||||
|
import LoadMoreButton from '../load-more-button/load-more-button';
|
||||||
|
|
||||||
export default function Content(): JSX.Element {
|
export default function Content(): JSX.Element {
|
||||||
const marketplaceContextValue = useContext( MarketplaceContext );
|
const marketplaceContextValue = useContext( MarketplaceContext );
|
||||||
const [ products, setProducts ] = useState< Product[] >( [] );
|
const [ allProducts, setAllProducts ] = useState< Product[] >( [] );
|
||||||
const { setIsLoading, selectedTab, setHasBusinessServices } =
|
const [ filteredProducts, setFilteredProducts ] = useState< Product[] >(
|
||||||
marketplaceContextValue;
|
[]
|
||||||
|
);
|
||||||
|
const [ currentPage, setCurrentPage ] = useState( 1 );
|
||||||
|
const [ totalPagesCategory, setTotalPagesCategory ] = useState( 1 );
|
||||||
|
const [ totalPagesExtensions, setTotalPagesExtensions ] = useState( 1 );
|
||||||
|
const [ totalPagesThemes, setTotalPagesThemes ] = useState( 1 );
|
||||||
|
const [ totalPagesBusinessServices, setTotalPagesBusinessServices ] =
|
||||||
|
useState( 1 );
|
||||||
|
const [ firstNewProductId, setFirstNewProductId ] = useState< number >( 0 );
|
||||||
|
const [ isLoadingMore, setIsLoadingMore ] = useState( false );
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
setIsLoading,
|
||||||
|
selectedTab,
|
||||||
|
setHasBusinessServices,
|
||||||
|
setSearchResultsCount,
|
||||||
|
} = marketplaceContextValue;
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
|
|
||||||
// On initial load of the in-app marketplace, fetch extensions, themes and business services
|
const searchCompleteAnnouncement = ( count: number ): void => {
|
||||||
// and check if there are any business services available on WCCOM
|
speak(
|
||||||
useEffect( () => {
|
sprintf(
|
||||||
const categories = [ '', 'themes', 'business-services' ];
|
// translators: %d is the number of products found.
|
||||||
const abortControllers = categories.map( () => new AbortController() );
|
__( '%d products found', 'woocommerce' ),
|
||||||
|
count
|
||||||
categories.forEach( ( category: string, index ) => {
|
)
|
||||||
const params = new URLSearchParams();
|
|
||||||
if ( category !== '' ) {
|
|
||||||
params.append( 'category', category );
|
|
||||||
}
|
|
||||||
|
|
||||||
const wccomSettings = getAdminSetting( 'wccomHelper', false );
|
|
||||||
if ( wccomSettings.storeCountry ) {
|
|
||||||
params.append( 'country', wccomSettings.storeCountry );
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchSearchResults( params, abortControllers[ index ].signal ).then(
|
|
||||||
( productList ) => {
|
|
||||||
if ( category === 'business-services' ) {
|
|
||||||
setHasBusinessServices( productList.length > 0 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return () => {
|
|
||||||
abortControllers.forEach( ( controller ) => {
|
|
||||||
controller.abort();
|
|
||||||
} );
|
|
||||||
};
|
};
|
||||||
} );
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [] );
|
|
||||||
|
|
||||||
// Get the content for this screen
|
const tagProductsWithType = (
|
||||||
useEffect( () => {
|
products: Product[],
|
||||||
|
type: ProductType
|
||||||
|
): Product[] => {
|
||||||
|
return products.map( ( product ) => ( {
|
||||||
|
...product,
|
||||||
|
type,
|
||||||
|
} ) );
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadMoreProducts = useCallback( () => {
|
||||||
|
setIsLoadingMore( true );
|
||||||
|
const params = new URLSearchParams();
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
|
||||||
if (
|
if ( query.category && query.category !== '_all' ) {
|
||||||
query.tab === undefined ||
|
params.append( 'category', query.category );
|
||||||
( query.tab &&
|
|
||||||
[ '', 'discover', 'my-subscriptions' ].includes( query.tab ) )
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading( true );
|
if ( query.tab === 'themes' || query.tab === 'business-services' ) {
|
||||||
setProducts( [] );
|
params.append( 'category', query.tab );
|
||||||
|
}
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
if ( query.term ) {
|
if ( query.term ) {
|
||||||
params.append( 'term', query.term );
|
params.append( 'term', query.term );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( query.category ) {
|
const wccomSettings = getAdminSetting( 'wccomHelper', false );
|
||||||
params.append(
|
if ( wccomSettings.storeCountry ) {
|
||||||
'category',
|
params.append( 'country', wccomSettings.storeCountry );
|
||||||
query.category === '_all' ? '' : query.category
|
}
|
||||||
|
|
||||||
|
params.append( 'page', ( currentPage + 1 ).toString() );
|
||||||
|
|
||||||
|
fetchSearchResults( params, abortController.signal )
|
||||||
|
.then( ( productList ) => {
|
||||||
|
setAllProducts( ( prevProducts ) => {
|
||||||
|
const flattenedPrevProducts = Array.isArray(
|
||||||
|
prevProducts[ 0 ]
|
||||||
|
)
|
||||||
|
? prevProducts.flat()
|
||||||
|
: prevProducts;
|
||||||
|
|
||||||
|
const newProducts = productList.products.filter(
|
||||||
|
( newProduct ) =>
|
||||||
|
! flattenedPrevProducts.some(
|
||||||
|
( prevProduct ) =>
|
||||||
|
prevProduct.id === newProduct.id
|
||||||
|
)
|
||||||
);
|
);
|
||||||
} else if ( query?.tab === 'themes' ) {
|
|
||||||
params.append( 'category', 'themes' );
|
if ( newProducts.length > 0 ) {
|
||||||
} else if ( query?.tab === 'business-services' ) {
|
setFirstNewProductId( newProducts[ 0 ].id ?? 0 );
|
||||||
params.append( 'category', 'business-services' );
|
}
|
||||||
} else if ( query?.tab === 'search' ) {
|
|
||||||
params.append( 'category', 'extensions-themes-business-services' );
|
const combinedProducts = [
|
||||||
|
...flattenedPrevProducts,
|
||||||
|
...newProducts,
|
||||||
|
];
|
||||||
|
|
||||||
|
return combinedProducts;
|
||||||
|
} );
|
||||||
|
|
||||||
|
speak( __( 'More products loaded', 'woocommerce' ) );
|
||||||
|
setCurrentPage( ( prevPage ) => prevPage + 1 );
|
||||||
|
setIsLoadingMore( false );
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
speak( __( 'Error loading more products', 'woocommerce' ) );
|
||||||
|
} )
|
||||||
|
.finally( () => {
|
||||||
|
setIsLoadingMore( false );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
abortController.abort();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
currentPage,
|
||||||
|
query.category,
|
||||||
|
query.term,
|
||||||
|
query.tab,
|
||||||
|
setIsLoadingMore,
|
||||||
|
] );
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
// if it's a paginated request, don't use this effect
|
||||||
|
if ( currentPage > 1 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories: Array< {
|
||||||
|
category: keyof SearchResultsCountType;
|
||||||
|
type: ProductType;
|
||||||
|
} > = [
|
||||||
|
{ category: 'extensions', type: ProductType.extension },
|
||||||
|
{ category: 'themes', type: ProductType.theme },
|
||||||
|
{
|
||||||
|
category: 'business-services',
|
||||||
|
type: ProductType.businessService,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const abortControllers = categories.map( () => new AbortController() );
|
||||||
|
|
||||||
|
setIsLoading( true );
|
||||||
|
setAllProducts( [] );
|
||||||
|
|
||||||
|
// If query.category is present and not '_all', only fetch that category
|
||||||
|
if ( query.category && query.category !== '_all' ) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
params.append( 'category', query.category );
|
||||||
|
|
||||||
|
if ( query.term ) {
|
||||||
|
params.append( 'term', query.term );
|
||||||
}
|
}
|
||||||
|
|
||||||
const wccomSettings = getAdminSetting( 'wccomHelper', false );
|
const wccomSettings = getAdminSetting( 'wccomHelper', false );
|
||||||
|
@ -106,69 +191,192 @@ export default function Content(): JSX.Element {
|
||||||
params.append( 'country', wccomSettings.storeCountry );
|
params.append( 'country', wccomSettings.storeCountry );
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchSearchResults( params, abortController.signal )
|
fetchSearchResults( params, abortControllers[ 0 ].signal )
|
||||||
.then( ( productList ) => {
|
.then( ( productList ) => {
|
||||||
setProducts( productList );
|
setAllProducts( productList.products );
|
||||||
|
setTotalPagesCategory( productList.totalPages );
|
||||||
|
setSearchResultsCount( {
|
||||||
|
[ query.tab ]: productList.totalProducts,
|
||||||
|
} );
|
||||||
|
|
||||||
|
searchCompleteAnnouncement( productList.totalProducts );
|
||||||
} )
|
} )
|
||||||
.catch( () => {
|
.catch( () => {
|
||||||
setProducts( [] );
|
setAllProducts( [] );
|
||||||
} )
|
} )
|
||||||
.finally( () => {
|
.finally( () => {
|
||||||
// we are recording both the new and legacy events here for now
|
setIsLoading( false );
|
||||||
// they're separate methods to make it easier to remove the legacy one later
|
} );
|
||||||
|
} else {
|
||||||
|
// Fetch all tabs when query.term or query.category changes
|
||||||
|
Promise.all(
|
||||||
|
categories.map( ( { category, type }, index ) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if ( category !== 'extensions' ) {
|
||||||
|
params.append( 'category', category );
|
||||||
|
}
|
||||||
|
if ( query.term ) {
|
||||||
|
params.append( 'term', query.term );
|
||||||
|
}
|
||||||
|
|
||||||
|
const wccomSettings = getAdminSetting(
|
||||||
|
'wccomHelper',
|
||||||
|
false
|
||||||
|
);
|
||||||
|
if ( wccomSettings.storeCountry ) {
|
||||||
|
params.append( 'country', wccomSettings.storeCountry );
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchSearchResults(
|
||||||
|
params,
|
||||||
|
abortControllers[ index ].signal
|
||||||
|
).then( ( productList ) => {
|
||||||
|
const typedProducts = tagProductsWithType(
|
||||||
|
productList.products,
|
||||||
|
type
|
||||||
|
);
|
||||||
|
if ( category === 'business-services' ) {
|
||||||
|
setHasBusinessServices( typedProducts.length > 0 );
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
products: typedProducts,
|
||||||
|
totalPages: productList.totalPages,
|
||||||
|
totalProducts: productList.totalProducts,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
} )
|
||||||
|
)
|
||||||
|
.then( ( results ) => {
|
||||||
|
const combinedProducts = results.flatMap(
|
||||||
|
( result ) => result.products
|
||||||
|
);
|
||||||
|
|
||||||
|
setAllProducts( combinedProducts );
|
||||||
|
|
||||||
|
setSearchResultsCount( {
|
||||||
|
extensions: results.find(
|
||||||
|
( i ) => i.type === 'extension'
|
||||||
|
)?.totalProducts,
|
||||||
|
themes: results.find( ( i ) => i.type === 'theme' )
|
||||||
|
?.totalProducts,
|
||||||
|
'business-services': results.find(
|
||||||
|
( i ) => i.type === 'business-service'
|
||||||
|
)?.totalProducts,
|
||||||
|
} );
|
||||||
|
|
||||||
|
results.forEach( ( result ) => {
|
||||||
|
switch ( result.type ) {
|
||||||
|
case ProductType.extension:
|
||||||
|
setTotalPagesExtensions( result.totalPages );
|
||||||
|
break;
|
||||||
|
case ProductType.theme:
|
||||||
|
setTotalPagesThemes( result.totalPages );
|
||||||
|
break;
|
||||||
|
case ProductType.businessService:
|
||||||
|
setTotalPagesBusinessServices(
|
||||||
|
result.totalPages
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
searchCompleteAnnouncement(
|
||||||
|
results.reduce( ( acc, curr ) => {
|
||||||
|
return acc + curr.totalProducts;
|
||||||
|
}, 0 )
|
||||||
|
);
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
setAllProducts( [] );
|
||||||
|
} )
|
||||||
|
.finally( () => {
|
||||||
|
setIsLoading( false );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
abortControllers.forEach( ( controller ) => {
|
||||||
|
controller.abort();
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
query.tab,
|
||||||
|
query.term,
|
||||||
|
query.category,
|
||||||
|
setHasBusinessServices,
|
||||||
|
setIsLoading,
|
||||||
|
setSearchResultsCount,
|
||||||
|
currentPage,
|
||||||
|
] );
|
||||||
|
|
||||||
|
// Filter the products based on the selected tab
|
||||||
|
useEffect( () => {
|
||||||
|
let filtered: Product[] | null;
|
||||||
|
switch ( selectedTab ) {
|
||||||
|
case 'extensions':
|
||||||
|
filtered = allProducts.filter(
|
||||||
|
( p ) => p.type === ProductType.extension
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'themes':
|
||||||
|
filtered = allProducts.filter(
|
||||||
|
( p ) => p.type === ProductType.theme
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'business-services':
|
||||||
|
filtered = allProducts.filter(
|
||||||
|
( p ) => p.type === ProductType.businessService
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
filtered = [];
|
||||||
|
}
|
||||||
|
setFilteredProducts( filtered );
|
||||||
|
}, [ selectedTab, allProducts ] );
|
||||||
|
|
||||||
|
// Record tab view events when the query changes
|
||||||
|
useEffect( () => {
|
||||||
const marketplaceViewProps = {
|
const marketplaceViewProps = {
|
||||||
view: query?.tab,
|
view: query?.tab,
|
||||||
search_term: query?.term,
|
search_term: query?.term,
|
||||||
product_type: query?.section,
|
product_type: query?.section,
|
||||||
category: query?.category,
|
category: query?.category,
|
||||||
};
|
};
|
||||||
|
|
||||||
recordMarketplaceView( marketplaceViewProps );
|
recordMarketplaceView( marketplaceViewProps );
|
||||||
recordLegacyTabView( marketplaceViewProps );
|
recordLegacyTabView( marketplaceViewProps );
|
||||||
setIsLoading( false );
|
}, [ query?.tab, query?.term, query?.section, query?.category ] );
|
||||||
} );
|
|
||||||
return () => {
|
// Reset current page when tab, term, or category changes
|
||||||
abortController.abort();
|
useEffect( () => {
|
||||||
};
|
setCurrentPage( 1 );
|
||||||
}, [
|
setFirstNewProductId( 0 );
|
||||||
query.term,
|
}, [ selectedTab, query?.category, query?.term ] );
|
||||||
query.category,
|
|
||||||
query?.tab,
|
// Maintain product focus for accessibility
|
||||||
setIsLoading,
|
useEffect( () => {
|
||||||
query?.section,
|
if ( firstNewProductId ) {
|
||||||
] );
|
setTimeout( () => {
|
||||||
|
const firstNewProduct = document.getElementById(
|
||||||
|
`product-${ firstNewProductId }`
|
||||||
|
);
|
||||||
|
if ( firstNewProduct ) {
|
||||||
|
firstNewProduct.focus();
|
||||||
|
}
|
||||||
|
}, 0 );
|
||||||
|
}
|
||||||
|
}, [ firstNewProductId ] );
|
||||||
|
|
||||||
const renderContent = (): JSX.Element => {
|
const renderContent = (): JSX.Element => {
|
||||||
switch ( selectedTab ) {
|
switch ( selectedTab ) {
|
||||||
case 'extensions':
|
case 'extensions':
|
||||||
return (
|
|
||||||
<Products
|
|
||||||
products={ products }
|
|
||||||
categorySelector={ true }
|
|
||||||
type={ ProductType.extension }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'themes':
|
case 'themes':
|
||||||
return (
|
|
||||||
<Products
|
|
||||||
products={ products }
|
|
||||||
categorySelector={ true }
|
|
||||||
type={ ProductType.theme }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'business-services':
|
case 'business-services':
|
||||||
return (
|
return (
|
||||||
<Products
|
<Products
|
||||||
products={ products }
|
products={ filteredProducts }
|
||||||
categorySelector={ true }
|
categorySelector={ true }
|
||||||
type={ ProductType.businessService }
|
type={ getProductType( selectedTab ) }
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'search':
|
|
||||||
return (
|
|
||||||
<SearchResults
|
|
||||||
products={ products }
|
|
||||||
type={ SearchResultType.all }
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'discover':
|
case 'discover':
|
||||||
|
@ -184,10 +392,29 @@ export default function Content(): JSX.Element {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shouldShowLoadMoreButton = () => {
|
||||||
|
if ( ! query.category || query.category === '_all' ) {
|
||||||
|
// Check against total pages for the selected tab
|
||||||
|
switch ( selectedTab ) {
|
||||||
|
case 'extensions':
|
||||||
|
return currentPage < totalPagesExtensions;
|
||||||
|
case 'themes':
|
||||||
|
return currentPage < totalPagesThemes;
|
||||||
|
case 'business-services':
|
||||||
|
return currentPage < totalPagesBusinessServices;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check against totalPagesCategory for specific category
|
||||||
|
return currentPage < totalPagesCategory;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-marketplace__content">
|
<div className="woocommerce-marketplace__content">
|
||||||
<Promotions />
|
<Promotions />
|
||||||
<InstallNewProductModal products={ products } />
|
<InstallNewProductModal products={ filteredProducts } />
|
||||||
{ selectedTab !== 'business-services' &&
|
{ selectedTab !== 'business-services' &&
|
||||||
selectedTab !== 'my-subscriptions' && <ConnectNotice /> }
|
selectedTab !== 'my-subscriptions' && <ConnectNotice /> }
|
||||||
{ selectedTab !== 'business-services' && <PluginInstallNotice /> }
|
{ selectedTab !== 'business-services' && <PluginInstallNotice /> }
|
||||||
|
@ -197,11 +424,15 @@ export default function Content(): JSX.Element {
|
||||||
{ selectedTab !== 'business-services' && (
|
{ selectedTab !== 'business-services' && (
|
||||||
<SubscriptionsExpiredExpiringNotice type="expiring" />
|
<SubscriptionsExpiredExpiringNotice type="expiring" />
|
||||||
) }
|
) }
|
||||||
{ selectedTab !== 'business-services' && (
|
|
||||||
<SubscriptionsExpiredExpiringNotice type="missing" />
|
|
||||||
) }
|
|
||||||
|
|
||||||
{ renderContent() }
|
{ renderContent() }
|
||||||
|
{ ! isLoading && shouldShowLoadMoreButton() && (
|
||||||
|
<LoadMoreButton
|
||||||
|
onLoadMore={ loadMoreProducts }
|
||||||
|
isBusy={ isLoadingMore }
|
||||||
|
disabled={ isLoadingMore }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-bottom: 1px solid $gutenberg-gray-300;
|
border-bottom: 1px solid $gutenberg-gray-300;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
gap: $medium-gap;
|
||||||
grid-template: "mktpl-title mktpl-search mktpl-meta" 60px
|
grid-template: "mktpl-title mktpl-search mktpl-meta" 60px
|
||||||
"mktpl-tabs mktpl-tabs mktpl-tabs" auto / 1fr 320px 36px;
|
"mktpl-tabs mktpl-tabs mktpl-tabs" auto / 1fr 320px 36px;
|
||||||
padding: 0 $content-spacing-large;
|
padding: 0 $content-spacing-large;
|
||||||
|
@ -73,17 +74,3 @@
|
||||||
padding: 0 $content-spacing-small;
|
padding: 0 $content-spacing-small;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-marketplace__search {
|
|
||||||
margin-right: $medium-gap;
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
input[type="search"] {
|
|
||||||
all: unset;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width <= $breakpoint-medium) {
|
|
||||||
margin: $content-spacing-small;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { Button } from '@wordpress/components';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { speak } from '@wordpress/a11y';
|
||||||
|
import { queueRecordEvent } from '@woocommerce/tracks';
|
||||||
|
|
||||||
|
interface LoadMoreProps {
|
||||||
|
onLoadMore: () => void;
|
||||||
|
isBusy: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoadMoreButton( props: LoadMoreProps ) {
|
||||||
|
const { onLoadMore, isBusy, disabled } = props;
|
||||||
|
function handleClick() {
|
||||||
|
queueRecordEvent( 'marketplace_load_more_button_clicked', {} );
|
||||||
|
onLoadMore();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isBusy ) {
|
||||||
|
speak( __( 'Loading more products', 'woocommerce' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className="woocommerce-marketplace__load-more"
|
||||||
|
variant={ 'secondary' }
|
||||||
|
onClick={ handleClick }
|
||||||
|
isBusy={ isBusy }
|
||||||
|
disabled={ disabled }
|
||||||
|
>
|
||||||
|
{ __( 'Load more', 'woocommerce' ) }
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
|
@ -191,6 +191,8 @@ function ProductCard( props: ProductCardProps ): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={ classNames }
|
className={ classNames }
|
||||||
|
id={ `product-${ product.id }` }
|
||||||
|
tabIndex={ -1 }
|
||||||
aria-hidden={ isLoading }
|
aria-hidden={ isLoading }
|
||||||
style={ inlineCss() }
|
style={ inlineCss() }
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { useEffect, useState } from '@wordpress/element';
|
import { useEffect, useState } from '@wordpress/element';
|
||||||
import { useQuery } from '@woocommerce/navigation';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -22,8 +21,6 @@ export default function NoResults( props: {
|
||||||
} ): JSX.Element {
|
} ): JSX.Element {
|
||||||
const [ productGroups, setProductGroups ] = useState< ProductGroup[] >();
|
const [ productGroups, setProductGroups ] = useState< ProductGroup[] >();
|
||||||
const [ isLoading, setIsLoading ] = useState( false );
|
const [ isLoading, setIsLoading ] = useState( false );
|
||||||
const query = useQuery();
|
|
||||||
const showCategorySelector = query.tab === 'search' && query.section;
|
|
||||||
const productGroupsForSearchType = {
|
const productGroupsForSearchType = {
|
||||||
[ SearchResultType.all ]: [
|
[ SearchResultType.all ]: [
|
||||||
'most-popular',
|
'most-popular',
|
||||||
|
@ -123,10 +120,6 @@ export default function NoResults( props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function categorySelector() {
|
function categorySelector() {
|
||||||
if ( ! showCategorySelector ) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( props.type === SearchResultType.all ) {
|
if ( props.type === SearchResultType.all ) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
export type SearchAPIJSONType = {
|
export type SearchAPIJSONType = {
|
||||||
products: Array< SearchAPIProductType >;
|
products: Array< SearchAPIProductType >;
|
||||||
|
total_pages: number;
|
||||||
|
total_products: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SearchAPIProductType = {
|
export type SearchAPIProductType = {
|
||||||
|
|
|
@ -9,10 +9,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.woocommerce-marketplace__sub-header {
|
.woocommerce-marketplace__sub-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
.woocommerce-marketplace__customize-your-store-button {
|
justify-content: space-between;
|
||||||
margin: 16px 0 6px auto;
|
gap: 32px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.woocommerce-marketplace__sub-header__categories {
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-marketplace__customize-your-store-button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import {
|
import {
|
||||||
createInterpolateElement,
|
createInterpolateElement,
|
||||||
useContext,
|
useContext,
|
||||||
|
@ -24,7 +24,6 @@ import ProductListContent from '../product-list-content/product-list-content';
|
||||||
import ProductLoader from '../product-loader/product-loader';
|
import ProductLoader from '../product-loader/product-loader';
|
||||||
import NoResults from '../product-list-content/no-results';
|
import NoResults from '../product-list-content/no-results';
|
||||||
import { Product, ProductType, SearchResultType } from '../product-list/types';
|
import { Product, ProductType, SearchResultType } from '../product-list/types';
|
||||||
import { MARKETPLACE_ITEMS_PER_PAGE } from '../constants';
|
|
||||||
import { ADMIN_URL } from '~/utils/admin-settings';
|
import { ADMIN_URL } from '~/utils/admin-settings';
|
||||||
import { ThemeSwitchWarningModal } from '~/customize-store/intro/warning-modals';
|
import { ThemeSwitchWarningModal } from '~/customize-store/intro/warning-modals';
|
||||||
|
|
||||||
|
@ -54,12 +53,10 @@ const LABELS = {
|
||||||
|
|
||||||
export default function Products( props: ProductsProps ) {
|
export default function Products( props: ProductsProps ) {
|
||||||
const marketplaceContextValue = useContext( MarketplaceContext );
|
const marketplaceContextValue = useContext( MarketplaceContext );
|
||||||
const { isLoading, selectedTab } = marketplaceContextValue;
|
const { isLoading } = marketplaceContextValue;
|
||||||
const label = LABELS[ props.type ].label;
|
const label = LABELS[ props.type ].label;
|
||||||
const singularLabel = LABELS[ props.type ].singularLabel;
|
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
const category = query?.category;
|
const category = query?.category;
|
||||||
const perPage = props.perPage ?? MARKETPLACE_ITEMS_PER_PAGE;
|
|
||||||
interface Theme {
|
interface Theme {
|
||||||
stylesheet?: string;
|
stylesheet?: string;
|
||||||
}
|
}
|
||||||
|
@ -94,42 +91,30 @@ export default function Products( props: ProductsProps ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the total number of products before we slice it later.
|
// Store the total number of products before we slice it later.
|
||||||
const productTotalCount = props.products?.length ?? 0;
|
const products = props.products ?? [];
|
||||||
const products = props.products?.slice( 0, perPage ) ?? [];
|
|
||||||
|
|
||||||
let title = sprintf(
|
|
||||||
// translators: %s: plural item type (e.g. extensions, themes)
|
|
||||||
__( '0 %s found', 'woocommerce' ),
|
|
||||||
label
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( productTotalCount > 0 ) {
|
|
||||||
title = sprintf(
|
|
||||||
// translators: %1$s: number of items, %2$s: singular item label, %3$s: plural item label
|
|
||||||
_n( '%1$s %2$s', '%1$s %3$s', productTotalCount, 'woocommerce' ),
|
|
||||||
productTotalCount,
|
|
||||||
singularLabel,
|
|
||||||
label
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelForClassName =
|
const labelForClassName =
|
||||||
label === 'business services' ? 'business-services' : label;
|
label === 'business services' ? 'business-services' : label;
|
||||||
|
|
||||||
const baseContainerClass = 'woocommerce-marketplace__search-';
|
const baseContainerClass = 'woocommerce-marketplace__search-';
|
||||||
const baseProductListTitleClass = 'product-list-title--';
|
|
||||||
|
|
||||||
const containerClassName = clsx( baseContainerClass + labelForClassName );
|
const containerClassName = clsx( baseContainerClass + labelForClassName );
|
||||||
const productListTitleClassName = clsx(
|
|
||||||
'woocommerce-marketplace__product-list-title',
|
|
||||||
baseContainerClass + baseProductListTitleClass + labelForClassName,
|
|
||||||
{ 'is-loading': isLoading }
|
|
||||||
);
|
|
||||||
const viewAllButonClassName = clsx(
|
const viewAllButonClassName = clsx(
|
||||||
'woocommerce-marketplace__view-all-button',
|
'woocommerce-marketplace__view-all-button',
|
||||||
baseContainerClass + 'button-' + labelForClassName
|
baseContainerClass + 'button-' + labelForClassName
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ( isLoading ) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ props.categorySelector && (
|
||||||
|
<CategorySelector type={ props.type } />
|
||||||
|
) }
|
||||||
|
<ProductLoader hasTitle={ false } type={ props.type } />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ( products.length === 0 ) {
|
if ( products.length === 0 ) {
|
||||||
let type = SearchResultType.all;
|
let type = SearchResultType.all;
|
||||||
|
|
||||||
|
@ -154,28 +139,14 @@ export default function Products( props: ProductsProps ) {
|
||||||
: ''
|
: ''
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( isLoading ) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{ props.categorySelector && (
|
|
||||||
<CategorySelector type={ props.type } />
|
|
||||||
) }
|
|
||||||
<ProductLoader hasTitle={ false } type={ props.type } />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ containerClassName }>
|
<div className={ containerClassName }>
|
||||||
{ selectedTab === 'search' && (
|
<nav className="woocommerce-marketplace__sub-header">
|
||||||
<h2 className={ productListTitleClassName }>
|
<div className="woocommerce-marketplace__sub-header__categories">
|
||||||
{ isLoading ? ' ' : title }
|
|
||||||
</h2>
|
|
||||||
) }
|
|
||||||
<div className="woocommerce-marketplace__sub-header">
|
|
||||||
{ props.categorySelector && (
|
{ props.categorySelector && (
|
||||||
<CategorySelector type={ props.type } />
|
<CategorySelector type={ props.type } />
|
||||||
) }
|
) }
|
||||||
|
</div>
|
||||||
{ props.type === 'theme' && (
|
{ props.type === 'theme' && (
|
||||||
<Button
|
<Button
|
||||||
className="woocommerce-marketplace__customize-your-store-button"
|
className="woocommerce-marketplace__customize-your-store-button"
|
||||||
|
@ -192,7 +163,7 @@ export default function Products( props: ProductsProps ) {
|
||||||
} }
|
} }
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
</div>
|
</nav>
|
||||||
{ isModalOpen && (
|
{ isModalOpen && (
|
||||||
<ThemeSwitchWarningModal
|
<ThemeSwitchWarningModal
|
||||||
setIsModalOpen={ setIsModalOpen }
|
setIsModalOpen={ setIsModalOpen }
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
@import "../../stylesheets/_variables.scss";
|
|
||||||
|
|
||||||
.woocommerce-marketplace__search-results {
|
|
||||||
.woocommerce-marketplace {
|
|
||||||
&__view-all-button {
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.woocommerce-marketplace__product-list-content--collapsed {
|
|
||||||
.woocommerce-marketplace__product-card {
|
|
||||||
&:nth-child(n+7) {
|
|
||||||
display: none;
|
|
||||||
@media screen and (min-width: $breakpoint-huge) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,193 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { useQuery } from '@woocommerce/navigation';
|
|
||||||
import { useContext } from '@wordpress/element';
|
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import './search-results.scss';
|
|
||||||
import { Product, ProductType, SearchResultType } from '../product-list/types';
|
|
||||||
import Products from '../products/products';
|
|
||||||
import NoResults from '../product-list-content/no-results';
|
|
||||||
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
|
||||||
import {
|
|
||||||
MARKETPLACE_ITEMS_PER_PAGE,
|
|
||||||
MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
|
|
||||||
} from '../../../marketplace/components/constants';
|
|
||||||
|
|
||||||
export interface SearchResultProps {
|
|
||||||
products: Product[];
|
|
||||||
type: SearchResultType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SearchResults( props: SearchResultProps ): JSX.Element {
|
|
||||||
const extensionList = props.products.filter(
|
|
||||||
( product ) => product.type === ProductType.extension
|
|
||||||
);
|
|
||||||
const themeList = props.products.filter(
|
|
||||||
( product ) => product.type === ProductType.theme
|
|
||||||
);
|
|
||||||
const businessServiceList = props.products.filter(
|
|
||||||
( product ) => product.type === ProductType.businessService
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasExtensions = extensionList.length > 0;
|
|
||||||
const hasThemes = themeList.length > 0;
|
|
||||||
const hasBusinessServices = businessServiceList.length > 0;
|
|
||||||
const hasOnlyExtensions =
|
|
||||||
hasExtensions && ! hasThemes && ! hasBusinessServices;
|
|
||||||
const hasOnlyThemes = hasThemes && ! hasExtensions && ! hasBusinessServices;
|
|
||||||
const hasOnlyBusinessServices =
|
|
||||||
hasBusinessServices && ! hasExtensions && ! hasThemes;
|
|
||||||
|
|
||||||
const marketplaceContextValue = useContext( MarketplaceContext );
|
|
||||||
const { isLoading, hasBusinessServices: canShowBusinessServices } =
|
|
||||||
marketplaceContextValue;
|
|
||||||
|
|
||||||
const query = useQuery();
|
|
||||||
const showCategorySelector = query.section ? true : false;
|
|
||||||
const searchTerm = query.term ? query.term : '';
|
|
||||||
|
|
||||||
type Overrides = {
|
|
||||||
categorySelector?: boolean;
|
|
||||||
showAllButton?: boolean;
|
|
||||||
perPage?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
function productsComponent(
|
|
||||||
products: Product[],
|
|
||||||
type: ProductType,
|
|
||||||
overrides: Overrides = {}
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<Products
|
|
||||||
products={ products }
|
|
||||||
type={ type }
|
|
||||||
categorySelector={
|
|
||||||
overrides.categorySelector ?? showCategorySelector
|
|
||||||
}
|
|
||||||
searchTerm={ searchTerm }
|
|
||||||
showAllButton={ overrides.showAllButton ?? true }
|
|
||||||
perPage={ overrides.perPage ?? MARKETPLACE_ITEMS_PER_PAGE }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function extensionsComponent( overrides: Overrides = {} ) {
|
|
||||||
return productsComponent(
|
|
||||||
extensionList,
|
|
||||||
ProductType.extension,
|
|
||||||
overrides
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function themesComponent( overrides: Overrides = {} ) {
|
|
||||||
return productsComponent( themeList, ProductType.theme, overrides );
|
|
||||||
}
|
|
||||||
|
|
||||||
function businessServicesComponent( overrides: Overrides = {} ) {
|
|
||||||
return productsComponent(
|
|
||||||
businessServiceList,
|
|
||||||
ProductType.businessService,
|
|
||||||
overrides
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = () => {
|
|
||||||
if ( query?.section === SearchResultType.extension ) {
|
|
||||||
return extensionsComponent( { showAllButton: false } );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( query?.section === SearchResultType.theme ) {
|
|
||||||
return themesComponent( { showAllButton: false } );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( query?.section === SearchResultType.businessService ) {
|
|
||||||
return businessServicesComponent( { showAllButton: false } );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Components can handle their isLoading state. So we can put all three on the page.
|
|
||||||
if ( isLoading ) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{ extensionsComponent() }
|
|
||||||
{ themesComponent() }
|
|
||||||
{ businessServicesComponent() }
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we did finish loading items, and there are no results, show the no results component.
|
|
||||||
if (
|
|
||||||
! isLoading &&
|
|
||||||
! hasExtensions &&
|
|
||||||
! hasThemes &&
|
|
||||||
! hasBusinessServices
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<NoResults
|
|
||||||
type={ SearchResultType.all }
|
|
||||||
showHeading={ true }
|
|
||||||
heading={
|
|
||||||
canShowBusinessServices
|
|
||||||
? __(
|
|
||||||
'No extensions, themes or business services found…',
|
|
||||||
'woocommerce'
|
|
||||||
)
|
|
||||||
: __(
|
|
||||||
'No extensions or themes found…',
|
|
||||||
'woocommerce'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're done loading, we can put these components on the page.
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{ hasExtensions
|
|
||||||
? extensionsComponent( {
|
|
||||||
categorySelector: hasOnlyExtensions || undefined,
|
|
||||||
showAllButton: hasOnlyExtensions
|
|
||||||
? false
|
|
||||||
: undefined,
|
|
||||||
perPage: hasOnlyExtensions
|
|
||||||
? MARKETPLACE_ITEMS_PER_PAGE
|
|
||||||
: MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
|
|
||||||
} )
|
|
||||||
: null }
|
|
||||||
{ hasThemes
|
|
||||||
? themesComponent( {
|
|
||||||
categorySelector: hasOnlyThemes || undefined,
|
|
||||||
showAllButton: hasOnlyThemes ? false : undefined,
|
|
||||||
perPage: hasOnlyThemes
|
|
||||||
? MARKETPLACE_ITEMS_PER_PAGE
|
|
||||||
: MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
|
|
||||||
} )
|
|
||||||
: null }
|
|
||||||
{ hasBusinessServices
|
|
||||||
? businessServicesComponent( {
|
|
||||||
categorySelector:
|
|
||||||
hasOnlyBusinessServices || undefined,
|
|
||||||
showAllButton: hasOnlyBusinessServices
|
|
||||||
? false
|
|
||||||
: undefined,
|
|
||||||
perPage: hasOnlyBusinessServices
|
|
||||||
? MARKETPLACE_ITEMS_PER_PAGE
|
|
||||||
: MARKETPLACE_SEARCH_RESULTS_PER_PAGE,
|
|
||||||
} )
|
|
||||||
: null }
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="woocommerce-marketplace__search-results">
|
|
||||||
{ content() }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -2,29 +2,15 @@
|
||||||
|
|
||||||
.woocommerce-marketplace__search {
|
.woocommerce-marketplace__search {
|
||||||
grid-area: mktpl-search;
|
grid-area: mktpl-search;
|
||||||
background: $gutenberg-gray-100;
|
margin-top: 15px;
|
||||||
border: 1.5px solid transparent;
|
width: 320px;
|
||||||
border-radius: 2px;
|
|
||||||
display: flex;
|
|
||||||
height: 40px;
|
|
||||||
padding: 4px 8px 4px 12px;
|
|
||||||
|
|
||||||
input[type="search"] {
|
|
||||||
all: unset;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
background: #fff;
|
|
||||||
border-color: var(--wp-admin-theme-color, #3858e9);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width <= $breakpoint-medium) {
|
@media (width <= $breakpoint-medium) {
|
||||||
margin: $grid-unit-20 $grid-unit-20 $grid-unit-10 $grid-unit-20;
|
margin: $grid-unit-20 $grid-unit-20 $grid-unit-10 $grid-unit-20;
|
||||||
|
width: calc(100% - $grid-unit-20 * 2);
|
||||||
|
|
||||||
|
.components-input-control__input {
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-marketplace__search-button {
|
|
||||||
all: unset;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,26 +2,20 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { Icon, search } from '@wordpress/icons';
|
import { useEffect, useState } from '@wordpress/element';
|
||||||
import { useContext, useEffect, useState } from '@wordpress/element';
|
|
||||||
import { navigateTo, getNewPath, useQuery } from '@woocommerce/navigation';
|
import { navigateTo, getNewPath, useQuery } from '@woocommerce/navigation';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line @woocommerce/dependency-group
|
||||||
|
import { SearchControl } from '@wordpress/components';
|
||||||
|
// The @ts-ignore is needed because the SearchControl types are not exported from the @wordpress/components package,
|
||||||
|
// even though the component itself is. This is likely due to an older version of the package being used.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import './search.scss';
|
import './search.scss';
|
||||||
import { MARKETPLACE_PATH } from '../constants';
|
import { MARKETPLACE_PATH } from '../constants';
|
||||||
import { MarketplaceContext } from '../../contexts/marketplace-context';
|
|
||||||
|
|
||||||
const searchPlaceholder = __(
|
|
||||||
'Search for extensions, themes, and business services',
|
|
||||||
'woocommerce'
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchPlaceholderNoBusinessServices = __(
|
|
||||||
'Search for extensions and themes',
|
|
||||||
'woocommerce'
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search component.
|
* Search component.
|
||||||
|
@ -30,14 +24,10 @@ const searchPlaceholderNoBusinessServices = __(
|
||||||
*/
|
*/
|
||||||
function Search(): JSX.Element {
|
function Search(): JSX.Element {
|
||||||
const [ searchTerm, setSearchTerm ] = useState( '' );
|
const [ searchTerm, setSearchTerm ] = useState( '' );
|
||||||
const { hasBusinessServices } = useContext( MarketplaceContext );
|
const searchPlaceholder = __( 'Search Marketplace', 'woocommerce' );
|
||||||
|
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
|
|
||||||
const placeholder = hasBusinessServices
|
|
||||||
? searchPlaceholder
|
|
||||||
: searchPlaceholderNoBusinessServices;
|
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
if ( query.term ) {
|
if ( query.term ) {
|
||||||
setSearchTerm( query.term );
|
setSearchTerm( query.term );
|
||||||
|
@ -46,21 +36,16 @@ function Search(): JSX.Element {
|
||||||
}
|
}
|
||||||
}, [ query.term ] );
|
}, [ query.term ] );
|
||||||
|
|
||||||
useEffect( () => {
|
|
||||||
if ( query.tab !== 'search' ) {
|
|
||||||
setSearchTerm( '' );
|
|
||||||
}
|
|
||||||
}, [ query.tab ] );
|
|
||||||
|
|
||||||
const runSearch = () => {
|
const runSearch = () => {
|
||||||
const term = searchTerm.trim();
|
const newQuery: { term?: string; tab?: string } = query;
|
||||||
|
|
||||||
const newQuery: { term?: string; tab?: string } = {};
|
// If we're on 'Discover' or 'My subscriptions' when a search is initiated, move to the extensions tab
|
||||||
if ( term !== '' ) {
|
if ( ! newQuery.tab || newQuery.tab === 'my-subscriptions' ) {
|
||||||
newQuery.term = term;
|
newQuery.tab = 'extensions';
|
||||||
newQuery.tab = 'search';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newQuery.term = searchTerm.trim();
|
||||||
|
|
||||||
// When the search term changes, we reset the query string on purpose.
|
// When the search term changes, we reset the query string on purpose.
|
||||||
navigateTo( {
|
navigateTo( {
|
||||||
url: getNewPath( newQuery, MARKETPLACE_PATH, {} ),
|
url: getNewPath( newQuery, MARKETPLACE_PATH, {} ),
|
||||||
|
@ -69,12 +54,6 @@ function Search(): JSX.Element {
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputChange = (
|
|
||||||
event: React.ChangeEvent< HTMLInputElement >
|
|
||||||
) => {
|
|
||||||
setSearchTerm( event.target.value );
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyUp = ( event: { key: string } ) => {
|
const handleKeyUp = ( event: { key: string } ) => {
|
||||||
if ( event.key === 'Enter' ) {
|
if ( event.key === 'Enter' ) {
|
||||||
runSearch();
|
runSearch();
|
||||||
|
@ -86,32 +65,14 @@ function Search(): JSX.Element {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-marketplace__search">
|
<SearchControl
|
||||||
<label
|
label={ searchPlaceholder }
|
||||||
className="screen-reader-text"
|
placeholder={ searchPlaceholder }
|
||||||
htmlFor="woocommerce-marketplace-search-query"
|
|
||||||
>
|
|
||||||
{ placeholder }
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="woocommerce-marketplace-search-query"
|
|
||||||
value={ searchTerm }
|
value={ searchTerm }
|
||||||
className="woocommerce-marketplace__search-input"
|
onChange={ setSearchTerm }
|
||||||
type="search"
|
|
||||||
name="woocommerce-marketplace-search-query"
|
|
||||||
placeholder={ placeholder }
|
|
||||||
onChange={ handleInputChange }
|
|
||||||
onKeyUp={ handleKeyUp }
|
onKeyUp={ handleKeyUp }
|
||||||
|
className="woocommerce-marketplace__search"
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
id="woocommerce-marketplace-search-button"
|
|
||||||
className="woocommerce-marketplace__search-button"
|
|
||||||
aria-label={ __( 'Search', 'woocommerce' ) }
|
|
||||||
onClick={ runSearch }
|
|
||||||
>
|
|
||||||
<Icon icon={ search } size={ 32 } />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,18 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
z-index: 26;
|
z-index: 26;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__update-count-extensions,
|
||||||
|
&__update-count-themes,
|
||||||
|
&__update-count-business-services {
|
||||||
|
background-color: $gutenberg-gray-300;
|
||||||
|
color: $gutenberg-gray-700;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (width <= $breakpoint-medium) {
|
@media (width <= $breakpoint-medium) {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { useContext, useEffect, useState } from '@wordpress/element';
|
import { useContext, useEffect, useState, useMemo } from '@wordpress/element';
|
||||||
import { Button } from '@wordpress/components';
|
import { Button } from '@wordpress/components';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
|
import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
|
||||||
|
@ -35,63 +35,26 @@ interface Tabs {
|
||||||
const wccomSettings = getAdminSetting( 'wccomHelper', {} );
|
const wccomSettings = getAdminSetting( 'wccomHelper', {} );
|
||||||
const wooUpdateCount = wccomSettings?.wooUpdateCount ?? 0;
|
const wooUpdateCount = wccomSettings?.wooUpdateCount ?? 0;
|
||||||
|
|
||||||
const tabs: Tabs = {
|
const setUrlTabParam = ( tabKey: string, query: Record< string, string > ) => {
|
||||||
search: {
|
const term = query.term ? { term: query.term.trim() } : {};
|
||||||
name: 'search',
|
|
||||||
title: __( 'Search results', 'woocommerce' ),
|
|
||||||
showUpdateCount: false,
|
|
||||||
updateCount: 0,
|
|
||||||
},
|
|
||||||
discover: {
|
|
||||||
name: 'discover',
|
|
||||||
title: __( 'Discover', 'woocommerce' ),
|
|
||||||
showUpdateCount: false,
|
|
||||||
updateCount: 0,
|
|
||||||
},
|
|
||||||
extensions: {
|
|
||||||
name: 'extensions',
|
|
||||||
title: __( 'Extensions', 'woocommerce' ),
|
|
||||||
showUpdateCount: false,
|
|
||||||
updateCount: 0,
|
|
||||||
},
|
|
||||||
themes: {
|
|
||||||
name: 'themes',
|
|
||||||
title: __( 'Themes', 'woocommerce' ),
|
|
||||||
showUpdateCount: false,
|
|
||||||
updateCount: 0,
|
|
||||||
},
|
|
||||||
'business-services': {
|
|
||||||
name: 'business-services',
|
|
||||||
title: __( 'Business services', 'woocommerce' ),
|
|
||||||
showUpdateCount: false,
|
|
||||||
updateCount: 0,
|
|
||||||
},
|
|
||||||
'my-subscriptions': {
|
|
||||||
name: 'my-subscriptions',
|
|
||||||
title: __( 'My subscriptions', 'woocommerce' ),
|
|
||||||
showUpdateCount: true,
|
|
||||||
updateCount: wooUpdateCount,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const setUrlTabParam = ( tabKey: string ) => {
|
|
||||||
navigateTo( {
|
navigateTo( {
|
||||||
url: getNewPath(
|
url: getNewPath(
|
||||||
{ tab: tabKey === DEFAULT_TAB_KEY ? undefined : tabKey },
|
{ tab: tabKey === DEFAULT_TAB_KEY ? undefined : tabKey },
|
||||||
MARKETPLACE_PATH,
|
MARKETPLACE_PATH,
|
||||||
{}
|
term
|
||||||
),
|
),
|
||||||
} );
|
} );
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVisibleTabs = ( selectedTab: string, hasBusinessServices = false ) => {
|
const getVisibleTabs = (
|
||||||
|
selectedTab: string,
|
||||||
|
hasBusinessServices = false,
|
||||||
|
tabs: Tabs
|
||||||
|
) => {
|
||||||
if ( selectedTab === '' ) {
|
if ( selectedTab === '' ) {
|
||||||
return tabs;
|
return tabs;
|
||||||
}
|
}
|
||||||
const currentVisibleTabs = { ...tabs };
|
const currentVisibleTabs = { ...tabs };
|
||||||
if ( selectedTab !== 'search' ) {
|
|
||||||
delete currentVisibleTabs.search;
|
|
||||||
}
|
|
||||||
if ( ! hasBusinessServices ) {
|
if ( ! hasBusinessServices ) {
|
||||||
delete currentVisibleTabs[ 'business-services' ];
|
delete currentVisibleTabs[ 'business-services' ];
|
||||||
}
|
}
|
||||||
|
@ -101,7 +64,9 @@ const getVisibleTabs = ( selectedTab: string, hasBusinessServices = false ) => {
|
||||||
|
|
||||||
const renderTabs = (
|
const renderTabs = (
|
||||||
marketplaceContextValue: MarketplaceContextType,
|
marketplaceContextValue: MarketplaceContextType,
|
||||||
visibleTabs: Tabs
|
visibleTabs: Tabs,
|
||||||
|
tabs: Tabs,
|
||||||
|
query: Record< string, string >
|
||||||
) => {
|
) => {
|
||||||
const { selectedTab, setSelectedTab } = marketplaceContextValue;
|
const { selectedTab, setSelectedTab } = marketplaceContextValue;
|
||||||
|
|
||||||
|
@ -110,7 +75,7 @@ const renderTabs = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedTab( tabKey );
|
setSelectedTab( tabKey );
|
||||||
setUrlTabParam( tabKey );
|
setUrlTabParam( tabKey, query );
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabContent = [];
|
const tabContent = [];
|
||||||
|
@ -143,7 +108,15 @@ const renderTabs = (
|
||||||
{ tabs[ tabKey ]?.title }
|
{ tabs[ tabKey ]?.title }
|
||||||
{ tabs[ tabKey ]?.showUpdateCount &&
|
{ tabs[ tabKey ]?.showUpdateCount &&
|
||||||
tabs[ tabKey ]?.updateCount > 0 && (
|
tabs[ tabKey ]?.updateCount > 0 && (
|
||||||
<span className="woocommerce-marketplace__update-count">
|
<span
|
||||||
|
className={ clsx(
|
||||||
|
'woocommerce-marketplace__update-count',
|
||||||
|
`woocommerce-marketplace__update-count-${ tabKey }`,
|
||||||
|
{
|
||||||
|
'is-active': tabKey === selectedTab,
|
||||||
|
}
|
||||||
|
) }
|
||||||
|
>
|
||||||
<span> { tabs[ tabKey ]?.updateCount } </span>
|
<span> { tabs[ tabKey ]?.updateCount } </span>
|
||||||
</span>
|
</span>
|
||||||
) }
|
) }
|
||||||
|
@ -157,23 +130,70 @@ const renderTabs = (
|
||||||
const Tabs = ( props: TabsProps ): JSX.Element => {
|
const Tabs = ( props: TabsProps ): JSX.Element => {
|
||||||
const { additionalClassNames } = props;
|
const { additionalClassNames } = props;
|
||||||
const marketplaceContextValue = useContext( MarketplaceContext );
|
const marketplaceContextValue = useContext( MarketplaceContext );
|
||||||
const { selectedTab, setSelectedTab, hasBusinessServices } =
|
const { selectedTab, isLoading, setSelectedTab, hasBusinessServices } =
|
||||||
marketplaceContextValue;
|
marketplaceContextValue;
|
||||||
const [ visibleTabs, setVisibleTabs ] = useState( getVisibleTabs( '' ) );
|
const { searchResultsCount } = marketplaceContextValue;
|
||||||
|
|
||||||
const query: Record< string, string > = useQuery();
|
const query: Record< string, string > = useQuery();
|
||||||
|
|
||||||
|
const tabs: Tabs = useMemo(
|
||||||
|
() => ( {
|
||||||
|
discover: {
|
||||||
|
name: 'discover',
|
||||||
|
title: __( 'Discover', 'woocommerce' ),
|
||||||
|
showUpdateCount: false,
|
||||||
|
updateCount: 0,
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
name: 'extensions',
|
||||||
|
title: __( 'Extensions', 'woocommerce' ),
|
||||||
|
showUpdateCount: !! query.term && ! isLoading,
|
||||||
|
updateCount: searchResultsCount.extensions,
|
||||||
|
},
|
||||||
|
themes: {
|
||||||
|
name: 'themes',
|
||||||
|
title: __( 'Themes', 'woocommerce' ),
|
||||||
|
showUpdateCount: !! query.term && ! isLoading,
|
||||||
|
updateCount: searchResultsCount.themes,
|
||||||
|
},
|
||||||
|
'business-services': {
|
||||||
|
name: 'business-services',
|
||||||
|
title: __( 'Business services', 'woocommerce' ),
|
||||||
|
showUpdateCount: !! query.term && ! isLoading,
|
||||||
|
updateCount: searchResultsCount[ 'business-services' ],
|
||||||
|
},
|
||||||
|
'my-subscriptions': {
|
||||||
|
name: 'my-subscriptions',
|
||||||
|
title: __( 'My subscriptions', 'woocommerce' ),
|
||||||
|
showUpdateCount: true,
|
||||||
|
updateCount: wooUpdateCount,
|
||||||
|
},
|
||||||
|
} ),
|
||||||
|
[ query, isLoading, searchResultsCount ]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [ visibleTabs, setVisibleTabs ] = useState(
|
||||||
|
getVisibleTabs( '', false, tabs )
|
||||||
|
);
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
if ( query?.tab && tabs[ query.tab ] ) {
|
if ( query?.tab && tabs[ query.tab ] ) {
|
||||||
setSelectedTab( query.tab );
|
setSelectedTab( query.tab );
|
||||||
} else if ( Object.keys( query ).length > 0 ) {
|
} else if ( Object.keys( query ).length > 0 ) {
|
||||||
setSelectedTab( DEFAULT_TAB_KEY );
|
setSelectedTab( DEFAULT_TAB_KEY );
|
||||||
}
|
}
|
||||||
}, [ query, setSelectedTab ] );
|
}, [ query, setSelectedTab, tabs ] );
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
setVisibleTabs( getVisibleTabs( selectedTab, hasBusinessServices ) );
|
setVisibleTabs(
|
||||||
}, [ selectedTab, hasBusinessServices ] );
|
getVisibleTabs( selectedTab, hasBusinessServices, tabs )
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( selectedTab === 'business-services' && ! hasBusinessServices ) {
|
||||||
|
setUrlTabParam( 'extensions', query );
|
||||||
|
}
|
||||||
|
}, [ selectedTab, hasBusinessServices, query, tabs ] );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className={ clsx(
|
className={ clsx(
|
||||||
|
@ -181,7 +201,7 @@ const Tabs = ( props: TabsProps ): JSX.Element => {
|
||||||
additionalClassNames || []
|
additionalClassNames || []
|
||||||
) }
|
) }
|
||||||
>
|
>
|
||||||
{ renderTabs( marketplaceContextValue, visibleTabs ) }
|
{ renderTabs( marketplaceContextValue, visibleTabs, tabs, query ) }
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect, createContext } from '@wordpress/element';
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
createContext,
|
||||||
|
} from '@wordpress/element';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { MarketplaceContextType } from './types';
|
import { SearchResultsCountType, MarketplaceContextType } from './types';
|
||||||
import { getAdminSetting } from '../../utils/admin-settings';
|
import { getAdminSetting } from '../../utils/admin-settings';
|
||||||
|
|
||||||
export const MarketplaceContext = createContext< MarketplaceContextType >( {
|
export const MarketplaceContext = createContext< MarketplaceContextType >( {
|
||||||
|
@ -18,6 +23,12 @@ export const MarketplaceContext = createContext< MarketplaceContextType >( {
|
||||||
addInstalledProduct: () => {},
|
addInstalledProduct: () => {},
|
||||||
hasBusinessServices: false,
|
hasBusinessServices: false,
|
||||||
setHasBusinessServices: () => {},
|
setHasBusinessServices: () => {},
|
||||||
|
searchResultsCount: {
|
||||||
|
extensions: 0,
|
||||||
|
themes: 0,
|
||||||
|
'business-services': 0,
|
||||||
|
},
|
||||||
|
setSearchResultsCount: () => {},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
export function MarketplaceContextProvider( props: {
|
export function MarketplaceContextProvider( props: {
|
||||||
|
@ -29,6 +40,22 @@ export function MarketplaceContextProvider( props: {
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const [ hasBusinessServices, setHasBusinessServices ] = useState( false );
|
const [ hasBusinessServices, setHasBusinessServices ] = useState( false );
|
||||||
|
const [ searchResultsCount, setSearchResultsCountState ] =
|
||||||
|
useState< SearchResultsCountType >( {
|
||||||
|
extensions: 0,
|
||||||
|
themes: 0,
|
||||||
|
'business-services': 0,
|
||||||
|
} );
|
||||||
|
|
||||||
|
const setSearchResultsCount = useCallback(
|
||||||
|
( updatedCounts: Partial< SearchResultsCountType > ) => {
|
||||||
|
setSearchResultsCountState( ( prev ) => ( {
|
||||||
|
...prev,
|
||||||
|
...updatedCounts,
|
||||||
|
} ) );
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Knowing installed products will help us to determine which products
|
* Knowing installed products will help us to determine which products
|
||||||
|
@ -59,6 +86,8 @@ export function MarketplaceContextProvider( props: {
|
||||||
addInstalledProduct,
|
addInstalledProduct,
|
||||||
hasBusinessServices,
|
hasBusinessServices,
|
||||||
setHasBusinessServices,
|
setHasBusinessServices,
|
||||||
|
searchResultsCount,
|
||||||
|
setSearchResultsCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -8,6 +8,12 @@ import { Options } from '@wordpress/notices';
|
||||||
*/
|
*/
|
||||||
import { Subscription } from '../components/my-subscriptions/types';
|
import { Subscription } from '../components/my-subscriptions/types';
|
||||||
|
|
||||||
|
export interface SearchResultsCountType {
|
||||||
|
extensions: number;
|
||||||
|
themes: number;
|
||||||
|
'business-services': number;
|
||||||
|
}
|
||||||
|
|
||||||
export type MarketplaceContextType = {
|
export type MarketplaceContextType = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
setIsLoading: ( isLoading: boolean ) => void;
|
setIsLoading: ( isLoading: boolean ) => void;
|
||||||
|
@ -17,6 +23,10 @@ export type MarketplaceContextType = {
|
||||||
addInstalledProduct: ( slug: string ) => void;
|
addInstalledProduct: ( slug: string ) => void;
|
||||||
hasBusinessServices: boolean;
|
hasBusinessServices: boolean;
|
||||||
setHasBusinessServices: ( hasBusinessServices: boolean ) => void;
|
setHasBusinessServices: ( hasBusinessServices: boolean ) => void;
|
||||||
|
searchResultsCount: SearchResultsCountType;
|
||||||
|
setSearchResultsCount: (
|
||||||
|
updatedCounts: Partial< SearchResultsCountType >
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SubscriptionsContextType = {
|
export type SubscriptionsContextType = {
|
||||||
|
|
|
@ -107,7 +107,11 @@ async function fetchJsonWithCache(
|
||||||
async function fetchSearchResults(
|
async function fetchSearchResults(
|
||||||
params: URLSearchParams,
|
params: URLSearchParams,
|
||||||
abortSignal?: AbortSignal
|
abortSignal?: AbortSignal
|
||||||
): Promise< Product[] > {
|
): Promise< {
|
||||||
|
products: Product[];
|
||||||
|
totalPages: number;
|
||||||
|
totalProducts: number;
|
||||||
|
} > {
|
||||||
const url =
|
const url =
|
||||||
MARKETPLACE_HOST +
|
MARKETPLACE_HOST +
|
||||||
MARKETPLACE_SEARCH_API_PATH +
|
MARKETPLACE_SEARCH_API_PATH +
|
||||||
|
@ -151,9 +155,12 @@ async function fetchSearchResults(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
resolve( products );
|
const totalPages = ( json as SearchAPIJSONType ).total_pages;
|
||||||
|
const totalProducts = ( json as SearchAPIJSONType )
|
||||||
|
.total_products;
|
||||||
|
resolve( { products, totalPages, totalProducts } );
|
||||||
} )
|
} )
|
||||||
.catch( () => reject );
|
.catch( reject );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,6 +181,17 @@ async function fetchDiscoverPageData(): Promise< ProductGroup[] > {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProductType( tab: string ): ProductType {
|
||||||
|
switch ( tab ) {
|
||||||
|
case 'themes':
|
||||||
|
return ProductType.theme;
|
||||||
|
case 'business-services':
|
||||||
|
return ProductType.businessService;
|
||||||
|
default:
|
||||||
|
return ProductType.extension;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function fetchCategories( type: ProductType ): Promise< CategoryAPIItem[] > {
|
function fetchCategories( type: ProductType ): Promise< CategoryAPIItem[] > {
|
||||||
const url = new URL( MARKETPLACE_HOST + MARKETPLACE_CATEGORY_API_PATH );
|
const url = new URL( MARKETPLACE_HOST + MARKETPLACE_CATEGORY_API_PATH );
|
||||||
|
|
||||||
|
@ -478,6 +496,7 @@ export {
|
||||||
fetchCategories,
|
fetchCategories,
|
||||||
fetchDiscoverPageData,
|
fetchDiscoverPageData,
|
||||||
fetchSearchResults,
|
fetchSearchResults,
|
||||||
|
getProductType,
|
||||||
fetchSubscriptions,
|
fetchSubscriptions,
|
||||||
refreshSubscriptions,
|
refreshSubscriptions,
|
||||||
getInstallUrl,
|
getInstallUrl,
|
||||||
|
|
|
@ -42,11 +42,6 @@ function recordMarketplaceView( props: MarketplaceViewProps ) {
|
||||||
eventProps.category = '_all';
|
eventProps.category = '_all';
|
||||||
}
|
}
|
||||||
|
|
||||||
// User clicks the `View All` button on search results
|
|
||||||
if ( view && view === 'search' && product_type && ! category ) {
|
|
||||||
eventProps.category = '_all';
|
|
||||||
}
|
|
||||||
|
|
||||||
recordEvent( 'marketplace_view', eventProps );
|
recordEvent( 'marketplace_view', eventProps );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,11 +75,6 @@ function recordLegacyTabView( props: MarketplaceViewProps ) {
|
||||||
case 'themes':
|
case 'themes':
|
||||||
oldEventProps.section = 'themes';
|
oldEventProps.section = 'themes';
|
||||||
break;
|
break;
|
||||||
case 'search':
|
|
||||||
oldEventName = 'extensions_view_search';
|
|
||||||
oldEventProps.section = view;
|
|
||||||
oldEventProps.search_term = search_term || '';
|
|
||||||
break;
|
|
||||||
case 'my-subscriptions':
|
case 'my-subscriptions':
|
||||||
oldEventName = 'subscriptions_view';
|
oldEventName = 'subscriptions_view';
|
||||||
oldEventProps.section = 'helper';
|
oldEventProps.section = 'helper';
|
||||||
|
|
|
@ -29,7 +29,7 @@ export const TaskListCompleted = ( {
|
||||||
className="woocommerce-task-card woocommerce-homescreen-card completed"
|
className="woocommerce-task-card woocommerce-homescreen-card completed"
|
||||||
>
|
>
|
||||||
<CardHeader size="medium">
|
<CardHeader size="medium">
|
||||||
<div className="wooocommerce-task-card__header">
|
<div className="woocommerce-task-card__header">
|
||||||
<img src={ HeaderImage } alt="Completed" />
|
<img src={ HeaderImage } alt="Completed" />
|
||||||
<h2>
|
<h2>
|
||||||
{ __(
|
{ __(
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: enhancement
|
||||||
|
|
||||||
|
Improve remote logging tool to simulate core error early in the request lifecycle before wp fully loaded
|
|
@ -143,10 +143,30 @@ add_action(
|
||||||
/**
|
/**
|
||||||
* Simulate a WooCommerce error for remote logging testing.
|
* Simulate a WooCommerce error for remote logging testing.
|
||||||
*
|
*
|
||||||
* @throws Exception A simulated WooCommerce error if the option is set.
|
* This function adds a filter to the 'woocommerce_template_path' hook
|
||||||
|
* that throws an exception, then triggers the filter by calling WC()->template_path().
|
||||||
|
*
|
||||||
|
* @throws Exception A simulated WooCommerce error for testing purposes.
|
||||||
*/
|
*/
|
||||||
function simulate_woocommerce_error() {
|
function simulate_woocommerce_error() {
|
||||||
|
// Return if WooCommerce is not loaded.
|
||||||
|
if ( ! function_exists( 'WC' ) || ! class_exists( 'WooCommerce' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a constant to prevent the error from being caught by the WP Error Handler.
|
||||||
|
if ( ! defined( 'WP_SANDBOX_SCRAPING' ) ) {
|
||||||
|
define( 'WP_SANDBOX_SCRAPING', true );
|
||||||
|
}
|
||||||
|
|
||||||
|
add_filter(
|
||||||
|
'woocommerce_template_path',
|
||||||
|
function() {
|
||||||
throw new Exception( 'Simulated WooCommerce error for remote logging test' );
|
throw new Exception( 'Simulated WooCommerce error for remote logging test' );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
WC()->template_path();
|
||||||
}
|
}
|
||||||
|
|
||||||
$simulate_error = get_option( 'wc_beta_tester_simulate_woocommerce_php_error', false );
|
$simulate_error = get_option( 'wc_beta_tester_simulate_woocommerce_php_error', false );
|
||||||
|
@ -155,7 +175,8 @@ if ( $simulate_error ) {
|
||||||
delete_option( 'wc_beta_tester_simulate_woocommerce_php_error' );
|
delete_option( 'wc_beta_tester_simulate_woocommerce_php_error' );
|
||||||
|
|
||||||
if ( 'core' === $simulate_error ) {
|
if ( 'core' === $simulate_error ) {
|
||||||
add_action( 'woocommerce_loaded', 'simulate_woocommerce_error' );
|
// Hook into the plugin_loaded action to simulate the error early before WP fully initializes.
|
||||||
|
add_action( 'plugin_loaded', 'simulate_woocommerce_error' );
|
||||||
} elseif ( 'beta-tester' === $simulate_error ) {
|
} elseif ( 'beta-tester' === $simulate_error ) {
|
||||||
throw new Exception( 'Test PHP exception from WooCommerce Beta Tester' );
|
throw new Exception( 'Test PHP exception from WooCommerce Beta Tester' );
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"core": null,
|
"core": "https://wordpress.org/wordpress-latest.zip",
|
||||||
"phpVersion": "7.4",
|
"phpVersion": "7.4",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"https://github.com/WP-API/Basic-Auth/archive/master.zip",
|
"https://github.com/WP-API/Basic-Auth/archive/master.zip",
|
||||||
|
|
|
@ -19,6 +19,9 @@ export default class ExpressPaymentMethodConfig
|
||||||
implements ExpressPaymentMethodConfigInstance
|
implements ExpressPaymentMethodConfigInstance
|
||||||
{
|
{
|
||||||
public name: string;
|
public name: string;
|
||||||
|
public title: string;
|
||||||
|
public description: string;
|
||||||
|
public gatewayId: string;
|
||||||
public content: ReactNode;
|
public content: ReactNode;
|
||||||
public edit: ReactNode;
|
public edit: ReactNode;
|
||||||
public paymentMethodId?: string;
|
public paymentMethodId?: string;
|
||||||
|
@ -27,13 +30,28 @@ export default class ExpressPaymentMethodConfig
|
||||||
|
|
||||||
constructor( config: ExpressPaymentMethodConfiguration ) {
|
constructor( config: ExpressPaymentMethodConfiguration ) {
|
||||||
// validate config
|
// validate config
|
||||||
|
|
||||||
|
const readableName =
|
||||||
|
typeof config.name === 'string'
|
||||||
|
? config.name.replace( /[_-]/g, ' ' )
|
||||||
|
: config.name;
|
||||||
|
const trimedDescription =
|
||||||
|
typeof config?.description === 'string' &&
|
||||||
|
config.description.length > 130
|
||||||
|
? config.description.slice( 0, 130 ) + '...'
|
||||||
|
: config.description;
|
||||||
|
|
||||||
ExpressPaymentMethodConfig.assertValidConfig( config );
|
ExpressPaymentMethodConfig.assertValidConfig( config );
|
||||||
this.name = config.name;
|
this.name = config.name;
|
||||||
|
this.title = config.title || readableName;
|
||||||
|
this.description = trimedDescription || '';
|
||||||
|
this.gatewayId = config.gatewayId || '';
|
||||||
this.content = config.content;
|
this.content = config.content;
|
||||||
this.edit = config.edit;
|
this.edit = config.edit;
|
||||||
this.paymentMethodId = config.paymentMethodId || this.name;
|
this.paymentMethodId = config.paymentMethodId || this.name;
|
||||||
this.supports = {
|
this.supports = {
|
||||||
features: config?.supports?.features || [ 'products' ],
|
features: config?.supports?.features || [ 'products' ],
|
||||||
|
style: config?.supports?.style || [],
|
||||||
};
|
};
|
||||||
this.canMakePaymentFromConfig = config.canMakePayment;
|
this.canMakePaymentFromConfig = config.canMakePayment;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,202 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { InspectorControls, HeightControl } from '@wordpress/block-editor';
|
||||||
|
import {
|
||||||
|
PanelBody,
|
||||||
|
ToggleControl,
|
||||||
|
RadioControl,
|
||||||
|
Notice,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
import ExternalLinkCard from '@woocommerce/editor-components/external-link-card';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import type { BlockAttributes } from '@wordpress/blocks';
|
||||||
|
import { select } from '@wordpress/data';
|
||||||
|
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
|
||||||
|
import { ADMIN_URL } from '@woocommerce/settings';
|
||||||
|
|
||||||
|
const allStyleControls = [ 'height', 'borderRadius' ];
|
||||||
|
|
||||||
|
const atLeastOnePaymentMethodSupportsOneOf = ( styleControl: string[] ) => {
|
||||||
|
const availableExpressMethods =
|
||||||
|
select( PAYMENT_STORE_KEY ).getAvailableExpressPaymentMethods();
|
||||||
|
|
||||||
|
return Object.values( availableExpressMethods ).reduce(
|
||||||
|
( acc, currentValue ) => {
|
||||||
|
return (
|
||||||
|
acc ||
|
||||||
|
currentValue?.supportsStyle.some( ( el ) =>
|
||||||
|
styleControl.includes( el )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExpressPaymentButtonStyleControls = ( {
|
||||||
|
attributes,
|
||||||
|
setAttributes,
|
||||||
|
}: {
|
||||||
|
attributes: BlockAttributes;
|
||||||
|
setAttributes: ( attrs: BlockAttributes ) => void;
|
||||||
|
} ) => {
|
||||||
|
const { buttonHeight, buttonBorderRadius } = attributes;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ atLeastOnePaymentMethodSupportsOneOf( [ 'height' ] ) && (
|
||||||
|
<RadioControl
|
||||||
|
label={ __( 'Button height', 'woocommerce' ) }
|
||||||
|
selected={ buttonHeight }
|
||||||
|
options={ [
|
||||||
|
{
|
||||||
|
label: __( 'Small (40px)', 'woocommerce' ),
|
||||||
|
value: '40',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __( 'Medium (48px)', 'woocommerce' ),
|
||||||
|
value: '48',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __( 'Large (55px)', 'woocommerce' ),
|
||||||
|
value: '55',
|
||||||
|
},
|
||||||
|
] }
|
||||||
|
onChange={ ( newValue: string ) =>
|
||||||
|
setAttributes( { buttonHeight: newValue } )
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
{ atLeastOnePaymentMethodSupportsOneOf( [ 'borderRadius' ] ) && (
|
||||||
|
<div className="border-radius-control-container">
|
||||||
|
<HeightControl
|
||||||
|
label={ __( 'Button border radius', 'woocommerce' ) }
|
||||||
|
value={ buttonBorderRadius }
|
||||||
|
onChange={ ( newValue: string ) => {
|
||||||
|
const valueOnly = newValue.replace( 'px', '' );
|
||||||
|
setAttributes( {
|
||||||
|
buttonBorderRadius: valueOnly,
|
||||||
|
} );
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExpressPaymentToggle = ( {
|
||||||
|
attributes,
|
||||||
|
setAttributes,
|
||||||
|
}: {
|
||||||
|
attributes: BlockAttributes;
|
||||||
|
setAttributes: ( attrs: BlockAttributes ) => void;
|
||||||
|
} ) => {
|
||||||
|
if ( attributes.showButtonStyles ) {
|
||||||
|
return (
|
||||||
|
<ExpressPaymentButtonStyleControls
|
||||||
|
attributes={ attributes }
|
||||||
|
setAttributes={ setAttributes }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExpressPaymentMethods = () => {
|
||||||
|
const availableExpressMethods =
|
||||||
|
select( PAYMENT_STORE_KEY ).getAvailableExpressPaymentMethods();
|
||||||
|
|
||||||
|
if ( Object.entries( availableExpressMethods ).length < 1 ) {
|
||||||
|
return (
|
||||||
|
<p className="wc-block-checkout__controls-text">
|
||||||
|
{ __(
|
||||||
|
'You currently have no express payment integrations active.',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className="wc-block-checkout__controls-text">
|
||||||
|
{ __(
|
||||||
|
'You currently have the following express payment integrations active.',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
|
{ Object.values( availableExpressMethods ).map( ( values ) => {
|
||||||
|
return (
|
||||||
|
<ExternalLinkCard
|
||||||
|
key={ values.name }
|
||||||
|
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=checkout§ion=${ encodeURIComponent(
|
||||||
|
values.gatewayId
|
||||||
|
) }` }
|
||||||
|
title={ values.title }
|
||||||
|
description={ values.description }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} ) }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleLabel = (
|
||||||
|
<>
|
||||||
|
{ __( 'Apply uniform styles', 'woocommerce' ) }{ ' ' }
|
||||||
|
<span className="express-payment-styles-beta-badge">Beta</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ExpressPaymentControls = ( {
|
||||||
|
attributes,
|
||||||
|
setAttributes,
|
||||||
|
}: {
|
||||||
|
attributes: BlockAttributes;
|
||||||
|
setAttributes: ( attrs: BlockAttributes ) => void;
|
||||||
|
} ) => {
|
||||||
|
return (
|
||||||
|
<InspectorControls>
|
||||||
|
{ atLeastOnePaymentMethodSupportsOneOf( allStyleControls ) && (
|
||||||
|
<PanelBody
|
||||||
|
title={ __( 'Button Settings', 'woocommerce' ) }
|
||||||
|
className="express-payment-button-settings"
|
||||||
|
>
|
||||||
|
<ToggleControl
|
||||||
|
label={ toggleLabel }
|
||||||
|
checked={ attributes.showButtonStyles }
|
||||||
|
onChange={ () =>
|
||||||
|
setAttributes( {
|
||||||
|
showButtonStyles: ! attributes.showButtonStyles,
|
||||||
|
} )
|
||||||
|
}
|
||||||
|
help={ __(
|
||||||
|
'Sets a consistent style for express payment buttons.',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
/>
|
||||||
|
<Notice
|
||||||
|
status="warning"
|
||||||
|
isDismissible={ false }
|
||||||
|
className="wc-block-checkout__notice express-payment-styles-notice"
|
||||||
|
>
|
||||||
|
<strong>{ __( 'Note', 'woocommerce' ) }:</strong>{ ' ' }
|
||||||
|
{ __(
|
||||||
|
'Some payment methods might not yet support all style controls',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</Notice>
|
||||||
|
<ExpressPaymentToggle
|
||||||
|
attributes={ attributes }
|
||||||
|
setAttributes={ setAttributes }
|
||||||
|
/>
|
||||||
|
</PanelBody>
|
||||||
|
) }
|
||||||
|
<PanelBody title={ __( 'Express Payment Methods', 'woocommerce' ) }>
|
||||||
|
<ExpressPaymentMethods />
|
||||||
|
</PanelBody>
|
||||||
|
</InspectorControls>
|
||||||
|
);
|
||||||
|
};
|
|
@ -34,3 +34,5 @@ export const BlockSettings = ( {
|
||||||
</InspectorControls>
|
</InspectorControls>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { ExpressPaymentControls } from './express-payment-settings';
|
||||||
|
|
|
@ -21,10 +21,22 @@ import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
*/
|
*/
|
||||||
import PaymentMethodErrorBoundary from './payment-method-error-boundary';
|
import PaymentMethodErrorBoundary from './payment-method-error-boundary';
|
||||||
import { STORE_KEY as PAYMENT_STORE_KEY } from '../../../data/payment/constants';
|
import { STORE_KEY as PAYMENT_STORE_KEY } from '../../../data/payment/constants';
|
||||||
|
import { useExpressPaymentContext } from '../../cart-checkout-shared/payment-methods/express-payment/express-payment-context';
|
||||||
|
|
||||||
const ExpressPaymentMethods = () => {
|
const ExpressPaymentMethods = () => {
|
||||||
const { isEditor } = useEditorContext();
|
const { isEditor } = useEditorContext();
|
||||||
|
|
||||||
|
const { showButtonStyles, buttonHeight, buttonBorderRadius } =
|
||||||
|
useExpressPaymentContext();
|
||||||
|
|
||||||
|
// API for passing styles to express payment buttons
|
||||||
|
const buttonAttributes = showButtonStyles
|
||||||
|
? {
|
||||||
|
height: buttonHeight,
|
||||||
|
borderRadius: buttonBorderRadius,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const { activePaymentMethod, paymentMethodData } = useSelect(
|
const { activePaymentMethod, paymentMethodData } = useSelect(
|
||||||
( select ) => {
|
( select ) => {
|
||||||
const store = select( PAYMENT_STORE_KEY );
|
const store = select( PAYMENT_STORE_KEY );
|
||||||
|
@ -150,6 +162,7 @@ const ExpressPaymentMethods = () => {
|
||||||
onError: onExpressPaymentError,
|
onError: onExpressPaymentError,
|
||||||
setExpressPaymentError:
|
setExpressPaymentError:
|
||||||
deprecatedSetExpressPaymentError,
|
deprecatedSetExpressPaymentError,
|
||||||
|
buttonAttributes,
|
||||||
} ) }
|
} ) }
|
||||||
</li>
|
</li>
|
||||||
) : null;
|
) : null;
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { useContext, createContext } from '@wordpress/element';
|
||||||
|
|
||||||
|
type ExpressPaymentContextProps = {
|
||||||
|
showButtonStyles: boolean;
|
||||||
|
buttonHeight: string;
|
||||||
|
buttonBorderRadius: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExpressPaymentContext: React.Context< ExpressPaymentContextProps > =
|
||||||
|
createContext< ExpressPaymentContextProps >( {
|
||||||
|
showButtonStyles: false,
|
||||||
|
buttonHeight: '48',
|
||||||
|
buttonBorderRadius: '4',
|
||||||
|
} );
|
||||||
|
|
||||||
|
export const useExpressPaymentContext = () => {
|
||||||
|
return useContext( ExpressPaymentContext );
|
||||||
|
};
|
|
@ -1,2 +1,2 @@
|
||||||
export { default as CartExpressPayment } from './cart-express-payment.js';
|
export { default as CartExpressPayment } from './cart-express-payment.js';
|
||||||
export { default as CheckoutExpressPayment } from './checkout-express-payment.js';
|
export { default as CheckoutExpressPayment } from './checkout-express-payment';
|
||||||
|
|
|
@ -14,6 +14,7 @@ $border-width: 1px;
|
||||||
> li {
|
> li {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
> img {
|
> img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -95,7 +96,7 @@ $border-width: 1px;
|
||||||
.wc-block-components-express-payment--cart {
|
.wc-block-components-express-payment--cart {
|
||||||
.wc-block-components-express-payment__event-buttons {
|
.wc-block-components-express-payment__event-buttons {
|
||||||
> li {
|
> li {
|
||||||
padding-bottom: $gap;
|
padding-bottom: $gap-small;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
// This needs to be defined in a separate file because we are mocking an import.
|
||||||
|
// The only way to do this is to define the mock and import it BEFORE the module being mocked.
|
||||||
|
export default jest.fn( () => ( { isEditor: false } ) );
|
|
@ -0,0 +1,187 @@
|
||||||
|
// This is the shape of the API exposed to the express payment methods via props
|
||||||
|
// Note that this is a public API!
|
||||||
|
export const getExpectedExpressPaymentProps = ( name: string ) => ( {
|
||||||
|
activePaymentMethod: undefined,
|
||||||
|
billing: {
|
||||||
|
appliedCoupons: [],
|
||||||
|
billingAddress: {
|
||||||
|
address_1: '',
|
||||||
|
address_2: '',
|
||||||
|
city: '',
|
||||||
|
company: '',
|
||||||
|
country: '',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
phone: '',
|
||||||
|
postcode: '',
|
||||||
|
state: '',
|
||||||
|
},
|
||||||
|
billingData: {
|
||||||
|
address_1: '',
|
||||||
|
address_2: '',
|
||||||
|
city: '',
|
||||||
|
company: '',
|
||||||
|
country: '',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
phone: '',
|
||||||
|
postcode: '',
|
||||||
|
state: '',
|
||||||
|
},
|
||||||
|
cartTotal: {
|
||||||
|
label: 'Total',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
cartTotalItems: [
|
||||||
|
{
|
||||||
|
key: 'total_items',
|
||||||
|
label: 'Subtotal:',
|
||||||
|
value: 0,
|
||||||
|
valueWithTax: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'total_fees',
|
||||||
|
label: 'Fees:',
|
||||||
|
value: 0,
|
||||||
|
valueWithTax: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'total_discount',
|
||||||
|
label: 'Discount:',
|
||||||
|
value: 0,
|
||||||
|
valueWithTax: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'total_tax',
|
||||||
|
label: 'Taxes:',
|
||||||
|
value: 0,
|
||||||
|
valueWithTax: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'total_shipping',
|
||||||
|
label: 'Shipping:',
|
||||||
|
value: 0,
|
||||||
|
valueWithTax: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currency: {
|
||||||
|
code: 'USD',
|
||||||
|
decimalSeparator: '.',
|
||||||
|
minorUnit: 2,
|
||||||
|
prefix: '$',
|
||||||
|
suffix: '',
|
||||||
|
symbol: '$',
|
||||||
|
thousandSeparator: ',',
|
||||||
|
},
|
||||||
|
customerId: 1,
|
||||||
|
displayPricesIncludingTax: false,
|
||||||
|
},
|
||||||
|
buttonAttributes: {
|
||||||
|
borderRadius: '4',
|
||||||
|
height: '48',
|
||||||
|
},
|
||||||
|
cartData: {
|
||||||
|
cartFees: [],
|
||||||
|
cartItems: [],
|
||||||
|
extensions: {},
|
||||||
|
},
|
||||||
|
checkoutStatus: {
|
||||||
|
isCalculating: false,
|
||||||
|
isComplete: false,
|
||||||
|
isIdle: true,
|
||||||
|
isProcessing: false,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
LoadingMask: expect.any( Function ),
|
||||||
|
PaymentMethodIcons: expect.any( Function ),
|
||||||
|
PaymentMethodLabel: expect.any( Function ),
|
||||||
|
ValidationInputError: expect.any( Function ),
|
||||||
|
},
|
||||||
|
emitResponse: {
|
||||||
|
noticeContexts: {
|
||||||
|
BILLING_ADDRESS: 'wc/checkout/billing-address',
|
||||||
|
CART: 'wc/cart',
|
||||||
|
CHECKOUT: 'wc/checkout',
|
||||||
|
CHECKOUT_ACTIONS: 'wc/checkout/checkout-actions',
|
||||||
|
CONTACT_INFORMATION: 'wc/checkout/contact-information',
|
||||||
|
EXPRESS_PAYMENTS: 'wc/checkout/express-payments',
|
||||||
|
ORDER_INFORMATION: 'wc/checkout/additional-information',
|
||||||
|
PAYMENTS: 'wc/checkout/payments',
|
||||||
|
SHIPPING_ADDRESS: 'wc/checkout/shipping-address',
|
||||||
|
SHIPPING_METHODS: 'wc/checkout/shipping-methods',
|
||||||
|
},
|
||||||
|
responseTypes: {
|
||||||
|
ERROR: 'error',
|
||||||
|
FAIL: 'failure',
|
||||||
|
SUCCESS: 'success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eventRegistration: {
|
||||||
|
onCheckoutAfterProcessingWithError: expect.any( Function ),
|
||||||
|
onCheckoutAfterProcessingWithSuccess: expect.any( Function ),
|
||||||
|
onCheckoutBeforeProcessing: expect.any( Function ),
|
||||||
|
onCheckoutFail: expect.any( Function ),
|
||||||
|
onCheckoutSuccess: expect.any( Function ),
|
||||||
|
onCheckoutValidation: expect.any( Function ),
|
||||||
|
onCheckoutValidationBeforeProcessing: expect.any( Function ),
|
||||||
|
onPaymentProcessing: expect.any( Function ),
|
||||||
|
onPaymentSetup: expect.any( Function ),
|
||||||
|
onShippingRateFail: expect.any( Function ),
|
||||||
|
onShippingRateSelectFail: expect.any( Function ),
|
||||||
|
onShippingRateSelectSuccess: expect.any( Function ),
|
||||||
|
onShippingRateSuccess: expect.any( Function ),
|
||||||
|
},
|
||||||
|
name,
|
||||||
|
onClick: expect.any( Function ),
|
||||||
|
onClose: expect.any( Function ),
|
||||||
|
onError: expect.any( Function ),
|
||||||
|
onSubmit: expect.any( Function ),
|
||||||
|
paymentStatus: {
|
||||||
|
hasError: false,
|
||||||
|
hasFailed: false,
|
||||||
|
isDoingExpressPayment: false,
|
||||||
|
isFinished: false,
|
||||||
|
isIdle: true,
|
||||||
|
isPristine: true,
|
||||||
|
isProcessing: false,
|
||||||
|
isReady: false,
|
||||||
|
isStarted: false,
|
||||||
|
isSuccessful: false,
|
||||||
|
},
|
||||||
|
setExpressPaymentError: expect.any( Function ),
|
||||||
|
shippingData: {
|
||||||
|
isSelectingRate: false,
|
||||||
|
needsShipping: true,
|
||||||
|
selectedRates: {},
|
||||||
|
setSelectedRates: expect.any( Function ),
|
||||||
|
setShippingAddress: expect.any( Function ),
|
||||||
|
shippingAddress: {
|
||||||
|
address_1: '',
|
||||||
|
address_2: '',
|
||||||
|
city: '',
|
||||||
|
company: '',
|
||||||
|
country: '',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
phone: '',
|
||||||
|
postcode: '',
|
||||||
|
state: '',
|
||||||
|
},
|
||||||
|
shippingRates: [],
|
||||||
|
shippingRatesLoading: false,
|
||||||
|
},
|
||||||
|
shippingStatus: {
|
||||||
|
shippingErrorStatus: {
|
||||||
|
hasError: false,
|
||||||
|
hasInvalidAddress: false,
|
||||||
|
isPristine: true,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
shippingErrorTypes: {
|
||||||
|
INVALID_ADDRESS: 'invalid_address',
|
||||||
|
NONE: 'none',
|
||||||
|
UNKNOWN: 'unknown_error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldSavePayment: false,
|
||||||
|
} );
|
|
@ -0,0 +1,139 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { PAYMENT_STORE_KEY } from '@woocommerce/block-data';
|
||||||
|
import {
|
||||||
|
registerExpressPaymentMethod,
|
||||||
|
__experimentalDeRegisterExpressPaymentMethod,
|
||||||
|
} from '@woocommerce/blocks-registry';
|
||||||
|
import { dispatch } from '@wordpress/data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import mockEditorContext from './__mocks__/editor-context';
|
||||||
|
import { getExpectedExpressPaymentProps } from './__mocks__/express-payment-props';
|
||||||
|
import ExpressPaymentMethods from '../express-payment-methods';
|
||||||
|
jest.mock( '@woocommerce/base-context', () => ( {
|
||||||
|
useEditorContext: mockEditorContext,
|
||||||
|
} ) );
|
||||||
|
|
||||||
|
// Button styles are disabled by default. We need to mock the express payment context
|
||||||
|
// to enable them.
|
||||||
|
jest.mock( '../express-payment/express-payment-context', () => {
|
||||||
|
return {
|
||||||
|
useExpressPaymentContext: jest.fn().mockReturnValue( {
|
||||||
|
showButtonStyles: true,
|
||||||
|
buttonHeight: '48',
|
||||||
|
buttonBorderRadius: '4',
|
||||||
|
} ),
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
|
||||||
|
const mockExpressPaymentMethodNames = [ 'paypal', 'google pay', 'apple pay' ];
|
||||||
|
|
||||||
|
const MockExpressButton = jest.fn( ( { name } ) => (
|
||||||
|
<div className="boo">{ `${ name } button` }</div>
|
||||||
|
) );
|
||||||
|
|
||||||
|
const MockEditorExpressButton = jest.fn( ( { name } ) => (
|
||||||
|
<div>{ `${ name } preview` }</div>
|
||||||
|
) );
|
||||||
|
|
||||||
|
const registerMockExpressPaymentMethods = () => {
|
||||||
|
mockExpressPaymentMethodNames.forEach( ( name ) => {
|
||||||
|
registerExpressPaymentMethod( {
|
||||||
|
name,
|
||||||
|
title: `${ name } payment method`,
|
||||||
|
description: `A test ${ name } payment method`,
|
||||||
|
gatewayId: 'test-express-payment-method',
|
||||||
|
paymentMethodId: name,
|
||||||
|
content: <MockExpressButton name={ name } />,
|
||||||
|
edit: <MockEditorExpressButton name={ name } />,
|
||||||
|
canMakePayment: () => true,
|
||||||
|
supports: {
|
||||||
|
features: [ 'products' ],
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
dispatch( PAYMENT_STORE_KEY ).__internalUpdateAvailablePaymentMethods();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deregisterMockExpressPaymentMethods = () => {
|
||||||
|
mockExpressPaymentMethodNames.forEach( ( name ) => {
|
||||||
|
__experimentalDeRegisterExpressPaymentMethod( name );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
describe( 'Express payment methods', () => {
|
||||||
|
afterAll( () => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
} );
|
||||||
|
describe( 'No payment methods available', () => {
|
||||||
|
it( 'should display no registered payment methods', () => {
|
||||||
|
render( <ExpressPaymentMethods /> );
|
||||||
|
|
||||||
|
const noPaymentMethods = screen.queryAllByText(
|
||||||
|
/No registered Payment Methods/
|
||||||
|
);
|
||||||
|
expect( noPaymentMethods.length ).toEqual( 1 );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'Payment methods available', () => {
|
||||||
|
beforeAll( () => {
|
||||||
|
registerMockExpressPaymentMethods();
|
||||||
|
} );
|
||||||
|
afterAll( () => {
|
||||||
|
deregisterMockExpressPaymentMethods();
|
||||||
|
} );
|
||||||
|
describe( 'In a frontend context', () => {
|
||||||
|
it( 'should display the element provided by paymentMethods.content', () => {
|
||||||
|
render( <ExpressPaymentMethods /> );
|
||||||
|
mockExpressPaymentMethodNames.forEach( ( name ) => {
|
||||||
|
const btn = screen.getByText( `${ name } button` );
|
||||||
|
expect( btn ).toBeVisible();
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
it( 'should pass the correct properties to the rendered element', () => {
|
||||||
|
render( <ExpressPaymentMethods /> );
|
||||||
|
mockExpressPaymentMethodNames.forEach( ( name ) => {
|
||||||
|
expect( MockExpressButton ).toHaveBeenCalledWith(
|
||||||
|
getExpectedExpressPaymentProps( name ),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
// This is a bit out of place, but the console warning is triggered when the
|
||||||
|
// usePaymentMethodInterface hook is called so we need to expect it here otherwise
|
||||||
|
// the test fails on unexpected console warnings.
|
||||||
|
expect( console ).toHaveWarnedWith(
|
||||||
|
'isPristine is deprecated since version 9.6.0. Please use isIdle instead. See: https://github.com/woocommerce/woocommerce-blocks/pull/8110'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
describe( 'In an editor context', () => {
|
||||||
|
beforeEach( () => {
|
||||||
|
mockEditorContext.mockImplementation( () => ( {
|
||||||
|
isEditor: true,
|
||||||
|
} ) );
|
||||||
|
} );
|
||||||
|
it( 'should display the element provided by paymentMethods.edit', () => {
|
||||||
|
render( <ExpressPaymentMethods /> );
|
||||||
|
mockExpressPaymentMethodNames.forEach( ( name ) => {
|
||||||
|
const btn = screen.getByText( `${ name } preview` );
|
||||||
|
expect( btn ).toBeVisible();
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
it( 'should pass the correct properties to the rendered element', () => {
|
||||||
|
render( <ExpressPaymentMethods /> );
|
||||||
|
mockExpressPaymentMethodNames.forEach( ( name ) => {
|
||||||
|
expect( MockEditorExpressButton ).toHaveBeenCalledWith(
|
||||||
|
getExpectedExpressPaymentProps( name ),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,23 @@
|
||||||
|
export type ExpressCheckoutAttributes = {
|
||||||
|
className?: string;
|
||||||
|
buttonHeight: string;
|
||||||
|
showButtonStyles: boolean;
|
||||||
|
buttonBorderRadius: string;
|
||||||
|
lock: {
|
||||||
|
move: boolean;
|
||||||
|
remove: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExpressCartAttributes = {
|
||||||
|
className: string;
|
||||||
|
buttonHeight: string;
|
||||||
|
showButtonStyles: boolean;
|
||||||
|
buttonBorderRadius: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExpressPaymentSettings = {
|
||||||
|
showButtonStyles: boolean;
|
||||||
|
buttonHeight: string;
|
||||||
|
buttonBorderRadius: string;
|
||||||
|
};
|
|
@ -13,6 +13,18 @@
|
||||||
"lock": false
|
"lock": false
|
||||||
},
|
},
|
||||||
"attributes": {
|
"attributes": {
|
||||||
|
"showButtonStyles": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"buttonHeight": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "48"
|
||||||
|
},
|
||||||
|
"buttonBorderRadius": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "4"
|
||||||
|
},
|
||||||
"lock": {
|
"lock": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"default": {
|
"default": {
|
||||||
|
|
|
@ -4,17 +4,23 @@
|
||||||
import { useBlockProps } from '@wordpress/block-editor';
|
import { useBlockProps } from '@wordpress/block-editor';
|
||||||
import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks';
|
import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { ExpressPaymentControls } from '@woocommerce/blocks/cart-checkout-shared';
|
||||||
|
import type { BlockAttributes } from '@wordpress/blocks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import Block from './block';
|
import Block from './block';
|
||||||
import './editor.scss';
|
import './editor.scss';
|
||||||
|
import type { ExpressCartAttributes } from '../../../cart-checkout-shared/types';
|
||||||
|
import { ExpressPaymentContext } from '../../../cart-checkout-shared/payment-methods/express-payment/express-payment-context';
|
||||||
|
|
||||||
export const Edit = ( {
|
export const Edit = ( {
|
||||||
attributes,
|
attributes,
|
||||||
|
setAttributes,
|
||||||
}: {
|
}: {
|
||||||
attributes: { className: string };
|
attributes: ExpressCartAttributes;
|
||||||
|
setAttributes: ( attrs: BlockAttributes ) => void;
|
||||||
} ): JSX.Element | null => {
|
} ): JSX.Element | null => {
|
||||||
const { paymentMethods, isInitialized } = useExpressPaymentMethods();
|
const { paymentMethods, isInitialized } = useExpressPaymentMethods();
|
||||||
const hasExpressPaymentMethods = Object.keys( paymentMethods ).length > 0;
|
const hasExpressPaymentMethods = Object.keys( paymentMethods ).length > 0;
|
||||||
|
@ -24,7 +30,9 @@ export const Edit = ( {
|
||||||
hasExpressPaymentMethods,
|
hasExpressPaymentMethods,
|
||||||
} ),
|
} ),
|
||||||
} );
|
} );
|
||||||
const { className } = attributes;
|
|
||||||
|
const { className, showButtonStyles, buttonHeight, buttonBorderRadius } =
|
||||||
|
attributes;
|
||||||
|
|
||||||
if ( ! isInitialized || ! hasExpressPaymentMethods ) {
|
if ( ! isInitialized || ! hasExpressPaymentMethods ) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -32,7 +40,15 @@ export const Edit = ( {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div { ...blockProps }>
|
<div { ...blockProps }>
|
||||||
|
<ExpressPaymentControls
|
||||||
|
attributes={ attributes }
|
||||||
|
setAttributes={ setAttributes }
|
||||||
|
/>
|
||||||
|
<ExpressPaymentContext.Provider
|
||||||
|
value={ { showButtonStyles, buttonHeight, buttonBorderRadius } }
|
||||||
|
>
|
||||||
<Block className={ className } />
|
<Block className={ className } />
|
||||||
|
</ExpressPaymentContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,3 +32,38 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.express-payment-styles-notice {
|
||||||
|
margin-bottom: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.express-payment-styles-beta-badge {
|
||||||
|
margin-left: $grid-unit-10;
|
||||||
|
padding: 3px $grid-unit-10;
|
||||||
|
height: $grid-unit-30;
|
||||||
|
border-radius: $radius-block-ui;
|
||||||
|
background-color: $gray-900;
|
||||||
|
color: $white;
|
||||||
|
align-items: center;
|
||||||
|
font-size: $helptext-font-size;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disabled changing units from px for border radius control
|
||||||
|
.border-radius-control-container select {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-height-control + .border-radius-control-container {
|
||||||
|
margin-top: $grid-unit-30;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center images rendered in place of buttons in the editor
|
||||||
|
.wc-block-components-express-payment {
|
||||||
|
.wc-block-components-express-payment__event-buttons {
|
||||||
|
> li {
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,32 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { getValidBlockAttributes } from '@woocommerce/base-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import Block from './block';
|
import Block from './block';
|
||||||
|
import { ExpressPaymentContext } from '../../../cart-checkout-shared/payment-methods/express-payment/express-payment-context';
|
||||||
|
import metadata from './block.json';
|
||||||
|
import { ExpressCheckoutAttributes } from '../../../cart-checkout-shared/types';
|
||||||
|
|
||||||
export default Block;
|
const FrontendBlock = ( attributes: ExpressCheckoutAttributes ) => {
|
||||||
|
const validAttributes = getValidBlockAttributes(
|
||||||
|
metadata.attributes,
|
||||||
|
attributes
|
||||||
|
);
|
||||||
|
|
||||||
|
const { showButtonStyles, buttonHeight, buttonBorderRadius, className } =
|
||||||
|
validAttributes;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ExpressPaymentContext.Provider
|
||||||
|
value={ { showButtonStyles, buttonHeight, buttonBorderRadius } }
|
||||||
|
>
|
||||||
|
<Block className={ className } />
|
||||||
|
</ExpressPaymentContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FrontendBlock;
|
||||||
|
|
|
@ -13,6 +13,18 @@
|
||||||
"lock": false
|
"lock": false
|
||||||
},
|
},
|
||||||
"attributes": {
|
"attributes": {
|
||||||
|
"showButtonStyles": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"buttonHeight": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "48"
|
||||||
|
},
|
||||||
|
"buttonBorderRadius": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "4"
|
||||||
|
},
|
||||||
"className": {
|
"className": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": ""
|
"default": ""
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { CheckoutExpressPayment } from '../../../cart-checkout-shared/payment-me
|
||||||
|
|
||||||
const Block = ( { className }: { className?: string } ): JSX.Element | null => {
|
const Block = ( { className }: { className?: string } ): JSX.Element | null => {
|
||||||
const { cartNeedsPayment } = useStoreCart();
|
const { cartNeedsPayment } = useStoreCart();
|
||||||
|
|
||||||
if ( ! cartNeedsPayment ) {
|
if ( ! cartNeedsPayment ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,23 @@
|
||||||
import { useBlockProps } from '@wordpress/block-editor';
|
import { useBlockProps } from '@wordpress/block-editor';
|
||||||
import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks';
|
import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { ExpressPaymentControls } from '@woocommerce/blocks/cart-checkout-shared';
|
||||||
|
import type { BlockAttributes } from '@wordpress/blocks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import Block from './block';
|
import Block from './block';
|
||||||
import './editor.scss';
|
import './editor.scss';
|
||||||
|
import type { ExpressCheckoutAttributes } from '../../../cart-checkout-shared/types';
|
||||||
|
import { ExpressPaymentContext } from '../../../cart-checkout-shared/payment-methods/express-payment/express-payment-context';
|
||||||
|
|
||||||
export const Edit = ( {
|
export const Edit = ( {
|
||||||
attributes,
|
attributes,
|
||||||
|
setAttributes,
|
||||||
}: {
|
}: {
|
||||||
attributes: {
|
attributes: ExpressCheckoutAttributes;
|
||||||
className?: string;
|
setAttributes: ( attrs: BlockAttributes ) => void;
|
||||||
lock: {
|
|
||||||
move: boolean;
|
|
||||||
remove: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
} ): JSX.Element | null => {
|
} ): JSX.Element | null => {
|
||||||
const { paymentMethods, isInitialized } = useExpressPaymentMethods();
|
const { paymentMethods, isInitialized } = useExpressPaymentMethods();
|
||||||
const hasExpressPaymentMethods = Object.keys( paymentMethods ).length > 0;
|
const hasExpressPaymentMethods = Object.keys( paymentMethods ).length > 0;
|
||||||
|
@ -39,9 +39,19 @@ export const Edit = ( {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { buttonHeight, buttonBorderRadius, showButtonStyles } = attributes;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div { ...blockProps }>
|
<div { ...blockProps }>
|
||||||
|
<ExpressPaymentControls
|
||||||
|
attributes={ attributes }
|
||||||
|
setAttributes={ setAttributes }
|
||||||
|
/>
|
||||||
|
<ExpressPaymentContext.Provider
|
||||||
|
value={ { showButtonStyles, buttonHeight, buttonBorderRadius } }
|
||||||
|
>
|
||||||
<Block />
|
<Block />
|
||||||
|
</ExpressPaymentContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,3 +27,38 @@
|
||||||
margin: 0 0 1em;
|
margin: 0 0 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.express-payment-styles-notice {
|
||||||
|
margin-bottom: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.express-payment-styles-beta-badge {
|
||||||
|
margin-left: $grid-unit-10;
|
||||||
|
padding: 3px $grid-unit-10;
|
||||||
|
height: $grid-unit-30;
|
||||||
|
border-radius: $radius-block-ui;
|
||||||
|
background-color: $gray-900;
|
||||||
|
color: $white;
|
||||||
|
align-items: center;
|
||||||
|
font-size: $helptext-font-size;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disabled changing units from px for border radius control
|
||||||
|
.border-radius-control-container select {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-height-control + .border-radius-control-container {
|
||||||
|
margin-top: $grid-unit-30;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center images rendered in place of buttons in the editor
|
||||||
|
.wc-block-components-express-payment {
|
||||||
|
.wc-block-components-express-payment__event-buttons {
|
||||||
|
> li {
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { getValidBlockAttributes } from '@woocommerce/base-utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import Block from './block';
|
||||||
|
import { ExpressPaymentContext } from '../../../cart-checkout-shared/payment-methods/express-payment/express-payment-context';
|
||||||
|
import metadata from './block.json';
|
||||||
|
import { ExpressCheckoutAttributes } from '../../../cart-checkout-shared/types';
|
||||||
|
|
||||||
|
const FrontendBlock = ( attributes: ExpressCheckoutAttributes ) => {
|
||||||
|
const validAttributes = getValidBlockAttributes(
|
||||||
|
metadata.attributes,
|
||||||
|
attributes
|
||||||
|
);
|
||||||
|
|
||||||
|
const { showButtonStyles, buttonHeight, buttonBorderRadius } =
|
||||||
|
validAttributes;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ExpressPaymentContext.Provider
|
||||||
|
value={ { showButtonStyles, buttonHeight, buttonBorderRadius } }
|
||||||
|
>
|
||||||
|
<Block />
|
||||||
|
</ExpressPaymentContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FrontendBlock;
|
|
@ -0,0 +1,10 @@
|
||||||
|
export type ExpressCheckoutAttributes = {
|
||||||
|
className?: string;
|
||||||
|
buttonHeight: string;
|
||||||
|
showButtonStyles: boolean;
|
||||||
|
buttonBorderRadius: string;
|
||||||
|
lock: {
|
||||||
|
move: boolean;
|
||||||
|
remove: boolean;
|
||||||
|
};
|
||||||
|
};
|
|
@ -33,7 +33,7 @@ registerCheckoutBlock( {
|
||||||
component: lazy(
|
component: lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "checkout-blocks/express-payment" */ './checkout-express-payment-block/block'
|
/* webpackChunkName: "checkout-blocks/express-payment" */ './checkout-express-payment-block/frontend'
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -123,6 +123,9 @@ const registerMockPaymentMethods = ( savedCards = true ) => {
|
||||||
};
|
};
|
||||||
registerExpressPaymentMethod( {
|
registerExpressPaymentMethod( {
|
||||||
name,
|
name,
|
||||||
|
title: 'Express Payment Method',
|
||||||
|
description: 'A test express payment method',
|
||||||
|
gatewayId: 'test-express-payment-method',
|
||||||
content: <Content />,
|
content: <Content />,
|
||||||
edit: <div>An express payment method</div>,
|
edit: <div>An express payment method</div>,
|
||||||
canMakePayment: mockedExpressCanMakePayment,
|
canMakePayment: mockedExpressCanMakePayment,
|
||||||
|
|
|
@ -123,6 +123,9 @@ const registerMockPaymentMethods = ( savedCards = true ) => {
|
||||||
};
|
};
|
||||||
registerExpressPaymentMethod( {
|
registerExpressPaymentMethod( {
|
||||||
name,
|
name,
|
||||||
|
title: `${ name } express payment method`,
|
||||||
|
description: `${ name } express payment method description`,
|
||||||
|
gatewayId: 'woo',
|
||||||
content: <Content />,
|
content: <Content />,
|
||||||
edit: <div>An express payment method</div>,
|
edit: <div>An express payment method</div>,
|
||||||
canMakePayment: () => true,
|
canMakePayment: () => true,
|
||||||
|
|
|
@ -163,11 +163,30 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
|
||||||
| PaymentMethodConfigInstance
|
| PaymentMethodConfigInstance
|
||||||
| ExpressPaymentMethodConfigInstance
|
| ExpressPaymentMethodConfigInstance
|
||||||
) => {
|
) => {
|
||||||
const { name } = paymentMethod;
|
if ( express ) {
|
||||||
|
const { name, title, description, gatewayId, supports } =
|
||||||
|
paymentMethod as ExpressPaymentMethodConfigInstance;
|
||||||
|
|
||||||
availablePaymentMethods = {
|
availablePaymentMethods = {
|
||||||
...availablePaymentMethods,
|
...availablePaymentMethods,
|
||||||
[ paymentMethod.name ]: { name },
|
[ paymentMethod.name ]: {
|
||||||
|
name,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
gatewayId,
|
||||||
|
supportsStyle: supports?.style,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
const { name } = paymentMethod as PaymentMethodConfigInstance;
|
||||||
|
|
||||||
|
availablePaymentMethods = {
|
||||||
|
...availablePaymentMethods,
|
||||||
|
[ paymentMethod.name ]: {
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Order payment methods.
|
// Order payment methods.
|
||||||
|
|
|
@ -31,6 +31,7 @@ export interface SupportsConfiguration {
|
||||||
features?: string[];
|
features?: string[];
|
||||||
// Deprecated, in favour of showSavedCards and showSaveOption
|
// Deprecated, in favour of showSavedCards and showSaveOption
|
||||||
savePaymentInfo?: boolean;
|
savePaymentInfo?: boolean;
|
||||||
|
style?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// we assign a value in the class for supports.features
|
// we assign a value in the class for supports.features
|
||||||
|
@ -119,10 +120,28 @@ export interface PaymentMethodConfiguration {
|
||||||
savedTokenComponent?: ReactNode | null;
|
savedTokenComponent?: ReactNode | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExpressPaymentMethodConfiguration = Omit<
|
export interface ExpressPaymentMethodConfiguration {
|
||||||
PaymentMethodConfiguration,
|
// A unique string to identify the payment method client side.
|
||||||
'icons' | 'label' | 'ariaLabel' | 'placeOrderButtonLabel'
|
name: string;
|
||||||
>;
|
// A human readable title for the payment method.
|
||||||
|
title?: string;
|
||||||
|
// A human readable description for the payment method.
|
||||||
|
description?: string;
|
||||||
|
// The gateway ID for the payment method.
|
||||||
|
gatewayId?: string;
|
||||||
|
// A react node for your payment method UI.
|
||||||
|
content: ReactNode;
|
||||||
|
// A react node to display a preview of your payment method in the editor.
|
||||||
|
edit: ReactNode;
|
||||||
|
// A callback to determine whether the payment method should be shown in the checkout.
|
||||||
|
canMakePayment: CanMakePaymentCallback;
|
||||||
|
// A unique string to represent the payment method server side. If not provided, defaults to name.
|
||||||
|
paymentMethodId?: string;
|
||||||
|
// Object that describes various features provided by the payment method.
|
||||||
|
supports: SupportsConfiguration;
|
||||||
|
// A React node that contains logic handling any processing your payment method has to do with saved payment methods if your payment method supports them
|
||||||
|
savedTokenComponent?: ReactNode | null;
|
||||||
|
}
|
||||||
|
|
||||||
export type PaymentMethods =
|
export type PaymentMethods =
|
||||||
| Record< string, PaymentMethodConfigInstance >
|
| Record< string, PaymentMethodConfigInstance >
|
||||||
|
@ -131,7 +150,16 @@ export type PaymentMethods =
|
||||||
/**
|
/**
|
||||||
* Used to represent payment methods in a context where storing objects is not allowed, i.e. in data stores.
|
* Used to represent payment methods in a context where storing objects is not allowed, i.e. in data stores.
|
||||||
*/
|
*/
|
||||||
export type PlainPaymentMethods = Record< string, { name: string } >;
|
export type PlainPaymentMethods = Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
gatewayId: string;
|
||||||
|
supportsStyle: string[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to represent payment methods in a context where storing objects is not allowed, i.e. in data stores.
|
* Used to represent payment methods in a context where storing objects is not allowed, i.e. in data stores.
|
||||||
|
@ -159,6 +187,9 @@ export interface PaymentMethodConfigInstance {
|
||||||
|
|
||||||
export interface ExpressPaymentMethodConfigInstance {
|
export interface ExpressPaymentMethodConfigInstance {
|
||||||
name: string;
|
name: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
gatewayId: string;
|
||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
edit: ReactNode;
|
edit: ReactNode;
|
||||||
paymentMethodId?: string;
|
paymentMethodId?: string;
|
||||||
|
|
|
@ -184,7 +184,7 @@ test.describe( 'Merchant → Local Pickup Settings', () => {
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test( 'updating the title in WC Settings updates the local pickup text in the block and vice/versa', async ( {
|
test.skip( 'updating the title in WC Settings updates the local pickup text in the block and vice/versa', async ( {
|
||||||
page,
|
page,
|
||||||
localPickupUtils,
|
localPickupUtils,
|
||||||
admin,
|
admin,
|
||||||
|
|
|
@ -87,7 +87,11 @@ test.describe( 'Product Collection', () => {
|
||||||
await admin.createNewPost();
|
await admin.createNewPost();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test( 'does not render', async ( { page, editor, pageObject } ) => {
|
test.skip( 'does not render', async ( {
|
||||||
|
page,
|
||||||
|
editor,
|
||||||
|
pageObject,
|
||||||
|
} ) => {
|
||||||
await pageObject.insertProductCollection();
|
await pageObject.insertProductCollection();
|
||||||
await pageObject.chooseCollectionInPost( 'featured' );
|
await pageObject.chooseCollectionInPost( 'featured' );
|
||||||
await pageObject.addFilter( 'Price Range' );
|
await pageObject.addFilter( 'Price Range' );
|
||||||
|
|
|
@ -235,7 +235,7 @@ test.describe( 'Product Collection registration', () => {
|
||||||
await expect( previewButtonLocator ).toBeHidden();
|
await expect( previewButtonLocator ).toBeHidden();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test( 'Should display properly in Product Catalog template', async ( {
|
test.skip( 'Should display properly in Product Catalog template', async ( {
|
||||||
pageObject,
|
pageObject,
|
||||||
editor,
|
editor,
|
||||||
} ) => {
|
} ) => {
|
||||||
|
@ -357,7 +357,7 @@ test.describe( 'Product Collection registration', () => {
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test( 'Product picker should be shown when selected product is deleted', async ( {
|
test.skip( 'Product picker should be shown when selected product is deleted', async ( {
|
||||||
pageObject,
|
pageObject,
|
||||||
admin,
|
admin,
|
||||||
editor,
|
editor,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"core": null,
|
"core": "https://wordpress.org/wordpress-latest.zip",
|
||||||
"phpVersion": "7.4",
|
"phpVersion": "7.4",
|
||||||
"plugins": [ "." ],
|
"plugins": [ "." ],
|
||||||
"config": {
|
"config": {
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Adds unified styles for the express checkout block
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Synchronise the express payment controls between the Cart & Checkout blocks
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Improve the express checkout experience with several design tweak, uniform button styles and editor improvements
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Available express payment methods are visible in the editor when selecting the express payment block
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: tweak
|
||||||
|
Comment: This is part of a bigger feature that will have its own changelog entry
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: tweak
|
||||||
|
|
||||||
|
UX improvements to the express payment block in the editor
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Update In-App Marketplace category selector
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Replace marketplace search component with SearchControl from @wordpress/components
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Fix the loading state for the In-App Marketplace search
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Expand the e2e suite we're running on WPCOM part #3.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Added a Load More button to product lists on the Extensions page, to request additional search results from WooCommerce.com.
|
|
@ -1,4 +0,0 @@
|
||||||
Significance: patch
|
|
||||||
Type: fix
|
|
||||||
|
|
||||||
Fix bug where manually triggering `added_to_cart` event without a button element caused an Exception.
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: tweak
|
||||||
|
Comment: Fix typo (task-list-completed.tsx)
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: tweak
|
||||||
|
Comment: This PR skips 4 e2e tests and makes no other changes
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: dev
|
||||||
|
|
||||||
|
Exclude skipped e2e test issues from stale bot closure
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Wrap parse_str under a check to resolve deprecation notice
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Fix k6 performance test that checks for incorrect title when viewing order page
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Change the way search results are displayed in the in-app marketplace
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add search result counts to the in-app marketplace header tabs (Extensions area)
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
[ Experimental ] Moved experimental product dataviews menu to Product top menu.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add global_unique_id parameter to products REST API
|
|
@ -125,6 +125,7 @@ class WC_Product_CSV_Importer_Controller {
|
||||||
|
|
||||||
// Check that file is within an allowed location.
|
// Check that file is within an allowed location.
|
||||||
if ( $is_valid_file ) {
|
if ( $is_valid_file ) {
|
||||||
|
$normalized_path = wp_normalize_path( $path );
|
||||||
$in_valid_location = false;
|
$in_valid_location = false;
|
||||||
$valid_locations = array();
|
$valid_locations = array();
|
||||||
$valid_locations[] = ABSPATH;
|
$valid_locations[] = ABSPATH;
|
||||||
|
@ -135,7 +136,8 @@ class WC_Product_CSV_Importer_Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ( $valid_locations as $valid_location ) {
|
foreach ( $valid_locations as $valid_location ) {
|
||||||
if ( 0 === stripos( $path, trailingslashit( realpath( $valid_location ) ) ) ) {
|
$normalized_location = wp_normalize_path( realpath( $valid_location ) );
|
||||||
|
if ( 0 === stripos( $normalized_path, trailingslashit( $normalized_location ) ) ) {
|
||||||
$in_valid_location = true;
|
$in_valid_location = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -658,18 +658,18 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
|
||||||
/**
|
/**
|
||||||
* Action to signal that the value of 'stock_quantity' for a variation is about to change.
|
* Action to signal that the value of 'stock_quantity' for a variation is about to change.
|
||||||
*
|
*
|
||||||
* @param WC_Product $product The variation whose stock is about to change.
|
|
||||||
*
|
|
||||||
* @since 4.9
|
* @since 4.9
|
||||||
|
*
|
||||||
|
* @param int $product The variation whose stock is about to change.
|
||||||
*/
|
*/
|
||||||
do_action( 'woocommerce_variation_before_set_stock', $product );
|
do_action( 'woocommerce_variation_before_set_stock', $product );
|
||||||
} else {
|
} else {
|
||||||
/**
|
/**
|
||||||
* Action to signal that the value of 'stock_quantity' for a product is about to change.
|
* Action to signal that the value of 'stock_quantity' for a product is about to change.
|
||||||
*
|
*
|
||||||
* @param WC_Product $product The product whose stock is about to change.
|
|
||||||
*
|
|
||||||
* @since 4.9
|
* @since 4.9
|
||||||
|
*
|
||||||
|
* @param int $product The product whose stock is about to change.
|
||||||
*/
|
*/
|
||||||
do_action( 'woocommerce_product_before_set_stock', $product );
|
do_action( 'woocommerce_product_before_set_stock', $product );
|
||||||
}
|
}
|
||||||
|
|
|
@ -259,6 +259,18 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $request['global_unique_id'] ) ) {
|
||||||
|
$global_unique_ids = array_map( 'trim', explode( ',', $request['global_unique_id'] ) );
|
||||||
|
$args['meta_query'] = $this->add_meta_query( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||||
|
$args,
|
||||||
|
array(
|
||||||
|
'key' => '_global_unique_id',
|
||||||
|
'value' => $global_unique_ids,
|
||||||
|
'compare' => 'IN',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Filter by tax class.
|
// Filter by tax class.
|
||||||
if ( ! empty( $request['tax_class'] ) ) {
|
if ( ! empty( $request['tax_class'] ) ) {
|
||||||
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
|
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
|
||||||
|
|
|
@ -243,9 +243,30 @@ function wc_trigger_stock_change_notifications( $order, $changes ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$order_notes = array();
|
$order_notes = array();
|
||||||
|
$no_stock_amount = absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) );
|
||||||
|
|
||||||
foreach ( $changes as $change ) {
|
foreach ( $changes as $change ) {
|
||||||
$order_notes[] = $change['product']->get_formatted_name() . ' ' . $change['from'] . '→' . $change['to'];
|
$order_notes[] = $change['product']->get_formatted_name() . ' ' . $change['from'] . '→' . $change['to'];
|
||||||
|
$low_stock_amount = absint( wc_get_low_stock_amount( wc_get_product( $change['product']->get_id() ) ) );
|
||||||
|
if ( $change['to'] <= $no_stock_amount ) {
|
||||||
|
/**
|
||||||
|
* Action to signal that the value of 'stock_quantity' for a variation is about to change.
|
||||||
|
*
|
||||||
|
* @since 4.9
|
||||||
|
*
|
||||||
|
* @param int $product The variation whose stock is about to change.
|
||||||
|
*/
|
||||||
|
do_action( 'woocommerce_no_stock', wc_get_product( $change['product']->get_id() ) );
|
||||||
|
} elseif ( $change['to'] <= $low_stock_amount ) {
|
||||||
|
/**
|
||||||
|
* Action to signal that the value of 'stock_quantity' for a product is about to change.
|
||||||
|
*
|
||||||
|
* @since 4.9
|
||||||
|
*
|
||||||
|
* @param int $product The product whose stock is about to change.
|
||||||
|
*/
|
||||||
|
do_action( 'woocommerce_low_stock', wc_get_product( $change['product']->get_id() ) );
|
||||||
|
}
|
||||||
|
|
||||||
if ( $change['to'] < 0 ) {
|
if ( $change['to'] < 0 ) {
|
||||||
/**
|
/**
|
||||||
|
@ -312,8 +333,6 @@ function wc_trigger_stock_change_actions( $product ) {
|
||||||
do_action( 'woocommerce_low_stock', $product );
|
do_action( 'woocommerce_low_stock', $product );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
add_action( 'woocommerce_variation_set_stock', 'wc_trigger_stock_change_actions' );
|
|
||||||
add_action( 'woocommerce_product_set_stock', 'wc_trigger_stock_change_actions' );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increase stock levels for items within an order.
|
* Increase stock levels for items within an order.
|
||||||
|
@ -485,11 +504,8 @@ function wc_get_low_stock_amount( WC_Product $product ) {
|
||||||
$low_stock_amount = $product->get_low_stock_amount();
|
$low_stock_amount = $product->get_low_stock_amount();
|
||||||
|
|
||||||
if ( '' === $low_stock_amount && $product->is_type( 'variation' ) ) {
|
if ( '' === $low_stock_amount && $product->is_type( 'variation' ) ) {
|
||||||
$parent_product = wc_get_product( $product->get_parent_id() );
|
$product = wc_get_product( $product->get_parent_id() );
|
||||||
|
$low_stock_amount = $product->get_low_stock_amount();
|
||||||
if ( $parent_product instanceof WC_Product ) {
|
|
||||||
$low_stock_amount = $parent_product->get_low_stock_amount();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( '' === $low_stock_amount ) {
|
if ( '' === $low_stock_amount ) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ Tags: online store, ecommerce, shop, shopping cart, sell online
|
||||||
Requires at least: 6.5
|
Requires at least: 6.5
|
||||||
Tested up to: 6.6
|
Tested up to: 6.6
|
||||||
Requires PHP: 7.4
|
Requires PHP: 7.4
|
||||||
Stable tag: 9.2.3
|
Stable tag: 9.3.2
|
||||||
License: GPLv3
|
License: GPLv3
|
||||||
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
|
|
@ -109,9 +109,9 @@ class Init {
|
||||||
}
|
}
|
||||||
$ptype_obj = get_post_type_object( 'product' );
|
$ptype_obj = get_post_type_object( 'product' );
|
||||||
add_submenu_page(
|
add_submenu_page(
|
||||||
'woocommerce',
|
'edit.php?post_type=product',
|
||||||
$ptype_obj->labels->name,
|
$ptype_obj->labels->name,
|
||||||
esc_html__( 'All Products', 'woocommerce' ),
|
esc_html__( 'All Products ( new )', 'woocommerce' ),
|
||||||
'manage_woocommerce',
|
'manage_woocommerce',
|
||||||
'woocommerce-products-dashboard',
|
'woocommerce-products-dashboard',
|
||||||
array( $this, 'woocommerce_products_dashboard' ),
|
array( $this, 'woocommerce_products_dashboard' ),
|
||||||
|
|
|
@ -154,13 +154,15 @@ class WCAdminHelper {
|
||||||
'post_type' => 'product',
|
'post_type' => 'product',
|
||||||
);
|
);
|
||||||
|
|
||||||
parse_str( wp_parse_url( $url, PHP_URL_QUERY ), $url_params );
|
$query_string = wp_parse_url( $url, PHP_URL_QUERY );
|
||||||
|
if ( $query_string ) {
|
||||||
|
parse_str( $query_string, $url_params );
|
||||||
foreach ( $params as $key => $param ) {
|
foreach ( $params as $key => $param ) {
|
||||||
if ( isset( $url_params[ $key ] ) && $url_params[ $key ] === $param ) {
|
if ( isset( $url_params[ $key ] ) && $url_params[ $key ] === $param ) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WC store pages.
|
// WC store pages.
|
||||||
$store_pages = array(
|
$store_pages = array(
|
||||||
|
|
|
@ -11,4 +11,18 @@ class CartExpressPaymentBlock extends AbstractInnerBlock {
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $block_name = 'cart-express-payment-block';
|
protected $block_name = 'cart-express-payment-block';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uniform default_styles for the express payment buttons
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
protected $default_styles = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current styles for the express payment buttons
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
protected $current_styles = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||||
|
|
||||||
|
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CheckoutExpressPaymentBlock class.
|
* CheckoutExpressPaymentBlock class.
|
||||||
*/
|
*/
|
||||||
|
@ -11,4 +14,128 @@ class CheckoutExpressPaymentBlock extends AbstractInnerBlock {
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $block_name = 'checkout-express-payment-block';
|
protected $block_name = 'checkout-express-payment-block';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default styles for the express payment buttons
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
protected $default_styles = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current styles for the express payment buttons
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
protected $current_styles = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise the block
|
||||||
|
*/
|
||||||
|
protected function initialize() {
|
||||||
|
parent::initialize();
|
||||||
|
|
||||||
|
$this->default_styles = array(
|
||||||
|
'showButtonStyles' => false,
|
||||||
|
'buttonHeight' => '48',
|
||||||
|
'buttonBorderRadius' => '4',
|
||||||
|
);
|
||||||
|
|
||||||
|
add_action( 'save_post', array( $this, 'sync_express_payment_attrs' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchorize the express payment attributes between the Cart and Checkout pages.
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID.
|
||||||
|
* @param WP_Post $post Post object.
|
||||||
|
*/
|
||||||
|
public function sync_express_payment_attrs( $post_id, $post ) {
|
||||||
|
if ( wc_get_page_id( 'cart' ) === $post_id ) {
|
||||||
|
$cart_or_checkout = 'cart';
|
||||||
|
} elseif ( wc_get_page_id( 'checkout' ) === $post_id ) {
|
||||||
|
$cart_or_checkout = 'checkout';
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is not a proper save action, maybe an autosave, so don't continue.
|
||||||
|
if ( empty( $post->post_status ) || 'inherit' === $post->post_status ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$block_name = 'woocommerce/' . $cart_or_checkout;
|
||||||
|
$page_id = 'woocommerce_' . $cart_or_checkout . '_page_id';
|
||||||
|
$template_name = 'page-' . $cart_or_checkout;
|
||||||
|
|
||||||
|
// Check if we are editing the cart/checkout page and that it contains a Cart/Checkout block.
|
||||||
|
// Cast to string for Cart/Checkout page ID comparison because get_option can return it as a string, so better to compare both values as strings.
|
||||||
|
if ( ! empty( $post->post_type ) && 'wp_template' !== $post->post_type && ( false === has_block( $block_name, $post ) || (string) get_option( $page_id ) !== (string) $post_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we are editing the Cart/Checkout template and that it contains a Cart/Checkout block.
|
||||||
|
if ( ( ! empty( $post->post_type ) && ! empty( $post->post_name ) && $template_name !== $post->post_name && 'wp_template' === $post->post_type ) || false === has_block( $block_name, $post ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $post->post_content ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse the post content to get the express payment attributes of the current page.
|
||||||
|
$blocks = parse_blocks( $post->post_content );
|
||||||
|
$attrs = CartCheckoutUtils::find_express_checkout_attributes( $blocks, $cart_or_checkout );
|
||||||
|
|
||||||
|
if ( ! is_array( $attrs ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$updated_attrs = array_merge( $this->default_styles, $attrs );
|
||||||
|
|
||||||
|
// We need to sync the attributes between the Cart and Checkout pages.
|
||||||
|
$other_page = 'cart' === $cart_or_checkout ? 'checkout' : 'cart';
|
||||||
|
|
||||||
|
$this->update_other_page_with_express_payment_attrs( $other_page, $updated_attrs );
|
||||||
|
} catch ( Exception $e ) {
|
||||||
|
wc_get_logger()->log( 'error', 'Error updating express payment attributes: ' . $e->getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the express payment attributes in the other page (Cart or Checkout).
|
||||||
|
*
|
||||||
|
* @param string $cart_or_checkout The page to update.
|
||||||
|
* @param array $updated_attrs The updated attributes.
|
||||||
|
*/
|
||||||
|
private function update_other_page_with_express_payment_attrs( $cart_or_checkout, $updated_attrs ) {
|
||||||
|
$page_id = 'cart' === $cart_or_checkout ? wc_get_page_id( 'cart' ) : wc_get_page_id( 'checkout' );
|
||||||
|
|
||||||
|
if ( -1 === $page_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$post = get_post( $page_id );
|
||||||
|
|
||||||
|
if ( empty( $post->post_content ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$blocks = parse_blocks( $post->post_content );
|
||||||
|
CartCheckoutUtils::update_blocks_with_new_attrs( $blocks, $cart_or_checkout, $updated_attrs );
|
||||||
|
|
||||||
|
$updated_content = serialize_blocks( $blocks );
|
||||||
|
remove_action( 'save_post', array( $this, 'sync_express_payment_attrs' ), 10, 2 );
|
||||||
|
|
||||||
|
wp_update_post(
|
||||||
|
array(
|
||||||
|
'ID' => $page_id,
|
||||||
|
'post_content' => $updated_content,
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
add_action( 'save_post', array( $this, 'sync_express_payment_attrs' ), 10, 2 );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -481,7 +481,7 @@ class BlockTemplateUtils {
|
||||||
* @return boolean
|
* @return boolean
|
||||||
*/
|
*/
|
||||||
public static function theme_has_template( $template_name ) {
|
public static function theme_has_template( $template_name ) {
|
||||||
return ! ! self::get_theme_template_path( $template_name, 'wp_template' );
|
return (bool) self::get_theme_template_path( $template_name, 'wp_template' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -491,7 +491,7 @@ class BlockTemplateUtils {
|
||||||
* @return boolean
|
* @return boolean
|
||||||
*/
|
*/
|
||||||
public static function theme_has_template_part( $template_name ) {
|
public static function theme_has_template_part( $template_name ) {
|
||||||
return ! ! self::get_theme_template_path( $template_name, 'wp_template_part' );
|
return (bool) self::get_theme_template_path( $template_name, 'wp_template_part' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -108,7 +108,7 @@ class CartCheckoutUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
$array_without_accents = array_map(
|
$array_without_accents = array_map(
|
||||||
function( $value ) {
|
function ( $value ) {
|
||||||
return is_array( $value )
|
return is_array( $value )
|
||||||
? self::deep_sort_with_accents( $value )
|
? self::deep_sort_with_accents( $value )
|
||||||
: remove_accents( wc_strtolower( html_entity_decode( $value ) ) );
|
: remove_accents( wc_strtolower( html_entity_decode( $value ) ) );
|
||||||
|
@ -129,7 +129,7 @@ class CartCheckoutUtils {
|
||||||
$shipping_zones = \WC_Shipping_Zones::get_zones();
|
$shipping_zones = \WC_Shipping_Zones::get_zones();
|
||||||
$formatted_shipping_zones = array_reduce(
|
$formatted_shipping_zones = array_reduce(
|
||||||
$shipping_zones,
|
$shipping_zones,
|
||||||
function( $acc, $zone ) {
|
function ( $acc, $zone ) {
|
||||||
$acc[] = [
|
$acc[] = [
|
||||||
'id' => $zone['id'],
|
'id' => $zone['id'],
|
||||||
'title' => $zone['zone_name'],
|
'title' => $zone['zone_name'],
|
||||||
|
@ -146,4 +146,47 @@ class CartCheckoutUtils {
|
||||||
];
|
];
|
||||||
return $formatted_shipping_zones;
|
return $formatted_shipping_zones;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively search the checkout block to find the express checkout block and
|
||||||
|
* get the button style attributes
|
||||||
|
*
|
||||||
|
* @param array $blocks Blocks to search.
|
||||||
|
* @param string $cart_or_checkout The block type to check.
|
||||||
|
*/
|
||||||
|
public static function find_express_checkout_attributes( $blocks, $cart_or_checkout ) {
|
||||||
|
$express_block_name = 'woocommerce/' . $cart_or_checkout . '-express-payment-block';
|
||||||
|
foreach ( $blocks as $block ) {
|
||||||
|
if ( ! empty( $block['blockName'] ) && $express_block_name === $block['blockName'] && ! empty( $block['attrs'] ) ) {
|
||||||
|
return $block['attrs'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $block['innerBlocks'] ) ) {
|
||||||
|
$answer = self::find_express_checkout_attributes( $block['innerBlocks'], $cart_or_checkout );
|
||||||
|
if ( $answer ) {
|
||||||
|
return $answer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an array of blocks, find the express payment block and update its attributes.
|
||||||
|
*
|
||||||
|
* @param array $blocks Blocks to search.
|
||||||
|
* @param string $cart_or_checkout The block type to check.
|
||||||
|
* @param array $updated_attrs The new attributes to set.
|
||||||
|
*/
|
||||||
|
public static function update_blocks_with_new_attrs( &$blocks, $cart_or_checkout, $updated_attrs ) {
|
||||||
|
$express_block_name = 'woocommerce/' . $cart_or_checkout . '-express-payment-block';
|
||||||
|
foreach ( $blocks as $key => &$block ) {
|
||||||
|
if ( ! empty( $block['blockName'] ) && $express_block_name === $block['blockName'] ) {
|
||||||
|
$blocks[ $key ]['attrs'] = $updated_attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $block['innerBlocks'] ) ) {
|
||||||
|
self::update_blocks_with_new_attrs( $block['innerBlocks'], $cart_or_checkout, $updated_attrs );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,16 @@ config = {
|
||||||
'**/merchant/create-order.spec.js',
|
'**/merchant/create-order.spec.js',
|
||||||
'**/merchant/create-page.spec.js',
|
'**/merchant/create-page.spec.js',
|
||||||
'**/merchant/create-post.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/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',
|
||||||
],
|
],
|
||||||
grepInvert: /@skip-on-default-wpcom/,
|
grepInvert: /@skip-on-default-wpcom/,
|
||||||
},
|
},
|
||||||
|
|
|
@ -103,7 +103,10 @@ const test = baseTest.extend( {
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
test.describe(
|
||||||
|
'Restricted coupon management',
|
||||||
|
{ tag: [ '@services', '@skip-on-default-wpcom' ] },
|
||||||
|
() => {
|
||||||
for ( const couponType of Object.keys( couponData ) ) {
|
for ( const couponType of Object.keys( couponData ) ) {
|
||||||
test( `can create new ${ couponType } coupon`, async ( {
|
test( `can create new ${ couponType } coupon`, async ( {
|
||||||
page,
|
page,
|
||||||
|
@ -124,7 +127,9 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder( '0' )
|
.getByPlaceholder( '0' )
|
||||||
.fill( couponData[ couponType ].amount );
|
.fill( couponData[ couponType ].amount );
|
||||||
await expect( page.getByText( 'Move to Trash' ) ).toBeVisible();
|
await expect(
|
||||||
|
page.getByText( 'Move to Trash' )
|
||||||
|
).toBeVisible();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// set up the restrictions for each coupon type
|
// set up the restrictions for each coupon type
|
||||||
|
@ -213,7 +218,10 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
||||||
const skipBrandsTests = true;
|
const skipBrandsTests = true;
|
||||||
|
|
||||||
// set exclude product brands
|
// set exclude product brands
|
||||||
if ( couponType === 'excludeProductBrands' && ! skipBrandsTests ) {
|
if (
|
||||||
|
couponType === 'excludeProductBrands' &&
|
||||||
|
! skipBrandsTests
|
||||||
|
) {
|
||||||
await test.step( 'set exclude product brands coupon', async () => {
|
await test.step( 'set exclude product brands coupon', async () => {
|
||||||
await page
|
await page
|
||||||
.getByRole( 'link', {
|
.getByRole( 'link', {
|
||||||
|
@ -224,7 +232,9 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
||||||
.getByPlaceholder( 'No brands' )
|
.getByPlaceholder( 'No brands' )
|
||||||
.pressSequentially( 'WooCommerce Apparels' );
|
.pressSequentially( 'WooCommerce Apparels' );
|
||||||
await page
|
await page
|
||||||
.getByRole( 'option', { name: 'WooCommerce Apparels' } )
|
.getByRole( 'option', {
|
||||||
|
name: 'WooCommerce Apparels',
|
||||||
|
} )
|
||||||
.click();
|
.click();
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
@ -272,7 +282,9 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
||||||
.click();
|
.click();
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder( 'No restrictions' )
|
.getByPlaceholder( 'No restrictions' )
|
||||||
.fill( couponData[ couponType ].allowedEmails[ 0 ] );
|
.fill(
|
||||||
|
couponData[ couponType ].allowedEmails[ 0 ]
|
||||||
|
);
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
// set usage limit
|
// set usage limit
|
||||||
|
@ -312,7 +324,9 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
||||||
|
|
||||||
// verify the creation of the coupon and basic details
|
// verify the creation of the coupon and basic details
|
||||||
await test.step( 'verify coupon creation', async () => {
|
await test.step( 'verify coupon creation', async () => {
|
||||||
await page.goto( 'wp-admin/edit.php?post_type=shop_coupon' );
|
await page.goto(
|
||||||
|
'wp-admin/edit.php?post_type=shop_coupon'
|
||||||
|
);
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole( 'cell', {
|
page.getByRole( 'cell', {
|
||||||
name: couponData[ couponType ].code,
|
name: couponData[ couponType ].code,
|
||||||
|
@ -491,9 +505,12 @@ test.describe( 'Restricted coupon management', { tag: [ '@services' ] }, () => {
|
||||||
.click();
|
.click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByLabel( 'Usage limit per user' )
|
page.getByLabel( 'Usage limit per user' )
|
||||||
).toHaveValue( couponData[ couponType ].usageLimitPerUser );
|
).toHaveValue(
|
||||||
|
couponData[ couponType ].usageLimitPerUser
|
||||||
|
);
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
} );
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -60,8 +60,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
// this shipping zone already exists, don't create it
|
// this shipping zone already exists, don't create it
|
||||||
} else {
|
} else {
|
||||||
await page.goto(
|
await page.goto(
|
||||||
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new',
|
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new'
|
||||||
{ waitUntil: 'networkidle' }
|
|
||||||
);
|
);
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder( 'Zone name' )
|
.getByPlaceholder( 'Zone name' )
|
||||||
|
@ -92,10 +91,8 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
.getByRole( 'button', { name: 'Continue' } )
|
.getByRole( 'button', { name: 'Continue' } )
|
||||||
.last()
|
.last()
|
||||||
.click();
|
.click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await page.locator( '#btn-ok' ).click();
|
await page.locator( '#btn-ok' ).click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
|
@ -132,8 +129,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
// this shipping zone already exists, don't create it
|
// this shipping zone already exists, don't create it
|
||||||
} else {
|
} else {
|
||||||
await page.goto(
|
await page.goto(
|
||||||
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new',
|
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new'
|
||||||
{ waitUntil: 'networkidle' }
|
|
||||||
);
|
);
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder( 'Zone name' )
|
.getByPlaceholder( 'Zone name' )
|
||||||
|
@ -159,10 +155,8 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
.getByRole( 'button', { name: 'Continue' } )
|
.getByRole( 'button', { name: 'Continue' } )
|
||||||
.last()
|
.last()
|
||||||
.click();
|
.click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await page.locator( '#btn-ok' ).click();
|
await page.locator( '#btn-ok' ).click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
|
@ -196,8 +190,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
// this shipping zone already exists, don't create it
|
// this shipping zone already exists, don't create it
|
||||||
} else {
|
} else {
|
||||||
await page.goto(
|
await page.goto(
|
||||||
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new',
|
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new'
|
||||||
{ waitUntil: 'networkidle' }
|
|
||||||
);
|
);
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder( 'Zone name' )
|
.getByPlaceholder( 'Zone name' )
|
||||||
|
@ -209,7 +202,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
input.click();
|
input.click();
|
||||||
input.fill( 'Canada' );
|
input.fill( 'Canada' );
|
||||||
|
|
||||||
await page.getByText( 'Canada' ).last().click();
|
await page.getByLabel( 'Canada', { exact: true } ).click();
|
||||||
|
|
||||||
// Close dropdown
|
// Close dropdown
|
||||||
await page.getByPlaceholder( 'Zone name' ).click();
|
await page.getByPlaceholder( 'Zone name' ).click();
|
||||||
|
@ -222,10 +215,8 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
.getByRole( 'button', { name: 'Continue' } )
|
.getByRole( 'button', { name: 'Continue' } )
|
||||||
.last()
|
.last()
|
||||||
.click();
|
.click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await page.locator( '#btn-ok' ).click();
|
await page.locator( '#btn-ok' ).click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
|
@ -240,7 +231,6 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
.click();
|
.click();
|
||||||
await page.getByLabel( 'Cost', { exact: true } ).fill( '10' );
|
await page.getByLabel( 'Cost', { exact: true } ).fill( '10' );
|
||||||
await page.getByRole( 'button', { name: 'Save' } ).last().click();
|
await page.getByRole( 'button', { name: 'Save' } ).last().click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await page.goto(
|
await page.goto(
|
||||||
'wp-admin/admin.php?page=wc-settings&tab=shipping'
|
'wp-admin/admin.php?page=wc-settings&tab=shipping'
|
||||||
|
@ -342,8 +332,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
// this shipping zone already exists, don't create it
|
// this shipping zone already exists, don't create it
|
||||||
} else {
|
} else {
|
||||||
await page.goto(
|
await page.goto(
|
||||||
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new',
|
'wp-admin/admin.php?page=wc-settings&tab=shipping&zone_id=new'
|
||||||
{ waitUntil: 'networkidle' }
|
|
||||||
);
|
);
|
||||||
await page.locator( '#zone_name' ).fill( shippingZoneNameFlatRate );
|
await page.locator( '#zone_name' ).fill( shippingZoneNameFlatRate );
|
||||||
|
|
||||||
|
@ -353,7 +342,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
input.click();
|
input.click();
|
||||||
input.type( 'Canada' );
|
input.type( 'Canada' );
|
||||||
|
|
||||||
await page.getByText( 'Canada' ).last().click();
|
await page.getByLabel( 'Canada', { exact: true } ).click();
|
||||||
|
|
||||||
// Close dropdown
|
// Close dropdown
|
||||||
await page.keyboard.press( 'Escape' );
|
await page.keyboard.press( 'Escape' );
|
||||||
|
@ -366,10 +355,7 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
.last()
|
.last()
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await page.locator( '#btn-ok' ).click();
|
await page.locator( '#btn-ok' ).click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
|
@ -384,13 +370,17 @@ test.describe( 'WooCommerce Shipping Settings - Add new shipping zone', () => {
|
||||||
.click();
|
.click();
|
||||||
await page.locator( '#woocommerce_flat_rate_cost' ).fill( '10' );
|
await page.locator( '#woocommerce_flat_rate_cost' ).fill( '10' );
|
||||||
await page.locator( '#btn-ok' ).click();
|
await page.locator( '#btn-ok' ).click();
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
|
|
||||||
await page.locator( 'text=Delete' ).waitFor();
|
await expect(
|
||||||
|
page.getByRole( 'cell', { name: 'Edit | Delete', exact: true } )
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
page.on( 'dialog', ( dialog ) => dialog.accept() );
|
page.on( 'dialog', ( dialog ) => dialog.accept() );
|
||||||
|
|
||||||
await page.locator( 'text=Delete' ).click();
|
await page
|
||||||
|
.getByRole( 'cell', { name: 'Edit | Delete', exact: true } )
|
||||||
|
.locator( 'text=Delete' )
|
||||||
|
.click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator( '.wc-shipping-zone-method-blank-state' )
|
page.locator( '.wc-shipping-zone-method-blank-state' )
|
||||||
|
@ -482,7 +472,6 @@ test.describe( 'Verifies shipping options from customer perspective', () => {
|
||||||
await context.clearCookies();
|
await context.clearCookies();
|
||||||
|
|
||||||
await page.goto( `/shop/?add-to-cart=${ productId }` );
|
await page.goto( `/shop/?add-to-cart=${ productId }` );
|
||||||
await page.waitForLoadState( 'networkidle' );
|
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test.afterAll( async ( { baseURL } ) => {
|
test.afterAll( async ( { baseURL } ) => {
|
||||||
|
|
|
@ -52,7 +52,14 @@ const test = baseTest.extend( {
|
||||||
|
|
||||||
test.describe(
|
test.describe(
|
||||||
'Add WooCommerce Blocks Into Page',
|
'Add WooCommerce Blocks Into Page',
|
||||||
{ tag: [ '@gutenberg', '@services', '@skip-on-default-pressable' ] },
|
{
|
||||||
|
tag: [
|
||||||
|
'@gutenberg',
|
||||||
|
'@services',
|
||||||
|
'@skip-on-default-pressable',
|
||||||
|
'@skip-on-default-wpcom',
|
||||||
|
],
|
||||||
|
},
|
||||||
() => {
|
() => {
|
||||||
test.beforeAll( async ( { api } ) => {
|
test.beforeAll( async ( { api } ) => {
|
||||||
// add product attribute
|
// add product attribute
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue