Merge branch 'trunk' into pr/35107

This commit is contained in:
Vedanshu Jain 2022-11-02 20:13:59 +05:30
commit ed03beb173
376 changed files with 46233 additions and 3515 deletions

View File

@ -14,18 +14,27 @@
Closes # . Closes # .
<!-- The next section is mandatory. If your PR doesn't require testing, please indicate that you are purposefully omitting instructions. -->
- [ ] This PR is a very minor change/addition and does not require testing instructions (if checked you can ignore/remove the next section).
<!-- Begin testing instructions -->
### How to test the changes in this Pull Request: ### How to test the changes in this Pull Request:
<!-- Otherwise, please include detailed instructions on how these changes can be tested (including pre-conditions, configuration, steps to take and expected results). It may help to write your instructions using pseudocode -- as if you're telling a computer how to execute the test. -->
1. 1.
2. 2.
3. 3.
<!-- End testing instructions -->
### Other information: ### Other information:
- [ ] Have you added an explanation of what your changes do and why you'd like us to include them? - [ ] 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 written new tests for your changes, as applicable?
- [ ] Have you successfully run tests with your changes locally? - [ ] Have you created a changelog file for each project being changed, ie `pnpm --filter=<project> changelog add`?
- [ ] Have you created a changelog file for each project being changed, ie `pnpm --filter=<project> run changelog add`?
<!-- Mark completed items with an [x] --> <!-- Mark completed items with an [x] -->

View File

@ -35,12 +35,14 @@ runs:
with: with:
node-version-file: .nvmrc node-version-file: .nvmrc
cache: pnpm cache: pnpm
registry-url: 'https://registry.npmjs.org'
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@e04e1d97f0c0481c6e1ba40f8a538454fe5d7709 uses: shivammathur/setup-php@e04e1d97f0c0481c6e1ba40f8a538454fe5d7709
with: with:
php-version: ${{ inputs.php-version }} php-version: ${{ inputs.php-version }}
coverage: none coverage: none
tools: phpcs, sirbrillig/phpcs-changed
- name: Cache Composer Dependencies - name: Cache Composer Dependencies
uses: actions/cache@fd5de65bc895cf536527842281bea11763fefd77 uses: actions/cache@fd5de65bc895cf536527842281bea11763fefd77

View File

@ -1,3 +1,85 @@
# See https://github.com/shufo/auto-assign-reviewer-by-files/blob/main/README.md for configuration format
".github/*": ".github/*":
- team: atlas - team: atlas
"packages/js/api/**/*":
- team: solaris
"packages/js/e2e-utils/**/*":
- team: solaris
"packages/js/e2e-environment/**/*":
- team: solaris
"packages/js/api-core-tests/**/*":
- team: solaris
"packages/js/e2e-core-tests/**/*":
- team: solaris
"packages/js/admin-e2e-tests/**/*":
- team: solaris
"packages/js/components/**/*":
- team: mothra
- team: ghidorah
"packages/js/csv-export/**/*":
- team: mothra
"packages/js/currency/**/*":
- team: mothra
"packages/js/customer-effort-score/**/*":
- team: mothra
"packages/js/data/**/*":
- team: mothra
- team: ghidorah
"packages/js/date/**/*":
- team: mothra
"packages/js/dependency-extraction-webpack-plugin/**/*":
- team: mothra
"packages/js/eslint-plugin/**/*":
- team: mothra
"packages/js/experimental/**/*":
- team: mothra
"packages/js/explat/**/*":
- team: mothra
- team: ghidorah
"packages/js/navigation/**/*":
- team: mothra
"packages/js/number/**/*":
- team: mothra
"packages/js/onboarding/**/*":
- team: ghidorah
"packages/js/tracks/**/*":
- team: mothra
"plugins/woocommerce/**/*":
- team: proton
"plugins/woocommerce/src/Admin/**/*":
- team: mothra
- team: ghidorah
"plugins/woocommerce/src/Internal/Admin/**/*":
- team: mothra
- team: ghidorah
"plugins/woocommerce-admin/**/*":
- team: mothra
- team: ghidorah
"plugins/woocommerce-beta-tester/**/*":
- team: atlas

View File

@ -27,3 +27,19 @@ jobs:
asset_path: plugins/woocommerce/woocommerce.zip asset_path: plugins/woocommerce/woocommerce.zip
asset_name: woocommerce.zip asset_name: woocommerce.zip
asset_content_type: application/zip asset_content_type: application/zip
update-code-reference:
if: github.event.release.prerelease == false && github.event.release.draft == false && github.repository_owner == 'woocommerce'
name: Update Code Reference
needs: build
runs-on: ubuntu-20.04
steps:
- name: Invoke Code Reference build and deploy workflow
uses: aurelien-baudet/workflow-dispatch@v2
with:
workflow: GitHub Pages deploy
repo: woocommerce/code-reference
token: ${{ secrets.CUSTOM_GH_TOKEN }}
ref: refs/heads/trunk
inputs: '{ "version": "${{ github.event.release.tag_name }}" }'

View File

@ -35,4 +35,4 @@ jobs:
uses: shufo/auto-assign-reviewer-by-files@f5f3db9ef06bd72ab6978996988c6462cbdaabf6 uses: shufo/auto-assign-reviewer-by-files@f5f3db9ef06bd72ab6978996988c6462cbdaabf6
with: with:
config: ".github/project-community-pr-assigner.yml" config: ".github/project-community-pr-assigner.yml"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.PR_ASSIGN_TOKEN }}

View File

@ -1,17 +1,20 @@
name: Run daily tests in an environment with COT enabled name: Run daily tests in an environment with COT enabled
on: on:
schedule: schedule:
- cron: "30 2 * * *" - cron: '30 2 * * *'
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
cot-e2e-tests-run: cot-e2e-tests-run:
name: Runs E2E tests with COT enabled. name: Runs E2E tests with COT enabled.
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
env:
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -20,6 +23,8 @@ jobs:
- name: Load docker images and start containers with COT enabled. - name: Load docker images and start containers with COT enabled.
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
env:
ENABLE_HPOS: 1
run: pnpm env:test:cot --filter=woocommerce run: pnpm env:test:cot --filter=woocommerce
- name: Download and install Chromium browser. - name: Download and install Chromium browser.
@ -30,7 +35,7 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
id: run_playwright_e2e_tests id: run_playwright_e2e_tests
env: env:
USE_WP_ENV: 1 USE_WP_ENV: 1
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js
@ -43,7 +48,7 @@ jobs:
steps.run_playwright_e2e_tests.conclusion != 'skipped' steps.run_playwright_e2e_tests.conclusion != 'skipped'
) )
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
run: pnpm exec allure generate --clean e2e/allure-results --output e2e/allure-report run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
- name: Archive Playwright E2E test report - name: Archive Playwright E2E test report
if: | if: |
@ -53,8 +58,8 @@ jobs:
with: with:
name: e2e-test-report---pr-${{ github.event.number }} name: e2e-test-report---pr-${{ github.event.number }}
path: | path: |
plugins/woocommerce/e2e/allure-results ${{ env.ALLURE_RESULTS_DIR }}
plugins/woocommerce/e2e/allure-report ${{ env.ALLURE_REPORT_DIR }}
if-no-files-found: ignore if-no-files-found: ignore
retention-days: 5 retention-days: 5
@ -62,7 +67,8 @@ jobs:
name: Runs API tests with COT enabled. name: Runs API tests with COT enabled.
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
env: env:
API_TEST_REPORT_DIR: ${{ github.workspace }}/api-test-report ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -71,6 +77,8 @@ jobs:
- name: Load docker images and start containers with COT enabled. - name: Load docker images and start containers with COT enabled.
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
env:
ENABLE_HPOS: 1
run: pnpm env:test:cot --filter=woocommerce run: pnpm env:test:cot --filter=woocommerce
- name: Run Playwright API tests. - name: Run Playwright API tests.
@ -81,6 +89,7 @@ jobs:
USER_KEY: admin USER_KEY: admin
USER_SECRET: password USER_SECRET: password
run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js
- name: Generate Playwright API Test report. - name: Generate Playwright API Test report.
id: generate_api_report id: generate_api_report
if: | if: |
@ -90,7 +99,8 @@ jobs:
steps.run_playwright_api_tests.conclusion != 'skipped' steps.run_playwright_api_tests.conclusion != 'skipped'
) )
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
run: pnpm exec allure generate --clean api-test-report/allure-results --output api-test-report/allure-report run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
- name: Archive Playwright API test report - name: Archive Playwright API test report
if: | if: |
always() && always() &&
@ -99,8 +109,8 @@ jobs:
with: with:
name: api-test-report---pr-${{ github.event.number }} name: api-test-report---pr-${{ github.event.number }}
path: | path: |
plugins/woocommerce/api-test-report/allure-results ${{ env.ALLURE_RESULTS_DIR }}
plugins/woocommerce/api-test-report/allure-report ${{ env.ALLURE_REPORT_DIR }}
if-no-files-found: ignore if-no-files-found: ignore
retention-days: 5 retention-days: 5

View File

@ -13,6 +13,9 @@ jobs:
name: Runs E2E tests with COT enabled. name: Runs E2E tests with COT enabled.
if: "${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'focus: custom order tables' }}" if: "${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'focus: custom order tables' }}"
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
env:
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -21,6 +24,8 @@ jobs:
- name: Load docker images and start containers with COT enabled. - name: Load docker images and start containers with COT enabled.
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
env:
ENABLE_HPOS: 1
run: pnpm env:test:cot --filter=woocommerce run: pnpm env:test:cot --filter=woocommerce
- name: Download and install Chromium browser. - name: Download and install Chromium browser.
@ -31,7 +36,7 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
id: run_playwright_e2e_tests id: run_playwright_e2e_tests
env: env:
USE_WP_ENV: 1 USE_WP_ENV: 1
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js
@ -44,7 +49,7 @@ jobs:
steps.run_playwright_e2e_tests.conclusion != 'skipped' steps.run_playwright_e2e_tests.conclusion != 'skipped'
) )
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
run: pnpm exec allure generate --clean e2e/allure-results --output e2e/allure-report run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
- name: Archive Playwright E2E test report - name: Archive Playwright E2E test report
if: | if: |
@ -54,8 +59,8 @@ jobs:
with: with:
name: e2e-test-report---pr-${{ github.event.number }} name: e2e-test-report---pr-${{ github.event.number }}
path: | path: |
plugins/woocommerce/e2e/allure-results ${{ env.ALLURE_RESULTS_DIR }}
plugins/woocommerce/e2e/allure-report ${{ env.ALLURE_REPORT_DIR }}
if-no-files-found: ignore if-no-files-found: ignore
retention-days: 5 retention-days: 5
@ -64,7 +69,8 @@ jobs:
if: "${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'focus: custom order tables' }}" if: "${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'focus: custom order tables' }}"
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
env: env:
API_TEST_REPORT_DIR: ${{ github.workspace }}/api-test-report ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -73,6 +79,8 @@ jobs:
- name: Load docker images and start containers with COT enabled. - name: Load docker images and start containers with COT enabled.
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
env:
ENABLE_HPOS: 1
run: pnpm env:test:cot --filter=woocommerce run: pnpm env:test:cot --filter=woocommerce
- name: Run Playwright API tests. - name: Run Playwright API tests.
@ -93,7 +101,7 @@ jobs:
steps.run_playwright_api_tests.conclusion != 'skipped' steps.run_playwright_api_tests.conclusion != 'skipped'
) )
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
run: pnpm exec allure generate --clean api-test-report/allure-results --output api-test-report/allure-report run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
- name: Archive Playwright API test report - name: Archive Playwright API test report
if: | if: |
@ -103,8 +111,8 @@ jobs:
with: with:
name: api-test-report---pr-${{ github.event.number }} name: api-test-report---pr-${{ github.event.number }}
path: | path: |
plugins/woocommerce/api-test-report/allure-results ${{ env.ALLURE_RESULTS_DIR }}
plugins/woocommerce/api-test-report/allure-report ${{ env.ALLURE_REPORT_DIR }}
if-no-files-found: ignore if-no-files-found: ignore
retention-days: 5 retention-days: 5

View File

@ -10,52 +10,112 @@ env:
GIT_AUTHOR_EMAIL: 'no-reply@woocommerce.com' GIT_AUTHOR_EMAIL: 'no-reply@woocommerce.com'
jobs: jobs:
update-changelog-in-trunk: changelog-version-update:
name: Update changelog in trunk name: Update changelog and version
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Get tag name
id: tag
uses: actions/github-script@v6
with:
script: |
const tag = ${{ toJSON( github.event.release.tag_name ) }}
console.log( `::set-output name=tag::release/${ tag.substring( 0, 3 ) }` )
- name: Git fetch trunk branch - name: Git fetch trunk branch
run: git fetch origin trunk run: git fetch origin trunk
- name: Copy changelog.txt to vm root - name: Copy readme.txt to vm root
run: cp changelog.txt ../../changelog.txt run: cp ./plugins/woocommerce/readme.txt ../../readme.txt
- name: Switch to trunk branch - name: Switch to trunk branch
run: git checkout trunk run: git checkout trunk
- name: Create a new branch based on trunk - name: Create a new branch based on trunk
run: git checkout -b update/changelog-from-release-${{ github.event.release.tag_name }} run: git checkout -b prep/post-release-tasks-${{ github.event.release.tag_name }}
- name: Copy saved changelog.txt to monorepo - name: Check if we need to continue processing
run: cp ../../changelog.txt ./changelog.txt uses: actions/github-script@v6
id: check
with:
script: |
const fs = require( 'node:fs' );
const version = ${{ toJSON( github.event.release.tag_name ) }}
fs.readFile( './plugins/woocommerce/readme.txt', 'utf-8', function( err, data ) {
if ( err ) {
console.error( err );
}
const regex = /Stable\stag:\s(\d+\.\d+\.\d+)/;
const stableVersion = data.match( regex )[1];
// If the release version is less than stable version we can bail.
if ( version.localeCompare( stableVersion, undefined, { numeric: true, sensitivity: 'base' } ) == -1 ) {
console.log( 'Release version is less than stable version. No automated action taken. A manual process is required.' );
console.log( `::set-output name=continue::false` )
return;
} else {
console.log( `::set-output name=continue::true` )
}
} )
- name: Update changelog.txt entries
uses: actions/github-script@v6
id: update-entries
if: steps.check.outputs.continue == 'true'
with:
script: |
const fs = require( 'node:fs' );
const version = ${{ toJSON( github.event.release.tag_name ) }}
// Read the saved readme.txt file from earlier.
fs.readFile( '../../readme.txt', 'utf-8', function( err, readme ) {
if ( err ) {
console.log( `::set-output name=continue::false` )
console.error( err );
}
const regex = /(== Changelog ==[\s\S]+)\s{2}\[See changelog for all versions\]\(https:\/\/raw\.githubusercontent\.com\/woocommerce\/woocommerce\/trunk\/changelog\.txt\)\./;
const entries = readme.match( regex )[1];
fs.readFile( './changelog.txt', 'utf-8', function( err, changelog ) {
if ( err ) {
console.log( `::set-output name=continue::false` )
console.error( err );
}
const regex = /== Changelog ==/;
const updatedChangelog = changelog.replace( regex, entries );
fs.writeFile( './changelog.txt', updatedChangelog, err => {
if ( err ) {
console.log( `::set-output name=continue::false` )
console.error( 'Unable to update changelog entries in changelog.txt' );
}
console.log( `::set-output name=continue::true` )
} )
} )
} )
- name: Commit changes - name: Commit changes
run: git commit -am "Update changelog.txt from release ${{ github.event.release.tag_name }}" if: steps.update-entries.outputs.continue == 'true'
run: git commit -am "Prep trunk post release ${{ github.event.release.tag_name }}"
- name: Push branch up - name: Push branch up
run: git push origin update/changelog-from-release-${{ github.event.release.tag_name }} if: steps.update-entries.outputs.continue == 'true'
run: git push origin prep/post-release-tasks-${{ github.event.release.tag_name }}
- name: Create the PR - name: Create the PR
if: steps.update-entries.outputs.continue == 'true'
uses: actions/github-script@v6 uses: actions/github-script@v6
with: with:
script: | script: |
const body = "This PR updates the changelog.txt based on the latest release: ${{ github.event.release.tag_name }}" const body = "This PR updates the changelog.txt entries based on the latest release: ${{ github.event.release.tag_name }}"
const pr = await github.rest.pulls.create({ const pr = await github.rest.pulls.create({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
title: "Update changelog.txt from release ${{ github.event.release.tag_name }}", title: "Update changelog.txt from release ${{ github.event.release.tag_name }}",
head: "update/changelog-from-release-${{ github.event.release.tag_name }}", head: "prep/post-release-tasks-${{ github.event.release.tag_name }}",
base: "trunk", base: "trunk",
body: body body: body
}) })

View File

@ -11,6 +11,9 @@ jobs:
e2e-tests-run: e2e-tests-run:
name: Runs E2E tests. name: Runs E2E tests.
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
env:
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report
outputs: outputs:
E2E_GRAND_TOTAL: ${{ steps.count_e2e_total.outputs.E2E_GRAND_TOTAL }} E2E_GRAND_TOTAL: ${{ steps.count_e2e_total.outputs.E2E_GRAND_TOTAL }}
steps: steps:
@ -21,6 +24,8 @@ jobs:
- name: Load docker images and start containers. - name: Load docker images and start containers.
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
env:
ENABLE_HPOS: 0
run: pnpm env:test --filter=woocommerce run: pnpm env:test --filter=woocommerce
- name: Download and install Chromium browser. - name: Download and install Chromium browser.
@ -40,9 +45,9 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
id: run_playwright_e2e_tests id: run_playwright_e2e_tests
env: env:
USE_WP_ENV: 1 USE_WP_ENV: 1
E2E_MAX_FAILURES: 15 E2E_MAX_FAILURES: 15
FORCE_COLOR: 1 FORCE_COLOR: 1
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js
@ -55,7 +60,7 @@ jobs:
steps.run_playwright_e2e_tests.conclusion != 'skipped' steps.run_playwright_e2e_tests.conclusion != 'skipped'
) )
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
run: pnpm exec allure generate --clean e2e/allure-results --output e2e/allure-report run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
- name: Archive Playwright E2E test report - name: Archive Playwright E2E test report
if: | if: |
@ -65,8 +70,8 @@ jobs:
with: with:
name: e2e-test-report---pr-${{ github.event.number }} name: e2e-test-report---pr-${{ github.event.number }}
path: | path: |
plugins/woocommerce/e2e/allure-results ${{ env.ALLURE_RESULTS_DIR }}
plugins/woocommerce/e2e/allure-report ${{ env.ALLURE_REPORT_DIR }}
if-no-files-found: ignore if-no-files-found: ignore
retention-days: 5 retention-days: 5
@ -74,7 +79,8 @@ jobs:
name: Runs API tests. name: Runs API tests.
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
env: env:
API_TEST_REPORT_DIR: ${{ github.workspace }}/api-test-report ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -83,6 +89,8 @@ jobs:
- name: Load docker images and start containers. - name: Load docker images and start containers.
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
env:
ENABLE_HPOS: 0
run: pnpm env:test --filter=woocommerce run: pnpm env:test --filter=woocommerce
- name: Run Playwright API tests. - name: Run Playwright API tests.
@ -103,7 +111,7 @@ jobs:
steps.run_playwright_api_tests.conclusion != 'skipped' steps.run_playwright_api_tests.conclusion != 'skipped'
) )
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
run: pnpm exec allure generate --clean api-test-report/allure-results --output api-test-report/allure-report run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
- name: Archive Playwright API test report - name: Archive Playwright API test report
if: | if: |
always() && always() &&
@ -112,8 +120,8 @@ jobs:
with: with:
name: api-test-report---pr-${{ github.event.number }} name: api-test-report---pr-${{ github.event.number }}
path: | path: |
plugins/woocommerce/api-test-report/allure-results ${{ env.ALLURE_RESULTS_DIR }}
plugins/woocommerce/api-test-report/allure-report ${{ env.ALLURE_REPORT_DIR }}
if-no-files-found: ignore if-no-files-found: ignore
retention-days: 5 retention-days: 5
@ -128,6 +136,8 @@ jobs:
- name: Load docker images and start containers. - name: Load docker images and start containers.
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
env:
ENABLE_HPOS: 0
run: | run: |
pnpm env:dev --filter=woocommerce pnpm env:dev --filter=woocommerce
pnpm env:performance-init --filter=woocommerce pnpm env:performance-init --filter=woocommerce

View File

@ -1,35 +1,46 @@
name: Run code sniff on PR name: Run code sniff on PR
on: on: pull_request
pull_request
defaults: defaults:
run: run:
shell: bash shell: bash
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
PHPCS: ./plugins/woocommerce/vendor/bin/phpcs # Run WooCommerce phpcs setup in phpcs-changed instead of default
jobs: jobs:
test: test:
name: Code sniff (PHP 7.4, WP Latest) name: Code sniff (PHP 7.4, WP Latest)
timeout-minutes: 15 timeout-minutes: 15
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup WooCommerce Monorepo - name: Get Changed Files
uses: ./.github/actions/setup-woocommerce-monorepo id: changed-files
with: uses: tj-actions/changed-files@v32
build: false with:
files: |
**/*.php
- name: Tool versions - name: Setup WooCommerce Monorepo
run: | if: steps.changed-files.outputs.any_changed == 'true'
php --version uses: ./.github/actions/setup-woocommerce-monorepo
composer --version with:
build: false
- name: Run code sniffer - name: Tool versions
uses: thenabeel/action-phpcs@v8 if: steps.changed-files.outputs.any_changed == 'true'
with: run: |
files: "**.php" php --version
phpcs_path: plugins/woocommerce/vendor/bin/phpcs composer --version
standard: phpcs.xml phpcs-changed --version
- name: Run PHPCS
if: steps.changed-files.outputs.any_changed == 'true'
run: |
HEAD_REF=$(git rev-parse HEAD)
git checkout $HEAD_REF
phpcs-changed --git --git-base ${{ github.base_ref }} ${{ steps.changed-files.outputs.all_changed_files }}

View File

@ -18,8 +18,9 @@ jobs:
id: run id: run
working-directory: tools/code-analyzer working-directory: tools/code-analyzer
run: | run: |
version=$(pnpm run analyzer major-minor "${{ github.head_ref || github.ref_name }}" "plugins/woocommerce/woocommerce.php" | tail -n 1) HEAD_REF=$(git rev-parse HEAD)
pnpm run analyzer "$GITHUB_HEAD_REF" $version -o "github" version=$(pnpm run analyzer major-minor "$HEAD_REF" "plugins/woocommerce/woocommerce.php" | tail -n 1)
pnpm run analyzer "$HEAD_REF" $version -o "github"
- name: Print results - name: Print results
id: results id: results
run: echo "::set-output name=results::${{ steps.run.outputs.templates }}${{ steps.run.outputs.wphooks }}${{ steps.run.outputs.schema }}${{ steps.run.outputs.database }}" run: echo "::set-output name=results::${{ steps.run.outputs.templates }}${{ steps.run.outputs.wphooks }}${{ steps.run.outputs.schema }}${{ steps.run.outputs.database }}"

View File

@ -0,0 +1,154 @@
/**
* Script to generate the test results summary.
*/
const { API_SUMMARY_PATH, E2E_PW_SUMMARY_PATH } = process.env;
/**
* Convert the given `duration` from milliseconds to a more user-friendly string.
* For example, if `duration = 323000`, this function would return `5m 23s`.
*
* @param {Number} duration Duration in millisecods, as read from either the `summary.json` file in the Allure report, or from the `test-results.json` file from the Jest-Puppeteer report.
* @returns String in "5m 23s" format.
*/
const getFormattedDuration = ( duration ) => {
const durationMinutes = Math.floor( duration / 1000 / 60 );
const durationSeconds = Math.floor( ( duration / 1000 ) % 60 );
return `${ durationMinutes }m ${ durationSeconds }s`;
};
/**
* Extract the test report statistics (the number of tests that passed, failed, skipped, etc.) from Allure report's `summary.json` file.
*
* @param {string} summaryJSONPath Path to the Allure report's `summary.json` file.
* @returns An object containing relevant statistics from the Allure report.
*/
const getAllureSummaryStats = ( summaryJSONPath ) => {
const summary = require( summaryJSONPath );
const { statistic, time } = summary;
const { passed, failed, skipped, broken, unknown, total } = statistic;
const { duration } = time;
return {
passed,
failed,
skipped,
broken,
unknown,
total,
duration,
};
};
/**
* Construct the array to be used for the API table row.
*
* @returns Array of API test result stats.
*/
const createAPITableRow = () => {
const { passed, failed, skipped, broken, unknown, total, duration } =
getAllureSummaryStats( API_SUMMARY_PATH );
const durationFormatted = getFormattedDuration( duration );
return [
'API Tests',
passed.toString(),
failed.toString(),
broken.toString(),
skipped.toString(),
unknown.toString(),
total.toString(),
durationFormatted,
];
};
/**
* Construct the array to be used for the E2E table row.
*
* @returns Array of E2E test result stats.
*/
const createE2ETableRow = () => {
const { passed, failed, skipped, broken, unknown, total, duration } =
getAllureSummaryStats( E2E_PW_SUMMARY_PATH );
const durationFormatted = getFormattedDuration( duration );
return [
'E2E Tests',
passed.toString(),
failed.toString(),
broken.toString(),
skipped.toString(),
unknown.toString(),
total.toString(),
durationFormatted,
];
};
/**
* Create the heading and test results table.
*
* @param core The GitHub Actions toolkit core object
*/
const addSummaryHeadingAndTable = ( core ) => {
const apiTableRow = createAPITableRow();
const e2eTableRow = createE2ETableRow();
core.summary.addHeading( 'Smoke tests on trunk' ).addTable( [
[
{ data: 'Test :test_tube:', header: true },
{ data: 'Passed :white_check_mark:', header: true },
{ data: 'Failed :rotating_light:', header: true },
{ data: 'Broken :construction:', header: true },
{ data: 'Skipped :next_track_button:', header: true },
{ data: 'Unknown :grey_question:', header: true },
{ data: 'Total :bar_chart:', header: true },
{ data: 'Duration :stopwatch:', header: true },
],
apiTableRow,
e2eTableRow,
] );
};
/**
* Add the summary footer.
*
* @param core The GitHub Actions toolkit core object
*/
const addSummaryFooter = ( core ) => {
core.summary
.addSeparator()
.addRaw( 'To view the full API test report, click ' )
.addLink(
'here.',
'https://woocommerce.github.io/woocommerce-test-reports/daily/api'
)
.addBreak()
.addRaw( 'To view the full E2E test report, click ' )
.addLink(
'here.',
'https://woocommerce.github.io/woocommerce-test-reports/daily/e2e'
)
.addBreak()
.addRaw( 'To view all test reports, visit the ' )
.addLink(
'WooCommerce Test Reports Dashboard.',
'https://woocommerce.github.io/woocommerce-test-reports/'
);
};
/**
* Generate the contents of the test results summary and post it on the workflow run.
*
* @param {*} params Objects passed from the calling GitHub Action workflow.
* @returns Stringified content of the test results summary.
*/
module.exports = async ( { core } ) => {
addSummaryHeadingAndTable( core );
addSummaryFooter( core );
const summary = core.summary.stringify();
await core.summary.write();
return summary;
};

