Merge branch 'trunk' into add/sync_of_deleted_orders
This commit is contained in:
commit
7ce0828c98
|
@ -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
|
|
@ -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).
|
||||
|
|
|
@ -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 -->
|
|
@ -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 }}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' )
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }}"
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 );
|
||||
}
|
|
@ -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"'
|
||||
|
30
README.md
30
README.md
|
@ -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.
|
||||
|
|
|
@ -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' );
|
134
changelog.txt
134
changelog.txt
|
@ -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**
|
||||
|
|
12
package.json
12
package.json
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Update pnpm to version 8.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Remove obw theme step tests
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Adding LayoutContext component and hook.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Update webpack config to use @woocommerce/internal-style-build's parser config
|
|
@ -0,0 +1 @@
|
|||
export * from './layout-context';
|
|
@ -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 ] );
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './LayoutContext';
|
|
@ -1 +1,2 @@
|
|||
export * from './plugins';
|
||||
export * from './components';
|
||||
|
|
|
@ -12,6 +12,7 @@ module.exports = {
|
|||
path: __dirname,
|
||||
},
|
||||
module: {
|
||||
parser: webpackConfig.parser,
|
||||
rules: webpackConfig.rules,
|
||||
},
|
||||
plugins: webpackConfig.plugins,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Update pnpm to version 8.
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Fix issue where width of select control dropdown was not correctly calculated when rendering was delayed.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: tweak
|
||||
|
||||
Improve a11y support to collapsible content component
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Create SelectTree component that uses TreeControl
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Migrate ellipsis-menu component to TS
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Migrate Rating component to TS
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Update pnpm to version 8.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Update TourKit README to correct primaryButton example and formatting.
|
|
@ -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.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add minFilterQueryLength, individuallySelectParent, and clearOnSelect props.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Apply wccom experimental select control changes
|
|
@ -0,0 +1,4 @@
|
|||
Significance: major
|
||||
Type: update
|
||||
|
||||
Updated AdvancedFilter to use createInterpolateElement instead of interpolateComponents.
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
Comment: Add unit tests
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Update select tree control dropdown menu for custom slot fill support for display within Modals
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Update webpack config to use @woocommerce/internal-style-build's parser config
|
|
@ -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"
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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'
|
||||
) }
|
||||
/>
|
||||
),
|
||||
} );
|
||||
}
|
||||
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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( {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 > = (
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './select-tree';
|
||||
export * from './select-tree-menu';
|
|
@ -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
|
||||
);
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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 );
|
||||
} );
|
||||
} );
|
|
@ -17,7 +17,8 @@ export function useExpander( {
|
|||
useEffect( () => {
|
||||
if (
|
||||
item.children?.length &&
|
||||
typeof shouldItemBeExpanded === 'function'
|
||||
typeof shouldItemBeExpanded === 'function' &&
|
||||
! isExpanded
|
||||
) {
|
||||
setExpanded( shouldItemBeExpanded( item ) );
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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[],
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
} );
|
||||
|
|
|
@ -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' > & {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 } />;
|
||||
};
|
|
@ -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 = {
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 );
|
||||
};
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"declarationDir": "./build-types",
|
||||
"composite": true,
|
||||
"typeRoots": [
|
||||
"./typings",
|
||||
"./node_modules/@types"
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue