Merge branch 'trunk' into add/sync_of_deleted_orders

This commit is contained in:
Nestor Soriano 2023-05-05 09:46:54 +02:00
commit 7ce0828c98
No known key found for this signature in database
GPG Key ID: 08110F3518C12CAD
1005 changed files with 33807 additions and 19858 deletions

15
.gitattributes vendored Normal file
View File

@ -0,0 +1,15 @@
* text=auto
# Force LF In Configuration Files
*.md text eol=lf
*.json text eol=lf
*.yml text eol=lf
# Force LF In Code Files
*.php text eol=lf
*.js text eol=lf
*.jsx text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.css text eol=lf
*.scss text eol=lf

View File

@ -7,11 +7,11 @@ There are many ways to contribute to the project!
- [Translating strings into your language](https://github.com/woocommerce/woocommerce/wiki/Translating-WooCommerce).
- Answering questions on the various WooCommerce communities like the [WP.org support forums](https://wordpress.org/support/plugin/woocommerce/).
- Testing open [issues](https://github.com/woocommerce/woocommerce/issues) or [pull requests](https://github.com/woocommerce/woocommerce/pulls) and sharing your findings in a comment.
- Testing WooCommerce beta versions and release candidates. Those are announced in the [WooCommerce development blog](https://woocommerce.wordpress.com/).
- Testing WooCommerce beta versions and release candidates. Those are announced in the [WooCommerce development blog](https://developer.woocommerce.com/blog/).
- Submitting fixes, improvements, and enhancements.
- To disclose a security issue to our team, [please submit a report via HackerOne](https://hackerone.com/automattic/).
If you wish to contribute code, please read the information in the sections below. Then [fork](https://help.github.com/articles/fork-a-repo/) WooCommerce, commit your changes, and [submit a pull request](https://help.github.com/articles/using-pull-requests/) 🎉
If you wish to contribute code, please read the information in the sections below. Then [fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) WooCommerce, commit your changes, and [submit a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) 🎉
We use the `good first issue` label to mark issues that are suitable for new contributors. You can find all the issues with this label [here](https://github.com/woocommerce/woocommerce/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+good+first+issue%22).
@ -21,6 +21,8 @@ If you have questions about the process to contribute code or want to discuss de
## Getting started
Please take a moment to review the [project readme](https://github.com/woocommerce/woocommerce/blob/trunk/README.md) and our [development notes](https://github.com/woocommerce/woocommerce/blob/trunk/DEVELOPMENT.md), which cover the basics needed to start working on this project. You may also be interested in the following resources:
- [How to set up WooCommerce development environment](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment)
- [Git Flow](https://github.com/woocommerce/woocommerce/wiki/WooCommerce-Git-Flow)
- [Minification of SCSS and JS](https://github.com/woocommerce/woocommerce/wiki/Minification-of-SCSS-and-JS)
@ -31,7 +33,7 @@ If you have questions about the process to contribute code or want to discuss de
## Coding Guidelines and Development 🛠
- Ensure you stick to the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/)
- Ensure you stick to the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/).
- Run our build process described in the document on [how to set up WooCommerce development environment](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment), it will install our pre-commit hook, code sniffs, dependencies, and more.
- Whenever possible please fix pre-existing code standards errors in the files that you change. It is ok to skip that for larger files or complex fixes.
- Ensure you use LF line endings in your code editor. Use [EditorConfig](http://editorconfig.org/) if your editor supports it so that indentation, line endings and other settings are auto configured.
@ -39,16 +41,13 @@ If you have questions about the process to contribute code or want to discuss de
- Ensure that your code supports the minimum supported versions of PHP and WordPress; this is shown at the top of the `readme.txt` file.
- Push the changes to your fork and submit a pull request on the trunk branch of the WooCommerce repository.
- Make sure to write good and detailed commit messages (see [this post](https://chris.beams.io/posts/git-commit/) for more on this) and follow all the applicable sections of the pull request template.
- Please avoid modifying the changelog directly or updating the .pot files. These will be updated by the WooCommerce team.
- Please create a change file for your changes by running `pnpm --filter=<project> changelog add`. For example, a change file for the WooCommerce Core project would be added by running `pnpm --filter=woocommerce changelog add`.
- Please avoid modifying the changelog directly or updating the .pot files. These will be updated by the WooCommerce team.
If you are contributing code to the (Javascript-driven) Gutenberg blocks, note that it's developed in an external package.
- [Blocks](https://github.com/woocommerce/woocommerce-gutenberg-products-block)
If you are contributing code to our (Javascript-driven) Gutenberg blocks, please note that they are developed in their [own repository](https://github.com/woocommerce/woocommerce-gutenberg-products-block) and have their [own issue tracker](https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues).
## Feature Requests 🚀
Feature requests can be [submitted to our issue tracker](https://github.com/woocommerce/woocommerce/issues/new?assignees=&labels=type%3A+enhancement%2Cstatus%3A+awaiting+triage&template=2-enhancement.yml&title=%5BEnhancement%5D%3A+). Be sure to include a description of the expected behavior and use case, and before submitting a request, please search for similar ones in the closed issues.
The best place to submit feature requests is over on our [dedicated feature request page](https://woocommerce.com/feature-requests/woocommerce/). You can easily search and vote for existing requests, or create new requests if necessary.
Feature request issues will remain closed until we see sufficient interest via comments and [👍 reactions](https://help.github.com/articles/about-discussions-in-issues-and-pull-requests/) from the community.
You can see a [list of current feature requests which require votes here](https://github.com/woocommerce/woocommerce/issues?q=is%3Aissue+sort%3Areactions-%2B1-desc+label%3A%22needs%3A+votes%22+).
Alternatively, if you wish to propose a straightforward technical enhancement that is unlikely to require much discussion, you can [open a new issue](https://github.com/woocommerce/woocommerce/issues/new?assignees=&labels=type%3A+enhancement%2Cstatus%3A+awaiting+triage&template=2-enhancement.yml&title=%5BEnhancement%5D%3A+) right here on GitHub and, for any that may require more discussion, consider syncing with us during office hours or publishing a thread on [GitHub Discussions](https://github.com/woocommerce/woocommerce/discussions).

View File

@ -1,10 +1,9 @@
### All Submissions:
### Submission Review Guidelines:
- [ ] Have you followed the [WooCommerce Contributing guideline](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md)?
- [ ] Does your code follow the [WordPress' coding standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/)?
- [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/woocommerce/woocommerce/pulls) for the same update/change?
<!-- Mark completed items with an [x] -->
- I have followed the [WooCommerce Contributing Guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md) and the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/).
- I have checked to ensure there aren't other open [Pull Requests](https://github.com/woocommerce/woocommerce/pulls) for the same update/change.
- I have reviewed my code for [security best practices](https://developer.wordpress.org/apis/security/).
- Following the above guidelines will result in quick merges and clear and detailed feedback when appropriate.
<!-- You can erase any parts of this template not applicable to your Pull Request. -->
@ -18,25 +17,12 @@ Closes # .
### How to test the changes in this Pull Request:
<!-- Please include detailed instructions on how these changes can be tested, make sure to review and follow the guide for writing high-quality testing instructions below. -->
<!-- Include detailed instructions on how these changes can be tested. Review and follow the guide for how to write high-quality testing instructions. -->
- [ ] Have you followed the [Writing high-quality testing instructions guide](https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions)?
Using the [WooCommerce Testing Instructions Guide](https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions), include your detailed testing instructions:
1.
2.
3.
<!-- End testing instructions -->
### Other information:
- [ ] Have you added an explanation of what your changes do and why you'd like us to include them?
- [ ] Have you written new tests for your changes, as applicable?
- [ ] Have you created a changelog file for each project being changed, ie `pnpm --filter=<project> changelog add`?
- [ ] Have you included testing instructions?
<!-- Mark completed items with an [x] -->
### FOR PR REVIEWER ONLY:
- [ ] I have reviewed that everything is sanitized/escaped appropriately for any SQL or XSS injection possibilities. I made sure Linting is not ignored or disabled.
<!-- End testing instructions -->

View File

@ -29,7 +29,7 @@ runs:
- name: Setup PNPM
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
with:
version: '7.29.1'
version: '8.3.1'
- name: Setup Node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
@ -46,7 +46,7 @@ runs:
tools: phpcs, sirbrillig/phpcs-changed
- name: Cache Composer Dependencies
uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8
with:
path: ~/.cache/composer/files
key: ${{ runner.os }}-php-${{ inputs.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
@ -58,14 +58,27 @@ runs:
pnpm -w install turbo
pnpm install ${{ steps.parse-input.outputs.INSTALL_FILTERS }}
- name: Get branch name
id: get_branch
shell: bash
run: |
if [ "${{ github.event_name }}" == "pull_request" ]; then
branch_name=$(echo "${{ github.head_ref }}" | tr '/' '-')
echo "CURRENT_BRANCH_NAME=$branch_name" >> $GITHUB_OUTPUT
else
echo "CURRENT_BRANCH_NAME=${{ github.ref_name }}" >> $GITHUB_OUTPUT
fi
- name: Cache Build Output
uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8
with:
path: node_modules/.cache/turbo
key: ${{ runner.os }}-build-output-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}
restore-keys: ${{ runner.os }}-build-output-
path: .turbo
key: ${{ runner.os }}-build-output-${{ steps.get_branch.outputs.CURRENT_BRANCH_NAME }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-output-${{ steps.get_branch.outputs.CURRENT_BRANCH_NAME }}
${{ runner.os }}-build-output
- name: Build
if: ${{ inputs.build == 'true' }}
shell: bash
run: pnpm -w exec turbo run turbo:build ${{ steps.parse-input.outputs.BUILD_FILTERS }}
run: pnpm -w exec turbo run turbo:build --cache-dir=".turbo" ${{ steps.parse-input.outputs.BUILD_FILTERS }}

View File

@ -1,82 +1,80 @@
name: Run CI
on:
push:
branches:
- trunk
- 'release/**'
workflow_dispatch:
on:
push:
branches:
- trunk
- 'release/**'
workflow_dispatch:
defaults:
run:
shell: bash
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
run:
shell: bash
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
test:
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }}
timeout-minutes: 30
runs-on: ubuntu-20.04
permissions:
contents: read
continue-on-error: ${{ matrix.wp == 'nightly' }}
strategy:
fail-fast: false
matrix:
php: [ '7.4', '8.0' ]
wp: [ 'latest' ]
include:
- wp: nightly
php: '7.4'
- wp: '6.0'
php: 7.4
- wp: '5.9'
php: 7.4
services:
database:
image: mysql:5.6
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
steps:
- uses: actions/checkout@v3
test:
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }}
timeout-minutes: 30
runs-on: ubuntu-20.04
permissions:
contents: read
continue-on-error: ${{ matrix.wp == 'nightly' }}
strategy:
fail-fast: false
matrix:
php: ['7.4', '8.0']
wp: ['latest']
include:
- wp: nightly
php: '7.4'
- wp: '6.1'
php: 7.4
services:
database:
image: mysql:5.6
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
steps:
- uses: actions/checkout@v3
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
build-filters: woocommerce
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
build-filters: woocommerce
- name: Tool versions
run: |
php --version
composer --version
- name: Tool versions
run: |
php --version
composer --version
- name: Build Admin feature config
working-directory: plugins/woocommerce
run: pnpm run build:feature-config
- name: Build Admin feature config
working-directory: plugins/woocommerce
run: pnpm run build:feature-config
- name: Add PHP8 Compatibility.
run: |
if [ "$(php -r "echo version_compare(PHP_VERSION,'8.0','>=');")" ]; then
cd plugins/woocommerce
curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip
unzip -d /tmp/phpunit-7.5-fork /tmp/phpunit-7.5-fork.zip
composer bin phpunit config --unset platform
composer bin phpunit config repositories.0 '{"type": "path", "url": "/tmp/phpunit-7.5-fork/phpunit-add-compatibility-with-php8-to-phpunit-7", "options": {"symlink": false}}'
composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-reqs
rm -rf ./vendor/phpunit/
composer dump-autoload
fi
- name: Add PHP8 Compatibility.
run: |
if [ "$(php -r "echo version_compare(PHP_VERSION,'8.0','>=');")" ]; then
cd plugins/woocommerce
curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip
unzip -d /tmp/phpunit-7.5-fork /tmp/phpunit-7.5-fork.zip
composer bin phpunit config --unset platform
composer bin phpunit config repositories.0 '{"type": "path", "url": "/tmp/phpunit-7.5-fork/phpunit-add-compatibility-with-php8-to-phpunit-7", "options": {"symlink": false}}'
composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-reqs
rm -rf ./vendor/phpunit/
composer dump-autoload
fi
- name: Init DB and WP
working-directory: plugins/woocommerce
run: ./tests/bin/install.sh woo_test root root 127.0.0.1 ${{ matrix.wp }}
- name: Init DB and WP
working-directory: plugins/woocommerce
run: ./tests/bin/install.sh woo_test root root 127.0.0.1 ${{ matrix.wp }}
- name: Run tests
working-directory: plugins/woocommerce
run: pnpm run test --color
- name: Run tests
working-directory: plugins/woocommerce
run: pnpm run test --color

View File

@ -85,6 +85,7 @@ jobs:
api-tests-run:
name: Runs API tests.
if: github.event.pull_request.user.login != 'github-actions[bot]'
runs-on: ubuntu-20.04
permissions:
contents: read
@ -137,6 +138,7 @@ jobs:
k6-tests-run:
name: Runs k6 Performance tests
if: github.event.pull_request.user.login != 'github-actions[bot]'
runs-on: ubuntu-20.04
permissions:
contents: read
@ -167,6 +169,7 @@ jobs:
if: |
always() &&
! github.event.pull_request.head.repo.fork &&
github.event.pull_request.user.login != 'github-actions[bot]' &&
(
contains( needs.*.result, 'success' ) ||
contains( needs.*.result, 'failure' )
@ -239,6 +242,7 @@ jobs:
if: |
always() &&
! github.event.pull_request.head.repo.fork &&
github.event.pull_request.user.login != 'github-actions[bot]' &&
(
contains( needs.*.result, 'success' ) ||
contains( needs.*.result, 'failure' )

View File

@ -22,7 +22,7 @@ jobs:
run: |
npm install -g pnpm
npm -g i @wordpress/env@5.1.0
pnpm install --filter code-analyzer --filter cli-core
pnpm install --filter code-analyzer --filter monorepo-utils
- name: Run analyzer
id: run
working-directory: tools/code-analyzer

View File

@ -13,56 +13,54 @@ concurrency:
permissions: {}
jobs:
test:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} ${{ matrix.hpos && 'HPOS' || '' }}
timeout-minutes: 30
runs-on: ubuntu-20.04
permissions:
contents: read
continue-on-error: ${{ matrix.wp == 'nightly' }}
env:
HPOS: ${{ matrix.hpos }}
strategy:
fail-fast: false
matrix:
php: [ '7.4', '8.0' ]
wp: [ "latest" ]
include:
- wp: nightly
php: '7.4'
- wp: '6.0'
php: 7.4
- wp: '5.9'
php: 7.4
- wp: 'latest'
php: '7.4'
hpos: true
services:
database:
image: mysql:5.6
test:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} ${{ matrix.hpos && 'HPOS' || '' }}
timeout-minutes: 30
runs-on: ubuntu-20.04
permissions:
contents: read
continue-on-error: ${{ matrix.wp == 'nightly' }}
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
steps:
- uses: actions/checkout@v3
HPOS: ${{ matrix.hpos }}
strategy:
fail-fast: false
matrix:
php: ['7.4', '8.0']
wp: ['latest']
include:
- wp: nightly
php: '7.4'
- wp: '6.1'
php: 7.4
- wp: 'latest'
php: '7.4'
hpos: true
services:
database:
image: mysql:5.6
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
steps:
- uses: actions/checkout@v3
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
php-version: ${{ matrix.php }}
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
php-version: ${{ matrix.php }}
- name: Tool versions
run: |
php --version
composer --version
- name: Tool versions
run: |
php --version
composer --version
- name: Init DB and WP
working-directory: plugins/woocommerce
run: ./tests/bin/install.sh woo_test root root 127.0.0.1 ${{ matrix.wp }}
- name: Init DB and WP
working-directory: plugins/woocommerce
run: ./tests/bin/install.sh woo_test root root 127.0.0.1 ${{ matrix.wp }}
- name: Run tests
working-directory: plugins/woocommerce
run: pnpm run test --filter=woocommerce --color
- name: Run tests
working-directory: plugins/woocommerce
run: pnpm run test --filter=woocommerce --color

View File

@ -14,7 +14,7 @@ on:
description: 'Slack Channel Override: The channel ID to send the Slack ping about the freeze'
env:
TIME_OVERRIDE: ${{ inputs.timeOverride }}
TIME_OVERRIDE: ${{ inputs.timeOverride || 'now' }}
GIT_COMMITTER_NAME: 'WooCommerce Bot'
GIT_COMMITTER_EMAIL: 'no-reply@woocommerce.com'
GIT_AUTHOR_NAME: 'WooCommerce Bot'
@ -23,127 +23,59 @@ env:
permissions: {}
jobs:
verify-code-freeze:
name: 'Verify that today is the day of the code freeze'
runs-on: ubuntu-20.04
outputs:
freeze: ${{ steps.check-freeze.outputs.freeze }}
steps:
- name: 'Install PHP'
uses: shivammathur/setup-php@8e2ac35f639d3e794c1da1f28999385ab6fdf0fc
with:
php-version: '7.4'
- name: 'Check whether today is the code freeze day'
id: check-freeze
shell: php {0}
run: |
<?php
$now = time();
if ( getenv( 'TIME_OVERRIDE' ) ) {
$now = strtotime( getenv( 'TIME_OVERRIDE' ) );
}
// Code freeze comes 22 days prior to release day.
$release_time = strtotime( '+22 days', $now );
$release_day_of_week = date( 'l', $release_time );
$release_day_of_month = (int) date( 'j', $release_time );
// If 22 days from now isn't the second Tuesday, then it's not code freeze day.
if ( 'Tuesday' !== $release_day_of_week || $release_day_of_month < 8 || $release_day_of_month > 14 ) {
file_put_contents( getenv( 'GITHUB_OUTPUT' ), "freeze=1\n", FILE_APPEND );
} else {
file_put_contents( getenv( 'GITHUB_OUTPUT' ), "freeze=0\n", FILE_APPEND );
}
maybe-create-next-milestone-and-release-branch:
name: 'Maybe create next milestone and release branch'
code-freeze-prep:
name: 'Verify that today is the day of the code freeze and prepare repository'
runs-on: ubuntu-20.04
permissions:
contents: write
issues: write
needs: verify-code-freeze
if: needs.verify-code-freeze.outputs.freeze == 0
pull-requests: write
outputs:
branch: ${{ steps.freeze.outputs.branch }}
release_version: ${{ steps.freeze.outputs.release_version }}
next_version: ${{ steps.freeze.outputs.next_version }}
freeze: ${{ steps.check-freeze.outputs.freeze }}
nextReleaseBranch: ${{ steps.branch.outputs.nextReleaseBranch }}
nextReleaseVersion: ${{ steps.milestone.outputs.nextReleaseVersion }}
nextDevelopmentVersion: ${{ steps.milestone.outputs.nextDevelopmentVersion }}
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 100
fetch-depth: 0
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
build: false
- name: Install prerequisites
run: |
npm install -g pnpm
pnpm install --filter monorepo-utils
- name: 'Run the script to enforce the code freeze'
id: freeze
run: php .github/workflows/scripts/release-code-freeze.php
- name: 'Check whether today is the code freeze day'
id: check-freeze
run: pnpm utils code-freeze verify-day -o $TIME_OVERRIDE
- name: Create next milestone
id: milestone
if: steps.check-freeze.outputs.freeze == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_OUTPUTS: 1
run: pnpm run utils code-freeze milestone -o ${{ github.repository_owner }}
prep-trunk:
name: Preps trunk for next development cycle
runs-on: ubuntu-20.04
permissions:
contents: write
pull-requests: write
needs: maybe-create-next-milestone-and-release-branch
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 100
- name: Create next release branch
id: branch
if: steps.check-freeze.outputs.freeze == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm run utils code-freeze branch -o ${{ github.repository_owner }}
- name: fetch-trunk
run: git fetch origin trunk
- name: checkout-trunk
run: git checkout trunk
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
- name: Create branch
run: git checkout -b prep/trunk-for-next-dev-cycle-${{ needs.maybe-create-next-milestone-and-release-branch.outputs.next_version }}
- name: Bump versions
working-directory: ./tools/version-bump
run: pnpm run version bump woocommerce -v ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.next_version }}.0-dev
- name: Checkout pnpm-lock.yaml to prevent issues
run: git checkout pnpm-lock.yaml
- name: Commit changes
run: git commit -am "Prep trunk for ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.next_version }} cycle"
- name: Push branch up
run: git push --no-verify origin prep/trunk-for-next-dev-cycle-${{ needs.maybe-create-next-milestone-and-release-branch.outputs.next_version }}
- name: Create the PR
uses: actions/github-script@v6
with:
script: |
const body = "This PR updates the versions in trunk to ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.next_version }} for next development cycle."
const pr = await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: "Prep trunk for ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.next_version }} cycle",
head: "prep/trunk-for-next-dev-cycle-${{ needs.maybe-create-next-milestone-and-release-branch.outputs.next_version }}",
base: "trunk",
body: body
})
- name: Prepare trunk for next development cycle
id: prep-trunk
if: steps.check-freeze.outputs.freeze == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm run utils code-freeze version-bump -o ${{ github.repository_owner }} -v ${{ steps.milestone.outputs.nextDevelopmentVersion }}.0-dev
notify-slack:
name: 'Sends code freeze notification to Slack'
if: ${{ inputs.skipSlackPing != true }}
runs-on: ubuntu-20.04
needs: maybe-create-next-milestone-and-release-branch
needs: code-freeze-prep
if: ${{ inputs.skipSlackPing != true }}
steps:
- name: Slack
uses: archive/github-actions-slack@v2.0.0
@ -152,16 +84,17 @@ jobs:
slack-bot-user-oauth-access-token: ${{ secrets.CODE_FREEZE_BOT_TOKEN }}
slack-channel: ${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }}
slack-text: |
:warning-8c: ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.release_version }} Code Freeze :ice_cube:
:warning-8c: ${{ needs.code-freeze-prep.outputs.nextReleaseVersion }} Code Freeze :ice_cube:
The automation to cut the release branch for ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.release_version }} has run. Any PRs that were not already merged will be a part of ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.next_version }} by default. If you have something that needs to make ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.release_version }} that hasn't yet been merged, please see the <${{ secrets.FG_LINK }}/code-freeze-for-woocommerce-core-release/|fieldguide page for the code freeze>.
The automation to cut the release branch for ${{ needs.code-freeze-prep.outputs.nextReleaseVersion }} has run. Any PRs that were not already merged will be a part of ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.nextDevelopmentVersion }} by default. If you have something that needs to make ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.nextReleaseVersion }} that hasn't yet been merged, please see the <${{ secrets.FG_LINK }}/code-freeze-for-woocommerce-core-release/|fieldguide page for the code freeze>.
trigger-changelog-action:
name: 'Trigger changelog action'
runs-on: ubuntu-20.04
permissions:
actions: write
needs: maybe-create-next-milestone-and-release-branch
needs: code-freeze-prep
if: needs.code-freeze-prep.outputs.freeze == 'true'
steps:
- name: 'Trigger changelog action'
uses: actions/github-script@v6
@ -173,7 +106,7 @@ jobs:
workflow_id: 'release-changelog.yml',
ref: 'trunk',
inputs: {
releaseVersion: "${{ needs.maybe-create-next-milestone-and-release-branch.outputs.release_version }}",
releaseBranch: "${{ needs.maybe-create-next-milestone-and-release-branch.outputs.branch }}"
releaseVersion: "${{ needs.code-freeze-prep.outputs.nextReleaseVersion }}",
releaseBranch: "${{ needs.code-freeze-prep.outputs.nextReleaseBranch }}"
}
})