View File

@ -1,99 +1,175 @@
name: Smoke test daily name: Smoke test daily
on: on:
schedule: # schedule:
- cron: '25 3 * * *' # - cron: '25 3 * * *'
workflow_dispatch: workflow_dispatch:
env:
API_ARTIFACT: api-daily--run-${{ github.run_number }}
E2E_ARTIFACT: e2e-daily--run-${{ github.run_number }}
FORCE_COLOR: 1
BRANCH_NAME: ${{ github.ref_name }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
login-run: e2e-tests:
name: Daily smoke test on trunk. name: E2E tests on trunk
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
if: always()
env: env:
API_TEST_REPORT_DIR: ${{ github.workspace }}/api-test-report BASE_URL: ${{ secrets.SMOKE_TEST_URL }}
outputs: ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }}
commit_message: ${{ steps.get_commit_message.outputs.commit_message }} ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }}
ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }}
CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }}
CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }}
DEFAULT_TIMEOUT_OVERRIDE: 120000
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
ref: trunk ref: ${{ env.BRANCH_NAME }}
- name: Setup WooCommerce Monorepo - name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo uses: ./.github/actions/setup-woocommerce-monorepo
with:
install-filters: woocommerce
build: false
- name: Install Jest - name: Download and install Chromium browser.
run: npm install -g jest
- name: Get latest commit message
id: get_commit_message
run: |
COMMIT_MESSAGE=$(git log --pretty=format:%s -1)
echo "::set-output name=commit_message::$COMMIT_MESSAGE"
- name: Run E2E smoke test.
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
env: run: pnpm exec playwright install chromium
SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_URL }}
SMOKE_TEST_ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }}
SMOKE_TEST_ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }}
SMOKE_TEST_ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }}
SMOKE_TEST_CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }}
SMOKE_TEST_CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }}
WC_E2E_SCREENSHOTS: 1
E2E_RETEST: 1
E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }}
E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }}
UPDATE_WC: 1
DEFAULT_TIMEOUT_OVERRIDE: 120000
run: |
pnpm exec wc-e2e docker:up
pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/update-woocommerce.js
pnpm exec wc-e2e test:e2e
- name: Run API smoke tests - name: Run 'Update WooCommerce' test.
working-directory: plugins/woocommerce
id: e2e-update
env:
UPDATE_WC: true
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js update-woocommerce.spec.js
- name: Run the rest of E2E tests.
timeout-minutes: 60
working-directory: plugins/woocommerce
id: e2e
env:
E2E_MAX_FAILURES: 15
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js basic.spec.js
- name: Generate Playwright E2E Test report.
id: generate_e2e_report
if: |
always() &&
(
steps.e2e-update.conclusion != 'cancelled' ||
steps.e2e-update.conclusion != 'skipped' ||
steps.e2e.conclusion != 'cancelled' ||
steps.e2e.conclusion != 'skipped'
)
working-directory: plugins/woocommerce
run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
- name: Archive E2E test report
if: |
always() &&
steps.generate_e2e_report.conclusion == 'success'
uses: actions/upload-artifact@v3
with:
name: ${{ env.E2E_ARTIFACT }}
path: |
${{ env.ALLURE_RESULTS_DIR }}
${{ env.ALLURE_REPORT_DIR }}
if-no-files-found: ignore
retention-days: 5
api-tests:
name: API tests on trunk
runs-on: ubuntu-20.04
needs: [e2e-tests]
if: always()
env:
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report
steps:
- uses: actions/checkout@v3
with:
ref: ${{ env.BRANCH_NAME }}
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
install-filters: woocommerce
build: false
- name: Run API tests.
if: always() if: always()
id: run_api_tests id: run_playwright_api_tests
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
env: env:
BASE_URL: ${{ secrets.SMOKE_TEST_URL }} BASE_URL: ${{ secrets.SMOKE_TEST_URL }}
USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }} USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }}
USER_SECRET: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} USER_SECRET: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }}
DEFAULT_TIMEOUT_OVERRIDE: 120000 DEFAULT_TIMEOUT_OVERRIDE: 120000
run: pnpm exec wc-api-tests test api run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js hello.test.js
- name: Generate API Test report.
id: generate_api_report
if: |
always() &&
(
steps.run_playwright_api_tests.conclusion != 'cancelled' ||
steps.run_playwright_api_tests.conclusion != 'skipped'
)
working-directory: plugins/woocommerce
run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
- name: Archive API test report - name: Archive API test report
if: | if: |
always() && always() &&
( steps.generate_api_report.conclusion == 'success'
steps.run_api_tests.conclusion != 'cancelled' ||
steps.run_api_tests.conclusion != 'skipped'
)
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: api-test-report---daily name: ${{ env.API_ARTIFACT }}
path: | path: |
${{ env.API_TEST_REPORT_DIR }}/allure-results ${{ env.ALLURE_RESULTS_DIR }}
${{ env.API_TEST_REPORT_DIR }}/allure-report ${{ env.ALLURE_REPORT_DIR }}
if-no-files-found: ignore
retention-days: 5 retention-days: 5
k6-tests:
name: k6 tests on trunk
runs-on: ubuntu-20.04
needs: [api-tests]
if: always()
steps:
- uses: actions/checkout@v3
with:
ref: ${{ env.BRANCH_NAME }}
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
install-filters: woocommerce
build: false
- name: Download and install Chromium browser.
working-directory: plugins/woocommerce
run: pnpm exec playwright install chromium
- name: Update performance test site with E2E test - name: Update performance test site with E2E test
if: always() if: always()
working-directory: plugins/woocommerce working-directory: plugins/woocommerce
env: env:
SMOKE_TEST_URL: ${{ secrets.SMOKE_TEST_PERF_URL }}/ BASE_URL: ${{ secrets.SMOKE_TEST_PERF_URL }}/
SMOKE_TEST_ADMIN_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }} ADMIN_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }}
SMOKE_TEST_ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }} ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }}
SMOKE_TEST_ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }} CUSTOMER_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }}
SMOKE_TEST_CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }} CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }}
SMOKE_TEST_CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }} UPDATE_WC: true
WC_E2E_SCREENSHOTS: 1 DEFAULT_TIMEOUT_OVERRIDE: 120000
E2E_RETEST: 1
E2E_RETRY_TIMES: 0
E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }}
E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }}
UPDATE_WC: 1
DEFAULT_TIMEOUT_OVERRIDE: 120000
run: | run: |
pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/update-woocommerce.js pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js update-woocommerce.spec.js
continue-on-error: true continue-on-error: true
- name: Install k6 - name: Install k6
@ -114,34 +190,15 @@ jobs:
run: | run: |
./k6 run plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js ./k6 run plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js
build:
name: Build zip for PR
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
build: false
- name: Build zip
working-directory: plugins/woocommerce
run: bash bin/build-zip.sh
- name: Upload the zip file as an artifact
uses: actions/upload-artifact@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: woocommerce
path: plugins/woocommerce/woocommerce.zip
retention-days: 7
test-plugins: test-plugins:
name: Smoke tests with ${{ matrix.plugin }} plugin installed name: Smoke tests with ${{ matrix.plugin }} plugin installed
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: [build] needs: [k6-tests]
if: always()
env:
USE_WP_ENV: 1
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -160,59 +217,164 @@ jobs:
- plugin: 'Contact Form 7' - plugin: 'Contact Form 7'
repo: 'takayukister/contact-form-7' repo: 'takayukister/contact-form-7'
steps: steps:
- name: Create dirs.
run: |
mkdir -p package/woocommerce
mkdir -p tmp/woocommerce
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
path: package/woocommerce ref: ${{ env.BRANCH_NAME }}
- name: Download WooCommerce ZIP. - name: Setup WooCommerce Monorepo
uses: actions/download-artifact@v3 uses: ./.github/actions/setup-woocommerce-monorepo
with:
name: woocommerce
path: tmp
- name: Extract and replace WooCommerce zip. - name: Launch wp-env e2e environment
working-directory: tmp working-directory: plugins/woocommerce
run: | run: pnpm env:test --filter=woocommerce
unzip woocommerce.zip -d .
rsync -a woocommerce/* ../package/woocommerce/plugins/woocommerce/
- name: Load docker images and start containers. - name: Download and install Chromium browser.
working-directory: package/woocommerce working-directory: plugins/woocommerce
run: pnpm docker:up --filter=woocommerce run: pnpm exec playwright install chromium
- name: Run tests command. - name: Run 'Upload plugin' test
working-directory: package/woocommerce/plugins/woocommerce id: e2e-upload
working-directory: plugins/woocommerce
env: env:
WC_E2E_SCREENSHOTS: 1
E2E_SLACK_TOKEN: ${{ secrets.SMOKE_TEST_SLACK_TOKEN }}
E2E_SLACK_CHANNEL: ${{ secrets.SMOKE_TEST_SLACK_CHANNEL }}
PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }} PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }}
PLUGIN_NAME: ${{ matrix.plugin }} PLUGIN_NAME: ${{ matrix.plugin }}
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }} GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: | run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js upload-plugin.spec.js
pnpm exec wc-e2e test:e2e tests/e2e/specs/smoke-tests/upload-plugin.js
pnpm exec wc-e2e test:e2e
publish-test-reports: - name: Run the rest of E2E tests
name: Publish test reports id: e2e
if: always() working-directory: plugins/woocommerce
env:
E2E_MAX_FAILURES: 15
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js basic.spec.js
- name: Generate E2E Test report.
id: report
if: |
always() &&
(
steps.e2e-upload.conclusion != 'cancelled' ||
steps.e2e-upload.conclusion != 'skipped' ||
steps.e2e.conclusion != 'cancelled' ||
steps.e2e.conclusion != 'skipped'
)
working-directory: plugins/woocommerce
run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
- name: Archive E2E test report
if: |
always() &&
steps.report.conclusion == 'success'
uses: actions/upload-artifact@v3
with:
name: Smoke tests with ${{ matrix.plugin }} plugin installed (run ${{ github.run_number }})
path: |
${{ env.ALLURE_RESULTS_DIR }}
${{ env.ALLURE_REPORT_DIR }}
if-no-files-found: ignore
retention-days: 5
trunk-results:
name: Publish report on smoke tests on trunk
if: always() &&
! github.event.pull_request.head.repo.fork
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: [login-run, build, test-plugins] needs: [test-plugins]
env:
GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }}
RUN_ID: ${{ github.run_id }}
API_ARTIFACT: api-test-report---daily
COMMIT_MESSAGE: ${{ needs.login-run.outputs.commit_message }}
steps: steps:
- name: Publish API test report - name: Create dirs
run: |
mkdir -p repo
mkdir -p artifacts/api
mkdir -p artifacts/e2e
mkdir -p output
- name: Checkout code
uses: actions/checkout@v3
with:
path: repo
ref: ${{ env.BRANCH_NAME }}
- name: Download API test report artifact
uses: actions/download-artifact@v3
with:
name: ${{ env.API_ARTIFACT }}
path: artifacts/api
- name: Download E2E test report artifact
uses: actions/download-artifact@v3
with:
name: ${{ env.E2E_ARTIFACT }}
path: artifacts/e2e
- name: Post test summary
uses: actions/github-script@v6
env:
API_SUMMARY_PATH: ${{ github.workspace }}/artifacts/api/allure-report/widgets/summary.json
E2E_PW_SUMMARY_PATH: ${{ github.workspace }}/artifacts/e2e/allure-report/widgets/summary.json
with:
result-encoding: string
script: |
const script = require( './repo/.github/workflows/scripts/prepare-test-summary-daily.js' )
return await script( { core } )
- name: Publish report
env:
GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }}
RUN_ID: ${{ github.run_id }}
run: | run: |
gh workflow run publish-test-reports-daily.yml \ gh workflow run publish-test-reports-daily.yml \
-f run_id=$RUN_ID \ -f run_id=$RUN_ID \
-f api_artifact=$API_ARTIFACT \ -f api_artifact="$API_ARTIFACT" \
-f commit_message="$COMMIT_MESSAGE" \ -f e2e_artifact="$E2E_ARTIFACT" \
-f s3_root=public \
--repo woocommerce/woocommerce-test-reports
plugins-results:
name: Publish report on smoke tests with plugins
if: |
always() &&
! github.event.pull_request.head.repo.fork
runs-on: ubuntu-20.04
needs: [test-plugins]
env:
GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }}
RUN_ID: ${{ github.run_id }}
ARTIFACT: Smoke tests with ${{ matrix.plugin }} plugin installed (run ${{ github.run_number }})
strategy:
fail-fast: false
matrix:
include:
- plugin: 'WooCommerce Payments'
repo: 'automattic/woocommerce-payments'
- plugin: 'WooCommerce PayPal Payments'
repo: 'woocommerce/woocommerce-paypal-payments'
- plugin: 'WooCommerce Shipping & Tax'
repo: 'automattic/woocommerce-services'
- plugin: 'WordPress SEO' # Yoast SEO in the UI, but the slug is wordpress-seo
repo: 'Yoast/wordpress-seo'
- plugin: 'Contact Form 7'
repo: 'takayukister/contact-form-7'
steps:
- name: Download test report artifact
uses: actions/download-artifact@v3
with:
name: ${{ env.ARTIFACT }}
# TODO: Add step to post job summary
- name: Get slug
id: get-slug
uses: actions/github-script@v6
with:
result-encoding: string
script: return "${{ matrix.repo }}".split( '/' ).pop()
- name: Publish reports
run: |
gh workflow run publish-test-reports-daily-plugins.yml \
-f run_id=$RUN_ID \
-f artifact="${{ env.ARTIFACT }}" \
-f plugin="${{ matrix.plugin }}" \
-f slug="${{ steps.get-slug.outputs.result }}" \
-f s3_root=public \
--repo woocommerce/woocommerce-test-reports --repo woocommerce/woocommerce-test-reports

View File

@ -1,5 +1,7 @@
# Unreleased # Unreleased
# 1.0.0
## Changed ## Changed
- Bumped jest version to v27 - Bumped jest version to v27
- Used the jest packaged bundled in this module to run tests - Used the jest packaged bundled in this module to run tests

View File

@ -1,6 +1,6 @@
{ {
"name": "@woocommerce/api-core-tests", "name": "@woocommerce/api-core-tests",
"version": "0.1.0", "version": "1.0.0",
"description": "API tests for WooCommerce", "description": "API tests for WooCommerce",
"main": "index.js", "main": "index.js",
"engines": { "engines": {

View File

@ -2,6 +2,59 @@
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [11.1.0](https://www.npmjs.com/package/@woocommerce/components/v/11.1.0) - 2022-10-24
- Minor - Allow passing of additional props to form inputs [#35160]
## [11.0.0](https://www.npmjs.com/package/@woocommerce/components/v/11.0.0) - 2022-10-20
- Patch - Export StepperProps for external usage [#35140]
- Patch - Fixed the initial setting of DateTimePickerControl's input field. [#35140]
- Patch - Fix EnrichedLabel Storybook story styles so they don't affect other stories. [#35140]
- Patch - Fixes DateTimePickerControl's debounce handling to work even if onChange prop changes. [#35140]
- Patch - Fix issue with form onChange handler, passing outdated values. [#35140]
- Patch - Update tag component styling [#35140]
- Patch - Add missing type definitions and add babel config for tests [#35140]
- Patch - Merging trunk with local [#35140]
- Patch - Removed unfinished and unused SplitDropdown component. [#35140]
- Patch - Assume ambiguous dates passed into DateTimePickerControl are UTC. [#35140]
- Patch - Remove default selected sortable item. [#35140]
- Minor - Fix Enriched-label styles
- Minor - Fix initially selected items in SelectControl component [#35140]
- Minor - Add date-only mode to DateTimePickerControl. [#35140]
- Minor - Add disabled option to the Select Control input component and alter the onInputChange callback [#35140]
- Minor - Add form input name dot notation name="product.dimensions.width" [#35140]
- Minor - Add FormSection component [#35140]
- Minor - Add ImageGallery component [#35140]
- Minor - Adding datetimepicker component. [#35140]
- Minor - Adding on-click toolbar to image gallery component items. [#35140]
- Minor - Add label prop to rich text editor [#35140]
- Minor - Add MediaUploader component [#35140]
- Minor - Add rich text editor component [#35140]
- Minor - Add SortableList component [#35140]
- Minor - Allow external tags in SelectControl component [#35140]
- Minor - Export ImportProps type. Add DateTimePickerControl to Form stories and tests. [#35140]
- Minor - Images Product management [#35140]
- Minor - Remove EnrichedLabel component in favor of Tooltip component [#35140]
- Minor - Update resetForm arguments, adding changed fields, touched fields and errors. [#35140]
- Minor - [PM Components] Create SplitDropdown component. #34180 [#35140]
- Minor - Add label, placeholder, and help props to DateTimePickerControl. [#35140]
- Minor - Adds setValues support to FormContext [#35140]
- Minor - Add support in SelectControl for using the popover slot for the popover. [#35140]
- Minor - Update experimental SelectControl compoment to expose a couple extra combobox functions from Downshift. [#35140]
- Minor - Update experimental SelectControl compoment to expose combobox functions from Downshift and provide additional options. [#35140]
- Minor - Update text input placement in SelectControl [#35140]
- Minor - Add component EnrichedLabel #34214 [#35140]
- Minor - Add new shippping class modal to a shipping class section in product page [#35140]
- Minor - Adjust build/test scripts to remove -- -- that was required for pnpm 6. [#35140]
- Minor - Fix node and pnpm versions via engines [#35140]
- Minor - Update Plugin installer component to TS [#35140]
- Minor - Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues [#35140]
- Minor - Fix DateTimePickerControl's onChange date arg to only be a string (TypeScript). [#35140]
- Minor - Improve experimental SelectControl accessibility [#35140]
- Minor - Improve Sortable component acessibility [#35140]
- - Create new experimental SelectControl component [#35140]
## [10.3.0](https://www.npmjs.com/package/@woocommerce/components/v/10.3.0) - 2022-08-12 ## [10.3.0](https://www.npmjs.com/package/@woocommerce/components/v/10.3.0) - 2022-08-12
- Patch - Added in missing TS definitions in package.json [#34279] - Patch - Added in missing TS definitions in package.json [#34279]

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Add component EnrichedLabel #34214

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Update resetForm arguments, adding changed fields, touched fields and errors.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: fix
Fix Enriched-label styles - #34382

View File

@ -1,4 +0,0 @@
Significance: patch
Type: tweak
Remove default selected sortable item.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Adding on-click toolbar to image gallery component items.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Add new shippping class modal to a shipping class section in product page

View File

@ -1,4 +0,0 @@
Significance: minor
Type: update
Update experimental SelectControl compoment to expose a couple extra combobox functions from Downshift.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add experimental ConditionalWrapper component

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Images Product management

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Adding datetimepicker component.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add date-only mode to DateTimePickerControl.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add SortableList component

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add form input name dot notation name="product.dimensions.width"

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add rich text editor component

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add ImageGallery component

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add MediaUploader component

View File

@ -1,4 +0,0 @@
Significance: minor
Type: update
Adds setValues support to FormContext

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add FormSection component

View File

@ -1,4 +0,0 @@
Significance: minor
Type: enhancement
Improve Sortable component acessibility

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
[PM Components] Create SplitDropdown component. #34180

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
Significance: patch
Type: enhancement
Update font size and spacing in the tooltip component

View File

@ -1,5 +0,0 @@
Significance: patch
Type: tweak
Comment: Minor update of react and react-dom to 17.0.2.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Fix issue with form onChange handler, passing outdated values.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: tweak
Assume ambiguous dates passed into DateTimePickerControl are UTC.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Fixes DateTimePickerControl's debounce handling to work even if onChange prop changes.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Fixed the initial setting of DateTimePickerControl's input field.

View File

@ -0,0 +1,5 @@
Significance: patch
Type: fix
Comment: Just a minor tweak to the CSS for the DateTimePickerControl suffix.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Fix EnrichedLabel Storybook story styles so they don't affect other stories.

View File

@ -1,6 +0,0 @@
Significance: patch
Type: fix
Export StepperProps for external usage

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Update variable name within useFormContext.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Update Plugin installer component to TS

View File

@ -1,5 +0,0 @@
Significance: patch
Type: tweak
Comment: Reverted change of last PR as part of #34614

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Fix up initial block selection in RichTextEditor and add media blocks

View File

@ -1,4 +0,0 @@
Significance: minor
Type: enhancement
Improve experimental SelectControl accessibility

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Add name to exported popover slot used to display SelectControl Menu, so it is only used for SelectControl menus.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: fix
Fix initially selected items in SelectControl component

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Removed unfinished and unused SplitDropdown component.

View File

@ -1,4 +0,0 @@
Significance: major
Type: add
Create new experimental SelectControl component

View File

@ -1,4 +0,0 @@
Significance: patch
Type: update
Update tag component styling

View File

@ -1,4 +0,0 @@
Significance: minor
Type: update
Add label, placeholder, and help props to DateTimePickerControl.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Added ability to force time when DateTimePickerControl is date-only (timeForDateOnly prop).

View File

@ -0,0 +1,4 @@
Significance: major
Type: update
Switch DateTimePickerControl formatting to PHP style, for WP compatibility.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: tweak
Fix DateTimePickerControl's onChange date arg to only be a string (TypeScript).

View File

@ -1,4 +0,0 @@
Significance: minor
Type: update
Update experimental SelectControl compoment to expose combobox functions from Downshift and provide additional options.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: update
Update text input placement in SelectControl

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Allow external tags in SelectControl component

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Adjust build/test scripts to remove -- -- that was required for pnpm 6.

View File

@ -1,6 +1,6 @@
{ {
"name": "@woocommerce/components", "name": "@woocommerce/components",
"version": "10.3.0", "version": "11.1.0",
"description": "UI components for WooCommerce.", "description": "UI components for WooCommerce.",
"author": "Automattic", "author": "Automattic",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",

View File

@ -1,8 +1,8 @@
.woocommerce-date-time-picker-control { .woocommerce-date-time-picker-control {
display: block; display: block;
.woocommerce-date-time-picker-control__input-control__suffix { .components-input-control__suffix {
padding-right: 8px; margin-right: 8px;
} }
.components-datetime__date { .components-datetime__date {

View File

@ -1,6 +1,7 @@
/** /**
* External dependencies * External dependencies
*/ */
import { format as formatDate } from '@wordpress/date';
import { import {
createElement, createElement,
useState, useState,
@ -22,9 +23,11 @@ import {
__experimentalInputControl as InputControl, __experimentalInputControl as InputControl,
} from '@wordpress/components'; } from '@wordpress/components';
export const defaultDateFormat = 'MM/DD/YYYY'; // PHP style formatting:
export const default12HourDateTimeFormat = 'MM/DD/YYYY h:mm a'; // https://wordpress.org/support/article/formatting-date-and-time/
export const default24HourDateTimeFormat = 'MM/DD/YYYY H:mm'; export const defaultDateFormat = 'm/d/Y';
export const default12HourDateTimeFormat = 'm/d/Y h:i a';
export const default24HourDateTimeFormat = 'm/d/Y H:i';
export type DateTimePickerControlOnChangeHandler = ( export type DateTimePickerControlOnChangeHandler = (
date: string, date: string,
@ -37,6 +40,7 @@ export type DateTimePickerControlProps = {
disabled?: boolean; disabled?: boolean;
isDateOnlyPicker?: boolean; isDateOnlyPicker?: boolean;
is12HourPicker?: boolean; is12HourPicker?: boolean;
timeForDateOnly?: 'start-of-day' | 'end-of-day';
onChange?: DateTimePickerControlOnChangeHandler; onChange?: DateTimePickerControlOnChangeHandler;
onBlur?: () => void; onBlur?: () => void;
label?: string; label?: string;
@ -49,6 +53,7 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( {
currentDate, currentDate,
isDateOnlyPicker = false, isDateOnlyPicker = false,
is12HourPicker = true, is12HourPicker = true,
timeForDateOnly = 'start-of-day',
dateTimeFormat, dateTimeFormat,
disabled = false, disabled = false,
onChange, onChange,
@ -103,9 +108,10 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( {
return moment.utc( dateString, moment.ISO_8601, true ); return moment.utc( dateString, moment.ISO_8601, true );
} }
function parseMoment( dateString?: string | null ): Moment { function parseMoment( dateString: string | null ): Moment {
// parse input date string as local time // parse input date string as local time;
return moment( dateString, displayFormat ); // be lenient of user input and try to match any format Moment can
return moment( dateString );
} }
function formatMomentIso( momentDate: Moment ): string { function formatMomentIso( momentDate: Moment ): string {
@ -113,7 +119,21 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( {
} }
function formatMoment( momentDate: Moment ): string { function formatMoment( momentDate: Moment ): string {
return momentDate.local().format( displayFormat ); return formatDate( displayFormat, momentDate.local() );
}
function maybeForceTime( momentDate: Moment ): Moment {
if ( ! isDateOnlyPicker ) return momentDate;
const updatedMomentDate = momentDate.clone();
if ( timeForDateOnly === 'start-of-day' ) {
updatedMomentDate.startOf( 'day' );
} else if ( timeForDateOnly === 'end-of-day' ) {
updatedMomentDate.endOf( 'day' );
}
return updatedMomentDate;
} }
function hasFocusLeftInputAndDropdownContent( function hasFocusLeftInputAndDropdownContent(
@ -161,15 +181,17 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( {
onChangePropFunctionRef.current = onChange; onChangePropFunctionRef.current = onChange;
}, [ onChange ] ); }, [ onChange ] );
const inputStringChangeHandlerFunctionRef = useRef< function inputStringChangeHandlerFunction(
( newInputString: string, fireOnChange: boolean ) => void newInputString: string,
>( ( newInputString: string, fireOnChange: boolean ) => { fireOnChange: boolean
) {
if ( ! isMounted.current ) return; if ( ! isMounted.current ) return;
const newDateTime = parseMoment( newInputString ); let newDateTime = parseMoment( newInputString );
const isValid = newDateTime.isValid(); const isValid = newDateTime.isValid();
if ( isValid ) { if ( isValid ) {
newDateTime = maybeForceTime( newDateTime );
setLastValidDate( newDateTime ); setLastValidDate( newDateTime );
} }
@ -182,7 +204,17 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( {
isValid isValid
); );
} }
} ); }
const inputStringChangeHandlerFunctionRef = useRef<
( newInputString: string, fireOnChange: boolean ) => void
>( inputStringChangeHandlerFunction );
// whenever forceTimeTo changes, we need to reset the ref to inputStringChangeHandlerFunction
// so that we are using the most current forceTimeTo value inside of it
useEffect( () => {
inputStringChangeHandlerFunctionRef.current =
inputStringChangeHandlerFunction;
}, [ timeForDateOnly ] );
const debouncedInputStringChangeHandler = useDebounce( const debouncedInputStringChangeHandler = useDebounce(
inputStringChangeHandlerFunctionRef.current, inputStringChangeHandlerFunctionRef.current,
@ -228,7 +260,7 @@ export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( {
} else { } else {
changeImmediate( currentDate || '', fireOnChange ); changeImmediate( currentDate || '', fireOnChange );
} }
}, [ currentDate, displayFormat ] ); }, [ currentDate, displayFormat, timeForDateOnly ] );
return ( return (
<Dropdown <Dropdown

View File

@ -28,11 +28,13 @@ Basic.args = {
help: 'Type a date and time or use the picker', help: 'Type a date and time or use the picker',
}; };
const customFormat = 'Y-m-d H:i';
export const CustomDateTimeFormat = Template.bind( {} ); export const CustomDateTimeFormat = Template.bind( {} );
CustomDateTimeFormat.args = { CustomDateTimeFormat.args = {
...Basic.args, ...Basic.args,
help: 'Format: YYYY-MM-DD HH:mm', help: 'Format: ' + customFormat,
dateTimeFormat: 'YYYY-MM-DD HH:mm', dateTimeFormat: customFormat,
}; };
function ControlledContainer( { children, ...props } ) { function ControlledContainer( { children, ...props } ) {
@ -97,3 +99,10 @@ ControlledDateOnly.args = {
isDateOnlyPicker: true, isDateOnlyPicker: true,
}; };
ControlledDateOnly.decorators = Controlled.decorators; ControlledDateOnly.decorators = Controlled.decorators;
export const ControlledDateOnlyEndOfDay = Template.bind( {} );
ControlledDateOnlyEndOfDay.args = {
...ControlledDateOnly.args,
timeForDateOnly: 'end-of-day',
};
ControlledDateOnlyEndOfDay.decorators = Controlled.decorators;

View File

@ -3,6 +3,7 @@
*/ */
import { render, waitFor, fireEvent } from '@testing-library/react'; import { render, waitFor, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { format as formatDate } from '@wordpress/date';
import { createElement, Fragment } from '@wordpress/element'; import { createElement, Fragment } from '@wordpress/element';
import moment from 'moment'; import moment from 'moment';
@ -102,7 +103,7 @@ describe( 'DateTimePickerControl', () => {
const input = container.querySelector( 'input' ); const input = container.querySelector( 'input' );
expect( input?.value ).toBe( expect( input?.value ).toBe(
dateTime.format( default24HourDateTimeFormat ) formatDate( default24HourDateTimeFormat, dateTime )
); );
} ); } );
@ -119,10 +120,10 @@ describe( 'DateTimePickerControl', () => {
const input = container.querySelector( 'input' ); const input = container.querySelector( 'input' );
expect( input?.value ).toBe( expect( input?.value ).toBe(
moment formatDate(
.utc( ambiguousISODateTimeString ) default24HourDateTimeFormat,
.local() moment.utc( ambiguousISODateTimeString ).local()
.format( default24HourDateTimeFormat ) )
); );
} ); } );
@ -139,10 +140,10 @@ describe( 'DateTimePickerControl', () => {
const input = container.querySelector( 'input' ); const input = container.querySelector( 'input' );
expect( input?.value ).toBe( expect( input?.value ).toBe(
moment formatDate(
.utc( unambiguousISODateTimeString ) default24HourDateTimeFormat,
.local() moment.utc( unambiguousISODateTimeString ).local()
.format( default24HourDateTimeFormat ) )
); );
} ); } );
@ -158,7 +159,7 @@ describe( 'DateTimePickerControl', () => {
const input = container.querySelector( 'input' ); const input = container.querySelector( 'input' );
expect( input?.value ).toBe( expect( input?.value ).toBe(
dateTime.format( default12HourDateTimeFormat ) formatDate( default12HourDateTimeFormat, dateTime )
); );
} ); } );
@ -174,7 +175,7 @@ describe( 'DateTimePickerControl', () => {
); );
const input = container.querySelector( 'input' ); const input = container.querySelector( 'input' );
expect( input?.value ).toBe( dateTime.format( dateTimeFormat ) ); expect( input?.value ).toBe( formatDate( dateTimeFormat, dateTime ) );
} ); } );
it( 'should update the input when currentDate is changed', () => { it( 'should update the input when currentDate is changed', () => {
@ -197,7 +198,7 @@ describe( 'DateTimePickerControl', () => {
const input = container.querySelector( 'input' ); const input = container.querySelector( 'input' );
expect( input?.value ).toBe( expect( input?.value ).toBe(
updatedDateTime.format( default24HourDateTimeFormat ) formatDate( default24HourDateTimeFormat, updatedDateTime )
); );
} ); } );
@ -305,9 +306,9 @@ describe( 'DateTimePickerControl', () => {
// TypeError: Cannot read properties of null (reading 'createEvent') // TypeError: Cannot read properties of null (reading 'createEvent')
it( 'should call onChange when the input is changed', async () => { it( 'should call onChange when the input is changed', async () => {
const originalDateTime = moment( '2022-09-15 02:30:40' ); const originalDateTime = moment( '2022-09-15 02:30:40' );
const dateTimeFormat = 'HH:mm, MM-DD-YYYY'; const dateTimeFormat = 'm-d-Y, H:i';
const newDateTimeInputString = '02:04, 06-08-2010'; const newDateTimeInputString = '06-08-2010, 02:04';
const newDateTime = moment( newDateTimeInputString, dateTimeFormat ); const newDateTime = moment( newDateTimeInputString );
const onChangeHandler = jest.fn(); const onChangeHandler = jest.fn();
const { container } = render( const { container } = render(
@ -335,6 +336,122 @@ describe( 'DateTimePickerControl', () => {
); );
}, 10000 ); }, 10000 );
// We need to bump up the timeout for this test because:
// 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577)
// 2. moment.js is slow
// Otherwise, the following error can occur on slow machines (such as our CI), because Jest times out and starts
// tearing down the component while test microtasks are still being executed
// (see https://github.com/facebook/jest/issues/12670)
// TypeError: Cannot read properties of null (reading 'createEvent')
it( 'should force time to the start of the day if date only', async () => {
const originalDateTime = moment( '09-15-2022' );
const newDateTimeInputString = '06-08-2010';
const newDateTime = moment( newDateTimeInputString ).startOf( 'day' );
const onChangeHandler = jest.fn();
const { container } = render(
<DateTimePickerControl
isDateOnlyPicker
timeForDateOnly={ 'start-of-day' }
currentDate={ originalDateTime.toISOString() }
onChange={ onChangeHandler }
onChangeDebounceWait={ 10 }
/>
);
const input = container.querySelector( 'input' );
userEvent.type(
input!,
'{selectall}{backspace}' + newDateTimeInputString
);
await waitFor(
() =>
expect( onChangeHandler ).toHaveBeenLastCalledWith(
newDateTime.toISOString(),
true
),
{ timeout: 100 }
);
}, 10000 );
// We need to bump up the timeout for this test because:
// 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577)
// 2. moment.js is slow
// Otherwise, the following error can occur on slow machines (such as our CI), because Jest times out and starts
// tearing down the component while test microtasks are still being executed
// (see https://github.com/facebook/jest/issues/12670)
// TypeError: Cannot read properties of null (reading 'createEvent')
it( 'should force time to the end of the day if date only', async () => {
const originalDateTime = moment( '09-15-2022' );
const newDateTimeInputString = '06-08-2010';
const newDateTime = moment( newDateTimeInputString ).endOf( 'day' );
const onChangeHandler = jest.fn();
const { container } = render(
<DateTimePickerControl
isDateOnlyPicker
timeForDateOnly={ 'end-of-day' }
currentDate={ originalDateTime.toISOString() }
onChange={ onChangeHandler }
onChangeDebounceWait={ 10 }
/>
);
const input = container.querySelector( 'input' );
userEvent.type(
input!,
'{selectall}{backspace}' + newDateTimeInputString
);
await waitFor(
() =>
expect( onChangeHandler ).toHaveBeenLastCalledWith(
newDateTime.toISOString(),
true
),
{ timeout: 100 }
);
}, 10000 );
// We need to bump up the timeout for this test because:
// 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577)
// 2. moment.js is slow
// Otherwise, the following error can occur on slow machines (such as our CI), because Jest times out and starts
// tearing down the component while test microtasks are still being executed
// (see https://github.com/facebook/jest/issues/12670)
// TypeError: Cannot read properties of null (reading 'createEvent')
it( 'should not force time to the start of the day if not date only', async () => {
const originalDateTime = moment( '09-15-2022' );
const newDateTimeInputString = '06-08-2010 7:00';
const newDateTime = moment( newDateTimeInputString );
const onChangeHandler = jest.fn();
const { container } = render(
<DateTimePickerControl
timeForDateOnly={ 'start-of-day' }
currentDate={ originalDateTime.toISOString() }
onChange={ onChangeHandler }
onChangeDebounceWait={ 10 }
/>
);
const input = container.querySelector( 'input' );
userEvent.type(
input!,
'{selectall}{backspace}' + newDateTimeInputString
);
await waitFor(
() =>
expect( onChangeHandler ).toHaveBeenLastCalledWith(
newDateTime.toISOString(),
true
),
{ timeout: 100 }
);
}, 10000 );
// We need to bump up the timeout for this test because: // We need to bump up the timeout for this test because:
// 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577) // 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577)
// 2. moment.js is slow // 2. moment.js is slow
@ -377,9 +494,9 @@ describe( 'DateTimePickerControl', () => {
// TypeError: Cannot read properties of null (reading 'createEvent') // TypeError: Cannot read properties of null (reading 'createEvent')
it( 'should call the current onChange when the input is changed', async () => { it( 'should call the current onChange when the input is changed', async () => {
const originalDateTime = moment( '2022-09-15 02:30:40' ); const originalDateTime = moment( '2022-09-15 02:30:40' );
const dateTimeFormat = 'HH:mm, MM-DD-YYYY'; const dateTimeFormat = 'm-d-Y, H:i';
const newDateTimeInputString = '02:04, 06-08-2010'; const newDateTimeInputString = '06-08-2010, 02:04';
const newDateTime = moment( newDateTimeInputString, dateTimeFormat ); const newDateTime = moment( newDateTimeInputString );
const originalOnChangeHandler = jest.fn(); const originalOnChangeHandler = jest.fn();
const newOnChangeHandler = jest.fn(); const newOnChangeHandler = jest.fn();

View File

@ -1,23 +0,0 @@
# EnrichedLabel
Use `EnrichedLabel` to create a label with a tooltip.
## Usage
```jsx
<EnrichedLabel
label="My label"
helpDescription="My description."
moreUrl="https://woocommerce.com"
tooltipLinkCallback={ () => alert( 'Learn More clicked' ) }
/>
```
### Props
| Name | Type | Default | Description |
| --------------------- | -------- | ------- | ----------------------------------------------------------------------- |
| `helpDescription` | String | `null` | Text that will be shown in the tooltip. |
| `label` | String | `null` | Text that will be shown in the label. |
| `moreUrl` | String | `null` | URL that will be added to the link `Learn More`, shown after the label. |
| `tooltipLinkCallback` | Function | `noop` | Callback that will be triggered after clicking the `Learn More` link. |

View File

@ -1,75 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, Popover } from '@wordpress/components';
import { createElement, Fragment, useState } from '@wordpress/element';
import interpolateComponents from '@automattic/interpolate-components';
import { Icon, help } from '@wordpress/icons';
/**
* Internal dependencies
*/
import Link from '../link';
type EnrichedLabelProps = {
helpDescription: string;
label: string;
moreUrl: string;
tooltipLinkCallback: () => void;
};
export const EnrichedLabel: React.FC< EnrichedLabelProps > = ( {
helpDescription,
label,
moreUrl,
tooltipLinkCallback,
} ) => {
const [ isPopoverVisible, setIsPopoverVisible ] = useState( false );
return (
<>
<span className="woocommerce-enriched-label__text">{ label }</span>
{ helpDescription && (
<div
className="woocommerce-enriched-label__help-wrapper"
onMouseLeave={ () => setIsPopoverVisible( false ) }
>
<Button
label={ __( 'Help button', 'woocommerce' ) }
onMouseEnter={ () => setIsPopoverVisible( true ) }
>
<Icon icon={ help } />
</Button>
{ isPopoverVisible && (
<Popover focusOnMount="container" position="top center">
{ interpolateComponents( {
mixedString:
helpDescription +
( moreUrl ? ' {{moreLink/}}' : '' ),
components: {
moreLink: moreUrl ? (
<Link
href={ moreUrl }
target="_blank"
type="external"
onClick={ tooltipLinkCallback }
>
{ __(
'Learn more',
'woocommerce'
) }
</Link>
) : (
<div />
),
},
} ) }
</Popover>
) }
</div>
) }
</>
);
};

View File

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

View File

@ -1,44 +0,0 @@
/**
* External dependencies
*/
import { CheckboxControl } from '@wordpress/components';
/**
* Internal dependencies
*/
import { EnrichedLabel } from '../';
import './style.scss';
export default {
title: 'WooCommerce Admin/components/EnrichedLabel',
component: EnrichedLabel,
argTypes: {
tooltipLinkCallback: { action: 'tooltipLinkCallback' },
},
};
const Template = ( args ) => (
<EnrichedLabel
label="My label"
helpDescription="My description."
moreUrl="https://woocommerce.com"
tooltipLinkCallback={ () => {
// eslint-disable-next-line no-alert
window.alert( 'Learn More clicked' );
} }
{ ...args }
/>
);
export const Basic = Template.bind( {} );
Basic.decorators = [
( story, props ) => {
return (
<CheckboxControl
className="woocommerce-enriched-label-story__checkbox-control"
label={ story( { args: { ...props.args } } ) }
onChange={ () => {} }
/>
);
},
];

View File

@ -1,23 +0,0 @@
.woocommerce-enriched-label-story__checkbox-control {
.woocommerce-enriched-label__help-wrapper {
.components-popover {
margin: 0;
}
}
.components-base-control__field {
display: flex;
.components-checkbox-control {
&__label {
display: flex;
}
&__input-container {
align-self: center;
}
.woocommerce-enriched-label__text {
align-self: center;
}
}
}
}

View File

@ -1,18 +0,0 @@
.woocommerce-enriched-label__text {
align-self: center;
}
.woocommerce-enriched-label__help-wrapper {
.components-button {
padding: 0;
height: 28px;
}
.components-popover {
.components-popover__content {
min-width: 360px;
> div {
padding: $gap $gap-large;
font-size: 16px;
}
}
}
}

View File

@ -5,13 +5,25 @@
left: 0; left: 0;
margin-top: $gap-smaller; margin-top: $gap-smaller;
box-sizing: border-box; box-sizing: border-box;
display: none; }
.components-popover.woocommerce-experimental-select-control__popover-menu {
background: $studio-white; background: $studio-white;
border: 1px solid $studio-gray-5; border: 1px solid $studio-gray-5;
border-radius: 3px; border-radius: 3px;
z-index: 10; display: none;
&.is-open.has-results { &.is-open.has-results {
display: block; display: block;
} }
} }
.woocommerce-experimental-select-control__popover-menu-container {
margin: 0;
max-height: 300px;
overflow-y: scroll;
> .category-field-dropdown__item:not( :first-child ) {
.category-field-dropdown__item-content {
border-top: 1px solid $gray-200;
}
}
}

View File

@ -1,8 +1,16 @@
/** /**
* External dependencies * External dependencies
*/ */
import { Popover } from '@wordpress/components';
import classnames from 'classnames'; import classnames from 'classnames';
import { createElement } from '@wordpress/element'; import {
createElement,
useEffect,
useRef,
useState,
createPortal,
Children,
} from '@wordpress/element';
/** /**
* Internal dependencies * Internal dependencies
@ -22,21 +30,65 @@ export const Menu = ( {
isOpen, isOpen,
className, className,
}: MenuProps ) => { }: MenuProps ) => {
const [ boundingRect, setBoundingRect ] = useState< DOMRect >();
const selectControlMenuRef = useRef< HTMLDivElement >( null );
useEffect( () => {
if ( selectControlMenuRef.current?.parentElement ) {
setBoundingRect(
selectControlMenuRef.current.parentElement.getBoundingClientRect()
);
}
}, [ selectControlMenuRef.current ] );
/* 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 ( return (
<ul <div
{ ...getMenuProps() } ref={ selectControlMenuRef }
className={ classnames( className={ classnames(
'woocommerce-experimental-select-control__menu', 'woocommerce-experimental-select-control__menu',
className, className
{
'is-open': isOpen,
'has-results': Array.isArray( children )
? children.length
: Boolean( children ),
}
) } ) }
> >
{ isOpen && children } <Popover
</ul> // @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-control-menu"
focusOnMount={ false }
className={ classnames(
'woocommerce-experimental-select-control__popover-menu',
{
'is-open': isOpen,
'has-results': Children.count( children ) > 0,
}
) }
position="bottom center"
animate={ false }
>
<ul
{ ...getMenuProps() }
className="woocommerce-experimental-select-control__popover-menu-container"
style={ {
width: boundingRect?.width,
} }
onMouseUp={ ( e ) =>
// Fix to prevent select control dropdown from closing when selecting within the Popover.
e.stopPropagation()
}
>
{ isOpen && children }
</ul>
</Popover>
</div>
); );
/* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
}; };
export const MenuSlot: 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-control-menu" />
</div>,
document.body
);

View File

@ -48,7 +48,10 @@ type SelectControlProps< ItemType > = {
) => ItemType[]; ) => ItemType[];
hasExternalTags?: boolean; hasExternalTags?: boolean;
multiple?: boolean; multiple?: boolean;
onInputChange?: ( value: string | undefined ) => void; onInputChange?: (
value: string | undefined,
changes: Partial< Omit< UseComboboxState< ItemType >, 'inputValue' > >
) => void;
onRemove?: ( item: ItemType ) => void; onRemove?: ( item: ItemType ) => void;
onSelect?: ( selected: ItemType ) => void; onSelect?: ( selected: ItemType ) => void;
onFocus?: ( data: { inputValue: string } ) => void; onFocus?: ( data: { inputValue: string } ) => void;
@ -59,6 +62,7 @@ type SelectControlProps< ItemType > = {
placeholder?: string; placeholder?: string;
selected: ItemType | ItemType[] | null; selected: ItemType | ItemType[] | null;
className?: string; className?: string;
disabled?: boolean;
}; };
export const selectControlStateChangeTypes = useCombobox.stateChangeTypes; export const selectControlStateChangeTypes = useCombobox.stateChangeTypes;
@ -102,6 +106,7 @@ function SelectControl< ItemType = DefaultItemType >( {
placeholder, placeholder,
selected, selected,
className, className,
disabled,
}: SelectControlProps< ItemType > ) { }: SelectControlProps< ItemType > ) {
const [ isFocused, setIsFocused ] = useState( false ); const [ isFocused, setIsFocused ] = useState( false );
const [ inputValue, setInputValue ] = useState( '' ); const [ inputValue, setInputValue ] = useState( '' );
@ -150,16 +155,14 @@ function SelectControl< ItemType = DefaultItemType >( {
initialSelectedItem: singleSelectedItem, initialSelectedItem: singleSelectedItem,
inputValue, inputValue,
items: filteredItems, items: filteredItems,
selectedItem: multiple ? null : undefined, selectedItem: multiple ? null : singleSelectedItem,
itemToString: getItemLabel, itemToString: getItemLabel,
onSelectedItemChange: ( { selectedItem } ) => onSelectedItemChange: ( { selectedItem } ) =>
selectedItem && onSelect( selectedItem ), selectedItem && onSelect( selectedItem ),
onInputValueChange: ( changes ) => { onInputValueChange: ( { inputValue: value, ...changes } ) => {
if ( changes.inputValue !== undefined ) { if ( value !== undefined ) {
setInputValue( changes.inputValue ); setInputValue( value );
if ( changes.isOpen ) { onInputChange( value, changes );
onInputChange( changes.inputValue );
}
} }
}, },
stateReducer: ( state, actionAndChanges ) => { stateReducer: ( state, actionAndChanges ) => {
@ -225,12 +228,14 @@ function SelectControl< ItemType = DefaultItemType >( {
> >
{ /* Downshift's getLabelProps handles the necessary label attributes. */ } { /* Downshift's getLabelProps handles the necessary label attributes. */ }
{ /* eslint-disable jsx-a11y/label-has-for */ } { /* eslint-disable jsx-a11y/label-has-for */ }
<label { label && (
{ ...getLabelProps() } <label
className="woocommerce-experimental-select-control__label" { ...getLabelProps() }
> className="woocommerce-experimental-select-control__label"
{ label } >
</label> { label }
</label>
) }
{ /* eslint-enable jsx-a11y/label-has-for */ } { /* eslint-enable jsx-a11y/label-has-for */ }
<ComboBox <ComboBox
comboBoxProps={ getComboboxProps() } comboBoxProps={ getComboboxProps() }
@ -245,6 +250,7 @@ function SelectControl< ItemType = DefaultItemType >( {
}, },
onBlur: () => setIsFocused( false ), onBlur: () => setIsFocused( false ),
placeholder, placeholder,
disabled,
} ) } } ) }
> >
<> <>

View File

@ -1,7 +1,13 @@
/** /**
* External dependencies * External dependencies
*/ */
import { CheckboxControl, Spinner } from '@wordpress/components'; import {
Button,
CheckboxControl,
Modal,
SlotFillProvider,
Spinner,
} from '@wordpress/components';
import React from 'react'; import React from 'react';
import { createElement, useState } from '@wordpress/element'; import { createElement, useState } from '@wordpress/element';
@ -11,7 +17,7 @@ import { createElement, useState } from '@wordpress/element';
import { SelectedType, DefaultItemType, getItemLabelType } from '../types'; import { SelectedType, DefaultItemType, getItemLabelType } from '../types';
import { MenuItem } from '../menu-item'; import { MenuItem } from '../menu-item';
import { SelectControl, selectControlStateChangeTypes } from '../'; import { SelectControl, selectControlStateChangeTypes } from '../';
import { Menu } from '../menu'; import { Menu, MenuSlot } from '../menu';
const sampleItems = [ const sampleItems = [
{ value: 'apple', label: 'Apple' }, { value: 'apple', label: 'Apple' },
@ -365,6 +371,45 @@ export const CustomItemType: React.FC = () => {
); );
}; };
export const SingleWithinModalUsingBodyDropdownPlacement: React.FC = () => {
const [ isOpen, setOpen ] = useState( true );
const [ selected, setSelected ] =
useState< SelectedType< DefaultItemType > >();
const [ selectedTwo, setSelectedTwo ] =
useState< SelectedType< DefaultItemType > >();
return (
<SlotFillProvider>
Selected: { JSON.stringify( selected ) }
<Button onClick={ () => setOpen( true ) }>
Show Dropdown in Modal
</Button>
{ isOpen && (
<Modal
title="Dropdown Modal"
onRequestClose={ () => setOpen( false ) }
>
<SelectControl
items={ sampleItems }
label="Single value"
selected={ selected }
onSelect={ ( item ) => item && setSelected( item ) }
onRemove={ () => setSelected( null ) }
/>
<SelectControl
items={ sampleItems }
label="Single value"
selected={ selectedTwo }
onSelect={ ( item ) => item && setSelectedTwo( item ) }
onRemove={ () => setSelectedTwo( null ) }
/>
</Modal>
) }
<MenuSlot />
</SlotFillProvider>
);
};
export default { export default {
title: 'WooCommerce Admin/experimental/SelectControl', title: 'WooCommerce Admin/experimental/SelectControl',
component: SelectControl, component: SelectControl,

View File

@ -1,9 +1,18 @@
/** /**
* External dependencies * External dependencies
*/ */
import { ChangeEvent } from 'react';
import { createContext, useContext } from '@wordpress/element'; import { createContext, useContext } from '@wordpress/element';
/**
* Internal dependencies
*/
import {
CheckboxProps,
ConsumerInputProps,
InputProps,
SelectControlProps,
} from './form';
export type FormErrors< Values > = { export type FormErrors< Values > = {
[ P in keyof Values ]?: FormErrors< Values[ P ] > | string; [ P in keyof Values ]?: FormErrors< Values[ P ] > | string;
}; };
@ -21,17 +30,18 @@ export type FormContext< Values extends Record< string, any > > = {
setValue: ( name: string, value: any ) => void; setValue: ( name: string, value: any ) => void;
setValues: ( valuesToSet: Values ) => void; setValues: ( valuesToSet: Values ) => void;
handleSubmit: () => Promise< Values >; handleSubmit: () => Promise< Values >;
getCheckboxControlProps< Value extends Values[ keyof Values ] >(
name: string,
inputProps?: ConsumerInputProps< Values >
): CheckboxProps< Values, Value >;
getSelectControlProps< Value extends Values[ keyof Values ] >(
name: string,
inputProps?: ConsumerInputProps< Values >
): SelectControlProps< Values, Value >;
getInputProps< Value extends Values[ keyof Values ] >( getInputProps< Value extends Values[ keyof Values ] >(
name: string name: string,
): { inputProps?: ConsumerInputProps< Values >
value: Value; ): InputProps< Values, Value >;
checked: boolean;
selected?: boolean;
onChange: ( value: ChangeEvent< HTMLInputElement > | Value ) => void;
onBlur: () => void;
className: string | undefined;
help: string | null | undefined;
};
isValidForm: boolean; isValidForm: boolean;
resetForm: ( resetForm: (
initialValues: Values, initialValues: Values,
@ -48,7 +58,7 @@ export const FormContext = createContext< FormContext< any > >(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useFormContext< Values extends Record< string, any > >() { export function useFormContext< Values extends Record< string, any > >() {
const formik = useContext< FormContext< Values > >( FormContext ); const formContext = useContext< FormContext< Values > >( FormContext );
return formik; return formContext;
} }

View File

@ -1,6 +1,7 @@
/** /**
* External dependencies * External dependencies
*/ */
import classnames from 'classnames';
import { import {
cloneElement, cloneElement,
useState, useState,
@ -17,6 +18,7 @@ import _setWith from 'lodash/setWith';
import _get from 'lodash/get'; import _get from 'lodash/get';
import _clone from 'lodash/clone'; import _clone from 'lodash/clone';
import _isEqual from 'lodash/isEqual'; import _isEqual from 'lodash/isEqual';
import _omit from 'lodash/omit';
/** /**
* Internal dependencies * Internal dependencies
@ -87,6 +89,40 @@ export type FormRef< Values > = {
resetForm: ( initialValues: Values ) => void; resetForm: ( initialValues: Values ) => void;
}; };
export type InputProps< Values, Value > = {
value: Value;
checked: boolean;
selected?: boolean;
onChange: (
value: ChangeEvent< HTMLInputElement > | Values[ keyof Values ]
) => void;
onBlur: () => void;
className: string | undefined;
help: string | null | undefined;
};
export type CheckboxProps< Values, Value > = Omit<
InputProps< Values, Value >,
'value' | 'selected'
>;
export type SelectControlProps< Values, Value > = Omit<
InputProps< Values, Value >,
'value'
> & {
value: string | undefined;
};
export type ConsumerInputProps< Values > = {
className?: string;
onChange?: (
value: ChangeEvent< HTMLInputElement > | Values[ keyof Values ]
) => void;
onBlur?: () => void;
[ key: string ]: unknown;
sanitize?: ( value: Values[ keyof Values ] ) => Values[ keyof Values ];
};
/** /**
* A form component to handle form state and provide input helper props. * A form component to handle form state and provide input helper props.
*/ */
@ -268,21 +304,19 @@ function FormComponent< Values extends Record< string, any > >(
}; };
function getInputProps< Value = Values[ keyof Values ] >( function getInputProps< Value = Values[ keyof Values ] >(
name: string name: string,
): { inputProps: ConsumerInputProps< Values > = {}
value: Value; ): InputProps< Values, Value > {
checked: boolean;
selected?: boolean;
onChange: (
value: ChangeEvent< HTMLInputElement > | Values[ keyof Values ]
) => void;
onBlur: () => void;
className: string | undefined;
help: string | null | undefined;
} {
const inputValue = _get( values, name ); const inputValue = _get( values, name );
const isTouched = touched[ name ]; const isTouched = touched[ name ];
const inputError = _get( errors, name ); const inputError = _get( errors, name );
const {
className: classNameProp,
onBlur: onBlurProp,
onChange: onChangeProp,
sanitize,
...additionalProps
} = inputProps;
return { return {
value: inputValue, value: inputValue,
@ -290,10 +324,50 @@ function FormComponent< Values extends Record< string, any > >(
selected: inputValue, selected: inputValue,
onChange: ( onChange: (
value: ChangeEvent< HTMLInputElement > | Values[ keyof Values ] value: ChangeEvent< HTMLInputElement > | Values[ keyof Values ]
) => handleChange( name, value ), ) => {
onBlur: () => handleBlur( name ), handleChange( name, value );
className: isTouched && inputError ? 'has-error' : undefined, if ( onChangeProp ) {
onChangeProp( value );
}
},
onBlur: () => {
if ( sanitize ) {
handleChange( name, sanitize( inputValue ) );
}
handleBlur( name );
if ( onBlurProp ) {
onBlurProp();
}
},
className: classnames( classNameProp, {
'has-error': isTouched && inputError,
} ),
help: isTouched ? ( inputError as string ) : null, help: isTouched ? ( inputError as string ) : null,
...additionalProps,
};
}
function getCheckboxControlProps< Value = Values[ keyof Values ] >(
name: string,
inputProps: ConsumerInputProps< Values > = {}
): CheckboxProps< Values, Value > {
return _omit( getInputProps( name, inputProps ), [
'selected',
'value',
] );
}
function getSelectControlProps< Value = Values[ keyof Values ] >(
name: string,
inputProps: ConsumerInputProps< Values > = {}
): SelectControlProps< Values, Value > {
const selectControlProps = getInputProps( name, inputProps );
return {
...selectControlProps,
value:
selectControlProps.value === undefined
? undefined
: String( selectControlProps.value ),
}; };
} }
@ -312,7 +386,9 @@ function FormComponent< Values extends Record< string, any > >(
setValue, setValue,
setValues, setValues,
handleSubmit, handleSubmit,
getCheckboxControlProps,
getInputProps, getInputProps,
getSelectControlProps,
isValidForm: ! Object.keys( errors ).length, isValidForm: ! Object.keys( errors ).length,
resetForm, resetForm,
}; };

View File

@ -9,7 +9,8 @@ import {
TextControl, TextControl,
} from '@wordpress/components'; } from '@wordpress/components';
import { useState } from '@wordpress/element'; import { useState } from '@wordpress/element';
import { Form } from '@woocommerce/components'; import { Form, DateTimePickerControl } from '@woocommerce/components';
import moment from 'moment';
const validate = ( values ) => { const validate = ( values ) => {
const errors = {}; const errors = {};
@ -19,6 +20,9 @@ const validate = ( values ) => {
if ( values.lastName.length < 3 ) { if ( values.lastName.length < 3 ) {
errors.lastName = 'Last name must be at least 3 characters'; errors.lastName = 'Last name must be at least 3 characters';
} }
if ( ! moment( values.date, moment.ISO_8601, true ).isValid() ) {
errors.date = 'Invalid date';
}
return errors; return errors;
}; };
@ -28,6 +32,7 @@ const initialValues = {
firstName: '', firstName: '',
lastName: '', lastName: '',
select: '3', select: '3',
date: '2014-10-24T13:02',
checkbox: true, checkbox: true,
radio: 'one', radio: 'one',
}; };
@ -68,6 +73,13 @@ export const Basic = () => {
] } ] }
{ ...getInputProps( 'select' ) } { ...getInputProps( 'select' ) }
/> />
<DateTimePickerControl
label="Date"
dateTimeFormat="YYYY-MM-DD HH:mm"
placeholder="Enter a date"
currentDate={ values.date }
{ ...getInputProps( 'date' ) }
/>
<CheckboxControl <CheckboxControl
label="Checkbox" label="Checkbox"
{ ...getInputProps( 'checkbox' ) } { ...getInputProps( 'checkbox' ) }

View File

@ -11,6 +11,7 @@ import { TextControl } from '@wordpress/components';
*/ */
import { Form, useFormContext } from '../'; import { Form, useFormContext } from '../';
import type { FormContext } from '../'; import type { FormContext } from '../';
import { DateTimePickerControl } from '../../date-time-picker-control';
const TestInputWithContext = () => { const TestInputWithContext = () => {
const formProps = useFormContext< { foo: string } >(); const formProps = useFormContext< { foo: string } >();
@ -407,6 +408,68 @@ describe( 'Form', () => {
); );
} ); } );
// We need to bump up the timeout for this test because:
// 1. userEvent.type() is slow (see https://github.com/testing-library/user-event/issues/577)
// 2. moment.js is slow
// Otherwise, the following error can occur on slow machines (such as our CI), because Jest times out and starts
// tearing down the component while test microtasks are still being executed
// (see https://github.com/facebook/jest/issues/12670)
// TypeError: Cannot read properties of null (reading 'createEvent')
it( 'should provide props that automatically handle DateTimePickerControl changes', async () => {
const newDateTimeInputString = 'invalid input';
type TestData = { date: string };
const mockOnChange = jest.fn();
function validate(): Record< string, string > {
return { date: 'This is a bad date' };
}
const { container, queryByText } = render(
<Form< TestData > onChange={ mockOnChange } validate={ validate }>
{ ( { getInputProps, values }: FormContext< TestData > ) => {
return (
<DateTimePickerControl
label={ 'Date' }
onChangeDebounceWait={ 10 }
currentDate={ values.date }
{ ...getInputProps( 'date' ) }
/>
);
} }
</Form>
);
const controlRoot = container.querySelector(
'.woocommerce-date-time-picker-control'
);
const input = controlRoot?.querySelector( 'input' );
userEvent.type(
input!,
'{selectall}{backspace}' + newDateTimeInputString
);
fireEvent.blur( input! );
await waitFor(
() => {
expect( mockOnChange ).toHaveBeenLastCalledWith(
{ name: 'date', value: newDateTimeInputString },
{ date: newDateTimeInputString },
false
);
expect( controlRoot?.classList.contains( 'has-error' ) ).toBe(
true
);
expect(
queryByText( 'This is a bad date' )
).toBeInTheDocument();
},
{ timeout: 100 }
);
}, 10000 );
describe( 'FormContext', () => { describe( 'FormContext', () => {
it( 'should allow nested field to use useFormContext to set field value', async () => { it( 'should allow nested field to use useFormContext to set field value', async () => {
const mockOnChange = jest.fn(); const mockOnChange = jest.fn();

View File

@ -9,7 +9,7 @@ import { createElement, Fragment } from '@wordpress/element';
*/ */
import Pill from '../pill'; import Pill from '../pill';
import { SortableHandle, NonSortableItem } from '../sortable'; import { SortableHandle, NonSortableItem } from '../sortable';
import { ConditionalWrapper } from '../util/conditional-wrapper'; import { ConditionalWrapper } from '../conditional-wrapper';
export type ImageGalleryItemProps = { export type ImageGalleryItemProps = {
id?: string; id?: string;

View File

@ -4,6 +4,7 @@ export { default as AnimationSlider } from './animation-slider';
export { default as Chart } from './chart'; export { default as Chart } from './chart';
export { default as ChartPlaceholder } from './chart/placeholder'; export { default as ChartPlaceholder } from './chart/placeholder';
export { CompareButton, CompareFilter } from './compare-filter'; export { CompareButton, CompareFilter } from './compare-filter';
export { ConditionalWrapper as __experimentalConditionalWrapper } from './conditional-wrapper';
export { default as Date } from './date'; export { default as Date } from './date';
export { default as DateRangeFilterPicker } from './date-range-filter-picker'; export { default as DateRangeFilterPicker } from './date-range-filter-picker';
export { default as DateRange } from './calendar/date-range'; export { default as DateRange } from './calendar/date-range';
@ -49,7 +50,10 @@ export {
MenuItem as __experimentalSelectControlMenuItem, MenuItem as __experimentalSelectControlMenuItem,
MenuItemProps as __experimentalSelectControlMenuItemProps, MenuItemProps as __experimentalSelectControlMenuItemProps,
} from './experimental-select-control/menu-item'; } from './experimental-select-control/menu-item';
export { Menu as __experimentalSelectControlMenu } from './experimental-select-control/menu'; export {
Menu as __experimentalSelectControlMenu,
MenuSlot as __experimentalSelectControlMenuSlot,
} from './experimental-select-control/menu';
export { default as ScrollTo } from './scroll-to'; export { default as ScrollTo } from './scroll-to';
export { Sortable } from './sortable'; export { Sortable } from './sortable';
export { ListItem } from './list-item'; export { ListItem } from './list-item';
@ -71,11 +75,11 @@ export { default as Tag } from './tag';
export { default as TextControl } from './text-control'; export { default as TextControl } from './text-control';
export { default as TextControlWithAffixes } from './text-control-with-affixes'; export { default as TextControlWithAffixes } from './text-control-with-affixes';
export { default as Timeline } from './timeline'; export { default as Timeline } from './timeline';
export { Tooltip as __experimentalTooltip } from './tooltip';
export { default as ViewMoreList } from './view-more-list'; export { default as ViewMoreList } from './view-more-list';
export { default as WebPreview } from './web-preview'; export { default as WebPreview } from './web-preview';
export { Badge } from './badge'; export { Badge } from './badge';
export { DynamicForm } from './dynamic-form'; export { DynamicForm } from './dynamic-form';
export { EnrichedLabel } from './enriched-label';
export { default as TourKit } from './tour-kit'; export { default as TourKit } from './tour-kit';
export * as TourKitTypes from './tour-kit/types'; export * as TourKitTypes from './tour-kit/types';
export { CollapsibleContent } from './collapsible-content'; export { CollapsibleContent } from './collapsible-content';

View File

@ -3,8 +3,8 @@
*/ */
import { useSelect, useDispatch } from '@wordpress/data'; import { useSelect, useDispatch } from '@wordpress/data';
import { useInstanceId } from '@wordpress/compose'; import { useInstanceId } from '@wordpress/compose';
import { BlockInstance, createBlock } from '@wordpress/blocks';
import { createElement, useEffect } from '@wordpress/element'; import { createElement, useEffect } from '@wordpress/element';
import { createBlock } from '@wordpress/blocks';
import { import {
BlockList, BlockList,
ObserveTyping, ObserveTyping,
@ -15,37 +15,50 @@ import {
WritingFlow, WritingFlow,
} from '@wordpress/block-editor'; } from '@wordpress/block-editor';
export const EditorWritingFlow: React.VFC = () => { type EditorWritingFlowProps = {
blocks: BlockInstance[];
onChange: ( changes: BlockInstance[] ) => void;
placeholder?: string;
};
export const EditorWritingFlow = ( {
blocks,
onChange,
placeholder = '',
}: EditorWritingFlowProps ) => {
const instanceId = useInstanceId( EditorWritingFlow ); const instanceId = useInstanceId( EditorWritingFlow );
const firstBlock = blocks[ 0 ];
const isEmpty = ! blocks.length;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This action is available in the block editor data store. // @ts-ignore This action is available in the block editor data store.
const { insertBlock } = useDispatch( blockEditorStore ); const { insertBlock, selectBlock } = useDispatch( blockEditorStore );
const { selectedBlockClientIds } = useSelect( ( select ) => {
const { isEmpty } = useSelect( ( select ) => {
const blocks = select( 'core/block-editor' ).getBlocks();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This selector is available in the block editor data store. // @ts-ignore This selector is available in the block editor data store.
const { getSelectedBlockClientIds } = select( blockEditorStore ); const { getSelectedBlockClientIds } = select( blockEditorStore );
return { return {
isEmpty: blocks.length
? blocks.length <= 1 &&
blocks[ 0 ].attributes?.content?.trim() === ''
: true,
firstBlock: blocks[ 0 ],
selectedBlockClientIds: getSelectedBlockClientIds(), selectedBlockClientIds: getSelectedBlockClientIds(),
}; };
} ); } );
useEffect( () => {
if ( selectedBlockClientIds?.length || ! firstBlock ) {
return;
}
selectBlock( firstBlock.clientId );
}, [ firstBlock, selectedBlockClientIds ] );
useEffect( () => { useEffect( () => {
if ( isEmpty ) { if ( isEmpty ) {
const initialBlock = createBlock( 'core/paragraph', { const initialBlock = createBlock( 'core/paragraph', {
content: '', content: '',
placeholder,
} ); } );
insertBlock( initialBlock ); insertBlock( initialBlock );
onChange( [ initialBlock ] );
} }
}, [] ); }, [ isEmpty ] );
return ( return (
/* Gutenberg handles the keyboard events when focusing the content editable area. */ /* Gutenberg handles the keyboard events when focusing the content editable area. */
@ -60,8 +73,6 @@ export const EditorWritingFlow: React.VFC = () => {
<BlockTools> <BlockTools>
<WritingFlow> <WritingFlow>
<ObserveTyping> <ObserveTyping>
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
{ /* @ts-ignore This action is available in the block editor data store. */ }
<BlockList /> <BlockList />
</ObserveTyping> </ObserveTyping>
</WritingFlow> </WritingFlow>

View File

@ -1,18 +1,14 @@
/** /**
* External dependencies * External dependencies
*/ */
import { BaseControl, Popover, SlotFillProvider } from '@wordpress/components';
import { BlockEditorProvider } from '@wordpress/block-editor'; import { BlockEditorProvider } from '@wordpress/block-editor';
import { BlockInstance } from '@wordpress/blocks'; import { BlockInstance } from '@wordpress/blocks';
import { SlotFillProvider } from '@wordpress/components'; import { createElement, useEffect, useState, useRef } from '@wordpress/element';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import {
createElement,
useCallback,
useEffect,
useState,
useRef,
} from '@wordpress/element';
import React from 'react'; import React from 'react';
import { uploadMedia } from '@wordpress/media-utils';
import { useUser } from '@woocommerce/data';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet. // @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group // eslint-disable-next-line @woocommerce/dependency-group
@ -28,16 +24,20 @@ registerBlocks();
type RichTextEditorProps = { type RichTextEditorProps = {
blocks: BlockInstance[]; blocks: BlockInstance[];
label?: string;
onChange: ( changes: BlockInstance[] ) => void; onChange: ( changes: BlockInstance[] ) => void;
entryId?: string; entryId?: string;
placeholder?: string;
}; };
export const RichTextEditor: React.VFC< RichTextEditorProps > = ( { export const RichTextEditor: React.VFC< RichTextEditorProps > = ( {
blocks, blocks,
label,
onChange, onChange,
placeholder = '',
} ) => { } ) => {
const blocksRef = useRef( blocks ); const blocksRef = useRef( blocks );
const { currentUserCan } = useUser();
const [ , setRefresh ] = useState( 0 ); const [ , setRefresh ] = useState( 0 );
// If there is a props change we need to update the ref and force re-render. // If there is a props change we need to update the ref and force re-render.
@ -59,8 +59,29 @@ export const RichTextEditor: React.VFC< RichTextEditorProps > = ( {
forceRerender(); forceRerender();
}, 200 ); }, 200 );
const mediaUpload = currentUserCan( 'upload_files' )
? ( {
onError,
...rest
}: {
onError: ( message: string ) => void;
} ) => {
uploadMedia(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore The upload function passes the remaining required props.
{
onError: ( { message } ) => onError( message ),
...rest,
}
);
}
: undefined;
return ( return (
<div className="woocommerce-rich-text-editor"> <div className="woocommerce-rich-text-editor">
{ label && (
<BaseControl.VisualLabel>{ label }</BaseControl.VisualLabel>
) }
<SlotFillProvider> <SlotFillProvider>
<BlockEditorProvider <BlockEditorProvider
value={ blocksRef.current } value={ blocksRef.current }
@ -70,13 +91,19 @@ export const RichTextEditor: React.VFC< RichTextEditorProps > = ( {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore This property was recently added in the block editor data store. // @ts-ignore This property was recently added in the block editor data store.
__experimentalClearBlockSelection: false, __experimentalClearBlockSelection: false,
mediaUpload,
} } } }
onInput={ debounceChange } onInput={ debounceChange }
onChange={ debounceChange } onChange={ debounceChange }
> >
<ShortcutProvider> <ShortcutProvider>
<EditorWritingFlow /> <EditorWritingFlow
blocks={ blocksRef.current }
onChange={ onChange }
placeholder={ placeholder }
/>
</ShortcutProvider> </ShortcutProvider>
<Popover.Slot />
</BlockEditorProvider> </BlockEditorProvider>
</SlotFillProvider> </SlotFillProvider>
</div> </div>

View File

@ -1,27 +1,22 @@
$toolbar-height: 40px;
.woocommerce-rich-text-editor { .woocommerce-rich-text-editor {
border: 1px solid $gray-600; .woocommerce-rich-text-editor__writing-flow {
border-radius: 2px; border: 1px solid $gray-600;
background: $white; border-radius: 2px;
background: $white;
}
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
.block-editor-inserter {
display: none;
}
.block-editor-block-contextual-toolbar.is-fixed, .block-editor-block-contextual-toolbar.is-fixed,
.block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar .components-toolbar-group, .block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar .components-toolbar-group,
.block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar .components-toolbar { .block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar .components-toolbar {
border-color: $gray-600; border-color: $gray-600;
} }
/* Hide rich text placeholder text */
.rich-text [data-rich-text-placeholder] {
display: none !important;
}
/* hide block boundary background styling */ /* hide block boundary background styling */
.rich-text:focus *[data-rich-text-format-boundary] { .rich-text:focus *[data-rich-text-format-boundary] {
background: none !important; background: none !important;
@ -37,11 +32,6 @@
outline: none; outline: none;
} }
.block-editor-block-list__empty-block-inserter,
.block-editor-block-list__insertion-point {
display: none !important;
}
.block-editor-writing-flow { .block-editor-writing-flow {
padding: $gap-small; padding: $gap-small;
} }
@ -54,11 +44,25 @@
} }
.components-accessible-toolbar { .components-accessible-toolbar {
height: $toolbar-height;
width: 100%; width: 100%;
background-color: $white; background-color: $white;
border-color: $gray-700; border-color: $gray-700;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
.components-button {
height: $toolbar-height;
}
}
.block-editor-block-mover:not(.is-horizontal) .block-editor-block-mover__move-button-container > * {
height: calc( $toolbar-height / 2 );
}
.block-editor-block-contextual-toolbar.is-fixed,
.components-toolbar-group {
min-height: $toolbar-height;
} }
.wp-block-quote { .wp-block-quote {

View File

@ -14,6 +14,8 @@ export const HEADING_BLOCK_ID = 'core/heading';
export const LIST_BLOCK_ID = 'core/list'; export const LIST_BLOCK_ID = 'core/list';
export const LIST_ITEM_BLOCK_ID = 'core/list-item'; export const LIST_ITEM_BLOCK_ID = 'core/list-item';
export const QUOTE_BLOCK_ID = 'core/quote'; export const QUOTE_BLOCK_ID = 'core/quote';
export const IMAGE_BLOCK_ID = 'core/image';
export const VIDEO_BLOCK_ID = 'core/video';
const ALLOWED_CORE_BLOCKS = [ const ALLOWED_CORE_BLOCKS = [
PARAGRAPH_BLOCK_ID, PARAGRAPH_BLOCK_ID,
@ -21,6 +23,8 @@ const ALLOWED_CORE_BLOCKS = [
LIST_BLOCK_ID, LIST_BLOCK_ID,
LIST_ITEM_BLOCK_ID, LIST_ITEM_BLOCK_ID,
QUOTE_BLOCK_ID, QUOTE_BLOCK_ID,
IMAGE_BLOCK_ID,
VIDEO_BLOCK_ID,
]; ];
const registerCoreBlocks = () => { const registerCoreBlocks = () => {

View File

@ -46,11 +46,11 @@
@import 'tag/style.scss'; @import 'tag/style.scss';
@import 'text-control/style.scss'; @import 'text-control/style.scss';
@import 'text-control-with-affixes/style.scss'; @import 'text-control-with-affixes/style.scss';
@import 'tooltip/style.scss';
@import 'timeline/style.scss'; @import 'timeline/style.scss';
@import 'view-more-list/style.scss'; @import 'view-more-list/style.scss';
@import 'web-preview/style.scss'; @import 'web-preview/style.scss';
@import 'badge/style.scss'; @import 'badge/style.scss';
@import 'dynamic-form/style.scss'; @import 'dynamic-form/style.scss';
@import 'enriched-label/style.scss';
@import 'tour-kit/style.scss'; @import 'tour-kit/style.scss';
@import 'collapsible-content/style.scss'; @import 'collapsible-content/style.scss';

View File

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

View File

@ -0,0 +1,36 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { Icon, warning } from '@wordpress/icons';
import React from 'react';
/**
* Internal dependencies
*/
import { Tooltip } from '../';
export const Basic = () => {
return (
<Tooltip
text={
<>
This is a <strong>tooltip</strong>!
</>
}
/>
);
};
export const CustomIcon = () => {
return (
<Tooltip text="I'm a tooltip with a custom button icon">
<Icon icon={ warning } />
</Tooltip>
);
};
export default {
title: 'WooCommerce Admin/experimental/Tooltip',
component: Tooltip,
};

View File

@ -0,0 +1,14 @@
.woocommerce-tooltip {
display: inline-flex;
.woocommerce-tooltip__button {
height: auto;
}
&__text .components-popover__content {
font-size: $default-font-size;
padding: $gap;
width: max-content;
}
}

View File

@ -0,0 +1,83 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, Popover } from '@wordpress/components';
import { createElement, Fragment, useState } from '@wordpress/element';
import { FocusEvent, KeyboardEvent } from 'react';
import { Icon, help } from '@wordpress/icons';
type Position =
| 'top left'
| 'top right'
| 'top center'
| 'middle left'
| 'middle right'
| 'middle center'
| 'bottom left'
| 'bottom right'
| 'bottom center';
type TooltipProps = {
children?: JSX.Element | string;
position?: Position;
text: JSX.Element | string;
};
export const Tooltip: React.FC< TooltipProps > = ( {
children = <Icon icon={ help } />,
position = 'top center',
text,
} ) => {
const [ isPopoverVisible, setIsPopoverVisible ] = useState( false );
return (
<>
<div className="woocommerce-tooltip">
<Button
className="woocommerce-tooltip__button"
onKeyDown={ (
event: KeyboardEvent< HTMLButtonElement >
) => {
if ( event.key !== 'Enter' ) {
return;
}
setIsPopoverVisible( true );
} }
onClick={ () => setIsPopoverVisible( ! isPopoverVisible ) }
label={ __( 'Help', 'woocommerce' ) }
>
{ children }
</Button>
{ isPopoverVisible && (
<Popover
focusOnMount="container"
position={ position }
className="woocommerce-tooltip__text"
onFocusOutside={ ( event: FocusEvent ) => {
if (
event.relatedTarget?.classList.contains(
'woocommerce-tooltip__button'
)
) {
return;
}
setIsPopoverVisible( false );
} }
onKeyDown={ (
event: KeyboardEvent< HTMLDivElement >
) => {
if ( event.key !== 'Escape' ) {
return;
}
setIsPopoverVisible( false );
} }
>
{ text }
</Popover>
) }
</div>
</>
);
};

View File

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

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update product attribute type name and export the product attribute types.

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