Merge branch 'feature/34548-multichannel-marketing-backend' into feature/34903-multichannel-marketing-frontend/main

This commit is contained in:
Gan Eng Chin 2023-01-18 17:34:08 +08:00
commit 4ae08a43b4
No known key found for this signature in database
GPG Key ID: 94D5D972860ADB01
205 changed files with 3610 additions and 1568 deletions

View File

@ -19,6 +19,7 @@ jobs:
permissions:
contents: read
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v3

View File

@ -1,7 +1,6 @@
name: Run tests against PR in an environment with COT enabled
on:
pull_request:
types: [labeled]
workflow_dispatch:
concurrency:
@ -13,7 +12,6 @@ permissions: {}
jobs:
cot-e2e-tests-run:
name: Runs E2E tests with COT enabled.
if: "${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'focus: custom order tables' }}"
runs-on: ubuntu-20.04
permissions:
contents: read
@ -70,7 +68,6 @@ jobs:
cot-api-tests-run:
name: Runs API tests with COT enabled.
if: "${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'focus: custom order tables' }}"
runs-on: ubuntu-20.04
permissions:
contents: read
@ -122,101 +119,101 @@ jobs:
if-no-files-found: ignore
retention-days: 5
test-summary:
name: Post test results
if: |
always() &&
! github.event.pull_request.head.repo.fork &&
(
contains( needs.*.result, 'success' ) ||
contains( needs.*.result, 'failure' )
)
runs-on: ubuntu-20.04
permissions:
contents: read
needs: [cot-api-tests-run, cot-e2e-tests-run]
steps:
- 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
- name: Download API test report artifact
uses: actions/download-artifact@v3
with:
name: api-test-report---pr-${{ github.event.number }}
path: artifacts/api
- name: Download Playwright E2E test report artifact
uses: actions/download-artifact@v3
with:
name: e2e-test-report---pr-${{ github.event.number }}
path: artifacts/e2e
- name: Prepare test summary
id: prepare-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
PR_NUMBER: ${{ github.event.number }}
SHA: ${{ github.event.pull_request.head.sha }}
with:
result-encoding: string
script: |
const script = require( './repo/.github/workflows/scripts/prepare-test-summary.js' )
return await script( { core } )
- name: Find PR comment by github-actions[bot]
uses: peter-evans/find-comment@v2
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: Test Results Summary
- name: Create or update PR comment
uses: peter-evans/create-or-update-comment@v2
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: ${{ steps.prepare-test-summary.outputs.result }}
edit-mode: replace
publish-test-reports:
name: Publish test reports
if: |
always() &&
! github.event.pull_request.head.repo.fork &&
(
contains( needs.*.result, 'success' ) ||
contains( needs.*.result, 'failure' )
)
runs-on: ubuntu-20.04
needs: [cot-api-tests-run, cot-e2e-tests-run]
env:
GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }}
PR_NUMBER: ${{ github.event.number }}
RUN_ID: ${{ github.run_id }}
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
steps:
- name: Publish test reports
env:
API_ARTIFACT: api-test-report---pr-${{ github.event.number }}
E2E_ARTIFACT: e2e-test-report---pr-${{ github.event.number }}
run: |
gh workflow run publish-test-reports-pr.yml \
-f run_id=$RUN_ID \
-f api_artifact=$API_ARTIFACT \
-f e2e_artifact=$E2E_ARTIFACT \
-f pr_number=$PR_NUMBER \
-f commit_sha=$COMMIT_SHA \
-f s3_root=public \
--repo woocommerce/woocommerce-test-reports
# test-summary:
# name: Post test results
# if: |
# always() &&
# ! github.event.pull_request.head.repo.fork &&
# (
# contains( needs.*.result, 'success' ) ||
# contains( needs.*.result, 'failure' )
# )
# runs-on: ubuntu-20.04
# permissions:
# contents: read
# needs: [cot-api-tests-run, cot-e2e-tests-run]
# steps:
# - 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
#
# - name: Download API test report artifact
# uses: actions/download-artifact@v3
# with:
# name: api-test-report---pr-${{ github.event.number }}
# path: artifacts/api
#
# - name: Download Playwright E2E test report artifact
# uses: actions/download-artifact@v3
# with:
# name: e2e-test-report---pr-${{ github.event.number }}
# path: artifacts/e2e
#
# - name: Prepare test summary
# id: prepare-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
# PR_NUMBER: ${{ github.event.number }}
# SHA: ${{ github.event.pull_request.head.sha }}
# with:
# result-encoding: string
# script: |
# const script = require( './repo/.github/workflows/scripts/prepare-test-summary.js' )
# return await script( { core } )
#
# - name: Find PR comment by github-actions[bot]
# uses: peter-evans/find-comment@v2
# id: find-comment
# with:
# issue-number: ${{ github.event.pull_request.number }}
# comment-author: 'github-actions[bot]'
# body-includes: Test Results Summary
#
# - name: Create or update PR comment
# uses: peter-evans/create-or-update-comment@v2
# with:
# comment-id: ${{ steps.find-comment.outputs.comment-id }}
# issue-number: ${{ github.event.pull_request.number }}
# body: ${{ steps.prepare-test-summary.outputs.result }}
# edit-mode: replace
#
# publish-test-reports:
# name: Publish test reports
# if: |
# always() &&
# ! github.event.pull_request.head.repo.fork &&
# (
# contains( needs.*.result, 'success' ) ||
# contains( needs.*.result, 'failure' )
# )
# runs-on: ubuntu-20.04
# needs: [cot-api-tests-run, cot-e2e-tests-run]
# env:
# GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }}
# PR_NUMBER: ${{ github.event.number }}
# RUN_ID: ${{ github.run_id }}
# COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
# steps:
# - name: Publish test reports
# env:
# API_ARTIFACT: api-test-report---pr-${{ github.event.number }}
# E2E_ARTIFACT: e2e-test-report---pr-${{ github.event.number }}
# run: |
# gh workflow run publish-test-reports-pr.yml \
# -f run_id=$RUN_ID \
# -f api_artifact=$API_ARTIFACT \
# -f e2e_artifact=$E2E_ARTIFACT \
# -f pr_number=$PR_NUMBER \
# -f commit_sha=$COMMIT_SHA \
# -f s3_root=public \
# --repo woocommerce/woocommerce-test-reports

View File

@ -0,0 +1,12 @@
# Duplicate workflow that returns success for this check when there is no relevant file change. See https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
name: Run tests against PR
on:
pull_request:
paths:
- '!**'
- '**/changelog/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: 'echo "No build required"'

View File

@ -1,7 +1,9 @@
name: Run tests against PR
on:
pull_request:
workflow_dispatch:
pull_request:
paths-ignore:
- '**/changelog/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@ -14,7 +16,7 @@ jobs:
name: Runs E2E tests.
runs-on: ubuntu-20.04
permissions:
contents: read
contents: read
env:
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-results
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-report
@ -84,7 +86,7 @@ jobs:
name: Runs API tests.
runs-on: ubuntu-20.04
permissions:
contents: read
contents: read
env:
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-results
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-report
@ -136,7 +138,7 @@ jobs:
name: Runs k6 Performance tests
runs-on: ubuntu-20.04
permissions:
contents: read
contents: read
steps:
- uses: actions/checkout@v3
@ -171,9 +173,9 @@ jobs:
runs-on: ubuntu-20.04
needs: [api-tests-run, e2e-tests-run]
permissions:
contents: read
issues: write
pull-requests: write
contents: read
issues: write
pull-requests: write
env:
E2E_GRAND_TOTAL: ${{needs.e2e-tests-run.outputs.E2E_GRAND_TOTAL}}
steps:

View File

@ -0,0 +1,12 @@
# Duplicate workflow that returns success for this check when there is no relevant file change. See https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
name: Build Live Branch
on:
pull_request:
paths:
- '!**'
- '**/changelog/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: 'echo "No build required"'

View File

@ -1,6 +1,8 @@
name: Build Live Branch
on:
pull_request:
paths-ignore:
- '**/changelog/**'
concurrency:
# Cancel concurrent jobs on pull_request but not push, by including the run_id in the concurrency group for the latter.
@ -14,7 +16,7 @@ jobs:
if: github.repository_owner == 'woocommerce'
runs-on: ubuntu-20.04
permissions:
contents: read
contents: read
steps:
- uses: actions/checkout@v3

View File

@ -0,0 +1,12 @@
# Duplicate workflow that returns success for this check when there is no relevant file change. See https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
name: Run code coverage on PR
on:
pull_request:
paths:
- '!**'
- '**/changelog/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: 'echo "No build required"'

View File