View File

@ -1,6 +1,6 @@
name: Remind reviewers to also review the testing instructions.
on:
pull_request:
pull_request_target:
types: [review_requested]
permissions: {}
@ -11,7 +11,25 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
- name: Install Octokit
run: npm --prefix .github/workflows/scripts install @octokit/action
- name: Install Actions Core
run: npm --prefix .github/workflows/scripts install @actions/core
- name: Check if user is a community contributor
id: is-community-contributor
run: node .github/workflows/scripts/is-community-contributor.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get the username of requested reviewers
if: steps.is-community-contributor.outputs.is-community == 'no'
id: get_reviewer_username
run: |
# Retrieves the username of all reviewers and stores them in a comma-separated list

View File

@ -1,87 +0,0 @@
<?php
// phpcs:ignoreFile
/**
* Script to automatically enforce the release code freeze.
*
* @package WooCommerce/GithubActions
*/
require_once __DIR__ . '/post-request-shared.php';
$now = time();
if ( getenv( 'TIME_OVERRIDE' ) ) {
$now = strtotime( getenv( 'TIME_OVERRIDE' ) );
}
/**
* Set an output for the GitHub action.
*
* @param string $name The name of the output.
* @param string $value The value of the output.
*/
function set_output( $name, $value ) {
file_put_contents( getenv( 'GITHUB_OUTPUT' ), "{$name}={$value}" . PHP_EOL, FILE_APPEND );
}
// Code freeze comes 22 days prior to release day.
$release_time = strtotime( '+22 days', $now );
$release_day_of_week = date( 'l', $release_time );
$release_day_of_month = (int) date( 'j', $release_time );
// If 22 days from now isn't the second Tuesday, then it's not code freeze day.
if ( 'Tuesday' !== $release_day_of_week || $release_day_of_month < 8 || $release_day_of_month > 14 ) {
echo 'Info: Today is not the Monday of the code freeze.' . PHP_EOL;
exit( 1 );
}
$latest_version_with_release = get_latest_version_with_release();
if ( empty( $latest_version_with_release ) ) {
echo '*** Error: Unable to get latest version with release' . PHP_EOL;
exit( 1 );
}
// Because we go from 5.9 to 6.0, we can get the next major_minor by adding 0.1 and formatting appropriately.
$latest_float = (float) $latest_version_with_release;
$branch_major_minor = number_format( $latest_float + 0.1, 1 );
$milestone_major_minor = number_format( $latest_float + 0.2, 1 );
// We use those values to get the release branch and next milestones that we need to create.
$release_branch_to_create = "release/{$branch_major_minor}";
$milestone_to_create = "{$milestone_major_minor}.0";
if ( getenv( 'GITHUB_OUTPUTS' ) ) {
echo 'Including GitHub Outputs...' . PHP_EOL;
set_output( 'next_version', $milestone_major_minor );
set_output( 'release_version', $branch_major_minor );
set_output( 'branch', $release_branch_to_create );
set_output( 'milestone', $milestone_to_create );
}
if ( getenv( 'DRY_RUN' ) ) {
echo 'DRY RUN: Skipping actual creation of release branch and milestone...' . PHP_EOL;
echo "Release Branch: {$release_branch_to_create}" . PHP_EOL;
echo "Milestone: {$milestone_to_create}" . PHP_EOL;
return;
}
if ( create_github_milestone( $milestone_to_create ) ) {
echo "Created milestone {$milestone_to_create}" . PHP_EOL;
} elseif ( '422' === $github_api_response_code ) {
// The milestone already existed when GitHub returns a 422 status.
echo "Notice: Unable to create {$milestone_to_create} milestone. Maybe it already exists? Skipping..." . PHP_EOL;
} else {
echo "*** Error: Unable to create {$milestone_to_create} milestone" . PHP_EOL;
}
if ( create_github_branch_from_branch( 'trunk', $release_branch_to_create ) ) {
echo "Created branch {$release_branch_to_create}" . PHP_EOL;
} elseif ( '422' === $github_api_response_code ) {
// The release branch already existed when GitHub returns a 422 status.
echo "Notice: Unable to create {$release_branch_to_create} branch. Maybe it already exists? Skipping..." . PHP_EOL;
exit( 1 );
} else {
echo "*** Error: Unable to create {$release_branch_to_create}" . PHP_EOL;
exit( 1 );
}

View File

@ -1,61 +0,0 @@
# Duplicate workflow that returns success for this check when the author is "github-actions". See https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
name: Status Check Bypass for Automation
on:
pull_request:
jobs:
bypass-lint:
if: ${{ github.event.pull_request.user.login == 'github-actions[bot]' }}
runs-on: ubuntu-latest
name: "Lint and Test JS"
steps:
- run: 'echo "No build required"'
bypass-7-4-latest:
if: ${{ github.event.pull_request.user.login == 'github-actions[bot]' }}
runs-on: ubuntu-latest
name: "PHP 7.4 WP latest"
steps:
- run: 'echo "No build required"'
bypass-8-0-latest:
if: ${{ github.event.pull_request.user.login == 'github-actions[bot]' }}
runs-on: ubuntu-latest
name: "PHP 8.0 WP latest"
steps:
- run: 'echo "No build required"'
bypass-api-tests:
if: ${{ github.event.pull_request.user.login == 'github-actions[bot]' }}
runs-on: ubuntu-latest
name: "Runs API tests."
steps:
- run: 'echo "No build required"'
bypass-k6:
if: ${{ github.event.pull_request.user.login == 'github-actions[bot]' }}
runs-on: ubuntu-latest
name: "Runs k6 Performance tests"
steps:
- run: 'echo "No build required"'
bypass-sniff:
if: ${{ github.event.pull_request.user.login == 'github-actions[bot]' }}
runs-on: ubuntu-latest
name: "Code sniff (PHP 7.4, WP Latest)"
steps:
- run: 'echo "No build required"'
bypass-changelogger-use:
if: ${{ github.event.pull_request.user.login == 'github-actions[bot]' }}
runs-on: ubuntu-latest
name: "Changelogger use"
steps:
- run: 'echo "No build required"'
bypass-e2e:
if: ${{ github.event.pull_request.user.login == 'github-actions[bot]' }}
runs-on: ubuntu-latest
name: "Runs E2E tests."
steps:
- run: 'echo "No build required"'
bypass-pr-highlight:
if: ${{ github.event.pull_request.user.login == 'github-actions[bot]' }}
runs-on: ubuntu-latest
name: "Check pull request changes to highlight"
steps:
- run: 'echo "No build required"'

View File

@ -10,10 +10,10 @@ To get up and running within the WooCommerce Monorepo, you will need to make sur
### Prerequisites
* [NVM](https://github.com/nvm-sh/nvm#installing-and-updating): While you can always install Node through other means, we recommend using NVM to ensure you're aligned with the version used by our development teams. Our repository contains [an `.nvmrc` file](.nvmrc) which helps ensure you are using the correct version of Node.
* [PNPM](https://pnpm.io/installation): Our repository utilizes PNPM to manage project dependencies and run various scripts involved in building and testing projects.
* [PHP 7.2+](https://www.php.net/manual/en/install.php): WooCommerce Core currently features a minimum PHP version of 7.2. It is also needed to run Composer and various project build scripts.
* [Composer](https://getcomposer.org/doc/00-intro.md): We use Composer to manage all of the dependencies for PHP packages and plugins.
- [NVM](https://github.com/nvm-sh/nvm#installing-and-updating): While you can always install Node through other means, we recommend using NVM to ensure you're aligned with the version used by our development teams. Our repository contains [an `.nvmrc` file](.nvmrc) which helps ensure you are using the correct version of Node.
- [PNPM](https://pnpm.io/installation): Our repository utilizes PNPM to manage project dependencies and run various scripts involved in building and testing projects.
- [PHP 7.2+](https://www.php.net/manual/en/install.php): WooCommerce Core currently features a minimum PHP version of 7.2. It is also needed to run Composer and various project build scripts.
- [Composer](https://getcomposer.org/doc/00-intro.md): We use Composer to manage all of the dependencies for PHP packages and plugins.
Once you've installed all of the prerequisites, you can run the following commands to get everything working.
@ -32,27 +32,31 @@ Check out [our development guide](DEVELOPMENT.md) if you would like a more compr
## Repository Structure
* [**Plugins**](plugins): Our repository contains plugins that relate to or otherwise aid in the development of WooCommerce.
* [**WooCommerce Core**](plugins/woocommerce): The core WooCommerce plugin is available in the plugins directory.
* [**Packages**](packages): Contained within the packages directory are all of the [PHP](packages/php) and [JavaScript](packages/js) provided for the community. Some of these are internal dependencies and are marked with an `internal-` prefix.
* [**Tools**](tools): We also have a growing number of tools within our repository. Many of these are intended to be utilities and scripts for use in the monorepo, but, this directory may also contain external tools.
- [**Plugins**](plugins): Our repository contains plugins that relate to or otherwise aid in the development of WooCommerce.
- [**WooCommerce Core**](plugins/woocommerce): The core WooCommerce plugin is available in the plugins directory.
- [**Packages**](packages): Contained within the packages directory are all of the [PHP](packages/php) and [JavaScript](packages/js) provided for the community. Some of these are internal dependencies and are marked with an `internal-` prefix.
- [**Tools**](tools): We also have a growing number of tools within our repository. Many of these are intended to be utilities and scripts for use in the monorepo, but, this directory may also contain external tools.
## Reporting Security Issues
To disclose a security issue to our team, [please submit a report via HackerOne here](https://hackerone.com/automattic/).
## Support
This repository is not suitable for support. Please don't use our issue tracker for support requests, but for core WooCommerce issues only. Support can take place through the appropriate channels:
* If you have a problem, you may want to start with the [self help guide](https://docs.woocommerce.com/document/woocommerce-self-service-guide/).
* The [WooCommerce.com premium support portal](https://woocommerce.com/contact-us/) for customers who have purchased themes or extensions.
* [Our community forum on wp.org](https://wordpress.org/support/plugin/woocommerce) which is available for all WooCommerce users.
* [The Official WooCommerce Facebook Group](https://www.facebook.com/groups/advanced.woocommerce).
* For customizations, you may want to check our list of [WooExperts](https://woocommerce.com/experts/) or [Codeable](https://codeable.io/).
- If you have a problem, you may want to start with the [self help guide](https://docs.woocommerce.com/document/woocommerce-self-service-guide/).
- The [WooCommerce.com premium support portal](https://woocommerce.com/contact-us/) for customers who have purchased themes or extensions.
- [Our community forum on wp.org](https://wordpress.org/support/plugin/woocommerce) which is available for all WooCommerce users.
- [The Official WooCommerce Facebook Group](https://www.facebook.com/groups/advanced.woocommerce).
- For customizations, you may want to check our list of [WooExperts](https://woocommerce.com/experts/) or [Codeable](https://codeable.io/).
NOTE: Unfortunately, we are unable to honor support requests in issues on this repository; as a result, any requests submitted in this manner will be closed.
## Community
For peer to peer support, real-time announcements, and office hours, please [join our slack community](https://woocommerce.com/community-slack/)!
## Contributing to WooCommerce
If you have a patch or have stumbled upon an issue with WooCommerce core, you can contribute this back to the code. Please read our [contributor guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md) for more information on how you can do this.

View File

@ -0,0 +1,3 @@
// Import the default config file and expose it in the project root.
// Useful for editor integrations.
module.exports = require( '@wordpress/prettier-config' );

View File

@ -1,5 +1,139 @@
== Changelog ==
= 7.6.1 2023-04-26 =
**WooCommerce**
* Fix - Fix regression in supporting nested date query arguments in HPOS. [#37827](https://github.com/woocommerce/woocommerce/pull/37827)
* Fix - Sync up date_column_name default for orders table, between stats and table data. [#37927](https://github.com/woocommerce/woocommerce/pull/37927)
* Fix - Revert "Change Variations form shown in Variations tab when there are no variations created (#36957)" [#37889](https://github.com/woocommerce/woocommerce/pull/37889)
* Fix Revert changes to use window.fetch in legacy cart JS [#37463](https://github.com/woocommerce/woocommerce/pull/37463)
* Update - Update WooCommerce Blocks to 9.8.5 [#37921](https://github.com/woocommerce/woocommerce/pull/37921)
= 7.6.0 2023-04-13 =
**WooCommerce**
* Fix - Fix incorrect usage of dispatch, useSelect, and setState calls in homescreen along with settings and onboarding package [#37641](https://github.com/woocommerce/woocommerce/pull/37641)
* Fix - Do not attempt to cache order during order creation (HPOS). [#37569](https://github.com/woocommerce/woocommerce/pull/37569)
* Fix - Add default value when calling get_option for woocommerce_task_list_tracked_completed_tasks. [#37397](https://github.com/woocommerce/woocommerce/pull/37397)
* Fix - When order meta data is saved via HPOS, it should be backfilled to the CPT data store. [#36593](https://github.com/woocommerce/woocommerce/pull/36593)
* Fix - Overwrite clone method to prevent duplicate data when saving a clone. [#37313](https://github.com/woocommerce/woocommerce/pull/37313)
* Fix - Add default button padding to TT2 stylesheet to fix some visual issues in WP 5.9 and 6.0 [#37018](https://github.com/woocommerce/woocommerce/pull/37018)
* Fix - Added skydropx slug back to shipping partners list so that it can be installed through the shipping task [#37286](https://github.com/woocommerce/woocommerce/pull/37286)
* Fix - Add HPOS compat for admin report functions. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
* Fix - Add HPOS compat for wc-user-functions.php. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
* Fix - Add support for null inputs to pnpm wc_add_number_precision [#36891](https://github.com/woocommerce/woocommerce/pull/36891)
* Fix - Add support for `after`, `before`, `modified_after` and `modified_before` params in local timezone. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
* Fix - Add validation when saving attributes and variations [#37046](https://github.com/woocommerce/woocommerce/pull/37046)
* Fix - Also delete when order type is placehoder, since it was created by HPOS. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
* Fix - Corrects a class reference in the ProductDownloadsServiceProvider. [#37052](https://github.com/woocommerce/woocommerce/pull/37052)
* Fix - Corrects a variable name in Reports\Stock\Stats. It was missed during the last name change. [#37057](https://github.com/woocommerce/woocommerce/pull/37057)
* Fix - Corrects class namespaces in Onboarding. It was missed during last restructuring. [#37056](https://github.com/woocommerce/woocommerce/pull/37056)
* Fix - Corrects imported classes. Class names should not begin with a backslash. [#37058](https://github.com/woocommerce/woocommerce/pull/37058)
* Fix - Ensure product importer imports all lines in a CSV file. [#36839](https://github.com/woocommerce/woocommerce/pull/36839)
* Fix - Fetch order first to refresh cache before returning prop. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
* Fix - Fix 0 rendered on short-circuit evaluation. [#37104](https://github.com/woocommerce/woocommerce/pull/37104)
* Fix - Fix ArrayUtil::get_value_or_default method not behaving as documented for null array values [#37053](https://github.com/woocommerce/woocommerce/pull/37053)
* Fix - Fix blank screen is displayed during OBW when using WP5.9 [#36903](https://github.com/woocommerce/woocommerce/pull/36903)
* Fix - Fix duplicated global attribute [#37109](https://github.com/woocommerce/woocommerce/pull/37109)
* Fix - fixed bug where jetpack connection owner field was assumed to be username when its actually display name [#37170](https://github.com/woocommerce/woocommerce/pull/37170)
* Fix - Fixed payments recommendations pane in WooCommerce Payment Settings using the wrong image prop [#37259](https://github.com/woocommerce/woocommerce/pull/37259)
* Fix - Fixes filtering by attributes in the Analytics Orders and Variations reports. [#37223](https://github.com/woocommerce/woocommerce/pull/37223)
* Fix - Fix incorrect VAT exempt behaviour on shop page when prices are exclusive of tax. [#33991](https://github.com/woocommerce/woocommerce/pull/33991)
* Fix - Fix React rendering falsy value in marketing page. [#37227](https://github.com/woocommerce/woocommerce/pull/37227)
* Fix - Fix the inability to apply a coupon whose code is "0" [#36924](https://github.com/woocommerce/woocommerce/pull/36924)
* Fix - fix typo in variable name [#36759](https://github.com/woocommerce/woocommerce/pull/36759)
* Fix - Fix unit test snapshots due to a dependency version change [#36435](https://github.com/woocommerce/woocommerce/pull/36435)
* Fix - Fix variations exported as draft being imported as draft (and thus remaining invisible) [#36933](https://github.com/woocommerce/woocommerce/pull/36933)
* Fix - Fix WP data resolution (`invalidateResolution`) not working with WP 5.9 in marketing page. [#37198](https://github.com/woocommerce/woocommerce/pull/37198)
* Fix - Handle date arguments in OrderTableQuery correctly by adjusting their timezones before running. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
* Fix - Load same stylesheets in the Site Editor as in the frontend [#36911](https://github.com/woocommerce/woocommerce/pull/36911)
* Fix - Loco Translate and wp-cli compatibility for woocommerce-admin translation files [#36739](https://github.com/woocommerce/woocommerce/pull/36739)
* Fix - Override react version to 17.0.2 [#37087](https://github.com/woocommerce/woocommerce/pull/37087)
* Fix - Prevent possible warning arising from use of woocommerce_wp_* family of functions. [#37026](https://github.com/woocommerce/woocommerce/pull/37026)
* Fix - Record values for toggled checkboxes/features in settings [#37242](https://github.com/woocommerce/woocommerce/pull/37242)
* Fix - Restore the sort order when orders are cached. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
* Fix - Treat order as seperate resource when validating for webhook since it's not necessarily a CPT anymore. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
* Fix - Update Customers report with latest user data after editing user. [#37237](https://github.com/woocommerce/woocommerce/pull/37237)
* Add - Add "Create a new campaign" modal in Campaigns card in Multichannel Marketing page. [#37044](https://github.com/woocommerce/woocommerce/pull/37044)
* Add - Add a cache for orders, to use when custom order tables are enabled [#35014](https://github.com/woocommerce/woocommerce/pull/35014)
* Add - Add an encoding selector to the product importer [#36819](https://github.com/woocommerce/woocommerce/pull/36819)
* Add - Add Campaigns card into Multichannel Marketing page. [#36735](https://github.com/woocommerce/woocommerce/pull/36735)
* Add - Added images support for the payment recommendations transaction processors [#37230](https://github.com/woocommerce/woocommerce/pull/37230)
* Add - Added `woocommerce_widget_layered_nav_filters_start/end` hooks around layered nav filters widget [#36705](https://github.com/woocommerce/woocommerce/pull/36705)
* Add - Add introduction banner to multichannel marketing page. [#37110](https://github.com/woocommerce/woocommerce/pull/37110)
* Add - Add marketplace suggestions and multichannel marketing information to WC Tracker. [#37017](https://github.com/woocommerce/woocommerce/pull/37017)
* Add - Add new feature flag for the product edit blocks experience [#37137](https://github.com/woocommerce/woocommerce/pull/37137)
* Add - Add productBlockEditorSettings script to be used for the Product Block Editor. [#37123](https://github.com/woocommerce/woocommerce/pull/37123)
* Add - Add support for new countries in WCPay [#36906](https://github.com/woocommerce/woocommerce/pull/36906)
* Add - Add wp-json/wc-admin/shipping-partner-suggestions API endpoint [#37155](https://github.com/woocommerce/woocommerce/pull/37155)
* Add - Allow sorting by menu_order in products widget. [#37002](https://github.com/woocommerce/woocommerce/pull/37002)
* Add - Create editor skeleton on add/edit product pages [#37023](https://github.com/woocommerce/woocommerce/pull/37023)
* Add - Creating product entity in auto-draft status, and adding support for retrieving preexisting products. [#37064](https://github.com/woocommerce/woocommerce/pull/37064)
* Add - Fixed image array in edit context for product/variations endpoint. [#28498](https://github.com/woocommerce/woocommerce/pull/28498)
* Add - Initial e2e tests for new product editor. [#36902](https://github.com/woocommerce/woocommerce/pull/36902)
* Add - Log to order notes when coupons are removed or applied. [#30642](https://github.com/woocommerce/woocommerce/pull/30642)
* Add - Make WC_Order::get_tax_location accessible publicly through a wrapper function. [#36953](https://github.com/woocommerce/woocommerce/pull/36953)
* Add - Update product post rest config when block editor feature is enabled. [#37206](https://github.com/woocommerce/woocommerce/pull/37206)
* Update - Update WooCommerce Blocks to 9.8.4 [#37492](https://github.com/woocommerce/woocommerce/pull/37492)
* Update - Update WooCommerce Blocks to 9.8.3 [#37477](https://github.com/woocommerce/woocommerce/pull/37477)
* Update - Update WooCommerce Blocks to 9.8.2 [#37373](https://github.com/woocommerce/woocommerce/pull/37373)
* Update - Add tabs and sections placeholders in product blocks template [#37174](https://github.com/woocommerce/woocommerce/pull/37174)
* Update - Change the default date used on Revenue and Orders report to 'date_paid' and create spotlight on both reports [#36653](https://github.com/woocommerce/woocommerce/pull/36653)
* Update - Change Variations form shown in Variations tab when there are no variations created [#36957](https://github.com/woocommerce/woocommerce/pull/36957)
* Update - Moving currencyContext to relevant package, and updating all references. [#36959](https://github.com/woocommerce/woocommerce/pull/36959)
* Update - Moving some components out of core and into product-editor package. [#36945](https://github.com/woocommerce/woocommerce/pull/36945)
* Update - Moving use-product-helper and related product hooks to product editor package. [#37006](https://github.com/woocommerce/woocommerce/pull/37006)
* Update - Refresh data source poller transients on wc_admin_daily [#37027](https://github.com/woocommerce/woocommerce/pull/37027)
* Update - Remove accordion from "Other payment providers" in payment task [#37205](https://github.com/woocommerce/woocommerce/pull/37205)
* Update - Remove Cart2Cart option from add product task [#37285](https://github.com/woocommerce/woocommerce/pull/37285)
* Update - Show link to store settings when stock management is disabled. [#37140](https://github.com/woocommerce/woocommerce/pull/37140)
* Update - Update create-wc-extension script within woocommerce-admin. [#36917](https://github.com/woocommerce/woocommerce/pull/36917)
* Update - Update imports of product slot fills to new @woocommerce/product-editor library [#36830](https://github.com/woocommerce/woocommerce/pull/36830)
* Update - Update obw payment gateways [#37233](https://github.com/woocommerce/woocommerce/pull/37233)
* Update - Update playwright api-core-tests to associate orders with real products to prevent extension issues for those that validate product ids [#37243](https://github.com/woocommerce/woocommerce/pull/37243)
* Update - Update playwright api-core-tests to handle cases where extensions add to shipping methods [#37239](https://github.com/woocommerce/woocommerce/pull/37239)
* Update - Update product template by adding the list price and sale price blocks. [#37211](https://github.com/woocommerce/woocommerce/pull/37211)
* Update - Updates automated release testing workflow to use Playwright [#36598](https://github.com/woocommerce/woocommerce/pull/36598)
* Update - Update template of product type to include product name block. [#37132](https://github.com/woocommerce/woocommerce/pull/37132)
* Update - Update the date modified field for an order when a refund for it is successfully processed. [#37047](https://github.com/woocommerce/woocommerce/pull/37047)
* Update - Update WooCommerce BLocks to 9.8.0 [#37210](https://github.com/woocommerce/woocommerce/pull/37210)
* Update - Update WooCommerce Blocks to 9.8.1 [#37238](https://github.com/woocommerce/woocommerce/pull/37238)
* Update - Updating rest namespace for product posttype to version 3. [#37028](https://github.com/woocommerce/woocommerce/pull/37028)
* Update - Use the currently activated theme color for completed tasks strikethough [#37001](https://github.com/woocommerce/woocommerce/pull/37001)
* Dev - Add @woocommerce/admin-layout package. [#37094](https://github.com/woocommerce/woocommerce/pull/37094)
* Dev - Add CES data store to @woocommerce/customer-effort-score [#37252](https://github.com/woocommerce/woocommerce/pull/37252)
* Dev - Add existing global attribute layout #36944 [#36944](https://github.com/woocommerce/woocommerce/pull/36944)
* Dev - Add missing woocommerce_run_on_woocommerce_admin_updated hook for the scheduled action registered in RemoteInboxNotificationsEngine [#36768](https://github.com/woocommerce/woocommerce/pull/36768)
* Dev - add wpLogin import to wc-baseline-load.js [#36940](https://github.com/woocommerce/woocommerce/pull/36940)
* Dev - Convert "Allow backorders?" into radio buttons [#37282](https://github.com/woocommerce/woocommerce/pull/37282)
* Dev - Fix lint issues [#36988](https://github.com/woocommerce/woocommerce/pull/36988)
* Dev - Fix the value of `UPDATE_WC` environment variable in the daily k6 performance tests. [#37049](https://github.com/woocommerce/woocommerce/pull/37049)
* Dev - Move CES components and utilities to @woocommerce/customer-effort-score [#37112](https://github.com/woocommerce/woocommerce/pull/37112)
* Dev - Move hook to confirm unsaved form changes to navigation package [#36752](https://github.com/woocommerce/woocommerce/pull/36752)
* Dev - Move product utils into product editor package [#36730](https://github.com/woocommerce/woocommerce/pull/36730)
* Dev - Set up React Fast Refresh in woocommerce-admin [#37165](https://github.com/woocommerce/woocommerce/pull/37165)
* Dev - Show "Stock status" as a collection of radio buttons [#37278](https://github.com/woocommerce/woocommerce/pull/37278)
* Dev - Show a message for variable products [#37185](https://github.com/woocommerce/woocommerce/pull/37185)
* Dev - Support E2E testing of draft releases. [#36997](https://github.com/woocommerce/woocommerce/pull/36997)
* Dev - Sync @wordpress package versions via syncpack. [#37034](https://github.com/woocommerce/woocommerce/pull/37034)
* Tweak - Add productId dependency when getting the product by id in ProductPage [#37152](https://github.com/woocommerce/woocommerce/pull/37152)
* Tweak - Add tracking for local pickup method in Checkout [#36847](https://github.com/woocommerce/woocommerce/pull/36847)
* Tweak - Add Tracks events for product inventory tab interactions. [#37202](https://github.com/woocommerce/woocommerce/pull/37202)
* Tweak - Change Avalara CTA copy in tax task to Download [#37224](https://github.com/woocommerce/woocommerce/pull/37224)
* Tweak - Make sure 'safe_text' settings are rendered as 'text' inputs for compatibility. [#37154](https://github.com/woocommerce/woocommerce/pull/37154)
* Tweak - Prevent 'woocommerce_ajax_order_items_removed' from generating PHP warnings. [#37178](https://github.com/woocommerce/woocommerce/pull/37178)
* Tweak - Rename "Manage stock?" label to "Stock management". [#37135](https://github.com/woocommerce/woocommerce/pull/37135)
* Tweak - Trigger event `woocommerce_attributes_saved` following successful product meta box ajax update. [#36943](https://github.com/woocommerce/woocommerce/pull/36943)
* Tweak - Visual tweaks for shipping partner banners [#37229](https://github.com/woocommerce/woocommerce/pull/37229)
* Performance - Bypass Action Scheduler for customer updates. [#37265](https://github.com/woocommerce/woocommerce/pull/37265)
* Performance - Switch wc_product_attributes_lookup table management to use truncate and dbDelta over drop table [#36872](https://github.com/woocommerce/woocommerce/pull/36872)
* Enhancement - Add 'display_context' argument to wc_get_price_to_display(). [#25080](https://github.com/woocommerce/woocommerce/pull/25080)
* Enhancement - Added woocommerce_reduce_order_item_stock action hook [#34721](https://github.com/woocommerce/woocommerce/pull/34721)
* Enhancement - Add the support for the C&C Blocks in declaring compatibility feature [#36426](https://github.com/woocommerce/woocommerce/pull/36426)
= 7.5.1 2023-03-21 =
**WooCommerce**

View File

@ -4,8 +4,8 @@
"description": "Monorepo for the WooCommerce ecosystem",
"homepage": "https://woocommerce.com/",
"engines": {
"node": "^16.13.1",
"pnpm": "^7.13.3"
"node": "^16.14.1",
"pnpm": "^8.3.1"
},
"private": true,
"repository": {
@ -17,6 +17,9 @@
"bugs": {
"url": "https://github.com/woocommerce/woocommerce/issues"
},
"bin": {
"utils": "./tools/monorepo-utils/bin/run"
},
"scripts": {
"build": "pnpm exec turbo run turbo:build",
"test": "pnpm exec turbo run turbo:test",
@ -26,7 +29,8 @@
"git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && husky install",
"create-extension": "node ./tools/create-extension/index.js",
"cherry-pick": "node ./tools/cherry-pick/bin/run",
"sync-dependencies": "pnpm exec syncpack -- fix-mismatches"
"sync-dependencies": "pnpm exec syncpack -- fix-mismatches",
"utils": "./tools/monorepo-utils/bin/run"
},
"devDependencies": {
"@babel/preset-env": "^7.20.2",
@ -55,7 +59,7 @@
"sass": "^1.59.3",
"sass-loader": "^10.4.1",
"syncpack": "^9.8.4",
"turbo": "^1.8.5",
"turbo": "^1.9.3",
"typescript": "^4.9.5",
"url-loader": "^1.1.2",
"webpack": "^5.76.2"

View File

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

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Remove obw theme step tests

View File

@ -5,8 +5,8 @@
"description": "E2E tests for the new WooCommerce interface.",
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/admin-e2e-tests/README.md",
"engines": {
"node": "^16.13.1",
"pnpm": "^7.13.3"
"node": "^16.14.1",
"pnpm": "^8.3.1"
},
"repository": {
"type": "git",

View File

@ -14,7 +14,6 @@ import {
StoreDetails,
StoreDetailsSection,
} from '../sections/onboarding/StoreDetailsSection';
import { ThemeSection } from '../sections/onboarding/ThemeSection';
import { BasePage } from './BasePage';
export class OnboardingWizard extends BasePage {
@ -24,7 +23,6 @@ export class OnboardingWizard extends BasePage {
industry: IndustrySection;
productTypes: ProductTypeSection;
business: BusinessSection;
themes: ThemeSection;
constructor( page: Page ) {
super( page );
@ -32,7 +30,6 @@ export class OnboardingWizard extends BasePage {
this.industry = new IndustrySection( page );
this.productTypes = new ProductTypeSection( page );
this.business = new BusinessSection( page );
this.themes = new ThemeSection( page );
}
async skipStoreSetup(): Promise< void > {
@ -90,7 +87,6 @@ export class OnboardingWizard extends BasePage {
productNumber: string;
currentlySelling: string;
};
themeTitle?: string;
} = {}
): Promise< void > {
await this.navigate();
@ -142,13 +138,5 @@ export class OnboardingWizard extends BasePage {
await this.business.uncheckAllRecommendedBusinessFeatures();
await this.continue();
await this.themes.isDisplayed();
// This navigates to the home screen
if ( options.themeTitle ) {
await this.themes.continueWithTheme( options.themeTitle );
} else {
await this.themes.continueWithActiveTheme();
}
}
}

View File

@ -1,29 +0,0 @@
/**
* Internal dependencies
*/
import { BasePage } from '../../pages/BasePage';
import { waitForElementByText } from '../../utils/actions';
export class ThemeSection extends BasePage {
async isDisplayed(): Promise< void > {
await waitForElementByText( 'h2', 'Choose a theme' );
await waitForElementByText( 'button', 'All themes' );
}
async continueWithActiveTheme(): Promise< void > {
await this.clickButtonWithText( 'Continue with my active theme' );
}
async continueWithTheme( themeTitle: string ): Promise< void > {
const title = await waitForElementByText( 'h2', themeTitle );
const chooseButton = await title?.evaluateHandle( ( element ) => {
const card = element.closest( '.components-card' );
return Array.from( card?.querySelectorAll( 'button' ) || [] ).find(
( el ) => el.textContent === 'Choose'
);
} );
if ( chooseButton ) {
await chooseButton.asElement()?.click();
}
}
}

View File

@ -107,11 +107,6 @@ export const testAdminOnboardingWizard = () => {
await profileWizard.continue();
} );
it( 'can complete the theme selection section', async () => {
await profileWizard.themes.isDisplayed();
await profileWizard.themes.continueWithActiveTheme();
} );
it( 'can select the right currency on settings page related to the onboarding country', async () => {
const settingsScreen = new WcSettings( page );
await settingsScreen.navigate();
@ -185,7 +180,7 @@ export const testSelectiveBundleWCPay = () => {
await profileWizard.continue();
} );
it( 'can choose not to install any extensions', async () => {
it( 'can choose not to install any extensions, and finish the rest of the wizard successfully', async () => {
await profileWizard.business.freeFeaturesIsDisplayed();
// Add WC Pay check
await profileWizard.business.expandRecommendedBusinessFeatures();
@ -198,13 +193,6 @@ export const testSelectiveBundleWCPay = () => {
await profileWizard.continue();
} );
it( 'can finish the rest of the wizard successfully', async () => {
await profileWizard.themes.isDisplayed();
// This navigates to the home screen
await profileWizard.themes.continueWithActiveTheme();
} );
it( 'should display the choose payments task, and not the woocommerce payments task', async () => {
const homescreen = new WcHomescreen( page );
await homescreen.isDisplayed();
@ -333,11 +321,8 @@ export const testDifferentStoreCurrenciesWCPay = () => {
}
await profileWizard.business.uncheckAllRecommendedBusinessFeatures();
await profileWizard.continue();
await profileWizard.themes.isDisplayed();
// This navigates to the home screen
await profileWizard.themes.continueWithActiveTheme();
await profileWizard.continue();
} );
it( `can select ${ spec.expectedCurrency } as the currency for ${ spec.countryRegion }`, async () => {
@ -583,7 +568,6 @@ export const testBusinessDetailsForm = () => {
await profileWizard.business.expandRecommendedBusinessFeatures();
await profileWizard.business.uncheckAllRecommendedBusinessFeatures();
await profileWizard.continue();
await profileWizard.themes.isDisplayed();
} );
} );
};

View File

@ -59,40 +59,5 @@ export const testAdminPurchaseSetupTask = () => {
).toBeDefined();
} );
} );
describe( 'selecting paid theme', () => {
beforeAll( async () => {
await resetWooCommerceState();
await profileWizard.navigate();
await profileWizard.walkThroughAndCompleteOnboardingWizard( {
themeTitle: 'Blooms',
} );
await homeScreen.isDisplayed();
await homeScreen.possiblyDismissWelcomeModal();
} );
it( 'should display add <theme name> to my store task', async () => {
expect(
await getElementByText( '*', 'Add Blooms to my store' )
).toBeDefined();
} );
it( 'should show paid features modal with option to buy now', async () => {
const task = await getElementByText(
'*',
'Add Blooms to my store'
);
await task?.click();
await waitForElementByText(
'h1',
'Would you like to add the following paid features to your store now?'
);
expect(
await getElementByText( 'button', 'Buy now' )
).toBeDefined();
} );
} );
} );
};

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding LayoutContext component and hook.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Update webpack config to use @woocommerce/internal-style-build's parser config

View File

@ -0,0 +1 @@
export * from './layout-context';

View File

@ -0,0 +1,68 @@
/**
* External dependencies
*/
import {
createElement,
createContext,
useContext,
useMemo,
} from '@wordpress/element';
export type LayoutContextType = {
layoutString: string;
extendLayout: ( item: string ) => LayoutContextType;
layoutParts: string[];
isDescendantOf: ( item: string ) => boolean;
};
type LayoutContextProviderProps = {
children: React.ReactNode;
value: LayoutContextType;
};
export const LayoutContext = createContext< LayoutContextType | undefined >(
undefined
);
export const getLayoutContextValue = (
layoutParts: LayoutContextType[ 'layoutParts' ] = []
): LayoutContextType => ( {
layoutParts: [ ...layoutParts ],
extendLayout: ( item ) => {
const newLayoutPath = [ ...layoutParts, item ];
return {
...getLayoutContextValue( newLayoutPath ),
layoutParts: newLayoutPath,
};
},
layoutString: layoutParts.join( '/' ),
isDescendantOf: ( item ) => layoutParts.includes( item ),
} );
export const LayoutContextProvider: React.FC< LayoutContextProviderProps > = ( {
children,
value,
} ) => (
<LayoutContext.Provider value={ value }>
{ children }
</LayoutContext.Provider>
);
export const useLayoutContext = () => {
const layoutContext = useContext( LayoutContext );
if ( layoutContext === undefined ) {
throw new Error(
'useLayoutContext must be used within a LayoutContextProvider'
);
}
return layoutContext;
};
export const useExtendLayout = ( item: string ) => {
const { extendLayout } = useLayoutContext();
return useMemo( () => extendLayout( item ), [ extendLayout, item ] );
};

View File

@ -0,0 +1 @@
export * from './LayoutContext';

View File

@ -1 +1,2 @@
export * from './plugins';
export * from './components';

View File

@ -12,6 +12,7 @@ module.exports = {
path: __dirname,
},
module: {
parser: webpackConfig.parser,
rules: webpackConfig.rules,
},
plugins: webpackConfig.plugins,

View File

@ -4,8 +4,8 @@
"description": "API tests for WooCommerce",
"main": "index.js",
"engines": {
"node": "^16.13.1",
"pnpm": "^7.13.3"
"node": "^16.14.1",
"pnpm": "^8.3.1"
},
"scripts": {
"e2e": "jest",

View File

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

View File

@ -5,8 +5,8 @@
"description": "A simple interface for interacting with a WooCommerce installation.",
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/api/README.md",
"engines": {
"node": "^16.13.1",
"pnpm": "^7.13.3"
"node": "^16.14.1",
"pnpm": "^8.3.1"
},
"repository": {
"type": "git",

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Fix issue where width of select control dropdown was not correctly calculated when rendering was delayed.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Improve a11y support to collapsible content component

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Create SelectTree component that uses TreeControl

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Migrate ellipsis-menu component to TS

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Migrate Rating component to TS

View File

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

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Update TourKit README to correct primaryButton example and formatting.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Fix issue where single item can not be cleared and text can not be selected upon click.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add minFilterQueryLength, individuallySelectParent, and clearOnSelect props.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Apply wccom experimental select control changes

View File

@ -0,0 +1,4 @@
Significance: major
Type: update
Updated AdvancedFilter to use createInterpolateElement instead of interpolateComponents.

View File

@ -0,0 +1,5 @@
Significance: patch
Type: dev
Comment: Add unit tests

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update select tree control dropdown menu for custom slot fill support for display within Modals

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Update webpack config to use @woocommerce/internal-style-build's parser config

View File

@ -5,8 +5,8 @@
"author": "Automattic",
"license": "GPL-3.0-or-later",
"engines": {
"node": "^16.13.1",
"pnpm": "^7.13.3"
"node": "^16.14.1",
"pnpm": "^8.3.1"
},
"keywords": [
"wordpress",
@ -36,8 +36,8 @@
"@types/wordpress__block-editor": "^7.0.0",
"@types/wordpress__block-library": "^2.6.1",
"@types/wordpress__blocks": "^11.0.7",
"@types/wordpress__rich-text": "^3.4.6",
"@types/wordpress__components": "^19.10.3",
"@types/wordpress__rich-text": "^3.4.6",
"@woocommerce/csv-export": "workspace:*",
"@woocommerce/currency": "workspace:*",
"@woocommerce/data": "workspace:*",
@ -87,10 +87,10 @@
"react-transition-group": "^4.4.2"
},
"peerDependencies": {
"@wordpress/data": "wp-6.0",
"lodash": "^4.17.0",
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2",
"@wordpress/data": "wp-6.0",
"lodash": "^4.17.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
@ -127,8 +127,8 @@
"@types/wordpress__media-utils": "^3.0.0",
"@types/wordpress__viewport": "^2.5.4",
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/internal-style-build": "workspace:*",
"@woocommerce/internal-js-tests": "workspace:*",
"@woocommerce/internal-style-build": "workspace:*",
"@wordpress/browserslist-config": "wp-6.0",
"@wordpress/scripts": "^12.6.1",
"concurrently": "^7.0.0",
@ -142,7 +142,6 @@
"rimraf": "^3.0.2",
"sass-loader": "^10.2.1",
"ts-jest": "^27.1.3",
"typescript": "^4.9.5",
"uuid": "^8.3.0",
"webpack": "^5.70.0",
"webpack-cli": "^3.3.12"

View File

@ -4,14 +4,14 @@ Displays a configurable set of filters which can modify query parameters. Displa
## Usage
Below is a config example complete with translation strings. Advanced filters makes use of [interpolateComponents](https://github.com/Automattic/interpolate-components#readme) to organize sentence structure, resulting in a filter visually represented as a sentence fragment in any language.
Below is a config example complete with translation strings. Advanced filters makes use of [createInterpolateElement](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-element/#createinterpolateelement) to organize sentence structure, resulting in a filter visually represented as a sentence fragment in any language.
```js
const config = {
title: __(
// A sentence describing filters for Orders
// See screen shot for context: https://cloudup.com/cSsUY9VeCVJ
'Orders Match {{select /}} Filters',
'Orders Match <select/> Filters',
'woocommerce'
),
filters: {
@ -25,10 +25,7 @@ const config = {
),
// A sentence describing an Order Status filter
// See screen shot for context: https://cloudup.com/cSsUY9VeCVJ
title: __(
'Order Status {{rule /}} {{filter /}}',
'woocommerce'
),
title: __( 'Order Status <rule/> <filter/>', 'woocommerce' ),
filter: __( 'Select an order status', 'woocommerce' ),
},
rules: [

View File

@ -3,10 +3,10 @@
*/
import PropTypes from 'prop-types';
import { SelectControl as Select, Spinner } from '@wordpress/components';
import interpolateComponents from '@automattic/interpolate-components';
import classnames from 'classnames';
import {
createElement,
createInterpolateElement,
Fragment,
useEffect,
useState,
@ -54,28 +54,22 @@ const getScreenReaderText = ( {
return '';
}
const filterStr = interpolateComponents( {
const filterStr = createInterpolateElement(
/* eslint-disable-next-line max-len */
/* translators: Sentence fragment describing a product attribute match. Example: "Color Is Not Blue" - attribute = Color, equals = Is Not, value = Blue */
mixedString: __(
'{{attribute /}} {{equals /}} {{value /}}',
'woocommerce'
),
components: {
__( '<attribute/> <equals/> <value/>', 'woocommerce' ),
{
attribute: <Fragment>{ attributeName }</Fragment>,
equals: <Fragment>{ rule.label }</Fragment>,
value: <Fragment>{ attributeTerm }</Fragment>,
},
} );
}
);
return textContent(
interpolateComponents( {
mixedString: config.labels.title,
components: {
filter: <Fragment>{ filterStr }</Fragment>,
rule: <Fragment />,
title: <Fragment />,
},
createInterpolateElement( config.labels.title, {
filter: <Fragment>{ filterStr }</Fragment>,
rule: <Fragment />,
title: <Fragment />,
} )
);
};
@ -154,114 +148,109 @@ const AttributeFilter = ( props ) => {
}
) }
>
{ interpolateComponents( {
mixedString: labels.title,
components: {
title: <span className={ className } />,
rule: (
<Select
className={ classnames(
className,
'woocommerce-filters-advanced__rule'
) }
options={ rules }
value={ rule }
onChange={ ( selectedValue ) =>
onFilterChange( {
property: 'rule',
value: selectedValue,
} )
}
aria-label={ labels.rule }
/>
),
filter: (
<div
className={ classnames(
className,
'woocommerce-filters-advanced__attribute-fieldset'
) }
>
{ ! Array.isArray( value ) ||
! value.length ||
selectedAttribute.length ? (
<Search
className="woocommerce-filters-advanced__input woocommerce-search"
onChange={ ( [ attr ] ) => {
setSelectedAttribute(
attr ? [ attr ] : []
);
setSelectedAttributeTerm( '' );
onFilterChange( {
property: 'value',
value: [
attr && attr.key,
].filter( Boolean ),
} );
} }
type="attributes"
placeholder={ __(
'Attribute name',
'woocommerce'
) }
multiple={ false }
selected={ selectedAttribute }
inlineTags
aria-label={ __(
'Attribute name',
'woocommerce'
) }
/>
{ createInterpolateElement( labels.title, {
title: <span className={ className } />,
rule: (
<Select
className={ classnames(
className,
'woocommerce-filters-advanced__rule'
) }
options={ rules }
value={ rule }
onChange={ ( selectedValue ) =>
onFilterChange( {
property: 'rule',
value: selectedValue,
} )
}
aria-label={ labels.rule }
/>
),
filter: (
<div
className={ classnames(
className,
'woocommerce-filters-advanced__attribute-fieldset'
) }
>
{ ! Array.isArray( value ) ||
! value.length ||
selectedAttribute.length ? (
<Search
className="woocommerce-filters-advanced__input woocommerce-search"
onChange={ ( [ attr ] ) => {
setSelectedAttribute(
attr ? [ attr ] : []
);
setSelectedAttributeTerm( '' );
onFilterChange( {
property: 'value',
value: [ attr && attr.key ].filter(
Boolean
),
} );
} }
type="attributes"
placeholder={ __(
'Attribute name',
'woocommerce'
) }
multiple={ false }
selected={ selectedAttribute }
inlineTags
aria-label={ __(
'Attribute name',
'woocommerce'
) }
/>
) : (
<Spinner />
) }
{ selectedAttribute.length > 0 &&
( attributeTerms.length ? (
<Fragment>
<span className="woocommerce-filters-advanced__attribute-field-separator">
=
</span>
<SelectControl
className="woocommerce-filters-advanced__input woocommerce-search"
placeholder={ __(
'Attribute value',
'woocommerce'
) }
inlineTags
isSearchable
multiple={ false }
showAllOnFocus
options={ attributeTerms }
selected={ selectedAttributeTerm }
onChange={ ( term ) => {
// Clearing the input using delete/backspace causes an empty array to be passed here.
if (
typeof term !== 'string'
) {
term = '';
}
setSelectedAttributeTerm(
term
);
onFilterChange( {
property: 'value',
value: [
selectedAttribute[ 0 ]
.key,
term,
].filter( Boolean ),
} );
} }
/>
</Fragment>
) : (
<Spinner />
) }
{ selectedAttribute.length > 0 &&
( attributeTerms.length ? (
<Fragment>
<span className="woocommerce-filters-advanced__attribute-field-separator">
=
</span>
<SelectControl
className="woocommerce-filters-advanced__input woocommerce-search"
placeholder={ __(
'Attribute value',
'woocommerce'
) }
inlineTags
isSearchable
multiple={ false }
showAllOnFocus
options={ attributeTerms }
selected={
selectedAttributeTerm
}
onChange={ ( term ) => {
// Clearing the input using delete/backspace causes an empty array to be passed here.
if (
typeof term !== 'string'
) {
term = '';
}
setSelectedAttributeTerm(
term
);
onFilterChange( {
property: 'value',
value: [
selectedAttribute[ 0 ]
.key,
term,
].filter( Boolean ),
} );
} }
/>
</Fragment>
) : (
<Spinner />
) ) }
</div>
),
},
) ) }
</div>
),
} ) }
</div>
{ screenReaderText && (

View File

@ -1,8 +1,12 @@
/**
* External dependencies
*/
import { createElement, Component, Fragment } from '@wordpress/element';
import interpolateComponents from '@automattic/interpolate-components';
import {
createElement,
createInterpolateElement,
Component,
Fragment,
} from '@wordpress/element';
import { SelectControl } from '@wordpress/components';
import { find, partial } from 'lodash';
import classnames from 'classnames';
@ -46,7 +50,7 @@ class DateFilter extends Component {
getBetweenString() {
return _x(
'{{after /}}{{span}} and {{/span}}{{before /}}',
'<after/><span> and </span><before/>',
'Date range inputs arranged on a single line',
'woocommerce'
);
@ -65,32 +69,22 @@ class DateFilter extends Component {
let filterStr = before.format( dateStringFormat );
if ( rule.value === 'between' ) {
filterStr = interpolateComponents( {
mixedString: this.getBetweenString(),
components: {
after: (
<Fragment>
{ after.format( dateStringFormat ) }
</Fragment>
),
before: (
<Fragment>
{ before.format( dateStringFormat ) }
</Fragment>
),
span: <Fragment />,
},
filterStr = createInterpolateElement( this.getBetweenString(), {
after: (
<Fragment>{ after.format( dateStringFormat ) }</Fragment>
),
before: (
<Fragment>{ before.format( dateStringFormat ) }</Fragment>
),
span: <Fragment />,
} );
}
return textContent(
interpolateComponents( {
mixedString: config.labels.title,
components: {
filter: <Fragment>{ filterStr }</Fragment>,
rule: <Fragment>{ rule.label }</Fragment>,
title: <Fragment />,
},
createInterpolateElement( config.labels.title, {
filter: <Fragment>{ filterStr }</Fragment>,
rule: <Fragment>{ rule.label }</Fragment>,
title: <Fragment />,
} )
);
}
@ -198,23 +192,20 @@ class DateFilter extends Component {
afterText,
afterError,
} = this.state;
return interpolateComponents( {
mixedString: this.getBetweenString(),
components: {
after: this.getFormControl( {
date: after,
error: afterError,
onUpdate: partial( this.onRangeDateChange, 'after' ),
text: afterText,
} ),
before: this.getFormControl( {
date: before,
error: beforeError,
onUpdate: partial( this.onRangeDateChange, 'before' ),
text: beforeText,
} ),
span: <span className="separator" />,
},
return createInterpolateElement( this.getBetweenString(), {
after: this.getFormControl( {
date: after,
error: afterError,
onUpdate: partial( this.onRangeDateChange, 'after' ),
text: afterText,
} ),
before: this.getFormControl( {
date: before,
error: beforeError,
onUpdate: partial( this.onRangeDateChange, 'before' ),
text: beforeText,
} ),
span: <span className="separator" />,
} );
}
@ -238,36 +229,33 @@ class DateFilter extends Component {
const { rule } = this.state;
const { labels, rules } = config;
const screenReaderText = this.getScreenReaderText( rule, config );
const children = interpolateComponents( {
mixedString: labels.title,
components: {
title: <span className={ className } />,
rule: (
<SelectControl
className={ classnames(
className,
'woocommerce-filters-advanced__rule'
) }
options={ rules }
value={ rule }
onChange={ this.onRuleChange }
aria-label={ labels.rule }
/>
),
filter: (
<div
className={ classnames(
className,
'woocommerce-filters-advanced__input-range',
{
'is-between': rule === 'between',
}
) }
>
{ this.getFilterInputs() }
</div>
),
},
const children = createInterpolateElement( labels.title, {
title: <span className={ className } />,
rule: (
<SelectControl
className={ classnames(
className,
'woocommerce-filters-advanced__rule'
) }
options={ rules }
value={ rule }
onChange={ this.onRuleChange }
aria-label={ labels.rule }
/>
),
filter: (
<div
className={ classnames(
className,
'woocommerce-filters-advanced__input-range',
{
'is-between': rule === 'between',
}
) }
>
{ this.getFilterInputs() }
</div>
),
} );
/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
return (

View File

@ -11,11 +11,15 @@ import {
Dropdown,
SelectControl,
} from '@wordpress/components';
import { createElement, Component, createRef } from '@wordpress/element';
import {
createElement,
createInterpolateElement,
Component,
createRef,
} from '@wordpress/element';
import { partial, difference, isEqual } from 'lodash';
import PropTypes from 'prop-types';
import AddOutlineIcon from 'gridicons/dist/add-outline';
import interpolateComponents from '@automattic/interpolate-components';
import {
getActiveFiltersFromQuery,
getDefaultOptionValue,
@ -143,22 +147,19 @@ class AdvancedFilters extends Component {
getTitle() {
const { match } = this.state;
const { config } = this.props;
return interpolateComponents( {
mixedString: config.title,
components: {
select: (
<SelectControl
className="woocommerce-filters-advanced__title-select"
options={ matches }
value={ match }
onChange={ this.onMatchChange }
aria-label={ __(
'Choose to apply any or all filters',
'woocommerce'
) }
/>
),
},
return createInterpolateElement( config.title, {
select: (
<SelectControl
className="woocommerce-filters-advanced__title-select"
options={ matches }
value={ match }
onChange={ this.onMatchChange }
aria-label={ __(
'Choose to apply any or all filters',
'woocommerce'
) }
/>
),
} );
}

View File

@ -1,10 +1,14 @@
/**
* External dependencies
*/
import { createElement, Component, Fragment } from '@wordpress/element';
import {
createElement,
createInterpolateElement,
Component,
Fragment,
} from '@wordpress/element';
import { SelectControl, TextControl } from '@wordpress/components';
import { get, find, isArray } from 'lodash';
import interpolateComponents from '@automattic/interpolate-components';
import classnames from 'classnames';
import { sprintf, __, _x } from '@wordpress/i18n';
import { CurrencyFactory } from '@woocommerce/currency';
@ -18,7 +22,7 @@ import { textContent } from './utils';
class NumberFilter extends Component {
getBetweenString() {
return _x(
'{{rangeStart /}}{{span}} and {{/span}}{{rangeEnd /}}',
'<rangeStart/><span> and </span><rangeEnd/>',
'Numerical range inputs arranged on a single line',
'woocommerce'
);
@ -46,24 +50,18 @@ class NumberFilter extends Component {
let filterStr = rangeStart;
if ( rule.value === 'between' ) {
filterStr = interpolateComponents( {
mixedString: this.getBetweenString(),
components: {
rangeStart: <Fragment>{ rangeStart }</Fragment>,
rangeEnd: <Fragment>{ rangeEnd }</Fragment>,
span: <Fragment />,
},
filterStr = createInterpolateElement( this.getBetweenString(), {
rangeStart: <Fragment>{ rangeStart }</Fragment>,
rangeEnd: <Fragment>{ rangeEnd }</Fragment>,
span: <Fragment />,
} );
}
return textContent(
interpolateComponents( {
mixedString: config.labels.title,
components: {
filter: <Fragment>{ filterStr }</Fragment>,
rule: <Fragment>{ rule.label }</Fragment>,
title: <Fragment />,
},
createInterpolateElement( config.labels.title, {
filter: <Fragment>{ filterStr }</Fragment>,
rule: <Fragment>{ rule.label }</Fragment>,
title: <Fragment />,
} )
);
}
@ -197,37 +195,34 @@ class NumberFilter extends Component {
} );
};
return interpolateComponents( {
mixedString: this.getBetweenString(),
components: {
rangeStart: this.getFormControl( {
type: inputType,
value: rangeStart || '',
label: sprintf(
/* eslint-disable-next-line max-len */
/* translators: Sentence fragment, "range start" refers to the first of two numeric values the field must be between. Screenshot for context: https://cloudup.com/cmv5CLyMPNQ */
__( '%(field)s range start', 'woocommerce' ),
{ field: get( config, [ 'labels', 'add' ] ) }
),
onChange: rangeStartOnChange,
currencySymbol,
symbolPosition,
} ),
rangeEnd: this.getFormControl( {
type: inputType,
value: rangeEnd || '',
label: sprintf(
/* eslint-disable-next-line max-len */
/* translators: Sentence fragment, "range end" refers to the second of two numeric values the field must be between. Screenshot for context: https://cloudup.com/cmv5CLyMPNQ */
__( '%(field)s range end', 'woocommerce' ),
{ field: get( config, [ 'labels', 'add' ] ) }
),
onChange: rangeEndOnChange,
currencySymbol,
symbolPosition,
} ),
span: <span className="separator" />,
},
return createInterpolateElement( this.getBetweenString(), {
rangeStart: this.getFormControl( {
type: inputType,
value: rangeStart || '',
label: sprintf(
/* eslint-disable-next-line max-len */
/* translators: Sentence fragment, "range start" refers to the first of two numeric values the field must be between. Screenshot for context: https://cloudup.com/cmv5CLyMPNQ */
__( '%(field)s range start', 'woocommerce' ),
{ field: get( config, [ 'labels', 'add' ] ) }
),
onChange: rangeStartOnChange,
currencySymbol,
symbolPosition,
} ),
rangeEnd: this.getFormControl( {
type: inputType,
value: rangeEnd || '',
label: sprintf(
/* eslint-disable-next-line max-len */
/* translators: Sentence fragment, "range end" refers to the second of two numeric values the field must be between. Screenshot for context: https://cloudup.com/cmv5CLyMPNQ */
__( '%(field)s range end', 'woocommerce' ),
{ field: get( config, [ 'labels', 'add' ] ) }
),
onChange: rangeEndOnChange,
currencySymbol,
symbolPosition,
} ),
span: <span className="separator" />,
} );
}
@ -237,38 +232,35 @@ class NumberFilter extends Component {
const { rule } = filter;
const { labels, rules } = config;
const children = interpolateComponents( {
mixedString: labels.title,
components: {
title: <span className={ className } />,
rule: (
<SelectControl
className={ classnames(
className,
'woocommerce-filters-advanced__rule'
) }
options={ rules }
value={ rule }
onChange={ ( value ) =>
onFilterChange( { property: 'rule', value } )
const children = createInterpolateElement( labels.title, {
title: <span className={ className } />,
rule: (
<SelectControl
className={ classnames(
className,
'woocommerce-filters-advanced__rule'
) }
options={ rules }
value={ rule }
onChange={ ( value ) =>
onFilterChange( { property: 'rule', value } )
}
aria-label={ labels.rule }
/>
),
filter: (
<div
className={ classnames(
className,
'woocommerce-filters-advanced__input-range',
{
'is-between': rule === 'between',
}
aria-label={ labels.rule }
/>
),
filter: (
<div
className={ classnames(
className,
'woocommerce-filters-advanced__input-range',
{
'is-between': rule === 'between',
}
) }
>
{ this.getFilterInputs() }
</div>
),
},
) }
>
{ this.getFilterInputs() }
</div>
),
} );
const screenReaderText = this.getScreenReaderText( filter, config );

View File

@ -1,12 +1,16 @@
/**
* External dependencies
*/
import { createElement, Component, Fragment } from '@wordpress/element';
import {
createElement,
createInterpolateElement,
Component,
Fragment,
} from '@wordpress/element';
import { SelectControl } from '@wordpress/components';
import { getIdsFromQuery } from '@woocommerce/navigation';
import { find, isEqual } from 'lodash';
import PropTypes from 'prop-types';
import interpolateComponents from '@automattic/interpolate-components';
import classnames from 'classnames';
/**
@ -86,13 +90,10 @@ class SearchFilter extends Component {
const filterStr = selected.map( ( item ) => item.label ).join( ', ' );
return textContent(
interpolateComponents( {
mixedString: config.labels.title,
components: {
filter: <Fragment>{ filterStr }</Fragment>,
rule: <Fragment>{ rule.label }</Fragment>,
title: <Fragment />,
},
createInterpolateElement( config.labels.title, {
filter: <Fragment>{ filterStr }</Fragment>,
rule: <Fragment>{ rule.label }</Fragment>,
title: <Fragment />,
} )
);
}
@ -103,40 +104,37 @@ class SearchFilter extends Component {
const { selected } = this.state;
const { rule } = filter;
const { input, labels, rules } = config;
const children = interpolateComponents( {
mixedString: labels.title,
components: {
title: <span className={ className } />,
rule: (
<SelectControl
className={ classnames(
className,
'woocommerce-filters-advanced__rule'
) }
options={ rules }
value={ rule }
onChange={ ( value ) =>
onFilterChange( { property: 'rule', value } )
}
aria-label={ labels.rule }
/>
),
filter: (
<Search
className={ classnames(
className,
'woocommerce-filters-advanced__input'
) }
onChange={ this.onSearchChange }
type={ input.type }
autocompleter={ input.autocompleter }
placeholder={ labels.placeholder }
selected={ selected }
inlineTags
aria-label={ labels.filter }
/>
),
},
const children = createInterpolateElement( labels.title, {
title: <span className={ className } />,
rule: (
<SelectControl
className={ classnames(
className,
'woocommerce-filters-advanced__rule'
) }
options={ rules }
value={ rule }
onChange={ ( value ) =>
onFilterChange( { property: 'rule', value } )
}
aria-label={ labels.rule }
/>
),
filter: (
<Search
className={ classnames(
className,
'woocommerce-filters-advanced__input'
) }
onChange={ this.onSearchChange }
type={ input.type }
autocompleter={ input.autocompleter }
placeholder={ labels.placeholder }
selected={ selected }
inlineTags
aria-label={ labels.filter }
/>
),
} );
const screenReaderText = this.getScreenReaderText( filter, config );

View File

@ -1,11 +1,15 @@
/**
* External dependencies
*/
import { createElement, Component, Fragment } from '@wordpress/element';
import {
createElement,
createInterpolateElement,
Component,
Fragment,
} from '@wordpress/element';
import { SelectControl, Spinner } from '@wordpress/components';
import { find } from 'lodash';
import PropTypes from 'prop-types';
import interpolateComponents from '@automattic/interpolate-components';
import classnames from 'classnames';
import { getDefaultOptionValue } from '@woocommerce/navigation';
@ -54,13 +58,10 @@ class SelectFilter extends Component {
find( config.input.options, { value: filter.value } ) || {};
return textContent(
interpolateComponents( {
mixedString: config.labels.title,
components: {
filter: <Fragment>{ value.label }</Fragment>,
rule: <Fragment>{ rule.label }</Fragment>,
title: <Fragment />,
},
createInterpolateElement( config.labels.title, {
filter: <Fragment>{ value.label }</Fragment>,
rule: <Fragment>{ rule.label }</Fragment>,
title: <Fragment />,
} )
);
}
@ -71,47 +72,44 @@ class SelectFilter extends Component {
const { options } = this.state;
const { rule, value } = filter;
const { labels, rules } = config;
const children = interpolateComponents( {
mixedString: labels.title,
components: {
title: <span className={ className } />,
rule: (
<SelectControl
className={ classnames(
className,
'woocommerce-filters-advanced__rule'
) }
options={ rules }
value={ rule }
onChange={ ( selectedValue ) =>
onFilterChange( {
property: 'rule',
value: selectedValue,
} )
}
aria-label={ labels.rule }
/>
),
filter: options ? (
<SelectControl
className={ classnames(
className,
'woocommerce-filters-advanced__input'
) }
options={ options }
value={ value }
onChange={ ( selectedValue ) =>
onFilterChange( {
property: 'value',
value: selectedValue,
} )
}
aria-label={ labels.filter }
/>
) : (
<Spinner />
),
},
const children = createInterpolateElement( labels.title, {
title: <span className={ className } />,
rule: (
<SelectControl
className={ classnames(
className,
'woocommerce-filters-advanced__rule'
) }
options={ rules }
value={ rule }
onChange={ ( selectedValue ) =>
onFilterChange( {
property: 'rule',
value: selectedValue,
} )
}
aria-label={ labels.rule }
/>
),
filter: options ? (
<SelectControl
className={ classnames(
className,
'woocommerce-filters-advanced__input'
) }
options={ options }
value={ value }
onChange={ ( selectedValue ) =>
onFilterChange( {
property: 'value',
value: selectedValue,
} )
}
aria-label={ labels.filter }
/>
) : (
<Spinner />
),
} );
const screenReaderText = this.getScreenReaderText( filter, config );

View File

@ -29,14 +29,14 @@ const query = {
};
const advancedFilters = {
title: 'Orders Match {{select /}} Filters',
title: 'Orders Match <select/> Filters',
filters: {
status: {
labels: {
add: 'Order Status',
remove: 'Remove order status filter',
rule: 'Select an order status filter match',
title: '{{title}}Order Status{{/title}} {{rule /}} {{filter /}}',
title: '<title>Order Status</title> <rule/> <filter/>',
filter: 'Select an order status',
},
rules: [
@ -63,7 +63,7 @@ const advancedFilters = {
placeholder: 'Search products',
remove: 'Remove products filter',
rule: 'Select a product filter match',
title: '{{title}}Product{{/title}} {{rule /}} {{filter /}}',
title: '<title>Product</title> <rule/> <filter/>',
filter: 'Select products',
},
rules: [
@ -87,7 +87,7 @@ const advancedFilters = {
add: 'Customer type',
remove: 'Remove customer filter',
rule: 'Select a customer filter match',
title: '{{title}}Customer is{{/title}} {{filter /}}',
title: '<title>Customer is</title> <filter/>',
filter: 'Select a customer type',
},
input: {
@ -104,7 +104,7 @@ const advancedFilters = {
add: 'Item Quantity',
remove: 'Remove item quantity filter',
rule: 'Select an item quantity filter match',
title: '{{title}}Item Quantity is{{/title}} {{rule /}} {{filter /}}',
title: '<title>Item Quantity is</title> <rule/> <filter/>',
},
rules: [
{
@ -129,7 +129,7 @@ const advancedFilters = {
add: 'Subtotal',
remove: 'Remove subtotal filter',
rule: 'Select a subtotal filter match',
title: '{{title}}Subtotal is{{/title}} {{rule /}} {{filter /}}',
title: '<title>Subtotal is</title> <rule/> <filter/>',
},
rules: [
{
@ -155,7 +155,7 @@ const advancedFilters = {
add: 'Date',
remove: 'Remove date filter',
rule: 'Select a date filter match',
title: '{{title}}Date{{/title}} {{rule /}} {{filter /}}',
title: '<title>Date</title> <rule/> <filter/>',
filter: 'Select a transaction date',
},
rules: [

View File

@ -34,14 +34,14 @@ const CURRENCY = {
};
const advancedFiltersConfig = {
title: 'Orders Match {{select /}} Filters',
title: 'Orders Match <select/> Filters',
filters: {
status: {
labels: {
add: 'Order Status',
remove: 'Remove order status filter',
rule: 'Select an order status filter match',
title: '{{title}}Order Status{{/title}} {{rule /}} {{filter /}}',
title: '<title>Order Status</title> <rule/> <filter/>',
filter: 'Select an order status',
},
rules: [
@ -68,7 +68,7 @@ const advancedFiltersConfig = {
placeholder: 'Search products',
remove: 'Remove products filter',
rule: 'Select a product filter match',
title: '{{title}}Product{{/title}} {{rule /}} {{filter /}}',
title: '<title>Product</title> <rule/> <filter/>',
filter: 'Select products',
},
rules: [
@ -92,7 +92,7 @@ const advancedFiltersConfig = {
add: 'Customer Type',
remove: 'Remove customer filter',
rule: 'Select a customer filter match',
title: '{{title}}Customer is{{/title}} {{filter /}}',
title: '<title>Customer is</title> <filter/>',
filter: 'Select a customer type',
},
input: {
@ -109,7 +109,7 @@ const advancedFiltersConfig = {
add: 'Item Quantity',
remove: 'Remove item quantity filter',
rule: 'Select an item quantity filter match',
title: '{{title}}Item Quantity is{{/title}} {{rule /}} {{filter /}}',
title: '<title>Item Quantity is</title> <rule/> <filter/>',
},
rules: [
{
@ -134,7 +134,7 @@ const advancedFiltersConfig = {
add: 'Subtotal',
remove: 'Remove subtotal filter',
rule: 'Select a subtotal filter match',
title: '{{title}}Subtotal is{{/title}} {{rule /}} {{filter /}}',
title: '<title>Subtotal is</title> <rule/> <filter/>',
},
rules: [
{
@ -160,7 +160,7 @@ const advancedFiltersConfig = {
add: 'Date',
remove: 'Remove date filter',
rule: 'Select a date filter match',
title: '{{title}}Date{{/title}} {{rule /}} {{filter /}}',
title: '<title>Date</title> <rule/> <filter/>',
filter: 'Select a transaction date',
},
rules: [

View File

@ -1,6 +1,7 @@
/**
* External dependencies
*/
import { useInstanceId } from '@wordpress/compose';
import { createElement, useState } from '@wordpress/element';
import { Icon, chevronDown, chevronUp } from '@wordpress/icons';
@ -33,27 +34,46 @@ export const CollapsibleContent: React.FC< CollapsedProps > = ( {
return persistRender ? 'visually-hidden' : 'hidden';
};
const collapsibleToggleId = useInstanceId(
CollapsibleContent,
'woocommerce-collapsible-content__toggle'
) as string;
const collapsibleContentId = useInstanceId(
CollapsibleContent,
'woocommerce-collapsible-content__content'
) as string;
const displayState = getState();
return (
<div
aria-expanded={ collapsed ? 'false' : 'true' }
className={ `woocommerce-collapsible-content` }
>
<div className="woocommerce-collapsible-content">
<button
type="button"
id={ collapsibleToggleId }
className="woocommerce-collapsible-content__toggle"
onClick={ () => setCollapsed( ! collapsed ) }
aria-expanded={ collapsed ? 'false' : 'true' }
aria-controls={
displayState !== 'hidden' ? collapsibleContentId : undefined
}
>
<div>
<span>{ toggleText }</span>
<Icon
icon={ collapsed ? chevronDown : chevronUp }
size={ 16 }
/>
</div>
</button>
<DisplayState state={ getState() }>
<DisplayState state={ displayState }>
<div
{ ...props }
className="woocommerce-collapsible-content__content"
id={ collapsibleContentId }
role="region"
aria-labelledby={ collapsibleToggleId }
>
{ children }
</div>

View File

@ -1,89 +0,0 @@
/**
* External dependencies
*/
import { createElement, Component } from '@wordpress/element';
import classnames from 'classnames';
import { Button, Dropdown, NavigableMenu } from '@wordpress/components';
import { Icon } from '@wordpress/icons';
import Ellipsis from 'gridicons/dist/ellipsis';
import PropTypes from 'prop-types';
/**
* This is a dropdown menu hidden behind a vertical ellipsis icon. When clicked, the inner MenuItems are displayed.
*/
class EllipsisMenu extends Component {
render() {
const { label, renderContent, className } = this.props;
if ( ! renderContent ) {
return null;
}
const renderEllipsis = ( { onToggle, isOpen } ) => {
const toggleClassname = classnames(
'woocommerce-ellipsis-menu__toggle',
{
'is-opened': isOpen,
}
);
return (
<Button
className={ toggleClassname }
onClick={ ( e ) => {
if ( this.props.onToggle ) {
this.props.onToggle( e );
}
onToggle( e );
} }
title={ label }
aria-expanded={ isOpen }
>
<Icon icon={ <Ellipsis /> } />
</Button>
);
};
const renderMenu = ( renderContentArgs ) => (
<NavigableMenu className="woocommerce-ellipsis-menu__content">
{ renderContent( renderContentArgs ) }
</NavigableMenu>
);
return (
<div
className={ classnames(
className,
'woocommerce-ellipsis-menu'
) }
>
<Dropdown
contentClassName="woocommerce-ellipsis-menu__popover"
position="bottom left"
renderToggle={ renderEllipsis }
renderContent={ renderMenu }
/>
</div>
);
}
}
EllipsisMenu.propTypes = {
/**
* The label shown when hovering/focusing on the icon button.
*/
label: PropTypes.string.isRequired,
/**
* A function returning `MenuTitle`/`MenuItem` components as a render prop. Arguments from Dropdown passed as function arguments.
*/
renderContent: PropTypes.func,
/**
* Classname to add to ellipsis menu.
*/
className: PropTypes.string,
/**
* Callback function when dropdown button is clicked, it provides the click event.
*/
onToggle: PropTypes.func,
};
export default EllipsisMenu;

View File

@ -0,0 +1,98 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import classnames from 'classnames';
import { Button, Dropdown, NavigableMenu } from '@wordpress/components';
import { Icon } from '@wordpress/icons';
import Ellipsis from 'gridicons/dist/ellipsis';
import React, { MouseEvent, KeyboardEvent, ReactNode } from 'react';
type CallbackProps = {
isOpen?: boolean;
onToggle: () => void;
onClose?: () => void;
};
type EllipsisMenuProps = {
/**
* The label shown when hovering/focusing on the icon button.
*/
label: string;
/**
* A function returning `MenuTitle`/`MenuItem` components as a render prop. Arguments from Dropdown passed as function arguments.
*/
renderContent?: ( props: CallbackProps ) => ReactNode | JSX.Element;
/**
* Classname to add to ellipsis menu.
*/
className?: string;
/**
* Callback function when dropdown button is clicked, it provides the click event.
*/
onToggle?: ( e: MouseEvent | KeyboardEvent ) => void;
};
/**
* This is a dropdown menu hidden behind a vertical ellipsis icon. When clicked, the inner MenuItems are displayed.
*/
const EllipsisMenu = ( {
label,
renderContent,
className,
onToggle,
}: EllipsisMenuProps ) => {
if ( ! renderContent ) {
return null;
}
const renderEllipsis = ( {
onToggle: toggleHandlerOverride,
isOpen,
}: CallbackProps ) => {
const toggleClassname = classnames(
'woocommerce-ellipsis-menu__toggle',
{
'is-opened': isOpen,
}
);
return (
<Button
className={ toggleClassname }
onClick={ ( e: MouseEvent | KeyboardEvent ) => {
if ( onToggle ) {
onToggle( e );
}
if ( toggleHandlerOverride ) {
toggleHandlerOverride();
}
} }
title={ label }
aria-expanded={ isOpen }
>
<Icon icon={ <Ellipsis /> } />
</Button>
);
};
const renderMenu = ( renderContentArgs: CallbackProps ) => (
<NavigableMenu className="woocommerce-ellipsis-menu__content">
{ renderContent( renderContentArgs ) }
</NavigableMenu>
);
return (
<div className={ classnames( className, 'woocommerce-ellipsis-menu' ) }>
<Dropdown
contentClassName="woocommerce-ellipsis-menu__popover"
position="bottom left"
renderToggle={ renderEllipsis }
renderContent={ renderMenu }
/>
</div>
);
};
export default EllipsisMenu;

View File

@ -1,129 +0,0 @@
/**
* External dependencies
*/
import { BaseControl, FormToggle } from '@wordpress/components';
import { createElement, Component, createRef } from '@wordpress/element';
import { DOWN, ENTER, SPACE, UP } from '@wordpress/keycodes';
import PropTypes from 'prop-types';
/**
* `MenuItem` is used to give the item an accessible wrapper, with the `menuitem` role and added keyboard functionality (`onInvoke`).
* `MenuItem`s can also be deemed "clickable", though this is disabled by default because generally the inner component handles
* the click event.
*/
class MenuItem extends Component {
constructor() {
super( ...arguments );
this.onClick = this.onClick.bind( this );
this.onFocusFormToggle = this.onFocusFormToggle.bind( this );
this.onKeyDown = this.onKeyDown.bind( this );
this.container = createRef();
}
onClick( event ) {
const { isClickable, onInvoke } = this.props;
if ( isClickable ) {
event.preventDefault();
onInvoke();
}
}
onKeyDown( event ) {
if ( event.target.isSameNode( event.currentTarget ) ) {
if ( event.keyCode === ENTER || event.keyCode === SPACE ) {
event.preventDefault();
this.props.onInvoke();
}
if ( event.keyCode === UP ) {
event.preventDefault();
}
if ( event.keyCode === DOWN ) {
event.preventDefault();
const nextElementToFocus =
event.target.nextSibling ||
event.target.parentNode.querySelector(
'.woocommerce-ellipsis-menu__item'
);
nextElementToFocus.focus();
}
}
}
onFocusFormToggle() {
this.container.current.focus();
}
render() {
const { checked, children, isCheckbox } = this.props;
if ( isCheckbox ) {
return (
<div
aria-checked={ checked }
ref={ this.container }
role="menuitemcheckbox"
tabIndex="0"
onKeyDown={ this.onKeyDown }
onClick={ this.onClick }
className="woocommerce-ellipsis-menu__item"
>
<BaseControl className="components-toggle-control">
<FormToggle
aria-hidden="true"
checked={ checked }
onChange={ this.props.onInvoke }
onFocus={ this.onFocusFormToggle }
onClick={ ( e ) => e.stopPropagation() }
tabIndex="-1"
/>
{ children }
</BaseControl>
</div>
);
}
return (
<div
role="menuitem"
tabIndex="0"
onKeyDown={ this.onKeyDown }
onClick={ this.onClick }
className="woocommerce-ellipsis-menu__item"
>
{ children }
</div>
);
}
}
MenuItem.propTypes = {
/**
* Whether the menu item is checked or not. Only relevant for menu items with `isCheckbox`.
*/
checked: PropTypes.bool,
/**
* A renderable component (or string) which will be displayed as the content of this item. Generally a `ToggleControl`.
*/
children: PropTypes.node,
/**
* Whether the menu item is a checkbox (will render a FormToggle and use the `menuitemcheckbox` role).
*/
isCheckbox: PropTypes.bool,
/**
* Boolean to control whether the MenuItem should handle the click event. Defaults to false, assuming your child component
* handles the click event.
*/
isClickable: PropTypes.bool,
/**
* A function called when this item is activated via keyboard ENTER or SPACE; or when the item is clicked
* (only if `isClickable` is set).
*/
onInvoke: PropTypes.func.isRequired,
};
MenuItem.defaultProps = {
isClickable: false,
isCheckbox: false,
};
export default MenuItem;

View File

@ -0,0 +1,115 @@
/**
* External dependencies
*/
import { BaseControl, FormToggle } from '@wordpress/components';
import { createElement } from '@wordpress/element';
import { DOWN, ENTER, SPACE, UP } from '@wordpress/keycodes';
import { useRef, MouseEvent, KeyboardEvent } from 'react';
type MenuItemProps = {
/**
* Whether the menu item is checked or not. Only relevant for menu items with `isCheckbox`.
*/
checked?: boolean;
/**
* A renderable component (or string) which will be displayed as the content of this item. Generally a `ToggleControl`.
*/
children?: React.ReactNode;
/**
* Whether the menu item is a checkbox (will render a FormToggle and use the `menuitemcheckbox` role).
*/
isCheckbox?: boolean;
/**
* Boolean to control whether the MenuItem should handle the click event. Defaults to false, assuming your child component
* handles the click event.
*/
isClickable?: boolean;
/**
* A function called when this item is activated via keyboard ENTER or SPACE; or when the item is clicked
* (only if `isClickable` is set).
*/
onInvoke: ( () => void ) | undefined;
};
const MenuItem = ( {
checked,
children,
isCheckbox = false,
isClickable = false,
onInvoke = () => {},
}: MenuItemProps ) => {
const container = useRef< HTMLInputElement >( null );
const onClick = ( event: MouseEvent< HTMLDivElement > ) => {
if ( isClickable ) {
event.preventDefault();
onInvoke();
}
};
const onKeyDown = ( event: KeyboardEvent< HTMLDivElement > ) => {
const eventTarget = event.target as HTMLElement;
if ( eventTarget.isSameNode( event.currentTarget ) ) {
if ( event.keyCode === ENTER || event.keyCode === SPACE ) {
event.preventDefault();
onInvoke();
}
if ( event.keyCode === UP ) {
event.preventDefault();
}
if ( event.keyCode === DOWN ) {
event.preventDefault();
const nextElementToFocus = ( eventTarget.nextSibling ||
eventTarget.parentNode?.querySelector(
'.woocommerce-ellipsis-menu__item'
) ) as HTMLElement;
nextElementToFocus.focus();
}
}
};
const onFocusFormToggle = () => {
container?.current?.focus();
};
if ( isCheckbox ) {
return (
<div
aria-checked={ checked }
ref={ container }
role="menuitemcheckbox"
tabIndex={ 0 }
onKeyDown={ onKeyDown }
onClick={ onClick }
className="woocommerce-ellipsis-menu__item"
>
{ /* id props is actuall an optional prop. It looks like DefinitelyTyped has out-of-date types*/ }
{ /* @ts-expect-error: Suprressing `id` is required prop error. */ }
<BaseControl className="components-toggle-control">
<FormToggle
aria-hidden="true"
checked={ checked }
onChange={ onInvoke }
onFocus={ onFocusFormToggle }
onClick={ ( e ) => e.stopPropagation() }
tabIndex={ -1 }
/>
{ children }
</BaseControl>
</div>
);
}
return (
<div
role="menuitem"
tabIndex={ 0 }
onKeyDown={ onKeyDown }
onClick={ onClick }
className="woocommerce-ellipsis-menu__item"
>
{ children }
</div>
);
};
export default MenuItem;

View File

@ -1,26 +1,23 @@
/**
* External dependencies
*/
import PropTypes from 'prop-types';
import { createElement } from '@wordpress/element';
import React from 'react';
/**
* `MenuTitle` is another valid Menu child, but this does not have any accessibility attributes associated
* (so this should not be used in place of the `EllipsisMenu` prop `label`).
*
* @param {Object} props
* @param {Node} props.children
* @return {Object} -
*/
const MenuTitle = ( { children } ) => {
return <div className="woocommerce-ellipsis-menu__title">{ children }</div>;
};
MenuTitle.propTypes = {
const MenuTitle = ( {
children,
}: {
/**
* A renderable component (or string) which will be displayed as the content of this item.
*/
children: PropTypes.node,
children: React.ReactNode;
} ) => {
return <div className="woocommerce-ellipsis-menu__title">{ children }</div>;
};
export default MenuTitle;

View File

@ -44,3 +44,18 @@
.woocommerce-experimental-select-control__suffix {
align-self: stretch;
}
.woocommerce-experimental-select-control__combox-box-toggle-button {
all: unset;
position: absolute;
right: 6px;
top: 50%;
transform: translateY( -50% );
> svg {
margin-top: 4px;
}
}
.woocommerce-experimental-select-control:not( .is-focused ) .woocommerce-experimental-select-control__combox-box-toggle-button {
pointer-events: none; // Prevents the icon from being clickable when the combobox is not focused, because otherwise we get a race condition when clicking on the icon, because focussing the combobox opens the menu, then sequentially the icon toggles it back closed
}

View File

@ -1,26 +1,42 @@
/**
* External dependencies
*/
import { createElement, MouseEvent, useRef } from 'react';
import { createElement, MouseEvent, useRef, forwardRef } from 'react';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import { Props } from './types';
import { Icon, chevronDown } from '@wordpress/icons';
type ComboBoxProps = {
children?: JSX.Element | JSX.Element[] | null;
comboBoxProps: Props;
inputProps: Props;
comboBoxProps: JSX.IntrinsicElements[ 'div' ];
inputProps: JSX.IntrinsicElements[ 'input' ];
getToggleButtonProps?: () => Omit<
JSX.IntrinsicElements[ 'button' ],
'ref'
>;
suffix?: JSX.Element | null;
showToggleButton?: boolean;
};
const ToggleButton = forwardRef< HTMLButtonElement >( ( props, ref ) => {
// using forwardRef here because getToggleButtonProps injects a ref prop
return (
<button
className="woocommerce-experimental-select-control__combox-box-toggle-button"
{ ...props }
ref={ ref }
>
<Icon icon={ chevronDown } />
</button>
);
} );
export const ComboBox = ( {
children,
comboBoxProps,
getToggleButtonProps = () => ( {} ),
inputProps,
suffix,
showToggleButton,
}: ComboBoxProps ) => {
const inputRef = useRef< HTMLInputElement | null >( null );
@ -29,9 +45,8 @@ export const ComboBox = ( {
return;
}
event.preventDefault();
if ( document.activeElement !== inputRef.current ) {
event.preventDefault();
inputRef.current.focus();
event.stopPropagation();
}
@ -60,12 +75,14 @@ export const ComboBox = ( {
<input
{ ...inputProps }
ref={ ( node ) => {
inputRef.current = node;
(
inputProps.ref as unknown as (
node: HTMLInputElement | null
) => void
)( node );
if ( typeof inputProps.ref === 'function' ) {
inputRef.current = node;
(
inputProps.ref as unknown as (
node: HTMLInputElement | null
) => void
)( node );
}
} }
/>
</div>
@ -75,6 +92,9 @@ export const ComboBox = ( {
{ suffix }
</div>
) }
{ showToggleButton && (
<ToggleButton { ...getToggleButtonProps() } />
) }
</div>
);
};

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { createElement, ReactElement } from 'react';
import { createElement, CSSProperties, ReactElement } from 'react';
/**
* Internal dependencies
@ -14,6 +14,7 @@ export type MenuItemProps< ItemType > = {
item: ItemType;
children: ReactElement | string;
getItemProps: getItemPropsType< ItemType >;
activeStyle?: CSSProperties;
};
export const MenuItem = < ItemType, >( {
@ -21,11 +22,12 @@ export const MenuItem = < ItemType, >( {
getItemProps,
index,
isActive,
activeStyle = { backgroundColor: '#bde4ff' },
item,
}: MenuItemProps< ItemType > ) => {
return (
<li
style={ isActive ? { backgroundColor: '#bde4ff' } : {} }
style={ isActive ? activeStyle : {} }
{ ...getItemProps( { item, index } ) }
className="woocommerce-experimental-select-control__menu-item"
>

View File

@ -10,6 +10,7 @@ import {
useState,
createPortal,
Children,
useLayoutEffect,
} from '@wordpress/element';
/**
@ -22,6 +23,8 @@ type MenuProps = {
getMenuProps: getMenuPropsType;
isOpen: boolean;
className?: string;
position?: Popover.Position;
scrollIntoViewOnOpen?: boolean;
};
export const Menu = ( {
@ -29,17 +32,32 @@ export const Menu = ( {
getMenuProps,
isOpen,
className,
position = 'bottom right',
scrollIntoViewOnOpen = false,
}: MenuProps ) => {
const [ boundingRect, setBoundingRect ] = useState< DOMRect >();
const selectControlMenuRef = useRef< HTMLDivElement >( null );
useEffect( () => {
if ( selectControlMenuRef.current?.parentElement ) {
useLayoutEffect( () => {
if (
selectControlMenuRef.current?.parentElement &&
selectControlMenuRef.current?.parentElement.clientWidth > 0
) {
setBoundingRect(
selectControlMenuRef.current.parentElement.getBoundingClientRect()
);
}
}, [ selectControlMenuRef.current ] );
}, [
selectControlMenuRef.current,
selectControlMenuRef.current?.clientWidth,
] );
// Scroll the selected item into view when the menu opens.
useEffect( () => {
if ( isOpen && scrollIntoViewOnOpen ) {
selectControlMenuRef.current?.scrollIntoView();
}
}, [ isOpen, scrollIntoViewOnOpen ] );
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
/* Disabled because of the onmouseup on the ul element below. */
@ -60,7 +78,7 @@ export const Menu = ( {
'has-results': Children.count( children ) > 0,
}
) }
position="bottom right"
position={ position }
animate={ false }
>
<ul

View File

@ -68,6 +68,7 @@ export type SelectControlProps< ItemType > = {
disabled?: boolean;
inputProps?: GetInputPropsOptions;
suffix?: JSX.Element | null;
showToggleButton?: boolean;
/**
* This is a feature already implemented in downshift@7.0.0 through the
* reducer. In order for us to use it this prop is added temporarily until
@ -123,6 +124,7 @@ function SelectControl< ItemType = DefaultItemType >( {
disabled,
inputProps = {},
suffix = <SuffixIcon icon={ search } />,
showToggleButton = false,
__experimentalOpenMenuOnFocus = false,
}: SelectControlProps< ItemType > ) {
const [ isFocused, setIsFocused ] = useState( false );
@ -154,12 +156,13 @@ function SelectControl< ItemType = DefaultItemType >( {
}
setInputValue( getItemLabel( singleSelectedItem ) );
}, [ singleSelectedItem ] );
}, [ getItemLabel, multiple, singleSelectedItem ] );
const {
isOpen,
getLabelProps,
getMenuProps,
getToggleButtonProps,
getInputProps,
getComboboxProps,
highlightedIndex,
@ -174,8 +177,13 @@ function SelectControl< ItemType = DefaultItemType >( {
items: filteredItems,
selectedItem: multiple ? null : singleSelectedItem,
itemToString: getItemLabel,
onSelectedItemChange: ( { selectedItem } ) =>
selectedItem && onSelect( selectedItem ),
onSelectedItemChange: ( { selectedItem } ) => {
if ( selectedItem ) {
onSelect( selectedItem );
} else if ( singleSelectedItem ) {
onRemove( singleSelectedItem );
}
},
onInputValueChange: ( { inputValue: value, ...changes } ) => {
if ( value !== undefined ) {
setInputValue( value );
@ -190,8 +198,13 @@ function SelectControl< ItemType = DefaultItemType >( {
// Set input back to selected item if there is a selected item, blank otherwise.
newChanges = {
...changes,
selectedItem:
! changes.inputValue?.length && ! multiple
? null
: changes.selectedItem,
inputValue:
changes.selectedItem === state.selectedItem &&
changes.inputValue?.length &&
! multiple
? getItemLabel( comboboxSingleSelectedItem )
: '',
@ -256,6 +269,7 @@ function SelectControl< ItemType = DefaultItemType >( {
{ /* eslint-enable jsx-a11y/label-has-for */ }
<ComboBox
comboBoxProps={ getComboboxProps() }
getToggleButtonProps={ getToggleButtonProps }
inputProps={ getInputProps( {
...getDropdownProps( {
preventKeyAction: isOpen,
@ -274,6 +288,7 @@ function SelectControl< ItemType = DefaultItemType >( {
...inputProps,
} ) }
suffix={ suffix }
showToggleButton={ showToggleButton }
>
<>
{ children( {

View File

@ -573,6 +573,24 @@ export const CustomSuffix: React.FC = () => {
);
};
export const ToggleButton: React.FC = () => {
const [ selected, setSelected ] =
useState< SelectedType< DefaultItemType > >();
return (
<SelectControl
items={ sampleItems }
label="Has toggle button"
selected={ selected }
onSelect={ ( item ) => item && setSelected( item ) }
onRemove={ () => setSelected( null ) }
suffix={ null }
showToggleButton={ true }
__experimentalOpenMenuOnFocus={ true }
/>
);
};
export default {
title: 'WooCommerce Admin/experimental/SelectControl',
component: SelectControl,

View File

@ -16,7 +16,8 @@ export type DefaultItemType = {
export type SelectedType< ItemType > = ItemType | null;
export type Props = {
[ key: string ]: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[ key: string ]: any;
};
export type getItemPropsType< ItemType > = (

View File

@ -0,0 +1,2 @@
export * from './select-tree';
export * from './select-tree-menu';

View File

@ -0,0 +1,154 @@
/**
* External dependencies
*/
import { Popover, Spinner } from '@wordpress/components';
import classnames from 'classnames';
import {
createElement,
useEffect,
useRef,
createPortal,
useLayoutEffect,
useState,
} from '@wordpress/element';
/**
* Internal dependencies
*/
import {
LinkedTree,
Tree,
TreeControlProps,
} from '../experimental-tree-control';
type MenuProps = {
isOpen: boolean;
isLoading?: boolean;
position?: Popover.Position;
scrollIntoViewOnOpen?: boolean;
items: LinkedTree[];
treeRef?: React.ForwardedRef< HTMLOListElement >;
onClose?: () => void;
} & Omit< TreeControlProps, 'items' >;
export const SelectTreeMenu = ( {
isLoading,
isOpen,
className,
position = 'bottom center',
scrollIntoViewOnOpen = false,
items,
treeRef: ref,
onClose = () => {},
shouldShowCreateButton,
...props
}: MenuProps ) => {
const [ boundingRect, setBoundingRect ] = useState< DOMRect >();
const selectControlMenuRef = useRef< HTMLDivElement >( null );
useLayoutEffect( () => {
if (
selectControlMenuRef.current?.parentElement &&
selectControlMenuRef.current?.parentElement.clientWidth > 0
) {
setBoundingRect(
selectControlMenuRef.current.parentElement.getBoundingClientRect()
);
}
}, [
selectControlMenuRef.current,
selectControlMenuRef.current?.clientWidth,
] );
// Scroll the selected item into view when the menu opens.
useEffect( () => {
if ( isOpen && scrollIntoViewOnOpen ) {
selectControlMenuRef.current?.scrollIntoView();
}
}, [ isOpen, scrollIntoViewOnOpen ] );
const shouldItemBeExpanded = ( item: LinkedTree ): boolean => {
if ( ! props.createValue || ! item.children?.length ) return false;
return item.children.some( ( child ) => {
if (
new RegExp( props.createValue || '', 'ig' ).test(
child.data.label
)
) {
return true;
}
return shouldItemBeExpanded( child );
} );
};
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
/* Disabled because of the onmouseup on the ul element below. */
return (
<div
ref={ selectControlMenuRef }
className="woocommerce-experimental-select-tree-control__menu"
>
<div>
<Popover
// @ts-expect-error this prop does exist, see: https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/popover/index.tsx#L180.
__unstableSlotName="woocommerce-select-tree-control-menu"
focusOnMount={ false }
className={ classnames(
'woocommerce-experimental-select-tree-control__popover-menu',
className,
{
'is-open': isOpen,
'has-results': items.length > 0,
}
) }
position={ position }
animate={ false }
onFocusOutside={ () => {
onClose();
} }
>
{ isOpen && (
<div>
{ isLoading ? (
<div
style={ {
width: boundingRect?.width,
} }
>
<Spinner />
</div>
) : (
<Tree
{ ...props }
id={ `${ props.id }-menu` }
ref={ ref }
items={ items }
onTreeBlur={ onClose }
shouldItemBeExpanded={
shouldItemBeExpanded
}
shouldShowCreateButton={
shouldShowCreateButton
}
style={ {
width: boundingRect?.width,
} }
/>
) }
</div>
) }
</Popover>
</div>
</div>
);
/* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
};
export const SelectTreeMenuSlot: React.FC = () =>
createPortal(
<div aria-live="off">
{ /* @ts-expect-error name does exist on PopoverSlot see: https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/popover/index.tsx#L555 */ }
<Popover.Slot name="woocommerce-select-tree-control-menu" />
</div>,
document.body
);

View File

@ -0,0 +1,19 @@
.woocommerce-experimental-select-control__combo-box-wrapper {
&:focus {
box-shadow: 0 0 0 1px var( --wp-admin-theme-color );
border-color: var( --wp-admin-theme-color );
}
}
.woocommerce-experimental-select-tree-control__dropdown {
display: block;
}
// That's the only way I could remove the padding from the @wordpress/components Dropdown.
.woocommerce-experimental-select-tree-control__dropdown-content .components-popover__content {
padding: 0;
}
.woocommerce-experimental-select-tree-control__popover-menu {
min-height: 100px;
}

View File

@ -0,0 +1,154 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* External dependencies
*/
import { createElement, useState } from '@wordpress/element';
import classNames from 'classnames';
import { search } from '@wordpress/icons';
import { useInstanceId } from '@wordpress/compose';
/**
* Internal dependencies
*/
import { useLinkedTree } from '../experimental-tree-control/hooks/use-linked-tree';
import { Item, TreeControlProps } from '../experimental-tree-control/types';
import { SelectedItems } from '../experimental-select-control/selected-items';
import { ComboBox } from '../experimental-select-control/combo-box';
import { SuffixIcon } from '../experimental-select-control/suffix-icon';
import { SelectTreeMenu } from './select-tree-menu';
interface SelectTreeProps extends TreeControlProps {
id: string;
selected?: Item[];
getSelectedItemProps?: any;
treeRef?: React.ForwardedRef< HTMLOListElement >;
suffix?: JSX.Element | null;
isLoading?: boolean;
label: string | JSX.Element;
onInputChange?: ( value: string | undefined ) => void;
}
export const SelectTree = function SelectTree( {
items,
getSelectedItemProps,
treeRef: ref,
suffix = <SuffixIcon icon={ search } />,
placeholder,
isLoading,
onInputChange,
shouldShowCreateButton,
...props
}: SelectTreeProps ) {
const linkedTree = useLinkedTree( items );
const menuInstanceId = useInstanceId(
SelectTree,
'woocommerce-select-tree-control__menu'
);
const [ isFocused, setIsFocused ] = useState( false );
const [ isOpen, setIsOpen ] = useState( false );
return (
<div
className="woocommerce-experimental-select-tree-control__dropdown"
tabIndex={ -1 }
>
<div
className={ classNames(
'woocommerce-experimental-select-control',
{
'is-focused': isFocused,
}
) }
>
<label
htmlFor={ `${ props.id }-input` }
id={ `${ props.id }-label` }
className="woocommerce-experimental-select-control__label"
>
{ props.label }
</label>
<ComboBox
comboBoxProps={ {
className:
'woocommerce-experimental-select-control__combo-box-wrapper',
role: 'combobox',
'aria-expanded': isOpen,
'aria-haspopup': 'tree',
'aria-labelledby': `${ props.id }-label`,
'aria-owns': `${ props.id }-menu`,
} }
inputProps={ {
className:
'woocommerce-experimental-select-control__input',
id: `${ props.id }-input`,
'aria-autocomplete': 'list',
'aria-controls': `${ props.id }-menu`,
autoComplete: 'off',
onFocus: () => {
if ( ! isOpen ) {
setIsOpen( true );
}
setIsFocused( true );
},
onBlur: ( event ) => {
// if blurring to an element inside the dropdown, don't close it
if (
isOpen &&
! document
.querySelector( '.' + menuInstanceId )
?.contains( event.relatedTarget )
) {
setIsOpen( false );
}
setIsFocused( false );
},
onKeyDown: ( event ) => {
setIsOpen( true );
if ( event.key === 'ArrowDown' ) {
event.preventDefault();
// focus on the first element from the Popover
(
document.querySelector(
`.${ menuInstanceId } input, .${ menuInstanceId } button`
) as HTMLInputElement | HTMLButtonElement
)?.focus();
}
if ( event.key === 'Tab' ) {
setIsOpen( false );
}
},
onChange: ( event ) =>
onInputChange &&
onInputChange( event.target.value ),
placeholder,
} }
suffix={ suffix }
>
<SelectedItems
items={ ( props.selected as Item[] ) || [] }
getItemLabel={ ( item ) => item?.label || '' }
getItemValue={ ( item ) => item?.value || '' }
onRemove={ ( item ) => {
if ( ! Array.isArray( item ) && props.onRemove ) {
props.onRemove( item );
setIsOpen( false );
}
} }
getSelectedItemProps={ () => ( {} ) }
/>
</ComboBox>
</div>
<SelectTreeMenu
{ ...props }
id={ `${ props.id }-menu` }
className={ menuInstanceId.toString() }
ref={ ref }
isOpen={ isOpen }
items={ linkedTree }
shouldShowCreateButton={ shouldShowCreateButton }
onClose={ () => setIsOpen( false ) }
/>
</div>
);
};

View File

@ -0,0 +1,168 @@
/**
* External dependencies
*/
import React, { createElement, useState } from 'react';
import { Button, Modal, SlotFillProvider } from '@wordpress/components';
/**
* Internal dependencies
*/
import { SelectTree } from '../select-tree';
import { Item } from '../../experimental-tree-control/types';
import { SelectTreeMenuSlot } from '../select-tree-menu';
const listItems: Item[] = [
{ value: '1', label: 'Technology' },
{ value: '1.1', label: 'Notebooks', parent: '1' },
{ value: '1.2', label: 'Phones', parent: '1' },
{ value: '1.2.1', label: 'iPhone', parent: '1.2' },
{ value: '1.2.1.1', label: 'iPhone 14 Pro', parent: '1.2.1' },
{ value: '1.2.1.2', label: 'iPhone 14 Pro Max', parent: '1.2.1' },
{ value: '1.2.2', label: 'Samsung', parent: '1.2' },
{ value: '1.2.2.1', label: 'Samsung Galaxy 22 Plus', parent: '1.2.2' },
{ value: '1.2.2.2', label: 'Samsung Galaxy 22 Ultra', parent: '1.2.2' },
{ value: '1.3', label: 'Wearables', parent: '1' },
{ value: '2', label: 'Hardware' },
{ value: '2.1', label: 'CPU', parent: '2' },
{ value: '2.2', label: 'GPU', parent: '2' },
{ value: '2.3', label: 'Memory RAM', parent: '2' },
{ value: '3', label: 'Other' },
];
const filterItems = ( items: Item[], searchValue ) => {
const filteredItems = items.filter( ( e ) =>
e.label.includes( searchValue )
);
const itemsToIterate = [ ...filteredItems ];
while ( itemsToIterate.length > 0 ) {
// The filter should include the parents of the filtered items
const element = itemsToIterate.pop();
if ( element ) {
const parent = listItems.find(
( item ) => item.value === element.parent
);
if ( parent && ! filteredItems.includes( parent ) ) {
filteredItems.push( parent );
itemsToIterate.push( parent );
}
}
}
return filteredItems;
};
export const MultipleSelectTree: React.FC = () => {
const [ value, setValue ] = React.useState( '' );
const [ selected, setSelected ] = React.useState< Item[] >( [] );
const items = filterItems( listItems, value );
return (
<SelectTree
id="multiple-select-tree"
label="Multiple Select Tree"
multiple
items={ items }
selected={ selected }
shouldNotRecursivelySelect
shouldShowCreateButton={ ( typedValue ) =>
! value ||
listItems.findIndex( ( item ) => item.label === typedValue ) ===
-1
}
createValue={ value }
// eslint-disable-next-line no-alert
onCreateNew={ () => alert( 'create new called' ) }
onInputChange={ ( a ) => setValue( a || '' ) }
onSelect={ ( selectedItems ) => {
if ( Array.isArray( selectedItems ) ) {
setSelected( [ ...selected, ...selectedItems ] );
}
} }
onRemove={ ( removedItems ) => {
const newValues = Array.isArray( removedItems )
? selected.filter(
( item ) =>
! removedItems.some(
( { value: removedValue } ) =>
item.value === removedValue
)
)
: selected.filter(
( item ) => item.value !== removedItems.value
);
setSelected( newValues );
} }
/>
);
};
export const SingleWithinModalUsingBodyDropdownPlacement: React.FC = () => {
const [ isOpen, setOpen ] = useState( true );
const [ value, setValue ] = useState( '' );
const [ selected, setSelected ] = useState< Item[] >( [] );
const items = filterItems( listItems, value );
return (
<SlotFillProvider>
Selected: { JSON.stringify( selected ) }
<Button onClick={ () => setOpen( true ) }>
Show Dropdown in Modal
</Button>
{ isOpen && (
<Modal
title="Dropdown Modal"
onRequestClose={ () => setOpen( false ) }
>
<SelectTree
id="multiple-select-tree"
label="Multiple Select Tree"
multiple
items={ items }
selected={ selected }
shouldNotRecursivelySelect
shouldShowCreateButton={ ( typedValue ) =>
! value ||
listItems.findIndex(
( item ) => item.label === typedValue
) === -1
}
createValue={ value }
// eslint-disable-next-line no-alert
onCreateNew={ () => alert( 'create new called' ) }
onInputChange={ ( a ) => setValue( a || '' ) }
onSelect={ ( selectedItems ) => {
if ( Array.isArray( selectedItems ) ) {
setSelected( [
...selected,
...selectedItems,
] );
}
} }
onRemove={ ( removedItems ) => {
const newValues = Array.isArray( removedItems )
? selected.filter(
( item ) =>
! removedItems.some(
( { value: removedValue } ) =>
item.value === removedValue
)
)
: selected.filter(
( item ) =>
item.value !== removedItems.value
);
setSelected( newValues );
} }
/>
</Modal>
) }
<SelectTreeMenuSlot />
</SlotFillProvider>
);
};
export default {
title: 'WooCommerce Admin/experimental/SelectTreeControl',
component: SelectTree,
};

View File

@ -0,0 +1,107 @@
import { render } from '@testing-library/react';
import React, { createElement } from '@wordpress/element';
import { SelectTree } from '../select-tree';
import { Item } from '../../experimental-tree-control';
const mockItems: Item[] = [
{
label: 'Item 1',
value: 'item-1',
},
{
label: 'Item 2',
value: 'item-2',
parent: 'item-1',
},
{
label: 'Item 3',
value: 'item-3',
},
];
const DEFAULT_PROPS = {
id: 'select-tree',
items: mockItems,
label: 'Select Tree',
placeholder: 'Type here',
};
describe( 'SelectTree', () => {
beforeEach( () => {
jest.clearAllMocks();
} );
it( 'should show the popover only when focused', () => {
const { queryByPlaceholderText, queryByText } = render(
<SelectTree { ...DEFAULT_PROPS } />
);
expect( queryByText( 'Item 1' ) ).not.toBeInTheDocument();
queryByPlaceholderText( 'Type here' )?.focus();
expect( queryByText( 'Item 1' ) ).toBeInTheDocument();
} );
it( 'should show create button when callback is true ', () => {
const { queryByText, queryByPlaceholderText } = render(
<SelectTree
{ ...DEFAULT_PROPS }
shouldShowCreateButton={ () => true }
/>
);
queryByPlaceholderText( 'Type here' )?.focus();
expect( queryByText( 'Create new' ) ).toBeInTheDocument();
} );
it( 'should not show create button when callback is false or no callback', () => {
const { queryByText, queryByPlaceholderText } = render(
<SelectTree { ...DEFAULT_PROPS } />
);
queryByPlaceholderText( 'Type here' )?.focus();
expect( queryByText( 'Create new' ) ).not.toBeInTheDocument();
} );
it( 'should show a root item when focused and child when expand button is clicked', () => {
const { queryByText, queryByLabelText, queryByPlaceholderText } =
render( <SelectTree { ...DEFAULT_PROPS } /> );
queryByPlaceholderText( 'Type here' )?.focus();
expect( queryByText( 'Item 1' ) ).toBeInTheDocument();
expect( queryByText( 'Item 2' ) ).not.toBeInTheDocument();
queryByLabelText( 'Expand' )?.click();
expect( queryByText( 'Item 2' ) ).toBeInTheDocument();
} );
it( 'should show selected items', () => {
const { queryAllByRole, queryByPlaceholderText } = render(
<SelectTree { ...DEFAULT_PROPS } selected={ [ mockItems[ 0 ] ] } />
);
queryByPlaceholderText( 'Type here' )?.focus();
expect( queryAllByRole( 'treeitem' )[ 0 ] ).toHaveAttribute(
'aria-selected',
'true'
);
} );
it( 'should show Create "<createValue>" button', () => {
const { queryByPlaceholderText, queryByText } = render(
<SelectTree
{ ...DEFAULT_PROPS }
createValue="new item"
shouldShowCreateButton={ () => true }
/>
);
queryByPlaceholderText( 'Type here' )?.focus();
expect( queryByText( 'Create "new item"' ) ).toBeInTheDocument();
} );
it( 'should call onCreateNew when Create "<createValue>" button is clicked', () => {
const mockFn = jest.fn();
const { queryByPlaceholderText, queryByText } = render(
<SelectTree
{ ...DEFAULT_PROPS }
createValue="new item"
shouldShowCreateButton={ () => true }
onCreateNew={ mockFn }
/>
);
queryByPlaceholderText( 'Type here' )?.focus();
queryByText( 'Create "new item"' )?.click();
expect( mockFn ).toBeCalledTimes( 1 );
} );
} );

View File

@ -17,7 +17,8 @@ export function useExpander( {
useEffect( () => {
if (
item.children?.length &&
typeof shouldItemBeExpanded === 'function'
typeof shouldItemBeExpanded === 'function' &&
! isExpanded
) {
setExpanded( shouldItemBeExpanded( item ) );
}

View File

@ -110,12 +110,14 @@ export function useKeyboard( {
onExpand,
onCollapse,
onToggleExpand,
onLastItemLoop,
}: {
item: LinkedTree;
isExpanded: boolean;
onExpand(): void;
onCollapse(): void;
onToggleExpand(): void;
onLastItemLoop?( event: React.KeyboardEvent< HTMLDivElement > ): void;
} ) {
function onKeyDown( event: React.KeyboardEvent< HTMLDivElement > ) {
if ( event.code === 'ArrowRight' ) {
@ -154,6 +156,9 @@ export function useKeyboard( {
event.code
);
element?.focus();
if ( event.code === 'ArrowDown' && ! element && onLastItemLoop ) {
onLastItemLoop( event );
}
}
if ( event.code === 'Home' ) {
@ -169,5 +174,5 @@ export function useKeyboard( {
}
}
return { onKeyDown };
return { onKeyDown, onLastItemLoop };
}

View File

@ -75,6 +75,7 @@ function hasSelectedSibblingChildren(
export function useSelection( {
item,
multiple,
shouldNotRecursivelySelect,
selected,
level,
index,
@ -89,6 +90,7 @@ export function useSelection( {
| 'index'
| 'onSelect'
| 'onRemove'
| 'shouldNotRecursivelySelect'
> ) {
const selectedItems = useMemo( () => {
if ( level === 1 && index === 0 ) {
@ -100,7 +102,11 @@ export function useSelection( {
const checkedStatus: CheckedStatus = useMemo( () => {
if ( item.data.value in selectedItems ) {
if ( multiple && isIndeterminate( selectedItems, item.children ) ) {
if (
multiple &&
! shouldNotRecursivelySelect &&
isIndeterminate( selectedItems, item.children )
) {
return 'indeterminate';
}
return 'checked';
@ -113,7 +119,7 @@ export function useSelection( {
if ( multiple ) {
value = [ item.data ];
if ( item.children.length ) {
if ( item.children.length && ! shouldNotRecursivelySelect ) {
value.push( ...getDeepChildren( item ) );
}
} else if ( item.children?.length ) {
@ -132,7 +138,7 @@ export function useSelection( {
function onSelectChildren( value: Item | Item[] ) {
if ( typeof onSelect !== 'function' ) return;
if ( multiple ) {
if ( multiple && ! shouldNotRecursivelySelect ) {
value = [ item.data, ...( value as Item[] ) ];
}
@ -142,7 +148,11 @@ export function useSelection( {
function onRemoveChildren( value: Item | Item[] ) {
if ( typeof onRemove !== 'function' ) return;
if ( multiple && item.children?.length ) {
if (
multiple &&
item.children?.length &&
! shouldNotRecursivelySelect
) {
const hasSelectedSibbling = hasSelectedSibblingChildren(
item.children,
value as Item[],

View File

@ -17,6 +17,7 @@ export function useTreeItem( {
item,
level,
multiple,
shouldNotRecursivelySelect,
selected,
index,
getLabel,
@ -24,6 +25,11 @@ export function useTreeItem( {
shouldItemBeHighlighted,
onSelect,
onRemove,
isExpanded,
onCreateNew,
shouldShowCreateButton,
onLastItemLoop,
onTreeBlur,
...props
}: TreeItemProps ) {
const nextLevel = level + 1;
@ -41,6 +47,7 @@ export function useTreeItem( {
index,
onSelect,
onRemove,
shouldNotRecursivelySelect,
} );
const highlighter = useHighlighter( {
@ -56,6 +63,7 @@ export function useTreeItem( {
const { onKeyDown } = useKeyboard( {
...expander,
onLastItemLoop,
item,
} );
@ -96,6 +104,7 @@ export function useTreeItem( {
getItemLabel: getLabel,
shouldItemBeExpanded,
shouldItemBeHighlighted,
shouldNotRecursivelySelect,
onSelect: selection.onSelectChildren,
onRemove: selection.onRemoveChildren,
},

View File

@ -8,7 +8,6 @@
import { TreeProps } from '../types';
export function useTree( {
ref,
items,
level = 1,
role = 'tree',
@ -19,6 +18,11 @@ export function useTree( {
shouldItemBeHighlighted,
onSelect,
onRemove,
shouldNotRecursivelySelect,
createValue,
onTreeBlur,
onCreateNew,
shouldShowCreateButton,
...props
}: TreeProps ) {
return {
@ -35,6 +39,7 @@ export function useTree( {
getLabel: getItemLabel,
shouldItemBeExpanded,
shouldItemBeHighlighted,
shouldNotRecursivelySelect,
onSelect,
onRemove,
},

View File

@ -12,4 +12,13 @@
border: 1px solid $gray-400;
border-radius: 2px;
}
&__button {
width: 100%;
&:hover,
&:focus-within {
outline: 1.5px solid var( --wp-admin-theme-color );
outline-offset: -1.5px;
background-color: $gray-100;
}
}
}

View File

@ -1,8 +1,12 @@
/**
* External dependencies
*/
import { Button, Icon } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import classNames from 'classnames';
import { createElement, forwardRef } from 'react';
import { createElement, forwardRef, Fragment, useRef } from 'react';
import { plus } from '@wordpress/icons';
import { useMergeRefs } from '@wordpress/compose';
/**
* Internal dependencies
@ -13,31 +17,93 @@ import { TreeProps } from './types';
export const Tree = forwardRef( function ForwardedTree(
props: TreeProps,
ref: React.ForwardedRef< HTMLOListElement >
forwardedRef: React.ForwardedRef< HTMLOListElement >
) {
const rootListRef = useRef< HTMLOListElement >( null );
const ref = useMergeRefs( [ rootListRef, forwardedRef ] );
const { level, items, treeProps, treeItemProps } = useTree( {
...props,
ref,
} );
if ( ! items.length ) return null;
const isCreateButtonVisible =
props.shouldShowCreateButton &&
props.shouldShowCreateButton( props.createValue );
return (
<ol
{ ...treeProps }
className={ classNames(
treeProps.className,
'experimental-woocommerce-tree',
`experimental-woocommerce-tree--level-${ level }`
<>
<ol
{ ...treeProps }
className={ classNames(
treeProps.className,
'experimental-woocommerce-tree',
`experimental-woocommerce-tree--level-${ level }`
) }
>
{ items.map( ( child, index ) => (
<TreeItem
{ ...treeItemProps }
isExpanded={ props.isExpanded }
key={ child.data.value }
item={ child }
index={ index }
// Button ref is not working, so need to use CSS directly
onLastItemLoop={ () => {
(
rootListRef.current
?.closest( 'ol[role="tree"]' )
?.parentElement?.querySelector(
'.experimental-woocommerce-tree__button'
) as HTMLButtonElement
)?.focus();
} }
/>
) ) }
</ol>
{ isCreateButtonVisible && (
<Button
className="experimental-woocommerce-tree__button"
onClick={ () => {
if ( props.onCreateNew ) {
props.onCreateNew();
}
if ( props.onTreeBlur ) {
props.onTreeBlur();
}
} }
// Component's event type definition is not working
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onKeyDown={ ( event: any ) => {
if (
event.key === 'ArrowUp' ||
event.key === 'ArrowDown'
) {
event.preventDefault();
if ( event.key === 'ArrowUp' ) {
const allHeadings =
event.nativeEvent.srcElement.previousSibling.querySelectorAll(
'.experimental-woocommerce-tree-item > .experimental-woocommerce-tree-item__heading'
);
allHeadings[ allHeadings.length - 1 ]
?.querySelector(
'.experimental-woocommerce-tree-item__label'
)
?.focus();
}
}
} }
>
<Icon icon={ plus } size={ 20 } />
{ props.createValue
? sprintf(
__( 'Create "%s"', 'woocommerce' ),
props.createValue
)
: __( 'Create new', 'woocommerce' ) }
</Button>
) }
>
{ items.map( ( child, index ) => (
<TreeItem
{ ...treeItemProps }
key={ child.data.value }
item={ child }
index={ index }
/>
) ) }
</ol>
</>
);
} );

View File

@ -14,7 +14,7 @@ export type CheckedStatus = 'checked' | 'unchecked' | 'indeterminate';
type BaseTreeProps = {
/**
* It contians one item if `multiple` value is false or
* It contains one item if `multiple` value is false or
* a list of items if it is true.
*/
selected?: Item | Item[];
@ -22,6 +22,24 @@ type BaseTreeProps = {
* Whether the tree items are single or multiple selected.
*/
multiple?: boolean;
/**
* In `multiple` mode, when this flag is also set, selecting children does
* not select their parents and selecting parents does not select their children.
*/
shouldNotRecursivelySelect?: boolean;
/**
* The value to be used for comparison to determine if 'create new' button should be shown.
*/
createValue?: string;
/**
* Called when the 'create new' button is clicked.
*/
onCreateNew?: () => void;
/**
* If passed, shows create button if return from callback is true
*/
shouldShowCreateButton?( value?: string ): boolean;
isExpanded?: boolean;
/**
* When `multiple` is true and a child item is selected, all its
* ancestors and its descendants are also selected. If it's false
@ -54,6 +72,10 @@ type BaseTreeProps = {
* @see {@link LinkedTree}
*/
shouldItemBeHighlighted?( item: LinkedTree ): boolean;
/**
* Called when the create button is clicked to help closing any related popover.
*/
onTreeBlur?(): void;
};
export type TreeProps = BaseTreeProps &
@ -108,8 +130,10 @@ export type TreeItemProps = BaseTreeProps &
level: number;
item: LinkedTree;
index: number;
isFocused?: boolean;
getLabel?( item: LinkedTree ): JSX.Element;
shouldItemBeExpanded?( item: LinkedTree ): boolean;
onLastItemLoop?( event: React.KeyboardEvent< HTMLDivElement > ): void;
};
export type TreeControlProps = Omit< TreeProps, 'items' | 'level' > & {

View File

@ -25,6 +25,16 @@ const ORDER_STATUSES = {
refunded: 'Refunded',
};
const CURRENCY = {
code: 'USD',
decimalSeparator: '.',
precision: 2,
priceFormat: '%1$s%2$s',
symbol: '$',
symbolPosition: 'left',
thousandSeparator: ',',
};
// Fetch store default date range and compose with date utility functions.
const defaultDateRange = 'period=month&compare=previous_year';
const storeGetDateParamsFromQuery = partialRight(
@ -58,14 +68,14 @@ const filters = [
];
const advancedFilters = {
title: 'Orders Match {{select /}} Filters',
title: 'Orders Match <select/> Filters',
filters: {
status: {
labels: {
add: 'Order Status',
remove: 'Remove order status filter',
rule: 'Select an order status filter match',
title: 'Order Status {{rule /}} {{filter /}}',
title: 'Order Status <rule/> <filter/>',
filter: 'Select an order status',
},
rules: [
@ -92,7 +102,7 @@ const advancedFilters = {
placeholder: 'Search products',
remove: 'Remove products filter',
rule: 'Select a product filter match',
title: 'Product {{rule /}} {{filter /}}',
title: 'Product <rule/> <filter/>',
filter: 'Select products',
},
rules: [
@ -116,7 +126,7 @@ const advancedFilters = {
add: 'Customer type',
remove: 'Remove customer filter',
rule: 'Select a customer filter match',
title: 'Customer is {{filter /}}',
title: 'Customer is <filter/>',
filter: 'Select a customer type',
},
input: {
@ -133,7 +143,7 @@ const advancedFilters = {
add: 'Item Quantity',
remove: 'Remove item quantity filter',
rule: 'Select an item quantity filter match',
title: 'Item Quantity is {{rule /}} {{filter /}}',
title: 'Item Quantity is <rule/> <filter/>',
},
rules: [
{
@ -158,7 +168,7 @@ const advancedFilters = {
add: 'Subtotal',
remove: 'Remove subtotal filter',
rule: 'Select a subtotal filter match',
title: 'Subtotal is {{rule /}} {{filter /}}',
title: 'Subtotal is <rule/> <filter/>',
},
rules: [
{
@ -225,6 +235,7 @@ export const Examples = () => (
query={ query }
filterTitle="Orders"
config={ advancedFilters }
currency={ CURRENCY }
/>
</Section>

View File

@ -95,7 +95,14 @@ export {
SlotContextType,
SlotContextHelpersType,
} from './slot-context';
export { TreeControl as __experimentalTreeControl } from './experimental-tree-control';
export {
TreeControl as __experimentalTreeControl,
Item as TreeItemType,
} from './experimental-tree-control';
export {
SelectTree as __experimentalSelectTreeControl,
SelectTreeMenuSlot as __experimentalSelectTreeMenuSlot,
} from './experimental-select-tree-control';
export { default as TreeSelectControl } from './tree-select-control';
// Exports below can be removed once the @woocommerce/product-editor package is released.

View File

@ -1,92 +0,0 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import classnames from 'classnames';
import { createElement, Component } from '@wordpress/element';
import StarIcon from 'gridicons/dist/star';
import PropTypes from 'prop-types';
/**
* Use `Rating` to display a set of stars, filled, empty or half-filled, that represents a
* rating in a scale between 0 and the prop `totalStars` (default 5).
*/
class Rating extends Component {
stars( icon ) {
const { size, totalStars } = this.props;
const starStyles = {
width: size + 'px',
height: size + 'px',
};
const stars = [];
for ( let i = 0; i < totalStars; i++ ) {
const Icon = icon || StarIcon;
stars.push( <Icon key={ 'star-' + i } style={ starStyles } /> );
}
return stars;
}
render() {
const { rating, totalStars, className, icon, outlineIcon } = this.props;
const classes = classnames( 'woocommerce-rating', className );
const perStar = 100 / totalStars;
const outlineStyles = {
width: Math.round( perStar * rating ) + '%',
};
const label = sprintf(
__( '%1$s out of %2$s stars.', 'woocommerce' ),
rating,
totalStars
);
return (
<div className={ classes } aria-label={ label }>
{ this.stars( icon ) }
<div
className="woocommerce-rating__star-outline"
style={ outlineStyles }
>
{ this.stars( outlineIcon || icon ) }
</div>
</div>
);
}
}
Rating.propTypes = {
/**
* Number of stars that should be filled. You can pass a partial number of stars like `2.5`.
*/
rating: PropTypes.number,
/**
* The total number of stars the rating is out of.
*/
totalStars: PropTypes.number,
/**
* The size in pixels the stars should be rendered at.
*/
size: PropTypes.number,
/**
* Additional CSS classes.
*/
className: PropTypes.string,
/**
* Icon used, defaults to StarIcon
*/
icon: PropTypes.elementType,
/**
* Outline icon used, the not selected rating. Defaults to props.icon or StarIcon
*/
outlineIcon: PropTypes.elementType,
};
Rating.defaultProps = {
rating: 0,
totalStars: 5,
size: 18,
};
export default Rating;

View File

@ -0,0 +1,74 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import classnames from 'classnames';
import { createElement } from '@wordpress/element';
import StarIcon from 'gridicons/dist/star';
type RatingProps = {
// Number of stars that should be filled. You can pass a partial number of stars like `2.5`.
rating?: number;
// The total number of stars the rating is out of.
totalStars?: number;
// The size in pixels the stars should be rendered at.
size?: number;
// Additional CSS classes.
className?: string;
// Icon used, defaults to StarIcon
icon?: React.ReactNode;
// Outline icon used, the not selected rating. Defaults to props.icon or StarIcon
outlineIcon?: React.ReactNode;
};
/**
* Use `Rating` to display a set of stars, filled, empty or half-filled, that represents a
* rating in a scale between 0 and the prop `totalStars` (default 5).
*/
const Rating = ( {
rating = 0,
totalStars = 5,
size = 18,
className,
icon,
outlineIcon,
}: RatingProps ) => {
const stars = ( _icon: React.ReactNode ) => {
const starStyles = {
width: size + 'px',
height: size + 'px',
};
const _stars = [];
for ( let i = 0; i < totalStars; i++ ) {
const Icon = _icon || StarIcon;
_stars.push( <Icon key={ 'star-' + i } style={ starStyles } /> );
}
return _stars;
};
const classes = classnames( 'woocommerce-rating', className );
const perStar = 100 / totalStars;
const outlineStyles = {
width: Math.round( perStar * rating ) + '%',
};
const label = sprintf(
__( '%1$s out of %2$s stars.', 'woocommerce' ),
rating,
totalStars
);
return (
<div className={ classes } aria-label={ label }>
{ stars( icon ) }
<div
className="woocommerce-rating__star-outline"
style={ outlineStyles }
>
{ stars( outlineIcon || icon ) }
</div>
</div>
);
};
export default Rating;

View File

@ -9,14 +9,18 @@ import { createElement } from '@wordpress/element';
*/
import Rating from './index';
type ProductRatingProps = {
product: {
average_rating?: number;
};
};
/**
* Display a set of stars representing the product's average rating.
*
* @param {Object} props
* @param {Object} props.product
* @return {Object} -
*/
const ProductRating = ( { product, ...props } ) => {
const ProductRating: React.VFC< ProductRatingProps > = ( {
product,
...props
} ) => {
const rating = ( product && product.average_rating ) || 0;
return <Rating rating={ rating } { ...props } />;
};

View File

@ -9,16 +9,20 @@ import { createElement } from '@wordpress/element';
*/
import Rating from './index';
type ReviewRatingProps = {
review: {
rating?: number;
};
};
/**
* Display a set of stars representing the review's rating.
*
* @param {Object} props
* @param {Object} props.review
* @return {Object} -
*/
const ReviewRating = ( { review, ...props } ) => {
const rating = ( review && review.rating ) || 0;
return <Rating rating={ rating } { ...props } />;
const ReviewRating: React.VFC< ReviewRatingProps > = ( {
review,
...props
} ) => {
return <Rating rating={ review.rating || 0 } { ...props } />;
};
ReviewRating.propTypes = {

View File

@ -1,15 +1,20 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import Rating from '../';
import Rating from '..';
export default {
title: 'WooCommerce Admin/components/Rating',
component: Rating,
args: {
rating: 4.5,
totalStars: Rating.defaultProps.totalStars,
size: Rating.defaultProps.size,
totalStars: 5,
size: 18,
},
};

View File

@ -9,7 +9,7 @@ import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import Rating from '../';
import Rating from '..';
import ProductRating from '../product';
import ReviewRating from '../review';

View File

@ -56,5 +56,6 @@
@import 'collapsible-content/style.scss';
@import 'form/style.scss';
@import 'experimental-tree-control/tree.scss';
@import 'experimental-select-tree-control/select-tree.scss';
@import 'product-section-layout/style.scss';
@import 'tree-select-control/index.scss';

View File

@ -148,9 +148,7 @@ const TableCard: React.VFC< TableCardProps > = ( {
) }
renderContent={ () => (
<Fragment>
{ /* @ts-expect-error: Ignoring the error until we migrate ellipsis-menu to TS*/ }
<MenuTitle>
{ /* @ts-expect-error: Allow string */ }
{ __( 'Columns:', 'woocommerce' ) }
</MenuTitle>
{ allHeaders.map(

View File

@ -28,13 +28,15 @@ function Tour() {
referenceElements: {
desktop: '.render-step-near-me',
},
meta: {
heading: 'Lorem ipsum dolor sit amet.',
descriptions: {
desktop: 'Lorem ipsum dolor sit amet.',
},
primaryButtonText: "Done"
},
meta: {
heading: 'Lorem ipsum dolor sit amet.',
descriptions: {
desktop: 'Lorem ipsum dolor sit amet.',
},
primaryButton: {
text: 'Done',
},
},
},
],
closeHandler: () => setShowTour( false ),
@ -57,8 +59,8 @@ function Tour() {
When a tour is rendered and focused, the following functionality exists:
- Close the tour on `ESC` key (in minimized view)
- Go to previous/next step on `left/right` arrow keys (in step view)
- Close the tour on `ESC` key (in minimized view)
- Go to previous/next step on `left/right` arrow keys (in step view)
## Configuration
@ -66,51 +68,52 @@ The main API for configuring a tour is the config object. See example usage and
`config.steps`: An array of objects that define the content we wish to render on the page. Each step defined by:
- `referenceElements` (optional): A set of `desktop` & `mobile` selectors to render the step near.
- `focusElement` (optional): A set of `desktop` & `mobile` & `iframe` selectors to automatically focus.
- `meta`: Arbitrary object that encloses the content we want to render for each step.
- `classNames` (optional): An array or CSV of CSS classes applied to a step.
- `referenceElements` (optional): A set of `desktop` & `mobile` selectors to render the step near.
- `focusElement` (optional): A set of `desktop` & `mobile` & `iframe` selectors to automatically focus.
- `meta`: Arbitrary object that encloses the content we want to render for each step.
- `classNames` (optional): An array or CSV of CSS classes applied to a step.
`config.closeHandler`: The callback responsible for closing the tour.
- `tourStep`: A React component that will be called to render each step. Receives the following properties:
- `tourStep`: A React component that will be called to render each step. Receives the following properties:
- `steps`: The steps defined for the tour.
- `currentStepIndex`
- `onDismiss`: Handler that dismissed/closes the tour.
- `onNext`: Handler that progresses the tour to the next step.
- `onPrevious`: Handler that takes the tour to the previous step.
- `onMinimize`: Handler that minimizes the tour (passes rendering to `tourMinimized`).
- `setInitialFocusedElement`: A dispatcher that assigns an element to be initially focused when a step renders (see examples).
- `onGoToStep`: Handler that progresses the tour to a given step index.
- `steps`: The steps defined for the tour.
- `currentStepIndex`
- `onDismiss`: Handler that dismissed/closes the tour.
- `onNext`: Handler that progresses the tour to the next step.
- `onPrevious`: Handler that takes the tour to the previous step.
- `onMinimize`: Handler that minimizes the tour (passes rendering to `tourMinimized`).
- `setInitialFocusedElement`: A dispatcher that assigns an element to be initially focused when a step renders (see examples).
- `onGoToStep`: Handler that progresses the tour to a given step index.
- `tourMinimized`: A React component that will be called to render a minimized view for the tour. Receives the following properties:
- `steps`: The steps defined for the tour.
- `currentStepIndex`
- `onDismiss`: Handler that dismissed/closes the tour.
- `onMaximize`: Handler that expands the tour (passes rendering to `tourStep`).
- `tourMinimized`: A React component that will be called to render a minimized view for the tour. Receives the following properties:
- `steps`: The steps defined for the tour.
- `currentStepIndex`
- `onDismiss`: Handler that dismissed/closes the tour.
- `onMaximize`: Handler that expands the tour (passes rendering to `tourStep`).
`config.options` (optional):
- `classNames` (optional): An array or CSV of CSS classes to enclose the main tour frame with.
- `classNames` (optional): An array or CSV of CSS classes to enclose the main tour frame with.
- `effects`: An object to enable/disable/combine various tour effects:
- `effects`: An object to enable/disable/combine various tour effects:
- `spotlight`: Adds a semi-transparent overlay and highlights the reference element when provided with a transparent box over it. Expects an object with optional styles to override the default highlight/spotlight behavior when provided (default: spotlight wraps the entire reference element).
- `interactivity`: An object that configures whether the user is allowed to interact with the referenced element during the tour
- `styles`: CSS properties that configures the styles applied to the spotlight overlay
- `arrowIndicator`: Adds an arrow tip pointing at the reference element when provided.
- `overlay`: Includes the semi-transparent overlay for all the steps (also blocks interactions with the rest of the page)
- `autoScroll`: The page scrolls up and down automatically such that the step target element is visible to the user.
- `spotlight`: Adds a semi-transparent overlay and highlights the reference element when provided with a transparent box over it. Expects an object with optional styles to override the default highlight/spotlight behavior when provided (default: spotlight wraps the entire reference element).
- `interactivity`: An object that configures whether the user is allowed to interact with the referenced element during the tour
- `styles`: CSS properties that configures the styles applied to the spotlight overlay
- `arrowIndicator`: Adds an arrow tip pointing at the reference element when provided.
- `overlay`: Includes the semi-transparent overlay for all the steps (also blocks interactions with the rest of the page)
- `autoScroll`: The page scrolls up and down automatically such that the step target element is visible to the user.
- `callbacks`: An object of callbacks to handle side effects from various interactions (see [types.ts](./src/types.ts)).
- `callbacks`: An object of callbacks to handle side effects from various interactions (see [types.ts](./src/types.ts)).
- `popperModifiers`: The tour uses Popper to position steps near reference elements (and for other effects). An implementation can pass its own modifiers to tailor the functionality further e.g. more offset or padding from the reference element.
- `tourRating` (optional - only in WPCOM Tour Kit variant):
- `enabled`: Whether to show rating in last step.
- `useTourRating`: (optional) A hook to provide the rating from an external source/state (see [types.ts](./src/types.ts)).
- `onTourRate`: (optional) A callback to fire off when a rating is submitted.
- `popperModifiers`: The tour uses Popper to position steps near reference elements (and for other effects). An implementation can pass its own modifiers to tailor the functionality further e.g. more offset or padding from the reference element.
- `tourRating` (optional - only in WPCOM Tour Kit variant):
- `portalElementId`: A string that lets you customize under which DOM element the Tour will be appended.
- `enabled`: Whether to show rating in last step.
- `useTourRating`: (optional) A hook to provide the rating from an external source/state (see [types.ts](./src/types.ts)).
- `onTourRate`: (optional) A callback to fire off when a rating is submitted.
- `portalElementId`: A string that lets you customize under which DOM element the Tour will be appended.
`placement` (Optional) : Describes the preferred placement of the popper. Possible values are left-start, left, left-end, top-start, top, top-end, right-start, right, right-end, bottom-start, bottom, and bottom-end.

View File

@ -64,6 +64,7 @@ import { ARROW_DOWN, ARROW_UP, ENTER, ESCAPE, ROOT_VALUE } from './constants';
* @param {string} [props.className] The class name for this component
* @param {boolean} [props.disabled] Disables the component
* @param {boolean} [props.includeParent] Includes parent with selection.
* @param {boolean} [props.individuallySelectParent] Considers parent as a single item (default: false).
* @param {boolean} [props.alwaysShowPlaceholder] Will always show placeholder (default: false)
* @param {Option[]} [props.options] Options to show in the component
* @param {string[]} [props.value] Selected values
@ -71,6 +72,8 @@ import { ARROW_DOWN, ARROW_UP, ENTER, ESCAPE, ROOT_VALUE } from './constants';
* @param {Function} [props.onChange] Callback when the selector changes
* @param {(visible: boolean) => void} [props.onDropdownVisibilityChange] Callback when the visibility of the dropdown options is changed.
* @param {Function} [props.onInputChange] Callback when the selector changes
* @param {number} [props.minFilterQueryLength] Minimum input length to filter results by.
* @param {boolean} [props.clearOnSelect] Clear input on select (default: true).
* @return {JSX.Element} The component
*/
const TreeSelectControl = ( {
@ -88,7 +91,10 @@ const TreeSelectControl = ( {
onDropdownVisibilityChange = noop,
onInputChange = noop,
includeParent = false,
individuallySelectParent = false,
alwaysShowPlaceholder = false,
minFilterQueryLength = 3,
clearOnSelect = true,
} ) => {
let instanceId = useInstanceId( TreeSelectControl );
instanceId = id ?? instanceId;
@ -126,7 +132,8 @@ const TreeSelectControl = ( {
const filterQuery = inputControlValue.trim().toLowerCase();
// we only trigger the filter when there are more than 3 characters in the input.
const filter = filterQuery.length >= 3 ? filterQuery : '';
const filter =
filterQuery.length >= minFilterQueryLength ? filterQuery : '';
/**
* Optimizes the performance for getting the tags info
@ -419,9 +426,11 @@ const TreeSelectControl = ( {
*/
const handleParentChange = ( checked, option ) => {
let newValue;
const changedValues = option.leaves
.filter( ( opt ) => opt.checked !== checked )
.map( ( opt ) => opt.value );
const changedValues = individuallySelectParent
? []
: option.leaves
.filter( ( opt ) => opt.checked !== checked )
.map( ( opt ) => opt.value );
if ( includeParent && option.value !== ROOT_VALUE ) {
changedValues.push( option.value );
}
@ -452,10 +461,12 @@ const TreeSelectControl = ( {
handleSingleChange( checked, option, parent );
}
onInputChange( '' );
setInputControlValue( '' );
if ( ! nodesExpanded.includes( option.parent ) ) {
controlRef.current.focus();
if ( clearOnSelect ) {
onInputChange( '' );
setInputControlValue( '' );
if ( ! nodesExpanded.includes( option.parent ) ) {
controlRef.current.focus();
}
}
};
@ -475,6 +486,7 @@ const TreeSelectControl = ( {
* @param {Event} e Event returned by the On Change function in the Input control
*/
const handleOnInputChange = ( e ) => {
setTreeVisible( true );
onInputChange( e.target.value );
setInputControlValue( e.target.value );
};

View File

@ -6,7 +6,6 @@
"declaration": true,
"declarationMap": true,
"declarationDir": "./build-types",
"composite": true,
"typeRoots": [
"./typings",
"./node_modules/@types"

View File

@ -0,0 +1,7 @@
declare module 'gridicons/dist/*' {
const value: React.ReactNode< {
size?: 12 | 18 | 24 | 36 | 48 | 54 | 72;
onClick?: ( event: MouseEvent | KeyboardEvent ) => void;
} >;
export default value;
}

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