Merge branch 'trunk' into add/49335-related-products-collection
This commit is contained in:
commit
1447c48ac4
|
@ -31,7 +31,7 @@ jobs:
|
||||||
run: unzip plugins/woocommerce/woocommerce.zip -d zipfile
|
run: unzip plugins/woocommerce/woocommerce.zip -d zipfile
|
||||||
|
|
||||||
- name: Upload the zip file as an artifact
|
- name: Upload the zip file as an artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
name: Performance metrics
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'plugins/woocommerce/composer.*'
|
||||||
|
- 'plugins/woocommerce/client/admin/config/**'
|
||||||
|
- 'plugins/woocommerce/includes/**'
|
||||||
|
- 'plugins/woocommerce/lib/**'
|
||||||
|
- 'plugins/woocommerce/patterns/**'
|
||||||
|
- 'plugins/woocommerce/src/**'
|
||||||
|
- 'plugins/woocommerce/templates/**'
|
||||||
|
- 'plugins/woocommerce/tests/metrics/**'
|
||||||
|
- 'plugins/woocommerce/.wp-env.json'
|
||||||
|
- '.github/workflows/pr-assess-performance.yml'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
WP_ARTIFACTS_PATH: ${{ github.workspace }}/tools/compare-perf/artifacts/
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
benchmark:
|
||||||
|
name: Evaluate performance metrics
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
name: Checkout (${{ github.event_name == 'pull_request' && github.head_ref || github.sha }})
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup-woocommerce-monorepo
|
||||||
|
name: Install Monorepo
|
||||||
|
with:
|
||||||
|
install: '@woocommerce/plugin-woocommerce...'
|
||||||
|
build: '@woocommerce/plugin-woocommerce'
|
||||||
|
build-type: 'full'
|
||||||
|
pull-playwright-cache: true
|
||||||
|
pull-package-deps: '@woocommerce/plugin-woocommerce'
|
||||||
|
|
||||||
|
#TODO: Inject WordPress version as per plugin requirements (relying to defaults currently).
|
||||||
|
- name: Start Test Environment
|
||||||
|
run: |
|
||||||
|
pnpm --filter="@woocommerce/plugin-woocommerce" test:e2e:install &
|
||||||
|
pnpm --filter="@woocommerce/plugin-woocommerce" env:test
|
||||||
|
|
||||||
|
# TODO: cache results if pushed to trunk
|
||||||
|
- name: Measure performance (@${{ github.sha }})
|
||||||
|
run: |
|
||||||
|
RESULTS_ID="editor_${{ github.sha }}_round-1" pnpm --filter="@woocommerce/plugin-woocommerce" test:metrics editor
|
||||||
|
RESULTS_ID="product-editor_${{ github.sha }}_round-1" pnpm --filter="@woocommerce/plugin-woocommerce" test:metrics product-editor
|
||||||
|
|
||||||
|
# In alignment with .github/workflows/scripts/run-metrics.sh, we should checkout 3d7d7f02017383937f1a4158d433d0e5d44b3dc9
|
||||||
|
# as baseline. But to avoid switching branches in 'Analyze results' step, we pick 55f855a2e6d769b5ae44305b2772eb30d3e721df
|
||||||
|
# which introduced reporting mode for the perf utility.
|
||||||
|
- name: Checkout (55f855a2e6d769b5ae44305b2772eb30d3e721df@trunk, further references as 'baseline')
|
||||||
|
run: |
|
||||||
|
git reset --hard && git checkout 55f855a2e6d769b5ae44305b2772eb30d3e721df
|
||||||
|
echo "WC_TRUNK_SHA=55f855a2e6d769b5ae44305b2772eb30d3e721df" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Artifacts download/upload would be more reliable, but we couldn't make it working...
|
||||||
|
- uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319
|
||||||
|
name: Cache measurements (baseline)
|
||||||
|
with:
|
||||||
|
path: tools/compare-perf/artifacts/*_${{ env.WC_TRUNK_SHA }}_*
|
||||||
|
key: ${{ runner.os }}-woocommerce-performance-measures-${{ env.WC_TRUNK_SHA }}
|
||||||
|
|
||||||
|
- name: Verify cached measurements (baseline)
|
||||||
|
run: |
|
||||||
|
if test -n "$(find tools/compare-perf/artifacts/ -maxdepth 1 -name '*_${{ env.WC_TRUNK_SHA }}_*' -print -quit)"
|
||||||
|
then
|
||||||
|
echo "WC_MEASURE_BASELINE=no" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
ls -l tools/compare-perf/artifacts/
|
||||||
|
echo "Triggering baseline benchmarking"
|
||||||
|
echo "WC_MEASURE_BASELINE=yes" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build (baseline)
|
||||||
|
if: ${{ env.WC_MEASURE_BASELINE == 'yes' }}
|
||||||
|
run: |
|
||||||
|
git clean -n -d -X ./packages ./plugins | grep -v vendor | grep -v node_modules | sed -e 's/Would remove //g' | tr '\n' '\0' | xargs -0 rm -r
|
||||||
|
pnpm install --filter='@woocommerce/plugin-woocommerce...' --frozen-lockfile --config.dedupe-peer-dependents=false
|
||||||
|
pnpm --filter='@woocommerce/plugin-woocommerce' build
|
||||||
|
|
||||||
|
#TODO: is baseline Wordpress version changes, restart environment targeting it.
|
||||||
|
|
||||||
|
- name: Measure performance (@${{ env.WC_TRUNK_SHA }})
|
||||||
|
if: ${{ env.WC_MEASURE_BASELINE == 'yes' }}
|
||||||
|
run: |
|
||||||
|
RESULTS_ID="editor_${{ env.WC_TRUNK_SHA }}_round-1" pnpm --filter="@woocommerce/plugin-woocommerce" test:metrics editor
|
||||||
|
RESULTS_ID="product-editor_${{ env.WC_TRUNK_SHA }}_round-1" pnpm --filter="@woocommerce/plugin-woocommerce" test:metrics product-editor
|
||||||
|
|
||||||
|
- name: Analyze results
|
||||||
|
run: |
|
||||||
|
pnpm install --filter='compare-perf...' --frozen-lockfile --config.dedupe-peer-dependents=false
|
||||||
|
pnpm --filter="compare-perf" run compare compare-performance ${{ github.sha }} ${{ env.WC_TRUNK_SHA }} --tests-branch ${{ github.sha }} --skip-benchmarking
|
||||||
|
|
||||||
|
# TODO: Publish to CodeVitals (see .github/workflows/scripts/run-metrics.sh) if pushed to trunk
|
|
@ -186,7 +186,7 @@ jobs:
|
||||||
run: bash bin/build-zip.sh
|
run: bash bin/build-zip.sh
|
||||||
|
|
||||||
- name: Upload the zip file as an artifact
|
- name: Upload the zip file as an artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
@ -216,7 +216,7 @@ jobs:
|
||||||
run: bash bin/build-zip.sh
|
run: bash bin/build-zip.sh
|
||||||
|
|
||||||
- name: Upload the zip file as an artifact
|
- name: Upload the zip file as an artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
@ -231,7 +231,7 @@ jobs:
|
||||||
if: ${{ needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' }}
|
if: ${{ needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' }}
|
||||||
steps:
|
steps:
|
||||||
- id: download
|
- id: download
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
@ -279,7 +279,7 @@ jobs:
|
||||||
working-directory: tools/monorepo-utils
|
working-directory: tools/monorepo-utils
|
||||||
|
|
||||||
- id: download
|
- id: download
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
@ -300,7 +300,7 @@ jobs:
|
||||||
if: ${{ needs.code-freeze-prep.outputs.isTodayAcceleratedFreeze == 'yes' }}
|
if: ${{ needs.code-freeze-prep.outputs.isTodayAcceleratedFreeze == 'yes' }}
|
||||||
steps:
|
steps:
|
||||||
- id: download
|
- id: download
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
@ -348,7 +348,7 @@ jobs:
|
||||||
working-directory: tools/monorepo-utils
|
working-directory: tools/monorepo-utils
|
||||||
|
|
||||||
- id: download
|
- id: download
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
@ -369,7 +369,7 @@ jobs:
|
||||||
if: ${{ needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' }}
|
if: ${{ needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' }}
|
||||||
steps:
|
steps:
|
||||||
- id: download
|
- id: download
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
@ -380,7 +380,7 @@ jobs:
|
||||||
run: unzip ${{ steps.download.outputs.download-path }}/woocommerce.zip -d zipfile
|
run: unzip ${{ steps.download.outputs.download-path }}/woocommerce.zip -d zipfile
|
||||||
|
|
||||||
- name: Upload the zip file as an artifact
|
- name: Upload the zip file as an artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
@ -395,7 +395,7 @@ jobs:
|
||||||
if: ${{ needs.code-freeze-prep.outputs.isTodayAcceleratedFreeze == 'yes' }}
|
if: ${{ needs.code-freeze-prep.outputs.isTodayAcceleratedFreeze == 'yes' }}
|
||||||
steps:
|
steps:
|
||||||
- id: download
|
- id: download
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
@ -406,7 +406,7 @@ jobs:
|
||||||
run: unzip ${{ steps.download.outputs.download-path }}/woocommerce.zip -d zipfile
|
run: unzip ${{ steps.download.outputs.download-path }}/woocommerce.zip -d zipfile
|
||||||
|
|
||||||
- name: Upload the zip file as an artifact
|
- name: Upload the zip file as an artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
name: Storybook GitHub Pages
|
name: Storybook GitHub Pages
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '30 2 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
# The hook documentation: https://git-scm.com/docs/githooks.html#_post_checkout
|
||||||
|
CHECKOUT_TYPE=$3
|
||||||
|
HEAD_NEW=$2
|
||||||
|
HEAD_PREVIOUS=$1
|
||||||
|
|
||||||
|
whiteColoured='\033[0m'
|
||||||
|
orangeColoured='\033[1;33m'
|
||||||
|
|
||||||
|
# '1' is a branch checkout
|
||||||
|
if [ "$CHECKOUT_TYPE" = '1' ]; then
|
||||||
|
# Prompt about pnpm versions mismatch when switching between branches.
|
||||||
|
currentPnpmVersion=$( ( command -v pnpm > /dev/null && pnpm -v 2>/dev/null ) || echo 'n/a' )
|
||||||
|
targetPnpmVersion=$( grep packageManager package.json | sed -nr 's/.+packageManager.+pnpm@([[:digit:].]+).+/\1/p' )
|
||||||
|
if [ "$currentPnpmVersion" != "$targetPnpmVersion" ]; then
|
||||||
|
printf "${orangeColoured}pnpm versions mismatch: in use '$currentPnpmVersion', needed '$targetPnpmVersion'. If you are working on something in this branch, here are some hints on how to solve this:\n"
|
||||||
|
printf "${orangeColoured}* actualize environment: 'nvm use && pnpm -v' (the most common case)\n"
|
||||||
|
printf "${orangeColoured}* install: 'npm install -g pnpm@$targetPnpmVersion'\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Auto-refresh dependencies when switching between branches.
|
||||||
|
changedManifests=$( ( git diff --name-only $HEAD_NEW $HEAD_PREVIOUS | grep -E '(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|composer.json|composer.lock)$' ) || echo '' )
|
||||||
|
if [ -n "$changedManifests" ]; then
|
||||||
|
printf "${whiteColoured}The following file(s) in the new branch differs from the original one, dependencies might need to be refreshed:\n"
|
||||||
|
printf "${whiteColoured} %s\n" $changedManifests
|
||||||
|
printf "${orangeColoured}If you are working on something in this branch, ensure to refresh dependencies with 'pnpm install --frozen-lockfile'\n"
|
||||||
|
fi
|
||||||
|
fi
|
|
@ -1,6 +1,8 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
# The hook documentation: https://git-scm.com/docs/githooks.html#_post_merge
|
||||||
|
|
||||||
changedManifests=$( ( git diff --name-only HEAD ORIG_HEAD | grep -E '(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|composer.json|composer.lock)$' ) || echo '' )
|
changedManifests=$( ( git diff --name-only HEAD ORIG_HEAD | grep -E '(package.json|pnpm-lock.yaml|pnpm-workspace.yaml|composer.json|composer.lock)$' ) || echo '' )
|
||||||
if [ -n "$changedManifests" ]; then
|
if [ -n "$changedManifests" ]; then
|
||||||
printf "It was a change in the following file(s) - refreshing dependencies:\n"
|
printf "It was a change in the following file(s) - refreshing dependencies:\n"
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
"MD003": { "style": "atx" },
|
"MD003": { "style": "atx" },
|
||||||
"MD007": { "indent": 4 },
|
"MD007": { "indent": 4 },
|
||||||
"MD013": { "line_length": 9999 },
|
"MD013": { "line_length": 9999 },
|
||||||
"MD024": { "allow_different_nesting": true },
|
"MD024": { "siblings_only": true },
|
||||||
"MD033": { "allowed_elements": ["video"] },
|
"MD033": { "allowed_elements": [ "video" ] },
|
||||||
"no-hard-tabs": false,
|
"no-hard-tabs": false,
|
||||||
"whitespace": false
|
"whitespace": false
|
||||||
}
|
}
|
||||||
|
|
2
.npmrc
2
.npmrc
|
@ -1,3 +1,5 @@
|
||||||
; adding this as npm 7 automatically installs peer dependencies but pnpm does not
|
; adding this as npm 7 automatically installs peer dependencies but pnpm does not
|
||||||
auto-install-peers=true
|
auto-install-peers=true
|
||||||
strict-peer-dependencies=false
|
strict-peer-dependencies=false
|
||||||
|
; See https://github.com/pnpm/pnpm/pull/8363 (we adding the setting now, to not miss when migrating to pnpm 9.7+)
|
||||||
|
manage-package-manager-versions=true
|
||||||
|
|
|
@ -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 )`
|
||||||
|
|
||||||
|
@ -139,23 +159,24 @@ 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` |
|
||||||
| `checkoutStatus` | Object | The current checkout status exposed as various boolean state. | `isCalculating`, `isComplete`, `isIdle`, `isProcessing` |
|
| `checkoutStatus` | Object | The current checkout status exposed as various boolean state. | `isCalculating`, `isComplete`, `isIdle`, `isProcessing` |
|
||||||
| `components` | Object | It exposes React components that can be implemented by your payment method for various common interface elements used by payment methods. | <!-- markdownlint-disable-line no-inline-html --><ul><li>`ValidationInputError`: a container for holding validation errors which typically you'll include after any inputs.</li><li>[`PaymentMethodLabel`](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e089ae17043fa525e8397d605f0f470959f2ae95/assets/js/payment-method-extensions/payment-methods/paypal/index.js#L37-L40): use this component for the payment method label, including an optional icon.</li><li>`PaymentMethodIcons`: a React component used for displaying payment method icons.</li><li>- `LoadingMask`: a wrapper component that handles displaying a loading state when the isLoading prop is true. Exposes the [LoadingMask component](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/c9074a4941919987dbad16a80f358b960336a09d/assets/js/base/components/loading-mask/index.js)</li></ul> |
|
| `components` | Object | It exposes React components that can be implemented by your payment method for various common interface elements used by payment methods. | <!-- markdownlint-disable-line no-inline-html --><ul><li>`ValidationInputError`: a container for holding validation errors which typically you'll include after any inputs.</li><li>[`PaymentMethodLabel`](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e089ae17043fa525e8397d605f0f470959f2ae95/assets/js/payment-method-extensions/payment-methods/paypal/index.js#L37-L40): use this component for the payment method label, including an optional icon.</li><li>`PaymentMethodIcons`: a React component used for displaying payment method icons.</li><li>- `LoadingMask`: a wrapper component that handles displaying a loading state when the isLoading prop is true. Exposes the [LoadingMask component](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/c9074a4941919987dbad16a80f358b960336a09d/assets/js/base/components/loading-mask/index.js)</li></ul> |
|
||||||
| `emitResponse` | Object | Contains some constants that can be helpful when using the event emitter. Read the _[Emitting Events](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e267cd96a4329a4eeef816b2ef627e113ebb72a5/docs/extensibility/checkout-flow-and-events.md#emitting-events)_ section for more details. | <!-- markdownlint-disable-line no-inline-html --><ul><li>`noticeContexts`: This is an object containing properties referencing areas where notices can be targeted in the checkout. The object has the following properties: <ul><li>`PAYMENTS`: This is a reference to the notice area in the payment methods step.</li><li>`EXPRESS_PAYMENTS`: This is a reference to the notice area in the express payment methods step.</li></ul></li><li>`responseTypes`: This is an object containing properties referencing the various response types that can be returned by observers for some event emitters. It makes it easier for autocompleting the types and avoiding typos due to human error. The types are `SUCCESS`, `FAIL`, `ERROR`. The values for these types also correspond to the [payment status types](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/src/Payments/PaymentResult.php#L21) from the [checkout endpoint response from the server](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/src/RestApi/StoreApi/Schemas/CheckoutSchema.php#L103-L113).</li></ul> |
|
| `emitResponse` | Object | Contains some constants that can be helpful when using the event emitter. Read the _[Emitting Events](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/e267cd96a4329a4eeef816b2ef627e113ebb72a5/docs/extensibility/checkout-flow-and-events.md#emitting-events)_ section for more details. | <!-- markdownlint-disable-line no-inline-html --><ul><li>`noticeContexts`: This is an object containing properties referencing areas where notices can be targeted in the checkout. The object has the following properties: <ul><li>`PAYMENTS`: This is a reference to the notice area in the payment methods step.</li><li>`EXPRESS_PAYMENTS`: This is a reference to the notice area in the express payment methods step.</li></ul></li><li>`responseTypes`: This is an object containing properties referencing the various response types that can be returned by observers for some event emitters. It makes it easier for autocompleting the types and avoiding typos due to human error. The types are `SUCCESS`, `FAIL`, `ERROR`. The values for these types also correspond to the [payment status types](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/src/Payments/PaymentResult.php#L21) from the [checkout endpoint response from the server](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/src/RestApi/StoreApi/Schemas/CheckoutSchema.php#L103-L113).</li></ul> |
|
||||||
| `eventRegistration` | object | Contains all the checkout event emitter registration functions. These are functions the payment method can register observers on to interact with various points in the checkout flow (see [this doc](./checkout-flow-and-events.md) for more info). | `onCheckoutValidation`, `onCheckoutSuccess`, `onCheckoutFail`, `onPaymentSetup`, `onShippingRateSuccess`, `onShippingRateFail`, `onShippingRateSelectSuccess`, `onShippingRateSelectFail` |
|
| `eventRegistration` | object | Contains all the checkout event emitter registration functions. These are functions the payment method can register observers on to interact with various points in the checkout flow (see [this doc](./checkout-flow-and-events.md) for more info). | `onCheckoutValidation`, `onCheckoutSuccess`, `onCheckoutFail`, `onPaymentSetup`, `onShippingRateSuccess`, `onShippingRateFail`, `onShippingRateSelectSuccess`, `onShippingRateSelectFail` |
|
||||||
| `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` |
|
||||||
| `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. | - |
|
| `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) |
|
||||||
| `shippingData` | Object | Contains all shipping related data (outside of the shipping status). | `shippingRates`, `shippingRatesLoading`, `selectedRates`, `setSelectedRates`, `isSelectingRate`, `shippingAddress`, `setShippingAddress`, and `needsShipping` |
|
| `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` |
|
||||||
| `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> |
|
||||||
| `shouldSavePayment` | Boolean | Indicates whether or not the shopper has selected to save their payment method details (for payment methods that support saved payments). True if selected, false otherwise. Defaults to false. | - |
|
| `shouldSavePayment` | Boolean | Indicates whether or not the shopper has selected to save their payment method details (for payment methods that support saved payments). True if selected, false otherwise. Defaults to false. | - |
|
||||||
|
|
||||||
- `isPristine`: This is true when the current payment status is `PRISTINE`.
|
- `isPristine`: This is true when the current payment status is `PRISTINE`.
|
||||||
- `isStarted`: This is true when the current payment status is `EXPRESS_STARTED`.
|
- `isStarted`: This is true when the current payment status is `EXPRESS_STARTED`.
|
||||||
|
@ -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": {
|
||||||
|
@ -1059,7 +1059,7 @@
|
||||||
"menu_title": "DOM Events",
|
"menu_title": "DOM Events",
|
||||||
"tags": "how-to",
|
"tags": "how-to",
|
||||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-collection-block/dom-events.md",
|
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-collection-block/dom-events.md",
|
||||||
"hash": "59a4b49eb146774d33229bc60ab7d8f74381493f6e7089ca8f0e2d0eb433a7a4",
|
"hash": "85cffe1cc273621f16f7362b5efe28ede9689cee0a6e87d0d426014bacc24b05",
|
||||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-collection-block/dom-events.md",
|
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-collection-block/dom-events.md",
|
||||||
"id": "c8d247b91472740075871e6b57a9583d893ac650"
|
"id": "c8d247b91472740075871e6b57a9583d893ac650"
|
||||||
}
|
}
|
||||||
|
@ -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": "472f5a240abe907fec83a8a9f88af6699f2d994aa7ae87faa1716a087baa66db",
|
"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": "dfe48a2a48d383c1f4e5b5bb4258d369dd4e80ac8582462b16bbfedd879cb0bf"
|
"hash": "a88d9ea54465c8bbd820042a92df79cbd48943e785b418fcaa04d0c0e66116c0"
|
||||||
}
|
}
|
|
@ -10,13 +10,13 @@ tags: how-to
|
||||||
|
|
||||||
This event is triggered when Product Collection block was rendered or re-rendered (e.g. due to page change).
|
This event is triggered when Product Collection block was rendered or re-rendered (e.g. due to page change).
|
||||||
|
|
||||||
### `wc-blocks_product_list_rendered` - `detail` parameters
|
### `detail` parameters
|
||||||
|
|
||||||
| Parameter | Type | Default value | Description |
|
| Parameter | Type | Default value | Description |
|
||||||
| ------------------ | ------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------ | ------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `collection` | string | `undefined` | Collection type. It's `undefined` for "create your own" collections as the type is not specified. For other Core collections it can be one of type: `woocommerce/product-collection/best-sellers`, `woocommerce/product-collection/featured`, `woocommerce/product-collection/new-arrivals`, `woocommerce/product-collection/on-sale`, `woocommerce/product-collection/top-rated`. For custom collections it will hold their name. |
|
| `collection` | string | `undefined` | Collection type. It's `undefined` for "create your own" collections as the type is not specified. For other Core collections it can be one of type: `woocommerce/product-collection/best-sellers`, `woocommerce/product-collection/featured`, `woocommerce/product-collection/new-arrivals`, `woocommerce/product-collection/on-sale`, `woocommerce/product-collection/top-rated`. For custom collections it will hold their name. |
|
||||||
|
|
||||||
### `wc-blocks_product_list_rendered` - Example usage
|
### Example usage
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
window.document.addEventListener(
|
window.document.addEventListener(
|
||||||
|
@ -32,14 +32,14 @@ window.document.addEventListener(
|
||||||
|
|
||||||
This event is triggered when some blocks are clicked in order to view product (redirect to product page).
|
This event is triggered when some blocks are clicked in order to view product (redirect to product page).
|
||||||
|
|
||||||
### `wc-blocks_viewed_product` - `detail` parameters
|
### `detail` parameters
|
||||||
|
|
||||||
| Parameter | Type | Default value | Description |
|
| Parameter | Type | Default value | Description |
|
||||||
| ------------------ | ------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------ | ------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `collection` | string | `undefined` | Collection type. It's `undefined` for "create your own" collections as the type is not specified. For other Core collections it can be one of type: `woocommerce/product-collection/best-sellers`, `woocommerce/product-collection/featured`, `woocommerce/product-collection/new-arrivals`, `woocommerce/product-collection/on-sale`, `woocommerce/product-collection/top-rated`. For custom collections it will hold their name. |
|
| `collection` | string | `undefined` | Collection type. It's `undefined` for "create your own" collections as the type is not specified. For other Core collections it can be one of type: `woocommerce/product-collection/best-sellers`, `woocommerce/product-collection/featured`, `woocommerce/product-collection/new-arrivals`, `woocommerce/product-collection/on-sale`, `woocommerce/product-collection/top-rated`. For custom collections it will hold their name. |
|
||||||
| `productId` | number | | Product ID |
|
| `productId` | number | | Product ID |
|
||||||
|
|
||||||
### `wc-blocks_viewed_product` Example usage
|
### Example usage
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
window.document.addEventListener(
|
window.document.addEventListener(
|
||||||
|
|
|
@ -147,13 +147,14 @@ These flows will continually evolve as the platform evolves with flows updated,
|
||||||
### Merchant - Settings
|
### Merchant - Settings
|
||||||
|
|
||||||
| User Type | Flow Area | Flow Name | Test File |
|
| User Type | Flow Area | Flow Name | Test File |
|
||||||
| --------- | --------- | -------------------------------------- | ---------------------------------------- |
|
| --------- | --------- |----------------------------------------|------------------------------------------|
|
||||||
| Merchant | Settings | Update General Settings | merchant/settings-general.spec.js |
|
| Merchant | Settings | Update General Settings | merchant/settings-general.spec.js |
|
||||||
| Merchant | Settings | Add Tax Rates | merchant/settings-tax.spec.js |
|
| Merchant | Settings | Add Tax Rates | merchant/settings-tax.spec.js |
|
||||||
| Merchant | Settings | Add Shipping Zones | merchant/create-shipping-zones.spec.js |
|
| Merchant | Settings | Add Shipping Zones | merchant/create-shipping-zones.spec.js |
|
||||||
| Merchant | Settings | Add Shipping Classes | merchant/create-shipping-classes.spec.js |
|
| Merchant | Settings | Add Shipping Classes | merchant/create-shipping-classes.spec.js |
|
||||||
| Merchant | Settings | Enable local pickup for checkout block | merchant/settings-shipping.spec.js |
|
| Merchant | Settings | Enable local pickup for checkout block | merchant/settings-shipping.spec.js |
|
||||||
| Merchant | Settings | Update payment settings | admin-tasks/payment.spec.js |
|
| Merchant | Settings | Update payment settings | admin-tasks/payment.spec.js |
|
||||||
|
| Merchant | Settings | Handle Product Brands | merchant/create-product-brand.spec.js |
|
||||||
|
|
||||||
### Merchant - Coupons
|
### Merchant - Coupons
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Added pre-API call permission checks for some API calls that were being called on non-admin accessible screens
|
|
@ -10,11 +10,14 @@ import { apiFetch } from '@wordpress/data-controls';
|
||||||
import { NAMESPACE } from '../constants';
|
import { NAMESPACE } from '../constants';
|
||||||
import { setNotes, setNotesQuery, setError } from './actions';
|
import { setNotes, setNotesQuery, setError } from './actions';
|
||||||
import { NoteQuery, Note } from './types';
|
import { NoteQuery, Note } from './types';
|
||||||
|
import { checkUserCapability } from '../utils';
|
||||||
|
|
||||||
export function* getNotes( query: NoteQuery = {} ) {
|
export function* getNotes( query: NoteQuery = {} ) {
|
||||||
const url = addQueryArgs( `${ NAMESPACE }/admin/notes`, query );
|
const url = addQueryArgs( `${ NAMESPACE }/admin/notes`, query );
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
yield checkUserCapability( 'manage_woocommerce' );
|
||||||
|
|
||||||
const notes: Note[] = yield apiFetch( {
|
const notes: Note[] = yield apiFetch( {
|
||||||
path: url,
|
path: url,
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
TaskListType,
|
TaskListType,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { Plugin } from '../plugins/types';
|
import { Plugin } from '../plugins/types';
|
||||||
|
import { checkUserCapability } from '../utils';
|
||||||
|
|
||||||
const resolveSelect =
|
const resolveSelect =
|
||||||
controls && controls.resolveSelect ? controls.resolveSelect : select;
|
controls && controls.resolveSelect ? controls.resolveSelect : select;
|
||||||
|
@ -68,6 +69,8 @@ export function* getEmailPrefill() {
|
||||||
export function* getTaskLists() {
|
export function* getTaskLists() {
|
||||||
const deprecatedTasks = new DeprecatedTasks();
|
const deprecatedTasks = new DeprecatedTasks();
|
||||||
try {
|
try {
|
||||||
|
yield checkUserCapability( 'manage_woocommerce' );
|
||||||
|
|
||||||
const results: TaskListType[] = yield apiFetch( {
|
const results: TaskListType[] = yield apiFetch( {
|
||||||
path: WC_ADMIN_NAMESPACE + '/onboarding/tasks',
|
path: WC_ADMIN_NAMESPACE + '/onboarding/tasks',
|
||||||
method: deprecatedTasks.hasDeprecatedTasks() ? 'POST' : 'GET',
|
method: deprecatedTasks.hasDeprecatedTasks() ? 'POST' : 'GET',
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {
|
||||||
RecommendedTypes,
|
RecommendedTypes,
|
||||||
JetpackConnectionDataResponse,
|
JetpackConnectionDataResponse,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { checkUserCapability } from '../utils';
|
||||||
|
|
||||||
// Can be removed in WP 5.9, wp.data is supported in >5.7.
|
// Can be removed in WP 5.9, wp.data is supported in >5.7.
|
||||||
const resolveSelect =
|
const resolveSelect =
|
||||||
|
@ -61,6 +62,8 @@ type ConnectJetpackResponse = {
|
||||||
export function* getActivePlugins() {
|
export function* getActivePlugins() {
|
||||||
yield setIsRequesting( 'getActivePlugins', true );
|
yield setIsRequesting( 'getActivePlugins', true );
|
||||||
try {
|
try {
|
||||||
|
yield checkUserCapability( 'manage_woocommerce' );
|
||||||
|
|
||||||
const url = WC_ADMIN_NAMESPACE + '/plugins/active';
|
const url = WC_ADMIN_NAMESPACE + '/plugins/active';
|
||||||
const results: PluginGetResponse = yield apiFetch( {
|
const results: PluginGetResponse = yield apiFetch( {
|
||||||
path: url,
|
path: url,
|
||||||
|
@ -77,6 +80,8 @@ export function* getInstalledPlugins() {
|
||||||
yield setIsRequesting( 'getInstalledPlugins', true );
|
yield setIsRequesting( 'getInstalledPlugins', true );
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
yield checkUserCapability( 'manage_woocommerce' );
|
||||||
|
|
||||||
const url = WC_ADMIN_NAMESPACE + '/plugins/installed';
|
const url = WC_ADMIN_NAMESPACE + '/plugins/installed';
|
||||||
const results: PluginGetResponse = yield apiFetch( {
|
const results: PluginGetResponse = yield apiFetch( {
|
||||||
path: url,
|
path: url,
|
||||||
|
@ -111,6 +116,8 @@ export function* getJetpackConnectionData() {
|
||||||
yield setIsRequesting( 'getJetpackConnectionData', true );
|
yield setIsRequesting( 'getJetpackConnectionData', true );
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
yield checkUserCapability( 'manage_woocommerce' );
|
||||||
|
|
||||||
const url = JETPACK_NAMESPACE + '/connection/data';
|
const url = JETPACK_NAMESPACE + '/connection/data';
|
||||||
|
|
||||||
const results: JetpackConnectionDataResponse = yield apiFetch( {
|
const results: JetpackConnectionDataResponse = yield apiFetch( {
|
||||||
|
|
|
@ -2,14 +2,15 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { addQueryArgs } from '@wordpress/url';
|
import { addQueryArgs } from '@wordpress/url';
|
||||||
import { apiFetch } from '@wordpress/data-controls';
|
import { apiFetch, select } from '@wordpress/data-controls';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { BaseQueryParams } from './types/query-params';
|
import { BaseQueryParams } from './types/query-params';
|
||||||
import { fetchWithHeaders } from './controls';
|
import { fetchWithHeaders } from './controls';
|
||||||
|
import { USER_STORE_NAME } from './user';
|
||||||
|
import { WCUser } from './user/types';
|
||||||
function replacer( _: string, value: unknown ) {
|
function replacer( _: string, value: unknown ) {
|
||||||
if ( value ) {
|
if ( value ) {
|
||||||
if ( Array.isArray( value ) ) {
|
if ( Array.isArray( value ) ) {
|
||||||
|
@ -100,3 +101,20 @@ export function* request< Query extends BaseQueryParams, DataType >(
|
||||||
return { items: response.data, totalCount };
|
return { items: response.data, totalCount };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to check if the current user has a specific capability.
|
||||||
|
*
|
||||||
|
* @param {string} capability - The capability to check (e.g. 'manage_woocommerce').
|
||||||
|
* @throws {Error} If the user does not have the required capability.
|
||||||
|
*/
|
||||||
|
export function* checkUserCapability( capability: string ) {
|
||||||
|
const currentUser: WCUser< 'capabilities' > = yield select(
|
||||||
|
USER_STORE_NAME,
|
||||||
|
'getCurrentUser'
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! currentUser.capabilities[ capability ] ) {
|
||||||
|
throw new Error( `User does not have ${ capability } capability.` );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -269,7 +269,8 @@ export const ActivityPanel = ( { isEmbedded, query } ) => {
|
||||||
visible:
|
visible:
|
||||||
( isEmbedded || ! isHomescreen ) &&
|
( isEmbedded || ! isHomescreen ) &&
|
||||||
! isPerformingSetupTask() &&
|
! isPerformingSetupTask() &&
|
||||||
! isProductScreen(),
|
! isProductScreen() &&
|
||||||
|
currentUserCan( 'manage_woocommerce' ),
|
||||||
};
|
};
|
||||||
|
|
||||||
const feedback = {
|
const feedback = {
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { GlobalStylesRenderer } from '@wordpress/edit-site/build-module/componen
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
import { trackEvent } from '../tracking';
|
||||||
import { editorIsLoaded } from '../utils';
|
import { editorIsLoaded } from '../utils';
|
||||||
import { BlockEditorContainer } from './block-editor-container';
|
import { BlockEditorContainer } from './block-editor-container';
|
||||||
|
|
||||||
|
@ -63,6 +64,7 @@ export const Editor = ( { isLoading }: { isLoading: boolean } ) => {
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
if ( ! isLoading ) {
|
if ( ! isLoading ) {
|
||||||
editorIsLoaded();
|
editorIsLoaded();
|
||||||
|
trackEvent( 'customize_your_store_assembler_hub_editor_loaded' );
|
||||||
}
|
}
|
||||||
}, [ isLoading ] );
|
}, [ isLoading ] );
|
||||||
|
|
||||||
|
|
|
@ -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,149 +33,350 @@ 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 tagProductsWithType = (
|
||||||
const params = new URLSearchParams();
|
products: Product[],
|
||||||
if ( category !== '' ) {
|
type: ProductType
|
||||||
params.append( 'category', category );
|
): Product[] => {
|
||||||
}
|
return products.map( ( product ) => ( {
|
||||||
|
...product,
|
||||||
|
type,
|
||||||
|
} ) );
|
||||||
|
};
|
||||||
|
|
||||||
const wccomSettings = getAdminSetting( 'wccomHelper', false );
|
const loadMoreProducts = useCallback( () => {
|
||||||
if ( wccomSettings.storeCountry ) {
|
setIsLoadingMore( true );
|
||||||
params.append( 'country', wccomSettings.storeCountry );
|
const params = new URLSearchParams();
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
useEffect( () => {
|
|
||||||
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 ) {
|
|
||||||
params.append(
|
|
||||||
'category',
|
|
||||||
query.category === '_all' ? '' : query.category
|
|
||||||
);
|
|
||||||
} else if ( query?.tab === 'themes' ) {
|
|
||||||
params.append( 'category', 'themes' );
|
|
||||||
} else if ( query?.tab === 'business-services' ) {
|
|
||||||
params.append( 'category', 'business-services' );
|
|
||||||
} else if ( query?.tab === 'search' ) {
|
|
||||||
params.append( 'category', 'extensions-themes-business-services' );
|
|
||||||
}
|
|
||||||
|
|
||||||
const wccomSettings = getAdminSetting( 'wccomHelper', false );
|
const wccomSettings = getAdminSetting( 'wccomHelper', false );
|
||||||
if ( wccomSettings.storeCountry ) {
|
if ( wccomSettings.storeCountry ) {
|
||||||
params.append( 'country', wccomSettings.storeCountry );
|
params.append( 'country', wccomSettings.storeCountry );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
params.append( 'page', ( currentPage + 1 ).toString() );
|
||||||
|
|
||||||
fetchSearchResults( params, abortController.signal )
|
fetchSearchResults( params, abortController.signal )
|
||||||
.then( ( productList ) => {
|
.then( ( productList ) => {
|
||||||
setProducts( 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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( newProducts.length > 0 ) {
|
||||||
|
setFirstNewProductId( newProducts[ 0 ].id ?? 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedProducts = [
|
||||||
|
...flattenedPrevProducts,
|
||||||
|
...newProducts,
|
||||||
|
];
|
||||||
|
|
||||||
|
return combinedProducts;
|
||||||
|
} );
|
||||||
|
|
||||||
|
speak( __( 'More products loaded', 'woocommerce' ) );
|
||||||
|
setCurrentPage( ( prevPage ) => prevPage + 1 );
|
||||||
|
setIsLoadingMore( false );
|
||||||
} )
|
} )
|
||||||
.catch( () => {
|
.catch( () => {
|
||||||
setProducts( [] );
|
speak( __( 'Error loading more products', 'woocommerce' ) );
|
||||||
} )
|
} )
|
||||||
.finally( () => {
|
.finally( () => {
|
||||||
// we are recording both the new and legacy events here for now
|
setIsLoadingMore( false );
|
||||||
// they're separate methods to make it easier to remove the legacy one later
|
|
||||||
const marketplaceViewProps = {
|
|
||||||
view: query?.tab,
|
|
||||||
search_term: query?.term,
|
|
||||||
product_type: query?.section,
|
|
||||||
category: query?.category,
|
|
||||||
};
|
|
||||||
|
|
||||||
recordMarketplaceView( marketplaceViewProps );
|
|
||||||
recordLegacyTabView( marketplaceViewProps );
|
|
||||||
setIsLoading( false );
|
|
||||||
} );
|
} );
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
abortController.abort();
|
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 );
|
||||||
|
if ( wccomSettings.storeCountry ) {
|
||||||
|
params.append( 'country', wccomSettings.storeCountry );
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchSearchResults( params, abortControllers[ 0 ].signal )
|
||||||
|
.then( ( productList ) => {
|
||||||
|
setAllProducts( productList.products );
|
||||||
|
setTotalPagesCategory( productList.totalPages );
|
||||||
|
setSearchResultsCount( {
|
||||||
|
[ query.tab ]: productList.totalProducts,
|
||||||
|
} );
|
||||||
|
|
||||||
|
searchCompleteAnnouncement( productList.totalProducts );
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
setAllProducts( [] );
|
||||||
|
} )
|
||||||
|
.finally( () => {
|
||||||
|
setIsLoading( false );
|
||||||
|
} );
|
||||||
|
} 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.term,
|
||||||
query.category,
|
query.category,
|
||||||
query?.tab,
|
setHasBusinessServices,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
query?.section,
|
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 = {
|
||||||
|
view: query?.tab,
|
||||||
|
search_term: query?.term,
|
||||||
|
product_type: query?.section,
|
||||||
|
category: query?.category,
|
||||||
|
};
|
||||||
|
recordMarketplaceView( marketplaceViewProps );
|
||||||
|
recordLegacyTabView( marketplaceViewProps );
|
||||||
|
}, [ query?.tab, query?.term, query?.section, query?.category ] );
|
||||||
|
|
||||||
|
// Reset current page when tab, term, or category changes
|
||||||
|
useEffect( () => {
|
||||||
|
setCurrentPage( 1 );
|
||||||
|
setFirstNewProductId( 0 );
|
||||||
|
}, [ selectedTab, query?.category, query?.term ] );
|
||||||
|
|
||||||
|
// Maintain product focus for accessibility
|
||||||
|
useEffect( () => {
|
||||||
|
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 }
|
{ props.categorySelector && (
|
||||||
</h2>
|
<CategorySelector type={ props.type } />
|
||||||
) }
|
) }
|
||||||
<div className="woocommerce-marketplace__sub-header">
|
</div>
|
||||||
{ props.categorySelector && (
|
|
||||||
<CategorySelector type={ props.type } />
|
|
||||||
) }
|
|
||||||
{ 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"
|
value={ searchTerm }
|
||||||
>
|
onChange={ setSearchTerm }
|
||||||
{ placeholder }
|
onKeyUp={ handleKeyUp }
|
||||||
</label>
|
className="woocommerce-marketplace__search"
|
||||||
<input
|
/>
|
||||||
id="woocommerce-marketplace-search-query"
|
|
||||||
value={ searchTerm }
|
|
||||||
className="woocommerce-marketplace__search-input"
|
|
||||||
type="search"
|
|
||||||
name="woocommerce-marketplace-search-query"
|
|
||||||
placeholder={ placeholder }
|
|
||||||
onChange={ handleInputChange }
|
|
||||||
onKeyUp={ handleKeyUp }
|
|
||||||
/>
|
|
||||||
<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';
|
||||||
|
|
|
@ -100,7 +100,10 @@ function log_remote_event() {
|
||||||
time(),
|
time(),
|
||||||
'critical',
|
'critical',
|
||||||
'Test PHP event from WC Beta Tester',
|
'Test PHP event from WC Beta Tester',
|
||||||
array( 'source' => 'wc-beta-tester' )
|
array(
|
||||||
|
'source' => 'wc-beta-tester',
|
||||||
|
'remote-logging' => true,
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( $result ) {
|
if ( $result ) {
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: tweak
|
||||||
|
|
||||||
|
Set remote-logging context to true in log remote event method
|
|
@ -25,7 +25,11 @@ interface CheckoutAddress {
|
||||||
setBillingAddress: ( data: Partial< BillingAddress > ) => void;
|
setBillingAddress: ( data: Partial< BillingAddress > ) => void;
|
||||||
setEmail: ( value: string ) => void;
|
setEmail: ( value: string ) => void;
|
||||||
useShippingAsBilling: boolean;
|
useShippingAsBilling: boolean;
|
||||||
|
editingBillingAddress: boolean;
|
||||||
|
editingShippingAddress: boolean;
|
||||||
setUseShippingAsBilling: ( useShippingAsBilling: boolean ) => void;
|
setUseShippingAsBilling: ( useShippingAsBilling: boolean ) => void;
|
||||||
|
setEditingBillingAddress: ( isEditing: boolean ) => void;
|
||||||
|
setEditingShippingAddress: ( isEditing: boolean ) => void;
|
||||||
defaultFields: AddressFields;
|
defaultFields: AddressFields;
|
||||||
showShippingFields: boolean;
|
showShippingFields: boolean;
|
||||||
showBillingFields: boolean;
|
showBillingFields: boolean;
|
||||||
|
@ -40,15 +44,25 @@ interface CheckoutAddress {
|
||||||
*/
|
*/
|
||||||
export const useCheckoutAddress = (): CheckoutAddress => {
|
export const useCheckoutAddress = (): CheckoutAddress => {
|
||||||
const { needsShipping } = useShippingData();
|
const { needsShipping } = useShippingData();
|
||||||
const { useShippingAsBilling, prefersCollection } = useSelect(
|
const {
|
||||||
( select ) => ( {
|
useShippingAsBilling,
|
||||||
useShippingAsBilling:
|
prefersCollection,
|
||||||
select( CHECKOUT_STORE_KEY ).getUseShippingAsBilling(),
|
editingBillingAddress,
|
||||||
prefersCollection: select( CHECKOUT_STORE_KEY ).prefersCollection(),
|
editingShippingAddress,
|
||||||
} )
|
} = useSelect( ( select ) => ( {
|
||||||
);
|
useShippingAsBilling:
|
||||||
const { __internalSetUseShippingAsBilling } =
|
select( CHECKOUT_STORE_KEY ).getUseShippingAsBilling(),
|
||||||
useDispatch( CHECKOUT_STORE_KEY );
|
prefersCollection: select( CHECKOUT_STORE_KEY ).prefersCollection(),
|
||||||
|
editingBillingAddress:
|
||||||
|
select( CHECKOUT_STORE_KEY ).getEditingBillingAddress(),
|
||||||
|
editingShippingAddress:
|
||||||
|
select( CHECKOUT_STORE_KEY ).getEditingShippingAddress(),
|
||||||
|
} ) );
|
||||||
|
const {
|
||||||
|
__internalSetUseShippingAsBilling,
|
||||||
|
setEditingBillingAddress,
|
||||||
|
setEditingShippingAddress,
|
||||||
|
} = useDispatch( CHECKOUT_STORE_KEY );
|
||||||
const {
|
const {
|
||||||
billingAddress,
|
billingAddress,
|
||||||
setBillingAddress,
|
setBillingAddress,
|
||||||
|
@ -77,6 +91,10 @@ export const useCheckoutAddress = (): CheckoutAddress => {
|
||||||
defaultFields,
|
defaultFields,
|
||||||
useShippingAsBilling,
|
useShippingAsBilling,
|
||||||
setUseShippingAsBilling: __internalSetUseShippingAsBilling,
|
setUseShippingAsBilling: __internalSetUseShippingAsBilling,
|
||||||
|
editingBillingAddress,
|
||||||
|
editingShippingAddress,
|
||||||
|
setEditingBillingAddress,
|
||||||
|
setEditingShippingAddress,
|
||||||
needsShipping,
|
needsShipping,
|
||||||
showShippingFields:
|
showShippingFields:
|
||||||
! forcedBillingAddress && needsShipping && ! prefersCollection,
|
! forcedBillingAddress && needsShipping && ! prefersCollection,
|
||||||
|
|
|
@ -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 }>
|
||||||
<Block className={ className } />
|
<ExpressPaymentControls
|
||||||
|
attributes={ attributes }
|
||||||
|
setAttributes={ setAttributes }
|
||||||
|
/>
|
||||||
|
<ExpressPaymentContext.Provider
|
||||||
|
value={ { showButtonStyles, buttonHeight, buttonBorderRadius } }
|
||||||
|
>
|
||||||
|
<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;
|
||||||
|
|
|
@ -7,14 +7,12 @@ import {
|
||||||
useCheckoutAddress,
|
useCheckoutAddress,
|
||||||
useEditorContext,
|
useEditorContext,
|
||||||
noticeContexts,
|
noticeContexts,
|
||||||
useShippingData,
|
|
||||||
} from '@woocommerce/base-context';
|
} from '@woocommerce/base-context';
|
||||||
import Noninteractive from '@woocommerce/base-components/noninteractive';
|
import Noninteractive from '@woocommerce/base-components/noninteractive';
|
||||||
import type { ShippingAddress, FormFieldsConfig } from '@woocommerce/settings';
|
import type { ShippingAddress, FormFieldsConfig } from '@woocommerce/settings';
|
||||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||||
import { useSelect } from '@wordpress/data';
|
import { useSelect } from '@wordpress/data';
|
||||||
import { CART_STORE_KEY } from '@woocommerce/block-data';
|
import { CART_STORE_KEY } from '@woocommerce/block-data';
|
||||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -36,14 +34,9 @@ const Block = ( {
|
||||||
showPhoneField: boolean;
|
showPhoneField: boolean;
|
||||||
requirePhoneField: boolean;
|
requirePhoneField: boolean;
|
||||||
} ): JSX.Element => {
|
} ): JSX.Element => {
|
||||||
const {
|
const { billingAddress, setShippingAddress, useBillingAsShipping } =
|
||||||
shippingAddress,
|
useCheckoutAddress();
|
||||||
billingAddress,
|
|
||||||
setShippingAddress,
|
|
||||||
useBillingAsShipping,
|
|
||||||
} = useCheckoutAddress();
|
|
||||||
const { isEditor } = useEditorContext();
|
const { isEditor } = useEditorContext();
|
||||||
const { needsShipping } = useShippingData();
|
|
||||||
|
|
||||||
// Syncs shipping address with billing address if "Force shipping to the customer billing address" is enabled.
|
// Syncs shipping address with billing address if "Force shipping to the customer billing address" is enabled.
|
||||||
useEffectOnce( () => {
|
useEffectOnce( () => {
|
||||||
|
@ -101,19 +94,6 @@ const Block = ( {
|
||||||
};
|
};
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// Default editing state for CustomerAddress component comes from the current address and whether or not we're in the editor.
|
|
||||||
const hasAddress = !! (
|
|
||||||
billingAddress.address_1 &&
|
|
||||||
( billingAddress.first_name || billingAddress.last_name )
|
|
||||||
);
|
|
||||||
const { email, ...billingAddressWithoutEmail } = billingAddress;
|
|
||||||
const billingMatchesShipping = isShallowEqual(
|
|
||||||
billingAddressWithoutEmail,
|
|
||||||
shippingAddress
|
|
||||||
);
|
|
||||||
const defaultEditingAddress =
|
|
||||||
isEditor || ! hasAddress || ( needsShipping && billingMatchesShipping );
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StoreNoticesContainer context={ noticeContext } />
|
<StoreNoticesContainer context={ noticeContext } />
|
||||||
|
@ -121,7 +101,6 @@ const Block = ( {
|
||||||
{ cartDataLoaded ? (
|
{ cartDataLoaded ? (
|
||||||
<CustomerAddress
|
<CustomerAddress
|
||||||
addressFieldsConfig={ addressFieldsConfig }
|
addressFieldsConfig={ addressFieldsConfig }
|
||||||
defaultEditing={ defaultEditingAddress }
|
|
||||||
/>
|
/>
|
||||||
) : null }
|
) : null }
|
||||||
</WrapperComponent>
|
</WrapperComponent>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useState, useCallback, useEffect } from '@wordpress/element';
|
import { useCallback, useEffect } from '@wordpress/element';
|
||||||
import { Form } from '@woocommerce/base-components/cart-checkout';
|
import { Form } from '@woocommerce/base-components/cart-checkout';
|
||||||
import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context';
|
import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context';
|
||||||
import type {
|
import type {
|
||||||
|
@ -20,19 +20,18 @@ import AddressCard from '../../address-card';
|
||||||
|
|
||||||
const CustomerAddress = ( {
|
const CustomerAddress = ( {
|
||||||
addressFieldsConfig,
|
addressFieldsConfig,
|
||||||
defaultEditing = false,
|
|
||||||
}: {
|
}: {
|
||||||
addressFieldsConfig: FormFieldsConfig;
|
addressFieldsConfig: FormFieldsConfig;
|
||||||
defaultEditing?: boolean;
|
|
||||||
} ) => {
|
} ) => {
|
||||||
const {
|
const {
|
||||||
billingAddress,
|
billingAddress,
|
||||||
setShippingAddress,
|
setShippingAddress,
|
||||||
setBillingAddress,
|
setBillingAddress,
|
||||||
useBillingAsShipping,
|
useBillingAsShipping,
|
||||||
|
editingBillingAddress: editing,
|
||||||
|
setEditingBillingAddress: setEditing,
|
||||||
} = useCheckoutAddress();
|
} = useCheckoutAddress();
|
||||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||||
const [ editing, setEditing ] = useState( defaultEditing );
|
|
||||||
|
|
||||||
// Forces editing state if store has errors.
|
// Forces editing state if store has errors.
|
||||||
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
|
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
|
||||||
|
@ -55,7 +54,7 @@ const CustomerAddress = ( {
|
||||||
if ( invalidProps.length > 0 && editing === false ) {
|
if ( invalidProps.length > 0 && editing === false ) {
|
||||||
setEditing( true );
|
setEditing( true );
|
||||||
}
|
}
|
||||||
}, [ editing, hasValidationErrors, invalidProps.length ] );
|
}, [ editing, hasValidationErrors, invalidProps.length, setEditing ] );
|
||||||
|
|
||||||
const onChangeAddress = useCallback(
|
const onChangeAddress = useCallback(
|
||||||
( values: AddressFormValues ) => {
|
( values: AddressFormValues ) => {
|
||||||
|
@ -86,7 +85,7 @@ const CustomerAddress = ( {
|
||||||
isExpanded={ editing }
|
isExpanded={ editing }
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[ billingAddress, addressFieldsConfig, editing ]
|
[ billingAddress, addressFieldsConfig, editing, setEditing ]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderAddressFormComponent = useCallback(
|
const renderAddressFormComponent = useCallback(
|
||||||
|
|
|
@ -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 }>
|
||||||
<Block />
|
<ExpressPaymentControls
|
||||||
|
attributes={ attributes }
|
||||||
|
setAttributes={ setAttributes }
|
||||||
|
/>
|
||||||
|
<ExpressPaymentContext.Provider
|
||||||
|
value={ { showButtonStyles, buttonHeight, buttonBorderRadius } }
|
||||||
|
>
|
||||||
|
<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;
|
||||||
|
};
|
||||||
|
};
|
|
@ -47,6 +47,7 @@ const Block = ( {
|
||||||
billingAddress,
|
billingAddress,
|
||||||
useShippingAsBilling,
|
useShippingAsBilling,
|
||||||
setUseShippingAsBilling,
|
setUseShippingAsBilling,
|
||||||
|
setEditingBillingAddress,
|
||||||
} = useCheckoutAddress();
|
} = useCheckoutAddress();
|
||||||
const { isEditor } = useEditorContext();
|
const { isEditor } = useEditorContext();
|
||||||
const isGuest = getSetting( 'currentUserId' ) === 0;
|
const isGuest = getSetting( 'currentUserId' ) === 0;
|
||||||
|
@ -116,10 +117,6 @@ const Block = ( {
|
||||||
const noticeContext = useShippingAsBilling
|
const noticeContext = useShippingAsBilling
|
||||||
? [ noticeContexts.SHIPPING_ADDRESS, noticeContexts.BILLING_ADDRESS ]
|
? [ noticeContexts.SHIPPING_ADDRESS, noticeContexts.BILLING_ADDRESS ]
|
||||||
: [ noticeContexts.SHIPPING_ADDRESS ];
|
: [ noticeContexts.SHIPPING_ADDRESS ];
|
||||||
const hasAddress = !! (
|
|
||||||
shippingAddress.address_1 &&
|
|
||||||
( shippingAddress.first_name || shippingAddress.last_name )
|
|
||||||
);
|
|
||||||
|
|
||||||
const { cartDataLoaded } = useSelect( ( select ) => {
|
const { cartDataLoaded } = useSelect( ( select ) => {
|
||||||
const store = select( CART_STORE_KEY );
|
const store = select( CART_STORE_KEY );
|
||||||
|
@ -128,9 +125,6 @@ const Block = ( {
|
||||||
};
|
};
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// Default editing state for CustomerAddress component comes from the current address and whether or not we're in the editor.
|
|
||||||
const defaultEditingAddress = isEditor || ! hasAddress;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StoreNoticesContainer context={ noticeContext } />
|
<StoreNoticesContainer context={ noticeContext } />
|
||||||
|
@ -138,7 +132,6 @@ const Block = ( {
|
||||||
{ cartDataLoaded ? (
|
{ cartDataLoaded ? (
|
||||||
<CustomerAddress
|
<CustomerAddress
|
||||||
addressFieldsConfig={ addressFieldsConfig }
|
addressFieldsConfig={ addressFieldsConfig }
|
||||||
defaultEditing={ defaultEditingAddress }
|
|
||||||
/>
|
/>
|
||||||
) : null }
|
) : null }
|
||||||
</WrapperComponent>
|
</WrapperComponent>
|
||||||
|
@ -151,6 +144,7 @@ const Block = ( {
|
||||||
if ( checked ) {
|
if ( checked ) {
|
||||||
syncBillingWithShipping();
|
syncBillingWithShipping();
|
||||||
} else {
|
} else {
|
||||||
|
setEditingBillingAddress( true );
|
||||||
clearBillingAddress( billingAddress );
|
clearBillingAddress( billingAddress );
|
||||||
}
|
}
|
||||||
} }
|
} }
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useState, useCallback, useEffect } from '@wordpress/element';
|
import { useCallback, useEffect } from '@wordpress/element';
|
||||||
import { Form } from '@woocommerce/base-components/cart-checkout';
|
import { Form } from '@woocommerce/base-components/cart-checkout';
|
||||||
import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context';
|
import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context';
|
||||||
import type {
|
import type {
|
||||||
|
@ -20,19 +20,18 @@ import AddressCard from '../../address-card';
|
||||||
|
|
||||||
const CustomerAddress = ( {
|
const CustomerAddress = ( {
|
||||||
addressFieldsConfig,
|
addressFieldsConfig,
|
||||||
defaultEditing = false,
|
|
||||||
}: {
|
}: {
|
||||||
addressFieldsConfig: FormFieldsConfig;
|
addressFieldsConfig: FormFieldsConfig;
|
||||||
defaultEditing?: boolean;
|
|
||||||
} ) => {
|
} ) => {
|
||||||
const {
|
const {
|
||||||
shippingAddress,
|
shippingAddress,
|
||||||
setShippingAddress,
|
setShippingAddress,
|
||||||
setBillingAddress,
|
setBillingAddress,
|
||||||
useShippingAsBilling,
|
useShippingAsBilling,
|
||||||
|
editingShippingAddress: editing,
|
||||||
|
setEditingShippingAddress: setEditing,
|
||||||
} = useCheckoutAddress();
|
} = useCheckoutAddress();
|
||||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||||
const [ editing, setEditing ] = useState( defaultEditing );
|
|
||||||
|
|
||||||
// Forces editing state if store has errors.
|
// Forces editing state if store has errors.
|
||||||
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
|
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
|
||||||
|
@ -54,7 +53,7 @@ const CustomerAddress = ( {
|
||||||
if ( invalidProps.length > 0 && editing === false ) {
|
if ( invalidProps.length > 0 && editing === false ) {
|
||||||
setEditing( true );
|
setEditing( true );
|
||||||
}
|
}
|
||||||
}, [ editing, hasValidationErrors, invalidProps.length ] );
|
}, [ editing, hasValidationErrors, invalidProps.length, setEditing ] );
|
||||||
|
|
||||||
const onChangeAddress = useCallback(
|
const onChangeAddress = useCallback(
|
||||||
( values: AddressFormValues ) => {
|
( values: AddressFormValues ) => {
|
||||||
|
@ -85,7 +84,7 @@ const CustomerAddress = ( {
|
||||||
isExpanded={ editing }
|
isExpanded={ editing }
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[ shippingAddress, addressFieldsConfig, editing ]
|
[ shippingAddress, addressFieldsConfig, editing, setEditing ]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderAddressFormComponent = useCallback(
|
const renderAddressFormComponent = useCallback(
|
||||||
|
|
|
@ -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'
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -21,15 +21,36 @@ import {
|
||||||
import type { ProductCollectionEditComponentProps } from '../types';
|
import type { ProductCollectionEditComponentProps } from '../types';
|
||||||
import { getCollectionByName } from '../collections';
|
import { getCollectionByName } from '../collections';
|
||||||
|
|
||||||
const ProductPicker = ( props: ProductCollectionEditComponentProps ) => {
|
const ProductPicker = (
|
||||||
|
props: ProductCollectionEditComponentProps & {
|
||||||
|
isDeletedProductReference: boolean;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
const blockProps = useBlockProps();
|
const blockProps = useBlockProps();
|
||||||
const attributes = props.attributes;
|
const { attributes, isDeletedProductReference } = props;
|
||||||
|
|
||||||
const collection = getCollectionByName( attributes.collection );
|
const collection = getCollectionByName( attributes.collection );
|
||||||
if ( ! collection ) {
|
if ( ! collection ) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const infoText = isDeletedProductReference
|
||||||
|
? __(
|
||||||
|
'Previously selected product is no longer available.',
|
||||||
|
'woocommerce'
|
||||||
|
)
|
||||||
|
: createInterpolateElement(
|
||||||
|
sprintf(
|
||||||
|
/* translators: %s: collection title */
|
||||||
|
__(
|
||||||
|
'<strong>%s</strong> requires a product to be selected in order to display associated items.',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
collection.title
|
||||||
|
),
|
||||||
|
{ strong: <strong /> }
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div { ...blockProps }>
|
<div { ...blockProps }>
|
||||||
<Placeholder className="wc-blocks-product-collection__editor-product-picker">
|
<Placeholder className="wc-blocks-product-collection__editor-product-picker">
|
||||||
|
@ -38,21 +59,7 @@ const ProductPicker = ( props: ProductCollectionEditComponentProps ) => {
|
||||||
icon={ info }
|
icon={ info }
|
||||||
className="wc-blocks-product-collection__info-icon"
|
className="wc-blocks-product-collection__info-icon"
|
||||||
/>
|
/>
|
||||||
<Text>
|
<Text>{ infoText }</Text>
|
||||||
{ createInterpolateElement(
|
|
||||||
sprintf(
|
|
||||||
/* translators: %s: collection title */
|
|
||||||
__(
|
|
||||||
'<strong>%s</strong> requires a product to be selected in order to display associated items.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
collection.title
|
|
||||||
),
|
|
||||||
{
|
|
||||||
strong: <strong />,
|
|
||||||
}
|
|
||||||
) }
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
<ProductControl
|
<ProductControl
|
||||||
selected={
|
selected={
|
||||||
|
|
|
@ -174,6 +174,10 @@ $max-button-width: calc(100% / #{$max-button-columns});
|
||||||
.wc-blocks-product-collection__info-icon {
|
.wc-blocks-product-collection__info-icon {
|
||||||
fill: var(--wp--preset--color--luminous-vivid-orange, #e26f56);
|
fill: var(--wp--preset--color--luminous-vivid-orange, #e26f56);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.woocommerce-search-list__search {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linked Product Control
|
// Linked Product Control
|
||||||
|
|
|
@ -5,11 +5,13 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
|
||||||
import { useState } from '@wordpress/element';
|
import { useState } from '@wordpress/element';
|
||||||
import { useSelect } from '@wordpress/data';
|
import { useSelect } from '@wordpress/data';
|
||||||
import { useGetLocation } from '@woocommerce/blocks/product-template/utils';
|
import { useGetLocation } from '@woocommerce/blocks/product-template/utils';
|
||||||
|
import { Spinner, Flex } from '@wordpress/components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
ProductCollectionContentProps,
|
||||||
ProductCollectionEditComponentProps,
|
ProductCollectionEditComponentProps,
|
||||||
ProductCollectionUIStatesInEditor,
|
ProductCollectionUIStatesInEditor,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
@ -17,7 +19,7 @@ import ProductCollectionPlaceholder from './product-collection-placeholder';
|
||||||
import ProductCollectionContent from './product-collection-content';
|
import ProductCollectionContent from './product-collection-content';
|
||||||
import CollectionSelectionModal from './collection-selection-modal';
|
import CollectionSelectionModal from './collection-selection-modal';
|
||||||
import './editor.scss';
|
import './editor.scss';
|
||||||
import { getProductCollectionUIStateInEditor } from '../utils';
|
import { useProductCollectionUIState } from '../utils';
|
||||||
import ProductPicker from './ProductPicker';
|
import ProductPicker from './ProductPicker';
|
||||||
|
|
||||||
const Edit = ( props: ProductCollectionEditComponentProps ) => {
|
const Edit = ( props: ProductCollectionEditComponentProps ) => {
|
||||||
|
@ -31,49 +33,65 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
|
||||||
[ clientId ]
|
[ clientId ]
|
||||||
);
|
);
|
||||||
|
|
||||||
const productCollectionUIStateInEditor =
|
const { productCollectionUIStateInEditor, isLoading } =
|
||||||
getProductCollectionUIStateInEditor( {
|
useProductCollectionUIState( {
|
||||||
hasInnerBlocks,
|
|
||||||
location,
|
location,
|
||||||
attributes: props.attributes,
|
attributes,
|
||||||
|
hasInnerBlocks,
|
||||||
usesReference: props.usesReference,
|
usesReference: props.usesReference,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
/**
|
// Show spinner while calculating Editor UI state.
|
||||||
* Component to render based on the UI state.
|
if ( isLoading ) {
|
||||||
*/
|
return (
|
||||||
let Component,
|
<Flex justify="center" align="center">
|
||||||
isUsingReferencePreviewMode = false;
|
<Spinner />
|
||||||
switch ( productCollectionUIStateInEditor ) {
|
</Flex>
|
||||||
case ProductCollectionUIStatesInEditor.COLLECTION_PICKER:
|
);
|
||||||
Component = ProductCollectionPlaceholder;
|
|
||||||
break;
|
|
||||||
case ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER:
|
|
||||||
Component = ProductPicker;
|
|
||||||
break;
|
|
||||||
case ProductCollectionUIStatesInEditor.VALID:
|
|
||||||
Component = ProductCollectionContent;
|
|
||||||
break;
|
|
||||||
case ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW:
|
|
||||||
Component = ProductCollectionContent;
|
|
||||||
isUsingReferencePreviewMode = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// By default showing collection chooser.
|
|
||||||
Component = ProductCollectionPlaceholder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const productCollectionContentProps: ProductCollectionContentProps = {
|
||||||
|
...props,
|
||||||
|
openCollectionSelectionModal: () => setIsSelectionModalOpen( true ),
|
||||||
|
location,
|
||||||
|
isUsingReferencePreviewMode:
|
||||||
|
productCollectionUIStateInEditor ===
|
||||||
|
ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW,
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = () => {
|
||||||
|
switch ( productCollectionUIStateInEditor ) {
|
||||||
|
case ProductCollectionUIStatesInEditor.COLLECTION_PICKER:
|
||||||
|
return <ProductCollectionPlaceholder { ...props } />;
|
||||||
|
case ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER:
|
||||||
|
return (
|
||||||
|
<ProductPicker
|
||||||
|
{ ...props }
|
||||||
|
isDeletedProductReference={ false }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case ProductCollectionUIStatesInEditor.DELETED_PRODUCT_REFERENCE:
|
||||||
|
return (
|
||||||
|
<ProductPicker
|
||||||
|
{ ...props }
|
||||||
|
isDeletedProductReference={ true }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case ProductCollectionUIStatesInEditor.VALID:
|
||||||
|
case ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW:
|
||||||
|
return (
|
||||||
|
<ProductCollectionContent
|
||||||
|
{ ...productCollectionContentProps }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <ProductCollectionPlaceholder { ...props } />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Component
|
{ renderComponent() }
|
||||||
{ ...props }
|
|
||||||
openCollectionSelectionModal={ () =>
|
|
||||||
setIsSelectionModalOpen( true )
|
|
||||||
}
|
|
||||||
isUsingReferencePreviewMode={ isUsingReferencePreviewMode }
|
|
||||||
location={ location }
|
|
||||||
usesReference={ props.usesReference }
|
|
||||||
/>
|
|
||||||
{ isSelectionModalOpen && (
|
{ isSelectionModalOpen && (
|
||||||
<CollectionSelectionModal
|
<CollectionSelectionModal
|
||||||
clientId={ clientId }
|
clientId={ clientId }
|
||||||
|
|
|
@ -7,10 +7,10 @@ import { InspectorAdvancedControls } from '@wordpress/block-editor';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import ForcePageReloadControl from './force-page-reload-control';
|
import ForcePageReloadControl from './force-page-reload-control';
|
||||||
import type { ProductCollectionEditComponentProps } from '../../types';
|
import type { ProductCollectionContentProps } from '../../types';
|
||||||
|
|
||||||
export default function ProductCollectionAdvancedInspectorControls(
|
export default function ProductCollectionAdvancedInspectorControls(
|
||||||
props: Omit< ProductCollectionEditComponentProps, 'preview' >
|
props: ProductCollectionContentProps
|
||||||
) {
|
) {
|
||||||
const { clientId, attributes, setAttributes } = props;
|
const { clientId, attributes, setAttributes } = props;
|
||||||
const { forcePageReload } = attributes;
|
const { forcePageReload } = attributes;
|
||||||
|
|
|
@ -27,7 +27,7 @@ import {
|
||||||
import metadata from '../../block.json';
|
import metadata from '../../block.json';
|
||||||
import { useTracksLocation } from '../../tracks-utils';
|
import { useTracksLocation } from '../../tracks-utils';
|
||||||
import {
|
import {
|
||||||
ProductCollectionEditComponentProps,
|
ProductCollectionContentProps,
|
||||||
ProductCollectionAttributes,
|
ProductCollectionAttributes,
|
||||||
CoreFilterNames,
|
CoreFilterNames,
|
||||||
FilterName,
|
FilterName,
|
||||||
|
@ -58,7 +58,7 @@ const prepareShouldShowFilter =
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProductCollectionInspectorControls = (
|
const ProductCollectionInspectorControls = (
|
||||||
props: ProductCollectionEditComponentProps
|
props: ProductCollectionContentProps
|
||||||
) => {
|
) => {
|
||||||
const { attributes, context, setAttributes } = props;
|
const { attributes, context, setAttributes } = props;
|
||||||
const { query, hideControls, displayLayout } = attributes;
|
const { query, hideControls, displayLayout } = attributes;
|
||||||
|
|
|
@ -18,7 +18,7 @@ import fastDeepEqual from 'fast-deep-equal/es6';
|
||||||
import type {
|
import type {
|
||||||
ProductCollectionAttributes,
|
ProductCollectionAttributes,
|
||||||
ProductCollectionQuery,
|
ProductCollectionQuery,
|
||||||
ProductCollectionEditComponentProps,
|
ProductCollectionContentProps,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { DEFAULT_ATTRIBUTES, INNER_BLOCKS_TEMPLATE } from '../constants';
|
import { DEFAULT_ATTRIBUTES, INNER_BLOCKS_TEMPLATE } from '../constants';
|
||||||
import {
|
import {
|
||||||
|
@ -68,7 +68,7 @@ const useQueryId = (
|
||||||
const ProductCollectionContent = ( {
|
const ProductCollectionContent = ( {
|
||||||
preview: { setPreviewState, initialPreviewState } = {},
|
preview: { setPreviewState, initialPreviewState } = {},
|
||||||
...props
|
...props
|
||||||
}: ProductCollectionEditComponentProps ) => {
|
}: ProductCollectionContentProps ) => {
|
||||||
const isInitialAttributesSet = useRef( false );
|
const isInitialAttributesSet = useRef( false );
|
||||||
const {
|
const {
|
||||||
clientId,
|
clientId,
|
||||||
|
|
|
@ -11,10 +11,10 @@ import { setQueryAttribute } from '../../utils';
|
||||||
import DisplaySettingsToolbar from './display-settings-toolbar';
|
import DisplaySettingsToolbar from './display-settings-toolbar';
|
||||||
import DisplayLayoutToolbar from './display-layout-toolbar';
|
import DisplayLayoutToolbar from './display-layout-toolbar';
|
||||||
import CollectionChooserToolbar from './collection-chooser-toolbar';
|
import CollectionChooserToolbar from './collection-chooser-toolbar';
|
||||||
import type { ProductCollectionEditComponentProps } from '../../types';
|
import type { ProductCollectionContentProps } from '../../types';
|
||||||
|
|
||||||
export default function ToolbarControls(
|
export default function ToolbarControls(
|
||||||
props: Omit< ProductCollectionEditComponentProps, 'preview' >
|
props: ProductCollectionContentProps
|
||||||
) {
|
) {
|
||||||
const { attributes, openCollectionSelectionModal, setAttributes } = props;
|
const { attributes, openCollectionSelectionModal, setAttributes } = props;
|
||||||
const { query, displayLayout } = attributes;
|
const { query, displayLayout } = attributes;
|
||||||
|
|
|
@ -14,9 +14,9 @@ export enum ProductCollectionUIStatesInEditor {
|
||||||
PRODUCT_REFERENCE_PICKER = 'product_context_picker',
|
PRODUCT_REFERENCE_PICKER = 'product_context_picker',
|
||||||
VALID_WITH_PREVIEW = 'uses_reference_preview_mode',
|
VALID_WITH_PREVIEW = 'uses_reference_preview_mode',
|
||||||
VALID = 'valid',
|
VALID = 'valid',
|
||||||
|
DELETED_PRODUCT_REFERENCE = 'deleted_product_reference',
|
||||||
// Future states
|
// Future states
|
||||||
// INVALID = 'invalid',
|
// INVALID = 'invalid',
|
||||||
// DELETED_PRODUCT_REFERENCE = 'deleted_product_reference',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductCollectionAttributes {
|
export interface ProductCollectionAttributes {
|
||||||
|
@ -110,7 +110,6 @@ export interface ProductCollectionQuery {
|
||||||
|
|
||||||
export type ProductCollectionEditComponentProps =
|
export type ProductCollectionEditComponentProps =
|
||||||
BlockEditProps< ProductCollectionAttributes > & {
|
BlockEditProps< ProductCollectionAttributes > & {
|
||||||
openCollectionSelectionModal: () => void;
|
|
||||||
preview?: {
|
preview?: {
|
||||||
initialPreviewState?: PreviewState;
|
initialPreviewState?: PreviewState;
|
||||||
setPreviewState?: SetPreviewState;
|
setPreviewState?: SetPreviewState;
|
||||||
|
@ -119,8 +118,13 @@ export type ProductCollectionEditComponentProps =
|
||||||
context: {
|
context: {
|
||||||
templateSlug: string;
|
templateSlug: string;
|
||||||
};
|
};
|
||||||
isUsingReferencePreviewMode: boolean;
|
};
|
||||||
|
|
||||||
|
export type ProductCollectionContentProps =
|
||||||
|
ProductCollectionEditComponentProps & {
|
||||||
location: WooCommerceBlockLocation;
|
location: WooCommerceBlockLocation;
|
||||||
|
isUsingReferencePreviewMode: boolean;
|
||||||
|
openCollectionSelectionModal: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProductCollectionOrder = 'asc' | 'desc';
|
export type TProductCollectionOrder = 'asc' | 'desc';
|
||||||
|
|
|
@ -3,10 +3,16 @@
|
||||||
*/
|
*/
|
||||||
import { store as blockEditorStore } from '@wordpress/block-editor';
|
import { store as blockEditorStore } from '@wordpress/block-editor';
|
||||||
import { addFilter } from '@wordpress/hooks';
|
import { addFilter } from '@wordpress/hooks';
|
||||||
import { select } from '@wordpress/data';
|
import { select, useSelect } from '@wordpress/data';
|
||||||
|
import { store as coreDataStore } from '@wordpress/core-data';
|
||||||
import { isWpVersion } from '@woocommerce/settings';
|
import { isWpVersion } from '@woocommerce/settings';
|
||||||
import type { BlockEditProps, Block } from '@wordpress/blocks';
|
import type { BlockEditProps, Block } from '@wordpress/blocks';
|
||||||
import { useEffect, useLayoutEffect, useState } from '@wordpress/element';
|
import {
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
} from '@wordpress/element';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import type { ProductResponseItem } from '@woocommerce/types';
|
import type { ProductResponseItem } from '@woocommerce/types';
|
||||||
import { getProduct } from '@woocommerce/editor-components/utils';
|
import { getProduct } from '@woocommerce/editor-components/utils';
|
||||||
|
@ -193,7 +199,7 @@ export const getUsesReferencePreviewMessage = (
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductCollectionUIStateInEditor = ( {
|
export const useProductCollectionUIState = ( {
|
||||||
location,
|
location,
|
||||||
usesReference,
|
usesReference,
|
||||||
attributes,
|
attributes,
|
||||||
|
@ -203,59 +209,111 @@ export const getProductCollectionUIStateInEditor = ( {
|
||||||
usesReference?: string[] | undefined;
|
usesReference?: string[] | undefined;
|
||||||
attributes: ProductCollectionAttributes;
|
attributes: ProductCollectionAttributes;
|
||||||
hasInnerBlocks: boolean;
|
hasInnerBlocks: boolean;
|
||||||
} ): ProductCollectionUIStatesInEditor => {
|
} ) => {
|
||||||
const isInRequiredLocation = usesReference?.includes( location.type );
|
// Fetch product to check if it's deleted.
|
||||||
const isCollectionSelected = !! attributes.collection;
|
// `product` will be undefined if it doesn't exist.
|
||||||
|
const productId = attributes.query?.productReference;
|
||||||
|
const { product, hasResolved } = useSelect(
|
||||||
|
( selectFunc ) => {
|
||||||
|
if ( ! productId ) {
|
||||||
|
return { product: null, hasResolved: true };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
const { getEntityRecord, hasFinishedResolution } =
|
||||||
* Case 1: Product context picker
|
selectFunc( coreDataStore );
|
||||||
*/
|
const selectorArgs = [ 'postType', 'product', productId ];
|
||||||
const isProductContextRequired = usesReference?.includes( 'product' );
|
return {
|
||||||
const isProductContextSelected =
|
product: getEntityRecord( ...selectorArgs ),
|
||||||
( attributes.query?.productReference ?? null ) !== null;
|
hasResolved: hasFinishedResolution(
|
||||||
if (
|
'getEntityRecord',
|
||||||
isCollectionSelected &&
|
selectorArgs
|
||||||
isProductContextRequired &&
|
),
|
||||||
! isInRequiredLocation &&
|
};
|
||||||
! isProductContextSelected
|
},
|
||||||
) {
|
[ productId ]
|
||||||
return ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER;
|
);
|
||||||
}
|
|
||||||
|
const productCollectionUIStateInEditor = useMemo( () => {
|
||||||
|
const isInRequiredLocation = usesReference?.includes( location.type );
|
||||||
|
const isCollectionSelected = !! attributes.collection;
|
||||||
|
|
||||||
/**
|
|
||||||
* Case 2: Preview mode - based on `usesReference` value
|
|
||||||
*/
|
|
||||||
if ( isInRequiredLocation ) {
|
|
||||||
/**
|
/**
|
||||||
* Block shouldn't be in preview mode when:
|
* Case 1: Product context picker
|
||||||
* 1. Current location is archive and termId is available.
|
|
||||||
* 2. Current location is product and productId is available.
|
|
||||||
*
|
|
||||||
* Because in these cases, we have required context on the editor side.
|
|
||||||
*/
|
*/
|
||||||
const isArchiveLocationWithTermId =
|
const isProductContextRequired = usesReference?.includes( 'product' );
|
||||||
location.type === LocationType.Archive &&
|
const isProductContextSelected =
|
||||||
( location.sourceData?.termId ?? null ) !== null;
|
( attributes.query?.productReference ?? null ) !== null;
|
||||||
const isProductLocationWithProductId =
|
|
||||||
location.type === LocationType.Product &&
|
|
||||||
( location.sourceData?.productId ?? null ) !== null;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
! isArchiveLocationWithTermId &&
|
isCollectionSelected &&
|
||||||
! isProductLocationWithProductId
|
isProductContextRequired &&
|
||||||
|
! isInRequiredLocation &&
|
||||||
|
! isProductContextSelected
|
||||||
) {
|
) {
|
||||||
return ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW;
|
return ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Case 2: Deleted product reference
|
||||||
* Case 3: Collection chooser
|
if (
|
||||||
*/
|
isCollectionSelected &&
|
||||||
if ( ! hasInnerBlocks && ! isCollectionSelected ) {
|
isProductContextRequired &&
|
||||||
return ProductCollectionUIStatesInEditor.COLLECTION_PICKER;
|
! isInRequiredLocation &&
|
||||||
}
|
isProductContextSelected
|
||||||
|
) {
|
||||||
|
const isProductDeleted =
|
||||||
|
productId &&
|
||||||
|
( product === undefined || product?.status === 'trash' );
|
||||||
|
if ( isProductDeleted ) {
|
||||||
|
return ProductCollectionUIStatesInEditor.DELETED_PRODUCT_REFERENCE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ProductCollectionUIStatesInEditor.VALID;
|
/**
|
||||||
|
* Case 3: Preview mode - based on `usesReference` value
|
||||||
|
*/
|
||||||
|
if ( isInRequiredLocation ) {
|
||||||
|
/**
|
||||||
|
* Block shouldn't be in preview mode when:
|
||||||
|
* 1. Current location is archive and termId is available.
|
||||||
|
* 2. Current location is product and productId is available.
|
||||||
|
*
|
||||||
|
* Because in these cases, we have required context on the editor side.
|
||||||
|
*/
|
||||||
|
const isArchiveLocationWithTermId =
|
||||||
|
location.type === LocationType.Archive &&
|
||||||
|
( location.sourceData?.termId ?? null ) !== null;
|
||||||
|
const isProductLocationWithProductId =
|
||||||
|
location.type === LocationType.Product &&
|
||||||
|
( location.sourceData?.productId ?? null ) !== null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
! isArchiveLocationWithTermId &&
|
||||||
|
! isProductLocationWithProductId
|
||||||
|
) {
|
||||||
|
return ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case 4: Collection chooser
|
||||||
|
*/
|
||||||
|
if ( ! hasInnerBlocks && ! isCollectionSelected ) {
|
||||||
|
return ProductCollectionUIStatesInEditor.COLLECTION_PICKER;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProductCollectionUIStatesInEditor.VALID;
|
||||||
|
}, [
|
||||||
|
location.type,
|
||||||
|
location.sourceData?.termId,
|
||||||
|
location.sourceData?.productId,
|
||||||
|
usesReference,
|
||||||
|
attributes.collection,
|
||||||
|
productId,
|
||||||
|
product,
|
||||||
|
hasInnerBlocks,
|
||||||
|
attributes.query?.productReference,
|
||||||
|
] );
|
||||||
|
|
||||||
|
return { productCollectionUIStateInEditor, isLoading: ! hasResolved };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSetPreviewState = ( {
|
export const useSetPreviewState = ( {
|
||||||
|
|
|
@ -2,47 +2,12 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
getContext as getContextFn,
|
getContext,
|
||||||
store,
|
store,
|
||||||
navigate as navigateFn,
|
navigate as navigateFn,
|
||||||
} from '@woocommerce/interactivity';
|
} from '@woocommerce/interactivity';
|
||||||
import { getSetting } from '@woocommerce/settings';
|
import { getSetting } from '@woocommerce/settings';
|
||||||
|
|
||||||
export interface ProductFiltersContext {
|
|
||||||
isDialogOpen: boolean;
|
|
||||||
hasPageWithWordPressAdminBar: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getContext = ( ns?: string ) =>
|
|
||||||
getContextFn< ProductFiltersContext >( ns );
|
|
||||||
|
|
||||||
store( 'woocommerce/product-filters', {
|
|
||||||
state: {
|
|
||||||
isDialogOpen: () => {
|
|
||||||
const context = getContext();
|
|
||||||
return context.isDialogOpen;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
openDialog: () => {
|
|
||||||
const context = getContext();
|
|
||||||
document.body.classList.add( 'wc-modal--open' );
|
|
||||||
context.hasPageWithWordPressAdminBar = Boolean(
|
|
||||||
document.getElementById( 'wpadminbar' )
|
|
||||||
);
|
|
||||||
|
|
||||||
context.isDialogOpen = true;
|
|
||||||
},
|
|
||||||
closeDialog: () => {
|
|
||||||
const context = getContext();
|
|
||||||
document.body.classList.remove( 'wc-modal--open' );
|
|
||||||
|
|
||||||
context.isDialogOpen = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
callbacks: {},
|
|
||||||
} );
|
|
||||||
|
|
||||||
const isBlockTheme = getSetting< boolean >( 'isBlockTheme' );
|
const isBlockTheme = getSetting< boolean >( 'isBlockTheme' );
|
||||||
const isProductArchive = getSetting< boolean >( 'isProductArchive' );
|
const isProductArchive = getSetting< boolean >( 'isProductArchive' );
|
||||||
const needsRefresh = getSetting< boolean >(
|
const needsRefresh = getSetting< boolean >(
|
||||||
|
@ -50,6 +15,28 @@ const needsRefresh = getSetting< boolean >(
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function isParamsEqual(
|
||||||
|
obj1: Record< string, string >,
|
||||||
|
obj2: Record< string, string >
|
||||||
|
): boolean {
|
||||||
|
const keys1 = Object.keys( obj1 );
|
||||||
|
const keys2 = Object.keys( obj2 );
|
||||||
|
|
||||||
|
// First check if both objects have the same number of keys
|
||||||
|
if ( keys1.length !== keys2.length ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all keys and values are the same
|
||||||
|
for ( const key of keys1 ) {
|
||||||
|
if ( obj1[ key ] !== obj2[ key ] ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function navigate( href: string, options = {} ) {
|
export function navigate( href: string, options = {} ) {
|
||||||
/**
|
/**
|
||||||
* We may need to reset the current page when changing filters.
|
* We may need to reset the current page when changing filters.
|
||||||
|
@ -79,3 +66,58 @@ export function navigate( href: string, options = {} ) {
|
||||||
}
|
}
|
||||||
return navigateFn( href, options );
|
return navigateFn( href, options );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProductFiltersContext {
|
||||||
|
isDialogOpen: boolean;
|
||||||
|
hasPageWithWordPressAdminBar: boolean;
|
||||||
|
params: Record< string, string >;
|
||||||
|
originalParams: Record< string, string >;
|
||||||
|
}
|
||||||
|
|
||||||
|
store( 'woocommerce/product-filters', {
|
||||||
|
state: {
|
||||||
|
isDialogOpen: () => {
|
||||||
|
const context = getContext< ProductFiltersContext >();
|
||||||
|
return context.isDialogOpen;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
openDialog: () => {
|
||||||
|
const context = getContext< ProductFiltersContext >();
|
||||||
|
document.body.classList.add( 'wc-modal--open' );
|
||||||
|
context.hasPageWithWordPressAdminBar = Boolean(
|
||||||
|
document.getElementById( 'wpadminbar' )
|
||||||
|
);
|
||||||
|
|
||||||
|
context.isDialogOpen = true;
|
||||||
|
},
|
||||||
|
closeDialog: () => {
|
||||||
|
const context = getContext< ProductFiltersContext >();
|
||||||
|
document.body.classList.remove( 'wc-modal--open' );
|
||||||
|
|
||||||
|
context.isDialogOpen = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
maybeNavigate: () => {
|
||||||
|
const { params, originalParams } =
|
||||||
|
getContext< ProductFiltersContext >();
|
||||||
|
|
||||||
|
if ( isParamsEqual( params, originalParams ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL( window.location.href );
|
||||||
|
const { searchParams } = url;
|
||||||
|
|
||||||
|
for ( const key in originalParams ) {
|
||||||
|
searchParams.delete( key, originalParams[ key ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( const key in params ) {
|
||||||
|
searchParams.set( key, params[ key ] );
|
||||||
|
}
|
||||||
|
navigate( url.href );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} );
|
|
@ -6,22 +6,15 @@ import { store, getContext } from '@woocommerce/interactivity';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../../frontend';
|
import { ProductFiltersContext } from '../../frontend';
|
||||||
|
|
||||||
type ActiveFiltersContext = {
|
|
||||||
queryId: number;
|
|
||||||
params: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
store( 'woocommerce/product-filter-active', {
|
store( 'woocommerce/product-filter-active', {
|
||||||
actions: {
|
actions: {
|
||||||
clearAll: () => {
|
clearAll: () => {
|
||||||
const { params } = getContext< ActiveFiltersContext >();
|
const productFiltersContext = getContext< ProductFiltersContext >(
|
||||||
const url = new URL( window.location.href );
|
'woocommerce/product-filters'
|
||||||
const { searchParams } = url;
|
);
|
||||||
|
productFiltersContext.params = {};
|
||||||
params.forEach( ( param ) => searchParams.delete( param ) );
|
|
||||||
navigate( url.href );
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { store, getContext } from '@woocommerce/interactivity';
|
import { store, getContext, getElement } from '@woocommerce/interactivity';
|
||||||
import { DropdownContext } from '@woocommerce/interactivity-components/dropdown';
|
|
||||||
import { HTMLElementEvent } from '@woocommerce/types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../../frontend';
|
import { ProductFiltersContext } from '../../frontend';
|
||||||
|
|
||||||
type AttributeFilterContext = {
|
type AttributeFilterContext = {
|
||||||
attributeSlug: string;
|
attributeSlug: string;
|
||||||
|
@ -16,102 +14,72 @@ type AttributeFilterContext = {
|
||||||
selectType: 'single' | 'multiple';
|
selectType: 'single' | 'multiple';
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ActiveAttributeFilterContext extends AttributeFilterContext {
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function nonNullable< T >( value: T ): value is NonNullable< T > {
|
|
||||||
return value !== null && value !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUrl(
|
|
||||||
selectedTerms: string[],
|
|
||||||
slug: string,
|
|
||||||
queryType: 'or' | 'and'
|
|
||||||
) {
|
|
||||||
const url = new URL( window.location.href );
|
|
||||||
const { searchParams } = url;
|
|
||||||
|
|
||||||
if ( selectedTerms.length > 0 ) {
|
|
||||||
searchParams.set( `filter_${ slug }`, selectedTerms.join( ',' ) );
|
|
||||||
searchParams.set( `query_type_${ slug }`, queryType );
|
|
||||||
} else {
|
|
||||||
searchParams.delete( `filter_${ slug }` );
|
|
||||||
searchParams.delete( `query_type_${ slug }` );
|
|
||||||
}
|
|
||||||
|
|
||||||
return url.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectedTermsFromUrl( slug: string ) {
|
|
||||||
const url = new URL( window.location.href );
|
|
||||||
return ( url.searchParams.get( `filter_${ slug }` ) || '' )
|
|
||||||
.split( ',' )
|
|
||||||
.filter( Boolean );
|
|
||||||
}
|
|
||||||
|
|
||||||
store( 'woocommerce/product-filter-attribute', {
|
store( 'woocommerce/product-filter-attribute', {
|
||||||
actions: {
|
actions: {
|
||||||
navigate: () => {
|
toggleFilter: () => {
|
||||||
const dropdownContext = getContext< DropdownContext >(
|
const { ref } = getElement();
|
||||||
'woocommerce/interactivity-dropdown'
|
const targetAttribute =
|
||||||
);
|
ref.getAttribute( 'data-attribute-value' ) ?? 'value';
|
||||||
const context = getContext< AttributeFilterContext >();
|
const termSlug = ref.getAttribute( targetAttribute );
|
||||||
const filters = dropdownContext.selectedItems
|
|
||||||
.map( ( item ) => item.value )
|
|
||||||
.filter( nonNullable );
|
|
||||||
|
|
||||||
navigate(
|
if ( ! termSlug ) return;
|
||||||
getUrl( filters, context.attributeSlug, context.queryType )
|
|
||||||
);
|
|
||||||
},
|
|
||||||
updateProducts: ( event: HTMLElementEvent< HTMLInputElement > ) => {
|
|
||||||
if ( ! event.target.value ) return;
|
|
||||||
|
|
||||||
const context = getContext< AttributeFilterContext >();
|
const { attributeSlug, queryType } =
|
||||||
|
getContext< AttributeFilterContext >();
|
||||||
let selectedTerms = getSelectedTermsFromUrl(
|
const productFiltersContext = getContext< ProductFiltersContext >(
|
||||||
context.attributeSlug
|
'woocommerce/product-filters'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event.target.checked &&
|
! (
|
||||||
! selectedTerms.includes( event.target.value )
|
`filter_${ attributeSlug }` in productFiltersContext.params
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
if ( context.selectType === 'multiple' )
|
productFiltersContext.params = {
|
||||||
selectedTerms.push( event.target.value );
|
...productFiltersContext.params,
|
||||||
if ( context.selectType === 'single' )
|
[ `filter_${ attributeSlug }` ]: termSlug,
|
||||||
selectedTerms = [ event.target.value ];
|
[ `query_type_${ attributeSlug }` ]: queryType,
|
||||||
} else {
|
};
|
||||||
selectedTerms = selectedTerms.filter(
|
return;
|
||||||
( value ) => value !== event.target.value
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(
|
const selectedTerms =
|
||||||
getUrl(
|
productFiltersContext.params[
|
||||||
selectedTerms,
|
`filter_${ attributeSlug }`
|
||||||
context.attributeSlug,
|
].split( ',' );
|
||||||
context.queryType
|
if ( selectedTerms.includes( termSlug ) ) {
|
||||||
)
|
const remainingSelectedTerms = selectedTerms.filter(
|
||||||
);
|
( term ) => term !== termSlug
|
||||||
},
|
);
|
||||||
removeFilter: () => {
|
if ( remainingSelectedTerms.length > 0 ) {
|
||||||
const { attributeSlug, queryType, value } =
|
productFiltersContext.params[
|
||||||
getContext< ActiveAttributeFilterContext >();
|
`filter_${ attributeSlug }`
|
||||||
|
] = remainingSelectedTerms.join( ',' );
|
||||||
|
} else {
|
||||||
|
const updatedParams = productFiltersContext.params;
|
||||||
|
|
||||||
let selectedTerms = getSelectedTermsFromUrl( attributeSlug );
|
delete updatedParams[ `filter_${ attributeSlug }` ];
|
||||||
|
delete updatedParams[ `query_type_${ attributeSlug }` ];
|
||||||
|
|
||||||
selectedTerms = selectedTerms.filter( ( item ) => item !== value );
|
productFiltersContext.params = updatedParams;
|
||||||
|
}
|
||||||
navigate( getUrl( selectedTerms, attributeSlug, queryType ) );
|
} else {
|
||||||
|
productFiltersContext.params[ `filter_${ attributeSlug }` ] =
|
||||||
|
selectedTerms.concat( termSlug ).join( ',' );
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
clearFilters: () => {
|
clearFilters: () => {
|
||||||
const { attributeSlug, queryType } =
|
const { attributeSlug } = getContext< AttributeFilterContext >();
|
||||||
getContext< ActiveAttributeFilterContext >();
|
const productFiltersContext = getContext< ProductFiltersContext >(
|
||||||
|
'woocommerce/product-filters'
|
||||||
|
);
|
||||||
|
const updatedParams = productFiltersContext.params;
|
||||||
|
|
||||||
navigate( getUrl( [], attributeSlug, queryType ) );
|
delete updatedParams[ `filter_${ attributeSlug }` ];
|
||||||
|
delete updatedParams[ `query_type_${ attributeSlug }` ];
|
||||||
|
|
||||||
|
productFiltersContext.params = updatedParams;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
import './editor.scss';
|
import './editor.scss';
|
||||||
import { EditProps } from './types';
|
import { EditProps } from './types';
|
||||||
|
import { getColorClasses, getColorVars } from './utils';
|
||||||
|
|
||||||
const Edit = ( props: EditProps ): JSX.Element => {
|
const Edit = ( props: EditProps ): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
|
@ -51,21 +52,9 @@ const Edit = ( props: EditProps ): JSX.Element => {
|
||||||
const blockProps = useBlockProps( {
|
const blockProps = useBlockProps( {
|
||||||
className: clsx( 'wc-block-product-filter-checkbox-list', {
|
className: clsx( 'wc-block-product-filter-checkbox-list', {
|
||||||
'is-loading': isLoading,
|
'is-loading': isLoading,
|
||||||
'has-option-element-border-color':
|
...getColorClasses( attributes ),
|
||||||
optionElementBorder.color || customOptionElementBorder,
|
|
||||||
'has-option-element-selected-color':
|
|
||||||
optionElementSelected.color || customOptionElementSelected,
|
|
||||||
'has-option-element-color':
|
|
||||||
optionElement.color || customOptionElement,
|
|
||||||
} ),
|
} ),
|
||||||
style: {
|
style: getColorVars( attributes ),
|
||||||
'--wc-product-filter-checkbox-list-option-element-border':
|
|
||||||
optionElementBorder.color || customOptionElementBorder,
|
|
||||||
'--wc-product-filter-checkbox-list-option-element-selected':
|
|
||||||
optionElementSelected.color || customOptionElementSelected,
|
|
||||||
'--wc-product-filter-checkbox-list-option-element':
|
|
||||||
optionElement.color || customOptionElement,
|
|
||||||
},
|
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const loadingState = useMemo( () => {
|
const loadingState = useMemo( () => {
|
||||||
|
@ -131,9 +120,9 @@ const Edit = ( props: EditProps ): JSX.Element => {
|
||||||
) ) }
|
) ) }
|
||||||
</ul>
|
</ul>
|
||||||
{ ! isLoading && isLongList && (
|
{ ! isLoading && isLongList && (
|
||||||
<span className="wc-block-product-filter-checkbox-list__show-more">
|
<button className="wc-block-product-filter-checkbox-list__show-more">
|
||||||
<small>{ __( 'Show more…', 'woocommerce' ) }</small>
|
{ __( 'Show more…', 'woocommerce' ) }
|
||||||
</span>
|
</button>
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
<InspectorControls group="color">
|
<InspectorControls group="color">
|
||||||
|
|
|
@ -11,10 +11,12 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||||
import metadata from './block.json';
|
import metadata from './block.json';
|
||||||
import Edit from './edit';
|
import Edit from './edit';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
import Save from './save';
|
||||||
|
|
||||||
if ( isExperimentalBlocksEnabled() ) {
|
if ( isExperimentalBlocksEnabled() ) {
|
||||||
registerBlockType( metadata, {
|
registerBlockType( metadata, {
|
||||||
edit: Edit,
|
edit: Edit,
|
||||||
icon: productFilterOptions,
|
icon: productFilterOptions,
|
||||||
|
save: Save,
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { useBlockProps } from '@wordpress/block-editor';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { BlockAttributes } from './types';
|
||||||
|
import { getColorClasses, getColorVars } from './utils';
|
||||||
|
|
||||||
|
const Save = ( {
|
||||||
|
attributes,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
attributes: BlockAttributes;
|
||||||
|
style: Record< string, string >;
|
||||||
|
} ) => {
|
||||||
|
const blockProps = useBlockProps.save( {
|
||||||
|
className: clsx(
|
||||||
|
'wc-block-product-filter-checkbox-list',
|
||||||
|
attributes.className,
|
||||||
|
getColorClasses( attributes )
|
||||||
|
),
|
||||||
|
style: {
|
||||||
|
...style,
|
||||||
|
...getColorVars( attributes ),
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
return <div { ...blockProps } />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Save;
|
|
@ -4,11 +4,6 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wc-block-product-filter-checkbox-list__item.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
:where(.wc-block-product-filter-checkbox-list__label) {
|
:where(.wc-block-product-filter-checkbox-list__label) {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -34,6 +29,7 @@
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
.has-option-element-color & {
|
.has-option-element-color & {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -51,6 +47,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
background: var(--wc-product-filter-checkbox-list-option-element, transparent);
|
background: var(--wc-product-filter-checkbox-list-option-element, transparent);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wc-block-product-filter-checkbox-list__input:checked + .wc-block-product-filter-checkbox-list__mark {
|
.wc-block-product-filter-checkbox-list__input:checked + .wc-block-product-filter-checkbox-list__mark {
|
||||||
|
@ -75,12 +72,15 @@
|
||||||
color: var(--wc-product-filter-checkbox-list-option-element-selected, currentColor);
|
color: var(--wc-product-filter-checkbox-list-option-element-selected, currentColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-checkbox-list__text) {
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
:where(.wc-block-product-filter-checkbox-list__show-more) {
|
:where(.wc-block-product-filter-checkbox-list__show-more) {
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
.wc-block-product-filter-checkbox-list__show-more.hidden {
|
border: none;
|
||||||
display: none;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { BlockAttributes } from './types';
|
||||||
|
|
||||||
|
function getCSSVar( slug: string | undefined, value: string | undefined ) {
|
||||||
|
if ( slug ) {
|
||||||
|
return `var(--wp--preset--color--${ slug })`;
|
||||||
|
}
|
||||||
|
return value || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColorVars( attributes: BlockAttributes ) {
|
||||||
|
const {
|
||||||
|
optionElement,
|
||||||
|
optionElementBorder,
|
||||||
|
optionElementSelected,
|
||||||
|
customOptionElement,
|
||||||
|
customOptionElementBorder,
|
||||||
|
customOptionElementSelected,
|
||||||
|
} = attributes;
|
||||||
|
|
||||||
|
const vars: Record< string, string > = {
|
||||||
|
'--wc-product-filter-checkbox-list-option-element': getCSSVar(
|
||||||
|
optionElement,
|
||||||
|
customOptionElement
|
||||||
|
),
|
||||||
|
'--wc-product-filter-checkbox-list-option-element-border': getCSSVar(
|
||||||
|
optionElementBorder,
|
||||||
|
customOptionElementBorder
|
||||||
|
),
|
||||||
|
'--wc-product-filter-checkbox-list-option-element-selected': getCSSVar(
|
||||||
|
optionElementSelected,
|
||||||
|
customOptionElementSelected
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.keys( vars ).reduce(
|
||||||
|
( acc: Record< string, string >, key ) => {
|
||||||
|
if ( vars[ key ] ) {
|
||||||
|
acc[ key ] = vars[ key ];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColorClasses( attributes: BlockAttributes ) {
|
||||||
|
const {
|
||||||
|
optionElement,
|
||||||
|
optionElementBorder,
|
||||||
|
optionElementSelected,
|
||||||
|
customOptionElement,
|
||||||
|
customOptionElementBorder,
|
||||||
|
customOptionElementSelected,
|
||||||
|
} = attributes;
|
||||||
|
|
||||||
|
return {
|
||||||
|
'has-option-element-color': optionElement || customOptionElement,
|
||||||
|
'has-option-element-border-color':
|
||||||
|
optionElementBorder || customOptionElementBorder,
|
||||||
|
'has-option-element-selected-color':
|
||||||
|
optionElementSelected || customOptionElementSelected,
|
||||||
|
};
|
||||||
|
}
|
|
@ -15,8 +15,44 @@
|
||||||
],
|
],
|
||||||
"supports": {},
|
"supports": {},
|
||||||
"usesContext": [
|
"usesContext": [
|
||||||
"filterData",
|
"filterData"
|
||||||
"isParentSelected"
|
|
||||||
],
|
],
|
||||||
"attributes": {}
|
"attributes": {
|
||||||
|
"chipText":{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"customChipText":{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"chipBackground":{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"customChipBackground":{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"chipBorder":{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"customChipBorder":{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"selectedChipText":{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"customSelectedChipText":{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"selectedChipBackground":{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"customSelectedChipBackground":{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"selectedChipBorder":{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"customSelectedChipBorder":{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,260 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useBlockProps } from '@wordpress/block-editor';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useMemo } from '@wordpress/element';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {
|
||||||
|
InspectorControls,
|
||||||
|
useBlockProps,
|
||||||
|
withColors,
|
||||||
|
// @ts-expect-error - no types.
|
||||||
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||||
|
__experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown,
|
||||||
|
// @ts-expect-error - no types.
|
||||||
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||||
|
__experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients,
|
||||||
|
} from '@wordpress/block-editor';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import './style.scss';
|
import { EditProps } from './types';
|
||||||
|
import './editor.scss';
|
||||||
|
import { getColorClasses, getColorVars } from './utils';
|
||||||
|
|
||||||
const Edit = () => {
|
const Edit = ( props: EditProps ): JSX.Element => {
|
||||||
return <div { ...useBlockProps() }>These are chips.</div>;
|
const colorGradientSettings = useMultipleOriginColorsAndGradients();
|
||||||
|
const {
|
||||||
|
context,
|
||||||
|
clientId,
|
||||||
|
attributes,
|
||||||
|
setAttributes,
|
||||||
|
chipText,
|
||||||
|
setChipText,
|
||||||
|
chipBackground,
|
||||||
|
setChipBackground,
|
||||||
|
chipBorder,
|
||||||
|
setChipBorder,
|
||||||
|
selectedChipText,
|
||||||
|
setSelectedChipText,
|
||||||
|
selectedChipBackground,
|
||||||
|
setSelectedChipBackground,
|
||||||
|
selectedChipBorder,
|
||||||
|
setSelectedChipBorder,
|
||||||
|
} = props;
|
||||||
|
const {
|
||||||
|
customChipText,
|
||||||
|
customChipBackground,
|
||||||
|
customChipBorder,
|
||||||
|
customSelectedChipText,
|
||||||
|
customSelectedChipBackground,
|
||||||
|
customSelectedChipBorder,
|
||||||
|
} = attributes;
|
||||||
|
const { filterData } = context;
|
||||||
|
const { isLoading, items } = filterData;
|
||||||
|
|
||||||
|
const blockProps = useBlockProps( {
|
||||||
|
className: clsx( 'wc-block-product-filter-chips', {
|
||||||
|
'is-loading': isLoading,
|
||||||
|
...getColorClasses( attributes ),
|
||||||
|
} ),
|
||||||
|
style: getColorVars( attributes ),
|
||||||
|
} );
|
||||||
|
|
||||||
|
const loadingState = useMemo( () => {
|
||||||
|
return [ ...Array( 10 ) ].map( ( _, i ) => (
|
||||||
|
<div
|
||||||
|
className="wc-block-product-filter-chips__item"
|
||||||
|
key={ i }
|
||||||
|
style={ {
|
||||||
|
/* stylelint-disable */
|
||||||
|
width: Math.floor( Math.random() * ( 100 - 25 ) ) + '%',
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
) );
|
||||||
|
}, [] );
|
||||||
|
|
||||||
|
if ( ! items ) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const threshold = 15;
|
||||||
|
const isLongList = items.length > threshold;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div { ...blockProps }>
|
||||||
|
<div className="wc-block-product-filter-chips__items">
|
||||||
|
{ isLoading && loadingState }
|
||||||
|
{ ! isLoading &&
|
||||||
|
( isLongList
|
||||||
|
? items.slice( 0, threshold )
|
||||||
|
: items
|
||||||
|
).map( ( item, index ) => (
|
||||||
|
<div
|
||||||
|
key={ index }
|
||||||
|
className="wc-block-product-filter-chips__item"
|
||||||
|
aria-checked={ !! item.selected }
|
||||||
|
>
|
||||||
|
<span className="wc-block-product-filter-chips__label">
|
||||||
|
{ item.label }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) ) }
|
||||||
|
</div>
|
||||||
|
{ ! isLoading && isLongList && (
|
||||||
|
<button className="wc-block-product-filter-chips__show-more">
|
||||||
|
{ __( 'Show more…', 'woocommerce' ) }
|
||||||
|
</button>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
<InspectorControls group="color">
|
||||||
|
{ colorGradientSettings.hasColorsOrGradients && (
|
||||||
|
<ColorGradientSettingsDropdown
|
||||||
|
__experimentalIsRenderedInSidebar
|
||||||
|
settings={ [
|
||||||
|
{
|
||||||
|
label: __(
|
||||||
|
'Unselected Chip Text',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
colorValue: chipText.color || customChipText,
|
||||||
|
onColorChange: ( colorValue: string ) => {
|
||||||
|
setChipText( colorValue );
|
||||||
|
setAttributes( {
|
||||||
|
customChipText: colorValue,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
resetAllFilter: () => {
|
||||||
|
setChipText( '' );
|
||||||
|
setAttributes( {
|
||||||
|
customChipText: '',
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __(
|
||||||
|
'Unselected Chip Border',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
colorValue:
|
||||||
|
chipBorder.color || customChipBorder,
|
||||||
|
onColorChange: ( colorValue: string ) => {
|
||||||
|
setChipBorder( colorValue );
|
||||||
|
setAttributes( {
|
||||||
|
customChipBorder: colorValue,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
resetAllFilter: () => {
|
||||||
|
setChipBorder( '' );
|
||||||
|
setAttributes( {
|
||||||
|
customChipBorder: '',
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __(
|
||||||
|
'Unselected Chip Background',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
colorValue:
|
||||||
|
chipBackground.color ||
|
||||||
|
customChipBackground,
|
||||||
|
onColorChange: ( colorValue: string ) => {
|
||||||
|
setChipBackground( colorValue );
|
||||||
|
setAttributes( {
|
||||||
|
customChipBackground: colorValue,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
resetAllFilter: () => {
|
||||||
|
setChipBackground( '' );
|
||||||
|
setAttributes( {
|
||||||
|
customChipBackground: '',
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __(
|
||||||
|
'Selected Chip Text',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
colorValue:
|
||||||
|
selectedChipText.color ||
|
||||||
|
customSelectedChipText,
|
||||||
|
onColorChange: ( colorValue: string ) => {
|
||||||
|
setSelectedChipText( colorValue );
|
||||||
|
setAttributes( {
|
||||||
|
customSelectedChipText: colorValue,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
resetAllFilter: () => {
|
||||||
|
setSelectedChipText( '' );
|
||||||
|
setAttributes( {
|
||||||
|
customSelectedChipText: '',
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __(
|
||||||
|
'Selected Chip Border',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
colorValue:
|
||||||
|
selectedChipBorder.color ||
|
||||||
|
customSelectedChipBorder,
|
||||||
|
onColorChange: ( colorValue: string ) => {
|
||||||
|
setSelectedChipBorder( colorValue );
|
||||||
|
setAttributes( {
|
||||||
|
customSelectedChipBorder: colorValue,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
resetAllFilter: () => {
|
||||||
|
setSelectedChipBorder( '' );
|
||||||
|
setAttributes( {
|
||||||
|
customSelectedChipBorder: '',
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __(
|
||||||
|
'Selected Chip Background',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
colorValue:
|
||||||
|
selectedChipBackground.color ||
|
||||||
|
customSelectedChipBackground,
|
||||||
|
onColorChange: ( colorValue: string ) => {
|
||||||
|
setSelectedChipBackground( colorValue );
|
||||||
|
setAttributes( {
|
||||||
|
customSelectedChipBackground:
|
||||||
|
colorValue,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
resetAllFilter: () => {
|
||||||
|
setSelectedChipBackground( '' );
|
||||||
|
setAttributes( {
|
||||||
|
customSelectedChipBackground: '',
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] }
|
||||||
|
panelId={ clientId }
|
||||||
|
{ ...colorGradientSettings }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</InspectorControls>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Edit;
|
export default withColors( {
|
||||||
|
chipText: 'chip-text',
|
||||||
|
chipBorder: 'chip-border',
|
||||||
|
chipBackground: 'chip-background',
|
||||||
|
selectedChipText: 'selected-chip-text',
|
||||||
|
selectedChipBorder: 'selected-chip-border',
|
||||||
|
selectedChipBackground: 'selected-chip-background',
|
||||||
|
} )( Edit );
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
.wc-block-product-filter-chips.is-loading {
|
||||||
|
.wc-block-product-filter-chips__item {
|
||||||
|
@include placeholder();
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { getElement, getContext, store } from '@woocommerce/interactivity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ChipsContext = {
|
||||||
|
items: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
checked: boolean;
|
||||||
|
}[];
|
||||||
|
showAll: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
store( 'woocommerce/product-filter-chips', {
|
||||||
|
actions: {
|
||||||
|
showAllItems: () => {
|
||||||
|
const context = getContext< ChipsContext >();
|
||||||
|
context.showAll = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
selectItem: () => {
|
||||||
|
const { ref } = getElement();
|
||||||
|
const value = ref.getAttribute( 'value' );
|
||||||
|
|
||||||
|
if ( ! value ) return;
|
||||||
|
|
||||||
|
const context = getContext< ChipsContext >();
|
||||||
|
|
||||||
|
context.items = context.items.map( ( item ) => {
|
||||||
|
if ( item.value.toString() === value ) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
checked: ! item.checked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} );
|
|
@ -10,11 +10,13 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||||
*/
|
*/
|
||||||
import metadata from './block.json';
|
import metadata from './block.json';
|
||||||
import Edit from './edit';
|
import Edit from './edit';
|
||||||
|
import Save from './save';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
if ( isExperimentalBlocksEnabled() ) {
|
if ( isExperimentalBlocksEnabled() ) {
|
||||||
registerBlockType( metadata, {
|
registerBlockType( metadata, {
|
||||||
edit: Edit,
|
edit: Edit,
|
||||||
icon: productFilterOptions,
|
icon: productFilterOptions,
|
||||||
|
save: Save,
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { useBlockProps } from '@wordpress/block-editor';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { BlockAttributes } from './types';
|
||||||
|
import { getColorClasses, getColorVars } from './utils';
|
||||||
|
|
||||||
|
const Save = ( {
|
||||||
|
attributes,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
attributes: BlockAttributes;
|
||||||
|
style: Record< string, string >;
|
||||||
|
} ) => {
|
||||||
|
const blockProps = useBlockProps.save( {
|
||||||
|
className: clsx(
|
||||||
|
'wc-block-product-filter-chips',
|
||||||
|
attributes.className,
|
||||||
|
getColorClasses( attributes )
|
||||||
|
),
|
||||||
|
style: {
|
||||||
|
...style,
|
||||||
|
...getColorVars( attributes ),
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
return <div { ...blockProps } />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Save;
|
|
@ -1,3 +1,55 @@
|
||||||
:where(.wc-block-product-filter-chips) {
|
:where(.wc-block-product-filter-chips__items) {
|
||||||
// WIP
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $gap-smallest;
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-chips__item) {
|
||||||
|
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
||||||
|
padding: $gap-smallest $gap-smaller;
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 0.875em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.has-chip-text & {
|
||||||
|
color: var(--wc-product-filter-chips-text);
|
||||||
|
}
|
||||||
|
.has-chip-background & {
|
||||||
|
background: var(--wc-product-filter-chips-background);
|
||||||
|
}
|
||||||
|
.has-chip-border & {
|
||||||
|
border-color: var(--wc-product-filter-chips-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-chips__item[aria-checked="true"]) {
|
||||||
|
background: currentColor;
|
||||||
|
|
||||||
|
.has-selected-chip-text & {
|
||||||
|
color: var(--wc-product-filter-chips-selected-text);
|
||||||
|
}
|
||||||
|
.has-selected-chip-background & {
|
||||||
|
background: var(--wc-product-filter-chips-selected-background);
|
||||||
|
}
|
||||||
|
.has-selected-chip-border & {
|
||||||
|
border-color: var(--wc-product-filter-chips-selected-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(
|
||||||
|
.wc-block-product-filter-chips:not(.has-selected-chip-text)
|
||||||
|
.wc-block-product-filter-chips__item[aria-checked="true"]
|
||||||
|
> .wc-block-product-filter-chips__label
|
||||||
|
) {
|
||||||
|
filter: invert(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-chips__show-more) {
|
||||||
|
text-decoration: underline;
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,44 @@ import { BlockEditProps } from '@wordpress/blocks';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
import { FilterBlockContext } from '../../types';
|
||||||
|
|
||||||
|
export type Color = {
|
||||||
|
slug?: string;
|
||||||
|
name?: string;
|
||||||
|
class?: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type BlockAttributes = {
|
export type BlockAttributes = {
|
||||||
className: string;
|
className: string;
|
||||||
|
chipText?: string;
|
||||||
|
customChipText?: string;
|
||||||
|
chipBackground?: string;
|
||||||
|
customChipBackground?: string;
|
||||||
|
chipBorder?: string;
|
||||||
|
customChipBorder?: string;
|
||||||
|
selectedChipText?: string;
|
||||||
|
customSelectedChipText?: string;
|
||||||
|
selectedChipBackground?: string;
|
||||||
|
customSelectedChipBackground?: string;
|
||||||
|
selectedChipBorder?: string;
|
||||||
|
customSelectedChipBorder?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditProps = BlockEditProps< BlockAttributes >;
|
export type EditProps = BlockEditProps< BlockAttributes > & {
|
||||||
|
style: Record< string, string >;
|
||||||
|
context: FilterBlockContext;
|
||||||
|
chipText: Color;
|
||||||
|
setChipText: ( value: string ) => void;
|
||||||
|
chipBackground: Color;
|
||||||
|
setChipBackground: ( value: string ) => void;
|
||||||
|
chipBorder: Color;
|
||||||
|
setChipBorder: ( value: string ) => void;
|
||||||
|
selectedChipText: Color;
|
||||||
|
setSelectedChipText: ( value: string ) => void;
|
||||||
|
selectedChipBackground: Color;
|
||||||
|
setSelectedChipBackground: ( value: string ) => void;
|
||||||
|
selectedChipBorder: Color;
|
||||||
|
setSelectedChipBorder: ( value: string ) => void;
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { BlockAttributes } from './types';
|
||||||
|
|
||||||
|
function getCSSVar( slug: string | undefined, value: string | undefined ) {
|
||||||
|
if ( slug ) {
|
||||||
|
return `var(--wp--preset--color--${ slug })`;
|
||||||
|
}
|
||||||
|
return value || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColorVars( attributes: BlockAttributes ) {
|
||||||
|
const {
|
||||||
|
chipText,
|
||||||
|
chipBackground,
|
||||||
|
chipBorder,
|
||||||
|
selectedChipText,
|
||||||
|
selectedChipBackground,
|
||||||
|
selectedChipBorder,
|
||||||
|
customChipText,
|
||||||
|
customChipBackground,
|
||||||
|
customChipBorder,
|
||||||
|
customSelectedChipText,
|
||||||
|
customSelectedChipBackground,
|
||||||
|
customSelectedChipBorder,
|
||||||
|
} = attributes;
|
||||||
|
|
||||||
|
const vars: Record< string, string > = {
|
||||||
|
'--wc-product-filter-chips-text': getCSSVar( chipText, customChipText ),
|
||||||
|
'--wc-product-filter-chips-background': getCSSVar(
|
||||||
|
chipBackground,
|
||||||
|
customChipBackground
|
||||||
|
),
|
||||||
|
'--wc-product-filter-chips-border': getCSSVar(
|
||||||
|
chipBorder,
|
||||||
|
customChipBorder
|
||||||
|
),
|
||||||
|
'--wc-product-filter-chips-selected-text': getCSSVar(
|
||||||
|
selectedChipText,
|
||||||
|
customSelectedChipText
|
||||||
|
),
|
||||||
|
'--wc-product-filter-chips-selected-background': getCSSVar(
|
||||||
|
selectedChipBackground,
|
||||||
|
customSelectedChipBackground
|
||||||
|
),
|
||||||
|
'--wc-product-filter-chips-selected-border': getCSSVar(
|
||||||
|
selectedChipBorder,
|
||||||
|
customSelectedChipBorder
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.keys( vars ).reduce(
|
||||||
|
( acc: Record< string, string >, key ) => {
|
||||||
|
if ( vars[ key ] ) {
|
||||||
|
acc[ key ] = vars[ key ];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColorClasses( attributes: BlockAttributes ) {
|
||||||
|
const {
|
||||||
|
chipText,
|
||||||
|
chipBackground,
|
||||||
|
chipBorder,
|
||||||
|
selectedChipText,
|
||||||
|
selectedChipBackground,
|
||||||
|
selectedChipBorder,
|
||||||
|
customChipText,
|
||||||
|
customChipBackground,
|
||||||
|
customChipBorder,
|
||||||
|
customSelectedChipText,
|
||||||
|
customSelectedChipBackground,
|
||||||
|
customSelectedChipBorder,
|
||||||
|
} = attributes;
|
||||||
|
|
||||||
|
return {
|
||||||
|
'has-chip-text-color': chipText || customChipText,
|
||||||
|
'has-chip-background-color': chipBackground || customChipBackground,
|
||||||
|
'has-chip-border-color': chipBorder || customChipBorder,
|
||||||
|
'has-selected-chip-text-color':
|
||||||
|
selectedChipText || customSelectedChipText,
|
||||||
|
'has-selected-chip-background-color':
|
||||||
|
selectedChipBackground || customSelectedChipBackground,
|
||||||
|
'has-selected-chip-border-color':
|
||||||
|
selectedChipBorder || customSelectedChipBorder,
|
||||||
|
};
|
||||||
|
}
|
|
@ -10,6 +10,10 @@ import {
|
||||||
import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks';
|
import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks';
|
||||||
import { useEffect } from '@wordpress/element';
|
import { useEffect } from '@wordpress/element';
|
||||||
import { useSelect } from '@wordpress/data';
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import ErrorPlaceholder, {
|
||||||
|
ErrorObject,
|
||||||
|
} from '@woocommerce/editor-components/error-placeholder';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -132,14 +136,16 @@ export const Edit = ( {
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
const mode = getMode( currentTemplateId, templateType );
|
const mode = getMode( currentTemplateId, templateType );
|
||||||
|
const newProductGalleryClientId =
|
||||||
|
attributes.productGalleryClientId || clientId;
|
||||||
|
|
||||||
setAttributes( {
|
setAttributes( {
|
||||||
...attributes,
|
...attributes,
|
||||||
mode,
|
mode,
|
||||||
productGalleryClientId: clientId,
|
productGalleryClientId: newProductGalleryClientId,
|
||||||
} );
|
} );
|
||||||
// Move the Thumbnails block to the correct above or below the Large Image based on the thumbnailsPosition attribute.
|
// Move the Thumbnails block to the correct above or below the Large Image based on the thumbnailsPosition attribute.
|
||||||
moveInnerBlocksToPosition( attributes, clientId );
|
moveInnerBlocksToPosition( attributes, newProductGalleryClientId );
|
||||||
}, [
|
}, [
|
||||||
setAttributes,
|
setAttributes,
|
||||||
attributes,
|
attributes,
|
||||||
|
@ -148,6 +154,18 @@ export const Edit = ( {
|
||||||
templateType,
|
templateType,
|
||||||
] );
|
] );
|
||||||
|
|
||||||
|
if ( attributes.productGalleryClientId !== clientId ) {
|
||||||
|
const error = {
|
||||||
|
message: __(
|
||||||
|
'productGalleryClientId and clientId codes mismatch.',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
type: 'general',
|
||||||
|
} as ErrorObject;
|
||||||
|
|
||||||
|
return <ErrorPlaceholder error={ error } isLoading={ false } />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div { ...blockProps }>
|
<div { ...blockProps }>
|
||||||
<InspectorControls>
|
<InspectorControls>
|
||||||
|
|
|
@ -136,10 +136,10 @@ export const moveInnerBlocksToPosition = (
|
||||||
): void => {
|
): void => {
|
||||||
const { getBlock, getBlockRootClientId, getBlockIndex } =
|
const { getBlock, getBlockRootClientId, getBlockIndex } =
|
||||||
select( 'core/block-editor' );
|
select( 'core/block-editor' );
|
||||||
const { moveBlockToPosition } = dispatch( 'core/block-editor' );
|
|
||||||
const productGalleryBlock = getBlock( clientId );
|
const productGalleryBlock = getBlock( clientId );
|
||||||
|
|
||||||
if ( productGalleryBlock ) {
|
if ( productGalleryBlock?.name === 'woocommerce/product-gallery' ) {
|
||||||
|
const { moveBlockToPosition } = dispatch( 'core/block-editor' );
|
||||||
const previousLayout = productGalleryBlock.innerBlocks.length
|
const previousLayout = productGalleryBlock.innerBlocks.length
|
||||||
? productGalleryBlock.innerBlocks[ 0 ].attributes.layout
|
? productGalleryBlock.innerBlocks[ 0 ].attributes.layout
|
||||||
: null;
|
: null;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue