Merge branch 'trunk' into add/chips-style-and-new-interactitity-implementation
This commit is contained in:
commit
67386dd979
|
@ -31,7 +31,7 @@ jobs:
|
|||
run: unzip plugins/woocommerce/woocommerce.zip -d zipfile
|
||||
|
||||
- name: Upload the zip file as an artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
|
|
@ -4,23 +4,25 @@ on:
|
|||
- cron: '0 0 * * *' # Run at 12 AM UTC.
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
SOURCE_REF: trunk
|
||||
TARGET_REF: nightly
|
||||
RELEASE_ID: 25945111
|
||||
|
||||
permissions: { }
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository_owner == 'woocommerce'
|
||||
name: Nightly builds
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [trunk]
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ matrix.build }}
|
||||
ref: ${{ env.SOURCE_REF }}
|
||||
|
||||
- name: Setup WooCommerce Monorepo
|
||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||
|
@ -31,26 +33,31 @@ jobs:
|
|||
working-directory: plugins/woocommerce
|
||||
run: bash bin/build-zip.sh
|
||||
|
||||
- name: Deploy nightly build
|
||||
uses: WebFreak001/deploy-nightly@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload nightly build
|
||||
uses: WebFreak001/deploy-nightly@46ecbabd7fad70d3e7d2c97fe8cd54e7a52e215b #v3.2.0
|
||||
with:
|
||||
upload_url: https://uploads.github.com/repos/${{ github.repository }}/releases/25945111/assets{?name,label}
|
||||
release_id: 25945111
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
upload_url: https://uploads.github.com/repos/${{ github.repository }}/releases/${{ env.RELEASE_ID }}/assets{?name,label}
|
||||
release_id: ${{ env.RELEASE_ID }}
|
||||
asset_path: plugins/woocommerce/woocommerce.zip
|
||||
asset_name: woocommerce-${{ matrix.build }}-nightly.zip
|
||||
asset_name: woocommerce-${{ env.SOURCE_REF }}-nightly.zip
|
||||
asset_content_type: application/zip
|
||||
max_releases: 1
|
||||
update:
|
||||
name: Update nightly tag commit ref
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Update nightly tag
|
||||
uses: richardsimko/github-tag-action@v1.0.5
|
||||
|
||||
- name: Update nightly tag commit ref
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
tag_name: nightly
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const sourceRef = process.env.SOURCE_REF;
|
||||
const targetRef = process.env.TARGET_REF;
|
||||
const branchData = await github.rest.repos.getBranch({
|
||||
...context.repo,
|
||||
branch: sourceRef,
|
||||
});
|
||||
|
||||
await github.rest.git.updateRef({
|
||||
...context.repo,
|
||||
ref: `tags/${ targetRef }`,
|
||||
sha: branchData.data.commit.sha,
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
||||
- name: Upload the zip file as an artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
@ -216,7 +216,7 @@ jobs:
|
|||
run: bash bin/build-zip.sh
|
||||
|
||||
- name: Upload the zip file as an artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
@ -231,7 +231,7 @@ jobs:
|
|||
if: ${{ needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' }}
|
||||
steps:
|
||||
- id: download
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
@ -279,7 +279,7 @@ jobs:
|
|||
working-directory: tools/monorepo-utils
|
||||
|
||||
- id: download
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
@ -300,7 +300,7 @@ jobs:
|
|||
if: ${{ needs.code-freeze-prep.outputs.isTodayAcceleratedFreeze == 'yes' }}
|
||||
steps:
|
||||
- id: download
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
@ -348,7 +348,7 @@ jobs:
|
|||
working-directory: tools/monorepo-utils
|
||||
|
||||
- id: download
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
@ -369,7 +369,7 @@ jobs:
|
|||
if: ${{ needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' }}
|
||||
steps:
|
||||
- id: download
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
@ -380,7 +380,7 @@ jobs:
|
|||
run: unzip ${{ steps.download.outputs.download-path }}/woocommerce.zip -d zipfile
|
||||
|
||||
- name: Upload the zip file as an artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
@ -395,7 +395,7 @@ jobs:
|
|||
if: ${{ needs.code-freeze-prep.outputs.isTodayAcceleratedFreeze == 'yes' }}
|
||||
steps:
|
||||
- id: download
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
@ -406,7 +406,7 @@ jobs:
|
|||
run: unzip ${{ steps.download.outputs.download-path }}/woocommerce.zip -d zipfile
|
||||
|
||||
- name: Upload the zip file as an artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
name: Storybook GitHub Pages
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 2 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/env bash
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
# '1' is branch
|
||||
CHECKOUT_TYPE=$3
|
||||
redColoured='\033[0;31m'
|
||||
whiteColoured='\033[0m'
|
||||
|
||||
if [ "$CHECKOUT_TYPE" = '1' ]; then
|
||||
canUpdateDependencies='no'
|
||||
|
||||
# Prompt about pnpm versions mismatch when switching between branches.
|
||||
currentPnpmVersion=$( ( command -v pnpm > /dev/null && pnpm -v ) || echo 'n/a' )
|
||||
targetPnpmVersion=$( grep packageManager package.json | sed -nr 's/.+packageManager.+pnpm@([[:digit:].]+).+/\1/p' )
|
||||
if [ "$currentPnpmVersion" != "$targetPnpmVersion" ]; then
|
||||
printf "${redColoured}pnpm versions mismatch: in use '$currentPnpmVersion', needed '$targetPnpmVersion'. Here some hints how to solve this:\n"
|
||||
printf "${redColoured}* actualize environment: 'nvm use && pnpm -v' (the most common case)\n"
|
||||
printf "${redColoured}* install: 'npm install -g pnpm@$targetPnpmVersion'\n"
|
||||
else
|
||||
canUpdateDependencies='yes'
|
||||
fi
|
||||
|
||||
# Auto-refresh dependencies when switching between branches.
|
||||
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
|
||||
printf "${whiteColoured}It was a change in the following file(s) - refreshing dependencies:\n"
|
||||
printf "${whiteColoured} %s\n" $changedManifests
|
||||
|
||||
if [ "$canUpdateDependencies" = 'yes' ]; then
|
||||
pnpm install --frozen-lockfile
|
||||
else
|
||||
printf "${redColoured}Skipping dependencies refresh. Please actualize pnpm version and execute 'pnpm install --frozen-lockfile' manually.\n"
|
||||
fi
|
||||
fi
|
||||
fi
|
|
@ -3,7 +3,7 @@
|
|||
"MD003": { "style": "atx" },
|
||||
"MD007": { "indent": 4 },
|
||||
"MD013": { "line_length": 9999 },
|
||||
"MD024": { "allow_different_nesting": true },
|
||||
"MD024": { "siblings_only": true },
|
||||
"MD033": { "allowed_elements": [ "video" ] },
|
||||
"no-hard-tabs": false,
|
||||
"whitespace": false
|
||||
|
|
2
.npmrc
2
.npmrc
|
@ -1,3 +1,5 @@
|
|||
; adding this as npm 7 automatically installs peer dependencies but pnpm does not
|
||||
auto-install-peers=true
|
||||
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
|
||||
|
|
15
CODEOWNERS
15
CODEOWNERS
|
@ -1 +1,14 @@
|
|||
/.github/ @woocommerce/atlas
|
||||
# Monorepo CI and package managers manifests.
|
||||
/.github/ @woocommerce/flux
|
||||
**/composer.json @woocommerce/flux
|
||||
**/package.json @woocommerce/flux
|
||||
|
||||
# Monorepo tooling folders.
|
||||
/bin/ @woocommerce/flux
|
||||
/tools/ @woocommerce/flux
|
||||
/packages/js/eslint-plugin/ @woocommerce/flux
|
||||
/packages/js/dependency-extraction-webpack-plugin/ @woocommerce/flux
|
||||
|
||||
# Files in root of repository
|
||||
/.* @woocommerce/flux
|
||||
/*.* @woocommerce/flux
|
||||
|
|
|
@ -20,8 +20,8 @@ To get up and running within the WooCommerce Monorepo, you will need to make sur
|
|||
Once you've installed all of the prerequisites, you can run the following commands to get everything working.
|
||||
|
||||
```bash
|
||||
# Ensure that you're using the correct version of Node
|
||||
nvm use
|
||||
# Ensure that correct version of Node is installed and being used
|
||||
nvm install
|
||||
# Install the PHP and Composer dependencies for all of the plugins, packages, and tools
|
||||
pnpm install
|
||||
# Build all of the plugins, packages, and tools in the monorepo
|
||||
|
|
|
@ -145,6 +145,7 @@
|
|||
* Update - Update core profiler continue button container on extension screen [#50582](https://github.com/woocommerce/woocommerce/pull/50582)
|
||||
* Update - Update Store Alert actions to have unique keys. [#50424](https://github.com/woocommerce/woocommerce/pull/50424)
|
||||
* Update - Update WooCommercePayments task is_supported to use default suggestions [#50585](https://github.com/woocommerce/woocommerce/pull/50585)
|
||||
* Update - Enhance CSV path and upload handling in product import [#51344](https://github.com/woocommerce/woocommerce/pull/51344)
|
||||
* Dev - Execute test env setup on host instead of wp-env container [#51021](https://github.com/woocommerce/woocommerce/pull/51021)
|
||||
* Dev - Added code docs with examples to the Analytics classes [#49425](https://github.com/woocommerce/woocommerce/pull/49425)
|
||||
* Dev - Add lost password e2e tests [#50611](https://github.com/woocommerce/woocommerce/pull/50611)
|
||||
|
|
|
@ -874,7 +874,7 @@
|
|||
"menu_title": "Development environment setup",
|
||||
"tags": "tutorial, setup",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/development-environment.md",
|
||||
"hash": "9e471d3f44a882fe61dcad9e5207d51b280a7220aae1bf6e4ae1fbdd68b7e3d4",
|
||||
"hash": "bf5d77349ea64d1b8e19fe6b7472be35ed92406c5aafe677ce92363fb13f94d4",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/development-environment.md",
|
||||
"id": "9080572a3904349c44c565ca7e1bef1212c58757"
|
||||
},
|
||||
|
@ -1059,7 +1059,7 @@
|
|||
"menu_title": "DOM Events",
|
||||
"tags": "how-to",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-collection-block/dom-events.md",
|
||||
"hash": "59a4b49eb146774d33229bc60ab7d8f74381493f6e7089ca8f0e2d0eb433a7a4",
|
||||
"hash": "85cffe1cc273621f16f7362b5efe28ede9689cee0a6e87d0d426014bacc24b05",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-collection-block/dom-events.md",
|
||||
"id": "c8d247b91472740075871e6b57a9583d893ac650"
|
||||
}
|
||||
|
@ -1229,7 +1229,7 @@
|
|||
"menu_title": "Core critical flows",
|
||||
"tags": "reference",
|
||||
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/core-critical-flows.md",
|
||||
"hash": "472f5a240abe907fec83a8a9f88af6699f2d994aa7ae87faa1716a087baa66db",
|
||||
"hash": "34109195216ebcb5b23e741391b9f355ba861777a5533d4ef1e341472cb5209e",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/core-critical-flows.md",
|
||||
"id": "e561b46694dba223c38b87613ce4907e4e14333a"
|
||||
},
|
||||
|
@ -1804,5 +1804,5 @@
|
|||
"categories": []
|
||||
}
|
||||
],
|
||||
"hash": "77c102c35a45b0681e7b70def9d639d764e4e5068121c2ef4dd23477c0f8784c"
|
||||
"hash": "47dc2a7e213e1e9d83e93a85dafdf8c7b539cc5474b4166eb6398b734d150ff3"
|
||||
}
|
|
@ -80,22 +80,16 @@ git clone https://github.com/woocommerce/woocommerce.git
|
|||
cd woocommerce
|
||||
```
|
||||
|
||||
### Activate the required Node version
|
||||
### Install and Activate the required Node version
|
||||
|
||||
```sh
|
||||
nvm use
|
||||
Found '/path/to/woocommerce/.nvmrc' with version <v12>
|
||||
Now using node v12.21.0 (npm v6.14.11)
|
||||
nvm install
|
||||
Found '/path/to/woocommerce/.nvmrc' with version <v20>
|
||||
v20.17.0 is already installed.
|
||||
Now using node v20.17.0 (npm v10.8.2)
|
||||
```
|
||||
|
||||
Note: if you don't have the required version of Node installed, NVM will alert you so you can install it:
|
||||
|
||||
```sh
|
||||
Found '/path/to/woocommerce/.nvmrc' with version <v12>
|
||||
N/A: version "v12 -> N/A" is not yet installed.
|
||||
|
||||
You need to run "nvm install v12" to install it before using it.
|
||||
```
|
||||
Note: if you don't have the required version of Node installed, NVM will install it.
|
||||
|
||||
### Install dependencies
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
||||
### `wc-blocks_product_list_rendered` - `detail` parameters
|
||||
### `detail` parameters
|
||||
|
||||
| Parameter | Type | Default value | Description |
|
||||
| ------------------ | ------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `collection` | string | `undefined` | Collection type. It's `undefined` for "create your own" collections as the type is not specified. For other Core collections it can be one of type: `woocommerce/product-collection/best-sellers`, `woocommerce/product-collection/featured`, `woocommerce/product-collection/new-arrivals`, `woocommerce/product-collection/on-sale`, `woocommerce/product-collection/top-rated`. For custom collections it will hold their name. |
|
||||
|
||||
### `wc-blocks_product_list_rendered` - Example usage
|
||||
### Example usage
|
||||
|
||||
```javascript
|
||||
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).
|
||||
|
||||
### `wc-blocks_viewed_product` - `detail` parameters
|
||||
### `detail` parameters
|
||||
|
||||
| Parameter | Type | Default value | Description |
|
||||
| ------------------ | ------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `collection` | string | `undefined` | Collection type. It's `undefined` for "create your own" collections as the type is not specified. For other Core collections it can be one of type: `woocommerce/product-collection/best-sellers`, `woocommerce/product-collection/featured`, `woocommerce/product-collection/new-arrivals`, `woocommerce/product-collection/on-sale`, `woocommerce/product-collection/top-rated`. For custom collections it will hold their name. |
|
||||
| `productId` | number | | Product ID |
|
||||
|
||||
### `wc-blocks_viewed_product` Example usage
|
||||
### Example usage
|
||||
|
||||
```javascript
|
||||
window.document.addEventListener(
|
||||
|
|
|
@ -147,13 +147,14 @@ These flows will continually evolve as the platform evolves with flows updated,
|
|||
### Merchant - Settings
|
||||
|
||||
| User Type | Flow Area | Flow Name | Test File |
|
||||
| --------- | --------- | -------------------------------------- | ---------------------------------------- |
|
||||
| --------- | --------- |----------------------------------------|------------------------------------------|
|
||||
| Merchant | Settings | Update General Settings | merchant/settings-general.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 Classes | merchant/create-shipping-classes.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 | Handle Product Brands | merchant/create-product-brand.spec.js |
|
||||
|
||||
### Merchant - Coupons
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Use page query param consistently as string for `getReportTableQuery`.
|
|
@ -561,7 +561,7 @@ export function getReportTableQuery(
|
|||
before: noIntervals
|
||||
? undefined
|
||||
: appendTimestamp( datesFromQuery.primary.before, 'end' ),
|
||||
page: query.paged || 1,
|
||||
page: query.paged || '1',
|
||||
per_page: query.per_page || QUERY_DEFAULTS.pageSize,
|
||||
...filterQuery,
|
||||
...tableQuery,
|
||||
|
|
|
@ -51,6 +51,48 @@ export default function Install( props: InstallProps ) {
|
|||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleInstallError = ( error: any ) => {
|
||||
loadSubscriptions( false ).then( () => {
|
||||
let errorMessage = sprintf(
|
||||
// translators: %s is the product name.
|
||||
__( '%s couldn’t be installed.', 'woocommerce' ),
|
||||
props.subscription.product_name
|
||||
);
|
||||
if ( error?.success === false && error?.data.message ) {
|
||||
errorMessage += ' ' + error.data.message;
|
||||
}
|
||||
addNotice(
|
||||
props.subscription.product_key,
|
||||
errorMessage,
|
||||
NoticeStatus.Error,
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: __(
|
||||
'Download and install manually',
|
||||
'woocommerce'
|
||||
),
|
||||
url: 'https://woocommerce.com/my-account/downloads/',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
stopInstall();
|
||||
|
||||
if ( props.onError ) {
|
||||
props.onError();
|
||||
}
|
||||
} );
|
||||
|
||||
recordEvent( 'marketplace_product_install_failed', {
|
||||
product_zip_slug: props.subscription.zip_slug,
|
||||
product_id: props.subscription.product_id,
|
||||
product_current_version: props.subscription.version,
|
||||
error_message: error?.data?.message,
|
||||
} );
|
||||
};
|
||||
|
||||
const install = () => {
|
||||
recordEvent( 'marketplace_product_install_button_clicked', {
|
||||
product_zip_slug: props.subscription.zip_slug,
|
||||
|
@ -90,45 +132,10 @@ export default function Install( props: InstallProps ) {
|
|||
props.onSuccess();
|
||||
}
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
loadSubscriptions( false ).then( () => {
|
||||
let errorMessage = sprintf(
|
||||
// translators: %s is the product name.
|
||||
__( '%s couldn’t be installed.', 'woocommerce' ),
|
||||
props.subscription.product_name
|
||||
);
|
||||
if ( error?.success === false && error?.data.message ) {
|
||||
errorMessage += ' ' + error.data.message;
|
||||
}
|
||||
addNotice(
|
||||
props.subscription.product_key,
|
||||
errorMessage,
|
||||
NoticeStatus.Error,
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: __( 'Try again', 'woocommerce' ),
|
||||
onClick: install,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
stopInstall();
|
||||
|
||||
if ( props.onError ) {
|
||||
props.onError();
|
||||
}
|
||||
} );
|
||||
|
||||
recordEvent( 'marketplace_product_install_failed', {
|
||||
product_zip_slug: props.subscription.zip_slug,
|
||||
product_id: props.subscription.product_id,
|
||||
product_current_version: props.subscription.version,
|
||||
error_message: error?.data?.message,
|
||||
} );
|
||||
} );
|
||||
.catch( handleInstallError );
|
||||
} else {
|
||||
getInstallUrl( props.subscription ).then( ( url: string ) => {
|
||||
getInstallUrl( props.subscription )
|
||||
.then( ( url: string ) => {
|
||||
recordEvent( 'marketplace_product_install_url', {
|
||||
product_zip_slug: props.subscription.zip_slug,
|
||||
product_id: props.subscription.product_id,
|
||||
|
@ -141,20 +148,10 @@ export default function Install( props: InstallProps ) {
|
|||
if ( url ) {
|
||||
window.open( url, '_self' );
|
||||
} else {
|
||||
addNotice(
|
||||
props.subscription.product_key,
|
||||
sprintf(
|
||||
// translators: %s is the product name.
|
||||
__(
|
||||
'%s couldn’t be installed. Please install the product manually.',
|
||||
'woocommerce'
|
||||
),
|
||||
props.subscription.product_name
|
||||
),
|
||||
NoticeStatus.Error
|
||||
);
|
||||
throw new Error();
|
||||
}
|
||||
} );
|
||||
} )
|
||||
.catch( handleInstallError );
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -100,7 +100,10 @@ function log_remote_event() {
|
|||
time(),
|
||||
'critical',
|
||||
'Test PHP event from WC Beta Tester',
|
||||
array( 'source' => 'wc-beta-tester' )
|
||||
array(
|
||||
'source' => 'wc-beta-tester',
|
||||
'remote-logging' => true,
|
||||
)
|
||||
);
|
||||
|
||||
if ( $result ) {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Set remote-logging context to true in log remote event method
|
|
@ -5,6 +5,7 @@ import { ValidatedTextInput } from '@woocommerce/blocks-components';
|
|||
import { AddressFormValues, ContactFormValues } from '@woocommerce/settings';
|
||||
import { useState, Fragment, useCallback } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Button } from '@ariakit/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -50,7 +51,8 @@ const AddressLine2Field = < T extends AddressFormValues | ContactFormValues >( {
|
|||
/>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
render={ <span /> }
|
||||
className={
|
||||
'wc-block-components-address-form__address_2-toggle'
|
||||
}
|
||||
|
@ -61,7 +63,7 @@ const AddressLine2Field = < T extends AddressFormValues | ContactFormValues >( {
|
|||
__( '+ Add %s', 'woocommerce' ),
|
||||
field.label.toLowerCase()
|
||||
) }
|
||||
</button>
|
||||
</Button>
|
||||
<input
|
||||
type="text"
|
||||
tabIndex={ -1 }
|
||||
|
|
|
@ -25,7 +25,11 @@ interface CheckoutAddress {
|
|||
setBillingAddress: ( data: Partial< BillingAddress > ) => void;
|
||||
setEmail: ( value: string ) => void;
|
||||
useShippingAsBilling: boolean;
|
||||
editingBillingAddress: boolean;
|
||||
editingShippingAddress: boolean;
|
||||
setUseShippingAsBilling: ( useShippingAsBilling: boolean ) => void;
|
||||
setEditingBillingAddress: ( isEditing: boolean ) => void;
|
||||
setEditingShippingAddress: ( isEditing: boolean ) => void;
|
||||
defaultFields: AddressFields;
|
||||
showShippingFields: boolean;
|
||||
showBillingFields: boolean;
|
||||
|
@ -40,15 +44,25 @@ interface CheckoutAddress {
|
|||
*/
|
||||
export const useCheckoutAddress = (): CheckoutAddress => {
|
||||
const { needsShipping } = useShippingData();
|
||||
const { useShippingAsBilling, prefersCollection } = useSelect(
|
||||
( select ) => ( {
|
||||
const {
|
||||
useShippingAsBilling,
|
||||
prefersCollection,
|
||||
editingBillingAddress,
|
||||
editingShippingAddress,
|
||||
} = useSelect( ( select ) => ( {
|
||||
useShippingAsBilling:
|
||||
select( CHECKOUT_STORE_KEY ).getUseShippingAsBilling(),
|
||||
prefersCollection: select( CHECKOUT_STORE_KEY ).prefersCollection(),
|
||||
} )
|
||||
);
|
||||
const { __internalSetUseShippingAsBilling } =
|
||||
useDispatch( CHECKOUT_STORE_KEY );
|
||||
editingBillingAddress:
|
||||
select( CHECKOUT_STORE_KEY ).getEditingBillingAddress(),
|
||||
editingShippingAddress:
|
||||
select( CHECKOUT_STORE_KEY ).getEditingShippingAddress(),
|
||||
} ) );
|
||||
const {
|
||||
__internalSetUseShippingAsBilling,
|
||||
setEditingBillingAddress,
|
||||
setEditingShippingAddress,
|
||||
} = useDispatch( CHECKOUT_STORE_KEY );
|
||||
const {
|
||||
billingAddress,
|
||||
setBillingAddress,
|
||||
|
@ -77,6 +91,10 @@ export const useCheckoutAddress = (): CheckoutAddress => {
|
|||
defaultFields,
|
||||
useShippingAsBilling,
|
||||
setUseShippingAsBilling: __internalSetUseShippingAsBilling,
|
||||
editingBillingAddress,
|
||||
editingShippingAddress,
|
||||
setEditingBillingAddress,
|
||||
setEditingShippingAddress,
|
||||
needsShipping,
|
||||
showShippingFields:
|
||||
! forcedBillingAddress && needsShipping && ! prefersCollection,
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from '@woocommerce/types';
|
||||
import { FormFieldsConfig, getSetting } from '@woocommerce/settings';
|
||||
import { formatAddress } from '@woocommerce/blocks/checkout/utils';
|
||||
import { Button } from '@ariakit/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -82,7 +83,8 @@ const AddressCard = ( {
|
|||
) }
|
||||
</address>
|
||||
{ onEdit && (
|
||||
<button
|
||||
<Button
|
||||
render={ <span /> }
|
||||
className="wc-block-components-address-card__edit"
|
||||
aria-controls={ target }
|
||||
aria-expanded={ isExpanded }
|
||||
|
@ -94,7 +96,7 @@ const AddressCard = ( {
|
|||
type="button"
|
||||
>
|
||||
{ __( 'Edit', 'woocommerce' ) }
|
||||
</button>
|
||||
</Button>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -7,14 +7,12 @@ import {
|
|||
useCheckoutAddress,
|
||||
useEditorContext,
|
||||
noticeContexts,
|
||||
useShippingData,
|
||||
} from '@woocommerce/base-context';
|
||||
import Noninteractive from '@woocommerce/base-components/noninteractive';
|
||||
import type { ShippingAddress, FormFieldsConfig } from '@woocommerce/settings';
|
||||
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { CART_STORE_KEY } from '@woocommerce/block-data';
|
||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -36,14 +34,9 @@ const Block = ( {
|
|||
showPhoneField: boolean;
|
||||
requirePhoneField: boolean;
|
||||
} ): JSX.Element => {
|
||||
const {
|
||||
shippingAddress,
|
||||
billingAddress,
|
||||
setShippingAddress,
|
||||
useBillingAsShipping,
|
||||
} = useCheckoutAddress();
|
||||
const { billingAddress, setShippingAddress, useBillingAsShipping } =
|
||||
useCheckoutAddress();
|
||||
const { isEditor } = useEditorContext();
|
||||
const { needsShipping } = useShippingData();
|
||||
|
||||
// Syncs shipping address with billing address if "Force shipping to the customer billing address" is enabled.
|
||||
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 (
|
||||
<>
|
||||
<StoreNoticesContainer context={ noticeContext } />
|
||||
|
@ -121,7 +101,6 @@ const Block = ( {
|
|||
{ cartDataLoaded ? (
|
||||
<CustomerAddress
|
||||
addressFieldsConfig={ addressFieldsConfig }
|
||||
defaultEditing={ defaultEditingAddress }
|
||||
/>
|
||||
) : null }
|
||||
</WrapperComponent>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState, useCallback, useEffect } from '@wordpress/element';
|
||||
import { useCallback, useEffect } from '@wordpress/element';
|
||||
import { Form } from '@woocommerce/base-components/cart-checkout';
|
||||
import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context';
|
||||
import type {
|
||||
|
@ -20,19 +20,18 @@ import AddressCard from '../../address-card';
|
|||
|
||||
const CustomerAddress = ( {
|
||||
addressFieldsConfig,
|
||||
defaultEditing = false,
|
||||
}: {
|
||||
addressFieldsConfig: FormFieldsConfig;
|
||||
defaultEditing?: boolean;
|
||||
} ) => {
|
||||
const {
|
||||
billingAddress,
|
||||
setShippingAddress,
|
||||
setBillingAddress,
|
||||
useBillingAsShipping,
|
||||
editingBillingAddress: editing,
|
||||
setEditingBillingAddress: setEditing,
|
||||
} = useCheckoutAddress();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
const [ editing, setEditing ] = useState( defaultEditing );
|
||||
|
||||
// Forces editing state if store has errors.
|
||||
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
|
||||
|
@ -55,7 +54,7 @@ const CustomerAddress = ( {
|
|||
if ( invalidProps.length > 0 && editing === false ) {
|
||||
setEditing( true );
|
||||
}
|
||||
}, [ editing, hasValidationErrors, invalidProps.length ] );
|
||||
}, [ editing, hasValidationErrors, invalidProps.length, setEditing ] );
|
||||
|
||||
const onChangeAddress = useCallback(
|
||||
( values: AddressFormValues ) => {
|
||||
|
@ -86,7 +85,7 @@ const CustomerAddress = ( {
|
|||
isExpanded={ editing }
|
||||
/>
|
||||
),
|
||||
[ billingAddress, addressFieldsConfig, editing ]
|
||||
[ billingAddress, addressFieldsConfig, editing, setEditing ]
|
||||
);
|
||||
|
||||
const renderAddressFormComponent = useCallback(
|
||||
|
|
|
@ -47,6 +47,7 @@ const Block = ( {
|
|||
billingAddress,
|
||||
useShippingAsBilling,
|
||||
setUseShippingAsBilling,
|
||||
setEditingBillingAddress,
|
||||
} = useCheckoutAddress();
|
||||
const { isEditor } = useEditorContext();
|
||||
const isGuest = getSetting( 'currentUserId' ) === 0;
|
||||
|
@ -116,10 +117,6 @@ const Block = ( {
|
|||
const noticeContext = useShippingAsBilling
|
||||
? [ noticeContexts.SHIPPING_ADDRESS, noticeContexts.BILLING_ADDRESS ]
|
||||
: [ noticeContexts.SHIPPING_ADDRESS ];
|
||||
const hasAddress = !! (
|
||||
shippingAddress.address_1 &&
|
||||
( shippingAddress.first_name || shippingAddress.last_name )
|
||||
);
|
||||
|
||||
const { cartDataLoaded } = useSelect( ( select ) => {
|
||||
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 (
|
||||
<>
|
||||
<StoreNoticesContainer context={ noticeContext } />
|
||||
|
@ -138,7 +132,6 @@ const Block = ( {
|
|||
{ cartDataLoaded ? (
|
||||
<CustomerAddress
|
||||
addressFieldsConfig={ addressFieldsConfig }
|
||||
defaultEditing={ defaultEditingAddress }
|
||||
/>
|
||||
) : null }
|
||||
</WrapperComponent>
|
||||
|
@ -151,6 +144,7 @@ const Block = ( {
|
|||
if ( checked ) {
|
||||
syncBillingWithShipping();
|
||||
} else {
|
||||
setEditingBillingAddress( true );
|
||||
clearBillingAddress( billingAddress );
|
||||
}
|
||||
} }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState, useCallback, useEffect } from '@wordpress/element';
|
||||
import { useCallback, useEffect } from '@wordpress/element';
|
||||
import { Form } from '@woocommerce/base-components/cart-checkout';
|
||||
import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context';
|
||||
import type {
|
||||
|
@ -20,19 +20,18 @@ import AddressCard from '../../address-card';
|
|||
|
||||
const CustomerAddress = ( {
|
||||
addressFieldsConfig,
|
||||
defaultEditing = false,
|
||||
}: {
|
||||
addressFieldsConfig: FormFieldsConfig;
|
||||
defaultEditing?: boolean;
|
||||
} ) => {
|
||||
const {
|
||||
shippingAddress,
|
||||
setShippingAddress,
|
||||
setBillingAddress,
|
||||
useShippingAsBilling,
|
||||
editingShippingAddress: editing,
|
||||
setEditingShippingAddress: setEditing,
|
||||
} = useCheckoutAddress();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
const [ editing, setEditing ] = useState( defaultEditing );
|
||||
|
||||
// Forces editing state if store has errors.
|
||||
const { hasValidationErrors, invalidProps } = useSelect( ( select ) => {
|
||||
|
@ -54,7 +53,7 @@ const CustomerAddress = ( {
|
|||
if ( invalidProps.length > 0 && editing === false ) {
|
||||
setEditing( true );
|
||||
}
|
||||
}, [ editing, hasValidationErrors, invalidProps.length ] );
|
||||
}, [ editing, hasValidationErrors, invalidProps.length, setEditing ] );
|
||||
|
||||
const onChangeAddress = useCallback(
|
||||
( values: AddressFormValues ) => {
|
||||
|
@ -85,7 +84,7 @@ const CustomerAddress = ( {
|
|||
isExpanded={ editing }
|
||||
/>
|
||||
),
|
||||
[ shippingAddress, addressFieldsConfig, editing ]
|
||||
[ shippingAddress, addressFieldsConfig, editing, setEditing ]
|
||||
);
|
||||
|
||||
const renderAddressFormComponent = useCallback(
|
||||
|
|
|
@ -10,6 +10,7 @@ import { CART_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
|||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { isPackageRateCollectable } from '@woocommerce/base-utils';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { Button } from '@ariakit/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -18,7 +19,6 @@ import { RatePrice, getLocalPickupPrices, getShippingPrices } from './shared';
|
|||
import type { minMaxPrices } from './shared';
|
||||
import { defaultLocalPickupText, defaultShippingText } from './constants';
|
||||
import { shippingAddressHasValidationErrors } from '../../../../data/cart/utils';
|
||||
import Button from '../../../../base/components/button';
|
||||
|
||||
const SHIPPING_RATE_ERROR = {
|
||||
hidden: true,
|
||||
|
@ -44,8 +44,8 @@ const LocalPickupSelector = ( {
|
|||
} ) => {
|
||||
return (
|
||||
<Button
|
||||
render={ <div /> }
|
||||
role="radio"
|
||||
removeTextWrap
|
||||
onClick={ onClick }
|
||||
className={ clsx( 'wc-block-checkout__shipping-method-option', {
|
||||
'wc-block-checkout__shipping-method-option--selected':
|
||||
|
@ -129,9 +129,9 @@ const ShippingSelector = ( {
|
|||
|
||||
return (
|
||||
<Button
|
||||
render={ <div /> }
|
||||
role="radio"
|
||||
onClick={ onClick }
|
||||
removeTextWrap
|
||||
className={ clsx( 'wc-block-checkout__shipping-method-option', {
|
||||
'wc-block-checkout__shipping-method-option--selected':
|
||||
checked === 'shipping',
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
useBlockProps,
|
||||
RichText,
|
||||
} from '@wordpress/block-editor';
|
||||
import Button from '@woocommerce/base-components/button';
|
||||
import { Button } from '@ariakit/react';
|
||||
import { useShippingData } from '@woocommerce/base-context/hooks';
|
||||
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
|
@ -53,12 +53,12 @@ const LocalPickupSelector = ( {
|
|||
} ) => {
|
||||
return (
|
||||
<Button
|
||||
render={ <div /> }
|
||||
className={ clsx( 'wc-block-checkout__shipping-method-option', {
|
||||
'wc-block-checkout__shipping-method-option--selected':
|
||||
checked === 'pickup',
|
||||
} ) }
|
||||
onClick={ onClick }
|
||||
removeTextWrap
|
||||
>
|
||||
{ showIcon === true && (
|
||||
<Icon
|
||||
|
@ -113,12 +113,12 @@ const ShippingSelector = ( {
|
|||
|
||||
return (
|
||||
<Button
|
||||
render={ <div /> }
|
||||
className={ clsx( 'wc-block-checkout__shipping-method-option', {
|
||||
'wc-block-checkout__shipping-method-option--selected':
|
||||
checked === 'shipping',
|
||||
} ) }
|
||||
onClick={ onClick }
|
||||
removeTextWrap
|
||||
>
|
||||
{ showIcon === true && (
|
||||
<Icon
|
||||
|
|
|
@ -9,22 +9,23 @@
|
|||
// We have avoided nesting all the styles in case specificity changes introduce regressions elsewhere.
|
||||
.edit-post-visual-editor {
|
||||
.wc-block-checkout__shipping-method-container {
|
||||
.wc-block-components-button.wc-block-checkout__shipping-method-option {
|
||||
.wc-block-checkout__shipping-method-option {
|
||||
min-height: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-post-visual-editor
|
||||
.wc-block-components-button.wc-block-checkout__shipping-method-option,
|
||||
.wc-block-components-button.wc-block-checkout__shipping-method-option {
|
||||
.edit-post-visual-editor .wc-block-checkout__shipping-method-option,
|
||||
.wc-block-checkout__shipping-method-option {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-height: 80px;
|
||||
box-sizing: border-box;
|
||||
flex-basis: 0;
|
||||
gap: 4px;
|
||||
padding: 16px 12px;
|
||||
|
@ -35,8 +36,8 @@
|
|||
outline: 1px solid $universal-border-light !important; // Overwriting Gutenberg styles
|
||||
border-radius: $universal-border-radius;
|
||||
cursor: pointer;
|
||||
&.components-button:hover:not(:disabled),
|
||||
&.components-button:focus:not(:disabled),
|
||||
&:hover:not(:disabled),
|
||||
&:focus:not(:disabled),
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: $universal-background;
|
||||
|
@ -65,6 +66,8 @@
|
|||
|
||||
.wc-block-checkout__shipping-method-option-price {
|
||||
@include font-size(small, 1rem);
|
||||
flex-basis: 100%;
|
||||
text-align: center;
|
||||
|
||||
em {
|
||||
text-transform: uppercase;
|
||||
|
|
|
@ -21,15 +21,36 @@ import {
|
|||
import type { ProductCollectionEditComponentProps } from '../types';
|
||||
import { getCollectionByName } from '../collections';
|
||||
|
||||
const ProductPicker = ( props: ProductCollectionEditComponentProps ) => {
|
||||
const ProductPicker = (
|
||||
props: ProductCollectionEditComponentProps & {
|
||||
isDeletedProductReference: boolean;
|
||||
}
|
||||
) => {
|
||||
const blockProps = useBlockProps();
|
||||
const attributes = props.attributes;
|
||||
const { attributes, isDeletedProductReference } = props;
|
||||
|
||||
const collection = getCollectionByName( attributes.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 (
|
||||
<div { ...blockProps }>
|
||||
<Placeholder className="wc-blocks-product-collection__editor-product-picker">
|
||||
|
@ -38,21 +59,7 @@ const ProductPicker = ( props: ProductCollectionEditComponentProps ) => {
|
|||
icon={ info }
|
||||
className="wc-blocks-product-collection__info-icon"
|
||||
/>
|
||||
<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>
|
||||
<Text>{ infoText }</Text>
|
||||
</HStack>
|
||||
<ProductControl
|
||||
selected={
|
||||
|
|
|
@ -174,6 +174,10 @@ $max-button-width: calc(100% / #{$max-button-columns});
|
|||
.wc-blocks-product-collection__info-icon {
|
||||
fill: var(--wp--preset--color--luminous-vivid-orange, #e26f56);
|
||||
}
|
||||
|
||||
.woocommerce-search-list__search {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Linked Product Control
|
||||
|
|
|
@ -5,11 +5,13 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
|
|||
import { useState } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useGetLocation } from '@woocommerce/blocks/product-template/utils';
|
||||
import { Spinner, Flex } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
ProductCollectionContentProps,
|
||||
ProductCollectionEditComponentProps,
|
||||
ProductCollectionUIStatesInEditor,
|
||||
} from '../types';
|
||||
|
@ -17,7 +19,7 @@ import ProductCollectionPlaceholder from './product-collection-placeholder';
|
|||
import ProductCollectionContent from './product-collection-content';
|
||||
import CollectionSelectionModal from './collection-selection-modal';
|
||||
import './editor.scss';
|
||||
import { getProductCollectionUIStateInEditor } from '../utils';
|
||||
import { useProductCollectionUIState } from '../utils';
|
||||
import ProductPicker from './ProductPicker';
|
||||
|
||||
const Edit = ( props: ProductCollectionEditComponentProps ) => {
|
||||
|
@ -31,49 +33,65 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
|
|||
[ clientId ]
|
||||
);
|
||||
|
||||
const productCollectionUIStateInEditor =
|
||||
getProductCollectionUIStateInEditor( {
|
||||
hasInnerBlocks,
|
||||
const { productCollectionUIStateInEditor, isLoading } =
|
||||
useProductCollectionUIState( {
|
||||
location,
|
||||
attributes: props.attributes,
|
||||
attributes,
|
||||
hasInnerBlocks,
|
||||
usesReference: props.usesReference,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Component to render based on the UI state.
|
||||
*/
|
||||
let Component,
|
||||
isUsingReferencePreviewMode = false;
|
||||
// Show spinner while calculating Editor UI state.
|
||||
if ( isLoading ) {
|
||||
return (
|
||||
<Flex justify="center" align="center">
|
||||
<Spinner />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const productCollectionContentProps: ProductCollectionContentProps = {
|
||||
...props,
|
||||
openCollectionSelectionModal: () => setIsSelectionModalOpen( true ),
|
||||
location,
|
||||
isUsingReferencePreviewMode:
|
||||
productCollectionUIStateInEditor ===
|
||||
ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW,
|
||||
};
|
||||
|
||||
const renderComponent = () => {
|
||||
switch ( productCollectionUIStateInEditor ) {
|
||||
case ProductCollectionUIStatesInEditor.COLLECTION_PICKER:
|
||||
Component = ProductCollectionPlaceholder;
|
||||
break;
|
||||
return <ProductCollectionPlaceholder { ...props } />;
|
||||
case ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER:
|
||||
Component = ProductPicker;
|
||||
break;
|
||||
return (
|
||||
<ProductPicker
|
||||
{ ...props }
|
||||
isDeletedProductReference={ false }
|
||||
/>
|
||||
);
|
||||
case ProductCollectionUIStatesInEditor.DELETED_PRODUCT_REFERENCE:
|
||||
return (
|
||||
<ProductPicker
|
||||
{ ...props }
|
||||
isDeletedProductReference={ true }
|
||||
/>
|
||||
);
|
||||
case ProductCollectionUIStatesInEditor.VALID:
|
||||
Component = ProductCollectionContent;
|
||||
break;
|
||||
case ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW:
|
||||
Component = ProductCollectionContent;
|
||||
isUsingReferencePreviewMode = true;
|
||||
break;
|
||||
return (
|
||||
<ProductCollectionContent
|
||||
{ ...productCollectionContentProps }
|
||||
/>
|
||||
);
|
||||
default:
|
||||
// By default showing collection chooser.
|
||||
Component = ProductCollectionPlaceholder;
|
||||
return <ProductCollectionPlaceholder { ...props } />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Component
|
||||
{ ...props }
|
||||
openCollectionSelectionModal={ () =>
|
||||
setIsSelectionModalOpen( true )
|
||||
}
|
||||
isUsingReferencePreviewMode={ isUsingReferencePreviewMode }
|
||||
location={ location }
|
||||
usesReference={ props.usesReference }
|
||||
/>
|
||||
{ renderComponent() }
|
||||
{ isSelectionModalOpen && (
|
||||
<CollectionSelectionModal
|
||||
clientId={ clientId }
|
||||
|
|
|
@ -7,10 +7,10 @@ import { InspectorAdvancedControls } from '@wordpress/block-editor';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import ForcePageReloadControl from './force-page-reload-control';
|
||||
import type { ProductCollectionEditComponentProps } from '../../types';
|
||||
import type { ProductCollectionContentProps } from '../../types';
|
||||
|
||||
export default function ProductCollectionAdvancedInspectorControls(
|
||||
props: Omit< ProductCollectionEditComponentProps, 'preview' >
|
||||
props: ProductCollectionContentProps
|
||||
) {
|
||||
const { clientId, attributes, setAttributes } = props;
|
||||
const { forcePageReload } = attributes;
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
import metadata from '../../block.json';
|
||||
import { useTracksLocation } from '../../tracks-utils';
|
||||
import {
|
||||
ProductCollectionEditComponentProps,
|
||||
ProductCollectionContentProps,
|
||||
ProductCollectionAttributes,
|
||||
CoreFilterNames,
|
||||
FilterName,
|
||||
|
@ -58,7 +58,7 @@ const prepareShouldShowFilter =
|
|||
};
|
||||
|
||||
const ProductCollectionInspectorControls = (
|
||||
props: ProductCollectionEditComponentProps
|
||||
props: ProductCollectionContentProps
|
||||
) => {
|
||||
const { attributes, context, setAttributes } = props;
|
||||
const { query, hideControls, displayLayout } = attributes;
|
||||
|
|
|
@ -18,7 +18,7 @@ import fastDeepEqual from 'fast-deep-equal/es6';
|
|||
import type {
|
||||
ProductCollectionAttributes,
|
||||
ProductCollectionQuery,
|
||||
ProductCollectionEditComponentProps,
|
||||
ProductCollectionContentProps,
|
||||
} from '../types';
|
||||
import { DEFAULT_ATTRIBUTES, INNER_BLOCKS_TEMPLATE } from '../constants';
|
||||
import {
|
||||
|
@ -68,7 +68,7 @@ const useQueryId = (
|
|||
const ProductCollectionContent = ( {
|
||||
preview: { setPreviewState, initialPreviewState } = {},
|
||||
...props
|
||||
}: ProductCollectionEditComponentProps ) => {
|
||||
}: ProductCollectionContentProps ) => {
|
||||
const isInitialAttributesSet = useRef( false );
|
||||
const {
|
||||
clientId,
|
||||
|
|
|
@ -11,10 +11,10 @@ import { setQueryAttribute } from '../../utils';
|
|||
import DisplaySettingsToolbar from './display-settings-toolbar';
|
||||
import DisplayLayoutToolbar from './display-layout-toolbar';
|
||||
import CollectionChooserToolbar from './collection-chooser-toolbar';
|
||||
import type { ProductCollectionEditComponentProps } from '../../types';
|
||||
import type { ProductCollectionContentProps } from '../../types';
|
||||
|
||||
export default function ToolbarControls(
|
||||
props: Omit< ProductCollectionEditComponentProps, 'preview' >
|
||||
props: ProductCollectionContentProps
|
||||
) {
|
||||
const { attributes, openCollectionSelectionModal, setAttributes } = props;
|
||||
const { query, displayLayout } = attributes;
|
||||
|
|
|
@ -14,9 +14,9 @@ export enum ProductCollectionUIStatesInEditor {
|
|||
PRODUCT_REFERENCE_PICKER = 'product_context_picker',
|
||||
VALID_WITH_PREVIEW = 'uses_reference_preview_mode',
|
||||
VALID = 'valid',
|
||||
DELETED_PRODUCT_REFERENCE = 'deleted_product_reference',
|
||||
// Future states
|
||||
// INVALID = 'invalid',
|
||||
// DELETED_PRODUCT_REFERENCE = 'deleted_product_reference',
|
||||
}
|
||||
|
||||
export interface ProductCollectionAttributes {
|
||||
|
@ -110,7 +110,6 @@ export interface ProductCollectionQuery {
|
|||
|
||||
export type ProductCollectionEditComponentProps =
|
||||
BlockEditProps< ProductCollectionAttributes > & {
|
||||
openCollectionSelectionModal: () => void;
|
||||
preview?: {
|
||||
initialPreviewState?: PreviewState;
|
||||
setPreviewState?: SetPreviewState;
|
||||
|
@ -119,8 +118,13 @@ export type ProductCollectionEditComponentProps =
|
|||
context: {
|
||||
templateSlug: string;
|
||||
};
|
||||
isUsingReferencePreviewMode: boolean;
|
||||
};
|
||||
|
||||
export type ProductCollectionContentProps =
|
||||
ProductCollectionEditComponentProps & {
|
||||
location: WooCommerceBlockLocation;
|
||||
isUsingReferencePreviewMode: boolean;
|
||||
openCollectionSelectionModal: () => void;
|
||||
};
|
||||
|
||||
export type TProductCollectionOrder = 'asc' | 'desc';
|
||||
|
|
|
@ -3,10 +3,16 @@
|
|||
*/
|
||||
import { store as blockEditorStore } from '@wordpress/block-editor';
|
||||
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 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 type { ProductResponseItem } from '@woocommerce/types';
|
||||
import { getProduct } from '@woocommerce/editor-components/utils';
|
||||
|
@ -193,7 +199,7 @@ export const getUsesReferencePreviewMessage = (
|
|||
return '';
|
||||
};
|
||||
|
||||
export const getProductCollectionUIStateInEditor = ( {
|
||||
export const useProductCollectionUIState = ( {
|
||||
location,
|
||||
usesReference,
|
||||
attributes,
|
||||
|
@ -203,7 +209,31 @@ export const getProductCollectionUIStateInEditor = ( {
|
|||
usesReference?: string[] | undefined;
|
||||
attributes: ProductCollectionAttributes;
|
||||
hasInnerBlocks: boolean;
|
||||
} ): ProductCollectionUIStatesInEditor => {
|
||||
} ) => {
|
||||
// Fetch product to check if it's deleted.
|
||||
// `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 } =
|
||||
selectFunc( coreDataStore );
|
||||
const selectorArgs = [ 'postType', 'product', productId ];
|
||||
return {
|
||||
product: getEntityRecord( ...selectorArgs ),
|
||||
hasResolved: hasFinishedResolution(
|
||||
'getEntityRecord',
|
||||
selectorArgs
|
||||
),
|
||||
};
|
||||
},
|
||||
[ productId ]
|
||||
);
|
||||
|
||||
const productCollectionUIStateInEditor = useMemo( () => {
|
||||
const isInRequiredLocation = usesReference?.includes( location.type );
|
||||
const isCollectionSelected = !! attributes.collection;
|
||||
|
||||
|
@ -222,8 +252,23 @@ export const getProductCollectionUIStateInEditor = ( {
|
|||
return ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER;
|
||||
}
|
||||
|
||||
// Case 2: Deleted product reference
|
||||
if (
|
||||
isCollectionSelected &&
|
||||
isProductContextRequired &&
|
||||
! isInRequiredLocation &&
|
||||
isProductContextSelected
|
||||
) {
|
||||
const isProductDeleted =
|
||||
productId &&
|
||||
( product === undefined || product?.status === 'trash' );
|
||||
if ( isProductDeleted ) {
|
||||
return ProductCollectionUIStatesInEditor.DELETED_PRODUCT_REFERENCE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Case 2: Preview mode - based on `usesReference` value
|
||||
* Case 3: Preview mode - based on `usesReference` value
|
||||
*/
|
||||
if ( isInRequiredLocation ) {
|
||||
/**
|
||||
|
@ -249,13 +294,26 @@ export const getProductCollectionUIStateInEditor = ( {
|
|||
}
|
||||
|
||||
/**
|
||||
* Case 3: Collection chooser
|
||||
* 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 = ( {
|
||||
|
|
|
@ -10,6 +10,10 @@ import {
|
|||
import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import ErrorPlaceholder, {
|
||||
ErrorObject,
|
||||
} from '@woocommerce/editor-components/error-placeholder';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -132,14 +136,16 @@ export const Edit = ( {
|
|||
|
||||
useEffect( () => {
|
||||
const mode = getMode( currentTemplateId, templateType );
|
||||
const newProductGalleryClientId =
|
||||
attributes.productGalleryClientId || clientId;
|
||||
|
||||
setAttributes( {
|
||||
...attributes,
|
||||
mode,
|
||||
productGalleryClientId: clientId,
|
||||
productGalleryClientId: newProductGalleryClientId,
|
||||
} );
|
||||
// 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,
|
||||
attributes,
|
||||
|
@ -148,6 +154,18 @@ export const Edit = ( {
|
|||
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 (
|
||||
<div { ...blockProps }>
|
||||
<InspectorControls>
|
||||
|
|
|
@ -136,10 +136,10 @@ export const moveInnerBlocksToPosition = (
|
|||
): void => {
|
||||
const { getBlock, getBlockRootClientId, getBlockIndex } =
|
||||
select( 'core/block-editor' );
|
||||
const { moveBlockToPosition } = dispatch( 'core/block-editor' );
|
||||
const productGalleryBlock = getBlock( clientId );
|
||||
|
||||
if ( productGalleryBlock ) {
|
||||
if ( productGalleryBlock?.name === 'woocommerce/product-gallery' ) {
|
||||
const { moveBlockToPosition } = dispatch( 'core/block-editor' );
|
||||
const previousLayout = productGalleryBlock.innerBlocks.length
|
||||
? productGalleryBlock.innerBlocks[ 0 ].attributes.layout
|
||||
: null;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
export const SEARCH_BLOCK_NAME = 'core/search';
|
||||
export const SEARCH_VARIATION_NAME = 'woocommerce/product-search';
|
||||
|
||||
export enum PositionOptions {
|
||||
OUTSIDE = 'button-outside',
|
||||
INSIDE = 'button-inside',
|
||||
NO_BUTTON = 'no-button',
|
||||
BUTTON_ONLY = 'button-only',
|
||||
INPUT_AND_BUTTON = 'input-and-button',
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
import { store as blockEditorStore, Warning } from '@wordpress/block-editor';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
@ -9,6 +10,7 @@ import { Icon, search } from '@wordpress/icons';
|
|||
import { getSettingWithCoercion } from '@woocommerce/settings';
|
||||
import { isBoolean } from '@woocommerce/types';
|
||||
import { Button } from '@wordpress/components';
|
||||
import type { Block as BlockType } from '@wordpress/blocks';
|
||||
import {
|
||||
// @ts-ignore waiting for @types/wordpress__blocks update
|
||||
registerBlockVariation,
|
||||
|
@ -21,8 +23,10 @@ import {
|
|||
*/
|
||||
import './style.scss';
|
||||
import './editor.scss';
|
||||
import { withProductSearchControls } from './inspector-controls';
|
||||
import Block from './block';
|
||||
import Edit from './edit';
|
||||
import { SEARCH_BLOCK_NAME, SEARCH_VARIATION_NAME } from './constants';
|
||||
|
||||
const isBlockVariationAvailable = getSettingWithCoercion(
|
||||
'isBlockVariationAvailable',
|
||||
|
@ -71,6 +75,7 @@ const PRODUCT_SEARCH_ATTRIBUTES = {
|
|||
query: {
|
||||
post_type: 'product',
|
||||
},
|
||||
namespace: SEARCH_VARIATION_NAME,
|
||||
};
|
||||
|
||||
const DeprecatedBlockEdit = ( { clientId }: { clientId: string } ) => {
|
||||
|
@ -115,7 +120,7 @@ const DeprecatedBlockEdit = ( { clientId }: { clientId: string } ) => {
|
|||
);
|
||||
};
|
||||
|
||||
registerBlockType( 'woocommerce/product-search', {
|
||||
registerBlockType( SEARCH_VARIATION_NAME, {
|
||||
title: __( 'Product Search', 'woocommerce' ),
|
||||
apiVersion: 3,
|
||||
icon: {
|
||||
|
@ -146,7 +151,7 @@ registerBlockType( 'woocommerce/product-search', {
|
|||
isMatch: ( { idBase, instance } ) =>
|
||||
idBase === 'woocommerce_product_search' && !! instance?.raw,
|
||||
transform: ( { instance } ) =>
|
||||
createBlock( 'woocommerce/product-search', {
|
||||
createBlock( SEARCH_VARIATION_NAME, {
|
||||
label:
|
||||
instance.raw.title ||
|
||||
PRODUCT_SEARCH_ATTRIBUTES.label,
|
||||
|
@ -172,9 +177,31 @@ registerBlockType( 'woocommerce/product-search', {
|
|||
},
|
||||
} );
|
||||
|
||||
function registerProductSearchNamespace( props: BlockType, blockName: string ) {
|
||||
if ( blockName === 'core/search' ) {
|
||||
// Gracefully handle if settings.attributes is undefined.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore -- We need this because `attributes` is marked as `readonly`
|
||||
props.attributes = {
|
||||
...props.attributes,
|
||||
namespace: {
|
||||
type: 'string',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
addFilter(
|
||||
'blocks.registerBlockType',
|
||||
SEARCH_VARIATION_NAME,
|
||||
registerProductSearchNamespace
|
||||
);
|
||||
|
||||
if ( isBlockVariationAvailable ) {
|
||||
registerBlockVariation( 'core/search', {
|
||||
name: 'woocommerce/product-search',
|
||||
name: SEARCH_VARIATION_NAME,
|
||||
title: __( 'Product Search', 'woocommerce' ),
|
||||
icon: {
|
||||
src: (
|
||||
|
@ -199,4 +226,9 @@ if ( isBlockVariationAvailable ) {
|
|||
),
|
||||
attributes: PRODUCT_SEARCH_ATTRIBUTES,
|
||||
} );
|
||||
addFilter(
|
||||
'editor.BlockEdit',
|
||||
SEARCH_BLOCK_NAME,
|
||||
withProductSearchControls
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { type ElementType, useEffect, useState } from '@wordpress/element';
|
||||
import { EditorBlock } from '@woocommerce/types';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { InspectorControls } from '@wordpress/block-editor';
|
||||
import {
|
||||
PanelBody,
|
||||
RadioControl,
|
||||
ToggleControl,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - Ignoring because `__experimentalToggleGroupControl` is not yet in the type definitions.
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalToggleGroupControl as ToggleGroupControl,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - Ignoring because `__experimentalToggleGroupControlOption` is not yet in the type definitions.
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
getInputAndButtonOption,
|
||||
getSelectedRadioControlOption,
|
||||
isInputAndButtonOption,
|
||||
isWooSearchBlockVariation,
|
||||
} from './utils';
|
||||
import { ButtonPositionProps, ProductSearchBlockProps } from './types';
|
||||
import { PositionOptions } from './constants';
|
||||
|
||||
const ProductSearchControls = ( props: ProductSearchBlockProps ) => {
|
||||
const { attributes, setAttributes } = props;
|
||||
const { buttonPosition, buttonUseIcon, showLabel } = attributes;
|
||||
const [ initialPosition, setInitialPosition ] =
|
||||
useState< ButtonPositionProps >( buttonPosition );
|
||||
|
||||
useEffect( () => {
|
||||
if (
|
||||
isInputAndButtonOption( buttonPosition ) &&
|
||||
initialPosition !== buttonPosition
|
||||
) {
|
||||
setInitialPosition( buttonPosition );
|
||||
}
|
||||
}, [ buttonPosition ] );
|
||||
|
||||
return (
|
||||
<InspectorControls group="styles">
|
||||
<PanelBody title={ __( 'Styles', 'woocommerce' ) }>
|
||||
<RadioControl
|
||||
selected={ getSelectedRadioControlOption( buttonPosition ) }
|
||||
options={ [
|
||||
{
|
||||
label: __( 'Input and button', 'woocommerce' ),
|
||||
value: PositionOptions.INPUT_AND_BUTTON,
|
||||
},
|
||||
{
|
||||
label: __( 'Input only', 'woocommerce' ),
|
||||
value: PositionOptions.NO_BUTTON,
|
||||
},
|
||||
{
|
||||
label: __( 'Button only', 'woocommerce' ),
|
||||
value: PositionOptions.BUTTON_ONLY,
|
||||
},
|
||||
] }
|
||||
onChange={ (
|
||||
selected: Partial< ButtonPositionProps > &
|
||||
PositionOptions.INPUT_AND_BUTTON
|
||||
) => {
|
||||
if ( selected !== PositionOptions.INPUT_AND_BUTTON ) {
|
||||
setAttributes( {
|
||||
buttonPosition: selected,
|
||||
} );
|
||||
} else {
|
||||
const newButtonPosition =
|
||||
getInputAndButtonOption( initialPosition );
|
||||
setAttributes( {
|
||||
buttonPosition: newButtonPosition,
|
||||
} );
|
||||
}
|
||||
} }
|
||||
/>
|
||||
{ buttonPosition !== PositionOptions.NO_BUTTON && (
|
||||
<>
|
||||
{ buttonPosition !== PositionOptions.BUTTON_ONLY && (
|
||||
<ToggleGroupControl
|
||||
label={ __( 'BUTTON POSITION', 'woocommerce' ) }
|
||||
isBlock
|
||||
onChange={ ( value: ButtonPositionProps ) => {
|
||||
setAttributes( {
|
||||
buttonPosition: value,
|
||||
} );
|
||||
} }
|
||||
value={ getInputAndButtonOption(
|
||||
buttonPosition
|
||||
) }
|
||||
>
|
||||
<ToggleGroupControlOption
|
||||
value={ PositionOptions.INSIDE }
|
||||
label={ __( 'Inside', 'woocommerce' ) }
|
||||
/>
|
||||
<ToggleGroupControlOption
|
||||
value={ PositionOptions.OUTSIDE }
|
||||
label={ __( 'Outside', 'woocommerce' ) }
|
||||
/>
|
||||
</ToggleGroupControl>
|
||||
) }
|
||||
<ToggleGroupControl
|
||||
label={ __( 'BUTTON APPEARANCE', 'woocommerce' ) }
|
||||
isBlock
|
||||
onChange={ ( value: boolean ) => {
|
||||
setAttributes( {
|
||||
buttonUseIcon: value,
|
||||
} );
|
||||
} }
|
||||
value={ buttonUseIcon }
|
||||
>
|
||||
<ToggleGroupControlOption
|
||||
value={ false }
|
||||
label={ __( 'Text', 'woocommerce' ) }
|
||||
/>
|
||||
<ToggleGroupControlOption
|
||||
value={ true }
|
||||
label={ __( 'Icon', 'woocommerce' ) }
|
||||
/>
|
||||
</ToggleGroupControl>
|
||||
</>
|
||||
) }
|
||||
<ToggleControl
|
||||
label={ __( 'Show input label', 'woocommerce' ) }
|
||||
checked={ showLabel }
|
||||
onChange={ ( showInputLabel: boolean ) =>
|
||||
setAttributes( {
|
||||
showLabel: showInputLabel,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
);
|
||||
};
|
||||
|
||||
export const withProductSearchControls =
|
||||
< T extends EditorBlock< T > >( BlockEdit: ElementType ) =>
|
||||
( props: ProductSearchBlockProps ) => {
|
||||
return isWooSearchBlockVariation( props ) ? (
|
||||
<>
|
||||
<ProductSearchControls { ...props } />
|
||||
<BlockEdit { ...props } />
|
||||
</>
|
||||
) : (
|
||||
<BlockEdit { ...props } />
|
||||
);
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { EditorBlock } from '@woocommerce/types';
|
||||
|
||||
export type ButtonPositionProps =
|
||||
| 'button-outside'
|
||||
| 'button-inside'
|
||||
| 'no-button'
|
||||
| 'button-only';
|
||||
|
||||
export interface SearchBlockAttributes {
|
||||
buttonPosition: ButtonPositionProps;
|
||||
buttonText?: string;
|
||||
buttonUseIcon: boolean;
|
||||
isSearchFieldHidden: boolean;
|
||||
label?: string;
|
||||
namespace?: string;
|
||||
placeholder?: string;
|
||||
showLabel: boolean;
|
||||
}
|
||||
|
||||
export type ProductSearchBlockProps = EditorBlock< SearchBlockAttributes >;
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
PositionOptions,
|
||||
SEARCH_BLOCK_NAME,
|
||||
SEARCH_VARIATION_NAME,
|
||||
} from './constants';
|
||||
import { ButtonPositionProps, ProductSearchBlockProps } from './types';
|
||||
|
||||
/**
|
||||
* Identifies if a block is a Search block variation from our conventions
|
||||
*
|
||||
* We are extending Gutenberg's core Search block with our variations, and
|
||||
* also adding extra namespaced attributes. If those namespaced attributes
|
||||
* are present, we can be fairly sure it is our own registered variation.
|
||||
*
|
||||
* @param {ProductSearchBlockProps} block - A WooCommerce block.
|
||||
*/
|
||||
export function isWooSearchBlockVariation( block: ProductSearchBlockProps ) {
|
||||
return (
|
||||
block.name === SEARCH_BLOCK_NAME &&
|
||||
block.attributes?.namespace === SEARCH_VARIATION_NAME
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given button position is a valid option for input and button placement.
|
||||
*
|
||||
* The function verifies if the provided `buttonPosition` matches one of the predefined
|
||||
* values for placing a button either inside or outside an input field.
|
||||
*
|
||||
* @param {string} buttonPosition - The position of the button to check.
|
||||
*/
|
||||
export function isInputAndButtonOption( buttonPosition: string ): boolean {
|
||||
return (
|
||||
buttonPosition === 'button-outside' ||
|
||||
buttonPosition === 'button-inside'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the option for the selected button position
|
||||
*
|
||||
* Based on the provided `buttonPosition`, the function returns a predefined option
|
||||
* if the position is valid for input and button placement. If the position is not
|
||||
* one of the predefined options, it returns the original `buttonPosition`.
|
||||
*
|
||||
* @param {string} buttonPosition - The position of the button to evaluate.
|
||||
*/
|
||||
export function getSelectedRadioControlOption(
|
||||
buttonPosition: string
|
||||
): string {
|
||||
if ( isInputAndButtonOption( buttonPosition ) ) {
|
||||
return PositionOptions.INPUT_AND_BUTTON;
|
||||
}
|
||||
return buttonPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate option for input and button placement based on the given value
|
||||
*
|
||||
* This function checks if the provided `value` is a valid option for placing a button either
|
||||
* inside or outside an input field. If the `value` is valid, it is returned as is. If the `value`
|
||||
* is not valid, the function returns a default option.
|
||||
*
|
||||
* @param {ButtonPositionProps} value - The position of the button to evaluate.
|
||||
*/
|
||||
export function getInputAndButtonOption( value: ButtonPositionProps ) {
|
||||
if ( isInputAndButtonOption( value ) ) {
|
||||
return value;
|
||||
}
|
||||
// The default value is 'inside' for input and button.
|
||||
return PositionOptions.OUTSIDE;
|
||||
}
|
|
@ -17,4 +17,6 @@ export const ACTION_TYPES = {
|
|||
SET_REDIRECT_URL: 'SET_REDIRECT_URL',
|
||||
SET_SHOULD_CREATE_ACCOUNT: 'SET_SHOULD_CREATE_ACCOUNT',
|
||||
SET_USE_SHIPPING_AS_BILLING: 'SET_USE_SHIPPING_AS_BILLING',
|
||||
SET_EDITING_BILLING_ADDRESS: 'SET_EDITING_BILLING_ADDRESS',
|
||||
SET_EDITING_SHIPPING_ADDRESS: 'SET_EDITING_SHIPPING_ADDRESS',
|
||||
} as const;
|
||||
|
|
|
@ -118,6 +118,30 @@ export const __internalSetUseShippingAsBilling = (
|
|||
useShippingAsBilling,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Set whether the billing address is being edited
|
||||
*
|
||||
* @param isEditing True if the billing address is being edited, false otherwise
|
||||
*/
|
||||
export const setEditingBillingAddress = ( isEditing: boolean ) => {
|
||||
return {
|
||||
type: types.SET_EDITING_BILLING_ADDRESS,
|
||||
isEditing,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether the shipping address is being edited
|
||||
*
|
||||
* @param isEditing True if the shipping address is being edited, false otherwise
|
||||
*/
|
||||
export const setEditingShippingAddress = ( isEditing: boolean ) => {
|
||||
return {
|
||||
type: types.SET_EDITING_SHIPPING_ADDRESS,
|
||||
isEditing,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether an account should be created for the user while checking out
|
||||
*
|
||||
|
@ -182,6 +206,8 @@ export type CheckoutAction =
|
|||
| typeof __internalSetCustomerId
|
||||
| typeof __internalSetCustomerPassword
|
||||
| typeof __internalSetUseShippingAsBilling
|
||||
| typeof setEditingBillingAddress
|
||||
| typeof setEditingShippingAddress
|
||||
| typeof __internalSetShouldCreateAccount
|
||||
| typeof __internalSetOrderNotes
|
||||
| typeof setPrefersCollection
|
||||
|
|
|
@ -23,8 +23,28 @@ export type CheckoutState = {
|
|||
shouldCreateAccount: boolean; // Should a user account be created?
|
||||
status: STATUS; // Status of the checkout
|
||||
useShippingAsBilling: boolean; // Should the billing form be hidden and inherit the shipping address?
|
||||
editingBillingAddress: boolean; // Is the billing address being edited?
|
||||
editingShippingAddress: boolean; // Is the shipping address being edited?
|
||||
};
|
||||
|
||||
// Default editing state for CustomerAddress component comes from the current address and whether or not we're in the editor.
|
||||
const hasBillingAddress = !! (
|
||||
checkoutData.billing_address.address_1 &&
|
||||
( checkoutData.billing_address.first_name ||
|
||||
checkoutData.billing_address.last_name )
|
||||
);
|
||||
|
||||
const hasShippingAddress = !! (
|
||||
checkoutData.shipping_address.address_1 &&
|
||||
( checkoutData.shipping_address.first_name ||
|
||||
checkoutData.shipping_address.last_name )
|
||||
);
|
||||
|
||||
const billingMatchesShipping = isSameAddress(
|
||||
checkoutData.billing_address,
|
||||
checkoutData.shipping_address
|
||||
);
|
||||
|
||||
export const defaultState: CheckoutState = {
|
||||
additionalFields: checkoutData.additional_fields || {},
|
||||
calculatingCount: 0,
|
||||
|
@ -38,8 +58,7 @@ export const defaultState: CheckoutState = {
|
|||
redirectUrl: '',
|
||||
shouldCreateAccount: false,
|
||||
status: STATUS.IDLE,
|
||||
useShippingAsBilling: isSameAddress(
|
||||
checkoutData.billing_address,
|
||||
checkoutData.shipping_address
|
||||
),
|
||||
useShippingAsBilling: billingMatchesShipping,
|
||||
editingBillingAddress: ! hasBillingAddress,
|
||||
editingShippingAddress: ! hasShippingAddress,
|
||||
};
|
||||
|
|
|
@ -130,6 +130,20 @@ const reducer = ( state = defaultState, action: CheckoutAction ) => {
|
|||
}
|
||||
break;
|
||||
|
||||
case types.SET_EDITING_BILLING_ADDRESS:
|
||||
newState = {
|
||||
...state,
|
||||
editingBillingAddress: action.isEditing,
|
||||
};
|
||||
break;
|
||||
|
||||
case types.SET_EDITING_SHIPPING_ADDRESS:
|
||||
newState = {
|
||||
...state,
|
||||
editingShippingAddress: action.isEditing,
|
||||
};
|
||||
break;
|
||||
|
||||
case types.SET_SHOULD_CREATE_ACCOUNT:
|
||||
if (
|
||||
action.shouldCreateAccount !== undefined &&
|
||||
|
|
|
@ -36,6 +36,14 @@ export const getUseShippingAsBilling = ( state: CheckoutState ) => {
|
|||
return state.useShippingAsBilling;
|
||||
};
|
||||
|
||||
export const getEditingBillingAddress = ( state: CheckoutState ) => {
|
||||
return state.editingBillingAddress;
|
||||
};
|
||||
|
||||
export const getEditingShippingAddress = ( state: CheckoutState ) => {
|
||||
return state.editingShippingAddress;
|
||||
};
|
||||
|
||||
export const getExtensionData = ( state: CheckoutState ) => {
|
||||
return state.extensionData;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# Checkout Store (`wc/store/checkout`) <!-- omit in toc -->
|
||||
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
> 💡 What's the difference between the Cart Store and the Checkout Store?
|
||||
>
|
||||
> The **Cart Store (`wc/store/cart`)** manages and retrieves data about the shopping cart, including items, customer data, and interactions like coupons.
|
||||
|
@ -173,6 +175,36 @@ const store = select( CHECKOUT_STORE_KEY );
|
|||
const useShippingAsBilling = store.getUseShippingAsBilling();
|
||||
```
|
||||
|
||||
### getEditingBillingAddress
|
||||
|
||||
Returns true if the billing address is being edited.
|
||||
|
||||
#### _Returns_ <!-- omit in toc -->
|
||||
|
||||
- `boolean`: True if the billing address is being edited.
|
||||
|
||||
#### _Example_ <!-- omit in toc -->
|
||||
|
||||
```js
|
||||
const store = select( CHECKOUT_STORE_KEY );
|
||||
const editingBillingAddress = store.getEditingBillingAddress();
|
||||
```
|
||||
|
||||
### getEditingShippingAddress
|
||||
|
||||
Returns true if the shipping address is being edited.
|
||||
|
||||
#### _Returns_ <!-- omit in toc -->
|
||||
|
||||
- `boolean`: True if the shipping address is being edited.
|
||||
|
||||
#### _Example_ <!-- omit in toc -->
|
||||
|
||||
```js
|
||||
const store = select( CHECKOUT_STORE_KEY );
|
||||
const editingShippingAddress = store.getEditingShippingAddress();
|
||||
```
|
||||
|
||||
### hasError
|
||||
|
||||
Returns true if an error occurred, and false otherwise.
|
||||
|
@ -293,7 +325,6 @@ const store = select( CHECKOUT_STORE_KEY );
|
|||
const isCalculating = store.isCalculating();
|
||||
```
|
||||
|
||||
|
||||
### prefersCollection
|
||||
|
||||
Returns true if the customer prefers to collect their order, and false otherwise.
|
||||
|
@ -326,6 +357,36 @@ const store = dispatch( CHECKOUT_STORE_KEY );
|
|||
store.setPrefersCollection( true );
|
||||
```
|
||||
|
||||
### setEditingBillingAddress
|
||||
|
||||
Set the billing address to editing state or collapsed state. Note that if the address has invalid fields, it will not be set to collapsed state.
|
||||
|
||||
#### _Parameters_ <!-- omit in toc -->
|
||||
|
||||
- _isEditing_ `boolean`: True to set the billing address to editing state, false to set it to collapsed state.
|
||||
|
||||
#### _Example_ <!-- omit in toc -->
|
||||
|
||||
```js
|
||||
const store = dispatch( CHECKOUT_STORE_KEY );
|
||||
store.setEditingBillingAddress( true );
|
||||
```
|
||||
|
||||
### setEditingShippingAddress
|
||||
|
||||
Set the shipping address to editing state or collapsed state. Note that if the address has invalid fields, it will not be set to collapsed state.
|
||||
|
||||
#### _Parameters_ <!-- omit in toc -->
|
||||
|
||||
- _isEditing_ `boolean`: True to set the shipping address to editing state, false to set it to collapsed state.
|
||||
|
||||
#### _Example_ <!-- omit in toc -->
|
||||
|
||||
```js
|
||||
const store = dispatch( CHECKOUT_STORE_KEY );
|
||||
store.setEditingShippingAddress( true );
|
||||
```
|
||||
|
||||
<!-- FEEDBACK -->
|
||||
|
||||
---
|
||||
|
|
|
@ -256,7 +256,7 @@
|
|||
"pnpm": "9.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.4.4",
|
||||
"@ariakit/react": "^0.4.5",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/modifiers": "^6.0.1",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
|
@ -288,7 +288,7 @@
|
|||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-sort": "^3.4.0",
|
||||
"html-react-parser": "3.0.4",
|
||||
"postcode-validator": "3.8.15",
|
||||
"postcode-validator": "3.9.2",
|
||||
"preact": "^10.19.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-number-format": "4.9.3",
|
||||
|
|
|
@ -13,6 +13,7 @@ const CUSTOM_REGEXES = new Map< string, RegExp >( [
|
|||
[ 'JP', /^([0-9]{3})([-]?)([0-9]{4})$/ ],
|
||||
[ 'KH', /^[0-9]{6}$/ ], // Cambodia (6-digit postal code).
|
||||
[ 'LI', /^(94[8-9][0-9])$/ ],
|
||||
[ 'MN', /^[0-9]{5}(-[0-9]{4})?$/ ], // Mongolia (5-digit postal code or 5-digit postal code followed by a hyphen and 4-digit postal code).
|
||||
[ 'NI', /^[1-9]{1}[0-9]{4}$/ ], // Nicaragua (5-digit postal code)
|
||||
[ 'NL', /^([1-9][0-9]{3})(\s?)(?!SA|SD|SS)[A-Z]{2}$/i ],
|
||||
[ 'SI', /^([1-9][0-9]{3})$/ ],
|
||||
|
|
|
@ -5,7 +5,8 @@ import { useState } from '@wordpress/element';
|
|||
import clsx from 'clsx';
|
||||
import { Icon, chevronUp, chevronDown } from '@wordpress/icons';
|
||||
import type { ReactNode, ReactElement } from 'react';
|
||||
|
||||
import { Button } from '@ariakit/react';
|
||||
import deprecated from '@wordpress/deprecated';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
@ -27,7 +28,11 @@ const Panel = ( {
|
|||
initialOpen = false,
|
||||
hasBorder = false,
|
||||
title,
|
||||
titleTag: TitleTag = 'div',
|
||||
/**
|
||||
* @deprecated The `titleTag` prop is deprecated and will be removed in a future version.
|
||||
* Use the `title` prop to pass a custom React element instead.
|
||||
*/
|
||||
titleTag,
|
||||
state,
|
||||
}: PanelProps ): ReactElement => {
|
||||
let [ isOpen, setIsOpen ] = useState< boolean >( initialOpen );
|
||||
|
@ -36,14 +41,20 @@ const Panel = ( {
|
|||
[ isOpen, setIsOpen ] = state;
|
||||
}
|
||||
|
||||
if ( titleTag ) {
|
||||
deprecated( "Panel component's titleTag prop", {
|
||||
since: '9.4.0',
|
||||
} );
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ clsx( className, 'wc-block-components-panel', {
|
||||
'has-border': hasBorder,
|
||||
} ) }
|
||||
>
|
||||
<TitleTag>
|
||||
<button
|
||||
<Button
|
||||
render={ <div /> }
|
||||
aria-expanded={ isOpen }
|
||||
className="wc-block-components-panel__button"
|
||||
onClick={ () => setIsOpen( ! isOpen ) }
|
||||
|
@ -54,8 +65,7 @@ const Panel = ( {
|
|||
icon={ isOpen ? chevronUp : chevronDown }
|
||||
/>
|
||||
{ title }
|
||||
</button>
|
||||
</TitleTag>
|
||||
</Button>
|
||||
{ isOpen && (
|
||||
<div className="wc-block-components-panel__content">
|
||||
{ children }
|
||||
|
|
|
@ -14,31 +14,27 @@
|
|||
}
|
||||
|
||||
.wc-block-components-panel__button {
|
||||
@include reset-box();
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
line-height: 1;
|
||||
margin-top: em(6px);
|
||||
padding-right: #{24px + $gap-smaller};
|
||||
padding-top: em($gap-small - 6px);
|
||||
padding-left: 0 !important;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
word-break: break-word;
|
||||
|
||||
&[aria-expanded="true"] {
|
||||
padding-bottom: $gap-smaller;
|
||||
margin-bottom: $gap-smaller;
|
||||
margin-bottom: $gap-smaller * 2;
|
||||
}
|
||||
|
||||
&,
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
@include reset-color();
|
||||
@include reset-typography();
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
> .wc-block-components-panel__button-icon {
|
||||
|
@ -58,21 +54,3 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Extra classes for specificity.
|
||||
.theme-twentytwentyone.theme-twentytwentyone.theme-twentytwentyone
|
||||
.wc-block-components-panel__button {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.theme-twentytwenty .wc-block-components-panel__button,
|
||||
.theme-twentyseventeen .wc-block-components-panel__button {
|
||||
background: none transparent;
|
||||
color: inherit;
|
||||
|
||||
&.wc-block-components-panel__button:hover,
|
||||
&.wc-block-components-panel__button:focus {
|
||||
background: none transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -207,7 +207,8 @@ class ProductCollectionPage {
|
|||
}
|
||||
|
||||
async chooseProductInEditorProductPickerIfAvailable(
|
||||
pageReference: Page | FrameLocator
|
||||
pageReference: Page | FrameLocator,
|
||||
productName = 'Album'
|
||||
) {
|
||||
const editorProductPicker = pageReference.locator(
|
||||
SELECTORS.productPicker
|
||||
|
@ -217,7 +218,7 @@ class ProductCollectionPage {
|
|||
await editorProductPicker
|
||||
.locator( 'label' )
|
||||
.filter( {
|
||||
hasText: 'Album',
|
||||
hasText: productName,
|
||||
} )
|
||||
.click();
|
||||
}
|
||||
|
|
|
@ -356,4 +356,84 @@ test.describe( 'Product Collection registration', () => {
|
|||
await expect( previewButtonLocator ).toBeHidden();
|
||||
} );
|
||||
} );
|
||||
|
||||
test( 'Product picker should be shown when selected product is deleted', async ( {
|
||||
pageObject,
|
||||
admin,
|
||||
editor,
|
||||
requestUtils,
|
||||
page,
|
||||
} ) => {
|
||||
// Add a new test product to the database
|
||||
let testProductId: number | null = null;
|
||||
const newProduct = await requestUtils.rest( {
|
||||
method: 'POST',
|
||||
path: 'wc/v3/products',
|
||||
data: {
|
||||
name: 'A Test Product',
|
||||
price: 10,
|
||||
},
|
||||
} );
|
||||
testProductId = newProduct.id;
|
||||
|
||||
await admin.createNewPost();
|
||||
await pageObject.insertProductCollection();
|
||||
await pageObject.chooseCollectionInPost(
|
||||
'myCustomCollectionWithProductContext'
|
||||
);
|
||||
|
||||
// Verify that product picker is shown in Editor
|
||||
const editorProductPicker = editor.canvas.locator(
|
||||
SELECTORS.productPicker
|
||||
);
|
||||
await expect( editorProductPicker ).toBeVisible();
|
||||
|
||||
// Once a product is selected, the product picker should be hidden
|
||||
await pageObject.chooseProductInEditorProductPickerIfAvailable(
|
||||
editor.canvas,
|
||||
'A Test Product'
|
||||
);
|
||||
await expect( editorProductPicker ).toBeHidden();
|
||||
|
||||
await editor.saveDraft();
|
||||
|
||||
// Delete the product
|
||||
await requestUtils.rest( {
|
||||
method: 'DELETE',
|
||||
path: `wc/v3/products/${ testProductId }`,
|
||||
} );
|
||||
|
||||
// Product picker should be shown in Editor
|
||||
await admin.page.reload();
|
||||
const deletedProductPicker = editor.canvas.getByText(
|
||||
'Previously selected product'
|
||||
);
|
||||
await expect( deletedProductPicker ).toBeVisible();
|
||||
|
||||
// Change status from "trash" to "publish"
|
||||
await requestUtils.rest( {
|
||||
method: 'PUT',
|
||||
path: `wc/v3/products/${ testProductId }`,
|
||||
data: {
|
||||
status: 'publish',
|
||||
},
|
||||
} );
|
||||
|
||||
// Product Picker shouldn't be shown as product is available now
|
||||
await page.reload();
|
||||
await expect( editorProductPicker ).toBeHidden();
|
||||
|
||||
// Delete the product from database, instead of trashing it
|
||||
await requestUtils.rest( {
|
||||
method: 'DELETE',
|
||||
path: `wc/v3/products/${ testProductId }`,
|
||||
params: {
|
||||
// Bypass trash and permanently delete the product
|
||||
force: true,
|
||||
},
|
||||
} );
|
||||
|
||||
// Product picker should be shown in Editor
|
||||
await expect( deletedProductPicker ).toBeVisible();
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Adjust Mongolia postcode validation to be 5 digits or 5 digits followed by 4 digits.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Product Collection: Added Editor UI for missing product reference
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Fix PHPCS warnings in OrdersTableQuery.php and ProductQuery.php
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
Comment: Fixed call to a member function is_visible() on string | content-product.php:23
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Expand the e2e suite we're running on WPCOM.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: enhancement
|
||||
|
||||
Harden styles for interactive elements in Checkout block to prevent style leakage.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
Comment: Changed Product attributes placeholder to e.g. length or weight
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: enhancement
|
||||
|
||||
Move address card state management to data stores in Checkout block.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add `locale` param when redirecting to the Jetpack auth page.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Expand the e2e suite we're running on WPCOM part #2.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add use-wp-horizon feature flag to set calpyso_env to horizon
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Fix bug where manually triggering `added_to_cart` event without a button element caused an Exception.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add inspector controls to Product Search block #51247
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Added missing wp-block- classes to order confirmation, store notices, and breadcrumb blocks.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
|
||||
Adds a filter for third party tax plugins to indicate that they have completed the tax task
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
Comment: Updating Markdown linter rule
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: enhancement
|
||||
|
||||
Improve remote logging structure and content
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: enhancement
|
||||
|
||||
Reducing noise in remote logging
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: enhancement
|
||||
|
||||
Reduce dependency of remote logging on WC_Tracks
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Improve performance of tax report export generation and fix tax_code for removed rates.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Fix error when adding the Product Gallery (Beta) block into a pattern
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
wc_get_cart_url should only return current URL if on the cart page. This excludes the usage of WOOCOMMERCE_CART.
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
Comment: Fix a type mismatch in UpdateProducts.php
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Set customer email in reports if customer data is available
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: enhancement
|
||||
|
||||
Introduce error handling on the in-app my subscriptions page
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Introduced Product Brands.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: enhancement
|
||||
|
||||
Refine PHP Fatal Error Counting in MC Stat
|
|
@ -39,6 +39,7 @@
|
|||
"launch-your-store": true,
|
||||
"product-editor-template-system": false,
|
||||
"blueprint": false,
|
||||
"reactify-classic-payments-settings": false
|
||||
"reactify-classic-payments-settings": false,
|
||||
"use-wp-horizon": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
"launch-your-store": true,
|
||||
"product-editor-template-system": false,
|
||||
"blueprint": true,
|
||||
"reactify-classic-payments-settings": false
|
||||
"reactify-classic-payments-settings": false,
|
||||
"use-wp-horizon": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
table.wp-list-table .column-taxonomy-product_brand {
|
||||
width: 10%;
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
/* Brand description on archives */
|
||||
.tax-product_brand .brand-description {
|
||||
overflow: hidden;
|
||||
zoom: 1;
|
||||
}
|
||||
.tax-product_brand .brand-description img.brand-thumbnail {
|
||||
width: 25%;
|
||||
float: right;
|
||||
}
|
||||
.tax-product_brand .brand-description .text {
|
||||
width: 72%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* Brand description widget */
|
||||
.widget_brand_description img {
|
||||
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
|
||||
-moz-box-sizing: border-box; /* Firefox, other Gecko */
|
||||
box-sizing: border-box; /* Opera/IE 8+ */
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
height: auto;
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
/* Brand thumbnails widget */
|
||||
ul.brand-thumbnails {
|
||||
margin-left: 0;
|
||||
margin-bottom: 0;
|
||||
clear: both;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:before {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
|
||||
ul.brand-thumbnails li {
|
||||
float: left;
|
||||
margin: 0 3.8% 1em 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 22.05%; /* 4 columns */
|
||||
}
|
||||
|
||||
ul.brand-thumbnails.fluid-columns li {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:not(.fluid-columns) li.first {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:not(.fluid-columns) li.last {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
ul.brand-thumbnails.columns-1 li {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
ul.brand-thumbnails.columns-2 li {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
ul.brand-thumbnails.columns-3 li {
|
||||
width: 30.75%;
|
||||
}
|
||||
|
||||
ul.brand-thumbnails.columns-5 li {
|
||||
width: 16.95%;
|
||||
}
|
||||
|
||||
ul.brand-thumbnails.columns-6 li {
|
||||
width: 13.5%;
|
||||
}
|
||||
|
||||
.brand-thumbnails li img {
|
||||
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
|
||||
-moz-box-sizing: border-box; /* Firefox, other Gecko */
|
||||
box-sizing: border-box; /* Opera/IE 8+ */
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
ul.brand-thumbnails:not(.fluid-columns) li {
|
||||
width: 48% !important;
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:not(.fluid-columns) li.first {
|
||||
clear: none;
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:not(.fluid-columns) li.last {
|
||||
margin-right: 3.8%
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:not(.fluid-columns) li:nth-of-type(odd) {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
ul.brand-thumbnails:not(.fluid-columns) li:nth-of-type(even) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Brand thumbnails description */
|
||||
.brand-thumbnails-description li {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.brand-thumbnails-description li .term-thumbnail img {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.brand-thumbnails-description li .term-description {
|
||||
margin-top: 1em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* A-Z Shortcode */
|
||||
#brands_a_z h3:target {
|
||||
text-decoration: underline;
|
||||
}
|
||||
ul.brands_index {
|
||||
list-style: none outside;
|
||||
overflow: hidden;
|
||||
zoom: 1;
|
||||
}
|
||||
ul.brands_index li {
|
||||
float: left;
|
||||
margin: 0 2px 2px 0;
|
||||
}
|
||||
ul.brands_index li a, ul.brands_index li span {
|
||||
border: 1px solid #ccc;
|
||||
padding: 6px;
|
||||
line-height: 1em;
|
||||
float: left;
|
||||
text-decoration: none;
|
||||
}
|
||||
ul.brands_index li span {
|
||||
border-color: #eee;
|
||||
color: #ddd;
|
||||
}
|
||||
ul.brands_index li a:hover {
|
||||
border-width: 2px;
|
||||
padding: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
ul.brands_index li a.active {
|
||||
border-width: 2px;
|
||||
padding: 5px;
|
||||
}
|
||||
div#brands_a_z a.top {
|
||||
border: 1px solid #ccc;
|
||||
padding: 4px;
|
||||
line-height: 1em;
|
||||
float: right;
|
||||
text-decoration: none;
|
||||
font-size: 0.8em;
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/* global wc_enhanced_select_params */
|
||||
/* global wpApiSettings */
|
||||
jQuery( function( $ ) {
|
||||
|
||||
function getEnhancedSelectFormatString() {
|
||||
return {
|
||||
'language': {
|
||||
errorLoading: function() {
|
||||
// Workaround for https://github.com/select2/select2/issues/4355 instead of i18n_ajax_error.
|
||||
return wc_enhanced_select_params.i18n_searching;
|
||||
},
|
||||
inputTooLong: function( args ) {
|
||||
var overChars = args.input.length - args.maximum;
|
||||
|
||||
if ( 1 === overChars ) {
|
||||
return wc_enhanced_select_params.i18n_input_too_long_1;
|
||||
}
|
||||
|
||||
return wc_enhanced_select_params.i18n_input_too_long_n.replace( '%qty%', overChars );
|
||||
},
|
||||
inputTooShort: function( args ) {
|
||||
var remainingChars = args.minimum - args.input.length;
|
||||
|
||||
if ( 1 === remainingChars ) {
|
||||
return wc_enhanced_select_params.i18n_input_too_short_1;
|
||||
}
|
||||
|
||||
return wc_enhanced_select_params.i18n_input_too_short_n.replace( '%qty%', remainingChars );
|
||||
},
|
||||
loadingMore: function() {
|
||||
return wc_enhanced_select_params.i18n_load_more;
|
||||
},
|
||||
maximumSelected: function( args ) {
|
||||
if ( args.maximum === 1 ) {
|
||||
return wc_enhanced_select_params.i18n_selection_too_long_1;
|
||||
}
|
||||
|
||||
return wc_enhanced_select_params.i18n_selection_too_long_n.replace( '%qty%', args.maximum );
|
||||
},
|
||||
noResults: function() {
|
||||
return wc_enhanced_select_params.i18n_no_matches;
|
||||
},
|
||||
searching: function() {
|
||||
return wc_enhanced_select_params.i18n_searching;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
$( document.body )
|
||||
.on( 'wc-enhanced-select-init', function() {
|
||||
// Ajax category search boxes
|
||||
$( ':input.wc-brands-search' ).filter( ':not(.enhanced)' ).each( function() {
|
||||
var select2_args = $.extend( {
|
||||
allowClear : $( this ).data( 'allow_clear' ) ? true : false,
|
||||
placeholder : $( this ).data( 'placeholder' ),
|
||||
minimumInputLength: $( this ).data( 'minimum_input_length' ) ? $( this ).data( 'minimum_input_length' ) : 3,
|
||||
escapeMarkup : function( m ) {
|
||||
return m;
|
||||
},
|
||||
ajax: {
|
||||
url: wpApiSettings.root + 'wc/v3/products/brands',
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
headers: {
|
||||
'X-WP-Nonce': wpApiSettings.nonce
|
||||
},
|
||||
data: function( params ) {
|
||||
return {
|
||||
hide_empty: 1,
|
||||
search: params.term
|
||||
};
|
||||
},
|
||||
processResults: function( data ) {
|
||||
const results = data
|
||||
.map( term => ({ id: term.slug, text: term.name + ' (' + term.count + ')' }) )
|
||||
return {
|
||||
results
|
||||
};
|
||||
},
|
||||
cache: true
|
||||
}
|
||||
}, getEnhancedSelectFormatString() );
|
||||
|
||||
$( this ).selectWoo( select2_args ).addClass( 'enhanced' );
|
||||
});
|
||||
})
|
||||
.trigger( 'wc-enhanced-select-init' );
|
||||
} catch( err ) {
|
||||
// If select2 failed (conflict?) log the error but don't stop other scripts breaking.
|
||||
window.console.log( err );
|
||||
}
|
||||
});
|
|
@ -173,6 +173,8 @@ jQuery( function( $ ) {
|
|||
* Update cart page elements after add to cart events.
|
||||
*/
|
||||
AddToCartHandler.prototype.updateButton = function( e, fragments, cart_hash, $button ) {
|
||||
// Some themes and plugins manually trigger added_to_cart without passing a button element, which in turn calls this function.
|
||||
// If there is no button we don't want to crash.
|
||||
$button = typeof $button === 'undefined' ? false : $button;
|
||||
|
||||
if ( $button ) {
|
||||
|
@ -222,6 +224,11 @@ jQuery( function( $ ) {
|
|||
* Update cart live region message after add/remove cart events.
|
||||
*/
|
||||
AddToCartHandler.prototype.alertCartUpdated = function( e, fragments, cart_hash, $button ) {
|
||||
// Some themes and plugins manually trigger added_to_cart without passing a button element, which in turn calls this function.
|
||||
// If there is no button we don't want to crash.
|
||||
$button = typeof $button === 'undefined' ? false : $button;
|
||||
|
||||
if ( $button ) {
|
||||
var message = $button.data( 'success_message' );
|
||||
|
||||
if ( !message ) {
|
||||
|
@ -235,6 +242,7 @@ jQuery( function( $ ) {
|
|||
.delay(1000)
|
||||
.text( message )
|
||||
.attr( 'aria-relevant', 'all' );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,792 @@
|
|||
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName.
|
||||
/**
|
||||
* Brands Admin Page
|
||||
*
|
||||
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
|
||||
*
|
||||
* @package WooCommerce\Admin
|
||||
* @version 9.4.0
|
||||
*/
|
||||
|
||||
declare( strict_types = 1);
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
|
||||
/**
|
||||
* WC_Brands_Admin class.
|
||||
*/
|
||||
class WC_Brands_Admin {
|
||||
|
||||
/**
|
||||
* Settings array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $settings_tabs;
|
||||
|
||||
/**
|
||||
* Admin fields.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $fields = array();
|
||||
|
||||
/**
|
||||
* __construct function.
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'scripts' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'styles' ) );
|
||||
add_action( 'product_brand_add_form_fields', array( $this, 'add_thumbnail_field' ) );
|
||||
add_action( 'product_brand_edit_form_fields', array( $this, 'edit_thumbnail_field' ), 10, 1 );
|
||||
add_action( 'created_term', array( $this, 'thumbnail_field_save' ), 10, 1 );
|
||||
add_action( 'edit_term', array( $this, 'thumbnail_field_save' ), 10, 1 );
|
||||
add_action( 'product_brand_pre_add_form', array( $this, 'taxonomy_description' ) );
|
||||
add_filter( 'woocommerce_sortable_taxonomies', array( $this, 'sort_brands' ) );
|
||||
add_filter( 'manage_edit-product_brand_columns', array( $this, 'columns' ) );
|
||||
add_filter( 'manage_product_brand_custom_column', array( $this, 'column' ), 10, 3 );
|
||||
add_filter( 'manage_product_posts_columns', array( $this, 'product_columns' ), 20, 1 );
|
||||
add_filter(
|
||||
'woocommerce_products_admin_list_table_filters',
|
||||
function ( $args ) {
|
||||
$args['product_brand'] = array( $this, 'render_product_brand_filter' );
|
||||
return $args;
|
||||
}
|
||||
);
|
||||
|
||||
$this->settings_tabs = array(
|
||||
'brands' => __( 'Brands', 'woocommerce' ),
|
||||
);
|
||||
|
||||
// Hiding setting for future depreciation. Only users who have touched this settings should see it.
|
||||
$setting_value = get_option( 'wc_brands_show_description' );
|
||||
if ( is_string( $setting_value ) ) {
|
||||
// Add the settings fields to each tab.
|
||||
$this->init_form_fields();
|
||||
add_action( 'woocommerce_get_sections_products', array( $this, 'add_settings_tab' ) );
|
||||
add_action( 'woocommerce_get_settings_products', array( $this, 'add_settings_section' ), null, 2 );
|
||||
}
|
||||
|
||||
add_action( 'woocommerce_update_options_catalog', array( $this, 'save_admin_settings' ) );
|
||||
|
||||
/* 2.1 */
|
||||
add_action( 'woocommerce_update_options_products', array( $this, 'save_admin_settings' ) );
|
||||
|
||||
// Add brands filtering to the coupon creation screens.
|
||||
add_action( 'woocommerce_coupon_options_usage_restriction', array( $this, 'add_coupon_brands_fields' ) );
|
||||
add_action( 'woocommerce_coupon_options_save', array( $this, 'save_coupon_brands' ) );
|
||||
|
||||
// Permalinks.
|
||||
add_filter( 'pre_update_option_woocommerce_permalinks', array( $this, 'validate_product_base' ) );
|
||||
|
||||
add_action( 'current_screen', array( $this, 'add_brand_base_setting' ) );
|
||||
|
||||
// CSV Import/Export Support.
|
||||
// https://github.com/woocommerce/woocommerce/wiki/Product-CSV-Importer-&-Exporter
|
||||
// Import.
|
||||
add_filter( 'woocommerce_csv_product_import_mapping_options', array( $this, 'add_column_to_importer_exporter' ), 10 );
|
||||
add_filter( 'woocommerce_csv_product_import_mapping_default_columns', array( $this, 'add_default_column_mapping' ), 10 );
|
||||
add_filter( 'woocommerce_product_import_inserted_product_object', array( $this, 'process_import' ), 10, 2 );
|
||||
|
||||
// Export.
|
||||
add_filter( 'woocommerce_product_export_column_names', array( $this, 'add_column_to_importer_exporter' ), 10 );
|
||||
add_filter( 'woocommerce_product_export_product_default_columns', array( $this, 'add_column_to_importer_exporter' ), 10 );
|
||||
add_filter( 'woocommerce_product_export_product_column_brand_ids', array( $this, 'get_column_value_brand_ids' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the settings for the new "Brands" subtab.
|
||||
*
|
||||
* @since 9.4.0
|
||||
*
|
||||
* @param array $settings Settings.
|
||||
* @param array $current_section Current section.
|
||||
*/
|
||||
public function add_settings_section( $settings, $current_section ) {
|
||||
if ( 'brands' === $current_section ) {
|
||||
$settings = $this->settings;
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new "Brands" subtab to the "Products" tab.
|
||||
*
|
||||
* @since 9.4.0
|
||||
* @param array $sections Sections.
|
||||
*/
|
||||
public function add_settings_tab( $sections ) {
|
||||
$sections = array_merge( $sections, $this->settings_tabs );
|
||||
return $sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display coupon filter fields relating to brands.
|
||||
*
|
||||
* @since 9.4.0
|
||||
* @return void
|
||||
*/
|
||||
public function add_coupon_brands_fields() {
|
||||
global $post;
|
||||
// Brands.
|
||||
?>
|
||||
<p class="form-field"><label for="product_brands"><?php esc_html_e( 'Product brands', 'woocommerce' ); ?></label>
|
||||
<select id="product_brands" name="product_brands[]" style="width: 50%;" class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'Any brand', 'woocommerce' ); ?>">
|
||||
<?php
|
||||
$category_ids = (array) get_post_meta( $post->ID, 'product_brands', true );
|
||||
$categories = get_terms(
|
||||
array(
|
||||
'taxonomy' => 'product_brand',
|
||||
'orderby' => 'name',
|
||||
'hide_empty' => false,
|
||||
)
|
||||
);
|
||||
|
||||
if ( $categories ) {
|
||||
foreach ( $categories as $cat ) {
|
||||
echo '<option value="' . esc_attr( $cat->term_id ) . '"' . selected( in_array( $cat->term_id, $category_ids, true ), true, false ) . '>' . esc_html( $cat->name ) . '</option>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<?php
|
||||
echo wc_help_tip( esc_html__( 'A product must be associated with this brand for the coupon to remain valid or, for "Product Discounts", products with these brands will be discounted.', 'woocommerce' ) );
|
||||
// Exclude Brands.
|
||||
?>
|
||||
<p class="form-field"><label for="exclude_product_brands"><?php esc_html_e( 'Exclude brands', 'woocommerce' ); ?></label>
|
||||
<select id="exclude_product_brands" name="exclude_product_brands[]" style="width: 50%;" class="wc-enhanced-select" multiple="multiple" data-placeholder="<?php esc_attr_e( 'No brands', 'woocommerce' ); ?>">
|
||||
<?php
|
||||
$category_ids = (array) get_post_meta( $post->ID, 'exclude_product_brands', true );
|
||||
$categories = get_terms(
|
||||
array(
|
||||
'taxonomy' => 'product_brand',
|
||||
'orderby' => 'name',
|
||||
'hide_empty' => false,
|
||||
)
|
||||
);
|
||||
|
||||
if ( $categories ) {
|
||||
foreach ( $categories as $cat ) {
|
||||
echo '<option value="' . esc_attr( $cat->term_id ) . '"' . selected( in_array( $cat->term_id, $category_ids, true ), true, false ) . '>' . esc_html( $cat->name ) . '</option>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<?php
|
||||
echo wc_help_tip( esc_html__( 'Product must not be associated with these brands for the coupon to remain valid or, for "Product Discounts", products associated with these brands will not be discounted.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save coupon filter fields relating to brands.
|
||||
*
|
||||
* @since 9.4.0
|
||||
* @param int $post_id Post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function save_coupon_brands( $post_id ) {
|
||||
$product_brands = isset( $_POST['product_brands'] ) ? array_map( 'intval', $_POST['product_brands'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
$exclude_product_brands = isset( $_POST['exclude_product_brands'] ) ? array_map( 'intval', $_POST['exclude_product_brands'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
|
||||
// Save.
|
||||
update_post_meta( $post_id, 'product_brands', $product_brands );
|
||||
update_post_meta( $post_id, 'exclude_product_brands', $exclude_product_brands );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare form fields to be used in the various tabs.
|
||||
*/
|
||||
public function init_form_fields() {
|
||||
|
||||
/**
|
||||
* Filter Brands settings.
|
||||
*
|
||||
* @since 9.4.0
|
||||
*
|
||||
* @param array $settings Brands settings.
|
||||
*/
|
||||
$this->settings = apply_filters(
|
||||
'woocommerce_brands_settings_fields',
|
||||
array(
|
||||
array(
|
||||
'name' => __( 'Brands Archives', 'woocommerce' ),
|
||||
'type' => 'title',
|
||||
'desc' => '',
|
||||
'id' => 'brands_archives',
|
||||
),
|
||||
array(
|
||||
'name' => __( 'Show description', 'woocommerce' ),
|
||||
'desc' => __( 'Choose to show the brand description on the archive page. Turn this off if you intend to use the description widget instead. Please note: this is only for themes that do not show the description.', 'woocommerce' ),
|
||||
'tip' => '',
|
||||
'id' => 'wc_brands_show_description',
|
||||
'css' => '',
|
||||
'std' => 'yes',
|
||||
'type' => 'checkbox',
|
||||
),
|
||||
array(
|
||||
'type' => 'sectionend',
|
||||
'id' => 'brands_archives',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function scripts() {
|
||||
$screen = get_current_screen();
|
||||
$version = Constants::get_constant( 'WC_VERSION' );
|
||||
$suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
|
||||
|
||||
if ( 'edit-product' === $screen->id ) {
|
||||
wp_register_script(
|
||||
'wc-brands-enhanced-select',
|
||||
WC()->plugin_url() . '/assets/js/admin/wc-brands-enhanced-select' . $suffix . '.js',
|
||||
array( 'jquery', 'selectWoo', 'wc-enhanced-select', 'wp-api' ),
|
||||
$version,
|
||||
true
|
||||
);
|
||||
wp_localize_script(
|
||||
'wc-brands-enhanced-select',
|
||||
'wc_brands_enhanced_select_params',
|
||||
array( 'ajax_url' => get_rest_url() . 'brands/search' )
|
||||
);
|
||||
wp_enqueue_script( 'wc-brands-enhanced-select' );
|
||||
}
|
||||
|
||||
if ( in_array( $screen->id, array( 'edit-product_brand' ), true ) ) {
|
||||
wp_enqueue_media();
|
||||
wp_enqueue_style( 'woocommerce_admin_styles' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue styles.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function styles() {
|
||||
$version = Constants::get_constant( 'WC_VERSION' );
|
||||
wp_enqueue_style( 'brands-admin-styles', WC()->plugin_url() . '/assets/css/brands-admin.css', array(), $version );
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin settings function.
|
||||
*/
|
||||
public function admin_settings() {
|
||||
woocommerce_admin_fields( $this->settings );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save admin settings function.
|
||||
*/
|
||||
public function save_admin_settings() {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( isset( $_GET['section'] ) && 'brands' === $_GET['section'] ) {
|
||||
woocommerce_update_options( $this->settings );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Category thumbnails.
|
||||
*/
|
||||
public function add_thumbnail_field() {
|
||||
global $woocommerce;
|
||||
?>
|
||||
<div class="form-field">
|
||||
<label><?php esc_html_e( 'Thumbnail', 'woocommerce' ); ?></label>
|
||||
<div id="product_cat_thumbnail" style="float:left;margin-right:10px;"><img src="<?php echo esc_url( wc_placeholder_img_src() ); ?>" width="60px" height="60px" /></div>
|
||||
<div style="line-height:60px;">
|
||||
<input type="hidden" id="product_cat_thumbnail_id" name="product_cat_thumbnail_id" />
|
||||
<button type="button" class="upload_image_button button"><?php esc_html_e( 'Upload/Add image', 'woocommerce' ); ?></button>
|
||||
<button type="button" class="remove_image_button button"><?php esc_html_e( 'Remove image', 'woocommerce' ); ?></button>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(function(){
|
||||
// Only show the "remove image" button when needed
|
||||
if ( ! jQuery('#product_cat_thumbnail_id').val() ) {
|
||||
jQuery('.remove_image_button').hide();
|
||||
}
|
||||
|
||||
// Uploading files
|
||||
var file_frame;
|
||||
|
||||
jQuery(document).on( 'click', '.upload_image_button', function( event ){
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// If the media frame already exists, reopen it.
|
||||
if ( file_frame ) {
|
||||
file_frame.open();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the media frame.
|
||||
file_frame = wp.media.frames.downloadable_file = wp.media({
|
||||
title: '<?php echo esc_js( __( 'Choose an image', 'woocommerce' ) ); ?>',
|
||||
button: {
|
||||
text: '<?php echo esc_js( __( 'Use image', 'woocommerce' ) ); ?>',
|
||||
},
|
||||
multiple: false
|
||||
});
|
||||
|
||||
// When an image is selected, run a callback.
|
||||
file_frame.on( 'select', function() {
|
||||
attachment = file_frame.state().get('selection').first().toJSON();
|
||||
|
||||
jQuery('#product_cat_thumbnail_id').val( attachment.id );
|
||||
jQuery('#product_cat_thumbnail img').attr('src', attachment.url );
|
||||
jQuery('.remove_image_button').show();
|
||||
});
|
||||
|
||||
// Finally, open the modal.
|
||||
file_frame.open();
|
||||
});
|
||||
|
||||
jQuery(document).on( 'click', '.remove_image_button', function( event ){
|
||||
jQuery('#product_cat_thumbnail img').attr('src', '<?php echo esc_js( wc_placeholder_img_src() ); ?>');
|
||||
jQuery('#product_cat_thumbnail_id').val('');
|
||||
jQuery('.remove_image_button').hide();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit thumbnail field row.
|
||||
*
|
||||
* @param WP_Term $term Current taxonomy term object.
|
||||
*/
|
||||
public function edit_thumbnail_field( $term ) {
|
||||
global $woocommerce;
|
||||
|
||||
$image = '';
|
||||
$thumbnail_id = get_term_meta( $term->term_id, 'thumbnail_id', true );
|
||||
if ( $thumbnail_id ) {
|
||||
$image = wp_get_attachment_url( $thumbnail_id );
|
||||
}
|
||||
if ( empty( $image ) ) {
|
||||
$image = wc_placeholder_img_src();
|
||||
}
|
||||
?>
|
||||
<tr class="form-field">
|
||||
<th scope="row" valign="top"><label><?php esc_html_e( 'Thumbnail', 'woocommerce' ); ?></label></th>
|
||||
<td>
|
||||
<div id="product_cat_thumbnail" style="float:left;margin-right:10px;"><img src="<?php echo esc_url( $image ); ?>" width="60px" height="60px" /></div>
|
||||
<div style="line-height:60px;">
|
||||
<input type="hidden" id="product_cat_thumbnail_id" name="product_cat_thumbnail_id" value="<?php echo esc_attr( $thumbnail_id ); ?>" />
|
||||
<button type="button" class="upload_image_button button"><?php esc_html_e( 'Upload/Add image', 'woocommerce' ); ?></button>
|
||||
<button type="button" class="remove_image_button button"><?php esc_html_e( 'Remove image', 'woocommerce' ); ?></button>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
|
||||
jQuery(function(){
|
||||
|
||||
// Only show the "remove image" button when needed
|
||||
if ( ! jQuery('#product_cat_thumbnail_id').val() )
|
||||
jQuery('.remove_image_button').hide();
|
||||
|
||||
// Uploading files
|
||||
var file_frame;
|
||||
|
||||
jQuery(document).on( 'click', '.upload_image_button', function( event ){
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// If the media frame already exists, reopen it.
|
||||
if ( file_frame ) {
|
||||
file_frame.open();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the media frame.
|
||||
file_frame = wp.media.frames.downloadable_file = wp.media({
|
||||
title: '<?php echo esc_js( __( 'Choose an image', 'woocommerce' ) ); ?>',
|
||||
button: {
|
||||
text: '<?php echo esc_js( __( 'Use image', 'woocommerce' ) ); ?>',
|
||||
},
|
||||
multiple: false
|
||||
});
|
||||
|
||||
// When an image is selected, run a callback.
|
||||
file_frame.on( 'select', function() {
|
||||
attachment = file_frame.state().get('selection').first().toJSON();
|
||||
|
||||
jQuery('#product_cat_thumbnail_id').val( attachment.id );
|
||||
jQuery('#product_cat_thumbnail img').attr('src', attachment.url );
|
||||
jQuery('.remove_image_button').show();
|
||||
});
|
||||
|
||||
// Finally, open the modal.
|
||||
file_frame.open();
|
||||
});
|
||||
|
||||
jQuery(document).on( 'click', '.remove_image_button', function( event ){
|
||||
jQuery('#product_cat_thumbnail img').attr('src', '<?php echo esc_js( wc_placeholder_img_src() ); ?>');
|
||||
jQuery('#product_cat_thumbnail_id').val('');
|
||||
jQuery('.remove_image_button').hide();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
<div class="clear"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves thumbnail field.
|
||||
*
|
||||
* @param int $term_id Term ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function thumbnail_field_save( $term_id ) {
|
||||
if ( isset( $_POST['product_cat_thumbnail_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_term_meta( $term_id, 'thumbnail_id', absint( $_POST['product_cat_thumbnail_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Description for brand page.
|
||||
*/
|
||||
public function taxonomy_description() {
|
||||
echo wp_kses_post( wpautop( __( 'Brands be added and managed from this screen. You can optionally upload a brand image to display in brand widgets and on brand archives', 'woocommerce' ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort brands function.
|
||||
*
|
||||
* @param array $sortable Sortable array.
|
||||
*/
|
||||
public function sort_brands( $sortable ) {
|
||||
$sortable[] = 'product_brand';
|
||||
return $sortable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add brands column in second-to-last position.
|
||||
*
|
||||
* @since 9.4.0
|
||||
* @param mixed $columns Columns.
|
||||
* @return array
|
||||
*/
|
||||
public function product_columns( $columns ) {
|
||||
if ( empty( $columns ) ) {
|
||||
return $columns;
|
||||
}
|
||||
|
||||
$column_index = 'taxonomy-product_brand';
|
||||
$brands_column = $columns[ $column_index ];
|
||||
unset( $columns[ $column_index ] );
|
||||
return array_merge(
|
||||
array_slice( $columns, 0, -2, true ),
|
||||
array( $column_index => $brands_column ),
|
||||
array_slice( $columns, -2, null, true )
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Columns function.
|
||||
*
|
||||
* @param mixed $columns Columns.
|
||||
*/
|
||||
public function columns( $columns ) {
|
||||
if ( empty( $columns ) ) {
|
||||
return $columns;
|
||||
}
|
||||
|
||||
$new_columns = array();
|
||||
$new_columns['cb'] = $columns['cb'];
|
||||
$new_columns['thumb'] = __( 'Image', 'woocommerce' );
|
||||
unset( $columns['cb'] );
|
||||
$columns = array_merge( $new_columns, $columns );
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Column function.
|
||||
*
|
||||
* @param mixed $columns Columns.
|
||||
* @param mixed $column Column.
|
||||
* @param mixed $id ID.
|
||||
*/
|
||||
public function column( $columns, $column, $id ) {
|
||||
if ( 'thumb' === $column ) {
|
||||
global $woocommerce;
|
||||
|
||||
$image = '';
|
||||
$thumbnail_id = get_term_meta( $id, 'thumbnail_id', true );
|
||||
|
||||
if ( $thumbnail_id ) {
|
||||
$image = wp_get_attachment_url( $thumbnail_id );
|
||||
}
|
||||
if ( empty( $image ) ) {
|
||||
$image = wc_placeholder_img_src();
|
||||
}
|
||||
|
||||
$columns .= '<img src="' . $image . '" alt="Thumbnail" class="wp-post-image" height="48" width="48" />';
|
||||
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders either dropdown or a search field for brands depending on the threshold value of
|
||||
* woocommerce_product_brand_filter_threshold filter.
|
||||
*/
|
||||
public function render_product_brand_filter() {
|
||||
// phpcs:disable WordPress.Security.NonceVerification
|
||||
$brands_count = (int) wp_count_terms( 'product_brand' );
|
||||
$current_brand_slug = wc_clean( wp_unslash( $_GET['product_brand'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
|
||||
/**
|
||||
* Filter the brands threshold count.
|
||||
*
|
||||
* @since 9.4.0
|
||||
*
|
||||
* @param int $value Threshold.
|
||||
*/
|
||||
if ( $brands_count <= apply_filters( 'woocommerce_product_brand_filter_threshold', 100 ) ) {
|
||||
wc_product_dropdown_categories(
|
||||
array(
|
||||
'pad_counts' => true,
|
||||
'show_count' => true,
|
||||
'orderby' => 'name',
|
||||
'selected' => $current_brand_slug,
|
||||
'show_option_none' => __( 'Filter by brand', 'woocommerce' ),
|
||||
'option_none_value' => '',
|
||||
'value_field' => 'slug',
|
||||
'taxonomy' => 'product_brand',
|
||||
'name' => 'product_brand',
|
||||
'class' => 'dropdown_product_brand',
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$current_brand = $current_brand_slug ? get_term_by( 'slug', $current_brand_slug, 'product_brand' ) : '';
|
||||
$selected_option = '';
|
||||
if ( $current_brand_slug && $current_brand ) {
|
||||
$selected_option = '<option value="' . esc_attr( $current_brand_slug ) . '" selected="selected">' . esc_html( htmlspecialchars( wp_kses_post( $current_brand->name ) ) ) . '</option>';
|
||||
}
|
||||
$placeholder = esc_attr__( 'Filter by brand', 'woocommerce' );
|
||||
?>
|
||||
<select class="wc-brands-search" name="product_brand" data-placeholder="<?php echo $placeholder; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" data-allow_clear="true">
|
||||
<?php echo $selected_option; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</select>
|
||||
<?php
|
||||
}
|
||||
// phpcs:enable WordPress.Security.NonceVerification
|
||||
}
|
||||
|
||||
/**
|
||||
* Add brand base permalink setting.
|
||||
*/
|
||||
public function add_brand_base_setting() {
|
||||
$screen = get_current_screen();
|
||||
if ( ! $screen || 'options-permalink' !== $screen->id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_settings_field(
|
||||
'woocommerce_product_brand_slug',
|
||||
__( 'Product brand base', 'woocommerce' ),
|
||||
array( $this, 'product_brand_slug_input' ),
|
||||
'permalink',
|
||||
'optional'
|
||||
);
|
||||
|
||||
$this->save_permalink_settings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a slug input box.
|
||||
*/
|
||||
public function product_brand_slug_input() {
|
||||
$permalink = get_option( 'woocommerce_brand_permalink', '' );
|
||||
?>
|
||||
<input name="woocommerce_product_brand_slug" type="text" class="regular-text code" value="<?php echo esc_attr( $permalink ); ?>" placeholder="<?php echo esc_attr_x( 'brand', 'slug', 'woocommerce' ); ?>" />
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save permalnks settings.
|
||||
*
|
||||
* We need to save the options ourselves;
|
||||
* settings api does not trigger save for the permalinks page.
|
||||
*/
|
||||
public function save_permalink_settings() {
|
||||
if ( ! is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $_POST['permalink_structure'], $_POST['wc-permalinks-nonce'], $_POST['woocommerce_product_brand_slug'] ) && wp_verify_nonce( wp_unslash( $_POST['wc-permalinks-nonce'] ), 'wc-permalinks' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
update_option( 'woocommerce_brand_permalink', wc_sanitize_permalink( trim( wc_clean( wp_unslash( $_POST['woocommerce_product_brand_slug'] ) ) ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the product base.
|
||||
*
|
||||
* Must have an additional slug, not just the brand as the base.
|
||||
*
|
||||
* @param array $value Value.
|
||||
*/
|
||||
public function validate_product_base( $value ) {
|
||||
if ( '/%product_brand%/' === trailingslashit( $value['product_base'] ) ) {
|
||||
$value['product_base'] = '/' . _x( 'product', 'slug', 'woocommerce' ) . $value['product_base'];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add csv column for importing/exporting.
|
||||
*
|
||||
* @param array $options Mapping options.
|
||||
* @return array $options
|
||||
*/
|
||||
public function add_column_to_importer_exporter( $options ) {
|
||||
$options['brand_ids'] = __( 'Brands', 'woocommerce' );
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add default column mapping.
|
||||
*
|
||||
* @param array $mappings Mappings.
|
||||
* @return array $mappings
|
||||
*/
|
||||
public function add_default_column_mapping( $mappings ) {
|
||||
$new_mapping = array( __( 'Brands', 'woocommerce' ) => 'brand_ids' );
|
||||
return array_merge( $mappings, $new_mapping );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add brands to newly imported product.
|
||||
*
|
||||
* @param WC_Product $product Product being imported.
|
||||
* @param array $data Raw CSV data.
|
||||
*/
|
||||
public function process_import( $product, $data ) {
|
||||
if ( empty( $data['brand_ids'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$brand_ids = array_map( 'intval', $this->parse_brands_field( $data['brand_ids'] ) );
|
||||
|
||||
wp_set_object_terms( $product->get_id(), $brand_ids, 'product_brand' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse brands field from a CSV during import.
|
||||
*
|
||||
* Based on WC_Product_CSV_Importer::parse_categories_field()
|
||||
*
|
||||
* @param string $value Field value.
|
||||
* @return array
|
||||
*/
|
||||
public function parse_brands_field( $value ) {
|
||||
|
||||
// Based on WC_Product_Importer::explode_values().
|
||||
$values = str_replace( '\\,', '::separator::', explode( ',', $value ) );
|
||||
$row_terms = array();
|
||||
foreach ( $values as $row_value ) {
|
||||
$row_terms[] = trim( str_replace( '::separator::', ',', $row_value ) );
|
||||
}
|
||||
|
||||
$brands = array();
|
||||
foreach ( $row_terms as $row_term ) {
|
||||
$parent = null;
|
||||
|
||||
// WC Core uses '>', but for some reason it's already escaped at this point.
|
||||
$_terms = array_map( 'trim', explode( '>', $row_term ) );
|
||||
$total = count( $_terms );
|
||||
|
||||
foreach ( $_terms as $index => $_term ) {
|
||||
$term = term_exists( $_term, 'product_brand', $parent );
|
||||
|
||||
if ( is_array( $term ) ) {
|
||||
$term_id = $term['term_id'];
|
||||
} else {
|
||||
$term = wp_insert_term( $_term, 'product_brand', array( 'parent' => intval( $parent ) ) );
|
||||
|
||||
if ( is_wp_error( $term ) ) {
|
||||
break; // We cannot continue if the term cannot be inserted.
|
||||
}
|
||||
|
||||
$term_id = $term['term_id'];
|
||||
}
|
||||
|
||||
// Only requires assign the last category.
|
||||
if ( ( 1 + $index ) === $total ) {
|
||||
$brands[] = $term_id;
|
||||
} else {
|
||||
// Store parent to be able to insert or query brands based in parent ID.
|
||||
$parent = $term_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $brands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get brands column value for csv export.
|
||||
*
|
||||
* @param string $value What will be exported.
|
||||
* @param WC_Product $product Product being exported.
|
||||
* @return string Brands separated by commas and child brands as "parent > child".
|
||||
*/
|
||||
public function get_column_value_brand_ids( $value, $product ) {
|
||||
$brand_ids = wp_parse_id_list( wp_get_post_terms( $product->get_id(), 'product_brand', array( 'fields' => 'ids' ) ) );
|
||||
|
||||
if ( ! count( $brand_ids ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Based on WC_CSV_Exporter::format_term_ids().
|
||||
$formatted_brands = array();
|
||||
foreach ( $brand_ids as $brand_id ) {
|
||||
$formatted_term = array();
|
||||
$ancestor_ids = array_reverse( get_ancestors( $brand_id, 'product_brand' ) );
|
||||
|
||||
foreach ( $ancestor_ids as $ancestor_id ) {
|
||||
$term = get_term( $ancestor_id, 'product_brand' );
|
||||
if ( $term && ! is_wp_error( $term ) ) {
|
||||
$formatted_term[] = $term->name;
|
||||
}
|
||||
}
|
||||
|
||||
$term = get_term( $brand_id, 'product_brand' );
|
||||
|
||||
if ( $term && ! is_wp_error( $term ) ) {
|
||||
$formatted_term[] = $term->name;
|
||||
}
|
||||
|
||||
$formatted_brands[] = implode( ' > ', $formatted_term );
|
||||
}
|
||||
|
||||
// Based on WC_CSV_Exporter::implode_values().
|
||||
$values_to_implode = array();
|
||||
foreach ( $formatted_brands as $brand ) {
|
||||
$brand = (string) is_scalar( $brand ) ? $brand : '';
|
||||
$values_to_implode[] = str_replace( ',', '\\,', $brand );
|
||||
}
|
||||
|
||||
return implode( ', ', $values_to_implode );
|
||||
}
|
||||
}
|
||||
|
||||
$GLOBALS['WC_Brands_Admin'] = new WC_Brands_Admin();
|
|
@ -215,114 +215,12 @@ class WC_Admin_Importers {
|
|||
* Ajax callback for importing one batch of products from a CSV.
|
||||
*/
|
||||
public function do_ajax_product_import() {
|
||||
global $wpdb;
|
||||
|
||||
check_ajax_referer( 'wc-product-import', 'security' );
|
||||
|
||||
if ( ! $this->import_allowed() || ! isset( $_POST['file'] ) ) { // PHPCS: input var ok.
|
||||
if ( ! $this->import_allowed() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Insufficient privileges to import products.', 'woocommerce' ) ) );
|
||||
}
|
||||
|
||||
include_once WC_ABSPATH . 'includes/admin/importers/class-wc-product-csv-importer-controller.php';
|
||||
include_once WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php';
|
||||
|
||||
$file = wc_clean( wp_unslash( $_POST['file'] ) ); // PHPCS: input var ok.
|
||||
$params = array(
|
||||
'delimiter' => ! empty( $_POST['delimiter'] ) ? wc_clean( wp_unslash( $_POST['delimiter'] ) ) : ',', // PHPCS: input var ok.
|
||||
'start_pos' => isset( $_POST['position'] ) ? absint( $_POST['position'] ) : 0, // PHPCS: input var ok.
|
||||
'mapping' => isset( $_POST['mapping'] ) ? (array) wc_clean( wp_unslash( $_POST['mapping'] ) ) : array(), // PHPCS: input var ok.
|
||||
'update_existing' => isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false, // PHPCS: input var ok.
|
||||
'character_encoding' => isset( $_POST['character_encoding'] ) ? wc_clean( wp_unslash( $_POST['character_encoding'] ) ) : '',
|
||||
|
||||
/**
|
||||
* Batch size for the product import process.
|
||||
*
|
||||
* @param int $size Batch size.
|
||||
*
|
||||
* @since 3.1.0
|
||||
*/
|
||||
'lines' => apply_filters( 'woocommerce_product_import_batch_size', 30 ),
|
||||
'parse' => true,
|
||||
);
|
||||
|
||||
// Log failures.
|
||||
if ( 0 !== $params['start_pos'] ) {
|
||||
$error_log = array_filter( (array) get_user_option( 'product_import_error_log' ) );
|
||||
} else {
|
||||
$error_log = array();
|
||||
}
|
||||
|
||||
$importer = WC_Product_CSV_Importer_Controller::get_importer( $file, $params );
|
||||
$results = $importer->import();
|
||||
$percent_complete = $importer->get_percent_complete();
|
||||
$error_log = array_merge( $error_log, $results['failed'], $results['skipped'] );
|
||||
|
||||
update_user_option( get_current_user_id(), 'product_import_error_log', $error_log );
|
||||
|
||||
if ( 100 === $percent_complete ) {
|
||||
// @codingStandardsIgnoreStart.
|
||||
$wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_original_id' ) );
|
||||
$wpdb->delete( $wpdb->posts, array(
|
||||
'post_type' => 'product',
|
||||
'post_status' => 'importing',
|
||||
) );
|
||||
$wpdb->delete( $wpdb->posts, array(
|
||||
'post_type' => 'product_variation',
|
||||
'post_status' => 'importing',
|
||||
) );
|
||||
// @codingStandardsIgnoreEnd.
|
||||
|
||||
// Clean up orphaned data.
|
||||
$wpdb->query(
|
||||
"
|
||||
DELETE {$wpdb->posts}.* FROM {$wpdb->posts}
|
||||
LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->posts}.post_parent
|
||||
WHERE wp.ID IS NULL AND {$wpdb->posts}.post_type = 'product_variation'
|
||||
"
|
||||
);
|
||||
$wpdb->query(
|
||||
"
|
||||
DELETE {$wpdb->postmeta}.* FROM {$wpdb->postmeta}
|
||||
LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->postmeta}.post_id
|
||||
WHERE wp.ID IS NULL
|
||||
"
|
||||
);
|
||||
// @codingStandardsIgnoreStart.
|
||||
$wpdb->query( "
|
||||
DELETE tr.* FROM {$wpdb->term_relationships} tr
|
||||
LEFT JOIN {$wpdb->posts} wp ON wp.ID = tr.object_id
|
||||
LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
|
||||
WHERE wp.ID IS NULL
|
||||
AND tt.taxonomy IN ( '" . implode( "','", array_map( 'esc_sql', get_object_taxonomies( 'product' ) ) ) . "' )
|
||||
" );
|
||||
// @codingStandardsIgnoreEnd.
|
||||
|
||||
// Send success.
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'position' => 'done',
|
||||
'percentage' => 100,
|
||||
'url' => add_query_arg( array( '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ) ), admin_url( 'edit.php?post_type=product&page=product_importer&step=done' ) ),
|
||||
'imported' => is_countable( $results['imported'] ) ? count( $results['imported'] ) : 0,
|
||||
'imported_variations' => is_countable( $results['imported_variations'] ) ? count( $results['imported_variations'] ) : 0,
|
||||
'failed' => is_countable( $results['failed'] ) ? count( $results['failed'] ) : 0,
|
||||
'updated' => is_countable( $results['updated'] ) ? count( $results['updated'] ) : 0,
|
||||
'skipped' => is_countable( $results['skipped'] ) ? count( $results['skipped'] ) : 0,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'position' => $importer->get_file_position(),
|
||||
'percentage' => $percent_complete,
|
||||
'imported' => is_countable( $results['imported'] ) ? count( $results['imported'] ) : 0,
|
||||
'imported_variations' => is_countable( $results['imported_variations'] ) ? count( $results['imported_variations'] ) : 0,
|
||||
'failed' => is_countable( $results['failed'] ) ? count( $results['failed'] ) : 0,
|
||||
'updated' => is_countable( $results['updated'] ) ? count( $results['updated'] ) : 0,
|
||||
'skipped' => is_countable( $results['skipped'] ) ? count( $results['skipped'] ) : 0,
|
||||
)
|
||||
);
|
||||
}
|
||||
WC_Product_CSV_Importer_Controller::dispatch_ajax();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -103,6 +103,56 @@ class WC_Product_CSV_Importer_Controller {
|
|||
return wc_is_file_valid_csv( $file, $check_path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs before controller actions to check that the file used during the import is valid.
|
||||
*
|
||||
* @since 9.3.0
|
||||
*
|
||||
* @param string $path Path to test.
|
||||
*
|
||||
* @throws \Exception When file validation fails.
|
||||
*/
|
||||
protected static function check_file_path( string $path ): void {
|
||||
$is_valid_file = false;
|
||||
|
||||
if ( ! empty( $path ) ) {
|
||||
$path = realpath( $path );
|
||||
$is_valid_file = false !== $path;
|
||||
}
|
||||
|
||||
// File must be readable.
|
||||
$is_valid_file = $is_valid_file && is_readable( $path );
|
||||
|
||||
// Check that file is within an allowed location.
|
||||
if ( $is_valid_file ) {
|
||||
$in_valid_location = false;
|
||||
$valid_locations = array();
|
||||
$valid_locations[] = ABSPATH;
|
||||
|
||||
$upload_dir = wp_get_upload_dir();
|
||||
if ( false === $upload_dir['error'] ) {
|
||||
$valid_locations[] = $upload_dir['basedir'];
|
||||
}
|
||||
|
||||
foreach ( $valid_locations as $valid_location ) {
|
||||
if ( 0 === stripos( $path, trailingslashit( realpath( $valid_location ) ) ) ) {
|
||||
$in_valid_location = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$is_valid_file = $in_valid_location;
|
||||
}
|
||||
|
||||
if ( ! $is_valid_file ) {
|
||||
throw new \Exception( esc_html__( 'File path provided for import is invalid.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( ! self::is_file_valid_csv( $path ) ) {
|
||||
throw new \Exception( esc_html__( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the valid filetypes for a CSV file.
|
||||
*
|
||||
|
@ -263,17 +313,151 @@ class WC_Product_CSV_Importer_Controller {
|
|||
* Dispatch current step and show correct view.
|
||||
*/
|
||||
public function dispatch() {
|
||||
$output = '';
|
||||
|
||||
try {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
if ( ! empty( $_POST['save_step'] ) && ! empty( $this->steps[ $this->step ]['handler'] ) ) {
|
||||
if ( is_callable( $this->steps[ $this->step ]['handler'] ) ) {
|
||||
call_user_func( $this->steps[ $this->step ]['handler'], $this );
|
||||
}
|
||||
}
|
||||
|
||||
ob_start();
|
||||
|
||||
if ( is_callable( $this->steps[ $this->step ]['view'] ) ) {
|
||||
call_user_func( $this->steps[ $this->step ]['view'], $this );
|
||||
}
|
||||
|
||||
$output = ob_get_clean();
|
||||
} catch ( \Exception $e ) {
|
||||
$this->add_error( $e->getMessage() );
|
||||
}
|
||||
|
||||
$this->output_header();
|
||||
$this->output_steps();
|
||||
$this->output_errors();
|
||||
call_user_func( $this->steps[ $this->step ]['view'], $this );
|
||||
echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output is HTML we've generated ourselves.
|
||||
$this->output_footer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes AJAX requests related to a product CSV import.
|
||||
*
|
||||
* @since 9.3.0
|
||||
*/
|
||||
public static function dispatch_ajax() {
|
||||
global $wpdb;
|
||||
|
||||
check_ajax_referer( 'wc-product-import', 'security' );
|
||||
|
||||
try {
|
||||
$file = wc_clean( wp_unslash( $_POST['file'] ?? '' ) ); // PHPCS: input var ok.
|
||||
self::check_file_path( $file );
|
||||
|
||||
$params = array(
|
||||
'delimiter' => ! empty( $_POST['delimiter'] ) ? wc_clean( wp_unslash( $_POST['delimiter'] ) ) : ',', // PHPCS: input var ok.
|
||||
'start_pos' => isset( $_POST['position'] ) ? absint( $_POST['position'] ) : 0, // PHPCS: input var ok.
|
||||
'mapping' => isset( $_POST['mapping'] ) ? (array) wc_clean( wp_unslash( $_POST['mapping'] ) ) : array(), // PHPCS: input var ok.
|
||||
'update_existing' => isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false, // PHPCS: input var ok.
|
||||
'character_encoding' => isset( $_POST['character_encoding'] ) ? wc_clean( wp_unslash( $_POST['character_encoding'] ) ) : '',
|
||||
|
||||
/**
|
||||
* Batch size for the product import process.
|
||||
*
|
||||
* @param int $size Batch size.
|
||||
*
|
||||
* @since 3.1.0
|
||||
*/
|
||||
'lines' => apply_filters( 'woocommerce_product_import_batch_size', 1 ),
|
||||
'parse' => true,
|
||||
);
|
||||
|
||||
// Log failures.
|
||||
if ( 0 !== $params['start_pos'] ) {
|
||||
$error_log = array_filter( (array) get_user_option( 'product_import_error_log' ) );
|
||||
} else {
|
||||
$error_log = array();
|
||||
}
|
||||
|
||||
include_once WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php';
|
||||
|
||||
$importer = self::get_importer( $file, $params );
|
||||
$results = $importer->import();
|
||||
$percent_complete = $importer->get_percent_complete();
|
||||
$error_log = array_merge( $error_log, $results['failed'], $results['skipped'] );
|
||||
|
||||
update_user_option( get_current_user_id(), 'product_import_error_log', $error_log );
|
||||
|
||||
if ( 100 === $percent_complete ) {
|
||||
// @codingStandardsIgnoreStart.
|
||||
$wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_original_id' ) );
|
||||
$wpdb->delete( $wpdb->posts, array(
|
||||
'post_type' => 'product',
|
||||
'post_status' => 'importing',
|
||||
) );
|
||||
$wpdb->delete( $wpdb->posts, array(
|
||||
'post_type' => 'product_variation',
|
||||
'post_status' => 'importing',
|
||||
) );
|
||||
// @codingStandardsIgnoreEnd.
|
||||
|
||||
// Clean up orphaned data.
|
||||
$wpdb->query(
|
||||
"
|
||||
DELETE {$wpdb->posts}.* FROM {$wpdb->posts}
|
||||
LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->posts}.post_parent
|
||||
WHERE wp.ID IS NULL AND {$wpdb->posts}.post_type = 'product_variation'
|
||||
"
|
||||
);
|
||||
$wpdb->query(
|
||||
"
|
||||
DELETE {$wpdb->postmeta}.* FROM {$wpdb->postmeta}
|
||||
LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->postmeta}.post_id
|
||||
WHERE wp.ID IS NULL
|
||||
"
|
||||
);
|
||||
// @codingStandardsIgnoreStart.
|
||||
$wpdb->query( "
|
||||
DELETE tr.* FROM {$wpdb->term_relationships} tr
|
||||
LEFT JOIN {$wpdb->posts} wp ON wp.ID = tr.object_id
|
||||
LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
|
||||
WHERE wp.ID IS NULL
|
||||
AND tt.taxonomy IN ( '" . implode( "','", array_map( 'esc_sql', get_object_taxonomies( 'product' ) ) ) . "' )
|
||||
" );
|
||||
// @codingStandardsIgnoreEnd.
|
||||
|
||||
// Send success.
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'position' => 'done',
|
||||
'percentage' => 100,
|
||||
'url' => add_query_arg( array( '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ) ), admin_url( 'edit.php?post_type=product&page=product_importer&step=done' ) ),
|
||||
'imported' => is_countable( $results['imported'] ) ? count( $results['imported'] ) : 0,
|
||||
'imported_variations' => is_countable( $results['imported_variations'] ) ? count( $results['imported_variations'] ) : 0,
|
||||
'failed' => is_countable( $results['failed'] ) ? count( $results['failed'] ) : 0,
|
||||
'updated' => is_countable( $results['updated'] ) ? count( $results['updated'] ) : 0,
|
||||
'skipped' => is_countable( $results['skipped'] ) ? count( $results['skipped'] ) : 0,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'position' => $importer->get_file_position(),
|
||||
'percentage' => $percent_complete,
|
||||
'imported' => is_countable( $results['imported'] ) ? count( $results['imported'] ) : 0,
|
||||
'imported_variations' => is_countable( $results['imported_variations'] ) ? count( $results['imported_variations'] ) : 0,
|
||||
'failed' => is_countable( $results['failed'] ) ? count( $results['failed'] ) : 0,
|
||||
'updated' => is_countable( $results['updated'] ) ? count( $results['updated'] ) : 0,
|
||||
'skipped' => is_countable( $results['skipped'] ) ? count( $results['skipped'] ) : 0,
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
wp_send_json_error( array( 'message' => $e->getMessage() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output information about the uploading process.
|
||||
*/
|
||||
|
@ -314,60 +498,20 @@ class WC_Product_CSV_Importer_Controller {
|
|||
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce already verified in WC_Product_CSV_Importer_Controller::upload_form_handler()
|
||||
$file_url = isset( $_POST['file_url'] ) ? wc_clean( wp_unslash( $_POST['file_url'] ) ) : '';
|
||||
|
||||
if ( empty( $file_url ) ) {
|
||||
if ( ! isset( $_FILES['import'] ) ) {
|
||||
return new WP_Error( 'woocommerce_product_csv_importer_upload_file_empty', __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.', 'woocommerce' ) );
|
||||
try {
|
||||
if ( ! empty( $file_url ) ) {
|
||||
$path = ABSPATH . $file_url;
|
||||
self::check_file_path( $path );
|
||||
} else {
|
||||
$csv_import_util = wc_get_container()->get( Automattic\WooCommerce\Internal\Admin\ImportExport\CSVUploadHelper::class );
|
||||
$upload = $csv_import_util->handle_csv_upload( 'product', 'import', self::get_valid_csv_filetypes() );
|
||||
$path = $upload['file'];
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
|
||||
if ( ! self::is_file_valid_csv( wc_clean( wp_unslash( $_FILES['import']['name'] ) ), false ) ) {
|
||||
return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) );
|
||||
return $path;
|
||||
} catch ( \Exception $e ) {
|
||||
return new \WP_Error( 'woocommerce_product_csv_importer_upload_invalid_file', $e->getMessage() );
|
||||
}
|
||||
|
||||
$overrides = array(
|
||||
'test_form' => false,
|
||||
'mimes' => self::get_valid_csv_filetypes(),
|
||||
);
|
||||
$import = $_FILES['import']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$upload = wp_handle_upload( $import, $overrides );
|
||||
|
||||
if ( isset( $upload['error'] ) ) {
|
||||
return new WP_Error( 'woocommerce_product_csv_importer_upload_error', $upload['error'] );
|
||||
}
|
||||
|
||||
// Construct the object array.
|
||||
$object = array(
|
||||
'post_title' => basename( $upload['file'] ),
|
||||
'post_content' => $upload['url'],
|
||||
'post_mime_type' => $upload['type'],
|
||||
'guid' => $upload['url'],
|
||||
'context' => 'import',
|
||||
'post_status' => 'private',
|
||||
);
|
||||
|
||||
// Save the data.
|
||||
$id = wp_insert_attachment( $object, $upload['file'] );
|
||||
|
||||
/*
|
||||
* Schedule a cleanup for one day from now in case of failed
|
||||
* import or missing wp_import_cleanup() call.
|
||||
*/
|
||||
wp_schedule_single_event( time() + DAY_IN_SECONDS, 'importer_scheduled_cleanup', array( $id ) );
|
||||
|
||||
return $upload['file'];
|
||||
} elseif (
|
||||
( 0 === stripos( realpath( ABSPATH . $file_url ), ABSPATH ) ) &&
|
||||
file_exists( ABSPATH . $file_url )
|
||||
) {
|
||||
if ( ! self::is_file_valid_csv( ABSPATH . $file_url ) ) {
|
||||
return new WP_Error( 'woocommerce_product_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
return ABSPATH . $file_url;
|
||||
}
|
||||
// phpcs:enable
|
||||
|
||||
return new WP_Error( 'woocommerce_product_csv_importer_upload_invalid_file', __( 'Please upload or provide the link to a valid CSV file.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -375,6 +519,8 @@ class WC_Product_CSV_Importer_Controller {
|
|||
*/
|
||||
protected function mapping_form() {
|
||||
check_admin_referer( 'woocommerce-csv-importer' );
|
||||
self::check_file_path( $this->file );
|
||||
|
||||
$args = array(
|
||||
'lines' => 1,
|
||||
'delimiter' => $this->delimiter,
|
||||
|
@ -412,18 +558,7 @@ class WC_Product_CSV_Importer_Controller {
|
|||
// Displaying this page triggers Ajax action to run the import with a valid nonce,
|
||||
// therefore this page needs to be nonce protected as well.
|
||||
check_admin_referer( 'woocommerce-csv-importer' );
|
||||
|
||||
if ( ! self::is_file_valid_csv( $this->file ) ) {
|
||||
$this->add_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) );
|
||||
$this->output_errors();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! is_file( $this->file ) ) {
|
||||
$this->add_error( __( 'The file does not exist, please try again.', 'woocommerce' ) );
|
||||
$this->output_errors();
|
||||
return;
|
||||
}
|
||||
self::check_file_path( $this->file );
|
||||
|
||||
if ( ! empty( $_POST['map_from'] ) && ! empty( $_POST['map_to'] ) ) {
|
||||
$mapping_from = wc_clean( wp_unslash( $_POST['map_from'] ) );
|
||||
|
|
|
@ -20,7 +20,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
<strong><?php echo esc_html( wc_attribute_label( $attribute->get_name() ) ); ?></strong>
|
||||
<input type="hidden" name="attribute_names[<?php echo esc_attr( $i ); ?>]" value="<?php echo esc_attr( $attribute->get_name() ); ?>" />
|
||||
<?php else : ?>
|
||||
<input type="text" class="attribute_name" name="attribute_names[<?php echo esc_attr( $i ); ?>]" value="<?php echo esc_attr( $attribute->get_name() ); ?>" placeholder="<?php esc_attr_e( 'f.e. size or color', 'woocommerce' ); ?>" />
|
||||
<input type="text" class="attribute_name" name="attribute_names[<?php echo esc_attr( $i ); ?>]" value="<?php echo esc_attr( $attribute->get_name() ); ?>" placeholder="<?php esc_attr_e( 'e.g. length or weight', 'woocommerce' ); ?>" />
|
||||
<?php endif; ?>
|
||||
<input type="hidden" name="attribute_position[<?php echo esc_attr( $i ); ?>]" class="attribute_position" value="<?php echo esc_attr( $attribute->get_position() ); ?>" />
|
||||
</td>
|
||||
|
|
|
@ -0,0 +1,369 @@
|
|||
<?php // phpcs:disable WordPress.Files.FileName.InvalidClassFileName
|
||||
|
||||
declare( strict_types = 1);
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Options;
|
||||
|
||||
//phpcs:disable Squiz.Classes.ClassFileName.NoMatch
|
||||
|
||||
/**
|
||||
* BlockTemplateUtils class used for serving block templates from Woo Blocks.
|
||||
* IMPORTANT: These methods have been duplicated from Gutenberg/lib/full-site-editing/block-templates.php as those functions are not for public usage.
|
||||
*
|
||||
* For internal use only by the Automattic\WooCommerce\Internal\Brands package.
|
||||
*
|
||||
* @version 9.4.0
|
||||
*/
|
||||
class BlockTemplateUtilsDuplicated {
|
||||
|
||||
/**
|
||||
* Directory names for block templates
|
||||
*
|
||||
* Directory names conventions for block templates have changed with Gutenberg 12.1.0,
|
||||
* however, for backwards-compatibility, we also keep the older conventions, prefixed
|
||||
* with `DEPRECATED_`.
|
||||
*
|
||||
* @var array {
|
||||
* @var string DEPRECATED_TEMPLATES Old directory name of the block templates directory.
|
||||
* @var string DEPRECATED_TEMPLATE_PARTS Old directory name of the block template parts directory.
|
||||
* @var string TEMPLATES_DIR_NAME Directory name of the block templates directory.
|
||||
* @var string TEMPLATE_PARTS_DIR_NAME Directory name of the block template parts directory.
|
||||
* }
|
||||
*/
|
||||
protected const DIRECTORY_NAMES = array(
|
||||
'DEPRECATED_TEMPLATES' => 'block-templates',
|
||||
'DEPRECATED_TEMPLATE_PARTS' => 'block-template-parts',
|
||||
'TEMPLATES' => 'templates',
|
||||
'TEMPLATE_PARTS' => 'parts',
|
||||
);
|
||||
|
||||
/**
|
||||
* WooCommerce plugin slug
|
||||
*
|
||||
* This is used to save templates to the DB which are stored against this value in the wp_terms table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected const PLUGIN_SLUG = 'woocommerce/woocommerce';
|
||||
|
||||
/**
|
||||
* Returns an array containing the references of
|
||||
* the passed blocks and their inner blocks.
|
||||
*
|
||||
* @param array $blocks array of blocks.
|
||||
*
|
||||
* @return array block references to the passed blocks and their inner blocks.
|
||||
*/
|
||||
public static function gutenberg_flatten_blocks( &$blocks ) {
|
||||
$all_blocks = array();
|
||||
$queue = array();
|
||||
foreach ( $blocks as &$block ) {
|
||||
$queue[] = &$block;
|
||||
}
|
||||
$queue_count = count( $queue );
|
||||
|
||||
while ( $queue_count > 0 ) {
|
||||
$block = &$queue[0];
|
||||
array_shift( $queue );
|
||||
$all_blocks[] = &$block;
|
||||
|
||||
if ( ! empty( $block['innerBlocks'] ) ) {
|
||||
foreach ( $block['innerBlocks'] as &$inner_block ) {
|
||||
$queue[] = &$inner_block;
|
||||
}
|
||||
}
|
||||
|
||||
$queue_count = count( $queue );
|
||||
}
|
||||
|
||||
return $all_blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses wp_template content and injects the current theme's
|
||||
* stylesheet as a theme attribute into each wp_template_part
|
||||
*
|
||||
* @param string $template_content serialized wp_template content.
|
||||
*
|
||||
* @return string Updated wp_template content.
|
||||
*/
|
||||
public static function gutenberg_inject_theme_attribute_in_content( $template_content ) {
|
||||
$has_updated_content = false;
|
||||
$new_content = '';
|
||||
$template_blocks = parse_blocks( $template_content );
|
||||
|
||||
$blocks = self::gutenberg_flatten_blocks( $template_blocks );
|
||||
foreach ( $blocks as &$block ) {
|
||||
if (
|
||||
'core/template-part' === $block['blockName'] &&
|
||||
! isset( $block['attrs']['theme'] )
|
||||
) {
|
||||
$block['attrs']['theme'] = wp_get_theme()->get_stylesheet();
|
||||
$has_updated_content = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $has_updated_content ) {
|
||||
foreach ( $template_blocks as &$block ) {
|
||||
$new_content .= serialize_block( $block );
|
||||
}
|
||||
|
||||
return $new_content;
|
||||
}
|
||||
|
||||
return $template_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a unified template object based a post Object.
|
||||
*
|
||||
* @param \WP_Post $post Template post.
|
||||
*
|
||||
* @return \WP_Block_Template|\WP_Error Template.
|
||||
*/
|
||||
public static function gutenberg_build_template_result_from_post( $post ) {
|
||||
$terms = get_the_terms( $post, 'wp_theme' );
|
||||
|
||||
if ( is_wp_error( $terms ) ) {
|
||||
return $terms;
|
||||
}
|
||||
|
||||
if ( ! $terms ) {
|
||||
return new \WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$theme = $terms[0]->name;
|
||||
$has_theme_file = true;
|
||||
|
||||
$template = new \WP_Block_Template();
|
||||
$template->wp_id = $post->ID;
|
||||
$template->id = $theme . '//' . $post->post_name;
|
||||
$template->theme = $theme;
|
||||
$template->content = $post->post_content;
|
||||
$template->slug = $post->post_name;
|
||||
$template->source = 'custom';
|
||||
$template->type = $post->post_type;
|
||||
$template->description = $post->post_excerpt;
|
||||
$template->title = $post->post_title;
|
||||
$template->status = $post->post_status;
|
||||
$template->has_theme_file = $has_theme_file;
|
||||
$template->is_custom = false;
|
||||
$template->post_types = array(); // Don't appear in any Edit Post template selector dropdown.
|
||||
|
||||
if ( 'wp_template_part' === $post->post_type ) {
|
||||
$type_terms = get_the_terms( $post, 'wp_template_part_area' );
|
||||
if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) {
|
||||
$template->area = $type_terms[0]->name;
|
||||
}
|
||||
}
|
||||
|
||||
// We are checking 'woocommerce' to maintain legacy templates which are saved to the DB,
|
||||
// prior to updating to use the correct slug.
|
||||
// More information found here: https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5423.
|
||||
if ( self::PLUGIN_SLUG === $theme || 'woocommerce' === strtolower( $theme ) ) {
|
||||
$template->origin = 'plugin';
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a unified template object based on a theme file.
|
||||
*
|
||||
* @param array|object $template_file Theme file.
|
||||
* @param string $template_type wp_template or wp_template_part.
|
||||
*
|
||||
* @return \WP_Block_Template Template.
|
||||
*/
|
||||
public static function gutenberg_build_template_result_from_file( $template_file, $template_type ) {
|
||||
$template_file = (object) $template_file;
|
||||
|
||||
// If the theme has an archive-products.html template but does not have product taxonomy templates
|
||||
// then we will load in the archive-product.html template from the theme to use for product taxonomies on the frontend.
|
||||
$template_is_from_theme = 'theme' === $template_file->source;
|
||||
$theme_name = wp_get_theme()->get( 'TextDomain' );
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$template_content = file_get_contents( $template_file->path );
|
||||
$template = new \WP_Block_Template();
|
||||
$template->id = $template_is_from_theme ? $theme_name . '//' . $template_file->slug : self::PLUGIN_SLUG . '//' . $template_file->slug;
|
||||
$template->theme = $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG;
|
||||
$template->content = self::gutenberg_inject_theme_attribute_in_content( $template_content );
|
||||
// Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909.
|
||||
$template->source = $template_file->source ? $template_file->source : 'plugin';
|
||||
$template->slug = $template_file->slug;
|
||||
$template->type = $template_type;
|
||||
$template->title = ! empty( $template_file->title ) ? $template_file->title : self::convert_slug_to_title( $template_file->slug );
|
||||
$template->status = 'publish';
|
||||
$template->has_theme_file = true;
|
||||
$template->origin = $template_file->source;
|
||||
$template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are.
|
||||
$template->post_types = array(); // Don't appear in any Edit Post template selector dropdown.
|
||||
$template->area = 'uncategorized';
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new template object so that we can make Woo Blocks default templates available in the current theme should they not have any.
|
||||
*
|
||||
* @param string $template_file Block template file path.
|
||||
* @param string $template_type wp_template or wp_template_part.
|
||||
* @param string $template_slug Block template slug e.g. single-product.
|
||||
* @param bool $template_is_from_theme If the block template file is being loaded from the current theme instead of Woo Blocks.
|
||||
*
|
||||
* @return object Block template object.
|
||||
*/
|
||||
public static function create_new_block_template_object( $template_file, $template_type, $template_slug, $template_is_from_theme = false ) {
|
||||
$theme_name = wp_get_theme()->get( 'TextDomain' );
|
||||
|
||||
$new_template_item = array(
|
||||
'slug' => $template_slug,
|
||||
'id' => $template_is_from_theme ? $theme_name . '//' . $template_slug : self::PLUGIN_SLUG . '//' . $template_slug,
|
||||
'path' => $template_file,
|
||||
'type' => $template_type,
|
||||
'theme' => $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG,
|
||||
// Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909.
|
||||
'source' => $template_is_from_theme ? 'theme' : 'plugin',
|
||||
'title' => self::convert_slug_to_title( $template_slug ),
|
||||
'description' => '',
|
||||
'post_types' => array(), // Don't appear in any Edit Post template selector dropdown.
|
||||
);
|
||||
|
||||
return (object) $new_template_item;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts template slugs into readable titles.
|
||||
*
|
||||
* @param string $template_slug The templates slug (e.g. single-product).
|
||||
* @return string Human friendly title converted from the slug.
|
||||
*/
|
||||
public static function convert_slug_to_title( $template_slug ) {
|
||||
switch ( $template_slug ) {
|
||||
case 'single-product':
|
||||
return __( 'Single Product', 'woocommerce' );
|
||||
case 'archive-product':
|
||||
return __( 'Product Archive', 'woocommerce' );
|
||||
case 'taxonomy-product_cat':
|
||||
return __( 'Product Category', 'woocommerce' );
|
||||
case 'taxonomy-product_tag':
|
||||
return __( 'Product Tag', 'woocommerce' );
|
||||
default:
|
||||
// Replace all hyphens and underscores with spaces.
|
||||
return ucwords( preg_replace( '/[\-_]/', ' ', $template_slug ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the first matching template part within themes directories
|
||||
*
|
||||
* Since [Gutenberg 12.1.0](https://github.com/WordPress/gutenberg/releases/tag/v12.1.0), the conventions for
|
||||
* block templates and parts directory has changed from `block-templates` and `block-templates-parts`
|
||||
* to `templates` and `parts` respectively.
|
||||
*
|
||||
* This function traverses all possible combinations of directory paths where a template or part
|
||||
* could be located and returns the first one which is readable, prioritizing the new convention
|
||||
* over the deprecated one, but maintaining that one for backwards compatibility.
|
||||
*
|
||||
* @param string $template_slug The slug of the template (i.e. without the file extension).
|
||||
* @param string $template_type Either `wp_template` or `wp_template_part`.
|
||||
*
|
||||
* @return string|null The matched path or `null` if no match was found.
|
||||
*/
|
||||
public static function get_theme_template_path( $template_slug, $template_type = 'wp_template' ) {
|
||||
$template_filename = $template_slug . '.html';
|
||||
$possible_templates_dir = 'wp_template' === $template_type ? array(
|
||||
self::DIRECTORY_NAMES['TEMPLATES'],
|
||||
self::DIRECTORY_NAMES['DEPRECATED_TEMPLATES'],
|
||||
) : array(
|
||||
self::DIRECTORY_NAMES['TEMPLATE_PARTS'],
|
||||
self::DIRECTORY_NAMES['DEPRECATED_TEMPLATE_PARTS'],
|
||||
);
|
||||
|
||||
// Combine the possible root directory names with either the template directory
|
||||
// or the stylesheet directory for child themes.
|
||||
$possible_paths = array_reduce(
|
||||
$possible_templates_dir,
|
||||
function ( $carry, $item ) use ( $template_filename ) {
|
||||
$filepath = DIRECTORY_SEPARATOR . $item . DIRECTORY_SEPARATOR . $template_filename;
|
||||
|
||||
$carry[] = get_template_directory() . $filepath;
|
||||
$carry[] = get_stylesheet_directory() . $filepath;
|
||||
|
||||
return $carry;
|
||||
},
|
||||
array()
|
||||
);
|
||||
|
||||
// Return the first matching.
|
||||
foreach ( $possible_paths as $path ) {
|
||||
if ( is_readable( $path ) ) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the theme has a template. So we know if to load our own in or not.
|
||||
*
|
||||
* @param string $template_name name of the template file without .html extension e.g. 'single-product'.
|
||||
* @return boolean
|
||||
*/
|
||||
public static function theme_has_template( $template_name ) {
|
||||
return (bool) self::get_theme_template_path( $template_name, 'wp_template' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the theme has a template. So we know if to load our own in or not.
|
||||
*
|
||||
* @param string $template_name name of the template file without .html extension e.g. 'single-product'.
|
||||
* @return boolean
|
||||
*/
|
||||
public static function theme_has_template_part( $template_name ) {
|
||||
return (bool) self::get_theme_template_path( $template_name, 'wp_template_part' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if they are using a compatible version of WP, or if not they have a compatible version of the Gutenberg plugin installed.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function supports_block_templates() {
|
||||
if (
|
||||
( ! function_exists( 'wp_is_block_theme' ) || ! wp_is_block_theme() ) &&
|
||||
( ! function_exists( 'gutenberg_supports_block_templates' ) || ! gutenberg_supports_block_templates() )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the blockified templates should be used or not.
|
||||
*
|
||||
* First, we need to make sure WordPress version is higher than 6.1 (lowest that supports Products block).
|
||||
* Then, if the option is not stored on the db, we need to check if the current theme is a block one or not.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function should_use_blockified_product_grid_templates() {
|
||||
$minimum_wp_version = '6.1';
|
||||
|
||||
if ( version_compare( $GLOBALS['wp_version'], $minimum_wp_version, '<' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$use_blockified_templates = wc_string_to_bool( get_option( Options::WC_BLOCK_USE_BLOCKIFIED_PRODUCT_GRID_BLOCK_AS_TEMPLATE ) );
|
||||
|
||||
if ( false === $use_blockified_templates ) {
|
||||
return function_exists( 'wc_current_theme_is_fse_theme' ) && wc_current_theme_is_fse_theme();
|
||||
}
|
||||
|
||||
return $use_blockified_templates;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
<?php
|
||||
|
||||
declare( strict_types = 1);
|
||||
|
||||
//phpcs:disable Squiz.Classes.ClassFileName.NoMatch, Squiz.Classes.ValidClassName.NotCamelCaps
|
||||
/**
|
||||
* Utils for compatibility with WooCommerce Full Site Editor Blocks
|
||||
*
|
||||
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
|
||||
*
|
||||
* @version 9.4.0
|
||||
*/
|
||||
class WC_Brands_Block_Templates {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'get_block_templates', array( $this, 'get_block_templates' ), 10, 3 );
|
||||
add_filter( 'get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
|
||||
add_filter( 'woocommerce_has_block_template', array( $this, 'has_block_template' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the taxonomy-product_brand template from DB in case a user customized it in FSE
|
||||
*
|
||||
* @return WP_Post|null The taxonomy-product_brand
|
||||
*/
|
||||
private function get_product_brand_template_db() {
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'name' => 'taxonomy-product_brand',
|
||||
'post_type' => 'wp_template',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 1,
|
||||
)
|
||||
);
|
||||
|
||||
if ( count( $posts ) ) {
|
||||
return $posts[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes a bug regarding taxonomies and FSE.
|
||||
* Without this, the system will always load archive-product.php version instead of taxonomy_product_brand.html
|
||||
* it will show a deprecation error if that happens.
|
||||
*
|
||||
* Triggered by woocommerce_has_block_template filter
|
||||
*
|
||||
* @param bool $has_template True if the template is available.
|
||||
* @param string $template_name The name of the template.
|
||||
*
|
||||
* @return bool True if the system is checking archive-product
|
||||
*/
|
||||
public function has_block_template( $has_template, $template_name ) {
|
||||
if ( 'archive-product' === $template_name || 'taxonomy-product_brand' === $template_name ) {
|
||||
$has_template = true;
|
||||
}
|
||||
|
||||
return $has_template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block template for Taxonomy Product Brand. First it attempts to load the last version from DB
|
||||
* Otherwise it loads the file based template.
|
||||
*
|
||||
* @param string $template_type The post_type for the template. Normally wp_template or wp_template_part.
|
||||
*
|
||||
* @return WP_Block_Template The taxonomy-product_brand template.
|
||||
*/
|
||||
private function get_product_brands_template( $template_type ) {
|
||||
$template_db = $this->get_product_brand_template_db();
|
||||
|
||||
if ( $template_db ) {
|
||||
return BlockTemplateUtilsDuplicated::gutenberg_build_template_result_from_post( $template_db );
|
||||
}
|
||||
|
||||
$template_path = BlockTemplateUtilsDuplicated::should_use_blockified_product_grid_templates()
|
||||
? WC()->plugin_path() . '/templates/templates/blockified/taxonomy-product_brand.html'
|
||||
: WC()->plugin_path() . '/templates/templates/taxonomy-product_brand.html';
|
||||
|
||||
$template_file = BlockTemplateUtilsDuplicated::create_new_block_template_object( $template_path, $template_type, 'taxonomy-product_brand', false );
|
||||
|
||||
return BlockTemplateUtilsDuplicated::gutenberg_build_template_result_from_file( $template_file, $template_type );
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to check if a template name is woocommerce/taxonomy-product_brand
|
||||
*
|
||||
* Notice depending on the version of WooCommerce this could be:
|
||||
*
|
||||
* woocommerce//taxonomy-product_brand
|
||||
* woocommerce/woocommerce//taxonomy-product_brand
|
||||
*
|
||||
* @param String $id The string to check if contains the template name.
|
||||
*
|
||||
* @return bool True if the template is woocommerce/taxonomy-product_brand
|
||||
*/
|
||||
private function is_taxonomy_product_brand_template( $id ) {
|
||||
return strpos( $id, 'woocommerce//taxonomy-product_brand' ) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block template for Taxonomy Product Brand if requested.
|
||||
* Triggered by get_block_file_template action
|
||||
*
|
||||
* @param WP_Block_Template|null $block_template The current Block Template loaded, if any.
|
||||
* @param string $id The template id normally in the format theme-slug//template-slug.
|
||||
* @param string $template_type The post_type for the template. Normally wp_template or wp_template_part.
|
||||
*
|
||||
* @return WP_Block_Template|null The taxonomy-product_brand template.
|
||||
*/
|
||||
public function get_block_file_template( $block_template, $id, $template_type ) {
|
||||
if ( $this->is_taxonomy_product_brand_template( $id ) && is_null( $block_template ) ) {
|
||||
$block_template = $this->get_product_brands_template( $template_type );
|
||||
}
|
||||
|
||||
return $block_template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the Block template in the template query results needed by FSE
|
||||
* Triggered by get_block_templates action
|
||||
*
|
||||
* @param array $query_result The list of templates to render in the query.
|
||||
* @param array $query The current query parameters.
|
||||
* @param string $template_type The post_type for the template. Normally wp_template or wp_template_part.
|
||||
*
|
||||
* @return WP_Block_Template[] Array of the matched Block Templates to render.
|
||||
*/
|
||||
public function get_block_templates( $query_result, $query, $template_type ) {
|
||||
// We don't want to run this if we are looking for template-parts. Like the header.
|
||||
if ( 'wp_template' !== $template_type ) {
|
||||
return $query_result;
|
||||
}
|
||||
|
||||
$post_id = isset( $_REQUEST['postId'] ) ? wc_clean( wp_unslash( $_REQUEST['postId'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$slugs = $query['slug__in'] ?? array();
|
||||
|
||||
// Only add the template if asking for Product Brands.
|
||||
if (
|
||||
in_array( 'taxonomy-product_brand', $slugs, true ) ||
|
||||
( ! $post_id && ! count( $slugs ) ) ||
|
||||
( ! count( $slugs ) && $this->is_taxonomy_product_brand_template( $post_id ) )
|
||||
) {
|
||||
$query_result[] = $this->get_product_brands_template( $template_type );
|
||||
}
|
||||
|
||||
return $query_result;
|
||||
}
|
||||
}
|
||||
|
||||
new WC_Brands_Block_Templates();
|
|
@ -77,6 +77,11 @@ class WC_Autoloader {
|
|||
return;
|
||||
}
|
||||
|
||||
// If the class is already loaded from a merged package, prevent autoloader from loading it as well.
|
||||
if ( \Automattic\WooCommerce\Packages::should_load_class( $class ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$file = $this->get_file_name_from_class( $class );
|
||||
$path = '';
|
||||
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
//phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
|
||||
|
||||
declare( strict_types = 1);
|
||||
|
||||
/**
|
||||
* Brand settings manager.
|
||||
*
|
||||
* This class is responsible for setting and getting brand settings for a coupon.
|
||||
*
|
||||
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
|
||||
*
|
||||
* @version 9.4.0
|
||||
*/
|
||||
class WC_Brands_Brand_Settings_Manager {
|
||||
/**
|
||||
* Brand settings for a coupon.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $brand_settings = array();
|
||||
|
||||
/**
|
||||
* Set brand settings for a coupon.
|
||||
*
|
||||
* @param WC_Coupon $coupon Coupon object.
|
||||
*/
|
||||
public static function set_brand_settings_on_coupon( $coupon ) {
|
||||
$coupon_id = $coupon->get_id();
|
||||
|
||||
// Check if the brand settings are already set for this coupon.
|
||||
if ( isset( self::$brand_settings[ $coupon_id ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$included_brands = get_post_meta( $coupon_id, 'product_brands', true );
|
||||
$included_brands = ! empty( $included_brands ) ? $included_brands : array();
|
||||
|
||||
$excluded_brands = get_post_meta( $coupon_id, 'exclude_product_brands', true );
|
||||
$excluded_brands = ! empty( $excluded_brands ) ? $excluded_brands : array();
|
||||
|
||||
// Store these settings in the static array.
|
||||
self::$brand_settings[ $coupon_id ] = array(
|
||||
'included_brands' => $included_brands,
|
||||
'excluded_brands' => $excluded_brands,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get brand settings for a coupon.
|
||||
*
|
||||
* @param WC_Coupon $coupon Coupon object.
|
||||
* @return array Brand settings (included and excluded brands).
|
||||
*/
|
||||
public static function get_brand_settings_on_coupon( $coupon ) {
|
||||
$coupon_id = $coupon->get_id();
|
||||
|
||||
if ( isset( self::$brand_settings[ $coupon_id ] ) ) {
|
||||
return self::$brand_settings[ $coupon_id ];
|
||||
}
|
||||
|
||||
// Default return value if no settings are found.
|
||||
return array(
|
||||
'included_brands' => array(),
|
||||
'excluded_brands' => array(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
|
||||
declare( strict_types = 1);
|
||||
|
||||
/**
|
||||
* WC_Brands_Coupons class.
|
||||
*
|
||||
* Important: For internal use only by the Automattic\WooCommerce\Internal\Brands package.
|
||||
*
|
||||
* @version 9.4.0
|
||||
*/
|
||||
class WC_Brands_Coupons {
|
||||
|
||||
const E_WC_COUPON_EXCLUDED_BRANDS = 301;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
// Coupon validation and error handling.
|
||||
add_filter( 'woocommerce_coupon_is_valid', array( $this, 'is_coupon_valid' ), 10, 3 );
|
||||
add_filter( 'woocommerce_coupon_is_valid_for_product', array( $this, 'is_valid_for_product' ), 10, 3 );
|
||||
add_filter( 'woocommerce_coupon_error', array( $this, 'brand_exclusion_error' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the coupon based on included and/or excluded product brands.
|
||||
*
|
||||
* If one of the following conditions are met, an exception will be thrown and
|
||||
* displayed as an error notice on the cart page:
|
||||
*
|
||||
* 1) Coupon has a brand requirement but no products in the cart have the brand.
|
||||
* 2) All products in the cart match the brand exclusion rule.
|
||||
* 3) For a cart discount, there is at least one product in cart that matches exclusion rule.
|
||||
*
|
||||
* @throws Exception Throws Exception for invalid coupons.
|
||||
* @param bool $valid Whether the coupon is valid.
|
||||
* @param WC_Coupon $coupon Coupon object.
|
||||
* @param WC_Discounts $discounts Discounts object.
|
||||
* @return bool $valid True if coupon is valid, otherwise Exception will be thrown.
|
||||
*/
|
||||
public function is_coupon_valid( $valid, $coupon, $discounts = null ) {
|
||||
$this->set_brand_settings_on_coupon( $coupon );
|
||||
|
||||
// Only check if coupon has brand restrictions on it.
|
||||
$brand_coupon_settings = WC_Brands_Brand_Settings_Manager::get_brand_settings_on_coupon( $coupon );
|
||||
|
||||
$brand_restrictions = ! empty( $brand_coupon_settings['included_brands'] ) || ! empty( $brand_coupon_settings['excluded_brands'] );
|
||||
if ( ! $brand_restrictions ) {
|
||||
return $valid;
|
||||
}
|
||||
|
||||
$included_brands_match = false;
|
||||
$excluded_brands_matches = 0;
|
||||
|
||||
$items = $discounts->get_items();
|
||||
|
||||
foreach ( $items as $item ) {
|
||||
$product_brands = $this->get_product_brands( $this->get_product_id( $item->product ) );
|
||||
|
||||
if ( ! empty( array_intersect( $product_brands, $brand_coupon_settings['included_brands'] ) ) ) {
|
||||
$included_brands_match = true;
|
||||
}
|
||||
|
||||
if ( ! empty( array_intersect( $product_brands, $brand_coupon_settings['excluded_brands'] ) ) ) {
|
||||
++$excluded_brands_matches;
|
||||
}
|
||||
}
|
||||
|
||||
// 1) Coupon has a brand requirement but no products in the cart have the brand.
|
||||
if ( ! $included_brands_match && ! empty( $brand_coupon_settings['included_brands'] ) ) {
|
||||
throw new Exception( WC_Coupon::E_WC_COUPON_NOT_APPLICABLE ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
// 2) All products in the cart match brand exclusion rule.
|
||||
if ( count( $items ) === $excluded_brands_matches ) {
|
||||
throw new Exception( self::E_WC_COUPON_EXCLUDED_BRANDS ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
// 3) For a cart discount, there is at least one product in cart that matches exclusion rule.
|
||||
if ( $coupon->is_type( 'fixed_cart' ) && $excluded_brands_matches > 0 ) {
|
||||
throw new Exception( self::E_WC_COUPON_EXCLUDED_BRANDS ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
return $valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a coupon is valid for a product.
|
||||
*
|
||||
* This allows percentage and product discounts to apply to only
|
||||
* the correct products in the cart.
|
||||
*
|
||||
* @param bool $valid Whether the product should get the coupon's discounts.
|
||||
* @param WC_Product $product WC Product Object.
|
||||
* @param WC_Coupon $coupon Coupon object.
|
||||
* @return bool $valid
|
||||
*/
|
||||
public function is_valid_for_product( $valid, $product, $coupon ) {
|
||||
|
||||
if ( ! is_a( $product, 'WC_Product' ) ) {
|
||||
return $valid;
|
||||
}
|
||||
$this->set_brand_settings_on_coupon( $coupon );
|
||||
|
||||
$product_id = $this->get_product_id( $product );
|
||||
$product_brands = $this->get_product_brands( $product_id );
|
||||
|
||||
// Check if coupon has a brand requirement and if this product has that brand attached.
|
||||
$brand_coupon_settings = WC_Brands_Brand_Settings_Manager::get_brand_settings_on_coupon( $coupon );
|
||||
if ( ! empty( $brand_coupon_settings['included_brands'] ) && empty( array_intersect( $product_brands, $brand_coupon_settings['included_brands'] ) ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if coupon has a brand exclusion and if this product has that brand attached.
|
||||
if ( ! empty( $brand_coupon_settings['excluded_brands'] ) && ! empty( array_intersect( $product_brands, $brand_coupon_settings['excluded_brands'] ) ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a custom error message when a cart discount coupon does not validate
|
||||
* because an excluded brand was found in the cart.
|
||||
*
|
||||
* @param string $err The error message.
|
||||
* @param string $err_code The error code.
|
||||
* @return string
|
||||
*/
|
||||
public function brand_exclusion_error( $err, $err_code ) {
|
||||
if ( self::E_WC_COUPON_EXCLUDED_BRANDS !== $err_code ) {
|
||||
return $err;
|
||||
}
|
||||
|
||||
return __( 'Sorry, this coupon is not applicable to the brands of selected products.', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of brands that are assigned to a specific product
|
||||
*
|
||||
* @param int $product_id Product id.
|
||||
* @return array brands
|
||||
*/
|
||||
private function get_product_brands( $product_id ) {
|
||||
return wp_get_post_terms( $product_id, 'product_brand', array( 'fields' => 'ids' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set brand settings as properties on coupon object. These properties are
|
||||
* lists of included product brand IDs and list of excluded brand IDs.
|
||||
*
|
||||
* @param WC_Coupon $coupon Coupon object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function set_brand_settings_on_coupon( $coupon ) {
|
||||
$brand_coupon_settings = WC_Brands_Brand_Settings_Manager::get_brand_settings_on_coupon( $coupon );
|
||||
|
||||
if ( ! empty( $brand_coupon_settings['included_brands'] ) && ! empty( $brand_coupon_settings['excluded_brands'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$included_brands = get_post_meta( $coupon->get_id(), 'product_brands', true );
|
||||
if ( empty( $included_brands ) ) {
|
||||
$included_brands = array();
|
||||
}
|
||||
|
||||
$excluded_brands = get_post_meta( $coupon->get_id(), 'exclude_product_brands', true );
|
||||
if ( empty( $excluded_brands ) ) {
|
||||
$excluded_brands = array();
|
||||
}
|
||||
|
||||
// Store these for later to avoid multiple look-ups.
|
||||
WC_Brands_Brand_Settings_Manager::set_brand_settings_on_coupon( $coupon );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the product (or variant) ID.
|
||||
*
|
||||
* @param WC_Product $product WC Product Object.
|
||||
* @return int Product ID
|
||||
*/
|
||||
private function get_product_id( $product ) {
|
||||
return $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id();
|
||||
}
|
||||
}
|
||||
|
||||
new WC_Brands_Coupons();
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue