Merge branch 'trunk' into try/add-settings-refresh-next

This commit is contained in:
Paul Sealock 2024-08-01 12:20:11 +12:00 committed by GitHub
commit 98782a5f4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1424 changed files with 22762 additions and 15359 deletions

View File

@ -0,0 +1,7 @@
# Report Flaky Tests
A GitHub action to report flaky E2E tests to GitHub issues.
**This package is still experimental and breaking changes could be introduced in future minor versions (`v0.x`). Use it at your own risks.**
<br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p>

View File

@ -0,0 +1,17 @@
name: 'Report flaky tests'
description: 'Report flaky tests to GitHub issues'
inputs:
repo-token:
description: 'GitHub token'
required: true
label:
description: 'The flaky-test label name'
required: true
default: 'flaky-test'
artifact-path:
description: 'The path of the downloaded artifact'
required: true
default: 'flaky-tests'
runs:
using: 'node20'
main: 'dist/index.js'

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,41 @@
/*!
* fill-range <https://github.com/jonschlinkert/fill-range>
*
* Copyright (c) 2014-present, Jon Schlinkert.
* Licensed under the MIT License.
*/
/*!
* is-number <https://github.com/jonschlinkert/is-number>
*
* Copyright (c) 2014-present, Jon Schlinkert.
* Released under the MIT License.
*/
/*!
* is-plain-object <https://github.com/jonschlinkert/is-plain-object>
*
* Copyright (c) 2014-2017, Jon Schlinkert.
* Released under the MIT License.
*/
/*!
* to-regex-range <https://github.com/micromatch/to-regex-range>
*
* Copyright (c) 2015-present, Jon Schlinkert.
* Released under the MIT License.
*/
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
/*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */
/**
* @license React
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

View File

@ -16,6 +16,9 @@ inputs:
pull-playwright-cache: pull-playwright-cache:
description: 'Given a boolean value, invokes Playwright dependencies caching.' description: 'Given a boolean value, invokes Playwright dependencies caching.'
default: false default: false
pull-package-deps:
description: 'Given a string value, will pull the package specific dependencies cache.'
default: false
runs: runs:
using: 'composite' using: 'composite'
steps: steps:
@ -31,27 +34,46 @@ runs:
uses: 'actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65' uses: 'actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65'
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
# We only want to use the cache if something is being installed. # The built-in caching is not fit to per-package caching we are aiming.
cache: ${{ inputs.install != 'false' && 'pnpm' || '' }} cache: ''
- name: 'Setup PHP' - name: 'Setup PHP'
if: ${{ inputs.php-version != 'false' }} if: ${{ inputs.php-version != 'false' }}
uses: 'shivammathur/setup-php@a36e1e52ff4a1c9e9c9be31551ee4712a6cb6bd0' uses: 'shivammathur/setup-php@a36e1e52ff4a1c9e9c9be31551ee4712a6cb6bd0'
with: with:
php-version: '${{ inputs.php-version }}' php-version: '${{ inputs.php-version }}'
coverage: 'none' coverage: 'none'
- name: 'Cache: identify pnpm caching directory'
if: ${{ inputs.pull-package-deps != 'false' }}
shell: 'bash'
run: |
echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: 'Cache: pnpm downloads'
if: ${{ inputs.pull-package-deps != 'false' }}
uses: 'actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319'
with:
path: "${{ env.PNPM_STORE_PATH }}"
key: "${{ runner.os }}-pnpm-${{ inputs.pull-package-deps }}-build:${{ inputs.build-type }}-${{ hashFiles( 'pnpm-lock.yaml' ) }}"
restore-keys: '${{ runner.os }}-pnpm-${{ inputs.pull-package-deps }}-build:${{ inputs.build-type }}-'
- name: 'Cache: node cache'
if: ${{ inputs.pull-package-deps != 'false' }}
uses: 'actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319'
with:
path: './node_modules/.cache'
key: "${{ runner.os }}-node-cache-${{ inputs.pull-package-deps }}-${{ hashFiles( 'pnpm-lock.yaml' ) }}"
restore-keys: '${{ runner.os }}-node-cache-${{ inputs.pull-package-deps }}-'
- name: 'Cache Composer Dependencies' - name: 'Cache Composer Dependencies'
if: ${{ inputs.build == 'false' }} if: ${{ inputs.pull-package-deps != 'false' }}
uses: 'actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319' uses: 'actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319'
with: with:
path: '~/.cache/composer/files' path: '~/.cache/composer/files'
key: "${{ runner.os }}-composer-${{ hashFiles( '**/composer.lock' ) }}" key: "${{ runner.os }}-composer-${{ inputs.pull-package-deps }}-${{ hashFiles( 'packages/*/*/composer.lock', 'plugins/*/composer.lock' ) }}"
restore-keys: '${{ runner.os }}-composer-' restore-keys: '${{ runner.os }}-composer-${{ inputs.pull-package-deps }}-'
- name: 'Cache: playwright downloads' - name: 'Cache: playwright downloads'
if: ${{ inputs.pull-playwright-cache != 'false' }} if: ${{ inputs.pull-playwright-cache != 'false' }}
uses: 'actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319' uses: 'actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319'
with: with:
path: '~/.cache/ms-playwright/' path: '~/.cache/ms-playwright/'
key: "${{ runner.os }}-playwright-${{ hashFiles( '**/pnpm-lock.yaml' ) }}" key: "${{ runner.os }}-playwright-${{ hashFiles( 'pnpm-lock.yaml' ) }}"
restore-keys: '${{ runner.os }}-playwright-' restore-keys: '${{ runner.os }}-playwright-'
- name: 'Parse Project Filters' - name: 'Parse Project Filters'
id: 'project-filters' id: 'project-filters'
@ -63,9 +85,16 @@ runs:
# Boolean inputs aren't parsed into filters so it'll either be "true" or there will be a filter. # Boolean inputs aren't parsed into filters so it'll either be "true" or there will be a filter.
if: ${{ inputs.install == 'true' || steps.project-filters.outputs.install != '' }} if: ${{ inputs.install == 'true' || steps.project-filters.outputs.install != '' }}
shell: 'bash' shell: 'bash'
run: 'pnpm install' # The installation command is a bit odd as it's a workaround for know bug - https://github.com/pnpm/pnpm/issues/6300.
# `pnpm install` filtering is broken: https://github.com/pnpm/pnpm/issues/6300 run: |
# run: 'pnpm install ${{ steps.project-filters.outputs.install }}' if [[ '${{ inputs.install }}' == '@woocommerce/plugin-woocommerce...' && '${{ inputs.build-type }}' == 'backend' ]]; then
# PHPUnit/REST testing optimized installation of the deps: minimalistic and parallellized between PHP/JS.
# JS deps installation is abit hard-core, but all we need actually is wp-env and playwright - we are good at that regard.
composer install --working-dir=./plugins/woocommerce --quiet &
pnpm install --filter='@woocommerce/plugin-woocommerce' --frozen-lockfile --config.dedupe-peer-dependents=false --ignore-scripts
else
pnpm install ${{ steps.project-filters.outputs.install }} --frozen-lockfile ${{ steps.project-filters.outputs.install != '' && '--config.dedupe-peer-dependents=false' || '' }}
fi
# We want to include an option to build projects using this action so that we can make # We want to include an option to build projects using this action so that we can make
# sure that the build cache is always used when building projects. # sure that the build cache is always used when building projects.
- name: 'Cache Build Output' - name: 'Cache Build Output'
@ -75,6 +104,8 @@ runs:
- name: 'Build' - name: 'Build'
# Boolean inputs aren't parsed into filters so it'll either be "true" or there will be a filter. # Boolean inputs aren't parsed into filters so it'll either be "true" or there will be a filter.
if: ${{ inputs.build == 'true' || steps.project-filters.outputs.build != '' }} if: ${{ inputs.build == 'true' || steps.project-filters.outputs.build != '' }}
env:
BROWSERSLIST_IGNORE_OLD_DATA: true
shell: 'bash' shell: 'bash'
run: | run: |
if [[ '${{ inputs.build-type }}' == 'backend' ]]; then if [[ '${{ inputs.build-type }}' == 'backend' ]]; then

View File

@ -12,4 +12,4 @@ jobs:
uses: acq688/Request-Reviewer-For-Team-Action@v1.1 uses: acq688/Request-Reviewer-For-Team-Action@v1.1
with: with:
config: '.github/automate-team-review-assignment-config.yml' config: '.github/automate-team-review-assignment-config.yml'
GITHUB_TOKEN: ${{ secrets.FINE_GRAINED_TOKEN_ACTIONS }} GITHUB_TOKEN: ${{ secrets.PR_ASSIGN_TOKEN }}

View File

@ -30,12 +30,8 @@ jobs:
- name: Setup WooCommerce Monorepo - name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo uses: ./.github/actions/setup-woocommerce-monorepo
- name: Cache PNPM Dependencies
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65
with: with:
node-version-file: .nvmrc pull-package-deps: '@woocommerce/plugin-woocommerce'
cache: pnpm
- name: Prepare plugin zips - name: Prepare plugin zips
id: prepare id: prepare

View File

@ -20,6 +20,8 @@ jobs:
- name: Setup WooCommerce Monorepo - name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo uses: ./.github/actions/setup-woocommerce-monorepo
with:
pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Build zip - name: Build zip
working-directory: plugins/woocommerce working-directory: plugins/woocommerce

View File