@ -1,59 +1,60 @@
name: Run code coverage on PR
on:
pull_request:
workflow_dispatch:
pull_request:
paths-ignore:
- '**/changelog/**'
workflow_dispatch:
defaults:
run:
shell: bash
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
run:
shell: bash
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
test:
name: Code coverage (PHP 7.4, WP Latest)
timeout-minutes: 30
runs-on: ubuntu-20.04
permissions:
contents: read
services:
database:
image: mysql:5.6
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 100
test:
name: Code coverage (PHP 7.4, WP Latest)
timeout-minutes: 30
runs-on: ubuntu-20.04
permissions:
contents: read
services:
database:
image: mysql:5.6
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 100
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
- name: Tool versions
run: |
php --version
composer --version
- name: Tool versions
run: |
php --version
composer --version
- name: Build Admin feature config
working-directory: plugins/woocommerce
run:
pnpm run build:feature-config
- name: Build Admin feature config
working-directory: plugins/woocommerce
run: pnpm run build:feature-config
- name: Init DB and WP
working-directory: plugins/woocommerce
run: bash tests/bin/install.sh woo_test root root 127.0.0.1 latest
- name: Init DB and WP
working-directory: plugins/woocommerce
run: bash tests/bin/install.sh woo_test root root 127.0.0.1 latest
- name: Run unit tests with code coverage. Allow to fail.
working-directory: plugins/woocommerce
run: |
RUN_CODE_COVERAGE=1 bash tests/bin/phpunit.sh
exit 0
- name: Send code coverage to Codecov.
run: |
bash <(curl -s https://codecov.io/bash)
- name: Run unit tests with code coverage. Allow to fail.
working-directory: plugins/woocommerce
run: |
RUN_CODE_COVERAGE=1 bash tests/bin/phpunit.sh
exit 0
- name: Send code coverage to Codecov.
run: |
bash <(curl -s https://codecov.io/bash)

View File

@ -0,0 +1,12 @@
# Duplicate workflow that returns success for this check when there is no relevant file change. See https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
name: Run code coverage on PR
on:
pull_request:
paths:
- '!**'
- '**/changelog/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: 'echo "No build required"'

View File

@ -1,5 +1,8 @@
name: Run code sniff on PR
on: pull_request
on:
pull_request:
paths-ignore:
- '**/changelog/**'
defaults:
run:
shell: bash
@ -17,7 +20,7 @@ jobs:
timeout-minutes: 15
runs-on: ubuntu-20.04
permissions:
contents: read
contents: read
steps:
- uses: actions/checkout@v3
with:

View File

@ -0,0 +1,12 @@
# Duplicate workflow that returns success for this check when there is no relevant file change. See https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
name: Highlight templates changes
on:
pull_request:
paths:
- '!**'
- '**/changelog/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: 'echo "No build required"'

View File

@ -1,5 +1,8 @@
name: Highlight templates changes
on: pull_request
on:
pull_request:
paths-ignore:
- '**/changelog/**'
permissions: {}
@ -8,7 +11,7 @@ jobs:
name: Check pull request changes to highlight
runs-on: ubuntu-20.04
permissions:
contents: read
contents: read
outputs:
results: ${{ steps.results.outputs.results }}
steps:
@ -29,32 +32,32 @@ jobs:
- name: Check results
uses: actions/github-script@v6
with:
script: |
const template = '${{ steps.run.outputs.templates }}';
script: |
const template = '${{ steps.run.outputs.templates }}';
if ( template === '' ) {
return;
}
if ( template === '' ) {
return;
}
const templateArr = template.split( '\n' );
const modTemplateArr = [];
let needsVersionBump = false;
const templateArr = template.split( '\n' );
const modTemplateArr = [];
let needsVersionBump = false;
templateArr.forEach( ( el ) => {
if ( el.match( /NOTICE/ ) ) {
modTemplateArr.pop();
return;
}
templateArr.forEach( ( el ) => {
if ( el.match( /NOTICE/ ) ) {
modTemplateArr.pop();
return;
}
if ( el.match( /WARNING/ ) ) {
needsVersionBump = true;
}
if ( el.match( /WARNING/ ) ) {
needsVersionBump = true;
}
modTemplateArr.push( el );
} );
modTemplateArr.push( el );
} );
const templateResult = modTemplateArr.join( '\n' );
const templateResult = modTemplateArr.join( '\n' );
if ( needsVersionBump ) {
core.setFailed( `Templates have changed but template versions were not bumped:\n${ templateResult }` );
}
if ( needsVersionBump ) {
core.setFailed( `Templates have changed but template versions were not bumped:\n${ templateResult }` );
}

View File

@ -1,37 +1,37 @@
name: Run lint checks potentially affecting projects across the monorepo
on:
pull_request:
branches:
- 'trunk'
pull_request:
branches:
- 'trunk'
concurrency:
group: changelogger-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
group: changelogger-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
changelogger_used:
name: Changelogger use
runs-on: ubuntu-20.04
permissions:
contents: read
timeout-minutes: 15
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
changelogger_used:
name: Changelogger use
runs-on: ubuntu-20.04
permissions:
contents: read
timeout-minutes: 15
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
build: false
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
build: false
- name: Check change files are touched for touched projects
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: php tools/monorepo/check-changelogger-use.php --debug "$BASE" "$HEAD"
- name: Check change files are touched for touched projects
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: php tools/monorepo/check-changelogger-use.php --debug "$BASE" "$HEAD"
- name: Run changelog validation
run: pnpm run -r changelog validate
- name: Run changelog validation
run: pnpm run -r changelog validate

View File

@ -1,6 +1,9 @@
name: Lint and tests for JS packages and woocommerce-admin/client
on: pull_request
on:
pull_request:
paths-ignore:
- '**/changelog/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@ -12,15 +15,15 @@ jobs:
name: Lint and Test JS
runs-on: ubuntu-20.04
permissions:
contents: read
contents: read
steps:
- uses: actions/checkout@v3
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
- name: Lint
run: pnpm run -r --filter='woocommerce/client/admin...' --filter='!@woocommerce/e2e*' --filter='!@woocommerce/api' --color lint
- name: Test
run: pnpm run test --filter='woocommerce/client/admin...' --filter='!@woocommerce/e2e*' --filter='!@woocommerce/api' --color
run: pnpm run test --filter='woocommerce/client/admin...' --filter='!@woocommerce/e2e*' --filter='!@woocommerce/api' --color

12
.github/workflows/pr-lint-test-skip.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# Duplicate workflow that returns success for this check when there is no relevant file change. See https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
name: Run smoke tests against pull request.
on:
pull_request:
paths:
- '!**'
- '**/changelog/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: 'echo "No build required"'

View File

@ -1,23 +1,23 @@
name: 'Label Pull Request Project'
on:
pull_request_target:
types:
- opened
- synchronize
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
pull_request_target:
types:
- opened
- synchronize
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
label_project:
runs-on: ubuntu-20.04
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/labeler@v3
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/project-pr-labeler.yml
label_project:
runs-on: ubuntu-20.04
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/labeler@v3
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
configuration-path: .github/project-pr-labeler.yml

View File

@ -0,0 +1,12 @@
# Duplicate workflow that returns success for this check when there is no relevant file change. See https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
name: Lint and tests for JS packages and woocommerce-admin/client
on:
pull_request:
paths:
- '!**'
- '**/changelog/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: 'echo "No build required"'

View File

@ -1,13 +1,15 @@
name: Run smoke tests against pull request.
on:
pull_request:
paths-ignore:
- '**/changelog/**'
branches:
- trunk
types:
- labeled
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
@ -17,8 +19,8 @@ jobs:
if: "${{ contains(github.event.label.name, 'run: smoke tests') }}"
runs-on: ubuntu-20.04
permissions:
contents: read
pull-requests: write
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v3

View File

@ -0,0 +1,12 @@
# Duplicate workflow that returns success for this check when there is no relevant file change. See https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
name: Run unit tests on PR
on:
pull_request:
paths:
- '!**'
- '**/changelog/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: 'echo "No build required"'

View File

@ -1,12 +1,14 @@
name: Run unit tests on PR
on:
pull_request
on:
pull_request:
paths-ignore:
- '**/changelog/**'
defaults:
run:
shell: bash
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
run:
shell: bash
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
@ -26,9 +28,9 @@ jobs:
include:
- wp: nightly
php: '7.4'
- wp: '5.9'
- wp: '6.0'
php: 7.4
- wp: '5.8'
- wp: '5.9'
php: 7.4
services:
database:
@ -44,25 +46,12 @@ jobs:
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
php-version: ${{ matrix.php }}
php-version: ${{ matrix.php }}
- name: Tool versions
run: |
php --version
composer --version
- name: Add PHP8 Compatibility.
run: |
if [ "$(php -r "echo version_compare(PHP_VERSION,'8.0','>=');")" ]; then
cd plugins/woocommerce
curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip
unzip -d /tmp/phpunit-7.5-fork /tmp/phpunit-7.5-fork.zip
composer bin phpunit config --unset platform
composer bin phpunit config repositories.0 '{"type": "path", "url": "/tmp/phpunit-7.5-fork/phpunit-add-compatibility-with-php8-to-phpunit-7", "options": {"symlink": false}}'
composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-reqs
rm -rf ./vendor/phpunit/
composer dump-autoload
fi
php --version
composer --version
- name: Init DB and WP
working-directory: plugins/woocommerce

View File

@ -49,7 +49,10 @@ This repository is not suitable for support. Please don't use our issue tracker
* [The Official WooCommerce Facebook Group](https://www.facebook.com/groups/advanced.woocommerce).
* For customizations, you may want to check our list of [WooExperts](https://woocommerce.com/experts/) or [Codeable](https://codeable.io/).
Support requests in issues on this repository will be closed on sight.
NOTE: Unfortunately, we are unable to honor support requests in issues on this repository; as a result, any requests submitted in this manner will be closed.
## Community
For peer to peer support, real-time announcements, and office hours, please [join our slack community](https://woocommerce.com/community-slack/)!
## Contributing to WooCommerce
If you have a patch or have stumbled upon an issue with WooCommerce core, you can contribute this back to the code. Please read our [contributor guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md) for more information on how you can do this.

View File

@ -1,5 +1,110 @@
== Changelog ==
= 7.3.0 2023-01-10 =
**WooCommerce**
* Fix - Remove redundant Pinterest plugin from marketing task [#36158](https://github.com/woocommerce/woocommerce/pull/36158)
* Fix - Corrects a hard-coded reference to the WP post meta table within the HPOS Migration Helper, that would fail on some sites. [#36100](https://github.com/woocommerce/woocommerce/pull/36100)
* Fix - Add a blank space between the emoji and the message within a notice popup [#35698](https://github.com/woocommerce/woocommerce/pull/35698)
* Fix - Add a data migration for changed New Zealand and Ukraine state codes [#35960](https://github.com/woocommerce/woocommerce/pull/35960)
* Fix - Add back missing scss files from assets. [#35624](https://github.com/woocommerce/woocommerce/pull/35624)
* Fix - Address HPOS synchronization issues relating to the deletion of orders. [#35723](https://github.com/woocommerce/woocommerce/pull/35723)
* Fix - Avoid a potential fatal error when forming edit-order URLs. [#35995](https://github.com/woocommerce/woocommerce/pull/35995)
* Fix - Fix call of array_key_exists in SSR. [#35598](https://github.com/woocommerce/woocommerce/pull/35598)
* Fix - Fix ellipsis dropdown menu is mostly hidden within the task list [#35949](https://github.com/woocommerce/woocommerce/pull/35949)
* Fix - Fixes fatal error resulting from translating the WooCommerce main menu. [#35695](https://github.com/woocommerce/woocommerce/pull/35695)
* Fix - Fix get orders REST API endpoint when using 'search' or 'parent' and HPOS is enabled [#35818](https://github.com/woocommerce/woocommerce/pull/35818)
* Fix - Fix handling of non-ASCII product attributes when the attributes lookup table is in use [#34432](https://github.com/woocommerce/woocommerce/pull/34432)
* Fix - Fix handling of statuses in orders list table (HPOS). [#35370](https://github.com/woocommerce/woocommerce/pull/35370)
* Fix - Fix logo icon for Google Listings and Ads. [#35732](https://github.com/woocommerce/woocommerce/pull/35732)
* Fix - Fix product tab to be shown on production build [#35976](https://github.com/woocommerce/woocommerce/pull/35976)
* Fix - Fix regexp used for filtering country dropdown on the store details step #35941 [#35942](https://github.com/woocommerce/woocommerce/pull/35942)
* Fix - Fix the gap in the featured product checkbox [#35710](https://github.com/woocommerce/woocommerce/pull/35710)
* Fix - Fix tooltips not appearing in the orders list admin page. [#35638](https://github.com/woocommerce/woocommerce/pull/35638)
* Fix - Fix unread note count on inbox panel [#35396](https://github.com/woocommerce/woocommerce/pull/35396)
* Fix - Fix unsaved modal propmt to not be shown during form submission [#35657](https://github.com/woocommerce/woocommerce/pull/35657)
* Fix - Fix version in template and function docblocks. [#35473](https://github.com/woocommerce/woocommerce/pull/35473)
* Fix - Fix WooCommerce Admin client React build warnings and remove unnecessary scss imports. [#35930](https://github.com/woocommerce/woocommerce/pull/35930)
* Fix - Fix wrong query param in onboarding product api call [#35926](https://github.com/woocommerce/woocommerce/pull/35926)
* Fix - If order types have not been registered, do not attempt to count orders. [#35820](https://github.com/woocommerce/woocommerce/pull/35820)
* Fix - Make the 'unprotected upload directory' notice dismissable. [#33544](https://github.com/woocommerce/woocommerce/pull/33544)
* Fix - Update Playwright to 1.28.0 and explicitly set PHP version in GH action [#35679](https://github.com/woocommerce/woocommerce/pull/35679)
* Fix - When importing product CSV, ensure line breaks within header columns do not break the import process. [#35880](https://github.com/woocommerce/woocommerce/pull/35880)
* Add - Add CES exit prompt for setting pages, when tracking is enabled. [#35761](https://github.com/woocommerce/woocommerce/pull/35761)
* Add - Add CES feedback functionality to the share feedback button within the Product MVP. [#35690](https://github.com/woocommerce/woocommerce/pull/35690)
* Add - Add Denmark postcode validation. [#35653](https://github.com/woocommerce/woocommerce/pull/35653)
* Add - Add exit prompt logic to get feedback if users leave product pages without saving when tracking is enabled. [#35728](https://github.com/woocommerce/woocommerce/pull/35728)
* Add - Add FormFileUpload component [#35358](https://github.com/woocommerce/woocommerce/pull/35358)
* Add - Add HPOS information to WC Tracker. [#35446](https://github.com/woocommerce/woocommerce/pull/35446)
* Add - Add new option to create new attribute within add attribute modal. [#35100](https://github.com/woocommerce/woocommerce/pull/35100)
* Add - Add new product management breadcrumbs to header [#35596](https://github.com/woocommerce/woocommerce/pull/35596)
* Add - Add new Product MVP CES footer for gathering feedback on the new product management screen. [#35652](https://github.com/woocommerce/woocommerce/pull/35652)
* Add - Add one-click installation to recommended extensions in Marketing page. [#35542](https://github.com/woocommerce/woocommerce/pull/35542)
* Add - Add pagination to variations list [#35979](https://github.com/woocommerce/woocommerce/pull/35979)
* Add - Add product settings menu in header [#35592](https://github.com/woocommerce/woocommerce/pull/35592)
* Add - Add product tab headers and move sections to respective tabs [#35862](https://github.com/woocommerce/woocommerce/pull/35862)
* Add - Add product variations list to new product management experience [#35889](https://github.com/woocommerce/woocommerce/pull/35889)
* Add - Add support for custom order types in HPOS admin UI. [#35658](https://github.com/woocommerce/woocommerce/pull/35658)
* Add - Add the woocommerce_order_applied_coupon hook [#35616](https://github.com/woocommerce/woocommerce/pull/35616)
* Add - Add tracks events for 'product_view_product_click' and 'product_view_product_dismiss' [#35582](https://github.com/woocommerce/woocommerce/pull/35582)
* Add - Introduces action `woocommerce_order_list_table_restrict_manage_orders` as an equivalent of the legacy `restrict_manage_posts` hook. [#36000](https://github.com/woocommerce/woocommerce/pull/36000)
* Add - Open categories menu list when the user focus the category field [#35606](https://github.com/woocommerce/woocommerce/pull/35606)
* Update - Match country name or ' - region' when filtering country select control #36120 [#36159](https://github.com/woocommerce/woocommerce/pull/36159)
* Update - Update WooCommerce Blocks to 9.1.3 [#36125](https://github.com/woocommerce/woocommerce/pull/36125)
* Update - Adapt the summary text in the product management form. [#35717](https://github.com/woocommerce/woocommerce/pull/35717)
* Update - Add Codisto for WooCommerce to free extensions list [#36009](https://github.com/woocommerce/woocommerce/pull/36009)
* Update - Add experimental open menu on focus option to the attribute and attribute term input fields. [#35758](https://github.com/woocommerce/woocommerce/pull/35758)
* Update - Add missing tracks events [#35262](https://github.com/woocommerce/woocommerce/pull/35262)
* Update - Add Pinterest for WooCommerce to free extensions list [#36003](https://github.com/woocommerce/woocommerce/pull/36003)
* Update - Automatically create the custom order tables if the corresponding feature is enabled [#35357](https://github.com/woocommerce/woocommerce/pull/35357)
* Update - Disable TikTok in the OBW [#35924](https://github.com/woocommerce/woocommerce/pull/35924)
* Update - Include taxes migration in MigrationHelper::migrate_country_states [#35967](https://github.com/woocommerce/woocommerce/pull/35967)
* Update - Increase consistency in relation to the way taxonomy term ordering is persisted. [#34645](https://github.com/woocommerce/woocommerce/pull/34645)
* Update - Make product form header and actions responsive for smaller viewports [#35623](https://github.com/woocommerce/woocommerce/pull/35623)
* Update - Remove welcome to woocommerce store note [#35342](https://github.com/woocommerce/woocommerce/pull/35342)
* Update - Surface Amazon Pay as "Additional Payment Options" for UK/EU/JP [#35726](https://github.com/woocommerce/woocommerce/pull/35726)
* Update - Update api-core-tests readme to reference correct directory for .env file [#35759](https://github.com/woocommerce/woocommerce/pull/35759)
* Update - Update country data in api-core-tests to prevent numerous test data updates [#35557](https://github.com/woocommerce/woocommerce/pull/35557)
* Update - update FAQ in readme consumed by .org [#35696](https://github.com/woocommerce/woocommerce/pull/35696)
* Update - Update WooCommerce Blocks to 9.1.1 [#36004](https://github.com/woocommerce/woocommerce/pull/36004)
* Update - Update wording for In-App Marketplace tour. [#35929](https://github.com/woocommerce/woocommerce/pull/35929)
* Update - Updating all CES events to support two questions in modal. [#35680](https://github.com/woocommerce/woocommerce/pull/35680)
* Dev - Allow the user to select multiple images in the Media Library [#35722](https://github.com/woocommerce/woocommerce/pull/35722)
* Dev - Check if blocks have been added to rich text editors before updating value [#35626](https://github.com/woocommerce/woocommerce/pull/35626)
* Dev - Make e2e tests compatible with nightly and release smoke test sites. [#35492](https://github.com/woocommerce/woocommerce/pull/35492)
* Dev - Move file picker by clicking card into the MediaUploader component [#35738](https://github.com/woocommerce/woocommerce/pull/35738)
* Dev - Update the "can manually add a variation" E2E test to prevent automatic creation of variations from all attributes. [#36008](https://github.com/woocommerce/woocommerce/pull/36008)
* Tweak - Avoid deprecation notices under PHP 8.1 when calling wp_parse_url(). [#35648](https://github.com/woocommerce/woocommerce/pull/35648)
* Tweak - Correct the usage of 'address' and 'addresses' within `wc_get_account_menu_items()`. [#32026](https://github.com/woocommerce/woocommerce/pull/32026)
* Tweak - Create ProductForm component to merge similar structures between AddProductPage and EditProductPage [#35783](https://github.com/woocommerce/woocommerce/pull/35783)
* Tweak - Improves efficiency of code responsible for determining plugin IDs (during feature compatibility checks). [#35727](https://github.com/woocommerce/woocommerce/pull/35727)
* Tweak - Make the formatted shipping address available via the `woocommerce_cart_no_shipping_available_html` hook. [#30723](https://github.com/woocommerce/woocommerce/pull/30723)
* Tweak - Make the OrdersTableDataStore init_order_record() and get_order_data_for_ids() functions protected rather than private [#35829](https://github.com/woocommerce/woocommerce/pull/35829)
* Tweak - Move CSS about notice outside of .woocommerce class scope [#35912](https://github.com/woocommerce/woocommerce/pull/35912)
* Tweak - Resolve an error in the product tracking code by testing to see if the `post_type` query var is set before checking its value. [#34501](https://github.com/woocommerce/woocommerce/pull/34501)
* Tweak - Simplify wording within the customer emails for on-hold orders. [#31886](https://github.com/woocommerce/woocommerce/pull/31886)
* Tweak - WooCommerce has now been tested up to WordPress 6.1.x. [#35985](https://github.com/woocommerce/woocommerce/pull/35985)
* Performance - Split CALC_FOUND_ROW query into seperate count query for better performance. [#35468](https://github.com/woocommerce/woocommerce/pull/35468)
* Enhancement - Add a bottom padding to the whole form [#35721](https://github.com/woocommerce/woocommerce/pull/35721)
* Enhancement - Add a confirmation modal when the user tries to navigate away with unsaved changes [#35625](https://github.com/woocommerce/woocommerce/pull/35625)
* Enhancement - Add a default placeholder title for newly added attributes and always show remove button for attributes [#35904](https://github.com/woocommerce/woocommerce/pull/35904)
* Enhancement - Add help tip for Product Image and Product Gallery meta boxes [#35834](https://github.com/woocommerce/woocommerce/pull/35834)
* Enhancement - Add support for product attribute taxonomy template [#35617](https://github.com/woocommerce/woocommerce/pull/35617)
* Enhancement - Add warning banner that informs the user if they have conflicting tax display settings [#36010](https://github.com/woocommerce/woocommerce/pull/36010)
* Enhancement - Change the width of pricing fields, SKU and Shipping Class to 50% in big screens (>960px) in new product management experience [#35545](https://github.com/woocommerce/woocommerce/pull/35545)
* Enhancement - Fix price field currency symbol position [#35718](https://github.com/woocommerce/woocommerce/pull/35718)
* Enhancement - Improve element stacking in modals on tablet and mobile [#35733](https://github.com/woocommerce/woocommerce/pull/35733)
* Security - Customers REST API endpoint will now return user metadata only when requester has an administrator role [#36408](https://github.com/woocommerce/woocommerce/pull/36408).
= 7.2.3 2023-1-9
**WooCommerce Blocks 8.9.4**
* Fix - fatal error in WordPress 5.8 when creating a post or page. #7496
* Fix - hangs in the block editor with WordPress 5.8. #8095
* Fix - Filter by Attribute block crashing in the editor of WordPress 5.8. #8101
= 7.2.2 2022-12-21 =
** WooCommerce**

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding WooProductFieldItem slotfill.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding the WooProductSectionItem slotfill component.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add product field store and helper functions for rendering fields from config.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Update spelling of Cancelled to Canceled for US English.

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ export { default as FilterPicker } from './filter-picker';
export { H, Section } from './section';
export { ImageGallery, ImageGalleryItem } from './image-gallery';
export { default as ImageUpload } from './image-upload';
export { default as Link } from './link';
export { Link } from './link';
export { default as List } from './list';
export { MediaUploader } from './media-uploader';
export { default as MenuItem } from './ellipsis-menu/menu-item';
@ -84,3 +84,6 @@ export { DynamicForm } from './dynamic-form';
export { default as TourKit } from './tour-kit';
export * as TourKitTypes from './tour-kit/types';
export { CollapsibleContent } from './collapsible-content';
export { createOrderedChildren, sortFillsByOrder } from './utils';
export { WooProductFieldItem as __experimentalWooProductFieldItem } from './woo-product-field-item';
export { WooProductSectionItem as __experimentalWooProductSectionItem } from './woo-product-section-item';

View File

@ -1,77 +0,0 @@
/**
* External dependencies
*/
import PropTypes from 'prop-types';
import { partial } from 'lodash';
import { createElement } from '@wordpress/element';
import { getHistory } from '@woocommerce/navigation';
/**
* Use `Link` to create a link to another resource. It accepts a type to automatically
* create wp-admin links, wc-admin links, and external links.
*/
function Link( { children, href, type, ...props } ) {
// @todo Investigate further if we can use <Link /> directly.
// With React Router 5+, <RouterLink /> cannot be used outside of the main <Router /> elements,
// which seems to include components imported from @woocommerce/components. For now, we can use the history object directly.
const wcAdminLinkHandler = ( onClick, event ) => {
// If cmd, ctrl, alt, or shift are used, use default behavior to allow opening in a new tab.
if (
event.ctrlKey ||
event.metaKey ||
event.altKey ||
event.shiftKey
) {
return;
}
event.preventDefault();
// If there is an onclick event, execute it.
const onClickResult = onClick ? onClick( event ) : true;
// Mimic browser behavior and only continue if onClickResult is not explicitly false.
if ( onClickResult === false ) {
return;
}
getHistory().push( event.target.closest( 'a' ).getAttribute( 'href' ) );
};
const passProps = {
...props,
'data-link-type': type,
};
if ( type === 'wc-admin' ) {
passProps.onClick = partial( wcAdminLinkHandler, passProps.onClick );
}
return (
<a href={ href } { ...passProps }>
{ children }
</a>
);
}
Link.propTypes = {
/**
* The resource to link to.
*/
href: PropTypes.string.isRequired,
/**
* Type of link. For wp-admin and wc-admin, the correct prefix is appended.
*/
type: PropTypes.oneOf( [ 'wp-admin', 'wc-admin', 'external' ] ).isRequired,
};
Link.defaultProps = {
type: 'wc-admin',
};
Link.contextTypes = {
router: PropTypes.object,
};
export default Link;

View File

@ -0,0 +1,94 @@
/**
* External dependencies
*/
import PropTypes from 'prop-types';
import { partial } from 'lodash';
import { createElement } from '@wordpress/element';
import { getHistory } from '@woocommerce/navigation';
import React from 'react';
interface LinkProps {
/** Type of link. For wp-admin and wc-admin, the correct prefix is appended. */
type?: 'wc-admin' | 'wp-admin' | 'external';
/** The resource to link to. */
href: string;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type LinkOnClickHandlerFunction = ( ...props: any ) => any; // we don't want to restrict this function at all
type LinkOnclickHandler = (
onClick: LinkOnClickHandlerFunction | undefined,
event: React.MouseEvent< Element > | undefined
) => void;
/**
* Use `Link` to create a link to another resource. It accepts a type to automatically
* create wp-admin links, wc-admin links, and external links.
*/
export const Link = ( {
href,
children,
type = 'wc-admin',
...props
}: React.AnchorHTMLAttributes< HTMLAnchorElement > &
LinkProps ): React.ReactElement => {
// ( { children, href, type, ...props } ) => {
// @todo Investigate further if we can use <Link /> directly.
// With React Router 5+, <RouterLink /> cannot be used outside of the main <Router /> elements,
// which seems to include components imported from @woocommerce/components. For now, we can use the history object directly.
const wcAdminLinkHandler: LinkOnclickHandler = ( onClick, event ) => {
// If cmd, ctrl, alt, or shift are used, use default behavior to allow opening in a new tab.
if (
event?.ctrlKey ||
event?.metaKey ||
event?.altKey ||
event?.shiftKey
) {
return;
}
event?.preventDefault();
// If there is an onclick event, execute it.
const onClickResult = onClick && event ? onClick( event ) : true;
// Mimic browser behavior and only continue if onClickResult is not explicitly false.
if ( onClickResult === false ) {
return;
}
if ( event?.target instanceof Element ) {
const closestEventTarget = event.target
.closest( 'a' )
?.getAttribute( 'href' );
if ( closestEventTarget ) {
getHistory().push( closestEventTarget );
} else {
// eslint-disable-next-line no-console
console.error(
'@woocommerce/components/link is trying to push an undefined state into navigation stack'
); // This shouldn't happen as we wrap with <a> below
}
}
};
const passProps = {
...props,
'data-link-type': type,
};
if ( type === 'wc-admin' ) {
passProps.onClick = partial( wcAdminLinkHandler, passProps.onClick );
}
return (
<a href={ href } { ...passProps }>
{ children }
</a>
);
};
Link.contextTypes = {
router: PropTypes.object,
};
export default Link;

View File

@ -3,11 +3,12 @@
*/
import { withConsole } from '@storybook/addon-console';
import { createElement } from '@wordpress/element';
import React from 'react';
/**
* Internal dependencies
*/
import Link from '../';
import Link from '..';
function logLinkClick( event ) {
const a = event.currentTarget;

View File

@ -1,13 +1,13 @@
/**
* External dependencies
*/
import { fireEvent, render } from '@testing-library/react';
import { render, screen, fireEvent } from '@testing-library/react';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import Link from '../index';
import { Link } from '..';
describe( 'Link', () => {
it( 'should render `external` links', () => {
@ -112,7 +112,7 @@ describe( 'Link', () => {
return false;
} );
const { container } = render(
render(
<Link
href="https://woocommerce.com"
type="external"
@ -122,7 +122,9 @@ describe( 'Link', () => {
</Link>
);
fireEvent.click( container.firstChild );
const testLink = screen.getByText( 'WooCommerce.com' );
fireEvent.click( testLink );
expect( clickHandler ).toHaveBeenCalled();
} );

View File

@ -0,0 +1,46 @@
# Product Fields
Product Fields are used within the WooCommerce Admin product editor, for rendering new fields using PHP.
## Example
```js
// product-field.js
( function ( element ) {
const el = element.createElement;
registerProductField( 'number', {
name: 'number',
render: () => {
return <InputControl type="number" />;
},
} );
} )( window.wp.element );
```
## API
### registerProductField
Registers a new product field provided a unique name and an object defining its
behavior.
_Usage_
```js
import { __ } from '@wordpress/i18n';
import { registerProductField } from '@woocommerce/components';
registerProductField( 'number', {
name: 'number',
render: () => {
return <InputControl type="number" />;
},
} );
```
_Parameters_
- _fieldName_ `string`: Field name.
- _settings_ `Object`: Field settings.
- _render_ `ComponentType`: React functional component to be rendered.

View File

@ -0,0 +1,2 @@
export * from './registration';
export * from './render';

View File

@ -0,0 +1,45 @@
/**
* External dependencies
*/
import { select, dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { store as productFieldStore } from '../store';
import { ProductFieldDefinition } from '../store/types';
/**
* Registers a new product field provided a unique name and an object defining its
* behavior. Once registered, the field is made available to use with the product form API.
*
* @param {string|Object} fieldName Field name.
* @param {Object} settings Field settings.
*
* @example
* ```js
* import { registerProductField } from '@woocommerce/components'
*
* registerProductFieldType( 'attributes-field', {
* } );
* ```
*/
export function registerProductField(
fieldName: string,
settings: ProductFieldDefinition
) {
if ( select( productFieldStore ).getProductField( fieldName ) ) {
// eslint-disable-next-line no-console
console.error(
'Product Field "' + fieldName + '" is already registered.'
);
return;
}
dispatch( productFieldStore ).registerProductField( {
attributes: {},
...settings,
} );
return select( productFieldStore ).getProductField( fieldName );
}

View File

@ -0,0 +1,28 @@
/**
* External dependencies
*/
import { select } from '@wordpress/data';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { store as productFieldStore } from '../store';
import { ProductFieldDefinition } from '../store/types';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function renderField( name: string, props: Record< string, any > ) {
const fieldConfig: ProductFieldDefinition =
select( productFieldStore ).getProductField( name );
if ( fieldConfig.render ) {
return <fieldConfig.render { ...props } />;
}
if ( fieldConfig.type ) {
return createElement( 'input', {
type: fieldConfig.type,
...props,
} );
}
return null;
}

View File

@ -0,0 +1,2 @@
export { store } from './store';
export * from './api';

View File

@ -0,0 +1,5 @@
export enum TYPES {
REGISTER_FIELD = 'REGISTER_FIELD',
}
export default TYPES;

View File

@ -0,0 +1,14 @@
/**
* Internal dependencies
*/
import TYPES from './action-types';
import { ProductFieldDefinition } from './types';
export function registerProductField( field: ProductFieldDefinition ) {
return {
type: TYPES.REGISTER_FIELD as const,
field,
};
}
export type Actions = ReturnType< typeof registerProductField >;

View File

@ -0,0 +1 @@
export const STORE_NAME = 'wc/admin/product/fields';

View File

@ -0,0 +1,21 @@
/**
* External dependencies
*/
import { createReduxStore, register } from '@wordpress/data';
/**
* Internal dependencies
*/
import reducer from './reducer';
import * as selectors from './selectors';
import * as actions from './actions';
import { STORE_NAME } from './constants';
export const store = createReduxStore( STORE_NAME, {
// @ts-expect-error reducer has correct format.
reducer,
selectors,
actions,
} );
register( store );

View File

@ -0,0 +1,32 @@
/**
* Internal dependencies
*/
import TYPES from './action-types';
import { Actions } from './actions';
import { ProductFieldState } from './types';
const reducer = (
state: ProductFieldState = {
fields: {},
},
payload: Actions
) => {
if ( payload && 'type' in payload ) {
switch ( payload.type ) {
case TYPES.REGISTER_FIELD:
return {
...state,
fields: {
...state.fields,
[ payload.field.name ]: payload.field,
},
};
default:
return state;
}
}
return state;
};
export type State = ReturnType< typeof reducer >;
export default reducer;

View File

@ -0,0 +1,20 @@
/**
* External dependencies
*/
import memoize from 'memoize-one';
/**
* Internal dependencies
*/
import { ProductFieldState } from './types';
export function getProductField( state: ProductFieldState, name: string ) {
return state.fields[ name ] || null;
}
export const getRegisteredProductFields = memoize(
( state: ProductFieldState ) => Object.keys( state.fields ),
( [ newState ], [ oldState ] ) => {
return newState.fields === oldState.fields;
}
);

View File

@ -0,0 +1,15 @@
/**
* External dependencies
*/
import { ComponentType } from 'react';
export type ProductFieldDefinition = {
name: string;
type?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
render?: ComponentType;
};
export type ProductFieldState = {
fields: Record< string, ProductFieldDefinition >;
};

View File

@ -0,0 +1,71 @@
/**
* External dependencies
*/
import React from 'react';
import { useState, createElement } from '@wordpress/element';
import { createRegistry, RegistryProvider, select } from '@wordpress/data';
import {
// @ts-expect-error `__experimentalInputControl` does exist.
__experimentalInputControl as InputControl,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import { store } from '../store';
import { registerProductField, renderField } from '../api';
const registry = createRegistry();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
registry.register( store );
registerProductField( 'text', {
name: 'text',
render: ( props ) => {
return <InputControl type="text" { ...props } />;
},
} );
registerProductField( 'number', {
name: 'number',
render: () => {
return <InputControl type="number" />;
},
} );
const RenderField = () => {
const fields: string[] = select( store ).getRegisteredProductFields();
const [ selectedField, setSelectedField ] = useState(
fields ? fields[ 0 ] : undefined
);
const handleChange = ( event ) => {
setSelectedField( event.target.value );
};
return (
<div>
<select value={ selectedField } onChange={ handleChange }>
{ fields.map( ( field ) => (
<option key={ field } value={ field }>
{ field }
</option>
) ) }
</select>
{ selectedField && renderField( selectedField, { name: 'test' } ) }
</div>
);
};
export const Basic: React.FC = () => {
return (
<RegistryProvider value={ registry }>
<RenderField />
</RegistryProvider>
);
};
export default {
title: 'WooCommerce Admin/experimental/product-fields',
component: Basic,
};

View File

@ -2,7 +2,6 @@
* External dependencies
*/
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import { createElement } from '@wordpress/element';
@ -11,25 +10,47 @@ import { createElement } from '@wordpress/element';
*/
import { placeholderWhiteBackground as placeholder } from './placeholder';
type Image = {
src?: string;
};
type ProductImageProps = {
/**
* Product or variation object. The image to display will be pulled from
* `product.images` or `variation.image`.
* See https://woocommerce.github.io/woocommerce-rest-api-docs/#product-properties
* and https://woocommerce.github.io/woocommerce-rest-api-docs/#product-variation-properties
*/
product?: {
images?: Array< Image >;
image?: Image;
// ProductImage is only interested in product.images or varation.image
// but product object can have other properties that we don't control.
// allowing `any` here
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} & Record< string, any >;
/** The width of image to display. */
width?: number;
/** The height of image to display. */
height?: number;
/** Additional CSS classes. */
className?: string;
/** Text to use as the image alt attribute. */
alt?: string;
};
/**
* Use `ProductImage` to display a product's or variation's featured image.
* If no image can be found, a placeholder matching the front-end image
* placeholder will be displayed.
*
* @param {Object} props
* @param {Object} props.product
* @param {string} props.alt
* @param {number} props.width
* @param {number} props.height
* @param {string} props.className
* @return {Object} -
*/
const ProductImage = ( {
const ProductImage: React.VFC< ProductImageProps > = ( {
product,
width = 33,
height = 33,
className = '',
alt,
width,
height,
className,
...props
} ) => {
// The first returned image from the API is the featured/product image.
@ -54,36 +75,4 @@ const ProductImage = ( {
);
};
ProductImage.propTypes = {
/**
* The width of image to display.
*/
width: PropTypes.number,
/**
* The height of image to display.
*/
height: PropTypes.number,
/**
* Additional CSS classes.
*/
className: PropTypes.string,
/**
* Product or variation object. The image to display will be pulled from
* `product.images` or `variation.image`.
* See https://woocommerce.github.io/woocommerce-rest-api-docs/#product-properties
* and https://woocommerce.github.io/woocommerce-rest-api-docs/#product-variation-properties
*/
product: PropTypes.object,
/**
* Text to use as the image alt attribute.
*/
alt: PropTypes.string,
};
ProductImage.defaultProps = {
width: 33,
height: 33,
className: '',
};
export default ProductImage;

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { ProductImage } from '@woocommerce/components';
import { createElement } from '@wordpress/element';
export const Basic = () => (
<div>

View File

@ -7,7 +7,7 @@ import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import ProductImage from '../';
import ProductImage from '..';
describe( 'ProductImage', () => {
test( 'should render the passed alt prop', () => {

View File

@ -66,7 +66,6 @@ const completer: AutoCompleter = {
const match = computeSuggestionMatch( product.name, query );
return (
<Fragment>
{ /* @ts-expect-error TODO: migrate ProductImage component to TS. */ }
<ProductImage
key="thumbnail"
className="woocommerce-search__result-thumbnail"

View File

@ -97,7 +97,6 @@ const completer: AutoCompleter = {
);
return (
<Fragment>
{ /* @ts-expect-error TODO: migrate ProductImage component to TS. */ }
<ProductImage
key="thumbnail"
className="woocommerce-search__result-thumbnail"

View File

@ -15,7 +15,7 @@ import { Level } from './context';
*
* @type {HTMLElement}
*/
export function H( props ) {
export function H( props: React.HTMLAttributes< HTMLHeadingElement > ) {
const level = useContext( Level );
const Heading = 'h' + Math.min( level, 6 );

View File

@ -1,56 +0,0 @@
/**
* External dependencies
*/
import PropTypes from 'prop-types';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { Level } from './context';
/**
* The section wrapper, used to indicate a sub-section (and change the header level context).
*
* @param {Object} props
* @param {import('react').ComponentType=} props.component
* @param {import('react').ReactNode} props.children Children to render in the tip.
* @param {string=} props.className
* @return {JSX.Element} -
*/
export function Section( { component, children, ...props } ) {
const Component = component || 'div';
return (
<Level.Consumer>
{ ( level ) => (
<Level.Provider value={ level + 1 }>
{ component === false ? (
children
) : (
<Component { ...props }>{ children }</Component>
) }
</Level.Provider>
) }
</Level.Consumer>
);
}
Section.propTypes = {
/**
* The wrapper component for this section. Optional, defaults to `div`. If passed false, no wrapper is used. Additional props
* passed to Section are passed on to the component.
*/
component: PropTypes.oneOfType( [
PropTypes.func,
PropTypes.string,
PropTypes.bool,
] ),
/**
* The children inside this section, rendered in the `component`. This increases the context level for the next heading used.
*/
children: PropTypes.node,
/**
* Optional classname
*/
className: PropTypes.string,
};

View File

@ -0,0 +1,42 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { Level } from './context';
type SectionProps = {
/** The wrapper component for this section. Optional, defaults to `div`. If passed false, no wrapper is used. Additional props passed to Section are passed on to the component. */
component?: React.ComponentType | string | false;
/** Optional classname */
className?: string;
/** The children inside this section, rendered in the `component`. This increases the context level for the next heading used. */
children: React.ReactNode;
};
/**
* The section wrapper, used to indicate a sub-section (and change the header level context).
*/
export const Section: React.VFC< SectionProps > = ( {
component,
children,
...props
} ) => {
const Component = component || 'div';
return (
<Level.Consumer>
{ ( level ) => (
<Level.Provider value={ level + 1 }>
{ component === false ? (
children
) : (
<Component { ...props }>{ children }</Component>
) }
</Level.Provider>
) }
</Level.Consumer>
);
};

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { H, Section } from '@woocommerce/components';
import { createElement } from '@wordpress/element';
export const Basic = () => (
<div>

View File

@ -0,0 +1,43 @@
/**
* External dependencies
*/
import React, { isValidElement, Fragment } from 'react';
import { Slot, Fill } from '@wordpress/components';
import { cloneElement, createElement } from '@wordpress/element';
/**
* Ordered fill item.
*
* @param {Node} children - Node children.
* @param {number} order - Node order.
* @param {Array} props - Fill props.
* @return {Node} Node.
*/
function createOrderedChildren< T = Fill.Props >(
children: React.ReactNode,
order: number,
props: T
) {
if ( typeof children === 'function' ) {
return cloneElement( children( props ), { order } );
} else if ( isValidElement( children ) ) {
return cloneElement( children, { ...props, order } );
}
throw Error( 'Invalid children type' );
}
export { createOrderedChildren };
/**
* Sort fills by order for slot children.
*
* @param {Array} fills - slot's `Fill`s.
* @return {Node} Node.
*/
export const sortFillsByOrder: Slot.Props[ 'children' ] = ( fills ) => {
// Copy fills array here because its type is readonly array that doesn't have .sort method in Typescript definition.
const sortedFills = [ ...fills ].sort( ( a, b ) => {
return a[ 0 ].props.order - b[ 0 ].props.order;
} );
return <Fragment>{ sortedFills }</Fragment>;
};

View File

@ -0,0 +1,42 @@
# WooProductFieldItem Slot & Fill
A Slotfill component that will allow you to add a new field to a specific section in the product editor.
## Usage
```jsx
<WooProductFieldItem id={ key } section="details" order={ 2 } pluginId="test-plugin" >
{ () => {
return (
<TextControl
label="Name"
name={ `product-mvp-name` }
placeholder="e.g. 12 oz Coffee Mug"
value="Test Name"
onChange={ () => console.debug( 'Changed!' ) }
/>
);
} }
</WooProductFieldItem>
<WooProductFieldItem.Slot section="details" />
```
### WooProductFieldItem (fill)
This is the fill component. You must provide the `id` prop to identify your product field fill with a unique string. This component will accept a series of props:
| Prop | Type | Description |
| -------------| -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `id` | String | A unique string to identify your fill. Used for configuiration management. |
| `section ` | String | The string used to identify the particular section where you want to render your field. |
| `pluginId` | String | A unique plugin ID to identify the plugin/extension that this fill is associated with. |
| `order` | Number | (optional) This number will dictate the order that the fields rendered by a Slot will be appear. |
### WooProductFieldItem.Slot (slot)
This is the slot component, and will not be used as frequently. It must also receive the required `location` prop that will be identical to the fill `location`.
| Name | Type | Description |
| ----------- | ------ | ---------------------------------------------------------------------------------------------------- |
| `section` | String | Unique to the section that the Slot appears, and must be the same as the one provided to any fills. |

View File

@ -0,0 +1 @@
export * from './woo-product-field-item';

View File

@ -0,0 +1,58 @@
/**
* External dependencies
*/
import React from 'react';
import { Slot, Fill } from '@wordpress/components';
import { createElement, Children } from '@wordpress/element';
/**
* Internal dependencies
*/
import { createOrderedChildren, sortFillsByOrder } from '../utils';
type WooProductFieldItemProps = {
id: string;
section: string;
pluginId: string;
order?: number;
};
type WooProductFieldSlotProps = {
section: string;
};
export const WooProductFieldItem: React.FC< WooProductFieldItemProps > & {
Slot: React.FC< Slot.Props & WooProductFieldSlotProps >;
} = ( { children, order = 1, section } ) => (
<Fill name={ `woocommerce_product_field_${ section }` }>
{ ( fillProps: Fill.Props ) => {
return createOrderedChildren< Fill.Props >(
children,
order,
fillProps
);
} }
</Fill>
);
WooProductFieldItem.Slot = ( { fillProps, section } ) => (
<Slot
name={ `woocommerce_product_field_${ section }` }
fillProps={ fillProps }
>
{ ( fills ) => {
if ( ! sortFillsByOrder ) {
return null;
}
return Children.map(
sortFillsByOrder( fills )?.props.children,
( child ) => (
<div className="woocommerce-product-form__field">
{ child }
</div>
)
);
} }
</Slot>
);

View File

@ -0,0 +1,46 @@
# WooProductSectionItem Slot & Fill
A Slotfill component that will allow you to add a new section to the product editor.
## Usage
```jsx
<WooProductSectionItem id={ key } location="tab/general" order={ 2 } pluginId="test-plugin" >
{ () => {
return (
<ProductSectionLayout
title={ __( 'Product test section', 'woocommerce' ) }
description={ __(
'In this area you can describe the section.',
'woocommerce'
) }
>
<Card>
<CardBody>{ /* Section content */ }</CardBody>
</Card>
</ProductSectionLayout>
);
} }
</WooProductSectionItem>
<WooProductSectionItem.Slot location="tab/general" />
```
### WooProductSectionItem (fill)
This is the fill component. You must provide the `id` prop to identify your section fill with a unique string. This component will accept a series of props:
| Prop | Type | Description |
| -------------| -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `id` | String | A unique string to identify your fill. Used for configuiration management. |
| `location` | String | The string used to identify the particular location that you want to render your section. |
| `pluginId` | String | A unique plugin ID to identify the plugin/extension that this fill is associated with. |
| `order` | Number | (optional) This number will dictate the order that the sections rendered by a Slot will be appear. |
### WooProductSectionItem.Slot (slot)
This is the slot component, and will not be used as frequently. It must also receive the required `location` prop that will be identical to the fill `location`.
| Name | Type | Description |
| ----------- | ------ | ---------------------------------------------------------------------------------------------------- |
| `location` | String | Unique to the location that the Slot appears, and must be the same as the one provided to any fills. |

View File

@ -0,0 +1 @@
export * from './woo-product-section-item';

View File

@ -0,0 +1,51 @@
/**
* External dependencies
*/
import React from 'react';
import { Slot, Fill } from '@wordpress/components';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { createOrderedChildren, sortFillsByOrder } from '../utils';
type WooProductSectionItemProps = {
id: string;
location: string;
pluginId: string;
order?: number;
};
type WooProductFieldSlotProps = {
location: string;
};
export const WooProductSectionItem: React.FC< WooProductSectionItemProps > & {
Slot: React.FC< Slot.Props & WooProductFieldSlotProps >;
} = ( { children, order = 1, location } ) => (
<Fill name={ `woocommerce_product_section_${ location }` }>
{ ( fillProps: Fill.Props ) => {
return createOrderedChildren< Fill.Props >(
children,
order,
fillProps
);
} }
</Fill>
);
WooProductSectionItem.Slot = ( { fillProps, location } ) => (
<Slot
name={ `woocommerce_product_section_${ location }` }
fillProps={ fillProps }
>
{ ( fills ) => {
if ( ! sortFillsByOrder ) {
return null;
}
return sortFillsByOrder( fills );
} }
</Slot>
);

View File

@ -29,12 +29,14 @@ export function CustomerEffortScoreConsole( { label } ) {
const onNoticeShown = () => console.log( 'onNoticeShown' );
const onModalShown = () => console.log( 'onModalShown' );
const onNoticeDismissed = () => console.log( 'onNoticeDismissed' );
const recordScore = ( score, comments ) => console.log( { score, comments } );
const recordScore = ( score, score2, comments ) => console.log( { score, score2, comments } );
return (
<CustomerEffortScore
recordScoreCallback={ recordScore }
label={ label }
title="My title"
firstQuestion="My first question"
secondQuestion="My optional second question"
onNoticeShownCallback={ onNoticeShown }
onNoticeDismissedCallback={ onNoticeDismissed }
onModalShownCallback={ onModalShown }
@ -62,7 +64,8 @@ const MyComponent = function() {
setCeses(
ceses.concat(
<CustomerEffortScoreConsole
label={ `survey ${ceses.length + 1}` }
title={ `survey ${ceses.length + 1}` }
firstQuestion="My first question"
key={ ceses.length + 1 }
/>
)

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Set secondQuestion and title as optional

View File

@ -19,11 +19,11 @@ type CustomerEffortScoreProps = {
secondScore: number,
comments: string
) => void;
title: string;
title?: string;
description?: string;
noticeLabel?: string;
firstQuestion: string;
secondQuestion: string;
secondQuestion?: string;
onNoticeShownCallback?: () => void;
onNoticeDismissedCallback?: () => void;
onModalShownCallback?: () => void;
@ -39,11 +39,11 @@ type CustomerEffortScoreProps = {
*
* @param {Object} props Component props.
* @param {Function} props.recordScoreCallback Function to call when the score should be recorded.
* @param {string} props.title The title displayed in the modal.
* @param {string} [props.title] The title displayed in the modal.
* @param {string} props.description The description displayed in the modal.
* @param {string} props.noticeLabel The notice label displayed in the notice.
* @param {string} props.firstQuestion The first survey question.
* @param {string} props.secondQuestion The second survey question.
* @param {string} [props.secondQuestion] The second survey question.
* @param {Function} props.onNoticeShownCallback Function to call when the notice is shown.
* @param {Function} props.onNoticeDismissedCallback Function to call when the notice is dismissed.
* @param {Function} props.onModalShownCallback Function to call when the modal is shown.
@ -120,7 +120,7 @@ CustomerEffortScore.propTypes = {
/**
* The title displayed in the modal.
*/
title: PropTypes.string.isRequired,
title: PropTypes.string,
/**
* The function to call when the notice is shown.
*/
@ -137,6 +137,14 @@ CustomerEffortScore.propTypes = {
* Icon (React component) to be displayed.
*/
icon: PropTypes.element,
/**
* The first survey question.
*/
firstQuestion: PropTypes.string.isRequired,
/**
* The second survey question.
*/
secondQuestion: PropTypes.string,
};
export { CustomerEffortScore };

View File

@ -28,14 +28,14 @@ import { __ } from '@wordpress/i18n';
* @param {string} props.title Title displayed in the modal.
* @param {string} props.description Description displayed in the modal.
* @param {string} props.firstQuestion The first survey question.
* @param {string} props.secondQuestion The second survey question.
* @param {string} [props.secondQuestion] An optional second survey question.
* @param {string} props.defaultScore Default score.
* @param {Function} props.onCloseModal Callback for when user closes modal by clicking cancel.
* @param {Function} props.customOptions List of custom score options, contains label and value.
*/
function CustomerFeedbackModal( {
recordScoreCallback,
title,
title = __( 'Please share your feedback', 'woocommerce' ),
description,
firstQuestion,
secondQuestion,
@ -48,10 +48,10 @@ function CustomerFeedbackModal( {
secondScore: number,
comments: string
) => void;
title: string;
title?: string;
description?: string;
firstQuestion: string;
secondQuestion: string;
secondQuestion?: string;
defaultScore?: number;
onCloseModal?: () => void;
customOptions?: { label: string; value: string }[];
@ -110,10 +110,8 @@ function CustomerFeedbackModal( {
const sendScore = () => {
if (
! (
Number.isInteger( firstQuestionScore ) &&
Number.isInteger( secondQuestionScore )
)
! Number.isInteger( firstQuestionScore ) ||
( secondQuestion && ! Number.isInteger( secondQuestionScore ) )
) {
setShowNoScoreMessage( true );
return;
@ -175,28 +173,32 @@ function CustomerFeedbackModal( {
/>
</div>
<Text
variant="subtitle.small"
as="p"
weight="600"
size="14"
lineHeight="20px"
>
{ secondQuestion }
</Text>
{ secondQuestion && (
<Text
variant="subtitle.small"
as="p"
weight="600"
size="14"
lineHeight="20px"
>
{ secondQuestion }
</Text>
) }
<div className="woocommerce-customer-effort-score__selection">
<RadioControl
selected={ secondQuestionScore.toString( 10 ) }
options={ options }
onChange={ ( value ) =>
onRadioControlChange(
value as string,
setSecondQuestionScore
)
}
/>
</div>
{ secondQuestion && (
<div className="woocommerce-customer-effort-score__selection">
<RadioControl
selected={ secondQuestionScore.toString( 10 ) }
options={ options }
onChange={ ( value ) =>
onRadioControlChange(
value as string,
setSecondQuestionScore
)
}
/>
</div>
) }
{ [ firstQuestionScore, secondQuestionScore ].some(
( score ) => score === 1 || score === 2
@ -250,9 +252,9 @@ function CustomerFeedbackModal( {
CustomerFeedbackModal.propTypes = {
recordScoreCallback: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
title: PropTypes.string,
firstQuestion: PropTypes.string.isRequired,
secondQuestion: PropTypes.string.isRequired,
secondQuestion: PropTypes.string,
defaultScore: PropTypes.number,
onCloseModal: PropTypes.func,
};

View File

@ -107,4 +107,34 @@ describe( 'CustomerFeedbackModal', () => {
} );
}
);
it( 'should render even if no second question is provided', async () => {
render(
<CustomerFeedbackModal
recordScoreCallback={ mockRecordScoreCallback }
title="Testing"
firstQuestion="First question"
/>
);
// Wait for the modal to render.
await screen.findByRole( 'dialog' );
// Should be only one neutral emoji, since there is one question only
expect( screen.getAllByLabelText( 'Neutral' ).length ).toBe( 1 );
} );
it( 'should render default title if no title is provided', async () => {
render(
<CustomerFeedbackModal
recordScoreCallback={ mockRecordScoreCallback }
firstQuestion="First question"
/>
);
// Wait for the modal to render.
await screen.findByRole( 'dialog' );
expect(
screen.queryByLabelText( 'Please share your feedback' )
).toBeInTheDocument();
} );
} );

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add manage_stock parent value to product variation type definition

View File

@ -55,13 +55,21 @@ export interface ProductVariationImage {
export type ProductVariation = Omit<
Product,
'name' | 'slug' | 'attributes' | 'images'
'name' | 'slug' | 'attributes' | 'images' | 'manage_stock'
> & {
attributes: ProductVariationAttribute[];
/**
* Variation image data.
*/
image?: ProductVariationImage;
/**
* Stock management at variation level. It can have a
* 'parent' value if the parent product is managing
* the stock at the time the variation was created.
*
* @default false
*/
manage_stock: boolean | 'parent';
};
type Query = Omit< ProductQuery, 'name' >;

View File

@ -16,7 +16,7 @@ const orderStatus = [
[ 'Processing', 'wc-processing' ],
[ 'On hold', 'wc-on-hold' ],
[ 'Completed', 'wc-completed' ],
[ 'Cancelled', 'wc-cancelled' ],
[ 'Canceled', 'wc-cancelled' ],
[ 'Refunded', 'wc-refunded' ],
[ 'Failed', 'wc-failed' ],
];

View File

@ -4,6 +4,7 @@
import { Component } from '@wordpress/element';
import { first, last } from 'lodash';
import { Spinner } from '@wordpress/components';
import { decodeEntities } from '@wordpress/html-entities';
import { Link } from '@woocommerce/components';
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
@ -49,7 +50,9 @@ export default class CategoryBreadcrumbs extends Component {
return category ? (
<div className="woocommerce-table__breadcrumbs">
{ this.getCategoryAncestors( category, categories ) }
{ decodeEntities(
this.getCategoryAncestors( category, categories )
) }
<Link
href={ getNewPath(
persistedQuery,
@ -61,7 +64,7 @@ export default class CategoryBreadcrumbs extends Component {
) }
type="wc-admin"
>
{ category.name }
{ decodeEntities( category.name ) }
</Link>
</div>
) : (

View File

@ -3,6 +3,7 @@
*/
import { CheckboxControl, Icon } from '@wordpress/components';
import { useEffect, useState } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
import { chevronDown, chevronUp } from '@wordpress/icons';
import { ProductCategory } from '@woocommerce/data';
import { __experimentalSelectControlMenuItemProps as MenuItemProps } from '@woocommerce/components';
@ -85,7 +86,7 @@ export const CategoryFieldItem: React.FC< CategoryFieldItemProps > = ( {
<div className="woocommerce-category-field-dropdown__toggle-placeholder"></div>
) }
<CheckboxControl
label={ item.data.name }
label={ decodeEntities( item.data.name ) }
checked={ selectedIds.includes( item.data.id ) }
onChange={ () => item.data }
/>

View File

@ -29,7 +29,11 @@ export default function useProductVariationNavigation( {
const persistedQuery = getPersistedQuery();
return {
actionHref: getNewPath( persistedQuery, `/product/${ product.id }` ),
actionHref: getNewPath(
persistedQuery,
`/product/${ product.id }`,
{}
),
prevHref: prevVariationId
? getNewPath(
persistedQuery,

View File

@ -1,31 +0,0 @@
/**
* Internal dependencies
*/
import { WooProductFieldItem } from './woo-product-field-item';
type ProductFieldLayoutProps = {
fieldName: string;
categoryName: string;
};
export const ProductFieldLayout: React.FC< ProductFieldLayoutProps > = ( {
fieldName,
categoryName,
children,
} ) => {
return (
<div className="product-field-layout">
<WooProductFieldItem.Slot
fieldName={ fieldName }
categoryName={ categoryName }
location="before"
/>
{ children }
<WooProductFieldItem.Slot
fieldName={ fieldName }
categoryName={ categoryName }
location="after"
/>
</div>
);
};

View File

@ -22,7 +22,7 @@
}
}
.woocommerce-product-form__field {
.woocommerce-product-form__field:not(:first-child) {
margin-top: $gap-large;
> .components-base-control {

View File

@ -8,7 +8,6 @@ import { FormSection } from '@woocommerce/components';
* Internal dependencies
*/
import './product-section-layout.scss';
import { ProductFieldLayout } from './product-field-layout';
type ProductSectionLayoutProps = {
title: string;
@ -31,12 +30,7 @@ export const ProductSectionLayout: React.FC< ProductSectionLayoutProps > = ( {
{ Children.map( children, ( child ) => {
if ( isValidElement( child ) && child.props.onChange ) {
return (
<ProductFieldLayout
fieldName={ child.props.name }
categoryName={ title }
>
{ child }
</ProductFieldLayout>
<div className="product-field-layout">{ child }</div>
);
}
return child;

View File

@ -1,115 +0,0 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
import { SlotFillProvider } from '@wordpress/components';
/**
* Internal dependencies
*/
import { ProductFieldLayout } from '../product-field-layout';
import { WooProductFieldItem } from '../woo-product-field-item';
describe( 'ProductFieldLayout', () => {
beforeEach( () => {
jest.clearAllMocks();
} );
it( 'should allow adding extra fields before the field using slot fill', () => {
const { queryByText } = render(
<SlotFillProvider>
<ProductFieldLayout
fieldName="Name"
categoryName="Product Details"
>
<div>Name field</div>
</ProductFieldLayout>
<div>
<WooProductFieldItem
fieldName="Name"
categoryName="Product Details"
location="before"
>
<div>New field</div>
</WooProductFieldItem>
</div>
</SlotFillProvider>
);
expect( queryByText( 'New field' ) ).toBeInTheDocument();
expect( queryByText( 'New field' )?.nextSibling?.textContent ).toEqual(
'Name field'
);
} );
it( 'should allow adding extra fields after the field using slot fill', () => {
const { queryByText } = render(
<SlotFillProvider>
<ProductFieldLayout
fieldName="Name"
categoryName="Product Details"
>
<div>Name field</div>
</ProductFieldLayout>
<div>
<WooProductFieldItem
fieldName="Name"
categoryName="Product Details"
location="after"
>
<div>New field</div>
</WooProductFieldItem>
</div>
</SlotFillProvider>
);
expect( queryByText( 'New field' ) ).toBeInTheDocument();
expect( queryByText( 'Name field' )?.nextSibling?.textContent ).toEqual(
'New field'
);
} );
it( 'should not render new slot fills when field name does not match', () => {
const { queryByText } = render(
<SlotFillProvider>
<ProductFieldLayout
fieldName="Name"
categoryName="Product Details"
>
<div>Name field</div>
</ProductFieldLayout>
<div>
<WooProductFieldItem
fieldName="Description"
categoryName="Product Details"
location="after"
>
<div>New field</div>
</WooProductFieldItem>
</div>
</SlotFillProvider>
);
expect( queryByText( 'New field' ) ).not.toBeInTheDocument();
} );
it( 'should not render new slot fills when category name does not match', () => {
const { queryByText } = render(
<SlotFillProvider>
<ProductFieldLayout
fieldName="Name"
categoryName="Product Details"
>
<div>Name field</div>
</ProductFieldLayout>
<div>
<WooProductFieldItem
fieldName="Name"
categoryName="Images"
location="after"
>
<div>New field</div>
</WooProductFieldItem>
</div>
</SlotFillProvider>
);
expect( queryByText( 'New field' ) ).not.toBeInTheDocument();
} );
} );

View File

@ -8,28 +8,10 @@ import { render } from '@testing-library/react';
*/
import { ProductSectionLayout } from '../product-section-layout';
jest.mock( '../product-field-layout', () => {
const productFieldLayoutMock: React.FC< {
fieldName: string;
categoryName: string;
} > = ( { children, fieldName, categoryName } ) => {
return (
<div className="product-field-layout-mock">
<span>fieldName: { fieldName }</span>
<span>categoryName: { categoryName }</span>
{ children }
</div>
);
};
return {
ProductFieldLayout: productFieldLayoutMock,
};
} );
const SampleInputField: React.FC< { name: string; onChange: () => void } > = ( {
name,
} ) => {
return <div>smaple-input-field-{ name }</div>;
return <div>sample-input-field-{ name }</div>;
};
describe( 'ProductSectionLayout', () => {
@ -59,12 +41,9 @@ describe( 'ProductSectionLayout', () => {
</ProductSectionLayout>
);
expect( queryByText( 'fieldName: name' ) ).toBeInTheDocument();
expect( queryAllByText( 'categoryName: Title' ).length ).toEqual( 2 );
expect( queryByText( 'smaple-input-field-name' ) ).toBeInTheDocument();
expect( queryByText( 'sample-input-field-name' ) ).toBeInTheDocument();
expect(
queryByText( 'smaple-input-field-description' )
queryByText( 'sample-input-field-description' )
).toBeInTheDocument();
} );

View File

@ -1,79 +0,0 @@
/**
* External dependencies
*/
import React from 'react';
import { Slot, Fill } from '@wordpress/components';
import { snakeCase } from 'lodash';
/**
* Internal dependencies
*/
import { createOrderedChildren, sortFillsByOrder } from '~/utils';
// TODO: move this to a published JS package once ready.
/**
* Create a Fill for extensions to add items to the Product edit page.
*
* @slotFill WooProductFieldItem
* @scope woocommerce-admin
* @example
* const MyProductDetailsFieldItem = () => (
* <WooProductFieldItem fieldName="name" categoryName="Product details" location="after">My header item</WooProductFieldItem>
* );
*
* registerPlugin( 'my-extension', {
* render: MyProductDetailsFieldItem,
* scope: 'woocommerce-admin',
* } );
* @param {Object} param0
* @param {Array} param0.children - Node children.
* @param {string} param0.fieldName - Field name.
* @param {string} param0.categoryName - Category name.
* @param {number} param0.order - Order of Fill component.
* @param {string} param0.location - Location before or after.
*/
export const WooProductFieldItem: React.FC< {
fieldName: string;
categoryName: string;
order?: number;
location: 'before' | 'after';
} > & {
Slot: React.FC<
Slot.Props & {
fieldName: string;
categoryName: string;
location: 'before' | 'after';
}
>;
} = ( { children, fieldName, categoryName, location, order = 1 } ) => {
const categoryKey = snakeCase( categoryName );
const fieldKey = snakeCase( fieldName );
return (
<Fill
name={ `woocommerce_product_${ categoryKey }_${ fieldKey }_${ location }` }
>
{ ( fillProps: Fill.Props ) => {
return createOrderedChildren( children, order, fillProps );
} }
</Fill>
);
};
WooProductFieldItem.Slot = ( {
fillProps,
fieldName,
categoryName,
location,
} ) => {
const categoryKey = snakeCase( categoryName );
const fieldKey = snakeCase( fieldName );
return (
<Slot
name={ `woocommerce_product_${ categoryKey }_${ fieldKey }_${ location }` }
fillProps={ fillProps }
>
{ sortFillsByOrder }
</Slot>
);
};

View File

@ -8,7 +8,7 @@ export const ProductFormTab: React.FC< {
name: string;
title: string;
children: JSX.Element | JSX.Element[] | string;
} > = ( { name, title, children } ) => {
} > = ( { name, children } ) => {
const classes = classnames(
'woocommerce-product-form-tab',
'woocommerce-product-form-tab__' + name

View File

@ -1,7 +1,11 @@
/**
* External dependencies
*/
import { Form, FormRef } from '@woocommerce/components';
import {
Form,
FormRef,
__experimentalWooProductSectionItem as WooProductSectionItem,
} from '@woocommerce/components';
import { PartialProduct, Product } from '@woocommerce/data';
import { Ref } from 'react';
@ -47,6 +51,7 @@ export const ProductForm: React.FC< {
<ProductDetailsSection />
<ImagesSection />
<AttributesSection />
<WooProductSectionItem.Slot location="tab/general" />
</ProductFormTab>
<ProductFormTab
name="pricing"

View File

@ -36,8 +36,14 @@ export const ProductVariationFormActions: React.FC = () => {
const onSave = async () => {
setIsSaving( true );
updateProductVariation< Promise< ProductVariation > >(
{ id: variationId, product_id: productId },
values
{ id: variationId, product_id: productId, context: 'edit' },
{
...values,
manage_stock:
values.manage_stock === 'parent'
? undefined
: values?.manage_stock,
}
)
.then( () => {
createNotice(

View File

@ -17,6 +17,7 @@ import {
useFormContext,
__experimentalRichTextEditor as RichTextEditor,
__experimentalTooltip as Tooltip,
__experimentalWooProductFieldItem as WooProductFieldItem,
} from '@woocommerce/components';
import interpolateComponents from '@automattic/interpolate-components';
import {
@ -241,6 +242,7 @@ export const ProductDetailsSection: React.FC = () => {
'woocommerce'
) }
/>
<WooProductFieldItem.Slot section="details" />
</CardBody>
</Card>
</ProductSectionLayout>

View File

@ -3,7 +3,8 @@
*/
import { __ } from '@wordpress/i18n';
import { recordEvent } from '@woocommerce/tracks';
import { Link } from '@woocommerce/components';
import { Link, useFormContext } from '@woocommerce/components';
import { Product, ProductAttribute } from '@woocommerce/data';
/**
* Internal dependencies
@ -12,6 +13,28 @@ import { ProductSectionLayout } from '../layout/product-section-layout';
import { Variations } from '../fields/variations';
export const ProductVariationsSection: React.FC = () => {
const {
getInputProps,
values: { id: productId },
} = useFormContext< Product >();
const { value: attributes }: { value: ProductAttribute[] } = getInputProps(
'attributes',
{
productId,
}
);
const options = attributes
? attributes.filter(
( attribute: ProductAttribute ) => attribute.variation
)
: [];
if ( options.length === 0 ) {
return null;
}
return (
<ProductSectionLayout
title={ __( 'Variations', 'woocommerce' ) }

View File

@ -4,6 +4,9 @@
import { useDispatch } from '@wordpress/data';
import { ITEMS_STORE_NAME } from '@woocommerce/data';
import { getAdminLink } from '@woocommerce/settings';
import { getNewPath, navigateTo } from '@woocommerce/navigation';
import { loadExperimentAssignment } from '@woocommerce/explat';
import moment from 'moment';
import { useState } from '@wordpress/element';
/**
@ -25,6 +28,21 @@ export const useCreateProductByType = () => {
}
setIsRequesting( true );
if ( type === 'physical' ) {
const momentDate = moment().utc();
const year = momentDate.format( 'YYYY' );
const month = momentDate.format( 'MM' );
const assignment = await loadExperimentAssignment(
`woocommerce_product_creation_experience_${ year }${ month }_v1`
);
if ( assignment.variationName === 'treatment' ) {
navigateTo( { url: getNewPath( {}, '/add-product', {} ) } );
return;
}
}
try {
const data: {
id?: number;

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add a wp cli command for activating live branches.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Enable the live branches feature.

View File

@ -6,7 +6,7 @@
"license": "GPL-3.0-or-later",
"prefer-stable": true,
"minimum-stability": "dev",
"version": "2.1.0",
"version": "2.2.0",
"require": {
"composer/installers": "~1.7"
},

View File

@ -153,6 +153,13 @@ Copy and paste the system status report from **WooCommerce > System Status** in
* @return string
*/
protected function construct_ssr() {
// This function depends on the WC core global being available. Sometimes, such as when we deactivate
// WC to install a live branches version, WC will not be available and cause a crash if we don't exit early
// here.
if ( ! class_exists( 'WC' ) ) {
return '';
}
if ( version_compare( WC()->version, '3.6', '<' ) ) {
return '';
}
@ -332,7 +339,7 @@ Copy and paste the system status report from **WooCommerce > System Status** in
$items_to_remove = array( 'wc-beta-tester-settings', 'wc-beta-tester-version-picker', 'wc-beta-tester' );
if ( isset( $submenu['plugins.php'] ) ) {
foreach ( $submenu['plugins.php'] as $key => $menu ) {
if ( in_array( $menu[2], $items_to_remove ) ) {
if ( in_array( $menu[2], $items_to_remove, true ) ) {
unset( $submenu['plugins.php'][ $key ] );
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* WooCommerce Beta Tester CLI controls
*
* @package WC_Beta_Tester
*/
if ( ! class_exists( 'WP_CLI_Command' ) ) {
return;
}
/**
* Control your local WooCommerce Beta Tester plugin.
*/
class WC_Beta_Tester_CLI extends WP_CLI_Command {
/**
* Install a live branch of the WooCommerce plugin
*
* ## Options
* <branch>
* : The branch to install.
*
* ## Examples
*
* wp wc-beta-tester install update/some-branch
*
* @param array $args Arguments passed to CLI.
*/
public function install( $args ) {
$installer = new WC_Beta_Tester_Live_Branches_Installer();
$branch = $args[0];
$info = $installer->get_branch_info_from_manifest( $branch );
if ( ! $info ) {
WP_CLI::error( "Could not find branch $branch in manifest" );
} else {
$install_result = $installer->install( $info->download_url, $info->branch, $info->version );
if ( is_wp_error( $install_result ) ) {
WP_CLI::error( $install_result->get_error_message() );
}
WP_CLI::success( "Installed $branch" );
}
}
/**
* Deactivate WooCommerce.
*
* ## Examples
* wp wc-beta-tester deactivate_woocommerce
*/
public function deactivate_woocommerce() {
$installer = new WC_Beta_Tester_Live_Branches_Installer();
$installer->deactivate_woocommerce();
WP_CLI::success( 'Deactivated WooCommerce' );
}
/**
* Activate a live branch of the WooCommerce plugin.
*
* ## Options
* <branch>
* : The branch to activate.
*
* ## Examples
*
* wp wc-beta-tester activate update/some-branch*
*
* @param array $args Arguments passed to CLI.
*/
public function activate( $args ) {
$installer = new WC_Beta_Tester_Live_Branches_Installer();
$branch = $args[0];
$info = $installer->get_branch_info_from_manifest( $branch );
if ( ! $info ) {
WP_CLI::error( "Could not find branch $branch in manifest" );
} else {
$installer->activate( $info->version );
WP_CLI::success( "Activated $branch" );
}
}
}

View File

@ -46,6 +46,26 @@ class WC_Beta_Tester_Live_Branches_Installer {
return $wp_filesystem;
}
/**
* Get the download url of a WooCommerce plugin version from the manifest.
*
* @param string $branch The name of the branch.
*/
public function get_branch_info_from_manifest( $branch ) {
$response = wp_remote_get( 'https://betadownload.jetpack.me/woocommerce-branches.json' );
$body = wp_remote_retrieve_body( $response );
$obj = json_decode( $body );
foreach ( $obj->pr as $key => $value ) {
if ( $value->branch === $branch ) {
return $value;
}
}
return false;
}
/**
* Install a WooCommerce plugin version by download url.
*

View File

@ -18,9 +18,7 @@ class WC_Beta_Tester_Live_Branches {
add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ) );
// By the time this code runs it appears too late to hook into `admin_menu`.
// NOTE - We don't have feature flags, so add the following code to enable it
// in development: `$this->register_page()`.
$this->register_page();
}
/**

View File

@ -7,7 +7,7 @@
"url": "git://github.com/woocommerce/woocommerce-beta-tester.git"
},
"title": "WooCommerce Beta Tester",
"version": "2.1.0",
"version": "2.2.0",
"homepage": "http://github.com/woocommerce/woocommerce-beta-tester",
"devDependencies": {
"@types/react": "^17.0.2",

View File

@ -3,7 +3,7 @@ Contributors: automattic, bor0, claudiosanches, claudiulodro, kloon, mikejolley,
Tags: woocommerce, woo commerce, beta, beta tester, bleeding edge, testing
Requires at least: 4.7
Tested up to: 6.0
Stable tag: 2.1.0
Stable tag: 2.2.0
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html

View File

@ -109,7 +109,7 @@ const BranchInfo = ( { branch }: { branch: Branch } ) => {
const WooCommerceVersionInfo = () => {
// @ts-ignore
const version = window?.wc?.WC_VERSION || 'unknown';
const version = window?.wc?.wcSettings?.WC_VERSION || 'unknown';
return (
<p>
@ -136,6 +136,8 @@ export const BranchList = ( { branches }: { branches: Branch[] } ) => {
uninstalledBranches[ 0 ]
);
const installedBranchesExist = !! installedBranches.length;
return (
<>
<Card elevation={ 3 } css={ cardStyle }>
@ -188,7 +190,7 @@ export const BranchList = ( { branches }: { branches: Branch[] } ) => {
</CardBody>
<CardFooter></CardFooter>
</Card>
{ installedBranches.length && (
{ installedBranchesExist && (
<Card elevation={ 3 } css={ cardStyle }>
<CardHeader>
<h2>Other Installed Branches</h2>

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