@ -14,48 +14,15 @@ on:
required: true required: true
default: 'default' default: 'default'
type: string type: string
workflow_dispatch:
inputs:
pr_simulate:
description: 'Would you like to run CI on a pull request? If so, enter the PR number here. If blank, the entire suite will be run.'
type: string
default: ''
concurrency: concurrency:
group: '${{ github.workflow }}-${{ github.ref }}' group: '${{ github.workflow }}-${{ github.ref }}-${{ inputs.trigger }}'
cancel-in-progress: true cancel-in-progress: true
env: env:
FORCE_COLOR: 1 FORCE_COLOR: 1
jobs: jobs:
dispatch-handler:
name: 'Handle dispatched workflow'
runs-on: 'ubuntu-20.04'
if: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_simulate }}
outputs:
head: ${{ steps.pr-info.outputs.head }}
base: ${{ steps.pr-info.outputs.base }}
steps:
- uses: actions/github-script@v7
name: 'Grab PR info.'
id: 'pr-info'
env:
PR: ${{ inputs.pr_simulate }}
with:
retries: 3
script: |
if ( ! process.env.PR ) {
return;
}
const PR = await github.rest.pulls.get( {
pull_number: process.env.PR,
repo: context.repo.repo,
owner: context.repo.owner,
} );
core.setOutput( 'head', PR.data.head.ref );
core.setOutput( 'base', PR.data.base.ref );
project-jobs: project-jobs:
# Since this is a monorepo, not every pull request or change is going to impact every project. # Since this is a monorepo, not every pull request or change is going to impact every project.
# Instead of running CI tasks on all projects indiscriminately, we use a command to detect # Instead of running CI tasks on all projects indiscriminately, we use a command to detect
@ -63,12 +30,6 @@ jobs:
# matrices that we can use to run CI tasks only on the projects that need them. # matrices that we can use to run CI tasks only on the projects that need them.
name: 'Build Project Jobs' name: 'Build Project Jobs'
runs-on: 'ubuntu-20.04' runs-on: 'ubuntu-20.04'
needs: 'dispatch-handler'
# Because forks of this repository may want to skip running this CI automatically, but still
# be able to run it via workflow_dispatch, if the SKIP_CI variable is truthy, and we're not
# running from a workflow_dispatch, we'll skip generating the project matrix and any jobs.
# Because dispatch-handler may be skipped, we need the always() here.
if: ${{ always() && ( github.event_name == 'workflow_dispatch' || ! vars.SKIP_CI ) }}
outputs: outputs:
lint-jobs: ${{ steps.project-jobs.outputs.lint-jobs }} lint-jobs: ${{ steps.project-jobs.outputs.lint-jobs }}
test-jobs: ${{ steps.project-jobs.outputs.test-jobs }} test-jobs: ${{ steps.project-jobs.outputs.test-jobs }}
@ -78,24 +39,19 @@ jobs:
name: 'Checkout' name: 'Checkout'
with: with:
fetch-depth: 0 fetch-depth: 0
# If the workflow wasn't triggered by dispatch, this will be empty and use defaults.
ref: ${{ needs.dispatch-handler.outputs.head }}
- uses: './.github/actions/setup-woocommerce-monorepo' - uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Setup Monorepo' name: 'Setup Monorepo'
with: with:
php-version: false # We don't want to waste time installing PHP since we aren't using it in this job. php-version: false # We don't want to waste time installing PHP since we aren't using it in this job.
- uses: actions/github-script@v7 - uses: actions/github-script@v7
name: 'Build Matrix' name: 'Build Matrix'
id: 'project-jobs' id: 'project-jobs'
env:
PR_SIM: ${{ needs.dispatch-handler.outputs.base }}
with: with:
script: | script: |
const prSim = process.env.PR_SIM; // Intended behaviour of the jobs generation:
// - PRs: run CI jobs aiming PRs and filter out jobs based on the content changes
let baseRef = prSim || ${{ toJson( github.base_ref ) }}; // - Pushes: run CI jobs aiming pushes without filtering based on the content changes
let baseRef = ${{ toJson( github.base_ref ) }};
if ( baseRef ) { if ( baseRef ) {
baseRef = `--base-ref origin/${ baseRef }`; baseRef = `--base-ref origin/${ baseRef }`;
} }
@ -118,41 +74,31 @@ jobs:
githubEvent = trigger; githubEvent = trigger;
} }
// Override the event 'workflow_dispatch' event type if we're simulating a PR.
if ( prSim ) {
githubEvent = 'pull_request';
}
const child_process = require( 'node:child_process' ); const child_process = require( 'node:child_process' );
child_process.execSync( `pnpm utils ci-jobs ${ baseRef } --event ${ githubEvent }` ); child_process.execSync( `pnpm utils ci-jobs ${ baseRef } --event ${ githubEvent }` );
project-lint-jobs: project-lint-jobs:
name: "Lint - ${{ matrix.projectName }} ${{ matrix.optional && ' (optional)' || ''}}" name: "Lint - ${{ matrix.projectName }} ${{ matrix.optional && ' (optional)' || ''}}"
runs-on: 'ubuntu-20.04' runs-on: 'ubuntu-20.04'
needs: [ needs: 'project-jobs'
'project-jobs', if: ${{ needs.project-jobs.outputs.lint-jobs != '[]' && github.event_name == 'pull_request' }}
'dispatch-handler'
]
# Because dispatch-handler may be skipped, we need the always() here.
if: ${{ always() && needs.project-jobs.outputs.lint-jobs != '[]' && ( github.event_name == 'pull_request' || inputs.pr_simulate != '' ) }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: ${{ fromJSON( needs.project-jobs.outputs.lint-jobs ) }} include: ${{ fromJSON( needs.project-jobs.outputs.lint-jobs ) }}
steps: steps:
- uses: 'actions/checkout@v4' - uses: 'actions/checkout@v4'
name: 'Checkout' name: 'Checkout'
with: with:
fetch-depth: 0 # the WooCommerce plugin package uses phpcs-changed for linting, which requires non-shallow git-history.
# If the workflow wasn't triggered by dispatch, this will be empty and use defaults. fetch-depth: ${{ ( ( matrix.projectName == '@woocommerce/plugin-woocommerce' && '0' ) || '1' ) }}
ref: ${{ needs.dispatch-handler.outputs.head }}
- uses: './.github/actions/setup-woocommerce-monorepo' - uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Setup Monorepo' name: 'Setup Monorepo'
id: 'setup-monorepo' id: 'setup-monorepo'
with: with:
install: '${{ matrix.projectName }}...' install: '${{ matrix.projectName }}...'
pull-package-deps: '${{ matrix.projectName }}'
- name: 'Lint' - name: 'Lint'
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }}' run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }}'
@ -160,23 +106,16 @@ jobs:
project-test-jobs: project-test-jobs:
name: "${{ matrix.name }}" name: "${{ matrix.name }}"
runs-on: 'ubuntu-20.04' runs-on: 'ubuntu-20.04'
needs: [ needs: 'project-jobs'
'project-jobs', if: ${{ needs.project-jobs.outputs.test-jobs != '[]' }}
'dispatch-handler'
]
if: ${{ always() && needs.project-jobs.outputs.test-jobs != '[]' }}
env: ${{ matrix.testEnv.envVars }} env: ${{ matrix.testEnv.envVars }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: ${{ fromJSON( needs.project-jobs.outputs.test-jobs ) }} include: ${{ fromJSON( needs.project-jobs.outputs.test-jobs ) }}
steps: steps:
- uses: 'actions/checkout@v4' - uses: 'actions/checkout@v4'
name: 'Checkout' name: 'Checkout'
with:
# If the workflow wasn't triggered by dispatch, this will be empty and use defaults.
ref: ${{ needs.dispatch-handler.outputs.head }}
- uses: './.github/actions/setup-woocommerce-monorepo' - uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Install Monorepo' name: 'Install Monorepo'
@ -186,14 +125,16 @@ jobs:
build: ${{ ( github.ref_type == 'tag' && 'false' ) || matrix.projectName }} build: ${{ ( github.ref_type == 'tag' && 'false' ) || matrix.projectName }}
build-type: ${{ ( matrix.testType == 'unit:php' && 'backend' ) || 'full' }} build-type: ${{ ( matrix.testType == 'unit:php' && 'backend' ) || 'full' }}
pull-playwright-cache: ${{ matrix.testEnv.shouldCreate && matrix.testType == 'e2e' }} pull-playwright-cache: ${{ matrix.testEnv.shouldCreate && matrix.testType == 'e2e' }}
pull-package-deps: '${{ matrix.projectName }}'
- name: 'Update wp-env config' - name: 'Update wp-env config'
if: ${{ github.ref_type == 'tag' }} if: ${{ github.ref_type == 'tag' }}
env: env:
RELEASE_TAG: ${{ github.ref_name }} RELEASE_TAG: ${{ github.ref_name }}
ARTIFACT_NAME: ${{ github.ref_name == 'nightly' && 'woocommerce-trunk-nightly.zip' || 'woocommerce.zip' }} ARTIFACT_NAME: ${{ github.ref_name == 'nightly' && 'woocommerce-trunk-nightly.zip' || 'woocommerce.zip' }}
working-directory: ${{ matrix.projectPath }} # band-aid to get the path to wp-env.json for blocks e2e tests, until they're migrated to plugins/woocommerce
run: node ./tests/e2e-pw/bin/override-wp-env-plugins.js WP_ENV_CONFIG_PATH: ${{ github.workspace }}/${{ matrix.testEnv.start == 'env:start:blocks' && 'plugins/woocommerce-blocks' || matrix.projectPath }}
run: node .github/workflows/scripts/override-wp-env-plugins.js
- name: 'Start Test Environment' - name: 'Start Test Environment'
id: 'prepare-test-environment' id: 'prepare-test-environment'
@ -270,7 +211,7 @@ jobs:
'project-lint-jobs', 'project-lint-jobs',
'project-test-jobs', 'project-test-jobs',
] ]
if: ${{ always() && github.event_name == 'pull_request' }} if: ${{ !cancelled() && github.event_name == 'pull_request' }}
steps: steps:
- uses: 'actions/checkout@v4' - uses: 'actions/checkout@v4'
name: 'Checkout' name: 'Checkout'
@ -299,7 +240,7 @@ jobs:
'project-lint-jobs', 'project-lint-jobs',
'project-test-jobs', 'project-test-jobs',
] ]
if: ${{ always() && github.event_name != 'pull_request' && ! github.event.pull_request.head.repo.fork }} if: ${{ !cancelled() && github.event_name != 'pull_request' && github.repository == 'woocommerce/woocommerce' }}
steps: steps:
- uses: 'actions/checkout@v4' - uses: 'actions/checkout@v4'
name: 'Checkout' name: 'Checkout'
@ -336,7 +277,7 @@ jobs:
'project-jobs', 'project-jobs',
'project-test-jobs', 'project-test-jobs',
] ]
if: ${{ always() && needs.project-jobs.outputs.report-jobs != '[]' && ! github.event.pull_request.head.repo.fork }} if: ${{ !cancelled() && needs.project-jobs.outputs.report-jobs != '[]' && github.repository == 'woocommerce/woocommerce' }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -393,45 +334,39 @@ jobs:
report-flaky-tests: report-flaky-tests:
name: 'Create issues for flaky tests' name: 'Create issues for flaky tests'
if: ${{ !cancelled() && ! github.event.pull_request.head.repo.fork }} if: ${{ !cancelled() && github.repository == 'woocommerce/woocommerce' && needs.project-jobs.outputs.test-jobs != '[]' }}
needs: [ 'project-test-jobs' ] needs:
[
'project-jobs',
'project-test-jobs',
]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
issues: write issues: write
steps: steps:
- uses: actions/checkout@v4 - uses: 'actions/checkout@v4'
with: name: 'Checkout'
repository: WordPress/gutenberg
ref: dbf201449e9736f672b61e422787d47659db327a
- uses: actions/download-artifact@v4 - uses: 'actions/download-artifact@v4'
id: download-artifact name: 'Download artifacts'
with: with:
pattern: flaky-tests* pattern: flaky-tests*
path: flaky-tests path: flaky-tests
merge-multiple: true merge-multiple: true
- name: 'Check if there are flaky tests reports' - name: 'Merge flaky tests reports'
run: | run: |
downloadPath='${{ steps.download-artifact.outputs.download-path }}' downloadPath='${{ steps.download-artifact.outputs.download-path || './flaky-tests' }}'
# make dir so that next step doesn't fail if it doesn't exist # make dir so that next step doesn't fail if it doesn't exist
mkdir -p $downloadPath mkdir -p $downloadPath
# any output means there are reports # any output means there are reports
echo "FLAKY_REPORTS=$(ls -A $downloadPath | head -1)" >> $GITHUB_ENV echo "FLAKY_REPORTS=$(ls -A $downloadPath | head -1)" >> $GITHUB_ENV
- name: 'Setup'
if: ${{ !!env.FLAKY_REPORTS }}
uses: ./.github/setup-node
- name: 'Build packages'
if: ${{ !!env.FLAKY_REPORTS }}
run: npm run build:packages
- name: 'Report flaky tests' - name: 'Report flaky tests'
if: ${{ !!env.FLAKY_REPORTS }} if: ${{ !!env.FLAKY_REPORTS }}
uses: ./packages/report-flaky-tests uses: './.github/actions/report-flaky-tests'
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
label: 'metric: flaky e2e test' label: 'metric: flaky e2e test'

View File

@ -52,6 +52,7 @@ jobs:
install: '@woocommerce/plugin-woocommerce...' install: '@woocommerce/plugin-woocommerce...'
build: '@woocommerce/plugin-woocommerce' build: '@woocommerce/plugin-woocommerce'
pull-playwright-cache: true pull-playwright-cache: true
pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Install Playwright dependencies - name: Install Playwright dependencies
run: pnpm exec playwright install chromium --with-deps run: pnpm exec playwright install chromium --with-deps

View File

@ -18,6 +18,8 @@ jobs:
- name: Setup WooCommerce Monorepo - name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo uses: ./.github/actions/setup-woocommerce-monorepo
with:
pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Build zip - name: Build zip
working-directory: plugins/woocommerce working-directory: plugins/woocommerce

View File

@ -24,6 +24,8 @@ jobs:
- name: Setup WooCommerce Monorepo - name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo uses: ./.github/actions/setup-woocommerce-monorepo
with:
pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Build zip - name: Build zip
working-directory: plugins/woocommerce working-directory: plugins/woocommerce

View File

@ -27,6 +27,7 @@ jobs:
with: with:
install: true install: true
build: './tools/package-release' build: './tools/package-release'
pull-package-deps: 'tools/package-release'
- name: Clean working directory - name: Clean working directory
run: git checkout pnpm-lock.yaml # in case for whatever reason the lockfile is out of sync, there won't be interference with npm publish. run: git checkout pnpm-lock.yaml # in case for whatever reason the lockfile is out of sync, there won't be interference with npm publish.

View File

@ -44,10 +44,16 @@ jobs:
# Both install and build are handled by compressed-size-action. # Both install and build are handled by compressed-size-action.
install: false install: false
build: false build: false
pull-package-deps: '@woocommerce/plugin-woocommerce'
- uses: preactjs/compressed-size-action@f780fd104362cfce9e118f9198df2ee37d12946c - uses: preactjs/compressed-size-action@f780fd104362cfce9e118f9198df2ee37d12946c
env:
BROWSERSLIST_IGNORE_OLD_DATA: true
with: with:
repo-token: '${{ secrets.GITHUB_TOKEN }}' repo-token: '${{ secrets.GITHUB_TOKEN }}'
pattern: './{packages/js/!(*e2e*|*internal*|*test*|*plugin*|*create*),plugins/woocommerce-blocks}/{build,build-style}/**/*.{js,css}' pattern: './{packages/js/!(*e2e*|*internal*|*test*|*plugin*|*create*),plugins/woocommerce-blocks}/{build,build-style}/**/*.{js,css}'
clean-script: '--if-present distclean' install-script: 'pnpm install --filter="@woocommerce/plugin-woocommerce..." --frozen-lockfile --config.dedupe-peer-dependents=false'
build-script: '--filter="@woocommerce/plugin-woocommerce" build'
clean-script: '--if-present buildclean'
minimum-change-threshold: 100 minimum-change-threshold: 100
omit-unchanged: true omit-unchanged: true

View File

@ -8,6 +8,8 @@ on:
- '**/changelog/**' - '**/changelog/**'
- '**/tests/**' - '**/tests/**'
- '**/*.md' - '**/*.md'
- '.github/**'
- '!.github/workflows/pr-build-live-branch.yml'
concurrency: concurrency:
# Cancel concurrent jobs on pull_request but not push, by including the run_id in the concurrency group for the latter. # Cancel concurrent jobs on pull_request but not push, by including the run_id in the concurrency group for the latter.
@ -39,12 +41,8 @@ jobs:
- name: Setup WooCommerce Monorepo - name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo uses: ./.github/actions/setup-woocommerce-monorepo
- name: Cache PNPM Dependencies
uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65
with: with:
node-version-file: .nvmrc pull-package-deps: '@woocommerce/plugin-woocommerce'
cache: pnpm
- name: Prepare plugin zips - name: Prepare plugin zips
id: prepare id: prepare

View File

@ -17,6 +17,8 @@ jobs:
with: with:
install: 'code-analyzer...' install: 'code-analyzer...'
build: 'code-analyzer' build: 'code-analyzer'
pull-package-deps: 'code-analyzer'
- name: 'Analyze' - name: 'Analyze'
id: 'analyze' id: 'analyze'
working-directory: 'tools/code-analyzer' working-directory: 'tools/code-analyzer'

View File

@ -26,6 +26,7 @@ jobs:
with: with:
install: true install: true
build: './tools/package-release' build: './tools/package-release'
pull-package-deps: 'tools/package-release'
- name: Execute script - name: Execute script
run: ./tools/package-release/bin/dev prepare ${{ github.event.inputs.packages }} run: ./tools/package-release/bin/dev prepare ${{ github.event.inputs.packages }}

View File

@ -191,6 +191,8 @@ jobs:
- name: Setup WooCommerce Monorepo - name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo uses: ./.github/actions/setup-woocommerce-monorepo
with:
pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Build zip - name: Build zip
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
@ -219,6 +221,8 @@ jobs:
- name: Setup WooCommerce Monorepo - name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo uses: ./.github/actions/setup-woocommerce-monorepo
with:
pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Build zip - name: Build zip
working-directory: plugins/woocommerce working-directory: plugins/woocommerce

View File

@ -22,6 +22,7 @@ jobs:
with: with:
install: '@woocommerce/plugin-woocommerce' install: '@woocommerce/plugin-woocommerce'
build: '@woocommerce/plugin-woocommerce' build: '@woocommerce/plugin-woocommerce'
pull-package-deps: '@woocommerce/plugin-woocommerce-beta-tester'
- name: Lint - name: Lint
working-directory: plugins/woocommerce-beta-tester working-directory: plugins/woocommerce-beta-tester

View File

@ -1,10 +1,14 @@
name: Remind reviewers to also review the testing instructions. name: Remind reviewers to also review the testing instructions and test coverage
on: on:
pull_request_target: pull_request_target:
types: [review_requested] types: [review_requested]
permissions: {} permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs: jobs:
add-testing-instructions-review-comment: add-testing-instructions-review-comment:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@ -51,7 +55,7 @@ jobs:
comment-author: 'github-actions[bot]' comment-author: 'github-actions[bot]'
body-includes: please make sure to review the testing instructions body-includes: please make sure to review the testing instructions
- name: Create or update PR comment asking for reviewers to review the testing instructions - name: Create or update PR comment asking for reviewers to review the testing instructions and test coverage
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d
with: with:
comment-id: ${{ steps.find-comment.outputs.comment-id }} comment-id: ${{ steps.find-comment.outputs.comment-id }}
@ -59,7 +63,7 @@ jobs:
body: | body: |
Hi ${{ env.REVIEWERS }}, ${{ env.TEAMS }} Hi ${{ env.REVIEWERS }}, ${{ env.TEAMS }}
Apart from reviewing the code changes, please make sure to review the testing instructions as well. Apart from reviewing the code changes, please make sure to review the testing instructions and verify that relevant tests (E2E, Unit, Integration, etc.) have been added or updated as needed.
You can follow this guide to find out what good testing instructions should look like: You can follow this guide to find out what good testing instructions should look like:
https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions

View File

@ -0,0 +1,69 @@
/* eslint-disable no-console */
const fs = require( 'fs' );
const { RELEASE_TAG, ARTIFACT_NAME, WP_ENV_CONFIG_PATH } = process.env;
if ( ! RELEASE_TAG ) {
console.error( 'Please set the RELEASE_TAG environment variable!' );
process.exit( 1 );
}
if ( ! ARTIFACT_NAME ) {
console.error( 'Please set the ARTIFACT_NAME environment variable!' );
process.exit( 1 );
}
if ( ! WP_ENV_CONFIG_PATH ) {
console.error( 'Please set the WP_ENV_CONFIG_PATH environment variable!' );
process.exit( 1 );
}
const artifactUrl = `https://github.com/woocommerce/woocommerce/releases/download/${ RELEASE_TAG }/${ ARTIFACT_NAME }`;
const configPath = `${ WP_ENV_CONFIG_PATH }/.wp-env.json`;
console.log( `Reading ${ configPath }` );
const data = fs.readFileSync( configPath, 'utf8' );
const wpEnvConfig = JSON.parse( data );
const overrideConfig = {};
if ( wpEnvConfig.plugins ) {
overrideConfig.plugins = wpEnvConfig.plugins;
}
if ( wpEnvConfig.env?.tests?.plugins ) {
overrideConfig.env = {
tests: {
plugins: wpEnvConfig.env.tests.plugins,
},
};
}
const entriesToReplace = [ '.', '../woocommerce' ];
for ( const entry of entriesToReplace ) {
// Search and replace in root plugins
let found = overrideConfig.plugins.indexOf( entry );
if ( found >= 0 ) {
console.log(
`Replacing ${ entry } with ${ artifactUrl } in root plugins`
);
overrideConfig.plugins[ found ] = artifactUrl;
}
// Search and replace in test env plugins
found = overrideConfig.env?.tests?.plugins?.indexOf( entry );
if ( found >= 0 ) {
console.log(
`Replacing ${ entry } with ${ artifactUrl } in env.tests.plugins`
);
overrideConfig.env.tests.plugins[ found ] = artifactUrl;
}
}
const overrideConfigPath = `${ WP_ENV_CONFIG_PATH }/.wp-env.override.json`;
console.log( `Saving ${ overrideConfigPath }` );
fs.writeFileSync(
overrideConfigPath,
JSON.stringify( overrideConfig, null, 2 )
);

View File

@ -7,6 +7,7 @@ on:
jobs: jobs:
run-tests: run-tests:
name: 'Run tests' name: 'Run tests'
if: github.repository == 'woocommerce/woocommerce'
uses: ./.github/workflows/ci.yml uses: ./.github/workflows/ci.yml
with: with:
trigger: 'daily-checks' trigger: 'daily-checks'

38
.github/workflows/tests-on-demand.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: 'On demand tests run'
on:
workflow_dispatch:
inputs:
trigger:
type: choice
description: 'Event name: it will be used to filter the jobs to run in ci.yml.'
required: true
options:
- push
- daily-checks
- pre-release
- on-demand
- custom
default: on-demand
custom-trigger:
type: string
description: 'Custom event name: In case the `Event name` choice is `custom`, this field is required.'
required: false
jobs:
validate-input:
runs-on: ubuntu-latest
steps:
- name: 'Validate input'
run: |
if [ "${{ inputs.trigger }}" == "custom" ] && [ -z "${{ inputs.custom-trigger }}" ]; then
echo "Custom event name is required when event name choice `custom`."
exit 1
fi
run-tests:
name: 'Run tests'
uses: ./.github/workflows/ci.yml
with:
trigger: ${{ inputs.trigger == 'custom' && inputs.custom-trigger || inputs.trigger }}
secrets: inherit

View File

@ -41,7 +41,43 @@
"@types/*", "@types/*",
"@typescript-eslint/*", "@typescript-eslint/*",
"@woocommerce/*", "@woocommerce/*",
"@wordpress/*", "@wordpress/api-fetch",
"@wordpress/autop",
"@wordpress/babel-preset-default",
"@wordpress/base-styles",
"@wordpress/block-editor",
"@wordpress/blocks",
"@wordpress/browserslist-config",
"@wordpress/components",
"@wordpress/compose",
"@wordpress/core-data",
"@wordpress/data",
"@wordpress/data-controls",
"@wordpress/date",
"@wordpress/dependency-extraction-webpack-plugin",
"@wordpress/deprecated",
"@wordpress/dom",
"@wordpress/dom-ready",
"@wordpress/e2e-test-utils",
"@wordpress/e2e-test-utils-playwright",
"@wordpress/e2e-tests",
"@wordpress/element",
"@wordpress/html-entities",
"@wordpress/i18n",
"@wordpress/icons",
"@wordpress/is-shallow-equal",
"@wordpress/notices",
"@wordpress/plugins",
"@wordpress/postcss-plugins-preset",
"@wordpress/postcss-themes",
"@wordpress/prettier-config",
"@wordpress/primitives",
"@wordpress/scripts",
"@wordpress/server-side-render",
"@wordpress/style-engine",
"@wordpress/stylelint-config",
"@wordpress/url",
"@wordpress/wordcount",
"babel*", "babel*",
"eslint*", "eslint*",
"glob*", "glob*",
@ -205,9 +241,10 @@
"@wordpress/env" "@wordpress/env"
], ],
"packages": [ "packages": [
"@woocommerce/block-library",
"**" "**"
], ],
"pinVersion": "^9.0.7" "pinVersion": "^9.7.0"
}, },
{ {
"dependencies": [ "dependencies": [

View File

@ -0,0 +1,44 @@
diff --git a/build-module/lock-unlock.js b/build-module/lock-unlock.js
index 2265f933ceec19f65ca6776c24c3f88b368d713f..e9e10980bfd1b584ab0a037c3b72edae29a2a26e 100644
--- a/build-module/lock-unlock.js
+++ b/build-module/lock-unlock.js
@@ -1,9 +1,34 @@
/**
- * WordPress dependencies
+ * External dependencies
*/
import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
-export const {
- lock,
- unlock
-} = __dangerousOptInToUnstableAPIsOnlyForCoreModules('I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.', '@wordpress/edit-site');
+
+// Workaround for Gutenberg private API consent string differences between WP 6.3 and 6.4+
+// The modified version checks for the WP version and replaces the consent string with the correct one.
+// This can be removed once we drop support for WP 6.3 in the "Customize Your Store" task.
+// See this PR for details: https://github.com/woocommerce/woocommerce/pull/40884
+
+const wordPressConsentString = {
+ 6.4: 'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.',
+ 6.5: 'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.',
+ 6.6: 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
+};
+
+function optInToUnstableAPIs() {
+ let error;
+ for ( const optInString of Object.values( wordPressConsentString ) ) {
+ try {
+ return __dangerousOptInToUnstableAPIsOnlyForCoreModules(
+ optInString,
+ '@wordpress/edit-site'
+ );
+ } catch ( anError ) {
+ error = anError;
+ }
+ }
+
+ throw error;
+}
+
+export const { lock, unlock } = optInToUnstableAPIs();
//# sourceMappingURL=lock-unlock.js.map

View File

@ -1,5 +1,255 @@
== Changelog == == Changelog ==
= 9.1.4 2024-07-26 =
**WooCommerce**
* Fix - Revert fixing terms count in tracking PR as it caused product_add_publish to be triggered more than usual. [#49797](https://github.com/woocommerce/woocommerce/pull/49797)
* Fix - Hardening against XSS via the Product Button unescaped attribute. [#50010](https://github.com/woocommerce/woocommerce/pull/50010)
* Fix - Enhance escaping for block attributes. [#50015](https://github.com/woocommerce/woocommerce/pull/50015)
= 9.1.2 2024-07-12 =
**WooCommerce**
* Fix - Revert 46857 to preserve backcompat with earlier WC versions. [#48753](https://github.com/woocommerce/woocommerce/pull/48753)
= 9.1.1 2024-07-11 =
**WooCommerce**
* Tweak - Revert #46262, as that PR would render input values invisible under certain conditions. [49404](https://github.com/woocommerce/woocommerce/pull/49404)
= 9.1.0 2024-07-10 =
**WooCommerce**
* Fix - Prevent HTML tags being rendered on order confirmation and emails [#49370](https://github.com/woocommerce/woocommerce/pull/49370)
* Security - Improve the way we cache information about recent customer activity, to prevent the wrong data being retrieved in some specific conditions involving multisite networks. [#49373](https://github.com/woocommerce/woocommerce/pull/49373)
* Fix - Prevent BatchProcessingController from cleaning up processors after a premature shutdown. [#49243](https://github.com/woocommerce/woocommerce/pull/49243)
* Fix - CYS: fix not template set correctly. [#49113](https://github.com/woocommerce/woocommerce/pull/49113)
* Fix - CYS: Disable readonly mode only when full composability feature flag is enabled. [#48752](https://github.com/woocommerce/woocommerce/pull/48752)
* Fix - CYS: fix crash of CYS on WordPress 6.6 [#48664](https://github.com/woocommerce/woocommerce/pull/48664)
* Fix - Revert "Set stock quantity value as 0 by default (#48448)" #48863 [#48863](https://github.com/woocommerce/woocommerce/pull/48863)
* Fix - Add product id to product_edit_view track in classic product edit screen [#47853](https://github.com/woocommerce/woocommerce/pull/47853)
* Fix - Address responsiveness issues in orders list table. [#47684](https://github.com/woocommerce/woocommerce/pull/47684)
* Fix - Add screen-reader-text styles to e-mails. [#47738](https://github.com/woocommerce/woocommerce/pull/47738)
* Fix - Adds new hook `woocommerce_rest_delete_shipping_zone_method` which will fire after a shipping zone method is deleted via the REST API. [#47862](https://github.com/woocommerce/woocommerce/pull/47862)
* Fix - Allow products with non-integer stock to be created via REST API. [#48541](https://github.com/woocommerce/woocommerce/pull/48541)
* Fix - Calling $product->get_status() after $product->save() on a new product now returns correct status. [#48241](https://github.com/woocommerce/woocommerce/pull/48241)
* Fix - Change the cursor to a pointer when hovering over the mini cart [#46996](https://github.com/woocommerce/woocommerce/pull/46996)
* Fix - CYS - Hovering over the header or footer on the "Design your homepage" section should not make them highlighted. [#48358](https://github.com/woocommerce/woocommerce/pull/48358)
* Fix - CYS - Select the next block after deleting the selected one (instead of the header). [#48316](https://github.com/woocommerce/woocommerce/pull/48316)
* Fix - CYS: apply white color to the heading elements in the core/cover block. [#48447](https://github.com/woocommerce/woocommerce/pull/48447)
* Fix - CYS: Fix crash homepage. [#48205](https://github.com/woocommerce/woocommerce/pull/48205)
* Fix - CYS: Fix CSS header. [#48389](https://github.com/woocommerce/woocommerce/pull/48389)
* Fix - CYS: fix logic to disable mover buttons. [#48502](https://github.com/woocommerce/woocommerce/pull/48502)
* Fix - CYS: fix tooltip position. [#48495](https://github.com/woocommerce/woocommerce/pull/48495)
* Fix - CYS: hide popover when the mouse pointer leaves the site preview and then back. [#48394](https://github.com/woocommerce/woocommerce/pull/48394)
* Fix - Do not create empty webhooks after failure to deliver deleted webhook. [#48480](https://github.com/woocommerce/woocommerce/pull/48480)
* Fix - Ensure attribute slugs with multibyte characters are handled property when outputting attributes in the REST API products endpoint [#48198](https://github.com/woocommerce/woocommerce/pull/48198)
* Fix - Ensure available stock is updated correctly when updating line items in orders via the REST API. [#47784](https://github.com/woocommerce/woocommerce/pull/47784)
* Fix - Ensure data filtered by `woocommerce_logger_log_message` does not carry across multiple log handlers [#48336](https://github.com/woocommerce/woocommerce/pull/48336)
* Fix - Ensure getPreviousDate default behaviour is comparing previous_year [#47951](https://github.com/woocommerce/woocommerce/pull/47951)
* Fix - Ensure permission checks for the customer downloads REST API endpoint use the correct customer ID. [#47854](https://github.com/woocommerce/woocommerce/pull/47854)
* Fix - Ensure that data containing multibyte characters and/or slashes that is appended to log entries gets encoded and rendered correctly [#48341](https://github.com/woocommerce/woocommerce/pull/48341)
* Fix - Fix a bug with the woocommerce_get_default_value_for_{key} filter that was preventing setting a falsey value on a checkbox (i.e. to uncheck it dynamically) [#48031](https://github.com/woocommerce/woocommerce/pull/48031)
* Fix - Fix activation limit for single license subscriptions on woocommerce.com [#47643](https://github.com/woocommerce/woocommerce/pull/47643)
* Fix - Fix a null parameter being passed into strpos in Admin/Orders/PageController.php [#48476](https://github.com/woocommerce/woocommerce/pull/48476)
* Fix - Fix bug where Core Profiler initiates a Jetpack connection even if it was already connected before [#48345](https://github.com/woocommerce/woocommerce/pull/48345)
* Fix - Fix bumped down data when analytics chart current period contains 29th Feb [#45874](https://github.com/woocommerce/woocommerce/pull/45874)
* Fix - Fix coming soon footer banner doesn't display properly on tablet and mobile [#47980](https://github.com/woocommerce/woocommerce/pull/47980)
* Fix - Fix e2e tests about the tabs selection during the product creation experience [#47860](https://github.com/woocommerce/woocommerce/pull/47860)
* Fix - Fix edit variable product test [#48288](https://github.com/woocommerce/woocommerce/pull/48288)
* Fix - Fix FlexSlider thumbnail animation for variable products with default form values on small devices. </details> <details> [#48137](https://github.com/woocommerce/woocommerce/pull/48137)
* Fix - Fix location settings not updated in tax task [#48606](https://github.com/woocommerce/woocommerce/pull/48606)
* Fix - Fix LYS private link URL parameter regardless of permalink settings [#48425](https://github.com/woocommerce/woocommerce/pull/48425)
* Fix - Fix product archive page not hidden behind the coming soon page [#48522](https://github.com/woocommerce/woocommerce/pull/48522)
* Fix - Fix Product Gallery block error on revisiting Single Product template without fully reloading the page. [#47636](https://github.com/woocommerce/woocommerce/pull/47636)
* Fix - Fix product tracks when importing #47857 [#47857](https://github.com/woocommerce/woocommerce/pull/47857)
* Fix - Fix some issues in performance tests #47735 [#47735](https://github.com/woocommerce/woocommerce/pull/47735)
* Fix - Fix the issue that the React-powered admin routing pages added after the filter initialization could not be displayed. [#47696](https://github.com/woocommerce/woocommerce/pull/47696)
* Fix - Fix the terms counts in wcadmin_product_add_publish event. [#48194](https://github.com/woocommerce/woocommerce/pull/48194)
* Fix - Fix two products being added to cart when Geolocate (with page caching support) was enabled and AJAX add to cart buttons disabled [#47761](https://github.com/woocommerce/woocommerce/pull/47761)
* Fix - Fix untranslated strings on CYS and marketplace [#48127](https://github.com/woocommerce/woocommerce/pull/48127)
* Fix - Honor empty "additional content" setting in e-mails. [#47809](https://github.com/woocommerce/woocommerce/pull/47809)
* Fix - Improve consistency of Setting-> Gateway Manage button for WooPayments gateway [#48212](https://github.com/woocommerce/woocommerce/pull/48212)
* Fix - In general, the `last_access` field of a REST API key should only be updated once-per-request.
* Fix - Make coupon metadata read robust against wrongly stored product related metadata [#48362](https://github.com/woocommerce/woocommerce/pull/48362)
* Fix - Moved WooCommerce block categories registration on the server-side, fixing a bug that would show warnings to developers trying to hook new blocks in such categories. [#47836](https://github.com/woocommerce/woocommerce/pull/47836)
* Fix - Possible availability of unpublished coupons on sites with an object cache has been addressed through improved cache management. [#47739](https://github.com/woocommerce/woocommerce/pull/47739)
* Fix - Prefer update URLs over PluginURI in My Subscriptions for plugins without a subscription. [#47950](https://github.com/woocommerce/woocommerce/pull/47950)
* Fix - Prevent on-sale badge from showing on top of the coming soon banner. [#48082](https://github.com/woocommerce/woocommerce/pull/48082)
* Fix - Prevent Product Gallery from being inserted on Posts and Pages. [#48228](https://github.com/woocommerce/woocommerce/pull/48228)
* Fix - Product Collection: prevent throwing warnings in some circumstances when rendering block [#48530](https://github.com/woocommerce/woocommerce/pull/48530)
* Fix - Product Price: Narrow down the ancestors of the block so it's available in inserter only in places where block makes sense [#47802](https://github.com/woocommerce/woocommerce/pull/47802)
* Fix - Re-enable variable product E2E test #48294 [#48294](https://github.com/woocommerce/woocommerce/pull/48294)
* Fix - Related Products: hides unusable options from Inspector Controls [#47845](https://github.com/woocommerce/woocommerce/pull/47845)
* Fix - Run possibly_add_template_id function in woocommerce_rest_prepare_product_variation_object hook [#48325](https://github.com/woocommerce/woocommerce/pull/48325)
* Fix - Scroll to view the templates section on the status page [#48125](https://github.com/woocommerce/woocommerce/pull/48125)
* Fix - Set stock quantity value as 0 by default #48448 [#48448](https://github.com/woocommerce/woocommerce/pull/48448)
* Fix - Update plugin installation error logger to use plugin track key for extension name [#47786](https://github.com/woocommerce/woocommerce/pull/47786)
* Fix - When a product attribute is updated, unchanged values should not be reset to their defaults. [#48120](https://github.com/woocommerce/woocommerce/pull/48120)
* Fix - WooCommerce: fixes the checks when migrating the product form template [#48386](https://github.com/woocommerce/woocommerce/pull/48386)
* Fix - [CYS Full Composability] Ensure that the assembler doesn't crash when the feature flag is enabled, but the site doesn't have the latest version of Gutenberg. [#47546](https://github.com/woocommerce/woocommerce/pull/47546)
* Add - Add CLI tools for the product attributes lookup table [#47311](https://github.com/woocommerce/woocommerce/pull/47311)
* Add - Add 'woocommerce_order_note_deleted' hook for order note deletions. [#47916](https://github.com/woocommerce/woocommerce/pull/47916)
* Add - Add CLI tools to enable and disable HPOS compatibility mode. [#48117](https://github.com/woocommerce/woocommerce/pull/48117)
* Add - Added 'woocommerce_restore_order_item_stock' filter for restored line item stock on canceled orders [#40848](https://github.com/woocommerce/woocommerce/pull/40848)
* Add - Add ErrorBoundary component for handling unexpect errors [#48250](https://github.com/woocommerce/woocommerce/pull/48250)
* Add - Add filter to dynamically exclude a page from Coming soon mode [#47787](https://github.com/woocommerce/woocommerce/pull/47787)
* Add - Add Printful product placement to Add Products task [#48520](https://github.com/woocommerce/woocommerce/pull/48520)
* Add - Add skipped test custom reporter to surface skipped tests in CI runs [#48195](https://github.com/woocommerce/woocommerce/pull/48195)
* Add - Add the ability to test experimental blocks via the Advanced > Features menu of WooCommerce settings. [#47701](https://github.com/woocommerce/woocommerce/pull/47701)
* Add - Add woocommerce_manage_stock option to the default_option_permissions list in the Options rest controller [#48239](https://github.com/woocommerce/woocommerce/pull/48239)
* Add - CYS: add CTA to our Fiverr Logo Maker landing page. [#48486](https://github.com/woocommerce/woocommerce/pull/48486)
* Add - CYS: add pattern category in the block toolbar. [#48501](https://github.com/woocommerce/woocommerce/pull/48501)
* Add - CYS: Add the Delete button to the Block Toolbar. [#48143](https://github.com/woocommerce/woocommerce/pull/48143)
* Add - CYS: Ensure that toolbar appears only when the homepage sidebar is open. [#48115](https://github.com/woocommerce/woocommerce/pull/48115)
* Add - CYS: Show Patterns from PTK. [#48207](https://github.com/woocommerce/woocommerce/pull/48207)
* Add - CYS: Show popover when the user clicks on the pattern [#47583](https://github.com/woocommerce/woocommerce/pull/47583)
* Add - Determine _product_template_id from 'woocommerce_product_editor_determine_product_template' filter [#47762](https://github.com/woocommerce/woocommerce/pull/47762)
* Add - Display an admin notice in Setting and Extension pages when there are expiring subscriptions and connected account doesn't have a payment method. [#47141](https://github.com/woocommerce/woocommerce/pull/47141)
* Add - Enhancements to background batch processing. [#48078](https://github.com/woocommerce/woocommerce/pull/48078)
* Add - Highlight the pattern when the user hovers it. [#47415](https://github.com/woocommerce/woocommerce/pull/47415)
* Add - LYS - Add 'Remove test orders' for WooPayments [#47832](https://github.com/woocommerce/woocommerce/pull/47832)
* Add - PFT: introduce controller and initialize it [#48221](https://github.com/woocommerce/woocommerce/pull/48221)
* Add - REST API: extened shipping_classes namespace with the /suggest-slug endpoint [#47896](https://github.com/woocommerce/woocommerce/pull/47896)
* Add - Updated shipstation copy [#48549](https://github.com/woocommerce/woocommerce/pull/48549)
* Add - WooCommerce: create a new product_form CPT [#48073](https://github.com/woocommerce/woocommerce/pull/48073)
* Add - WooCommerce: introduce `product-editor-template-system` feature flag [#48136](https://github.com/woocommerce/woocommerce/pull/48136)
* Add - WooCommerce: update CPT product_form posts when plugin updates [#48265](https://github.com/woocommerce/woocommerce/pull/48265)
* Add - WooCommerce Blocks: Added a GitHub Action to create issues for flaky E2E tests [#47758](https://github.com/woocommerce/woocommerce/pull/47758)
* Update - Add feature flag for Printful placement [#49104](https://github.com/woocommerce/woocommerce/pull/49104)
* Update - Add a control to enable a separator on the Checkout block's "Checkout Terms" block. This will enable a separator above the block that can be turned off in case the block is moved. [#47565](https://github.com/woocommerce/woocommerce/pull/47565)
* Update - Change the item schemas for Orders and Order Refunds API endpoints to correctly specify that the rate_id property in a tax_line object is an integer, not a string [#47779](https://github.com/woocommerce/woocommerce/pull/47779)
* Update - Clean up theming sections in WooCommerce blocks docs [#48420](https://github.com/woocommerce/woocommerce/pull/48420)
* Update - CYS - Exclude two testimonials patterns from registering since they depend on Jetpack. [#48233](https://github.com/woocommerce/woocommerce/pull/48233)
* Update - CYS - Fix active/inactive patterns for each of the sections in the assembler. [#48458](https://github.com/woocommerce/woocommerce/pull/48458)
* Update - CYS - Install the patterns during the CYS flow if the transient is not set. [#48274](https://github.com/woocommerce/woocommerce/pull/48274)
* Update - CYS - Redirect to the same section after installing fonts or patterns on the assembler. [#48227](https://github.com/woocommerce/woocommerce/pull/48227)
* Update - CYS - Show tooltips on the Shuffle and Delete buttons in the assembler toolbar. [#48465](https://github.com/woocommerce/woocommerce/pull/48465)
* Update - CYS: set new default patterns. [#48467](https://github.com/woocommerce/woocommerce/pull/48467)
* Update - Display return to cart link on mobile devices. [#48103](https://github.com/woocommerce/woocommerce/pull/48103)
* Update - Docs: update documentation regarding Compatibility Layer [#48456](https://github.com/woocommerce/woocommerce/pull/48456)
* Update - Expand block templates documentation [#48247](https://github.com/woocommerce/woocommerce/pull/48247)
* Update - Experimental blocks now have "(Experimental)" suffix [#48071](https://github.com/woocommerce/woocommerce/pull/48071)
* Update - fix: label improvement on my order page template [#48374](https://github.com/woocommerce/woocommerce/pull/48374)
* Update - Improve WooCommerce block template names in the Add New Template screen. [#48106](https://github.com/woocommerce/woocommerce/pull/48106)
* Update - Invalidate cache for SiteGround Speed Optimizer [#48523](https://github.com/woocommerce/woocommerce/pull/48523)
* Update - Optimize the regeneration of the product attributes lookup table [#47700](https://github.com/woocommerce/woocommerce/pull/47700)
* Update - Product Archive templates: Replace the default block from Products (Beta) to Product Collection block [#48112](https://github.com/woocommerce/woocommerce/pull/48112)
* Update - Product Block Editor: disable the `product-editor-template-system` feature flag as default, even for the development environment. [#48378](https://github.com/woocommerce/woocommerce/pull/48378)
* Update - Product Collection: Handpicked Products filter now allows searching from 2 characters and more and updates available results as you type [#48379](https://github.com/woocommerce/woocommerce/pull/48379)
* Update - Product Elements: hide Product Summary from Single Product block and only show Excerpt variation [#48253](https://github.com/woocommerce/woocommerce/pull/48253)
* Update - Product Rating Stars and Product Rating Counter from the inserter [#48229](https://github.com/woocommerce/woocommerce/pull/48229)
* Update - Products (Beta): hide block from inserter in favor of Product Collection block [#48204](https://github.com/woocommerce/woocommerce/pull/48204)
* Update - Product Summary: Increase the length of the description from 55 to 100 words (max supported by core/post-excerpt) [#47651](https://github.com/woocommerce/woocommerce/pull/47651)
* Update - Reduced the number of FlexSlider animation engines from 2 to 1, now always using CSS3 transitions. [#46564](https://github.com/woocommerce/woocommerce/pull/46564)
* Update - Replace the use of options endpoint with the LYS API endpoint to query woocommerce_admin_launch_your_store_survey_completed option. [#47915](https://github.com/woocommerce/woocommerce/pull/47915)
* Update - The archive product title will now be updated to the title of the current shop
page. If the page does not exist, it will fall back to "Shop". [#48255](https://github.com/woocommerce/woocommerce/pull/48255)
* Update - Toggle LYS feature flag on for 9.1 [#48244](https://github.com/woocommerce/woocommerce/pull/48244)
* Update - Update input fields styles of the Checkout block [#46362](https://github.com/woocommerce/woocommerce/pull/46362)
* Update - WooCommerce: store the template description in the `product_form` excerpt property. [#48327](https://github.com/woocommerce/woocommerce/pull/48327)
* Update - Wrap activity panels in error boundary [#48415](https://github.com/woocommerce/woocommerce/pull/48415)
* Update - [CYS] Ensure fetch PTK patterns requests are always done async to improve performance. [#47551](https://github.com/woocommerce/woocommerce/pull/47551)
* Update - [CYS] Refactor the pattern registration and add patterns from the PTK API. [#47306](https://github.com/woocommerce/woocommerce/pull/47306)
* Update - [CYS] Remove the restriction to TT4 and allow users to proceed to the pattern assembler with any block themes. Update intro page design. [#46916](https://github.com/woocommerce/woocommerce/pull/46916)
* Update - [CYS] Show a message when tracking is not allowed in patterns and add the ability for users to opt-in and fetch patterns. </details> <details> [#48095](https://github.com/woocommerce/woocommerce/pull/48095)
* Dev - Improve E2E selector by making it stricter. Wait for text due to AJAX call. [#48471](https://github.com/woocommerce/woocommerce/pull/48471)
* Dev - Added e2e test to check ability to connect to woocommerce.com [#48028](https://github.com/woocommerce/woocommerce/pull/48028)
* Dev - Added test enviornments [#48101](https://github.com/woocommerce/woocommerce/pull/48101)
* Dev - Add previous error class to checkout endpoint response [#47489](https://github.com/woocommerce/woocommerce/pull/47489)
* Dev - Add test for wcpay_connect_account_clicked track [#48347](https://github.com/woocommerce/woocommerce/pull/48347)
* Dev - Add tests for some product editor tracks [#48245](https://github.com/woocommerce/woocommerce/pull/48245)
* Dev - Blocks E2E: Remove confusing utilities in favor of native locator functionality. [#47904](https://github.com/woocommerce/woocommerce/pull/47904)
* Dev - CI: merge test jobs [#48175](https://github.com/woocommerce/woocommerce/pull/48175)
* Dev - Clean up eslint comments after rules update in Blocks E2E tests. [#47875](https://github.com/woocommerce/woocommerce/pull/47875)
* Dev - Clean up tasklist progression headercard experiment [#47983](https://github.com/woocommerce/woocommerce/pull/47983)
* Dev - Clean up welcome modal code [#48346](https://github.com/woocommerce/woocommerce/pull/48346)
* Dev - Do not dismiss the error snackbar automatically, fix E2E test #48192 [#48192](https://github.com/woocommerce/woocommerce/pull/48192)
* Dev - E2E test: Improve analytics data spec by disabling the task list reminder bar [#48357](https://github.com/woocommerce/woocommerce/pull/48357)
* Dev - E2E tests: configure snapshotPathTemplate [#47773](https://github.com/woocommerce/woocommerce/pull/47773)
* Dev - E2E tests: fixing flakiness in checkout block and launch your store tests [#48016](https://github.com/woocommerce/woocommerce/pull/48016)
* Dev - E2E tests: fixing flaky assembler homepage test [#48356](https://github.com/woocommerce/woocommerce/pull/48356)
* Dev - E2E tests: fixing flaky checkout block test [#48527](https://github.com/woocommerce/woocommerce/pull/48527)
* Dev - E2E tests: fixing flaky color palette picker test [#48496](https://github.com/woocommerce/woocommerce/pull/48496)
* Dev - E2E tests: fixing flaky connect to woo test [#48613](https://github.com/woocommerce/woocommerce/pull/48613)
* Dev - E2E tests: fixing flaky customize store transitional test [#48532](https://github.com/woocommerce/woocommerce/pull/48532)
* Dev - E2E tests: fixing flaky logo picker test [#48503](https://github.com/woocommerce/woocommerce/pull/48503)
* Dev - E2E tests: fixing flaky merchant create variable product test [#48276](https://github.com/woocommerce/woocommerce/pull/48276)
* Dev - E2E tests: fixing flaky merchant customer list test [#48463](https://github.com/woocommerce/woocommerce/pull/48463)
* Dev - E2E tests: fixing flaky merchant product attribute test [#48230](https://github.com/woocommerce/woocommerce/pull/48230)
* Dev - E2E tests: fixing flaky merchant user create and logging [#48446](https://github.com/woocommerce/woocommerce/pull/48446)
* Dev - E2E tests: fixing flaky shopper checkout coupons [#48555](https://github.com/woocommerce/woocommerce/pull/48555)
* Dev - E2E tests: fixing flaky shopper search browse products in the shop [#48560](https://github.com/woocommerce/woocommerce/pull/48560)
* Dev - E2E tests: fixing flaky store owner core profiler test [#48430](https://github.com/woocommerce/woocommerce/pull/48430)
* Dev - E2E tests: fixing skipped mini cart test [#47756](https://github.com/woocommerce/woocommerce/pull/47756)
* Dev - E2E tests: fixing skipped tests [#47859](https://github.com/woocommerce/woocommerce/pull/47859)
* Dev - E2E tests: improve existing merchant e2e tests for creating page and post [#48162](https://github.com/woocommerce/woocommerce/pull/48162)
* Dev - E2E tests: improve existing util for inserting blocks via shortcut [#48225](https://github.com/woocommerce/woocommerce/pull/48225)
* Dev - E2E tests: improving cart util and updating relevant tests [#48475](https://github.com/woocommerce/woocommerce/pull/48475)
* Dev - E2E tests: updated the test ignore pattern for Gutenberg tests project [#47764](https://github.com/woocommerce/woocommerce/pull/47764)
* Dev - E2E tests: update tests checking if blocks can be added [#48211](https://github.com/woocommerce/woocommerce/pull/48211)
* Dev - E2E tests: update the report configuration for all core jobs [#48424](https://github.com/woocommerce/woocommerce/pull/48424)
* Dev - Fix a filters block e2e test that was mistakenly merged incorrectly. [#48122](https://github.com/woocommerce/woocommerce/pull/48122)
* Dev - Fixing a flaky core profiler e2e test [#47917](https://github.com/woocommerce/woocommerce/pull/47917)
* Dev - Fix path to test results for api core tests [#48490](https://github.com/woocommerce/woocommerce/pull/48490)
* Dev - Implement unit test for tracks wcadmin_page_view, wcadmin_tasklist_view, wcadmin_tasklist_task_completed, wcadmin_tasklist_click [#47876](https://github.com/woocommerce/woocommerce/pull/47876)
* Dev - Include blocks e2e in ci.yml [#48224](https://github.com/woocommerce/woocommerce/pull/48224)
* Dev - Migrate release smoke workflow to the new CI setup [#48113](https://github.com/woocommerce/woocommerce/pull/48113)
* Dev - Product Editor: Move variation pricing fields to General tab. [#48155](https://github.com/woocommerce/woocommerce/pull/48155)
* Dev - Remove the isFeaturePlugin function, which was used to turn off experimental block styling (but was non functional). Also remove associated code in FeatureGating class. [#47866](https://github.com/woocommerce/woocommerce/pull/47866)
* Dev - Remove WOOCOMMERCE_BLOCKS_PHASE completely from the monorepo, introduce BUNDLE_EXPERIMENTAL_BLOCKS just for the purpose of building/bundling experimental blocks [#47807](https://github.com/woocommerce/woocommerce/pull/47807)
* Dev - Skipped flaky test: test_order_updated_webhook_delivered_once [#48064](https://github.com/woocommerce/woocommerce/pull/48064)
* Dev - Streamline the implementation of the Blocks' E2E utilities. [#47660](https://github.com/woocommerce/woocommerce/pull/47660)
* Dev - Streamline the usage of WP CLI in Blocks E2E tests. [#47869](https://github.com/woocommerce/woocommerce/pull/47869)
* Dev - Tweak the paths that should trigger e2e tests. [#48067](https://github.com/woocommerce/woocommerce/pull/48067)
* Dev - Unskip some tests that have been skipped for flakiness [#47772](https://github.com/woocommerce/woocommerce/pull/47772)
* Dev - Update @wordpress/env version to 9.7.0 [#48443](https://github.com/woocommerce/woocommerce/pull/48443)
* Dev - Updated Core Profilers XState version to V5 [#48135](https://github.com/woocommerce/woocommerce/pull/48135)
* Dev - Update Playwright from 1.41.1 to 1.44.1 (latest) and fixed tests [#48291](https://github.com/woocommerce/woocommerce/pull/48291)
* Dev - Update pnpm-lock with updated React [#47973](https://github.com/woocommerce/woocommerce/pull/47973)
* Dev - Update the React version in the pnpm-lock file [#47993](https://github.com/woocommerce/woocommerce/pull/47993)
* Dev - Update the URLs for order-related e2e tests to use new URLs from HPOS [#46397](https://github.com/woocommerce/woocommerce/pull/46397)
* Dev - [e2e tests] Fix e2e test reports paths [#48320](https://github.com/woocommerce/woocommerce/pull/48320)
* Tweak - Update Printful label [#48778](https://github.com/woocommerce/woocommerce/pull/48778)
* Tweak - Add a close button to dismiss store alerts [#48453](https://github.com/woocommerce/woocommerce/pull/48453)
* Tweak - Adds a defensive check to reduce error log noise when regenerating images. [#47785](https://github.com/woocommerce/woocommerce/pull/47785)
* Tweak - Adds best practice advice to the API key generation screen. [#48483](https://github.com/woocommerce/woocommerce/pull/48483)
* Tweak - CYS - Update the copy for the intro tour. [#48202](https://github.com/woocommerce/woocommerce/pull/48202)
* Tweak - CYS: Refactor routing approach. [#48312](https://github.com/woocommerce/woocommerce/pull/48312)
* Tweak - Include 'original_post_status' in HPOS edit form. [#48196](https://github.com/woocommerce/woocommerce/pull/48196)
* Tweak - Minor improvements to BlockTemplatesController instantiation [#48107](https://github.com/woocommerce/woocommerce/pull/48107)
* Tweak - Only load 'productCount' and 'experimentalBlocksEnabled' settings in admin [#48152](https://github.com/woocommerce/woocommerce/pull/48152)
* Tweak - Product Editor: Skip momentarily the 'can create a variation option and publish the product' E2E test [#47618](https://github.com/woocommerce/woocommerce/pull/47618)
* Tweak - Remove checkstyle.xml file [#47844](https://github.com/woocommerce/woocommerce/pull/47844)
* Tweak - Remove unused woocommerce_task_list_prompt_shown option [#48304](https://github.com/woocommerce/woocommerce/pull/48304)
* Tweak - Update coming soon banner text to use translation function [#47742](https://github.com/woocommerce/woocommerce/pull/47742)
* Tweak - Update LYS survey completion track props [#47985](https://github.com/woocommerce/woocommerce/pull/47985)
* Tweak - Update printful copy. [#48626](https://github.com/woocommerce/woocommerce/pull/48626)
* Tweak - Update WC blocks e2e tests to WordPress 6.6 [#48436](https://github.com/woocommerce/woocommerce/pull/48436)
* Tweak - Verify if the coming soon cache is displayed when launching the store and alerts the user if it is still present. [#48586](https://github.com/woocommerce/woocommerce/pull/48586)
* Performance - Add DISTINCT keyword for smaller response and performance. [#48139](https://github.com/woocommerce/woocommerce/pull/48139)
* Performance - CYS - Optimize the `Choose a professionally designed theme` intro page image. [#48566](https://github.com/woocommerce/woocommerce/pull/48566)
* Performance - Replaced `classnames` package with the faster and smaller `clsx` package. [#47760](https://github.com/woocommerce/woocommerce/pull/47760)
* Performance - Revert changing the title of the edit comments screen when editing a review. [#48485](https://github.com/woocommerce/woocommerce/pull/48485)
* Enhancement - Accessibility enhancement for the whole shop accounts section [#47144](https://github.com/woocommerce/woocommerce/pull/47144)
* Enhancement - Add information about block/shortcode/template usage on Cart and Checkout pages to the WC system report. [#48300](https://github.com/woocommerce/woocommerce/pull/48300)
* Enhancement - CYS: add shuffle feature. [#47356](https://github.com/woocommerce/woocommerce/pull/47356)
* Enhancement - CYS: allow to the user to move the pattern. [#47322](https://github.com/woocommerce/woocommerce/pull/47322)
* Enhancement - Enhancement editor loading speed [#47425](https://github.com/woocommerce/woocommerce/pull/47425)
* Enhancement - Handle core profiler get countries error [#48317](https://github.com/woocommerce/woocommerce/pull/48317)
* Enhancement - If a variable product doesn't have a Product Image but variations do have images, the zoom and flex slider will be initiated as expected [#47714](https://github.com/woocommerce/woocommerce/pull/47714)
* Enhancement - Improve spacing between steps in the Checkout block on mobile and desktop [#47565](https://github.com/woocommerce/woocommerce/pull/47565)
* Enhancement - Increase connection timeout to 30 seconds for the requests in WCCOM connection flow [#47842](https://github.com/woocommerce/woocommerce/pull/47842)
* Enhancement - Limit coming soon options API call to home screen [#48303](https://github.com/woocommerce/woocommerce/pull/48303)
* Enhancement - Modified order status tooltip labels [#47861](https://github.com/woocommerce/woocommerce/pull/47861)
* Enhancement - Optimize text wrapping for wc admin pages [#48131](https://github.com/woocommerce/woocommerce/pull/48131)
* Enhancement - Remove the previous product management experience [#47814](https://github.com/woocommerce/woocommerce/pull/47814)
= 9.0.2 2024-06-24 = = 9.0.2 2024-06-24 =
**WooCommerce** **WooCommerce**
@ -21,9 +271,9 @@
* Fix - Fix settings-api textarea validation to prevent insertion of iframes in description areas by default [#48432](https://github.com/woocommerce/woocommerce/pull/48432) * Fix - Fix settings-api textarea validation to prevent insertion of iframes in description areas by default [#48432](https://github.com/woocommerce/woocommerce/pull/48432)
* Fix - #47626 changed the classes on the legacy admin settings save button and broke saving standard tax rates [#48201](https://github.com/woocommerce/woocommerce/pull/48201) * Fix - #47626 changed the classes on the legacy admin settings save button and broke saving standard tax rates [#48201](https://github.com/woocommerce/woocommerce/pull/48201)
* Fix - Revert "Remove customer-effort-score-tracks" feature flag #48235 [#48235](https://github.com/woocommerce/woocommerce/pull/48235) * Fix - Revert "Remove customer-effort-score-tracks" feature flag #48235 [#48235](https://github.com/woocommerce/woocommerce/pull/48235)
* Fix - Fix db update notice redirection bug where it redirects without checking for db update action. </details> <details> <summary>Changelog Entry Comment</summary> [#48163](https://github.com/woocommerce/woocommerce/pull/48163) * Fix - Fix db update notice redirection bug where it redirects without checking for db update action. [#48163](https://github.com/woocommerce/woocommerce/pull/48163)
* Fix - Add missing URL to discover more link in LYS tour [#48109](https://github.com/woocommerce/woocommerce/pull/48109) * Fix - Add missing URL to discover more link in LYS tour [#48109](https://github.com/woocommerce/woocommerce/pull/48109)
* Fix - Fix: "On Sale" collection isn't displaying on Editor side </details> <details> [#47994](https://github.com/woocommerce/woocommerce/pull/47994) * Fix - Fix: "On Sale" collection isn't displaying on Editor side [#47994](https://github.com/woocommerce/woocommerce/pull/47994)
* Fix - Make the plugin autoinstall process more robust [#47798](https://github.com/woocommerce/woocommerce/pull/47798) * Fix - Make the plugin autoinstall process more robust [#47798](https://github.com/woocommerce/woocommerce/pull/47798)
* Fix - Prevent tracking files from being enqueued on the front end. [#47938](https://github.com/woocommerce/woocommerce/pull/47938) * Fix - Prevent tracking files from being enqueued on the front end. [#47938](https://github.com/woocommerce/woocommerce/pull/47938)
* Fix - Fix: Product Collection block does not display properly when editing template/post [#47871](https://github.com/woocommerce/woocommerce/pull/47871) * Fix - Fix: Product Collection block does not display properly when editing template/post [#47871](https://github.com/woocommerce/woocommerce/pull/47871)

View File

@ -0,0 +1,44 @@
---
post_title: Check if a Payment Method Support Refunds, Subscriptions or Pre-orders
menu_title: Payment method support for refunds, subscriptions, pre-orders
tags: payment-methods
current wccom url: https://woocommerce.com/document/check-if-payment-gateway-supports-refunds-subscriptions-preorders/
---
# Check if a Payment Method Support Refunds, Subscriptions or Pre-orders
If a payment method's documentation doesnt clearly outline the supported features, you can often find what features are supported by looking at payment methods code.
Payment methods can add support for certain features from WooCommerce and its extensions. For example, a payment method can support refunds, subscriptions or pre-orders functionality.
## Simplify Commerce example
Taking the Simplify Commerce payment method as an example, open the plugin files in your favorite editor and search for `$this->supports`. You'll find the supported features:
```php
class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway {
/** * Constructor */
public function __construct() {
$this->id
= 'simplify_commerce';
$this->method_title
= __( 'Simplify Commerce', 'woocommerce' );
$this->method_description = __( 'Take payments via Simplify Commerce - uses simplify.js to create card tokens and the Simplify Commerce SDK. Requires SSL when sandbox is disabled.', 'woocommerce' );
$this->has_fields = true;
$this->supports = array(
'subscriptions',
'products',
'subscription_cancellation',
'subscription_reactivation',
'subscription_suspension',
'subscription_amount_changes',
'subscription_payment_method_change',
'subscription_date_changes',
'default_credit_card_form',
'refunds',
'pre-orders'
);
```
If you dont find `$this->supports` in the plugin files, that may mean that the payment method isnt correctly declaring support for refunds, subscripts or pre-orders.

View File

@ -0,0 +1,81 @@
---
post_title: Code snippets for configuring special tax scenarios
menu_title: Configuring special tax scenarios
tags: code-snippet, tax
current wccom url: https://woocommerce.com/document/setting-up-taxes-in-woocommerce/configuring-specific-tax-setups-in-woocommerce/#configuring-special-tax-setups
---
# Code snippets for configuring special tax scenarios
## Scenario A: Charge the same price regardless of location and taxes
Scenario A: Charge the same price regardless of location and taxes
If a store enters product prices including taxes, but levies various location-based tax rates, the prices will appear to change depending on which tax rate is applied. In reality, the base price remains the same, but the taxes influence the total. [Follow this link for a detailed explanation](https://woocommerce.com/document/how-taxes-work-in-woocommerce/#cross-border-taxes).
Some merchants prefer to dynamically change product base prices to account for the changes in taxes and so keep the total price consistent regardless of tax rate. Enable that functionality by adding the following snippet to your child themes functions.php file or via a code snippet plugin.
```php
<?php
add_filter( 'woocommerce_adjust_non_base_location_prices', '__return_false' );
```
## Scenario B: Charge tax based on the subtotal amount
The following snippet is useful in case where a store only ads taxes when the subtotal reaches a specified minimum. In the code snippet below that minimum is 110 of the stores currency. Adjust the snippet according to your requirements.
```php
<?php
add_filter( 'woocommerce_product_get_tax_class', 'big_apple_get_tax_class', 1, 2 );
function big_apple_get_tax_class( $tax_class, $product ) {
if ( WC()->cart->subtotal <= 110 )
$tax_class = 'Zero Rate';
return $tax_class;
}
```
## Scenario C: Apply different tax rates based on the customer role
Some merchants may require different tax rates to be applied based on a customer role to accommodate for wholesale status or tax exemption.
To enable this functionality, add the following snippet to your child themes functions.php file or via a code snippet plugin. In this snippet, users with “administrator” capabilities will be assigned the **Zero rate tax class**. Adjust it according to your requirements.
```php
<?php
/**
* Apply a different tax rate based on the user role.
*/
function wc_diff_rate_for_user( $tax_class, $product ) {
if ( is_user_logged_in() && current_user_can( 'administrator' ) ) {
$tax_class = 'Zero Rate';
}
return $tax_class;
}
add_filter( 'woocommerce_product_get_tax_class', 'wc_diff_rate_for_user', 1, 2 );
add_filter( 'woocommerce_product_variation_get_tax_class', 'wc_diff_rate_for_user', 1, 2 );
```
## Scenario D: Show 0 value taxes
Taxes that have 0-value are hidden by default. To show them regardless, add the following snippet to your themes functions.php file or via a code snippet plugins:
```php
add_filter( 'woocommerce_order_hide_zero_taxes', '__return_false' );
```
## Scenario E: Suffixes on the main variable product
One of the tax settings for WooCommerce enables the use of suffixes to add additional information to product prices. Its available for use with the variations of a variable product, but is disabled at the main variation level as it can impact website performance when there are many variations.
The method responsible for the related price output can be customized via filter hooks if needed for variable products. This will require customization that can be implemented via this filter:
```php
add_filter( 'woocommerce_show_variation_price', '__return_true' );
```

View File

@ -0,0 +1,13 @@
---
post_title: Disabling Marketplace Suggestions Programmatically
menu_title: Disabling marketplace suggestions
current wccom url: https://woocommerce.com/document/woocommerce-marketplace-suggestions-settings/#section-6
---
## Disabling Marketplace Suggestions Programmatically
For those who prefer to programmatically disable marketplace suggestions that are fetched from woocommerce.com, add the `woocommerce_allow_marketplace_suggestions` filter to your themes `functions.php` or a custom plugin.
For example:
This filter will completely remove Marketplace Suggestions from your WooCommerce admin.

View File

@ -0,0 +1,41 @@
---
post_title: Displaying Custom Fields in Your Theme or Site
menu_title: Displaying custom fields in theme
tags: code-snippet
current wccom url: https://woocommerce.com/document/custom-product-fields/
---
## Displaying Custom Fields in Your Theme or Site
You can use the metadata from custom fields you add to your products to display the added information within your theme or site.
To display the custom fields for each product, you have to edit your themes files. Heres an example of how you might display a custom field within the single product pages after the short description:
![image](https://github.com/woocommerce/woocommerce-developer-advocacy/assets/15178758/ed417ed8-4462-45b9-96b6-c0141afaeb2b)
```php
<?php
// Display a product custom field within single product pages after the short description
function woocommerce_custom_field_example() {
if ( ! is_product() ) {
return;
}
global $product;
if ( ! is_object( $product ) ) {
$product = wc_get_product( get_the_ID() );
}
$custom_field_value = get_post_meta( $product->get_id(), 'woo_custom_field', true );
if ( ! empty( $custom_field_value ) ) {
echo '<div class="custom-field">' . esc_html( $custom_field_value ) . '</div>';
}
}
add_action( 'woocommerce_before_add_to_cart_form', 'woocommerce_custom_field_example', 10 );
```

View File

@ -0,0 +1,147 @@
---
post_title: Free Shipping Customizations
menu_title: Free shipping customizations
tags: code-snippets
current wccom url: https://woocommerce.com/document/free-shipping/#advanced-settings-customization
combined with: https://woocommerce.com/document/hide-other-shipping-methods-when-free-shipping-is-available/#use-a-plugin
---
## Free Shipping: Advanced Settings/Customization
### Overview
By default, WooCommerce shows all shipping methods that match the customer and the cart contents. This means Free Shipping also shows along with Flat Rate and other Shipping Methods.
The functionality to hide all other methods, and only show Free Shipping, requires either custom PHP code or a plugin/extension.
### Adding code
Before adding snippets, clear your WooCommerce cache. Go to WooCommerce > System Status > Tools > WooCommerce Transients > Clear transients.
Add this code to your child themes `functions.php`, or via a plugin that allows custom functions to be added. Please dont add custom code directly to a parent themes `functions.php` as changes are entirely erased when a parent theme updates.
## Code Snippets
### Enabling or Disabling Free Shipping via Hooks
You can hook into the `is_available` function of the free shipping method.
```php
return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available );
```
This means you can use `add_filter()` on `woocommerce_shipping_free_shipping_is_available` and return `true` or `false`.
### How do I only show Free Shipping?
The following snippet hides everything but `free_shipping`, if its available and the customer's cart qualifies.
```php
/**
* Hide shipping rates when free shipping is available.
* Updated to support WooCommerce 2.6 Shipping Zones.
*
* @param array $rates Array of rates found for the package.
* @return array
*/
function my_hide_shipping_when_free_is_available( $rates ) {
$free = array();
foreach ( $rates as $rate_id => $rate ) {
if ( 'free_shipping' === $rate->method_id ) {
$free[ $rate_id ] = $rate;
break;
}
}
return ! empty( $free ) ? $free : $rates;
}
add_filter( 'woocommerce_package_rates', 'my_hide_shipping_when_free_is_available', 100 );
```
### How do I only show Local Pickup and Free Shipping?
The snippet below hides everything but `free_shipping` and `local_pickup`, if its available and the customer's cart qualifies.
```php
/**
* Hide shipping rates when free shipping is available, but keep "Local pickup"
* Updated to support WooCommerce 2.6 Shipping Zones
*/
function hide_shipping_when_free_is_available( $rates, $package ) {
$new_rates = array();
foreach ( $rates as $rate_id => $rate ) {
// Only modify rates if free_shipping is present.
if ( 'free_shipping' === $rate->method_id ) {
$new_rates[ $rate_id ] = $rate;
break;
}
}
if ( ! empty( $new_rates ) ) {
//Save local pickup if it's present.
foreach ( $rates as $rate_id => $rate ) {
if ('local_pickup' === $rate->method_id ) {
$new_rates[ $rate_id ] = $rate;
break;
}
}
return $new_rates;
}
return $rates;
}
add_filter( 'woocommerce_package_rates', 'hide_shipping_when_free_is_available', 10, 2 );
```
### Only show free shipping in all states except…
This snippet results in showing only free shipping in all states except the exclusion list. It hides free shipping if the customer is in one of the states listed:
```php
/**
* Hide ALL shipping options when free shipping is available and customer is NOT in certain states
*
* Change $excluded_states = array( 'AK','HI','GU','PR' ); to include all the states that DO NOT have free shipping
*/
add_filter( 'woocommerce_package_rates', 'hide_all_shipping_when_free_is_available' , 10, 2 );
/**
* Hide ALL Shipping option when free shipping is available
*
* @param array $available_methods
*/
function hide_all_shipping_when_free_is_available( $rates, $package ) {
$excluded_states = array( 'AK','HI','GU','PR' );
if( isset( $rates['free_shipping'] ) AND !in_array( WC()->customer->shipping_state, $excluded_states ) ) :
// Get Free Shipping array into a new array
$freeshipping = array();
$freeshipping = $rates['free_shipping'];
// Empty the $available_methods array
unset( $rates );
// Add Free Shipping back into $avaialble_methods
$rates = array();
$rates[] = $freeshipping;
endif;
if( isset( $rates['free_shipping'] ) AND in_array( WC()->customer->shipping_state, $excluded_states ) ) {
// remove free shipping option
unset( $rates['free_shipping'] );
}
return $rates;
}
```
### Enable Shipping Methods on a per Class / Product Basis, split orders, or other scenarios?
Need more flexibility? Take a look at our [premium Shipping Method extensions](https://woocommerce.com/product-category/woocommerce-extensions/shipping-methods/).

View File

@ -0,0 +1,37 @@
---
post_title: Legacy Local Pickup Advanced Settings and Customization
tags: code-snippet
current wccom url: https://woocommerce.com/document/local-pickup/#advanced-settings-customization
note: Docs links out to Skyverge's site for howto add a custom email - do we have our own alternative?
---
# Advanced settings and customization for legacy Local Pickup
## Disable local taxes when using local pickup
Local Pickup calculates taxes based on your stores location (address) by default, and not the customers address. Add this snippet at the end of your theme's `functions.php` to use your standard tax configuration instead:
```php
add_filter( 'woocommerce_apply_base_tax_for_local_pickup', '__return_false' );
```
Regular taxes is then used when local pickup is selected, instead of store-location-based taxes.
## Changing the location for local taxes
To charge local taxes based on the postcode and city of the local pickup location, you need to define the shops base city and post code using this example code:
```php
add_filter( 'woocommerce_countries_base_postcode', create_function( '', 'return "80903";' ) );
add_filter( 'woocommerce_countries_base_city', create_function( '', 'return "COLORADO SPRINGS";' ) );
```
Update `80903` to reflect your preferred postcode/zip, and `COLORADO SPRINGS` with your preferred town or city.
## Custom emails for local pickup
_Shipping Address_ is not displayed on the admin order emails when Local Pickup is used as the shipping method.
Since all core shipping options use the standard order flow, customers receive the same order confirmation email whether they select local pickup or any other shipping option.
Use this guide to create custom emails for local pickup if youd like to send a separate email for local pickup orders: [How to Add a Custom WooCommerce Email](https://www.skyverge.com/blog/how-to-add-a-custom-woocommerce-email/).

View File

@ -0,0 +1,29 @@
---
post_title: Making your translation upgrade safe
menu_title: Translation upgrade safety
tags: code-snippet
current wccom url: https://woocommerce.com/document/woocommerce-localization/#making-your-translation-upgrade-safe
---
# Making your translation upgrade safe
Like all other plugins, WooCommerce keeps translations in `wp-content/languages/plugins`.
However, if you want to include a custom translation, you can add them to `wp-content/languages/woocommerce`, or you can use a snippet to load a custom translation stored elsewhere:
```php
// Code to be placed in functions.php of your theme or a custom plugin file.
add_filter( 'load_textdomain_mofile', 'load_custom_plugin_translation_file', 10, 2 );
/*
* Replace 'textdomain' with your plugin's textdomain. e.g. 'woocommerce'.
* File to be named, for example, yourtranslationfile-en_GB.mo
* File to be placed, for example, wp-content/lanaguages/textdomain/yourtranslationfile-en_GB.mo
*/
function load_custom_plugin_translation_file( $mofile, $domain ) {
if ( 'textdomain' === $domain ) {
$mofile = WP_LANG_DIR . '/textdomain/yourtranslationfile-' . get_locale() . '.mo';
}
return $mofile;
}
```

View File

@ -0,0 +1,210 @@
---
post_title: Shipping Method API
menu_title: Shipping method API
tags: shipping, API
current wccom url: https://woocommerce.com/document/shipping-method-api/
---
# Shipping Method API
WooCommerce has a shipping method API which plugins can use to add their own rates. This article outlines steps to create a new shipping method and interact with the API.
## Create a plugin
First, create a regular WordPress/WooCommerce plugin (see [Create a plugin](https://woocommerce.com/document/create-a-plugin/)). Youll define your shipping method class in this plugin file and maintain it outside of WooCommerce.
## Create a function to house your class
To ensure the classes you need to extend exist, you should wrap your class in a function which is called after all plugins are loaded:
```php
function your_shipping_method_init() {
// Your class will go here
}
add_action( 'woocommerce_shipping_init', 'your_shipping_method_init' );
```
## Create your class
Create your class and place it inside the function you just created. Make sure it extends the shipping method class so that you have access to the API. Youll see below we also init our shipping method options.
```php
if ( ! class_exists( 'WC_Your_Shipping_Method' ) ) {
class WC_Your_Shipping_Method extends WC_Shipping_Method {
/**
* Constructor for your shipping class
*
* @access public
* @return void
*/
public function __construct() {
$this->id = 'your_shipping_method';
$this->title = __( 'Your Shipping Method' );
$this->method_description = __( 'Description of your shipping method' ); //
$this->enabled = "yes"; // This can be added as an setting but for this example its forced enabled
$this->init();
}
/**
* Init your settings
*
* @access public
* @return void
*/
function init() {
// Load the settings API
$this->init_form_fields(); // This is part of the settings API. Override the method to add your own settings
$this->init_settings(); // This is part of the settings API. Loads settings you previously init.
// Save settings in admin if you have any defined
add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
}
/**
* calculate_shipping function.
*
* @access public
* @param mixed $package
* @return void
*/
public function calculate_shipping( $package ) {
// This is where you'll add your rates
}
}
}
```
As well as declaring your class, you also need to tell WooCommerce it exists with another function:
```php
function add_your_shipping_method( $methods ) {
$methods['your_shipping_method'] = 'WC_Your_Shipping_Method';
return $methods;
}
add_filter( 'woocommerce_shipping_methods', 'add_your_shipping_method' );
```
## Defining settings/options
You can define your options once the above is in place by using the settings API. In the snippets above youll notice we `init_form_fields` and `init_settings`. These load up the settings API. To see how to add settings, see [WooCommerce settings API](https://woocommerce.com/document/settings-api/).
## The calculate_shipping() method
Add your rates by usign the `calculate_shipping()` method. WooCommerce calls this when doing shipping calculations. Do your plugin specific calculations here and then add the rates via the API. Like so:
```php
$rate = array(
'label' => "Label for the rate",
'cost' => '10.99',
'calc_tax' => 'per_item'
);
// Register the rate
$this->add_rate( $rate );
```
`Add_rate` takes an array of options. The defaults/possible values for the array are as follows:
```php
$defaults = array(
'label' => '', // Label for the rate
'cost' => '0', // Amount for shipping or an array of costs (for per item shipping)
'taxes' => '', // Pass an array of taxes, or pass nothing to have it calculated for you, or pass 'false' to calculate no tax for this method
'calc_tax' => 'per_order' // Calc tax per_order or per_item. Per item needs an array of costs passed via 'cost'
);
```
Your shipping method can pass as many rates as you want just ensure that the id for each is different. The user will get to choose rate during checkout.
## Piecing it all together
The skeleton shipping method code all put together looks like this:
```php
<?php
/*
Plugin Name: Your Shipping plugin
Plugin URI: https://woocommerce.com/
Description: Your shipping method plugin
Version: 1.0.0
Author: WooThemes
Author URI: https://woocommerce.com/
*/
/**
* Check if WooCommerce is active
*/
if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {
function your_shipping_method_init() {
if ( ! class_exists( 'WC_Your_Shipping_Method' ) ) {
class WC_Your_Shipping_Method extends WC_Shipping_Method {
/**
* Constructor for your shipping class
*
* @access public
* @return void
*/
public function __construct() {
$this->id = 'your_shipping_method'; // Id for your shipping method. Should be uunique.
$this->method_title = __( 'Your Shipping Method' ); // Title shown in admin
$this->method_description = __( 'Description of your shipping method' ); // Description shown in admin
$this->enabled = "yes"; // This can be added as an setting but for this example its forced enabled
$this->title = "My Shipping Method"; // This can be added as an setting but for this example its forced.
$this->init();
}
/**
* Init your settings
*
* @access public
* @return void
*/
function init() {
// Load the settings API
$this->init_form_fields(); // This is part of the settings API. Override the method to add your own settings
$this->init_settings(); // This is part of the settings API. Loads settings you previously init.
// Save settings in admin if you have any defined
add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
}
/**
* calculate_shipping function.
*
* @access public
* @param array $package
* @return void
*/
public function calculate_shipping( $package = array() ) {
$rate = array(
'label' => $this->title,
'cost' => '10.99',
'calc_tax' => 'per_item'
);
// Register the rate
$this->add_rate( $rate );
}
}
}
}
add_action( 'woocommerce_shipping_init', 'your_shipping_method_init' );
function add_your_shipping_method( $methods ) {
$methods['your_shipping_method'] = 'WC_Your_Shipping_Method';
return $methods;
}
add_filter( 'woocommerce_shipping_methods', 'add_your_shipping_method' );
}
```
For further information, please check out the [Shipping Method API Wiki](https://github.com/woocommerce/woocommerce/wiki/Shipping-Method-API).

View File

@ -0,0 +1,22 @@
---
post_title: SSL and HTTPS and WooCommerce
menu_title: SSL and HTTPS and WooCommerce
tags: code-snippet
current wccom url: https://woocommerce.com/document/ssl-and-https/#websites-behind-load-balancers-or-reverse-proxies
---
## Websites behind load balancers or reverse proxies
WooCommerce uses the `is_ssl()` WordPress function to verify if your website using SSL or not.
`is_ssl()` checks if the connection is via HTTPS or on Port 443. However, this wont work for websites behind load balancers, especially websites hosted at Network Solutions. For details, read [WordPress is_ssl() function reference notes](https://codex.wordpress.org/Function_Reference/is_ssl#Notes).
Websites behind load balancers or reverse proxies that support `HTTP_X_FORWARDED_PROTO` can be fixed by adding the following code to the `wp-config.php` file, above the require_once call:
```php
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' == $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
$_SERVER['HTTPS'] = 'on';
}
```
**Note:** If you use CloudFlare, you need to configure it. Check their documentation.

View File

@ -0,0 +1,26 @@
---
post_title: Uninstall and remove all WooCommerce Data
menu_title: Uninstalling and removing data
tags: code-snippet
current wccom url: https://woocommerce.com/document/installing-uninstalling-woocommerce/#uninstalling-woocommerce
---
# Uninstall and remove all WooCommerce Data
The WooCommerce plugin can be uninstalled like any other WordPress plugin. By default, the WooCommerce data is left in place though.
If you need to remove *all* WooCommerce data as well, including products, order data, coupons, etc., you need to to modify the sites `wp-config.php` *before* deactivating and deleting the WooCommerce plugin.
As this action is destructive and permanent, the information is provided as is. WooCommerce Support cannot help with this process or anything that happens as a result.
To fully remove all WooCommerce data from your WordPress site, open `wp-config.php`, scroll down to the bottom of the file, and add the following constant on its own line above `/* Thats all, stop editing. */`.
```php
define( 'WC_REMOVE_ALL_DATA', true );
/* Thats all, stop editing! Happy publishing. */
```
Then, once the changes are saved to the file, when you deactivate and delete WooCommerce, all of its data is removed from your WordPress site database.
![Uninstall WooCommerce WPConfig](https://woocommerce.com/wp-content/uploads/2020/03/uninstall_wocommerce_plugin_wpconfig.png)

View File

@ -0,0 +1,35 @@
---
post_title: Using NGINX server to protect your upload directory
menu_title: NGINX server to protect upload directory
tags: code-snippet
current wccom url: https://woocommerce.com/document/digital-downloadable-product-handling/#protecting-your-uploads-directory
---
## Using NGINX server to protect your upload directory
If you using NGINX server for your site along with **X-Accel-Redirect/X-Sendfile** or **Force Downloads** download method, it is necessary that you add this configuration for better security:
```php
# Protect WooCommerce upload folder from being accessed directly.
# You may want to change this config if you are using "X-Accel-Redirect/X-Sendfile" or "Force Downloads" method for downloadable products.
# Place this config towards the end of "server" block in NGINX configuration.
location ~* /wp-content/uploads/woocommerce_uploads/ {
if ( $upstream_http_x_accel_redirect = "" ) {
return 403;
}
internal;
}
```
And this the configuration in case you are using **Redirect only** download method:
```php
# Protect WooCommerce upload folder from being accessed directly.
# You may want to change this config if you are using "Redirect Only" method for downloadable products.
# Place this config towards the end of "server" block in NGINX configuration.
location ~* /wp-content/uploads/woocommerce_uploads/ {
autoindex off;
}
```
If you do not know which web server you are using, please reach out to your host along with a link to this support page.

View File

@ -52,6 +52,15 @@
"category_slug": "code-snippets", "category_slug": "code-snippets",
"category_title": "Code Snippets", "category_title": "Code Snippets",
"posts": [ "posts": [
{
"post_title": "Using NGINX server to protect your upload directory",
"menu_title": "NGINX server to protect upload directory",
"tags": "code-snippet",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/using_nginx_server_to_protect_your_uploads_directory.md",
"hash": "5d7afe5c8217c3a5f753eb2f468b8304f7f9b5b1275461abf2146e4de82ed6b2",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/using_nginx_server_to_protect_your_uploads_directory.md",
"id": "8b325d3483f9a8d09961ca1082839752137faebf"
},
{ {
"post_title": "Useful core functions", "post_title": "Useful core functions",
"tags": "code-snippet", "tags": "code-snippet",
@ -60,6 +69,15 @@
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/useful-functions.md", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/useful-functions.md",
"id": "0d99f1dee7c104b5899fd62b96157fb6709ebfb8" "id": "0d99f1dee7c104b5899fd62b96157fb6709ebfb8"
}, },
{
"post_title": "Uninstall and remove all WooCommerce Data",
"menu_title": "Uninstalling and removing data",
"tags": "code-snippet",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/uninstall_remove_all_woocommerce_data.md",
"hash": "73483ff158ceac81685a9cd52335dc98e99ac7f84d89cdbcf4ce994e18afe30d",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/uninstall_remove_all_woocommerce_data.md",
"id": "36b571fcf2471737729ab4769e2c721b2248187f"
},
{ {
"post_title": "Unhook and remove WooCommerce emails", "post_title": "Unhook and remove WooCommerce emails",
"tags": "code-snippet", "tags": "code-snippet",
@ -68,6 +86,24 @@
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/unhook--remove-woocommerce-emails.md", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/unhook--remove-woocommerce-emails.md",
"id": "0fdfe3b483ae74a9e5dc1fc21b80814462222ec3" "id": "0fdfe3b483ae74a9e5dc1fc21b80814462222ec3"
}, },
{
"post_title": "SSL and HTTPS and WooCommerce",
"menu_title": "SSL and HTTPS and WooCommerce",
"tags": "code-snippet",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/ssl_and_https_and_woocommerce_websites_behind_load_balanacers_or_reverse_proxies.md",
"hash": "92a5091c27d1af6c0b49df143dd13886fb2cb30538fa877f68000cab69f4f502",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/ssl_and_https_and_woocommerce_websites_behind_load_balanacers_or_reverse_proxies.md",
"id": "78d5b5a20ce6471b74f809386eff41fffe2d1adb"
},
{
"post_title": "Shipping Method API",
"menu_title": "Shipping method API",
"tags": "shipping, API",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/shipping_method_api.md",
"hash": "bd7cbc361fe94acaa40fbc5befa8d14f302705a9d700dd7d7e78a482b003fe0b",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/shipping_method_api.md",
"id": "a419b97e5594918a015c61227ad9226c509eb314"
},
{ {
"post_title": "Rename a country", "post_title": "Rename a country",
"tags": "code-snippet", "tags": "code-snippet",
@ -84,6 +120,15 @@
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/number-of-products-per-row.md", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/number-of-products-per-row.md",
"id": "7369dc328c49206771a2f8d0da5d920c480b5207" "id": "7369dc328c49206771a2f8d0da5d920c480b5207"
}, },
{
"post_title": "Making your translation upgrade safe",
"menu_title": "Translation upgrade safety",
"tags": "code-snippet",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/making_translations_upgrade_safe.md",
"hash": "e2d296630d7af888a072de51870f3b4ff311b3c29f706fda735bd8f9122c8710",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/making_translations_upgrade_safe.md",
"id": "0c1add87ef9f5452b4c8404bb55021ad8265c171"
},
{ {
"post_title": "Add link to logged data", "post_title": "Add link to logged data",
"menu_title": "Add link to logged data", "menu_title": "Add link to logged data",
@ -93,6 +138,40 @@
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/link-to-logged-data.md", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/link-to-logged-data.md",
"id": "34da337f79be5ce857024f541a99d302174ca37d" "id": "34da337f79be5ce857024f541a99d302174ca37d"
}, },
{
"post_title": "Legacy Local Pickup Advanced Settings and Customization",
"tags": "code-snippet",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/legacy_local_pickup_advacned_settings_and_customization.md",
"hash": "d0269f1ee2700356672a032e4e54491666b901765045f7c5224ef07eeb9d9598",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/legacy_local_pickup_advacned_settings_and_customization.md",
"id": "c4d4a2276fc251082a80a8330eea1eb62a97c3bb"
},
{
"post_title": "Free Shipping Customizations",
"menu_title": "Free shipping customizations",
"tags": "code-snippets",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/free_shipping_customization.md",
"hash": "c18884a45e4e1cc7b174820c2553d2722df95b98f6783c2700096a5b7e19bffd",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/free_shipping_customization.md",
"id": "cac6f1ccd661588e9a5fa7405643e9c6d4da388e"
},
{
"post_title": "Displaying Custom Fields in Your Theme or Site",
"menu_title": "Displaying custom fields in theme",
"tags": "code-snippet",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/displaying_custom_fields_in_your_theme_or_site.md",
"hash": "8048c2e9e5d25268d17d4f4ca7929e265eddbd4653318dd8f544856ddecd39dd",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/displaying_custom_fields_in_your_theme_or_site.md",
"id": "3e3fd004afda355cf9dbb05f0967523d6d0da1ce"
},
{
"post_title": "Disabling Marketplace Suggestions Programmatically",
"menu_title": "Disabling marketplace suggestions",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/disabling_marketplace_suggestions_programmatically.md",
"hash": "3d5bd50d64a46efaea99efb0a87dfdb8882cb83598b7be8a8154ad0e464eb6f5",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/disabling_marketplace_suggestions_programmatically.md",
"id": "94a7a28e5dd3d9394650e66abec2429445e87028"
},
{ {
"post_title": "Customizing checkout fields using actions and filters", "post_title": "Customizing checkout fields using actions and filters",
"tags": "code-snippet", "tags": "code-snippet",
@ -101,6 +180,24 @@
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/customising-checkout-fields.md", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/customising-checkout-fields.md",
"id": "83097d3b7414557fc80dcf9f8f1a708bbdcdd884" "id": "83097d3b7414557fc80dcf9f8f1a708bbdcdd884"
}, },
{
"post_title": "Code snippets for configuring special tax scenarios",
"menu_title": "Configuring special tax scenarios",
"tags": "code-snippet, tax",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/configuring_special_tax_scenarios.md",
"hash": "128193e0e980f484f354c93e59d34c3948f112e4a1c99158cf3e5d9969db9352",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/configuring_special_tax_scenarios.md",
"id": "a8ab8b6734ba2ac5af7c6653635d15548abdab2a"
},
{
"post_title": "Check if a Payment Method Support Refunds, Subscriptions or Pre-orders",
"menu_title": "Payment method support for refunds, subscriptions, pre-orders",
"tags": "payment-methods",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/check_payment_method_support.md",
"hash": "6cae4b1fda5980c327c99d6bae8b1978fd05849f07179f0699a174b57d27b862",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/check_payment_method_support.md",
"id": "2919c9fc523bce46f43a5f35f821d0c6623c5ede"
},
{ {
"post_title": "Change a currency symbol", "post_title": "Change a currency symbol",
"tags": "code-snippet", "tags": "code-snippet",
@ -350,7 +447,7 @@
{ {
"post_title": "Logging in WooCommerce", "post_title": "Logging in WooCommerce",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/logging.md", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/logging.md",
"hash": "844689b6d9c482fb217a512db6ddab0afd4d76b2b9e378a9302681d2a8dfe517", "hash": "7e66b9ea605944c5926cf6099fb8fb323976c014fef7dd768c91cef17b091edd",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/logging.md", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/logging.md",
"id": "c684e2efba45051a4e1f98eb5e6ef6bab194f25c" "id": "c684e2efba45051a4e1f98eb5e6ef6bab194f25c"
}, },
@ -716,7 +813,7 @@
"post_title": "Product editor development handbook", "post_title": "Product editor development handbook",
"menu_title": "Development handbook", "menu_title": "Development handbook",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-editor-development/product-editor.md", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-editor-development/product-editor.md",
"hash": "cc5f82b66e949e3df2928b5e6b1217e8804c43b8e7b75ebc930cd0f90aef7bbe", "hash": "b574a4a5476899342cd229033a22ecdf9859914ea34446f8276e2b0ad5cb8c7f",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-editor-development/product-editor.md", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-editor-development/product-editor.md",
"id": "59450404de2750d918137e7cf523e52bedfd7214", "id": "59450404de2750d918137e7cf523e52bedfd7214",
"links": { "links": {
@ -1367,5 +1464,5 @@
"categories": [] "categories": []
} }
], ],
"hash": "cf89b5c07e5b7a9007eb4afe021b02ddce4611c81a863b0a431ae57491a3b37f" "hash": "d8fe058ebcce4d1f40585f1634b1e98e27063d81dd0dd7783217ab60d0bc915a"
} }

View File

@ -140,9 +140,9 @@ wc_get_logger()->info(
### Best practices ### Best practices
* Rather than using the `WC_Logger`s `log()` method directly, it's better to use one of the wrapper methods that's specific to the log level. E.g. `info()` or `error()`. * Rather than using the `WC_Logger`'s `log()` method directly, it's better to use one of the wrapper methods that's specific to the log level. E.g. `info()` or `error()`.
* Write a message that is a complete, coherent sentence. This will make it more useful for people who aren't familiar with the codebase. * Write a message that is a complete, coherent sentence. This will make it more useful for people who aren't familiar with the codebase.
* Log messages should not be translatable (see the discussion about this in the comments). Keeping the message in English makes it easier to search for solutions based on the message contents, and also makes it easier for Happiness Engineers to understand what's happening, since they may not speak the same language as the site owner. * Log messages should not be translatable. Keeping the message in English makes it easier to search for solutions based on the message contents, and also makes it easier for anyone troubleshooting to understand what's happening, since they may not speak the same language as the site owner.
* Ideally, each log entry message should be a single line (i.e. no line breaks within the message string). Additional lines or extra data should be put in the context array. * Ideally, each log entry message should be a single line (i.e. no line breaks within the message string). Additional lines or extra data should be put in the context array.
* Avoid outputting structured data in the message string. Put it in a key in the context array instead. The logger will handle converting it to JSON and making it legible in the log viewer. * Avoid outputting structured data in the message string. Put it in a key in the context array instead. The logger will handle converting it to JSON and making it legible in the log viewer.
* If you need to include a stack trace, let the logger generate it for you. * If you need to include a stack trace, let the logger generate it for you.
@ -159,7 +159,7 @@ The `WC_Logger` class can be substituted for another class via the `woocommerce_
In WooCommerce, a log handler is a PHP class that takes the raw log data and transforms it into a log entry that can be stored or dispatched. WooCommerce ships with four different log handler classes: In WooCommerce, a log handler is a PHP class that takes the raw log data and transforms it into a log entry that can be stored or dispatched. WooCommerce ships with four different log handler classes:
* `Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2`: This is the default handler, representing the "file system" log storage method. It records log entries to files. * `Automattic\\WooCommerce\\Internal\\Admin\\Logging\\LogHandlerFileV2`: This is the default handler, representing the "file system" log storage method. It records log entries to files.
* `WC_Log_Handler_File`: This is the old default handler that also records log entries to files. It may be deprecated in the future, and it is not recommended to use this class or extend it. * `WC_Log_Handler_File`: This is the old default handler that also records log entries to files. It may be deprecated in the future, and it is not recommended to use this class or extend it.
* `WC_Log_Handler_DB`: This handler represents the "database" log storage method. It records log entries to the database. * `WC_Log_Handler_DB`: This handler represents the "database" log storage method. It records log entries to the database.
* `WC_Log_Handler_Email`: This handler does not store log entries, but instead sends them as email messages. Emails are sent to the site admin email address. This handler has [some limitations](https://github.com/woocommerce/woocommerce/blob/fe81a4cf27601473ad5c394a4f0124c785aaa4e6/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-email.php#L15-L27). * `WC_Log_Handler_Email`: This handler does not store log entries, but instead sends them as email messages. Emails are sent to the site admin email address. This handler has [some limitations](https://github.com/woocommerce/woocommerce/blob/fe81a4cf27601473ad5c394a4f0124c785aaa4e6/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-email.php#L15-L27).
@ -185,6 +185,10 @@ add_filter( 'woocommerce_register_log_handlers', 'my_wc_log_handlers' );
You may want to create your own log handler class in order to send logs somewhere else, such as a Slack channel or perhaps an InfluxDB instance. Your class must extend the [`WC_Log_Handler`](https://woocommerce.github.io/code-reference/classes/WC-Log-Handler.html) abstract class and implement the [`WC_Log_Handler_Interface`](https://woocommerce.github.io/code-reference/classes/WC-Log-Handler-Interface.html) interface. The [`WC_Log_Handler_Email`](https://github.com/woocommerce/woocommerce/blob/6688c60fe47ad42d49deedab8be971288e4786c1/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-email.php) handler class provides a good example of how to set this up. You may want to create your own log handler class in order to send logs somewhere else, such as a Slack channel or perhaps an InfluxDB instance. Your class must extend the [`WC_Log_Handler`](https://woocommerce.github.io/code-reference/classes/WC-Log-Handler.html) abstract class and implement the [`WC_Log_Handler_Interface`](https://woocommerce.github.io/code-reference/classes/WC-Log-Handler-Interface.html) interface. The [`WC_Log_Handler_Email`](https://github.com/woocommerce/woocommerce/blob/6688c60fe47ad42d49deedab8be971288e4786c1/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-email.php) handler class provides a good example of how to set this up.
### Log file storage location
When using the "file system" log handler, by default the log files are stored in the `wc-logs` subdirectory of the WordPress `uploads` directory, which means they might be publicly accessible. WooCommerce adds an `.htaccess` file to prevent access to `wc-logs`, but not all web servers recognize that file. If you have the option, you may want to consider storing your log files in a directory outside of the web root. Make sure the directory has the same user/group permissions as the `uploads` directory so that WordPress can access it. Then use the `woocommerce_log_directory` filter hook to set the path to your custom directory.
### Turning off noisy logs ### Turning off noisy logs
If there is a particular log that is recurring frequently and clogging up your log files, you should probably figure out why it keeps getting triggered and resolve the issue. However, if that's not possible, you can add a callback to the `woocommerce_logger_log_message` filter hook to ignore that particular log while still allowing other logs to get through: If there is a particular log that is recurring frequently and clogging up your log files, you should probably figure out why it keeps getting triggered and resolve the issue. However, if that's not possible, you can add a callback to the `woocommerce_logger_log_message` filter hook to ignore that particular log while still allowing other logs to get through:

View File

@ -39,3 +39,4 @@ Please note that this check is currently not being enforced: the product editor
- [Examples on Template API usage](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/ProductTemplates/README.md/) - [Examples on Template API usage](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/ProductTemplates/README.md/)
- [Related hooks and Template API documentation](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/BlockTemplates/README.md) - [Related hooks and Template API documentation](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/BlockTemplates/README.md)
- [Generic blocks documentation](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/blocks/generic/README.md) - [Generic blocks documentation](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/blocks/generic/README.md)
- [Validations and error handling](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/contexts/validation-context/README.md)

View File

@ -22,8 +22,8 @@
"test": "pnpm -r test", "test": "pnpm -r test",
"lint": "pnpm -r lint", "lint": "pnpm -r lint",
"cherry-pick": "node ./tools/cherry-pick/bin/run", "cherry-pick": "node ./tools/cherry-pick/bin/run",
"clean": "rimraf -g '**/node_modules' '**/.wireit' && pnpm store prune && pnpm i", "clean": "rimraf -g '**/node_modules' '**/.wireit' && pnpm store prune",
"distclean": "git clean --force -d -X", "buildclean": "git clean --force -d -X ./packages ./plugins ./tools",
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"postinstall": "pnpm git:update-hooks", "postinstall": "pnpm git:update-hooks",
"git:update-hooks": "if test -d .git; then rm -rf .git/hooks && mkdir -p .git/hooks && husky install; else husky install; fi", "git:update-hooks": "if test -d .git; then rm -rf .git/hooks && mkdir -p .git/hooks && husky install; else husky install; fi",
@ -75,6 +75,9 @@
"@types/react": "^17.0.71", "@types/react": "^17.0.71",
"react-resize-aware": "3.1.1", "react-resize-aware": "3.1.1",
"@automattic/tour-kit>@wordpress/element": "4.4.1" "@automattic/tour-kit>@wordpress/element": "4.4.1"
},
"patchedDependencies": {
"@wordpress/edit-site@5.15.0": "bin/patches/@wordpress__edit-site@5.15.0.patch"
} }
} }
} }

View File

@ -1,4 +1,4 @@
Significance: patch Significance: patch
Type: dev Type: dev
Update pnpm to 9.1.0 Update dependencies

View File

@ -39,10 +39,7 @@ describe( 'Shipping methods API tests', () => {
expect( body.method_id ).toEqual( methodId ); expect( body.method_id ).toEqual( methodId );
expect( body.method_title ).toEqual( methodTitle ); expect( body.method_title ).toEqual( methodTitle );
expect( body.enabled ).toEqual( true ); expect( body.enabled ).toEqual( true );
expect( body.settings.cost.value || '' ).toEqual( cost || '' );
if ( [ 'flat_rate', 'local_pickup' ].includes( methodId ) ) {
expect( body.settings.cost.value ).toEqual( cost );
}
// Cleanup: Delete the shipping method // Cleanup: Delete the shipping method
await shippingMethodsApi.delete.shippingMethod( await shippingMethodsApi.delete.shippingMethod(

View File

@ -10,9 +10,9 @@ features:
_\* TypeScript Definitions and Repositories are currently only supported for [Products](https://woocommerce.github.io/woocommerce-rest-api-docs/#products), and partially supported for [Orders](https://woocommerce.github.io/woocommerce-rest-api-docs/#orders)._ _\* TypeScript Definitions and Repositories are currently only supported for [Products](https://woocommerce.github.io/woocommerce-rest-api-docs/#products), and partially supported for [Orders](https://woocommerce.github.io/woocommerce-rest-api-docs/#orders)._
## Differences from @woocommerce/woocomerce-rest-api ## Differences from @woocommerce/woocommerce-rest-api
WooCommerce has two API clients in JavaScript for interacting with a WooCommerce installation's RESTful API. This package, and the [@woocommerce/woocomerce-rest-api](https://www.npmjs.com/package/@woocommerce/woocommerce-rest-api) package. WooCommerce has two API clients in JavaScript for interacting with a WooCommerce installation's RESTful API. This package, and the [@woocommerce/woocommerce-rest-api](https://www.npmjs.com/package/@woocommerce/woocommerce-rest-api) package.
The main difference between them is the Repositories and the TypeScript definitions for the supported endpoints. When using Axios directly, as you can do with both libraries, you query the WooCommerce API in a raw object format, following the [API documentation](https://woocommerce.github.io/woocommerce-rest-api-docs/#introduction) parameters. Comparatively, with the Repositories provided in this package, you have the parameters as properties of an object, which gives you the benefits of auto-complete and strict types, for instance. The main difference between them is the Repositories and the TypeScript definitions for the supported endpoints. When using Axios directly, as you can do with both libraries, you query the WooCommerce API in a raw object format, following the [API documentation](https://woocommerce.github.io/woocommerce-rest-api-docs/#introduction) parameters. Comparatively, with the Repositories provided in this package, you have the parameters as properties of an object, which gives you the benefits of auto-complete and strict types, for instance.
@ -104,7 +104,7 @@ The following methods are available on all repositories if the corresponding met
- `read( objectId )` - Read a single object of the model type - `read( objectId )` - Read a single object of the model type
- `update( objectId, {...properties} )` - Update a single object of the model type - `update( objectId, {...properties} )` - Update a single object of the model type
#### Child Repositories #### Child Repositories Use
In child model repositories, each method requires the `parentId` as the first parameter: In child model repositories, each method requires the `parentId` as the first parameter:

View File

@ -1,4 +1,4 @@
Significance: patch Significance: patch
Type: tweak Type: tweak
Comment: Fix typo (README.md)
Correct spelling errors

View File

@ -1,4 +1,4 @@
Significance: patch Significance: patch
Type: dev Type: dev
Update pnpm to 9.1.0 Update dependencies

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
CI: added a missing dev-dependency for passing tests in updated CI environment.

View File

@ -0,0 +1,5 @@
Significance: patch
Type: fix
Comment: Fix missing onEscape handle in SelectTree

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Fix typo in experimental select control tests

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Allow adding HTML to label prop in label component

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update SelectTree and Tree controls to allow highlighting items without focus

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
SelectTree: allow navigation between items and input using tab and arrow keys

View File

@ -171,6 +171,7 @@
"jest-cli": "~27.5.1", "jest-cli": "~27.5.1",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"postcss-loader": "^4.3.0", "postcss-loader": "^4.3.0",
"qrcode.react": "^3.1.0",
"react": "^17.0.2", "react": "^17.0.2",
"rimraf": "5.0.5", "rimraf": "5.0.5",
"sass-loader": "^10.5.0", "sass-loader": "^10.5.0",

View File

@ -2,14 +2,23 @@
* External dependencies * External dependencies
*/ */
import classnames from 'classnames'; import classnames from 'classnames';
import { createElement } from '@wordpress/element'; import {
createElement,
forwardRef,
useImperativeHandle,
useRef,
} from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities'; import { decodeEntities } from '@wordpress/html-entities';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import Tag from '../tag'; import Tag from '../tag';
import { getItemLabelType, getItemValueType } from './types'; import {
getItemLabelType,
getItemValueType,
SelectedItemFocusHandle,
} from './types';
type SelectedItemsProps< ItemType > = { type SelectedItemsProps< ItemType > = {
isReadOnly: boolean; isReadOnly: boolean;
@ -22,16 +31,23 @@ type SelectedItemsProps< ItemType > = {
[ key: string ]: string; [ key: string ]: string;
}; };
onRemove: ( item: ItemType ) => void; onRemove: ( item: ItemType ) => void;
onBlur?: ( event: React.FocusEvent ) => void;
onSelectedItemsEnd?: () => void;
}; };
export const SelectedItems = < ItemType, >( { const PrivateSelectedItems = < ItemType, >(
{
isReadOnly, isReadOnly,
items, items,
getItemLabel, getItemLabel,
getItemValue, getItemValue,
getSelectedItemProps, getSelectedItemProps,
onRemove, onRemove,
}: SelectedItemsProps< ItemType > ) => { onBlur,
onSelectedItemsEnd,
}: SelectedItemsProps< ItemType >,
ref: React.ForwardedRef< SelectedItemFocusHandle >
) => {
const classes = classnames( const classes = classnames(
'woocommerce-experimental-select-control__selected-items', 'woocommerce-experimental-select-control__selected-items',
{ {
@ -39,6 +55,16 @@ export const SelectedItems = < ItemType, >( {
} }
); );
const lastRemoveButtonRef = useRef< HTMLButtonElement >( null );
useImperativeHandle(
ref,
() => {
return () => lastRemoveButtonRef.current?.focus();
},
[]
);
if ( isReadOnly ) { if ( isReadOnly ) {
return ( return (
<div className={ classes }> <div className={ classes }>
@ -51,6 +77,25 @@ export const SelectedItems = < ItemType, >( {
); );
} }
const focusSibling = ( event: React.KeyboardEvent< HTMLDivElement > ) => {
const selectedItem = ( event.target as HTMLElement ).closest(
'.woocommerce-experimental-select-control__selected-item'
);
const sibling =
event.key === 'ArrowLeft' || event.key === 'Backspace'
? selectedItem?.previousSibling
: selectedItem?.nextSibling;
if ( sibling ) {
(
( sibling as HTMLElement ).querySelector(
'.woocommerce-tag__remove'
) as HTMLElement
)?.focus();
return true;
}
return false;
};
return ( return (
<div className={ classes }> <div className={ classes }>
{ items.map( ( item, index ) => { { items.map( ( item, index ) => {
@ -71,6 +116,30 @@ export const SelectedItems = < ItemType, >( {
onClick={ ( event ) => { onClick={ ( event ) => {
event.preventDefault(); event.preventDefault();
} } } }
onKeyDown={ ( event ) => {
if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight'
) {
const focused = focusSibling( event );
if (
! focused &&
event.key === 'ArrowRight' &&
onSelectedItemsEnd
) {
onSelectedItemsEnd();
}
} else if (
event.key === 'ArrowUp' ||
event.key === 'ArrowDown'
) {
event.preventDefault(); // prevent unwanted scroll
} else if ( event.key === 'Backspace' ) {
onRemove( item );
focusSibling( event );
}
} }
onBlur={ onBlur }
> >
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ } { /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
{ /* @ts-ignore Additional props are not required. */ } { /* @ts-ignore Additional props are not required. */ }
@ -78,6 +147,11 @@ export const SelectedItems = < ItemType, >( {
id={ getItemValue( item ) } id={ getItemValue( item ) }
remove={ () => () => onRemove( item ) } remove={ () => () => onRemove( item ) }
label={ getItemLabel( item ) } label={ getItemLabel( item ) }
ref={
index === items.length - 1
? lastRemoveButtonRef
: undefined
}
/> />
</div> </div>
); );
@ -85,3 +159,9 @@ export const SelectedItems = < ItemType, >( {
</div> </div>
); );
}; };
export const SelectedItems = forwardRef( PrivateSelectedItems ) as < ItemType >(
props: SelectedItemsProps< ItemType > & {
ref?: React.ForwardedRef< SelectedItemFocusHandle >;
}
) => ReturnType< typeof PrivateSelectedItems >;

View File

@ -49,7 +49,7 @@ describe( 'useAsyncFilter', () => {
expect( filter ).toHaveBeenCalledWith( inputValue ); expect( filter ).toHaveBeenCalledWith( inputValue );
} ); } );
it( 'should trigger onFilterStart at the begining of the filtering', async () => { it( 'should trigger onFilterStart at the beginning of the filtering', async () => {
const filteredItems: string[] = []; const filteredItems: string[] = [];
onFilterStart.mockImplementation( ( value = '' ) => { onFilterStart.mockImplementation( ( value = '' ) => {
@ -78,7 +78,7 @@ describe( 'useAsyncFilter', () => {
expect( filter ).toHaveBeenCalledWith( inputValue ); expect( filter ).toHaveBeenCalledWith( inputValue );
} ); } );
it( 'should trigger onFilterEnd when filtering is fullfiled', async () => { it( 'should trigger onFilterEnd when filtering is fulfilled', async () => {
const filteredItems: string[] = []; const filteredItems: string[] = [];
filter.mockResolvedValue( filteredItems ); filter.mockResolvedValue( filteredItems );

View File

@ -58,3 +58,5 @@ export type getItemLabelType< ItemType > = ( item: ItemType | null ) => string;
export type getItemValueType< ItemType > = ( export type getItemValueType< ItemType > = (
item: ItemType | null item: ItemType | null
) => string | number; ) => string | number;
export type SelectedItemFocusHandle = () => void;

View File

@ -10,6 +10,7 @@ import {
useLayoutEffect, useLayoutEffect,
useState, useState,
} from '@wordpress/element'; } from '@wordpress/element';
import { escapeRegExp } from 'lodash';
/** /**
* Internal dependencies * Internal dependencies
@ -26,6 +27,7 @@ type MenuProps = {
isLoading?: boolean; isLoading?: boolean;
position?: Popover.Position; position?: Popover.Position;
scrollIntoViewOnOpen?: boolean; scrollIntoViewOnOpen?: boolean;
highlightedIndex?: number;
items: LinkedTree[]; items: LinkedTree[];
treeRef?: React.ForwardedRef< HTMLOListElement >; treeRef?: React.ForwardedRef< HTMLOListElement >;
onClose?: () => void; onClose?: () => void;
@ -44,6 +46,7 @@ export const SelectTreeMenu = ( {
onEscape, onEscape,
shouldShowCreateButton, shouldShowCreateButton,
onFirstItemLoop, onFirstItemLoop,
onExpand,
...props ...props
}: MenuProps ) => { }: MenuProps ) => {
const [ boundingRect, setBoundingRect ] = useState< DOMRect >(); const [ boundingRect, setBoundingRect ] = useState< DOMRect >();
@ -66,7 +69,7 @@ export const SelectTreeMenu = ( {
// Scroll the selected item into view when the menu opens. // Scroll the selected item into view when the menu opens.
useEffect( () => { useEffect( () => {
if ( isOpen && scrollIntoViewOnOpen ) { if ( isOpen && scrollIntoViewOnOpen ) {
selectControlMenuRef.current?.scrollIntoView(); selectControlMenuRef.current?.scrollIntoView?.();
} }
}, [ isOpen, scrollIntoViewOnOpen ] ); }, [ isOpen, scrollIntoViewOnOpen ] );
@ -74,9 +77,10 @@ export const SelectTreeMenu = ( {
if ( ! props.createValue || ! item.children?.length ) return false; if ( ! props.createValue || ! item.children?.length ) return false;
return item.children.some( ( child ) => { return item.children.some( ( child ) => {
if ( if (
new RegExp( props.createValue || '', 'ig' ).test( new RegExp(
child.data.label escapeRegExp( props.createValue || '' ),
) 'ig'
).test( child.data.label )
) { ) {
return true; return true;
} }
@ -130,6 +134,7 @@ export const SelectTreeMenu = ( {
ref={ ref } ref={ ref }
items={ items } items={ items }
onTreeBlur={ onClose } onTreeBlur={ onClose }
onExpand={ onExpand }
shouldItemBeExpanded={ shouldItemBeExpanded={
shouldItemBeExpanded shouldItemBeExpanded
} }

View File

@ -8,6 +8,7 @@ import {
useEffect, useEffect,
useState, useState,
Fragment, Fragment,
useRef,
} from '@wordpress/element'; } from '@wordpress/element';
import { useInstanceId } from '@wordpress/compose'; import { useInstanceId } from '@wordpress/compose';
import { BaseControl, Button, TextControl } from '@wordpress/components'; import { BaseControl, Button, TextControl } from '@wordpress/components';
@ -18,13 +19,23 @@ import { speak } from '@wordpress/a11y';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { useLinkedTree } from '../experimental-tree-control/hooks/use-linked-tree'; import {
import { Item, TreeControlProps } from '../experimental-tree-control/types'; toggleNode,
createLinkedTree,
getVisibleNodeIndex as getVisibleNodeIndex,
getNodeDataByIndex,
} from '../experimental-tree-control/linked-tree-utils';
import {
Item,
LinkedTree,
TreeControlProps,
} from '../experimental-tree-control/types';
import { SelectedItems } from '../experimental-select-control/selected-items'; import { SelectedItems } from '../experimental-select-control/selected-items';
import { ComboBox } from '../experimental-select-control/combo-box'; import { ComboBox } from '../experimental-select-control/combo-box';
import { SuffixIcon } from '../experimental-select-control/suffix-icon'; import { SuffixIcon } from '../experimental-select-control/suffix-icon';
import { SelectTreeMenu } from './select-tree-menu'; import { SelectTreeMenu } from './select-tree-menu';
import { escapeHTML } from '../utils'; import { escapeHTML } from '../utils';
import { SelectedItemFocusHandle } from '../experimental-select-control/types';
interface SelectTreeProps extends TreeControlProps { interface SelectTreeProps extends TreeControlProps {
id: string; id: string;
@ -48,12 +59,22 @@ export const SelectTree = function SelectTree( {
initialInputValue, initialInputValue,
onInputChange, onInputChange,
shouldShowCreateButton, shouldShowCreateButton,
help = __( 'Separate with commas or the Enter key.', 'woocommerce' ), help,
isClearingAllowed = false, isClearingAllowed = false,
onClear = () => {}, onClear = () => {},
...props ...props
}: SelectTreeProps ) { }: SelectTreeProps ) {
const linkedTree = useLinkedTree( items ); const [ linkedTree, setLinkedTree ] = useState< LinkedTree[] >( [] );
const [ highlightedIndex, setHighlightedIndex ] = useState( -1 );
// whenever the items change, the linked tree needs to be recalculated
useEffect( () => {
setLinkedTree( createLinkedTree( items, props.createValue ) );
}, [ items.length ] );
// reset highlighted index when the input value changes
useEffect( () => setHighlightedIndex( -1 ), [ props.createValue ] );
const selectTreeInstanceId = useInstanceId( const selectTreeInstanceId = useInstanceId(
SelectTree, SelectTree,
'woocommerce-experimental-select-tree-control__dropdown' 'woocommerce-experimental-select-tree-control__dropdown'
@ -63,6 +84,8 @@ export const SelectTree = function SelectTree( {
'woocommerce-select-tree-control__menu' 'woocommerce-select-tree-control__menu'
) as string; ) as string;
const selectedItemsFocusHandle = useRef< SelectedItemFocusHandle >( null );
function isEventOutside( event: React.FocusEvent ) { function isEventOutside( event: React.FocusEvent ) {
const isInsideSelect = document const isInsideSelect = document
.getElementById( selectTreeInstanceId ) .getElementById( selectTreeInstanceId )
@ -74,7 +97,10 @@ export const SelectTree = function SelectTree( {
'.woocommerce-experimental-select-tree-control__popover-menu' '.woocommerce-experimental-select-tree-control__popover-menu'
) )
?.contains( event.relatedTarget ); ?.contains( event.relatedTarget );
return ! ( isInsideSelect || isInsidePopover ); const isInRemoveTag = event.relatedTarget?.classList.contains(
'woocommerce-tag__remove'
);
return ! isInsideSelect && ! isInRemoveTag && ! isInsidePopover;
} }
const recalculateInputValue = () => { const recalculateInputValue = () => {
@ -104,6 +130,19 @@ export const SelectTree = function SelectTree( {
} }
}, [ isFocused ] ); }, [ isFocused ] );
// Scroll the newly highlighted item into view
useEffect(
() =>
document
.querySelector(
'.experimental-woocommerce-tree-item--highlighted'
)
?.scrollIntoView?.( {
block: 'nearest',
} ),
[ highlightedIndex ]
);
let placeholder: string | undefined = ''; let placeholder: string | undefined = '';
if ( Array.isArray( props.selected ) ) { if ( Array.isArray( props.selected ) ) {
placeholder = props.selected.length === 0 ? props.placeholder : ''; placeholder = props.selected.length === 0 ? props.placeholder : '';
@ -111,12 +150,30 @@ export const SelectTree = function SelectTree( {
placeholder = props.placeholder; placeholder = props.placeholder;
} }
// reset highlighted index when the input value changes
useEffect( () => {
if (
highlightedIndex === items.length &&
! shouldShowCreateButton?.( props.createValue )
) {
setHighlightedIndex( items.length - 1 );
}
}, [ props.createValue ] );
const inputProps: React.InputHTMLAttributes< HTMLInputElement > = { const inputProps: React.InputHTMLAttributes< HTMLInputElement > = {
className: 'woocommerce-experimental-select-control__input', className: 'woocommerce-experimental-select-control__input',
id: `${ props.id }-input`, id: `${ props.id }-input`,
'aria-autocomplete': 'list', 'aria-autocomplete': 'list',
'aria-controls': `${ props.id }-menu`, 'aria-activedescendant':
highlightedIndex >= 0
? `woocommerce-experimental-tree-control__menu-item-${ highlightedIndex }`
: undefined,
'aria-controls': menuInstanceId,
'aria-owns': menuInstanceId,
role: 'combobox',
autoComplete: 'off', autoComplete: 'off',
'aria-expanded': isOpen,
'aria-haspopup': 'tree',
disabled, disabled,
onFocus: ( event ) => { onFocus: ( event ) => {
if ( props.multiple ) { if ( props.multiple ) {
@ -141,45 +198,133 @@ export const SelectTree = function SelectTree( {
} }
}, },
onBlur: ( event ) => { onBlur: ( event ) => {
if ( isOpen && isEventOutside( event ) ) { event.preventDefault();
if ( isEventOutside( event ) ) {
setIsOpen( false ); setIsOpen( false );
setIsFocused( false );
recalculateInputValue(); recalculateInputValue();
} }
setIsFocused( false );
}, },
onKeyDown: ( event ) => { onKeyDown: ( event ) => {
setIsOpen( true ); setIsOpen( true );
if ( event.key === 'ArrowDown' ) { if ( event.key === 'ArrowDown' ) {
event.preventDefault(); event.preventDefault();
// focus on the first element from the Popover if (
( // is advancing from the last menu item to the create button
document.querySelector( highlightedIndex === items.length - 1 &&
`#${ menuInstanceId } input, #${ menuInstanceId } button` shouldShowCreateButton?.( props.createValue )
) as HTMLInputElement | HTMLButtonElement ) {
)?.focus(); setHighlightedIndex( items.length );
} else {
const visibleNodeIndex = getVisibleNodeIndex(
linkedTree,
Math.min( highlightedIndex + 1, items.length ),
'down'
);
if ( visibleNodeIndex !== undefined ) {
setHighlightedIndex( visibleNodeIndex );
} }
if ( event.key === 'Tab' || event.key === 'Escape' ) { }
} else if ( event.key === 'ArrowUp' ) {
event.preventDefault();
if ( highlightedIndex > 0 ) {
const visibleNodeIndex = getVisibleNodeIndex(
linkedTree,
Math.max( highlightedIndex - 1, -1 ),
'up'
);
if ( visibleNodeIndex !== undefined ) {
setHighlightedIndex( visibleNodeIndex );
}
} else {
setHighlightedIndex( -1 );
}
} else if ( event.key === 'Tab' || event.key === 'Escape' ) {
setIsOpen( false ); setIsOpen( false );
recalculateInputValue(); recalculateInputValue();
} } else if ( event.key === 'Enter' || event.key === ',' ) {
if ( event.key === ',' || event.key === 'Enter' ) {
event.preventDefault(); event.preventDefault();
if (
highlightedIndex === items.length &&
shouldShowCreateButton
) {
props.onCreateNew?.();
} else if (
// is selecting an item
highlightedIndex !== -1
) {
const nodeData = getNodeDataByIndex(
linkedTree,
highlightedIndex
);
if ( ! nodeData ) {
return;
}
if ( props.multiple && Array.isArray( props.selected ) ) {
if (
! Boolean(
props.selected.find(
( i ) => i.label === nodeData.label
)
)
) {
if ( props.onSelect ) {
props.onSelect( nodeData );
}
} else if ( props.onRemove ) {
props.onRemove( nodeData );
}
setInputValue( '' );
} else {
onInputChange?.( nodeData.label );
props.onSelect?.( nodeData );
setIsOpen( false );
setIsFocused( false );
focusOnInput();
}
} else if ( inputValue ) {
// no highlighted item, but there is an input value, check if it matches any item
const item = items.find( const item = items.find(
( i ) => i.label === escapeHTML( inputValue ) ( i ) => i.label === escapeHTML( inputValue )
); );
const isAlreadySelected = const isAlreadySelected = Array.isArray( props.selected )
Array.isArray( props.selected ) && ? Boolean(
Boolean(
props.selected.find( props.selected.find(
( i ) => i.label === escapeHTML( inputValue ) ( i ) =>
i.label === escapeHTML( inputValue )
) )
); )
if ( props.onSelect && item && ! isAlreadySelected ) { : props.selected?.label === escapeHTML( inputValue );
props.onSelect( item ); if ( item && ! isAlreadySelected ) {
props.onSelect?.( item );
setInputValue( '' ); setInputValue( '' );
recalculateInputValue(); recalculateInputValue();
} }
} }
} else if (
event.key === 'Backspace' &&
// test if the cursor is at the beginning of the input with nothing selected
( event.target as HTMLInputElement ).selectionStart === 0 &&
( event.target as HTMLInputElement ).selectionEnd === 0 &&
selectedItemsFocusHandle.current
) {
selectedItemsFocusHandle.current();
} else if ( event.key === 'ArrowRight' ) {
setLinkedTree(
toggleNode( linkedTree, highlightedIndex, true )
);
} else if ( event.key === 'ArrowLeft' ) {
setLinkedTree(
toggleNode( linkedTree, highlightedIndex, false )
);
} else if ( event.key === 'Home' ) {
event.preventDefault();
setHighlightedIndex( 0 );
} else if ( event.key === 'End' ) {
event.preventDefault();
setHighlightedIndex( items.length - 1 );
}
}, },
onChange: ( event ) => { onChange: ( event ) => {
if ( onInputChange ) { if ( onInputChange ) {
@ -219,7 +364,14 @@ export const SelectTree = function SelectTree( {
<BaseControl <BaseControl
label={ props.label } label={ props.label }
id={ `${ props.id }-input` } id={ `${ props.id }-input` }
help={ help } help={
props.multiple && ! help
? __(
'Separate with commas or the Enter key.',
'woocommerce'
)
: help
}
> >
<> <>
{ props.multiple ? ( { props.multiple ? (
@ -227,16 +379,18 @@ export const SelectTree = function SelectTree( {
comboBoxProps={ { comboBoxProps={ {
className: className:
'woocommerce-experimental-select-control__combo-box-wrapper', 'woocommerce-experimental-select-control__combo-box-wrapper',
role: 'combobox',
'aria-expanded': isOpen,
'aria-haspopup': 'tree',
'aria-owns': `${ props.id }-menu`,
} } } }
inputProps={ inputProps } inputProps={ inputProps }
suffix={ suffix={
<div className="woocommerce-experimental-select-control__suffix-items"> <div className="woocommerce-experimental-select-control__suffix-items">
{ isClearingAllowed && isOpen && ( { isClearingAllowed && isOpen && (
<Button onClick={ handleClear }> <Button
label={ __(
'Remove all',
'woocommerce'
) }
onClick={ handleClear }
>
<SuffixIcon <SuffixIcon
className="woocommerce-experimental-select-control__icon-clear" className="woocommerce-experimental-select-control__icon-clear"
icon={ closeSmall } icon={ closeSmall }
@ -253,7 +407,12 @@ export const SelectTree = function SelectTree( {
> >
<SelectedItems <SelectedItems
isReadOnly={ isReadOnly } isReadOnly={ isReadOnly }
items={ ( props.selected as Item[] ) || [] } ref={ selectedItemsFocusHandle }
items={
! Array.isArray( props.selected )
? [ props.selected ]
: props.selected
}
getItemLabel={ ( item ) => getItemLabel={ ( item ) =>
item?.label || '' item?.label || ''
} }
@ -262,12 +421,20 @@ export const SelectTree = function SelectTree( {
} }
onRemove={ ( item ) => { onRemove={ ( item ) => {
if ( if (
item &&
! Array.isArray( item ) && ! Array.isArray( item ) &&
props.onRemove props.onRemove
) { ) {
props.onRemove( item ); props.onRemove( item );
} }
} } } }
onBlur={ ( event ) => {
if ( isEventOutside( event ) ) {
setIsOpen( false );
setIsFocused( false );
}
} }
onSelectedItemsEnd={ focusOnInput }
getSelectedItemProps={ () => ( {} ) } getSelectedItemProps={ () => ( {} ) }
/> />
</ComboBox> </ComboBox>
@ -311,8 +478,18 @@ export const SelectTree = function SelectTree( {
isEventOutside={ isEventOutside } isEventOutside={ isEventOutside }
isLoading={ isLoading } isLoading={ isLoading }
isOpen={ isOpen } isOpen={ isOpen }
highlightedIndex={ highlightedIndex }
onExpand={ ( index, value ) => {
setLinkedTree(
toggleNode( linkedTree, index, value )
);
} }
items={ linkedTree } items={ linkedTree }
shouldShowCreateButton={ shouldShowCreateButton } shouldShowCreateButton={ shouldShowCreateButton }
onEscape={ () => {
focusOnInput();
setIsOpen( false );
} }
onClose={ () => { onClose={ () => {
setIsOpen( false ); setIsOpen( false );
} } } }

View File

@ -1,4 +1,6 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import React, { createElement } from '@wordpress/element'; import React, { createElement } from '@wordpress/element';
import { SelectTree } from '../select-tree'; import { SelectTree } from '../select-tree';
import { Item } from '../../experimental-tree-control'; import { Item } from '../../experimental-tree-control';
@ -26,6 +28,44 @@ const DEFAULT_PROPS = {
placeholder: 'Type here', placeholder: 'Type here',
}; };
const TestComponent = ( { multiple }: { multiple?: boolean } ) => {
const [ typedValue, setTypedValue ] = useState( '' );
const [ selected, setSelected ] = useState< any >( [] );
return createElement( SelectTree, {
...DEFAULT_PROPS,
multiple,
shouldShowCreateButton: () => true,
onInputChange: ( value ) => {
setTypedValue( value || '' );
},
createValue: typedValue,
selected: Array.isArray( selected )
? selected.map( ( i ) => ( {
value: String( i.id ),
label: i.name,
} ) )
: {
value: String( selected.id ),
label: selected.name,
},
onSelect: ( item: Item | Item[] ) =>
item && Array.isArray( item )
? setSelected(
item.map( ( i ) => ( {
id: +i.value,
name: i.label,
parent: i.parent ? +i.parent : 0,
} ) )
)
: setSelected( {
id: +item.value,
name: item.label,
parent: item.parent ? +item.parent : 0,
} ),
} );
};
describe( 'SelectTree', () => { describe( 'SelectTree', () => {
beforeEach( () => { beforeEach( () => {
jest.clearAllMocks(); jest.clearAllMocks();
@ -36,7 +76,7 @@ describe( 'SelectTree', () => {
<SelectTree { ...DEFAULT_PROPS } /> <SelectTree { ...DEFAULT_PROPS } />
); );
expect( queryByText( 'Item 1' ) ).not.toBeInTheDocument(); expect( queryByText( 'Item 1' ) ).not.toBeInTheDocument();
queryByRole( 'textbox' )?.focus(); queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Item 1' ) ).toBeInTheDocument(); expect( queryByText( 'Item 1' ) ).toBeInTheDocument();
} ); } );
@ -47,20 +87,21 @@ describe( 'SelectTree', () => {
shouldShowCreateButton={ () => true } shouldShowCreateButton={ () => true }
/> />
); );
queryByRole( 'textbox' )?.focus(); queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Create new' ) ).toBeInTheDocument(); expect( queryByText( 'Create new' ) ).toBeInTheDocument();
} ); } );
it( 'should not show create button when callback is false or no callback', () => { it( 'should not show create button when callback is false or no callback', () => {
const { queryByText, queryByRole } = render( const { queryByText, queryByRole } = render(
<SelectTree { ...DEFAULT_PROPS } /> <SelectTree { ...DEFAULT_PROPS } />
); );
queryByRole( 'textbox' )?.focus(); queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Create new' ) ).not.toBeInTheDocument(); expect( queryByText( 'Create new' ) ).not.toBeInTheDocument();
} ); } );
it( 'should show a root item when focused and child when expand button is clicked', () => { it( 'should show a root item when focused and child when expand button is clicked', () => {
const { queryByText, queryByLabelText, queryByRole } = const { queryByText, queryByLabelText, queryByRole } = render(
render( <SelectTree { ...DEFAULT_PROPS } /> ); <SelectTree { ...DEFAULT_PROPS } />
queryByRole( 'textbox' )?.focus(); );
queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Item 1' ) ).toBeInTheDocument(); expect( queryByText( 'Item 1' ) ).toBeInTheDocument();
expect( queryByText( 'Item 2' ) ).not.toBeInTheDocument(); expect( queryByText( 'Item 2' ) ).not.toBeInTheDocument();
@ -72,7 +113,7 @@ describe( 'SelectTree', () => {
const { queryAllByRole, queryByRole } = render( const { queryAllByRole, queryByRole } = render(
<SelectTree { ...DEFAULT_PROPS } selected={ [ mockItems[ 0 ] ] } /> <SelectTree { ...DEFAULT_PROPS } selected={ [ mockItems[ 0 ] ] } />
); );
queryByRole( 'textbox' )?.focus(); queryByRole( 'combobox' )?.focus();
expect( queryAllByRole( 'treeitem' )[ 0 ] ).toHaveAttribute( expect( queryAllByRole( 'treeitem' )[ 0 ] ).toHaveAttribute(
'aria-selected', 'aria-selected',
'true' 'true'
@ -87,7 +128,7 @@ describe( 'SelectTree', () => {
shouldShowCreateButton={ () => true } shouldShowCreateButton={ () => true }
/> />
); );
queryByRole( 'textbox' )?.focus(); queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Create "new item"' ) ).toBeInTheDocument(); expect( queryByText( 'Create "new item"' ) ).toBeInTheDocument();
} ); } );
it( 'should call onCreateNew when Create "<createValue>" button is clicked', () => { it( 'should call onCreateNew when Create "<createValue>" button is clicked', () => {
@ -100,8 +141,34 @@ describe( 'SelectTree', () => {
onCreateNew={ mockFn } onCreateNew={ mockFn }
/> />
); );
queryByRole( 'textbox' )?.focus(); queryByRole( 'combobox' )?.focus();
queryByText( 'Create "new item"' )?.click(); queryByText( 'Create "new item"' )?.click();
expect( mockFn ).toBeCalledTimes( 1 ); expect( mockFn ).toBeCalledTimes( 1 );
} ); } );
it( 'correctly selects existing item in single mode with arrow keys', async () => {
const { findByRole } = render( <TestComponent /> );
const combobox = ( await findByRole( 'combobox' ) ) as HTMLInputElement;
combobox.focus();
userEvent.keyboard( '{arrowdown}{enter}' );
expect( combobox.value ).toBe( 'Item 1' );
} );
it( 'correctly selects existing item in single mode by typing and pressing Enter', async () => {
const { findByRole } = render( <TestComponent /> );
const combobox = ( await findByRole( 'combobox' ) ) as HTMLInputElement;
combobox.focus();
userEvent.keyboard( 'Item 1{enter}' );
userEvent.tab();
expect( combobox.value ).toBe( 'Item 1' );
} );
it( 'correctly selects existing item in multiple mode by typing and pressing Enter', async () => {
const { findByRole, getAllByText } = render(
<TestComponent multiple />
);
const combobox = ( await findByRole( 'combobox' ) ) as HTMLInputElement;
combobox.focus();
userEvent.keyboard( 'Item 1' );
userEvent.keyboard( '{enter}' );
expect( combobox.value ).toBe( '' ); // input is cleared
expect( getAllByText( 'Item 1' )[ 0 ] ).toBeInTheDocument(); // item is selected (turns into a token)
} );
} ); } );

View File

@ -1,50 +0,0 @@
/**
* External dependencies
*/
import { useMemo } from 'react';
/**
* Internal dependencies
*/
import { Item, LinkedTree } from '../types';
type MemoItems = {
[ value: Item[ 'value' ] ]: LinkedTree;
};
function findChildren(
items: Item[],
parent?: Item[ 'parent' ],
memo: MemoItems = {}
): LinkedTree[] {
const children: Item[] = [];
const others: Item[] = [];
items.forEach( ( item ) => {
if ( item.parent === parent ) {
children.push( item );
} else {
others.push( item );
}
memo[ item.value ] = {
parent: undefined,
data: item,
children: [],
};
} );
return children.map( ( child ) => {
const linkedTree = memo[ child.value ];
linkedTree.parent = child.parent ? memo[ child.parent ] : undefined;
linkedTree.children = findChildren( others, child.value, memo );
return linkedTree;
} );
}
export function useLinkedTree( items: Item[] ): LinkedTree[] {
const linkedTree = useMemo( () => {
return findChildren( items, undefined, {} );
}, [ items ] );
return linkedTree;
}

View File

@ -31,6 +31,10 @@ export function useTreeItem( {
onLastItemLoop, onLastItemLoop,
onFirstItemLoop, onFirstItemLoop,
onTreeBlur, onTreeBlur,
onEscape,
highlightedIndex,
isHighlighted,
onExpand,
...props ...props
}: TreeItemProps ) { }: TreeItemProps ) {
const nextLevel = level + 1; const nextLevel = level + 1;
@ -78,16 +82,19 @@ export function useTreeItem( {
getLabel, getLabel,
treeItemProps: { treeItemProps: {
...props, ...props,
role: 'none', id:
'woocommerce-experimental-tree-control__menu-item-' +
item.index,
role: 'option',
}, },
headingProps: { headingProps: {
role: 'treeitem', role: 'treeitem',
'aria-selected': selection.checkedStatus !== 'unchecked', 'aria-selected': selection.checkedStatus !== 'unchecked',
'aria-expanded': item.children.length 'aria-expanded': item.children.length
? expander.isExpanded ? item.data.isExpanded
: undefined, : undefined,
'aria-owns': 'aria-owns':
item.children.length && expander.isExpanded item.children.length && item.data.isExpanded
? subTreeId ? subTreeId
: undefined, : undefined,
style: { style: {

View File

@ -10,7 +10,7 @@ import { TreeProps } from '../types';
export function useTree( { export function useTree( {
items, items,
level = 1, level = 1,
role = 'tree', role = 'listbox',
multiple, multiple,
selected, selected,
getItemLabel, getItemLabel,
@ -24,6 +24,9 @@ export function useTree( {
onCreateNew, onCreateNew,
shouldShowCreateButton, shouldShowCreateButton,
onFirstItemLoop, onFirstItemLoop,
onEscape,
highlightedIndex,
onExpand,
...props ...props
}: TreeProps ) { }: TreeProps ) {
return { return {

View File

@ -0,0 +1,211 @@
/**
* Internal dependencies
*/
import { AugmentedItem, Item, LinkedTree } from './types';
type MemoItems = {
[ value: AugmentedItem[ 'value' ] ]: LinkedTree;
};
const shouldItemBeExpanded = (
item: LinkedTree,
createValue: string | undefined
): boolean => {
if ( ! createValue || ! item.children?.length ) return false;
return item.children.some( ( child ) => {
if ( new RegExp( createValue || '', 'ig' ).test( child.data.label ) ) {
return true;
}
return shouldItemBeExpanded( child, createValue );
} );
};
function findChildren(
items: AugmentedItem[],
memo: MemoItems = {},
parent?: AugmentedItem[ 'parent' ],
createValue?: string | undefined
): LinkedTree[] {
const children: AugmentedItem[] = [];
const others: AugmentedItem[] = [];
items.forEach( ( item ) => {
if ( item.parent === parent ) {
children.push( item );
} else {
others.push( item );
}
memo[ item.value ] = {
parent: undefined,
data: item,
children: [],
};
} );
return children.map( ( child ) => {
const linkedTree = memo[ child.value ];
linkedTree.parent = child.parent ? memo[ child.parent ] : undefined;
linkedTree.children = findChildren(
others,
memo,
child.value,
createValue
);
linkedTree.data.isExpanded =
linkedTree.children.length === 0
? true
: shouldItemBeExpanded( linkedTree, createValue );
return linkedTree;
} );
}
function populateIndexes(
linkedTree: LinkedTree[],
startCount = 0
): LinkedTree[] {
let count = startCount;
function populate( tree: LinkedTree[] ): number {
for ( const node of tree ) {
node.index = count;
count++;
if ( node.children ) {
count = populate( node.children );
}
}
return count;
}
populate( linkedTree );
return linkedTree;
}
// creates a linked tree from an array of Items
export function createLinkedTree(
items: Item[],
value: string | undefined
): LinkedTree[] {
const augmentedItems = items.map( ( i ) => ( {
...i,
isExpanded: false,
} ) );
return populateIndexes(
findChildren( augmentedItems, {}, undefined, value )
);
}
// Toggles the expanded state of a node in a linked tree
export function toggleNode(
tree: LinkedTree[],
number: number,
value: boolean
): LinkedTree[] {
return tree.map( ( node ) => {
return {
...node,
children: node.children
? toggleNode( node.children, number, value )
: node.children,
data: {
...node.data,
isExpanded:
node.index === number ? value : node.data.isExpanded,
},
...( node.parent
? {
parent: {
...node.parent,
data: {
...node.parent.data,
isExpanded:
node.parent.index === number
? value
: node.parent.data.isExpanded,
},
},
}
: {} ),
};
} );
}
// Gets the index of the next/previous visible node in the linked tree
export function getVisibleNodeIndex(
tree: LinkedTree[],
highlightedIndex: number,
direction: 'up' | 'down'
): number | undefined {
if ( direction === 'down' ) {
for ( const node of tree ) {
if ( ! node.parent || node.parent.data.isExpanded ) {
if (
node.index !== undefined &&
node.index >= highlightedIndex
) {
return node.index;
}
const visibleNodeIndex = getVisibleNodeIndex(
node.children,
highlightedIndex,
direction
);
if ( visibleNodeIndex !== undefined ) {
return visibleNodeIndex;
}
}
}
} else {
for ( let i = tree.length - 1; i >= 0; i-- ) {
const node = tree[ i ];
if ( ! node.parent || node.parent.data.isExpanded ) {
const visibleNodeIndex = getVisibleNodeIndex(
node.children,
highlightedIndex,
direction
);
if ( visibleNodeIndex !== undefined ) {
return visibleNodeIndex;
}
if (
node.index !== undefined &&
node.index <= highlightedIndex
) {
return node.index;
}
}
}
}
return undefined;
}
// Counts the number of nodes in a LinkedTree
export function countNumberOfNodes( linkedTree: LinkedTree[] ) {
let count = 0;
for ( const node of linkedTree ) {
count++;
if ( node.children ) {
count += countNumberOfNodes( node.children );
}
}
return count;
}
// Gets the data of a node by its index
export function getNodeDataByIndex(
linkedTree: LinkedTree[],
index: number
): Item | undefined {
for ( const node of linkedTree ) {
if ( node.index === index ) {
return node.data;
}
if ( node.children ) {
const child = getNodeDataByIndex( node.children, index );
if ( child ) {
return child;
}
}
}
return undefined;
}

View File

@ -6,7 +6,7 @@ import { createElement, forwardRef } from 'react';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { useLinkedTree } from './hooks/use-linked-tree'; import { createLinkedTree } from './linked-tree-utils';
import { Tree } from './tree'; import { Tree } from './tree';
import { TreeControlProps } from './types'; import { TreeControlProps } from './types';
@ -14,7 +14,7 @@ export const TreeControl = forwardRef( function ForwardedTree(
{ items, ...props }: TreeControlProps, { items, ...props }: TreeControlProps,
ref: React.ForwardedRef< HTMLOListElement > ref: React.ForwardedRef< HTMLOListElement >
) { ) {
const linkedTree = useLinkedTree( items ); const linkedTree = createLinkedTree( items, props.createValue );
return <Tree { ...props } ref={ ref } items={ linkedTree } />; return <Tree { ...props } ref={ ref } items={ linkedTree } />;
} ); } );

View File

@ -6,6 +6,8 @@ $control-size: $gap-large;
&--highlighted { &--highlighted {
> .experimental-woocommerce-tree-item__heading { > .experimental-woocommerce-tree-item__heading {
background-color: $gray-100; background-color: $gray-100;
outline: 1.5px solid var( --wp-admin-theme-color );
outline-offset: -1.5px;
} }
} }

View File

@ -24,21 +24,25 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
treeItemProps, treeItemProps,
headingProps, headingProps,
treeProps, treeProps,
expander: { isExpanded, onToggleExpand },
selection, selection,
highlighter: { isHighlighted },
getLabel, getLabel,
} = useTreeItem( { } = useTreeItem( {
...props, ...props,
ref, ref,
} ); } );
function handleEscapePress( function handleKeyDown( event: React.KeyboardEvent< HTMLElement > ) {
event: React.KeyboardEvent< HTMLInputElement >
) {
if ( event.key === 'Escape' && props.onEscape ) { if ( event.key === 'Escape' && props.onEscape ) {
event.preventDefault(); event.preventDefault();
props.onEscape(); props.onEscape();
} else if ( event.key === 'ArrowLeft' ) {
if ( item.index !== undefined ) {
props.onExpand?.( item.index, false );
}
} else if ( event.key === 'ArrowRight' ) {
if ( item.index !== undefined ) {
props.onExpand?.( item.index, true );
}
} }
} }
@ -50,7 +54,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
'experimental-woocommerce-tree-item', 'experimental-woocommerce-tree-item',
{ {
'experimental-woocommerce-tree-item--highlighted': 'experimental-woocommerce-tree-item--highlighted':
isHighlighted, props.isHighlighted,
} }
) } ) }
> >
@ -67,7 +71,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
} }
checked={ selection.checkedStatus === 'checked' } checked={ selection.checkedStatus === 'checked' }
onChange={ selection.onSelectChild } onChange={ selection.onSelectChild }
onKeyDown={ handleEscapePress } onKeyDown={ handleKeyDown }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore __nextHasNoMarginBottom is a valid prop // @ts-ignore __nextHasNoMarginBottom is a valid prop
__nextHasNoMarginBottom={ true } __nextHasNoMarginBottom={ true }
@ -80,7 +84,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
onChange={ ( event ) => onChange={ ( event ) =>
selection.onSelectChild( event.target.checked ) selection.onSelectChild( event.target.checked )
} }
onKeyDown={ handleEscapePress } onKeyDown={ handleKeyDown }
/> />
) } ) }
@ -94,11 +98,21 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
{ Boolean( item.children?.length ) && ( { Boolean( item.children?.length ) && (
<div className="experimental-woocommerce-tree-item__expander"> <div className="experimental-woocommerce-tree-item__expander">
<Button <Button
icon={ isExpanded ? chevronUp : chevronDown } icon={
onClick={ onToggleExpand } item.data.isExpanded ? chevronUp : chevronDown
}
onClick={ () => {
if ( item.index !== undefined ) {
props.onExpand?.(
item.index,
! item.data.isExpanded
);
}
} }
onKeyDown={ handleKeyDown }
className="experimental-woocommerce-tree-item__expander" className="experimental-woocommerce-tree-item__expander"
aria-label={ aria-label={
isExpanded item.data.isExpanded
? __( 'Collapse', 'woocommerce' ) ? __( 'Collapse', 'woocommerce' )
: __( 'Expand', 'woocommerce' ) : __( 'Expand', 'woocommerce' )
} }
@ -107,8 +121,13 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
) } ) }
</div> </div>
{ Boolean( item.children.length ) && isExpanded && ( { Boolean( item.children.length ) && item.data.isExpanded && (
<Tree { ...treeProps } /> <Tree
{ ...treeProps }
highlightedIndex={ props.highlightedIndex }
onExpand={ props.onExpand }
onEscape={ props.onEscape }
/>
) } ) }
</li> </li>
); );

View File

@ -16,7 +16,8 @@
width: 100%; width: 100%;
cursor: default; cursor: default;
&:hover, &:hover,
&:focus-within { &:focus-within,
&--highlighted {
outline: 1.5px solid var(--wp-admin-theme-color); outline: 1.5px solid var(--wp-admin-theme-color);
outline-offset: -1.5px; outline-offset: -1.5px;
background-color: $gray-100; background-color: $gray-100;

View File

@ -14,6 +14,7 @@ import { useMergeRefs } from '@wordpress/compose';
import { useTree } from './hooks/use-tree'; import { useTree } from './hooks/use-tree';
import { TreeItem } from './tree-item'; import { TreeItem } from './tree-item';
import { TreeProps } from './types'; import { TreeProps } from './types';
import { countNumberOfNodes } from './linked-tree-utils';
export const Tree = forwardRef( function ForwardedTree( export const Tree = forwardRef( function ForwardedTree(
props: TreeProps, props: TreeProps,
@ -27,6 +28,8 @@ export const Tree = forwardRef( function ForwardedTree(
ref, ref,
} ); } );
const numberOfItems = countNumberOfNodes( items );
const isCreateButtonVisible = const isCreateButtonVisible =
props.shouldShowCreateButton && props.shouldShowCreateButton &&
props.shouldShowCreateButton( props.createValue ); props.shouldShowCreateButton( props.createValue );
@ -45,7 +48,12 @@ export const Tree = forwardRef( function ForwardedTree(
{ items.map( ( child, index ) => ( { items.map( ( child, index ) => (
<TreeItem <TreeItem
{ ...treeItemProps } { ...treeItemProps }
isExpanded={ props.isExpanded } isHighlighted={
props.highlightedIndex === child.index
}
onExpand={ props.onExpand }
highlightedIndex={ props.highlightedIndex }
isExpanded={ child.data.isExpanded }
key={ child.data.value } key={ child.data.value }
item={ child } item={ child }
index={ index } index={ index }
@ -53,7 +61,7 @@ export const Tree = forwardRef( function ForwardedTree(
onLastItemLoop={ () => { onLastItemLoop={ () => {
( (
rootListRef.current rootListRef.current
?.closest( 'ol[role="tree"]' ) ?.closest( 'ol[role="listbox"]' )
?.parentElement?.querySelector( ?.parentElement?.querySelector(
'.experimental-woocommerce-tree__button' '.experimental-woocommerce-tree__button'
) as HTMLButtonElement ) as HTMLButtonElement
@ -67,7 +75,17 @@ export const Tree = forwardRef( function ForwardedTree(
) : null } ) : null }
{ isCreateButtonVisible && ( { isCreateButtonVisible && (
<Button <Button
className="experimental-woocommerce-tree__button" id={
'woocommerce-experimental-tree-control__menu-item-' +
numberOfItems
}
className={ classNames(
'experimental-woocommerce-tree__button',
{
'experimental-woocommerce-tree__button--highlighted':
props.highlightedIndex === numberOfItems,
}
) }
onClick={ () => { onClick={ () => {
if ( props.onCreateNew ) { if ( props.onCreateNew ) {
props.onCreateNew(); props.onCreateNew();

View File

@ -4,10 +4,15 @@ export interface Item {
label: string; label: string;
} }
export type AugmentedItem = Item & {
isExpanded: boolean;
};
export interface LinkedTree { export interface LinkedTree {
parent?: LinkedTree; parent?: LinkedTree;
data: Item; data: AugmentedItem;
children: LinkedTree[]; children: LinkedTree[];
index?: number;
} }
export type CheckedStatus = 'checked' | 'unchecked' | 'indeterminate'; export type CheckedStatus = 'checked' | 'unchecked' | 'indeterminate';
@ -18,6 +23,11 @@ type BaseTreeProps = {
* a list of items if it is true. * a list of items if it is true.
*/ */
selected?: Item | Item[]; selected?: Item | Item[];
onExpand?( index: number, value: boolean ): void;
highlightedIndex?: number;
/** /**
* Whether the tree items are single or multiple selected. * Whether the tree items are single or multiple selected.
*/ */
@ -137,6 +147,7 @@ export type TreeItemProps = BaseTreeProps &
item: LinkedTree; item: LinkedTree;
index: number; index: number;
isFocused?: boolean; isFocused?: boolean;
isHighlighted?: boolean;
getLabel?( item: LinkedTree ): JSX.Element; getLabel?( item: LinkedTree ): JSX.Element;
shouldItemBeExpanded?( item: LinkedTree ): boolean; shouldItemBeExpanded?( item: LinkedTree ): boolean;
onLastItemLoop?( event: React.KeyboardEvent< HTMLDivElement > ): void; onLastItemLoop?( event: React.KeyboardEvent< HTMLDivElement > ): void;

View File

@ -87,7 +87,7 @@ describe( 'Search', () => {
userEvent.type( getByRole( 'combobox' ), 'A' ); userEvent.type( getByRole( 'combobox' ), 'A' );
// Wait for async options processing. // Wait for async options processing.
await waitFor( () => { await waitFor( () => {
expect( optionsSpy ).toBeCalledWith( 'A' ); expect( optionsSpy ).toHaveBeenCalledWith( 'A' );
} ); } );
await waitFor( () => { await waitFor( () => {
expect( queryAllByRole( 'option' ) ).toHaveLength( 3 ); expect( queryAllByRole( 'option' ) ).toHaveLength( 3 );
@ -119,7 +119,7 @@ describe( 'Search', () => {
userEvent.type( getByRole( 'combobox' ), 'A' ); userEvent.type( getByRole( 'combobox' ), 'A' );
// Wait for async options processing. // Wait for async options processing.
await waitFor( () => { await waitFor( () => {
expect( optionsSpy ).toBeCalledWith( 'A' ); expect( optionsSpy ).toHaveBeenCalledWith( 'A' );
} ); } );
await waitFor( () => { await waitFor( () => {
expect( queryAllByRole( 'option' ) ).toHaveLength( 3 ); expect( queryAllByRole( 'option' ) ).toHaveLength( 3 );

View File

@ -2,16 +2,20 @@
* External dependencies * External dependencies
*/ */
import { __, sprintf } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { createElement, Fragment, useState } from '@wordpress/element'; import {
createElement,
forwardRef,
Fragment,
useState,
} from '@wordpress/element';
import classnames from 'classnames'; import classnames from 'classnames';
import { Button, Popover } from '@wordpress/components'; import { Button, Popover } from '@wordpress/components';
import { Icon, closeSmall } from '@wordpress/icons'; import { Icon, closeSmall } from '@wordpress/icons';
import { decodeEntities } from '@wordpress/html-entities'; import { decodeEntities } from '@wordpress/html-entities';
import { withInstanceId } from '@wordpress/compose'; import { Ref } from 'react';
import { useInstanceId } from '@wordpress/compose';
type Props = { type Props = {
/** A unique ID for this instance of the component. This is automatically generated by withInstanceId. */
instanceId: number | string;
/** The name for this item, displayed as the tag's text. */ /** The name for this item, displayed as the tag's text. */
label: string; label: string;
/** A unique ID for this item. This is used to identify the item when the remove button is clicked. */ /** A unique ID for this item. This is used to identify the item when the remove button is clicked. */
@ -28,17 +32,22 @@ type Props = {
className?: string; className?: string;
}; };
const Tag: React.VFC< Props > = ( { const Tag = forwardRef(
(
{
id, id,
instanceId,
label, label,
popoverContents, popoverContents,
remove, remove,
screenReaderLabel, screenReaderLabel,
className, className,
} ) => { }: Props,
removeButtonRef: Ref< HTMLButtonElement >
) => {
const [ isVisible, setIsVisible ] = useState( false ); const [ isVisible, setIsVisible ] = useState( false );
const instanceId = useInstanceId( Tag ) as string;
screenReaderLabel = screenReaderLabel || label; screenReaderLabel = screenReaderLabel || label;
if ( ! label ) { if ( ! label ) {
// A null label probably means something went wrong // A null label probably means something went wrong
@ -52,7 +61,9 @@ const Tag: React.VFC< Props > = ( {
const labelId = `woocommerce-tag__label-${ instanceId }`; const labelId = `woocommerce-tag__label-${ instanceId }`;
const labelTextNode = ( const labelTextNode = (
<Fragment> <Fragment>
<span className="screen-reader-text">{ screenReaderLabel }</span> <span className="screen-reader-text">
{ screenReaderLabel }
</span>
<span aria-hidden="true">{ label }</span> <span aria-hidden="true">{ label }</span>
</Fragment> </Fragment>
); );
@ -80,9 +91,13 @@ const Tag: React.VFC< Props > = ( {
{ remove && ( { remove && (
<Button <Button
className="woocommerce-tag__remove" className="woocommerce-tag__remove"
ref={ removeButtonRef }
onClick={ remove( id ) } onClick={ remove( id ) }
label={ sprintf(
// translators: %s is the name of the tag being removed. // translators: %s is the name of the tag being removed.
label={ sprintf( __( 'Remove %s', 'woocommerce' ), label ) } __( 'Remove %s', 'woocommerce' ),
label
) }
aria-describedby={ labelId } aria-describedby={ labelId }
> >
<Icon <Icon
@ -94,6 +109,7 @@ const Tag: React.VFC< Props > = ( {
) } ) }
</span> </span>
); );
}; }
);
export default withInstanceId( Tag ); export default Tag;

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Rename Google Listings and Ads with Google for WooCommerce #### Comment <!-- If the changes in this pull request don't warrant a changelog entry, you can alternatively supply a comment here. Note that comments are only accepted with a significance of "Patch" -->

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Fix typos in resolvers error message

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Rename Google Listings and Ads with Google for WooCommerce

View File

@ -52,7 +52,7 @@ export const pluginNames = {
'Mercado Pago payments for WooCommerce', 'Mercado Pago payments for WooCommerce',
'woocommerce' 'woocommerce'
), ),
'google-listings-and-ads': __( 'Google Listings and Ads', 'woocommerce' ), 'google-listings-and-ads': __( 'Google for WooCommerce', 'woocommerce' ),
'woo-razorpay': __( 'Razorpay', 'woocommerce' ), 'woo-razorpay': __( 'Razorpay', 'woocommerce' ),
mailpoet: __( 'MailPoet', 'woocommerce' ), mailpoet: __( 'MailPoet', 'woocommerce' ),
'pinterest-for-woocommerce': __( 'pinterest-for-woocommerce': __(

View File

@ -35,7 +35,7 @@ const getIntHeaderValues = (
const value = response.headers.get( key ); const value = response.headers.get( key );
if ( value === undefined ) { if ( value === undefined ) {
throw new Error( throw new Error(
`Malformed response from server. '${ key }' header is missing when retriving ./report/${ endpoint }.` `Malformed response from server. '${ key }' header is missing when retrieving ./report/${ endpoint }.`
); );
} }
return parseInt( value, 10 ); return parseInt( value, 10 );

View File

@ -26,7 +26,7 @@ export function* getReviews( query: ReviewsQueryParams ) {
if ( totalCountFromHeader === undefined ) { if ( totalCountFromHeader === undefined ) {
throw new Error( throw new Error(
"Malformed response from server. 'x-wp-total' header is missing when retriving ./products/reviews." "Malformed response from server. 'x-wp-total' header is missing when retrieving ./products/reviews."
); );
} }
const totalCount = parseInt( totalCountFromHeader, 10 ); const totalCount = parseInt( totalCountFromHeader, 10 );

View File

@ -2,11 +2,48 @@
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.2.0](https://www.npmjs.com/package/@woocommerce/packages/js/experimental/v/3.2.0) - 2022-07-08 ## [3.3.0](https://www.npmjs.com/package/@woocommerce/experimental/v/3.3.0) - 2024-07-25
- Patch - Added in missing TS definitions in package.json [#34154]
- Patch - Check for note actions before checking length [#35396]
- Patch - Corrected build configuration for packages that weren't outputting minified code. [#43716]
- Patch - Fix invalid return callback ref warning [#37655]
- Patch - Fix Launch Your Store task item should not be clickable once completed [#46361]
- Patch - Fix missing fills prop in useSlotFills return object for wp.components >= 21.2.0 [#36887]
- Patch - Fix remote inbox layout overflows the page width [#47451]
- Minor - Bump node version. [#45148]
- Patch - bump php version in packages/js/*/composer.json [#42020]
- Patch - Support direction prop to control which direction hidden items open. [#36806]
- Patch - update references to woocommerce.com to now reference woo.com [#41241]
- Patch - Update TaskItem to include a badge next to the title. Update also related components TaskList and SetupTaskList, as well as docs, storybook, and tests. [#40034]
- Patch - Update Woo.com references to WooCommerce.com. [#46259]
- Patch - Add missing type definitions and add babel config for tests [#34428]
- Minor - Adjust build/test scripts to remove -- -- that was required for pnpm 6. [#34661]
- Minor - Fix lint issues [#36988]
- Minor - Fix node and pnpm versions via engines [#34773]
- Minor - Improve the "Dismiss" button visibility [#35060]
- Patch - Make eslint emit JSON report for annotating PRs. [#39704]
- Minor - Match TypeScript version with syncpack [#34787]
- Patch - Merging trunk with local [#34322]
- Minor - Sync @wordpress package versions via syncpack. [#37034]
- Patch - Update dependencies [#48645]
- Patch - Update eslint to 8.32.0 across the monorepo. [#36700]
- Patch - Update events that should trigger the test job(s) [#47612]
- Minor - Update pnpm monorepo-wide to 8.6.5 [#38990]
- Minor - Update pnpm to 8.6.7 [#39245]
- Patch - Update pnpm to 9.1.0 [#47385]
- Minor - Update pnpm to version 8. [#37915]
- Minor - Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues [#35007]
- Patch - Update webpack config to use @woocommerce/internal-style-build's parser config [#37195]
- Patch - Upgraded Storybook to 6.5.17-alpha.0 for TypeScript 5 compatibility [#39745]
- Minor - Upgrade TypeScript to 5.1.6 [#39531]
- Patch - Correct spelling errors [#37887]
## [3.2.0](https://www.npmjs.com/package/@woocommerce/experimental/v/3.2.0) - 2022-07-08
- Minor - Remove PHP and Composer dependencies for packaged JS packages - Minor - Remove PHP and Composer dependencies for packaged JS packages
## [3.1.0](https://www.npmjs.com/package/@woocommerce/packages/js/experimental/v/3.1.0) - 2022-06-14 ## [3.1.0](https://www.npmjs.com/package/@woocommerce/experimental/v/3.1.0) - 2022-06-14
- Minor - Add Jetpack Changelogger - Minor - Add Jetpack Changelogger
- Minor - Update TaskItem props type definition. - Minor - Update TaskItem props type definition.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Comment: Just changing package.json command for lint

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Comment: Only a change to development tooling.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Comment: This is a CI-only change.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Comment: This is a developer-only build tooling related change.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Corrected build configuration for packages that weren't outputting minified code.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Comment: adds `glob`, `rimraf`, and `uuid` to Syncpack

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Fix Launch Your Store task item should not be clickable once completed

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Comment: Fix a persistent build bug where TS would try compile files outside of src and typings in packages/js/experimental

View File

@ -1,4 +0,0 @@
Significance: patch
Type: update
bump php version in packages/js/*/composer.json

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Update events that should trigger the test job(s)

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Improve the "Dismiss" button visibility

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Update eslint to 8.32.0 across the monorepo.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Add missing type definitions and add babel config for tests

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Fix node and pnpm versions via engines

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Make eslint emit JSON report for annotating PRs.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Sync @wordpress package versions via syncpack.

View File

@ -1,5 +0,0 @@
Significance: patch
Type: dev
Comment: Package scripts were modified to support simplified running of turbo commands in the monorepo.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Update pnpm to 8.6.7

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Fix lint issues

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Update pnpm monorepo-wide to 8.6.5

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Update pnpm to version 8.

Some files were not shown because too many files have changed in this diff Show More