Merge branch 'woocommerce:trunk' into trunk
This commit is contained in:
commit
00b96fb940
|
@ -9,6 +9,8 @@
|
|||
|
||||
### Changes proposed in this Pull Request:
|
||||
|
||||
<!-- If necessary, indicate if this PR is part of a bigger feature. Add a label with the format `focus: name of the feature [team:name of the team]`. -->
|
||||
|
||||
<!-- Describe the changes made to this Pull Request and the reason for such changes. -->
|
||||
|
||||
Closes # .
|
||||
|
@ -25,4 +27,4 @@ Using the [WooCommerce Testing Instructions Guide](https://github.com/woocommerc
|
|||
2.
|
||||
3.
|
||||
|
||||
<!-- End testing instructions -->
|
||||
<!-- End testing instructions -->
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
name: 'Changelog Auto Add'
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
hello-world:
|
||||
name: 'Hello World'
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Hello
|
||||
run: echo "Hello World"
|
|
@ -58,23 +58,24 @@ jobs:
|
|||
working-directory: plugins/woocommerce
|
||||
run: pnpm run build:feature-config
|
||||
|
||||
- name: Add PHP8 Compatibility.
|
||||
run: |
|
||||
if [ "$(php -r "echo version_compare(PHP_VERSION,'8.0','>=');")" ]; then
|
||||
cd plugins/woocommerce
|
||||
curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip
|
||||
unzip -d /tmp/phpunit-7.5-fork /tmp/phpunit-7.5-fork.zip
|
||||
composer bin phpunit config --unset platform
|
||||
composer bin phpunit config repositories.0 '{"type": "path", "url": "/tmp/phpunit-7.5-fork/phpunit-add-compatibility-with-php8-to-phpunit-7", "options": {"symlink": false}}'
|
||||
composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-reqs
|
||||
rm -rf ./vendor/phpunit/
|
||||
composer dump-autoload
|
||||
fi
|
||||
- id: parseMatrix
|
||||
name: Parse Matrix Variables
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const parseWPVersion = require( './.github/workflows/scripts/parse-wp-version' );
|
||||
parseWPVersion( '${{ matrix.wp }}' ).then( ( version ) => {
|
||||
core.setOutput( 'wpVersion', version );
|
||||
} );
|
||||
|
||||
- name: Init DB and WP
|
||||
working-directory: plugins/woocommerce
|
||||
run: ./tests/bin/install.sh woo_test root root 127.0.0.1 ${{ matrix.wp }}
|
||||
- name: Prepare Testing Environment
|
||||
env:
|
||||
WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }}
|
||||
WP_ENV_PHP_VERSION: ${{ matrix.php }}
|
||||
run: pnpm --filter=woocommerce env:test
|
||||
|
||||
- name: Run tests
|
||||
working-directory: plugins/woocommerce
|
||||
run: pnpm run test --color
|
||||
- name: Run Tests
|
||||
env:
|
||||
WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }}
|
||||
WP_ENV_PHP_VERSION: ${{ matrix.php }}
|
||||
run: pnpm --filter=woocommerce test:unit:env
|
||||
|
|
|
@ -13,7 +13,6 @@ permissions: {}
|
|||
|
||||
jobs:
|
||||
e2e-tests-run:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
|
||||
name: Runs E2E tests.
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
|
@ -85,7 +84,6 @@ jobs:
|
|||
|
||||
api-tests-run:
|
||||
name: Runs API tests.
|
||||
if: github.event.pull_request.user.login != 'github-actions[bot]'
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
name: Run code coverage on PR
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**/changelog/**'
|
||||
workflow_dispatch:
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
|
||||
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: Tool versions
|
||||
run: |
|
||||
php --version
|
||||
composer --version
|
||||
|
||||
- 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: 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)
|
|
@ -16,7 +16,6 @@ permissions: {}
|
|||
|
||||
jobs:
|
||||
test:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
|
||||
name: Code sniff (PHP 7.4, WP Latest)
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-20.04
|
||||
|
|
|
@ -8,7 +8,7 @@ permissions: {}
|
|||
|
||||
jobs:
|
||||
analyze:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
|
||||
if: ${{ github.event.pull_request.user.login != 'github-actions[bot]' }}
|
||||
name: Check pull request changes to highlight
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
|
|
|
@ -11,7 +11,6 @@ permissions: {}
|
|||
|
||||
jobs:
|
||||
changelogger_used:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
|
||||
name: Changelogger use
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
|
|
|
@ -12,7 +12,6 @@ permissions: {}
|
|||
|
||||
jobs:
|
||||
lint-test-js:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
|
||||
name: Lint and Test JS
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
|
|
|
@ -14,7 +14,7 @@ permissions: {}
|
|||
|
||||
jobs:
|
||||
test:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
|
||||
if: ${{ github.event.pull_request.user.login != 'github-actions[bot]' }}
|
||||
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} ${{ matrix.hpos && 'HPOS' || '' }}
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-20.04
|
||||
|
@ -52,15 +52,24 @@ jobs:
|
|||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
|
||||
- name: Tool versions
|
||||
run: |
|
||||
php --version
|
||||
composer --version
|
||||
- id: parseMatrix
|
||||
name: Parse Matrix Variables
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const parseWPVersion = require( './.github/workflows/scripts/parse-wp-version' );
|
||||
parseWPVersion( '${{ matrix.wp }}' ).then( ( version ) => {
|
||||
core.setOutput( 'wpVersion', version );
|
||||
} );
|
||||
|
||||
- name: Init DB and WP
|
||||
working-directory: plugins/woocommerce
|
||||
run: ./tests/bin/install.sh woo_test root root 127.0.0.1 ${{ matrix.wp }}
|
||||
- name: Prepare Testing Environment
|
||||
env:
|
||||
WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }}
|
||||
WP_ENV_PHP_VERSION: ${{ matrix.php }}
|
||||
run: pnpm --filter=woocommerce env:test
|
||||
|
||||
- name: Run tests
|
||||
working-directory: plugins/woocommerce
|
||||
run: pnpm run test --filter=woocommerce --color
|
||||
- name: Run Tests
|
||||
env:
|
||||
WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }}
|
||||
WP_ENV_PHP_VERSION: ${{ matrix.php }}
|
||||
run: pnpm --filter=woocommerce test:unit:env
|
||||
|
|
|
@ -1,119 +0,0 @@
|
|||
name: 'Release: Generate changelog'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
releaseBranch:
|
||||
description: 'The name of the release branch, in the format `release/x.y`'
|
||||
required: true
|
||||
releaseVersion:
|
||||
description: 'The version of the release, in the format `x.y`'
|
||||
required: true
|
||||
|
||||
env:
|
||||
GIT_COMMITTER_NAME: 'WooCommerce Bot'
|
||||
GIT_COMMITTER_EMAIL: 'no-reply@woocommerce.com'
|
||||
GIT_AUTHOR_NAME: 'WooCommerce Bot'
|
||||
GIT_AUTHOR_EMAIL: 'no-reply@woocommerce.com'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
create-changelog-prs:
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup WooCommerce Monorepo
|
||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||
with:
|
||||
build: false
|
||||
|
||||
- name: 'Git fetch the release branch'
|
||||
run: git fetch origin ${{ inputs.releaseBranch }}
|
||||
|
||||
- name: 'Checkout the release branch'
|
||||
run: git checkout ${{ inputs.releaseBranch }}
|
||||
|
||||
- name: 'Create a new branch for the changelog update PR'
|
||||
run: git checkout -b ${{ format( 'update/{0}-changelog', inputs.releaseVersion ) }}
|
||||
|
||||
- name: 'Generate the changelog file'
|
||||
run: pnpm --filter=woocommerce run changelog write --add-pr-num -n -vvv --use-version ${{ inputs.releaseVersion }}
|
||||
|
||||
- name: Checkout pnpm-lock.yaml to prevent issues
|
||||
run: git checkout pnpm-lock.yaml
|
||||
|
||||
- name: 'git rm deleted files'
|
||||
run: git rm $(git ls-files --deleted)
|
||||
|
||||
- name: 'Commit deletion'
|
||||
run: git commit -m "Delete changelog files from ${{ inputs.releaseVersion }} release"
|
||||
|
||||
- name: 'Remember the deletion commit hash'
|
||||
id: rev-parse
|
||||
run: echo "hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 'Insert NEXT_CHANGELOG contents into readme.txt'
|
||||
run: php .github/workflows/scripts/release-changelog.php
|
||||
|
||||
- name: 'git add readme.txt'
|
||||
run: git add plugins/woocommerce/readme.txt
|
||||
|
||||
- name: 'Commit readme'
|
||||
run: git commit -m "Update the readme files for the ${{ inputs.releaseVersion }} release"
|
||||
|
||||
- name: 'Push update branch to origin'
|
||||
run: git push origin ${{ format( 'update/{0}-changelog', inputs.releaseVersion ) }}
|
||||
|
||||
- name: 'Stash any other undesired changes'
|
||||
run: git stash
|
||||
|
||||
- name: 'Checkout trunk'
|
||||
run: git checkout trunk
|
||||
|
||||
- name: 'Create a branch for the changelog files deletion'
|
||||
run: git checkout -b ${{ format( 'delete/{0}-changelog', inputs.releaseVersion ) }}
|
||||
|
||||
- name: 'Cherry-pick the deletion commit'
|
||||
run: git cherry-pick ${{ steps.rev-parse.outputs.hash }}
|
||||
|
||||
- name: 'Push deletion branch to origin'
|
||||
run: git push origin ${{ format( 'delete/{0}-changelog', inputs.releaseVersion ) }}
|
||||
|
||||
- name: 'Create release branch PR'
|
||||
id: release-pr
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const result = await github.rest.pulls.create( {
|
||||
owner: "${{ github.repository_owner }}",
|
||||
repo: "${{ github.event.repository.name }}",
|
||||
head: "${{ format( 'update/{0}-changelog', inputs.releaseVersion ) }}",
|
||||
base: "${{ inputs.releaseBranch }}",
|
||||
title: "${{ format( 'Release: Prepare the changelog for {0}', inputs.releaseVersion ) }}",
|
||||
body: "${{ format( 'This pull request was automatically generated during the code freeze to prepare the changelog for {0}', inputs.releaseVersion ) }}"
|
||||
} );
|
||||
|
||||
return result.data.number;
|
||||
|
||||
- name: 'Create trunk PR'
|
||||
id: trunk-pr
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const result = await github.rest.pulls.create( {
|
||||
owner: "${{ github.repository_owner }}",
|
||||
repo: "${{ github.event.repository.name }}",
|
||||
head: "${{ format( 'delete/{0}-changelog', inputs.releaseVersion ) }}",
|
||||
base: "trunk",
|
||||
title: "${{ format( 'Release: Remove {0} change files', inputs.releaseVersion ) }}",
|
||||
body: "${{ format( 'This pull request was automatically generated during the code freeze to remove the changefiles from {0} that are compiled into the `{1}` branch via #{2}', inputs.releaseVersion, inputs.releaseBranch, steps.release-pr.outputs.result ) }}"
|
||||
} );
|
||||
|
||||
return result.data.number;
|
|
@ -41,10 +41,24 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
|
||||
with:
|
||||
version: '8.3.1'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
npm install -g pnpm
|
||||
pnpm install --filter monorepo-utils
|
||||
pnpm install --filter monorepo-utils --ignore-scripts
|
||||
# ignore scripts speeds up setup signficantly, but we still need to build monorepo utils
|
||||
pnpm build
|
||||
working-directory: tools/monorepo-utils
|
||||
|
||||
- name: 'Check whether today is the code freeze day'
|
||||
id: check-freeze
|
||||
|
@ -71,42 +85,47 @@ jobs:
|
|||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: pnpm run utils code-freeze version-bump -o ${{ github.repository_owner }} -v ${{ steps.milestone.outputs.nextDevelopmentVersion }}.0-dev
|
||||
|
||||
- name: Generate changelog changes
|
||||
id: changelog
|
||||
if: steps.check-freeze.outputs.freeze == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: pnpm run utils code-freeze changelog -o ${{ github.repository_owner }} -v ${{ steps.milestone.outputs.nextReleaseVersion }}
|
||||
|
||||
notify-slack:
|
||||
name: 'Sends code freeze notification to Slack'
|
||||
runs-on: ubuntu-20.04
|
||||
needs: code-freeze-prep
|
||||
if: ${{ needs.code-freeze-prep.outputs.freeze == 'true' && inputs.skipSlackPing != true }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
|
||||
with:
|
||||
version: '8.3.1'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
pnpm install --filter monorepo-utils --ignore-scripts
|
||||
# ignore scripts speeds up setup signficantly, but we still need to build monorepo utils
|
||||
pnpm build
|
||||
working-directory: tools/monorepo-utils
|
||||
|
||||
- name: Slack
|
||||
uses: archive/github-actions-slack@v2.0.0
|
||||
id: notify
|
||||
with:
|
||||
slack-bot-user-oauth-access-token: ${{ secrets.CODE_FREEZE_BOT_TOKEN }}
|
||||
slack-channel: ${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }}
|
||||
slack-text: |
|
||||
:warning-8c: ${{ needs.code-freeze-prep.outputs.nextReleaseVersion }} Code Freeze :ice_cube:
|
||||
|
||||
The automation to cut the release branch for ${{ needs.code-freeze-prep.outputs.nextReleaseVersion }} has run. Any PRs that were not already merged will be a part of ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.nextDevelopmentVersion }} by default. If you have something that needs to make ${{ needs.maybe-create-next-milestone-and-release-branch.outputs.nextReleaseVersion }} that hasn't yet been merged, please see the <${{ secrets.FG_LINK }}/code-freeze-for-woocommerce-core-release/|fieldguide page for the code freeze>.
|
||||
|
||||
trigger-changelog-action:
|
||||
name: 'Trigger changelog action'
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
actions: write
|
||||
needs: code-freeze-prep
|
||||
if: needs.code-freeze-prep.outputs.freeze == 'true'
|
||||
steps:
|
||||
- name: 'Trigger changelog action'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'release-changelog.yml',
|
||||
ref: 'trunk',
|
||||
inputs: {
|
||||
releaseVersion: "${{ needs.code-freeze-prep.outputs.nextReleaseVersion }}",
|
||||
releaseBranch: "${{ needs.code-freeze-prep.outputs.nextReleaseBranch }}"
|
||||
}
|
||||
})
|
||||
run: |
|
||||
pnpm utils slack "${{ secrets.CODE_FREEZE_BOT_TOKEN }}" "
|
||||
:warning-8c: ${{ needs.code-freeze-prep.outputs.nextReleaseVersion }} Code Freeze :ice_cube:
|
||||
The automation to cut the release branch for ${{ needs.code-freeze-prep.outputs.nextReleaseVersion }} has run. Any PRs that were not already merged will be a part of ${{ needs.code-freeze-prep.outputs.nextDevelopmentVersion }} by default. If you have something that needs to make ${{ needs.code-freeze-prep.outputs.nextReleaseVersion }} that hasn't yet been merged, please see the <${{ secrets.FG_LINK }}/code-freeze-for-woocommerce-core-release/|fieldguide page for the code freeze>.
|
||||
" "${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }}"
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
const https = require( 'http' );
|
||||
|
||||
/**
|
||||
For convenience, this method will convert between a display-friendly version format and one used
|
||||
internally by wp-env. We lean towards using WordPress.org ZIPs which requires us to reference
|
||||
the full URL to the archive. For instance, instead of needing the action to fully define the
|
||||
URL to the nightly build we can pass "nightly" to this function and retrieve it.
|
||||
|
||||
@param {string} wpVersion The display-friendly version. Supports ("master", "trunk", "nightly",
|
||||
"latest", "X.X" for version lines, and "X.X.X" for specific versions)
|
||||
@return {Promise.<string>} The wp-env "core" property".
|
||||
**/
|
||||
module.exports = async function parseWPVersion( wpVersion ) {
|
||||
// Start with versions we can infer immediately.
|
||||
switch ( wpVersion ) {
|
||||
case 'master':
|
||||
case 'trunk': {
|
||||
return 'WordPress/WordPress#master';
|
||||
}
|
||||
|
||||
case 'nightly': {
|
||||
return 'https://wordpress.org/nightly-builds/wordpress-latest.zip';
|
||||
}
|
||||
|
||||
case 'latest': {
|
||||
return 'https://wordpress.org/latest.zip';
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
// We're going to download the correct zip archive based on the version they're requesting.
|
||||
const parsedVersion = wpVersion.match( /([0-9]+)\.([0-9]+)(?:\.([0-9]+))?/ );
|
||||
if ( ! parsedVersion ) {
|
||||
throw new Error( `Invalid 'wp-version': ${ wpVersion } must be 'trunk', 'nightly', 'latest', 'X.X', or 'X.X.X'.` );
|
||||
}
|
||||
|
||||
// When they've provided a specific version we can just provide that.
|
||||
if ( parsedVersion[ 3 ] !== undefined ) {
|
||||
let zipVersion = `${ parsedVersion[ 1 ] }.${ parsedVersion[ 2 ] }`;
|
||||
// .0 versions do not have a patch.
|
||||
if ( parsedVersion[ 3 ] !== '0' ) {
|
||||
zipVersion += `.${ parsedVersion[ 3 ] }`;
|
||||
}
|
||||
|
||||
resolve( `https://wordpress.org/wordpress-${ zipVersion }.zip` );
|
||||
}
|
||||
|
||||
const request = https.get(
|
||||
'http://api.wordpress.org/core/stable-check/1.0/',
|
||||
( response ) => {
|
||||
// Listen for the response data.
|
||||
let data = '';
|
||||
response.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
// Once we have the entire response we can process it.
|
||||
response.on('end', () => {
|
||||
// Parse the response and find the latest version of every minor release.
|
||||
const latestVersions = {};
|
||||
const rawVersions = JSON.parse( data );
|
||||
for ( const v in rawVersions ) {
|
||||
// Parse the version so we can find the latest.
|
||||
const matches = v.match( /([0-9]+)\.([0-9]+)(?:\.([0-9]+))?/ );
|
||||
const minor = `${ matches[1] }.${ matches[2] }`;
|
||||
const patch = matches[ 3 ] === undefined ? 0 : parseInt( matches[ 3 ] );
|
||||
|
||||
// We will only be keeping the latest release of each minor.
|
||||
if ( latestVersions[ minor ] === undefined || patch > latestVersions[ minor ] ) {
|
||||
latestVersions[ minor ] = patch;
|
||||
}
|
||||
}
|
||||
|
||||
let zipVersion = `${ parsedVersion[ 1 ] }.${ parsedVersion[ 2 ] }`;
|
||||
// .0 versions do not have a patch.
|
||||
if ( latestVersions[ zipVersion ] !== 0 ) {
|
||||
zipVersion += `.${ latestVersions[ zipVersion ]}`;
|
||||
}
|
||||
|
||||
resolve( `https://wordpress.org/wordpress-${ zipVersion }.zip` );
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
request.on( 'error', ( error ) => {
|
||||
reject( error );
|
||||
} );
|
||||
} );
|
||||
}
|
|
@ -48,7 +48,7 @@ jobs:
|
|||
working-directory: plugins/woocommerce
|
||||
env:
|
||||
UPDATE_WC: nightly
|
||||
run: pnpm exec playwright test --config=tests/e2e-pw/daily.playwright.config.js update-woocommerce.spec.js
|
||||
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js update-woocommerce.spec.js
|
||||
|
||||
- name: Run API tests.
|
||||
working-directory: plugins/woocommerce
|
||||
|
@ -200,15 +200,17 @@ jobs:
|
|||
- plugin: 'WooCommerce Subscriptions'
|
||||
repo: WC_SUBSCRIPTIONS_REPO
|
||||
private: true
|
||||
- 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'
|
||||
- plugin: 'Gutenberg'
|
||||
repo: 'WordPress/gutenberg'
|
||||
- plugin: 'Gutenberg - Nightly'
|
||||
repo: 'bph/gutenberg'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup WooCommerce Monorepo
|
||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||
with:
|
||||
build-filters: woocommerce
|
||||
|
||||
- name: Launch wp-env e2e environment
|
||||
working-directory: plugins/woocommerce
|
||||
|
@ -224,13 +226,13 @@ jobs:
|
|||
PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }}
|
||||
PLUGIN_NAME: ${{ matrix.plugin }}
|
||||
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
|
||||
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js upload-plugin.spec.js
|
||||
run: pnpm test:e2e-pw upload-plugin.spec.js
|
||||
|
||||
- name: Run the rest of E2E tests
|
||||
working-directory: plugins/woocommerce
|
||||
env:
|
||||
E2E_MAX_FAILURES: 15
|
||||
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js
|
||||
run: pnpm test:e2e-pw
|
||||
|
||||
- name: Generate E2E Test report.
|
||||
if: success() || failure()
|
||||
|
@ -321,36 +323,29 @@ jobs:
|
|||
matrix:
|
||||
include:
|
||||
- plugin: 'WooCommerce Payments'
|
||||
repo: 'automattic/woocommerce-payments'
|
||||
slug: woocommerce-payments
|
||||
- plugin: 'WooCommerce PayPal Payments'
|
||||
repo: 'woocommerce/woocommerce-paypal-payments'
|
||||
slug: 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'
|
||||
slug: woocommerce-services
|
||||
- plugin: 'WooCommerce Subscriptions'
|
||||
slug: woocommerce-subscriptions
|
||||
- plugin: 'Gutenberg'
|
||||
slug: gutenberg
|
||||
- plugin: 'Gutenberg - Nightly'
|
||||
slug: gutenberg-nightly
|
||||
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 slug="${{ matrix.slug }}" \
|
||||
-f s3_root=public \
|
||||
--repo woocommerce/woocommerce-test-reports
|
||||
|
|
|
@ -657,12 +657,12 @@ jobs:
|
|||
repo: WC_SUBSCRIPTIONS_REPO
|
||||
private: true
|
||||
env_description: 'woocommerce-subscriptions'
|
||||
- plugin: 'WordPress SEO' # Yoast SEO in the UI, but the slug is wordpress-seo
|
||||
repo: 'Yoast/wordpress-seo'
|
||||
env_description: 'wordpress-seo'
|
||||
- plugin: 'Contact Form 7'
|
||||
repo: 'takayukister/contact-form-7'
|
||||
env_description: 'contact-form-7'
|
||||
- plugin: 'Gutenberg'
|
||||
repo: 'WordPress/gutenberg'
|
||||
env_description: 'gutenberg'
|
||||
- plugin: 'Gutenberg - Nightly'
|
||||
repo: 'bph/gutenberg'
|
||||
env_description: 'gutenberg-nightly'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
|
|
@ -23,26 +23,26 @@ Here are some examples of the ways you can use Turborepo / pnpm commands:
|
|||
```bash
|
||||
# Lint and build all plugins, packages, and tools. Note the use of `-r` for lint,
|
||||
# turbo does not run the lint at this time.
|
||||
pnpm run -r lint && pnpm run build
|
||||
pnpm run -r lint && pnpm run build
|
||||
|
||||
# Build WooCommerce Core and all of its dependencies
|
||||
pnpm run --filter='woocommerce' build
|
||||
pnpm run --filter='woocommerce' build
|
||||
|
||||
# Lint the @woocommerce/components package - note the different argument order, turbo scripts
|
||||
# are not running lints at this point in time.
|
||||
pnpm run -r --filter='@woocommerce/components' lint
|
||||
pnpm run -r --filter='@woocommerce/components' lint
|
||||
|
||||
# Test all of the @woocommerce scoped packages
|
||||
pnpm run --filter='@woocommerce/*' test
|
||||
pnpm run --filter='@woocommerce/*' test
|
||||
|
||||
# Build all of the JavaScript packages
|
||||
pnpm run --filter='./packages/js/*' build
|
||||
pnpm run --filter='./packages/js/*' build
|
||||
|
||||
# Build everything except WooCommerce Core
|
||||
pnpm run --filter='!woocommerce' build
|
||||
pnpm run --filter='!woocommerce' build
|
||||
|
||||
# Build everything that has changed since the last commit
|
||||
pnpm run --filter='[HEAD^1]' build
|
||||
pnpm run --filter='[HEAD^1]' build
|
||||
```
|
||||
|
||||
### Cache busting Turbo
|
||||
|
@ -90,3 +90,25 @@ pnpm -- wp-env destroy
|
|||
Each of the [plugins in our repository](plugins) support using this tool to spin up a development environment. Note that rather than having a single top-level environment, each plugin has its own. This is done in order to prevent conflicts between them.
|
||||
|
||||
Please check out [the official documentation](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/) if you would like to learn more about this tool.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Installing PHP in Unix (e.g. Ubuntu)
|
||||
|
||||
Many unix systems such as Ubuntu will have PHP already installed. Sometimes without the extra packages you need to run WordPress and this will cause you to run into troubles.
|
||||
|
||||
Use your package manager to add the extra PHP packages you'll need.
|
||||
e.g. in Ubuntu you can run:
|
||||
|
||||
```
|
||||
sudo apt update
|
||||
sudo apt install php-bcmath \
|
||||
php-curl \
|
||||
php-imagick \
|
||||
php-intl \
|
||||
php-json \
|
||||
php-mbstring \
|
||||
php-mysql \
|
||||
php-xml \
|
||||
php-zip
|
||||
```
|
||||
|
|
|
@ -12,7 +12,7 @@ To get up and running within the WooCommerce Monorepo, you will need to make sur
|
|||
|
||||
- [NVM](https://github.com/nvm-sh/nvm#installing-and-updating): While you can always install Node through other means, we recommend using NVM to ensure you're aligned with the version used by our development teams. Our repository contains [an `.nvmrc` file](.nvmrc) which helps ensure you are using the correct version of Node.
|
||||
- [PNPM](https://pnpm.io/installation): Our repository utilizes PNPM to manage project dependencies and run various scripts involved in building and testing projects.
|
||||
- [PHP 7.2+](https://www.php.net/manual/en/install.php): WooCommerce Core currently features a minimum PHP version of 7.2. It is also needed to run Composer and various project build scripts.
|
||||
- [PHP 7.2+](https://www.php.net/manual/en/install.php): WooCommerce Core currently features a minimum PHP version of 7.2. It is also needed to run Composer and various project build scripts. See [troubleshooting](DEVELOPMENT.md#troubleshooting) for troubleshooting problems installing PHP.
|
||||
- [Composer](https://getcomposer.org/doc/00-intro.md): We use Composer to manage all of the dependencies for PHP packages and plugins.
|
||||
|
||||
Once you've installed all of the prerequisites, you can run the following commands to get everything working.
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Make DateTimePickerControl a ForwardedRef component"
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
|
||||
Add onKeyDown and readOnlyWhenClosed options to experimentalSelectControl
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Wrap selected items in experimental select control
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: enhancement
|
||||
|
||||
Use BaseControl in the SelectTree label
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Ref } from 'react';
|
||||
import { format as formatDate } from '@wordpress/date';
|
||||
import {
|
||||
createElement,
|
||||
|
@ -9,6 +10,7 @@ import {
|
|||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
forwardRef,
|
||||
} from '@wordpress/element';
|
||||
import { Icon, calendar } from '@wordpress/icons';
|
||||
import moment, { Moment } from 'moment';
|
||||
|
@ -48,306 +50,329 @@ export type DateTimePickerControlProps = {
|
|||
placeholder?: string;
|
||||
help?: string | null;
|
||||
onChangeDebounceWait?: number;
|
||||
} & Omit< React.HTMLAttributes< HTMLDivElement >, 'onChange' >;
|
||||
} & Omit< React.HTMLAttributes< HTMLInputElement >, 'onChange' >;
|
||||
|
||||
export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( {
|
||||
currentDate,
|
||||
isDateOnlyPicker = false,
|
||||
is12HourPicker = true,
|
||||
timeForDateOnly = 'start-of-day',
|
||||
dateTimeFormat,
|
||||
disabled = false,
|
||||
onChange,
|
||||
onBlur,
|
||||
label,
|
||||
placeholder,
|
||||
help,
|
||||
className = '',
|
||||
onChangeDebounceWait = 500,
|
||||
}: DateTimePickerControlProps ) => {
|
||||
const instanceId = useInstanceId( DateTimePickerControl );
|
||||
const id = `inspector-date-time-picker-control-${ instanceId }`;
|
||||
const inputControl = useRef< InputControl >();
|
||||
export const DateTimePickerControl = forwardRef(
|
||||
function ForwardedDateTimePickerControl(
|
||||
{
|
||||
currentDate,
|
||||
isDateOnlyPicker = false,
|
||||
is12HourPicker = true,
|
||||
timeForDateOnly = 'start-of-day',
|
||||
dateTimeFormat,
|
||||
disabled = false,
|
||||
onChange,
|
||||
onBlur,
|
||||
label,
|
||||
placeholder,
|
||||
help,
|
||||
className = '',
|
||||
onChangeDebounceWait = 500,
|
||||
...props
|
||||
}: DateTimePickerControlProps,
|
||||
ref: Ref< HTMLInputElement >
|
||||
) {
|
||||
const id = useInstanceId(
|
||||
DateTimePickerControl,
|
||||
'inspector-date-time-picker-control',
|
||||
props.id
|
||||
) as string;
|
||||
const inputControl = useRef< InputControl >();
|
||||
|
||||
const displayFormat = useMemo( () => {
|
||||
if ( dateTimeFormat ) {
|
||||
return dateTimeFormat;
|
||||
}
|
||||
|
||||
if ( isDateOnlyPicker ) {
|
||||
return defaultDateFormat;
|
||||
}
|
||||
|
||||
if ( is12HourPicker ) {
|
||||
return default12HourDateTimeFormat;
|
||||
}
|
||||
|
||||
return default24HourDateTimeFormat;
|
||||
}, [ dateTimeFormat, isDateOnlyPicker, is12HourPicker ] );
|
||||
|
||||
function parseAsISODateTime(
|
||||
dateString?: string | null,
|
||||
assumeLocalTime = false
|
||||
): Moment {
|
||||
return assumeLocalTime
|
||||
? moment( dateString, moment.ISO_8601, true ).utc()
|
||||
: moment.utc( dateString, moment.ISO_8601, true );
|
||||
}
|
||||
|
||||
function parseAsLocalDateTime( dateString: string | null ): Moment {
|
||||
// parse input date string as local time;
|
||||
// be lenient of user input and try to match any format Moment can
|
||||
return moment( dateString );
|
||||
}
|
||||
|
||||
const maybeForceTime = useCallback(
|
||||
( momentDate: Moment ) => {
|
||||
if ( ! isDateOnlyPicker || ! momentDate.isValid() )
|
||||
return momentDate;
|
||||
|
||||
// We want to set to the start/end of the local time, so
|
||||
// we need to put our Moment instance into "local" mode
|
||||
const updatedMomentDate = momentDate.clone().local();
|
||||
|
||||
if ( timeForDateOnly === 'start-of-day' ) {
|
||||
updatedMomentDate.startOf( 'day' );
|
||||
} else if ( timeForDateOnly === 'end-of-day' ) {
|
||||
updatedMomentDate.endOf( 'day' );
|
||||
const displayFormat = useMemo( () => {
|
||||
if ( dateTimeFormat ) {
|
||||
return dateTimeFormat;
|
||||
}
|
||||
|
||||
return updatedMomentDate;
|
||||
},
|
||||
[ isDateOnlyPicker, timeForDateOnly ]
|
||||
);
|
||||
if ( isDateOnlyPicker ) {
|
||||
return defaultDateFormat;
|
||||
}
|
||||
|
||||
function hasFocusLeftInputAndDropdownContent(
|
||||
event: React.FocusEvent< HTMLInputElement >
|
||||
): boolean {
|
||||
return ! event.relatedTarget?.closest(
|
||||
'.components-dropdown__content'
|
||||
if ( is12HourPicker ) {
|
||||
return default12HourDateTimeFormat;
|
||||
}
|
||||
|
||||
return default24HourDateTimeFormat;
|
||||
}, [ dateTimeFormat, isDateOnlyPicker, is12HourPicker ] );
|
||||
|
||||
function parseAsISODateTime(
|
||||
dateString?: string | null,
|
||||
assumeLocalTime = false
|
||||
): Moment {
|
||||
return assumeLocalTime
|
||||
? moment( dateString, moment.ISO_8601, true ).utc()
|
||||
: moment.utc( dateString, moment.ISO_8601, true );
|
||||
}
|
||||
|
||||
function parseAsLocalDateTime( dateString: string | null ): Moment {
|
||||
// parse input date string as local time;
|
||||
// be lenient of user input and try to match any format Moment can
|
||||
return moment( dateString );
|
||||
}
|
||||
|
||||
const maybeForceTime = useCallback(
|
||||
( momentDate: Moment ) => {
|
||||
if ( ! isDateOnlyPicker || ! momentDate.isValid() )
|
||||
return momentDate;
|
||||
|
||||
// We want to set to the start/end of the local time, so
|
||||
// we need to put our Moment instance into "local" mode
|
||||
const updatedMomentDate = momentDate.clone().local();
|
||||
|
||||
if ( timeForDateOnly === 'start-of-day' ) {
|
||||
updatedMomentDate.startOf( 'day' );
|
||||
} else if ( timeForDateOnly === 'end-of-day' ) {
|
||||
updatedMomentDate.endOf( 'day' );
|
||||
}
|
||||
|
||||
return updatedMomentDate;
|
||||
},
|
||||
[ isDateOnlyPicker, timeForDateOnly ]
|
||||
);
|
||||
|
||||
function hasFocusLeftInputAndDropdownContent(
|
||||
event: React.FocusEvent< HTMLInputElement >
|
||||
): boolean {
|
||||
return ! event.relatedTarget?.closest(
|
||||
'.components-dropdown__content'
|
||||
);
|
||||
}
|
||||
|
||||
const formatDateTimeForDisplay = useCallback(
|
||||
( dateTime: Moment ) => {
|
||||
return dateTime.isValid()
|
||||
? // @ts-expect-error TODO - fix this type error with moment
|
||||
formatDate( displayFormat, dateTime.local() )
|
||||
: dateTime.creationData().input?.toString() || '';
|
||||
},
|
||||
[ displayFormat ]
|
||||
);
|
||||
|
||||
function formatDateTimeAsISO( dateTime: Moment ): string {
|
||||
return dateTime.isValid()
|
||||
? dateTime.utc().toISOString()
|
||||
: dateTime.creationData().input?.toString() || '';
|
||||
}
|
||||
|
||||
const currentDateTime = parseAsISODateTime( currentDate );
|
||||
|
||||
const [ inputString, setInputString ] = useState(
|
||||
currentDateTime.isValid()
|
||||
? formatDateTimeForDisplay( maybeForceTime( currentDateTime ) )
|
||||
: ''
|
||||
);
|
||||
|
||||
const inputStringDateTime = useMemo( () => {
|
||||
return maybeForceTime( parseAsLocalDateTime( inputString ) );
|
||||
}, [ inputString, maybeForceTime ] );
|
||||
|
||||
// We keep a ref to the onChange prop so that we can be sure we are
|
||||
// always using the more up-to-date value, even if it changes
|
||||
// it while a debounced onChange handler is in progress
|
||||
const onChangeRef = useRef<
|
||||
DateTimePickerControlOnChangeHandler | undefined
|
||||
>();
|
||||
useEffect( () => {
|
||||
onChangeRef.current = onChange;
|
||||
}, [ onChange ] );
|
||||
|
||||
const setInputStringAndMaybeCallOnChange = useCallback(
|
||||
( newInputString: string, isUserTypedInput: boolean ) => {
|
||||
// InputControl doesn't fire an onChange if what the user has typed
|
||||
// matches the current value of the input field. To get around this,
|
||||
// we pull the value directly out of the input field. This fixes
|
||||
// the issue where the user ends up typing the same value. Unless they
|
||||
// are typing extra slow. Without this workaround, we miss the last
|
||||
// character typed.
|
||||
const lastTypedValue = inputControl.current.value;
|
||||
|
||||
const newDateTime = maybeForceTime(
|
||||
isUserTypedInput
|
||||
? parseAsLocalDateTime( lastTypedValue )
|
||||
: parseAsISODateTime( newInputString, true )
|
||||
);
|
||||
const isDateTimeSame =
|
||||
newDateTime.isSame( inputStringDateTime );
|
||||
|
||||
if ( isUserTypedInput ) {
|
||||
setInputString( lastTypedValue );
|
||||
} else if ( ! isDateTimeSame ) {
|
||||
setInputString( formatDateTimeForDisplay( newDateTime ) );
|
||||
}
|
||||
|
||||
if (
|
||||
typeof onChangeRef.current === 'function' &&
|
||||
! isDateTimeSame
|
||||
) {
|
||||
onChangeRef.current(
|
||||
newDateTime.isValid()
|
||||
? formatDateTimeAsISO( newDateTime )
|
||||
: lastTypedValue,
|
||||
newDateTime.isValid()
|
||||
);
|
||||
}
|
||||
},
|
||||
[ formatDateTimeForDisplay, inputStringDateTime, maybeForceTime ]
|
||||
);
|
||||
|
||||
const debouncedSetInputStringAndMaybeCallOnChange = useDebounce(
|
||||
setInputStringAndMaybeCallOnChange,
|
||||
onChangeDebounceWait
|
||||
);
|
||||
|
||||
function focusInputControl() {
|
||||
if ( inputControl.current ) {
|
||||
inputControl.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
const getUserInputOrUpdatedCurrentDate = useCallback( () => {
|
||||
if ( currentDate !== undefined ) {
|
||||
const newDateTime = maybeForceTime(
|
||||
parseAsISODateTime( currentDate, false )
|
||||
);
|
||||
|
||||
if ( ! newDateTime.isValid() ) {
|
||||
// keep the invalid string, so the user can correct it
|
||||
return currentDate;
|
||||
}
|
||||
|
||||
if ( ! newDateTime.isSame( inputStringDateTime ) ) {
|
||||
return formatDateTimeForDisplay( newDateTime );
|
||||
}
|
||||
|
||||
// the new currentDate is the same date as the inputString,
|
||||
// so keep exactly what the user typed in
|
||||
return inputString;
|
||||
}
|
||||
|
||||
// the component is uncontrolled (not using currentDate),
|
||||
// so just return the input string
|
||||
return inputString;
|
||||
}, [
|
||||
currentDate,
|
||||
formatDateTimeForDisplay,
|
||||
inputString,
|
||||
maybeForceTime,
|
||||
] );
|
||||
|
||||
// We keep a ref to the onBlur prop so that we can be sure we are
|
||||
// always using the more up-to-date value, otherwise, we get in
|
||||
// any infinite loop when calling onBlur
|
||||
const onBlurRef = useRef< () => void >();
|
||||
useEffect( () => {
|
||||
onBlurRef.current = onBlur;
|
||||
}, [ onBlur ] );
|
||||
|
||||
const callOnBlurIfDropdownIsNotOpening = useCallback( ( willOpen ) => {
|
||||
if ( ! willOpen && typeof onBlurRef.current === 'function' ) {
|
||||
// in case the component is blurred before a debounced
|
||||
// change has been processed, immediately set the input string
|
||||
// to the current value of the input field, so that
|
||||
// it won't be set back to the pre-change value
|
||||
setInputStringAndMaybeCallOnChange(
|
||||
inputControl.current.value,
|
||||
true
|
||||
);
|
||||
onBlurRef.current();
|
||||
}
|
||||
}, [] );
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className={ classNames(
|
||||
'woocommerce-date-time-picker-control',
|
||||
className
|
||||
) }
|
||||
position="bottom left"
|
||||
focusOnMount={ false }
|
||||
// @ts-expect-error `onToggle` does exist.
|
||||
onToggle={ callOnBlurIfDropdownIsNotOpening }
|
||||
renderToggle={ ( { isOpen, onClose, onToggle } ) => (
|
||||
<BaseControl id={ id } label={ label } help={ help }>
|
||||
<InputControl
|
||||
{ ...props }
|
||||
id={ id }
|
||||
ref={ ( element: HTMLInputElement ) => {
|
||||
inputControl.current = element;
|
||||
if ( typeof ref === 'function' ) {
|
||||
ref( element );
|
||||
}
|
||||
} }
|
||||
disabled={ disabled }
|
||||
value={ getUserInputOrUpdatedCurrentDate() }
|
||||
onChange={ ( newValue: string ) =>
|
||||
debouncedSetInputStringAndMaybeCallOnChange(
|
||||
newValue,
|
||||
true
|
||||
)
|
||||
}
|
||||
onBlur={ (
|
||||
event: React.FocusEvent< HTMLInputElement >
|
||||
) => {
|
||||
if (
|
||||
hasFocusLeftInputAndDropdownContent( event )
|
||||
) {
|
||||
// close the dropdown, which will also trigger
|
||||
// the component's onBlur to be called
|
||||
onClose();
|
||||
}
|
||||
} }
|
||||
suffix={
|
||||
<Icon
|
||||
icon={ calendar }
|
||||
className="calendar-icon woocommerce-date-time-picker-control__input-control__suffix"
|
||||
onClick={ focusInputControl }
|
||||
size={ 16 }
|
||||
/>
|
||||
}
|
||||
placeholder={ placeholder }
|
||||
describedBy={ sprintf(
|
||||
/* translators: A datetime format */
|
||||
__(
|
||||
'Date input describing a selected date in format %s',
|
||||
'woocommerce'
|
||||
),
|
||||
dateTimeFormat
|
||||
) }
|
||||
onFocus={ () => {
|
||||
if ( isOpen ) {
|
||||
return; // the dropdown is already open, do we don't need to do anything
|
||||
}
|
||||
|
||||
onToggle(); // show the dropdown
|
||||
} }
|
||||
aria-expanded={ isOpen }
|
||||
/>
|
||||
</BaseControl>
|
||||
) }
|
||||
popoverProps={ {
|
||||
className: 'woocommerce-date-time-picker-control__popover',
|
||||
} }
|
||||
renderContent={ () => {
|
||||
const Picker = isDateOnlyPicker
|
||||
? DatePicker
|
||||
: WpDateTimePicker;
|
||||
|
||||
return (
|
||||
<Picker
|
||||
// @ts-expect-error null is valid for currentDate
|
||||
currentDate={
|
||||
inputStringDateTime.isValid()
|
||||
? formatDateTimeAsISO( inputStringDateTime )
|
||||
: null
|
||||
}
|
||||
onChange={ ( newDateTimeISOString: string ) =>
|
||||
setInputStringAndMaybeCallOnChange(
|
||||
newDateTimeISOString,
|
||||
false
|
||||
)
|
||||
}
|
||||
is12Hour={ is12HourPicker }
|
||||
/>
|
||||
);
|
||||
} }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const formatDateTimeForDisplay = useCallback(
|
||||
( dateTime: Moment ) => {
|
||||
return dateTime.isValid()
|
||||
? // @ts-expect-error TODO - fix this type error with moment
|
||||
formatDate( displayFormat, dateTime.local() )
|
||||
: dateTime.creationData().input?.toString() || '';
|
||||
},
|
||||
[ displayFormat ]
|
||||
);
|
||||
|
||||
function formatDateTimeAsISO( dateTime: Moment ): string {
|
||||
return dateTime.isValid()
|
||||
? dateTime.utc().toISOString()
|
||||
: dateTime.creationData().input?.toString() || '';
|
||||
}
|
||||
|
||||
const currentDateTime = parseAsISODateTime( currentDate );
|
||||
|
||||
const [ inputString, setInputString ] = useState(
|
||||
currentDateTime.isValid()
|
||||
? formatDateTimeForDisplay( maybeForceTime( currentDateTime ) )
|
||||
: ''
|
||||
);
|
||||
|
||||
const inputStringDateTime = useMemo( () => {
|
||||
return maybeForceTime( parseAsLocalDateTime( inputString ) );
|
||||
}, [ inputString, maybeForceTime ] );
|
||||
|
||||
// We keep a ref to the onChange prop so that we can be sure we are
|
||||
// always using the more up-to-date value, even if it changes
|
||||
// it while a debounced onChange handler is in progress
|
||||
const onChangeRef = useRef<
|
||||
DateTimePickerControlOnChangeHandler | undefined
|
||||
>();
|
||||
useEffect( () => {
|
||||
onChangeRef.current = onChange;
|
||||
}, [ onChange ] );
|
||||
|
||||
const setInputStringAndMaybeCallOnChange = useCallback(
|
||||
( newInputString: string, isUserTypedInput: boolean ) => {
|
||||
// InputControl doesn't fire an onChange if what the user has typed
|
||||
// matches the current value of the input field. To get around this,
|
||||
// we pull the value directly out of the input field. This fixes
|
||||
// the issue where the user ends up typing the same value. Unless they
|
||||
// are typing extra slow. Without this workaround, we miss the last
|
||||
// character typed.
|
||||
const lastTypedValue = inputControl.current.value;
|
||||
|
||||
const newDateTime = maybeForceTime(
|
||||
isUserTypedInput
|
||||
? parseAsLocalDateTime( lastTypedValue )
|
||||
: parseAsISODateTime( newInputString, true )
|
||||
);
|
||||
const isDateTimeSame = newDateTime.isSame( inputStringDateTime );
|
||||
|
||||
if ( isUserTypedInput ) {
|
||||
setInputString( lastTypedValue );
|
||||
} else if ( ! isDateTimeSame ) {
|
||||
setInputString( formatDateTimeForDisplay( newDateTime ) );
|
||||
}
|
||||
|
||||
if (
|
||||
typeof onChangeRef.current === 'function' &&
|
||||
! isDateTimeSame
|
||||
) {
|
||||
onChangeRef.current(
|
||||
newDateTime.isValid()
|
||||
? formatDateTimeAsISO( newDateTime )
|
||||
: lastTypedValue,
|
||||
newDateTime.isValid()
|
||||
);
|
||||
}
|
||||
},
|
||||
[ formatDateTimeForDisplay, inputStringDateTime, maybeForceTime ]
|
||||
);
|
||||
|
||||
const debouncedSetInputStringAndMaybeCallOnChange = useDebounce(
|
||||
setInputStringAndMaybeCallOnChange,
|
||||
onChangeDebounceWait
|
||||
);
|
||||
|
||||
function focusInputControl() {
|
||||
if ( inputControl.current ) {
|
||||
inputControl.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
const getUserInputOrUpdatedCurrentDate = useCallback( () => {
|
||||
if ( currentDate !== undefined ) {
|
||||
const newDateTime = maybeForceTime(
|
||||
parseAsISODateTime( currentDate, false )
|
||||
);
|
||||
|
||||
if ( ! newDateTime.isValid() ) {
|
||||
// keep the invalid string, so the user can correct it
|
||||
return currentDate;
|
||||
}
|
||||
|
||||
if ( ! newDateTime.isSame( inputStringDateTime ) ) {
|
||||
return formatDateTimeForDisplay( newDateTime );
|
||||
}
|
||||
|
||||
// the new currentDate is the same date as the inputString,
|
||||
// so keep exactly what the user typed in
|
||||
return inputString;
|
||||
}
|
||||
|
||||
// the component is uncontrolled (not using currentDate),
|
||||
// so just return the input string
|
||||
return inputString;
|
||||
}, [ currentDate, formatDateTimeForDisplay, inputString, maybeForceTime ] );
|
||||
|
||||
// We keep a ref to the onBlur prop so that we can be sure we are
|
||||
// always using the more up-to-date value, otherwise, we get in
|
||||
// any infinite loop when calling onBlur
|
||||
const onBlurRef = useRef< () => void >();
|
||||
useEffect( () => {
|
||||
onBlurRef.current = onBlur;
|
||||
}, [ onBlur ] );
|
||||
|
||||
const callOnBlurIfDropdownIsNotOpening = useCallback( ( willOpen ) => {
|
||||
if ( ! willOpen && typeof onBlurRef.current === 'function' ) {
|
||||
// in case the component is blurred before a debounced
|
||||
// change has been processed, immediately set the input string
|
||||
// to the current value of the input field, so that
|
||||
// it won't be set back to the pre-change value
|
||||
setInputStringAndMaybeCallOnChange(
|
||||
inputControl.current.value,
|
||||
true
|
||||
);
|
||||
onBlurRef.current();
|
||||
}
|
||||
}, [] );
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className={ classNames(
|
||||
'woocommerce-date-time-picker-control',
|
||||
className
|
||||
) }
|
||||
position="bottom left"
|
||||
focusOnMount={ false }
|
||||
// @ts-expect-error `onToggle` does exist.
|
||||
onToggle={ callOnBlurIfDropdownIsNotOpening }
|
||||
renderToggle={ ( { isOpen, onClose, onToggle } ) => (
|
||||
<BaseControl id={ id } label={ label } help={ help }>
|
||||
<InputControl
|
||||
id={ id }
|
||||
ref={ inputControl }
|
||||
disabled={ disabled }
|
||||
value={ getUserInputOrUpdatedCurrentDate() }
|
||||
onChange={ ( newValue: string ) =>
|
||||
debouncedSetInputStringAndMaybeCallOnChange(
|
||||
newValue,
|
||||
true
|
||||
)
|
||||
}
|
||||
onBlur={ (
|
||||
event: React.FocusEvent< HTMLInputElement >
|
||||
) => {
|
||||
if (
|
||||
hasFocusLeftInputAndDropdownContent( event )
|
||||
) {
|
||||
// close the dropdown, which will also trigger
|
||||
// the component's onBlur to be called
|
||||
onClose();
|
||||
}
|
||||
} }
|
||||
suffix={
|
||||
<Icon
|
||||
icon={ calendar }
|
||||
className="calendar-icon woocommerce-date-time-picker-control__input-control__suffix"
|
||||
onClick={ focusInputControl }
|
||||
size={ 16 }
|
||||
/>
|
||||
}
|
||||
placeholder={ placeholder }
|
||||
describedBy={ sprintf(
|
||||
/* translators: A datetime format */
|
||||
__(
|
||||
'Date input describing a selected date in format %s',
|
||||
'woocommerce'
|
||||
),
|
||||
dateTimeFormat
|
||||
) }
|
||||
onFocus={ () => {
|
||||
if ( isOpen ) {
|
||||
return; // the dropdown is already open, do we don't need to do anything
|
||||
}
|
||||
|
||||
onToggle(); // show the dropdown
|
||||
} }
|
||||
aria-expanded={ isOpen }
|
||||
/>
|
||||
</BaseControl>
|
||||
) }
|
||||
popoverProps={ {
|
||||
className: 'woocommerce-date-time-picker-control__popover',
|
||||
} }
|
||||
renderContent={ () => {
|
||||
const Picker = isDateOnlyPicker ? DatePicker : WpDateTimePicker;
|
||||
|
||||
return (
|
||||
<Picker
|
||||
// @ts-expect-error null is valid for currentDate
|
||||
currentDate={
|
||||
inputStringDateTime.isValid()
|
||||
? formatDateTimeAsISO( inputStringDateTime )
|
||||
: null
|
||||
}
|
||||
onChange={ ( newDateTimeISOString: string ) =>
|
||||
setInputStringAndMaybeCallOnChange(
|
||||
newDateTimeISOString,
|
||||
false
|
||||
)
|
||||
}
|
||||
is12Hour={ is12HourPicker }
|
||||
/>
|
||||
);
|
||||
} }
|
||||
/>
|
||||
);
|
||||
};
|
||||
);
|
||||
|
|
|
@ -105,4 +105,6 @@ Name | Type | Default | Description
|
|||
`onInputChange` | Function | `() => null` | A callback that fires when the user input has changed
|
||||
`onRemove` | Function | `() => null` | A callback that fires when a selected item has been removed
|
||||
`onSelect` | Function | `() => null` | A callback that fires when an item has been selected
|
||||
`selected` | Array or Item | `undefined` | An array of selected items or a single selected item
|
||||
`selected` | Array or Item | `undefined` | An array of selected items or a single selected item\
|
||||
`onKeyDown` | Function | `() => null` | A callback that fires when a key is pressed
|
||||
`readOnlyWhenClosed` | Boolean | `false` | Whether the input should be read-only when the menu is closed
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
.woocommerce-experimental-select-control__items-wrapper {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 2px $gap-smaller;
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ export type SelectControlProps< ItemType > = {
|
|||
) => void;
|
||||
onRemove?: ( item: ItemType ) => void;
|
||||
onSelect?: ( selected: ItemType ) => void;
|
||||
onKeyDown?: ( e: KeyboardEvent ) => void;
|
||||
onFocus?: ( data: { inputValue: string } ) => void;
|
||||
stateReducer?: (
|
||||
state: UseComboboxState< ItemType | null >,
|
||||
|
@ -70,6 +71,8 @@ export type SelectControlProps< ItemType > = {
|
|||
inputProps?: GetInputPropsOptions;
|
||||
suffix?: JSX.Element | null;
|
||||
showToggleButton?: boolean;
|
||||
readOnlyWhenClosed?: boolean;
|
||||
|
||||
/**
|
||||
* This is a feature already implemented in downshift@7.0.0 through the
|
||||
* reducer. In order for us to use it this prop is added temporarily until
|
||||
|
@ -118,6 +121,7 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
onRemove = () => null,
|
||||
onSelect = () => null,
|
||||
onFocus = () => null,
|
||||
onKeyDown = () => null,
|
||||
stateReducer = ( state, actionAndChanges ) => actionAndChanges.changes,
|
||||
placeholder,
|
||||
selected,
|
||||
|
@ -126,6 +130,7 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
inputProps = {},
|
||||
suffix = <SuffixIcon icon={ chevronDown } />,
|
||||
showToggleButton = false,
|
||||
readOnlyWhenClosed = true,
|
||||
__experimentalOpenMenuOnFocus = false,
|
||||
}: SelectControlProps< ItemType > ) {
|
||||
const [ isFocused, setIsFocused ] = useState( false );
|
||||
|
@ -247,7 +252,7 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
onRemove( item );
|
||||
};
|
||||
|
||||
const isReadOnly = ! isOpen && ! isFocused;
|
||||
const isReadOnly = readOnlyWhenClosed && ! isOpen && ! isFocused;
|
||||
|
||||
const selectedItemTags = multiple ? (
|
||||
<SelectedItems
|
||||
|
@ -305,6 +310,7 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
setIsFocused( false );
|
||||
}
|
||||
},
|
||||
onKeyDown,
|
||||
placeholder,
|
||||
disabled,
|
||||
...inputProps,
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
.woocommerce-experimental-select-control__selected-items.is-read-only {
|
||||
font-size: 13px;
|
||||
color: $gray-900;
|
||||
font-family: var(--wp--preset--font-family--system-font);
|
||||
.woocommerce-experimental-select-control__selected-items {
|
||||
flex-wrap: wrap;
|
||||
|
||||
&.is-read-only {
|
||||
font-size: 13px;
|
||||
color: $gray-900;
|
||||
font-family: var(--wp--preset--font-family--system-font);
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-experimental-select-control__selected-item {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { chevronDown } from '@wordpress/icons';
|
|||
import classNames from 'classnames';
|
||||
import { createElement, useState } from '@wordpress/element';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { TextControl } from '@wordpress/components';
|
||||
import { BaseControl, TextControl } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -130,61 +130,55 @@ export const SelectTree = function SelectTree( {
|
|||
}
|
||||
) }
|
||||
>
|
||||
<label
|
||||
htmlFor={ `${ props.id }-input` }
|
||||
id={ `${ props.id }-label` }
|
||||
className="woocommerce-experimental-select-control__label"
|
||||
>
|
||||
{ props.label }
|
||||
</label>
|
||||
{ props.multiple ? (
|
||||
<ComboBox
|
||||
comboBoxProps={ {
|
||||
className:
|
||||
'woocommerce-experimental-select-control__combo-box-wrapper',
|
||||
role: 'combobox',
|
||||
'aria-expanded': isOpen,
|
||||
'aria-haspopup': 'tree',
|
||||
'aria-labelledby': `${ props.id }-label`,
|
||||
'aria-owns': `${ props.id }-menu`,
|
||||
} }
|
||||
inputProps={ inputProps }
|
||||
suffix={ suffix }
|
||||
>
|
||||
<SelectedItems
|
||||
isReadOnly={ isReadOnly }
|
||||
items={ ( props.selected as Item[] ) || [] }
|
||||
getItemLabel={ ( item ) => item?.label || '' }
|
||||
getItemValue={ ( item ) => item?.value || '' }
|
||||
onRemove={ ( item ) => {
|
||||
if (
|
||||
! Array.isArray( item ) &&
|
||||
props.onRemove
|
||||
) {
|
||||
props.onRemove( item );
|
||||
<BaseControl label={ props.label } id={ `${ props.id }-input` }>
|
||||
{ props.multiple ? (
|
||||
<ComboBox
|
||||
comboBoxProps={ {
|
||||
className:
|
||||
'woocommerce-experimental-select-control__combo-box-wrapper',
|
||||
role: 'combobox',
|
||||
'aria-expanded': isOpen,
|
||||
'aria-haspopup': 'tree',
|
||||
'aria-owns': `${ props.id }-menu`,
|
||||
} }
|
||||
inputProps={ inputProps }
|
||||
suffix={ suffix }
|
||||
>
|
||||
<SelectedItems
|
||||
isReadOnly={ isReadOnly }
|
||||
items={ ( props.selected as Item[] ) || [] }
|
||||
getItemLabel={ ( item ) => item?.label || '' }
|
||||
getItemValue={ ( item ) => item?.value || '' }
|
||||
onRemove={ ( item ) => {
|
||||
if (
|
||||
! Array.isArray( item ) &&
|
||||
props.onRemove
|
||||
) {
|
||||
props.onRemove( item );
|
||||
}
|
||||
} }
|
||||
getSelectedItemProps={ () => ( {} ) }
|
||||
/>
|
||||
</ComboBox>
|
||||
) : (
|
||||
<TextControl
|
||||
{ ...inputProps }
|
||||
value={ props.createValue || '' }
|
||||
onChange={ ( value ) => {
|
||||
if ( onInputChange ) onInputChange( value );
|
||||
const item = items.find(
|
||||
( i ) => i.label === value
|
||||
);
|
||||
if ( props.onSelect && item ) {
|
||||
props.onSelect( item );
|
||||
}
|
||||
if ( ! value && props.onRemove ) {
|
||||
props.onRemove( props.selected as Item );
|
||||
}
|
||||
} }
|
||||
getSelectedItemProps={ () => ( {} ) }
|
||||
/>
|
||||
</ComboBox>
|
||||
) : (
|
||||
<TextControl
|
||||
{ ...inputProps }
|
||||
value={ props.createValue || '' }
|
||||
onChange={ ( value ) => {
|
||||
if ( onInputChange ) onInputChange( value );
|
||||
const item = items.find(
|
||||
( i ) => i.label === value
|
||||
);
|
||||
if ( props.onSelect && item ) {
|
||||
props.onSelect( item );
|
||||
}
|
||||
if ( ! value && props.onRemove ) {
|
||||
props.onRemove( props.selected as Item );
|
||||
}
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
) }
|
||||
</BaseControl>
|
||||
</div>
|
||||
<SelectTreeMenu
|
||||
{ ...props }
|
||||
|
|
|
@ -33,34 +33,36 @@ export const Tree = forwardRef( function ForwardedTree(
|
|||
|
||||
return (
|
||||
<>
|
||||
<ol
|
||||
{ ...treeProps }
|
||||
className={ classNames(
|
||||
treeProps.className,
|
||||
'experimental-woocommerce-tree',
|
||||
`experimental-woocommerce-tree--level-${ level }`
|
||||
) }
|
||||
>
|
||||
{ items.map( ( child, index ) => (
|
||||
<TreeItem
|
||||
{ ...treeItemProps }
|
||||
isExpanded={ props.isExpanded }
|
||||
key={ child.data.value }
|
||||
item={ child }
|
||||
index={ index }
|
||||
// Button ref is not working, so need to use CSS directly
|
||||
onLastItemLoop={ () => {
|
||||
(
|
||||
rootListRef.current
|
||||
?.closest( 'ol[role="tree"]' )
|
||||
?.parentElement?.querySelector(
|
||||
'.experimental-woocommerce-tree__button'
|
||||
) as HTMLButtonElement
|
||||
)?.focus();
|
||||
} }
|
||||
/>
|
||||
) ) }
|
||||
</ol>
|
||||
{ items.length || isCreateButtonVisible ? (
|
||||
<ol
|
||||
{ ...treeProps }
|
||||
className={ classNames(
|
||||
treeProps.className,
|
||||
'experimental-woocommerce-tree',
|
||||
`experimental-woocommerce-tree--level-${ level }`
|
||||
) }
|
||||
>
|
||||
{ items.map( ( child, index ) => (
|
||||
<TreeItem
|
||||
{ ...treeItemProps }
|
||||
isExpanded={ props.isExpanded }
|
||||
key={ child.data.value }
|
||||
item={ child }
|
||||
index={ index }
|
||||
// Button ref is not working, so need to use CSS directly
|
||||
onLastItemLoop={ () => {
|
||||
(
|
||||
rootListRef.current
|
||||
?.closest( 'ol[role="tree"]' )
|
||||
?.parentElement?.querySelector(
|
||||
'.experimental-woocommerce-tree__button'
|
||||
) as HTMLButtonElement
|
||||
)?.focus();
|
||||
} }
|
||||
/>
|
||||
) ) }
|
||||
</ol>
|
||||
) : null }
|
||||
{ isCreateButtonVisible && (
|
||||
<Button
|
||||
className="experimental-woocommerce-tree__button"
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Added types for resolveSelect where applicable.
|
|
@ -14,6 +14,7 @@ import * as actions from './actions';
|
|||
import * as resolvers from './resolvers';
|
||||
import reducer, { State } from './reducer';
|
||||
import { WPDataSelectors } from '../types';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
export * from './types';
|
||||
export type { State };
|
||||
|
||||
|
@ -35,4 +36,7 @@ declare module '@wordpress/data' {
|
|||
function select(
|
||||
key: typeof STORE_NAME
|
||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_NAME
|
||||
): PromiseifySelectors< SelectFromMap< typeof selectors > >;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import * as actions from './actions';
|
|||
import reducer, { State } from './reducer';
|
||||
import { WPDataSelectors } from '../types';
|
||||
import controls from '../controls';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
export * from './types';
|
||||
export type { State };
|
||||
|
||||
|
@ -33,4 +34,7 @@ declare module '@wordpress/data' {
|
|||
function select(
|
||||
key: typeof STORE_NAME
|
||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_NAME
|
||||
): PromiseifySelectors< SelectFromMap< typeof selectors > >;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import * as actions from './actions';
|
|||
import * as resolvers from './resolvers';
|
||||
import reducer, { State } from './reducer';
|
||||
import { WPDataSelectors } from '../types';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
export * from './types';
|
||||
export type { State };
|
||||
|
||||
|
@ -35,4 +36,7 @@ declare module '@wordpress/data' {
|
|||
function select(
|
||||
key: typeof STORE_NAME
|
||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_NAME
|
||||
): PromiseifySelectors< SelectFromMap< typeof selectors > >;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import reducer, { State } from './reducer';
|
|||
import controls from '../controls';
|
||||
import { WPDataActions, WPDataSelectors } from '../types';
|
||||
import { getItemsType } from './selectors';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
export * from './types';
|
||||
export type { State };
|
||||
|
||||
|
@ -40,4 +41,7 @@ declare module '@wordpress/data' {
|
|||
key: typeof STORE_NAME
|
||||
): DispatchFromMap< typeof actions & WPDataActions >;
|
||||
function select( key: typeof STORE_NAME ): ItemsSelector;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_NAME
|
||||
): PromiseifySelectors< ItemsSelector >;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import reducer, { State } from './reducer';
|
|||
import * as resolvers from './resolvers';
|
||||
import initDispatchers from './dispatchers';
|
||||
import { WPDataActions, WPDataSelectors } from '../types';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
|
||||
registerStore< State >( STORE_NAME, {
|
||||
reducer: reducer as Reducer< State, AnyAction >,
|
||||
|
@ -35,4 +36,7 @@ declare module '@wordpress/data' {
|
|||
function select(
|
||||
key: typeof STORE_NAME
|
||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_NAME
|
||||
): PromiseifySelectors< SelectFromMap< typeof selectors > >;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import * as selectors from './selectors';
|
|||
import * as actions from './actions';
|
||||
import * as resolvers from './resolvers';
|
||||
import reducer, { State } from './reducer';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
|
||||
export * from './types';
|
||||
export type { State };
|
||||
|
@ -36,4 +37,7 @@ declare module '@wordpress/data' {
|
|||
function select(
|
||||
key: typeof STORE_NAME
|
||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_NAME
|
||||
): PromiseifySelectors< SelectFromMap< typeof selectors > >;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import * as actions from './actions';
|
|||
import * as resolvers from './resolvers';
|
||||
import reducer, { State } from './reducer';
|
||||
import { WPDataActions, WPDataSelectors } from '../types';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
export * from './types';
|
||||
export type { State };
|
||||
|
||||
|
@ -35,4 +36,7 @@ declare module '@wordpress/data' {
|
|||
key: typeof STORE_NAME
|
||||
): DispatchFromMap< typeof actions & WPDataActions >;
|
||||
function select( key: typeof STORE_NAME ): OnboardingSelector;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_NAME
|
||||
): PromiseifySelectors< OnboardingSelector >;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import * as resolvers from './resolvers';
|
|||
import reducer, { State } from './reducer';
|
||||
import { controls } from './controls';
|
||||
import { WPDataActions, WPDataSelectors } from '../types';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
export * from './types';
|
||||
export type { State };
|
||||
|
||||
|
@ -34,4 +35,7 @@ declare module '@wordpress/data' {
|
|||
function select(
|
||||
key: typeof STORE_NAME
|
||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_NAME
|
||||
): PromiseifySelectors< SelectFromMap< typeof selectors > >;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import * as selectors from './selectors';
|
|||
import reducer from './reducer';
|
||||
import { STORE_KEY } from './constants';
|
||||
import { WPDataActions } from '../types';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
|
||||
export const PAYMENT_GATEWAYS_STORE_NAME = STORE_KEY;
|
||||
|
||||
|
@ -33,4 +34,7 @@ declare module '@wordpress/data' {
|
|||
function select(
|
||||
key: typeof STORE_KEY
|
||||
): SelectFromMap< typeof selectors > & WPDataActions;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_KEY
|
||||
): PromiseifySelectors< SelectFromMap< typeof selectors > >;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import * as actions from './actions';
|
|||
import * as resolvers from './resolvers';
|
||||
import reducer, { State } from './reducer';
|
||||
import { WPDataActions, WPDataSelectors } from '../types';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
export * from './types';
|
||||
export type { State };
|
||||
|
||||
|
@ -33,4 +34,7 @@ declare module '@wordpress/data' {
|
|||
function select(
|
||||
key: typeof STORE_NAME
|
||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_NAME
|
||||
): PromiseifySelectors< SelectFromMap< typeof selectors > >;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import * as actions from './actions';
|
|||
import * as resolvers from './resolvers';
|
||||
import reducer, { State } from './reducer';
|
||||
import { WPDataSelectors } from '../types';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
export * from './types';
|
||||
export type { State };
|
||||
|
||||
|
@ -34,4 +35,7 @@ declare module '@wordpress/data' {
|
|||
function select(
|
||||
key: typeof STORE_NAME
|
||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_NAME
|
||||
): PromiseifySelectors< SelectFromMap< typeof selectors > >;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
ReportStatObjectInfer,
|
||||
ReportStatQueryParams,
|
||||
} from './types';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
export * from './types';
|
||||
export type { State };
|
||||
|
||||
|
@ -57,4 +58,7 @@ declare module '@wordpress/data' {
|
|||
key: typeof STORE_NAME
|
||||
): DispatchFromMap< typeof actions & WPDataActions >;
|
||||
function select( key: typeof STORE_NAME ): ReportsSelect;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_NAME
|
||||
): PromiseifySelectors< ReportsSelect >;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import * as resolvers from './resolvers';
|
|||
import controls from '../controls';
|
||||
import reducer, { State } from './reducer';
|
||||
import { WPDataActions, WPDataSelectors } from '../types';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
|
||||
export * from './types';
|
||||
export type { State };
|
||||
|
@ -39,4 +40,7 @@ declare module '@wordpress/data' {
|
|||
function select(
|
||||
key: typeof STORE_NAME
|
||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_NAME
|
||||
): PromiseifySelectors< ReviewSelector >;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import * as actions from './actions';
|
|||
import * as resolvers from './resolvers';
|
||||
import reducer, { State } from './reducer';
|
||||
import { SettingsState } from './types';
|
||||
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||
export * from './types';
|
||||
export type { State };
|
||||
|
||||
|
@ -37,4 +38,7 @@ declare module '@wordpress/data' {
|
|||
function select(
|
||||
key: typeof STORE_NAME
|
||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
||||
function resolveSelect(
|
||||
key: typeof STORE_NAME
|
||||
): PromiseifySelectors< SelectFromMap< typeof selectors > >;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Type helper that maps select() return types to their resolveSelect() return types.
|
||||
* This works by mapping over each Selector, and returning a function that
|
||||
* returns a Promise of the Selector's return type.
|
||||
*/
|
||||
|
||||
export type PromiseifySelectors< Selectors > = {
|
||||
[ SelectorFunction in keyof Selectors ]: Selectors[ SelectorFunction ] extends (
|
||||
...args: infer SelectorArgs
|
||||
) => infer SelectorReturnType
|
||||
? ( ...args: SelectorArgs ) => Promise< SelectorReturnType >
|
||||
: never;
|
||||
};
|
|
@ -5,6 +5,10 @@ import { setLocaleData } from '@wordpress/i18n';
|
|||
import { registerStore } from '@wordpress/data';
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
// Mock the config module to avoid errors like:
|
||||
// Core Error: Could not find config value for key ${ key }. Please make sure that if you need it then it has a default value assigned in config/_shared.json.
|
||||
jest.mock( '@automattic/calypso-config' );
|
||||
|
||||
// Due to the dependency @wordpress/compose which introduces the use of
|
||||
// ResizeObserver this global mock is required for some tests to work.
|
||||
global.ResizeObserver = require( 'resize-observer-polyfill' );
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Moved geolocation country matching functions to @woocommerce/onboarding
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"rootDir": "./src",
|
||||
"preset": "../node_modules/@woocommerce/internal-js-tests/jest-preset.js"
|
||||
}
|
|
@ -37,14 +37,19 @@
|
|||
"@wordpress/components": "wp-6.0",
|
||||
"@wordpress/element": "wp-6.0",
|
||||
"@wordpress/i18n": "wp-6.0",
|
||||
"string-similarity": "4.0.4",
|
||||
"gridicons": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.5",
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@types/string-similarity" : "4.0.0",
|
||||
"@types/wordpress__components": "^19.10.3",
|
||||
"@types/wordpress__data": "^6.0.0",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@woocommerce/eslint-plugin": "workspace:*",
|
||||
"@woocommerce/internal-style-build": "workspace:*",
|
||||
"@woocommerce/internal-js-tests": "workspace:*",
|
||||
"@wordpress/browserslist-config": "wp-6.0",
|
||||
"css-loader": "^3.6.0",
|
||||
"eslint": "^8.32.0",
|
||||
|
@ -62,6 +67,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"turbo:build": "pnpm run build:js && pnpm run build:css",
|
||||
"turbo:test": "jest --config ./jest.config.json",
|
||||
"prepare": "composer install",
|
||||
"changelog": "composer exec -- changelogger",
|
||||
"clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*",
|
||||
|
@ -70,12 +76,15 @@
|
|||
"build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json",
|
||||
"build:css": "webpack",
|
||||
"start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\" \"webpack --watch\"",
|
||||
"test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name",
|
||||
"prepack": "pnpm run clean && pnpm run build",
|
||||
"lint:fix": "eslint src --fix"
|
||||
"lint:fix": "eslint src --fix",
|
||||
"test-staged": "jest --bail --config ./jest.config.json --findRelatedTests"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.(t|j)s?(x)": [
|
||||
"pnpm lint:fix"
|
||||
"pnpm lint:fix",
|
||||
"pnpm test-staged"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,3 +15,4 @@ export { WooPaymentGatewayConfigure } from './components/WooPaymentGatewayConfig
|
|||
export { WooOnboardingTaskListItem } from './components/WooOnboardingTaskListItem';
|
||||
export { WooOnboardingTaskListHeader } from './components/WooOnboardingTaskListHeader';
|
||||
export { WooOnboardingTask } from './components/WooOnboardingTask';
|
||||
export * from './utils/countries';
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import stringSimilarity from 'string-similarity';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getMappingRegion } from './location-mapping';
|
||||
|
||||
/**
|
||||
* Country state option.
|
||||
*/
|
||||
export type CountryStateOption = {
|
||||
key: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type Location = {
|
||||
country_short?: string;
|
||||
region?: string;
|
||||
city?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a country option for the given location.
|
||||
*/
|
||||
export const findCountryOption = (
|
||||
countryStateOptions: CountryStateOption[],
|
||||
location: Location | undefined,
|
||||
minimumSimilarity = 0.7
|
||||
) => {
|
||||
if ( ! location ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let match = null;
|
||||
let matchSimilarity = minimumSimilarity;
|
||||
// eslint-disable-next-line @wordpress/no-unused-vars-before-return -- don't want to put this inside the loop
|
||||
const mappingRegion = getMappingRegion( location );
|
||||
|
||||
for ( const option of countryStateOptions ) {
|
||||
// Country matches exactly.
|
||||
if ( option.key === location.country_short ) {
|
||||
return option;
|
||||
}
|
||||
|
||||
// Countries have regions such as 'US:CA'.
|
||||
const countryCode = option.key.split( ':' )[ 0 ];
|
||||
if (
|
||||
countryCode === location.country_short &&
|
||||
option.label.includes( '—' )
|
||||
) {
|
||||
const wcRegion = option.label.split( '—' )[ 1 ].trim();
|
||||
|
||||
// Region matches exactly with mapping.
|
||||
if ( mappingRegion === wcRegion ) {
|
||||
return option;
|
||||
}
|
||||
|
||||
// Find the region with the highest similarity.
|
||||
const similarity = Math.max(
|
||||
stringSimilarity.compareTwoStrings(
|
||||
wcRegion,
|
||||
location.region || ''
|
||||
),
|
||||
stringSimilarity.compareTwoStrings(
|
||||
wcRegion,
|
||||
location.city || ''
|
||||
)
|
||||
);
|
||||
if ( similarity >= matchSimilarity ) {
|
||||
match = option;
|
||||
matchSimilarity = similarity;
|
||||
}
|
||||
}
|
||||
}
|
||||
return match;
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Location } from '.';
|
||||
|
||||
/**
|
||||
* This file is used to map a location to a WC region label
|
||||
* so that we can find the correct country option
|
||||
* since WPCOM geolocation returns a different
|
||||
* region or city name.
|
||||
*/
|
||||
|
||||
// Key is the country code, value is an object with keys as region/city names and values as WC region labels.
|
||||
const MAPPING: Record< string, Record< string, string > > = {
|
||||
PH: {
|
||||
'National Capital Region': __( 'Metro Manila', 'woocommerce' ),
|
||||
},
|
||||
IT: {
|
||||
Rome: __( 'Roma', 'woocommerce' ),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a WC mapping region name for the given country, region and city.
|
||||
*/
|
||||
export const getMappingRegion = ( {
|
||||
country_short: countryCode,
|
||||
region = '',
|
||||
city = '',
|
||||
}: Location ) => {
|
||||
if ( ! countryCode ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const countryMapping = MAPPING[ countryCode ];
|
||||
if ( ! countryMapping ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const regionMapping = countryMapping[ region ];
|
||||
if ( regionMapping ) {
|
||||
return regionMapping;
|
||||
}
|
||||
|
||||
const cityMapping = countryMapping[ city ];
|
||||
if ( cityMapping ) {
|
||||
return cityMapping;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default MAPPING;
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,96 @@
|
|||
/* eslint-disable camelcase */
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { findCountryOption } from '../../';
|
||||
import { countryStateOptions } from './country-options';
|
||||
import { locations } from './locations';
|
||||
|
||||
describe( 'findCountryOption', () => {
|
||||
it( 'should return null on undefined location', () => {
|
||||
const location = undefined;
|
||||
expect( findCountryOption( countryStateOptions, location ) ).toEqual(
|
||||
null
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should return null when not found', () => {
|
||||
const location = { country_short: 'US' };
|
||||
expect( findCountryOption( countryStateOptions, location ) ).toBeNull();
|
||||
} );
|
||||
|
||||
it.each( [
|
||||
{
|
||||
location: { country_short: 'TW' },
|
||||
expected: {
|
||||
key: 'TW',
|
||||
label: 'Taiwan',
|
||||
},
|
||||
},
|
||||
{
|
||||
location: { country_short: 'US', region: 'California' },
|
||||
expected: {
|
||||
key: 'US:CA',
|
||||
label: 'United States (US) — California',
|
||||
},
|
||||
},
|
||||
{
|
||||
location: { country_short: 'ES', region: 'Madrid, Comunidad de' },
|
||||
expected: {
|
||||
key: 'ES:M',
|
||||
label: 'Spain — Madrid',
|
||||
},
|
||||
},
|
||||
{
|
||||
location: {
|
||||
country_short: 'AR',
|
||||
region: 'Ciudad Autonoma de Buenos Aires',
|
||||
},
|
||||
expected: {
|
||||
key: 'AR:C',
|
||||
label: 'Argentina — Ciudad Autónoma de Buenos Aires',
|
||||
},
|
||||
},
|
||||
{
|
||||
location: {
|
||||
country_short: 'IT',
|
||||
region: 'Lazio',
|
||||
city: 'Rome',
|
||||
},
|
||||
expected: {
|
||||
key: 'IT:RM',
|
||||
label: 'Italy — Roma',
|
||||
},
|
||||
},
|
||||
{
|
||||
location: {
|
||||
country_short: 'PH',
|
||||
region: 'National Capital Region',
|
||||
city: 'Makati',
|
||||
},
|
||||
expected: {
|
||||
key: 'PH:00',
|
||||
label: 'Philippines — Metro Manila',
|
||||
},
|
||||
},
|
||||
] )(
|
||||
'should return the country option for location $expected',
|
||||
( { location, expected } ) => {
|
||||
expect(
|
||||
findCountryOption( countryStateOptions, location, 0.4 )
|
||||
).toEqual( expected );
|
||||
}
|
||||
);
|
||||
|
||||
it( 'should return a country option for > 98% locations', () => {
|
||||
let matchCount = 0;
|
||||
locations.forEach( ( location ) => {
|
||||
if ( findCountryOption( countryStateOptions, location, 0.4 ) ) {
|
||||
matchCount++;
|
||||
}
|
||||
} );
|
||||
expect( matchCount / locations.length ).toBeGreaterThan( 0.98 );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,41 @@
|
|||
/* eslint-disable camelcase */
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getMappingRegion } from '../../location-mapping';
|
||||
|
||||
describe( 'getMappingRegion', () => {
|
||||
it( 'should return null for an empty location', () => {
|
||||
expect( getMappingRegion( {} ) ).toBeNull();
|
||||
} );
|
||||
|
||||
it( 'should return null for a location that is not in the mapping', () => {
|
||||
expect(
|
||||
getMappingRegion( { country_short: 'US', region: 'California' } )
|
||||
).toBeNull();
|
||||
} );
|
||||
|
||||
it( 'should return null for a location with no region', () => {
|
||||
expect( getMappingRegion( { country_short: 'PH' } ) ).toBeNull();
|
||||
} );
|
||||
|
||||
it( 'should return the region for a location that is in the mapping with a region', () => {
|
||||
expect(
|
||||
getMappingRegion( {
|
||||
country_short: 'PH',
|
||||
region: 'National Capital Region',
|
||||
} )
|
||||
).toBe( 'Metro Manila' );
|
||||
} );
|
||||
|
||||
it( 'should return the region for a location that is in the mapping with a city', () => {
|
||||
expect(
|
||||
getMappingRegion( {
|
||||
country_short: 'IT',
|
||||
region: 'Lazio',
|
||||
city: 'Rome',
|
||||
} )
|
||||
).toBe( 'Roma' );
|
||||
} );
|
||||
} );
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +0,0 @@
|
|||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.545 14.4296L13.9351 20.0409C13.7898 20.1865 13.6172 20.3019 13.4272 20.3807C13.2373 20.4595 13.0336 20.5 12.828 20.5C12.6224 20.5 12.4187 20.4595 12.2288 20.3807C12.0388 20.3019 11.8662 20.1865 11.7209 20.0409L5 13.3261V5.5H12.8241L19.545 12.2226C19.8364 12.5159 20 12.9126 20 13.3261C20 13.7396 19.8364 14.1363 19.545 14.4296V14.4296Z" stroke="#1E1E1E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="9" cy="9.5" r="1" fill="#1E1E1E"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 585 B |
|
@ -1,7 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" view-box="0 0 24 24">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5 5.5h14a.5.5 0 01.5.5v1.5a.5.5 0 01-.5.5H5a.5.5 0 01-.5-.5V6a.5.5 0 01.5-.5zM4 9.232A2 2 0 013 7.5V6a2 2 0 012-2h14a2 2 0 012 2v1.5a2 2 0 01-1 1.732V18a2 2 0 01-2 2H6a2 2 0 01-2-2V9.232zm1.5.268V18a.5.5 0 00.5.5h12a.5.5 0 00.5-.5V9.5h-13z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 371 B |
|
@ -1,14 +0,0 @@
|
|||
<svg view-box="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M19 5H5C4.72386 5 4.5 5.22386 4.5 5.5V19.5C4.5 19.7761 4.72386 20 5 20H19C19.2761 20 19.5 19.7761 19.5 19.5V5.5C19.5 5.22386 19.2761 5 19 5ZM5 3.5C3.89543 3.5 3 4.39543 3 5.5V19.5C3 20.6046 3.89543 21.5 5 21.5H19C20.1046 21.5 21 20.6046 21 19.5V5.5C21 4.39543 20.1046 3.5 19 3.5H5Z"
|
||||
fill="#1E1E1E"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15.4772 10.9623C15.7683 10.6792 16.2317 10.6792 16.5228 10.9623L20.5228 14.8511L19.4772 15.9266L16 12.546L12.5228 15.9266C12.2719 16.1706 11.8857 16.2086 11.5921 16.0183L8.59643 14.0766L4.44186 17.106L3.55811 15.894L8.12953 12.5607C8.38061 12.3776 8.71858 12.3683 8.97934 12.5373L11.906 14.4342L15.4772 10.9623Z"
|
||||
fill="#1E1E1E"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 820 B |
|
@ -1,8 +0,0 @@
|
|||
<svg view-box="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6 6H18C18.2761 6 18.5 6.22386 18.5 6.5V13.5H15.5H14C14 14.6046 13.1046 15.5 12 15.5C10.8954 15.5 10 14.6046 10 13.5H8.5H5.5V6.5C5.5 6.22386 5.72386 6 6 6ZM5.5 15V18.5C5.5 18.7761 5.72386 19 6 19H18C18.2761 19 18.5 18.7761 18.5 18.5V15H15.1632C14.6015 16.1825 13.3962 17 12 17C10.6038 17 9.39855 16.1825 8.83682 15H5.5ZM4 13.5V6.5C4 5.39543 4.89543 4.5 6 4.5H18C19.1046 4.5 20 5.39543 20 6.5V13.5V15V18.5C20 19.6046 19.1046 20.5 18 20.5H6C4.89543 20.5 4 19.6046 4 18.5V15V13.5Z"
|
||||
fill="#1E1E1E"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 625 B |
|
@ -1,7 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" view-box="0 0 24 24">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.83 6.342l.602.3.625-.25.443-.176v12.569l-.443-.178-.625-.25-.603.301-1.444.723-2.41-.804-.475-.158-.474.158-2.41.803-1.445-.722-.603-.3-.625.25-.443.177V6.215l.443.178.625.25.603-.301 1.444-.722 2.41.803.475.158.474-.158 2.41-.803 1.445.722zM20 4l-1.5.6-1 .4-2-1-3 1-3-1-2 1-1-.4L5 4v17l1.5-.6 1-.4 2 1 3-1 3 1 2-1 1 .4 1.5.6V4zm-3.5 6.25v-1.5h-8v1.5h8zm0 3v-1.5h-8v1.5h8zm-8 3v-1.5h8v1.5h-8z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 527 B |
|
@ -1,8 +0,0 @@
|
|||
<svg view-box="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3.5 7.75C3.5 6.7835 4.2835 6 5.25 6H14.75H15.5V6.75V9H17.25H17.5607L17.7803 9.21967L20.7803 12.2197L21 12.4393V12.75V14.75C21 15.7165 20.2165 16.5 19.25 16.5H19.2377C19.2458 16.5822 19.25 16.6656 19.25 16.75C19.25 18.1307 18.1307 19.25 16.75 19.25C15.3693 19.25 14.25 18.1307 14.25 16.75C14.25 16.6656 14.2542 16.5822 14.2623 16.5H14H10.2377C10.2458 16.5822 10.25 16.6656 10.25 16.75C10.25 18.1307 9.13071 19.25 7.75 19.25C6.36929 19.25 5.25 18.1307 5.25 16.75C5.25 16.6656 5.25418 16.5822 5.26234 16.5H4.25H3.5V15.75V7.75ZM14 15V9.75V9V7.5H5.25C5.11193 7.5 5 7.61193 5 7.75V15H5.96464C6.41837 14.5372 7.05065 14.25 7.75 14.25C8.44935 14.25 9.08163 14.5372 9.53536 15H14ZM18.5354 15H19.25C19.3881 15 19.5 14.8881 19.5 14.75V13.0607L16.9393 10.5H15.5V14.5845C15.8677 14.3717 16.2946 14.25 16.75 14.25C17.4493 14.25 18.0816 14.5372 18.5354 15ZM6.7815 16.5C6.76094 16.5799 6.75 16.6637 6.75 16.75C6.75 17.3023 7.19772 17.75 7.75 17.75C8.30228 17.75 8.75 17.3023 8.75 16.75C8.75 16.6637 8.73906 16.5799 8.7185 16.5C8.60749 16.0687 8.21596 15.75 7.75 15.75C7.28404 15.75 6.89251 16.0687 6.7815 16.5ZM15.7815 16.5C15.7609 16.5799 15.75 16.6637 15.75 16.75C15.75 17.3023 16.1977 17.75 16.75 17.75C17.3023 17.75 17.75 17.3023 17.75 16.75C17.75 16.6637 17.7391 16.5799 17.7185 16.5C17.7144 16.4841 17.7099 16.4683 17.705 16.4526C17.5784 16.0456 17.1987 15.75 16.75 15.75C16.284 15.75 15.8925 16.0687 15.7815 16.5Z"
|
||||
fill="#1E1E1E"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Fix validation behavior#37984
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Update shipping class block to match new designs#38044
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Only register blocks when user navigates to the product edit page#38200
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add error specific messages to product save functionality
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Make DateTimePickerControl a ForwardedRef
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Wait for editor changes to be debounced before closing modal
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Fix double scrollbars on product editor page
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
List price gets reset on blur when error is shown#38221
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Remove css unrelated to the product block editor.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Limit 'email me when stock reaches' field to numerical only#38244
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Adjust styling based on internal styling feedback
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: enhancement
|
||||
|
||||
Use SelectTree in the Parent Category field
|
|
@ -45,7 +45,7 @@ export function Edit( { attributes }: { attributes: BlockAttributes } ) {
|
|||
<span className="woocommerce-product-form__checkbox-tooltip-icon">
|
||||
<Icon
|
||||
icon={ help }
|
||||
size={ 20 }
|
||||
size={ 22 }
|
||||
fill="#949494"
|
||||
/>
|
||||
</span>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
.components-base-control__field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.components-checkbox-control__label {
|
||||
|
@ -11,6 +12,8 @@
|
|||
|
||||
&-tooltip-icon {
|
||||
margin: -2px 0 0 $gap-small;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,8 +13,8 @@ export { init as initRegularPrice } from './regular-price';
|
|||
export { init as initSalePrice } from './sale-price';
|
||||
export { init as initScheduleSale } from './schedule-sale';
|
||||
export { init as initSection } from './section';
|
||||
export { init as initShippingClass } from './shipping-class';
|
||||
export { init as initShippingDimensions } from './shipping-dimensions';
|
||||
export { init as initShippingFee } from './shipping-fee';
|
||||
export { init as initSummary } from './summary';
|
||||
export { init as initTab } from './tab';
|
||||
export { init as initInventoryQuantity } from './inventory-quantity';
|
||||
|
|
|
@ -3,16 +3,16 @@
|
|||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Link } from '@woocommerce/components';
|
||||
|
||||
import { Product } from '@woocommerce/data';
|
||||
import {
|
||||
createElement,
|
||||
Fragment,
|
||||
createInterpolateElement,
|
||||
} from '@wordpress/element';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import {
|
||||
BaseControl,
|
||||
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||
|
@ -23,54 +23,91 @@ import {
|
|||
// eslint-disable-next-line @woocommerce/dependency-group
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
|
||||
export function Edit() {
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useValidation } from '../../contexts/validation-context';
|
||||
import { InventoryEmailBlockAttributes } from './types';
|
||||
|
||||
export function Edit( {
|
||||
clientId,
|
||||
}: BlockEditProps< InventoryEmailBlockAttributes > ) {
|
||||
const blockProps = useBlockProps();
|
||||
const notifyLowStockAmount = getSetting( 'notifyLowStockAmount', 2 );
|
||||
|
||||
const [ lowStockAmount, setLowStockAmount ] = useEntityProp(
|
||||
const [ lowStockAmount, setLowStockAmount ] = useEntityProp< number >(
|
||||
'postType',
|
||||
'product',
|
||||
'low_stock_amount'
|
||||
);
|
||||
|
||||
const id = useInstanceId( BaseControl, 'low_stock_amount' ) as string;
|
||||
|
||||
const {
|
||||
ref: lowStockAmountRef,
|
||||
error: lowStockAmountValidationError,
|
||||
validate: validateLowStockAmount,
|
||||
} = useValidation< Product >(
|
||||
`low_stock_amount-${ clientId }`,
|
||||
async function stockQuantityValidator() {
|
||||
if ( lowStockAmount && lowStockAmount < 0 ) {
|
||||
return __(
|
||||
'This field must be a positive number.',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
},
|
||||
[ lowStockAmount ]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div { ...blockProps }>
|
||||
<div className="wp-block-columns">
|
||||
<div className="wp-block-column">
|
||||
<BaseControl
|
||||
id={ 'product_inventory_email' }
|
||||
id={ id }
|
||||
label={ __(
|
||||
'Email me when stock reaches',
|
||||
'woocommerce'
|
||||
) }
|
||||
help={ createInterpolateElement(
|
||||
__(
|
||||
'Make sure to enable notifications in <link>store settings.</link>',
|
||||
'woocommerce'
|
||||
),
|
||||
{
|
||||
link: (
|
||||
<Link
|
||||
href={ `${ getSetting(
|
||||
'adminUrl'
|
||||
) }admin.php?page=wc-settings&tab=products§ion=inventory` }
|
||||
target="_blank"
|
||||
type="external"
|
||||
></Link>
|
||||
help={
|
||||
lowStockAmountValidationError ||
|
||||
createInterpolateElement(
|
||||
__(
|
||||
'Make sure to enable notifications in <link>store settings.</link>',
|
||||
'woocommerce'
|
||||
),
|
||||
}
|
||||
) }
|
||||
{
|
||||
link: (
|
||||
<Link
|
||||
href={ `${ getSetting(
|
||||
'adminUrl'
|
||||
) }admin.php?page=wc-settings&tab=products§ion=inventory` }
|
||||
target="_blank"
|
||||
type="external"
|
||||
></Link>
|
||||
),
|
||||
}
|
||||
)
|
||||
}
|
||||
className={
|
||||
lowStockAmountValidationError && 'has-error'
|
||||
}
|
||||
>
|
||||
<InputControl
|
||||
name={ 'woocommerce-product-name' }
|
||||
id={ id }
|
||||
ref={ lowStockAmountRef }
|
||||
name={ 'low_stock_amount' }
|
||||
placeholder={ sprintf(
|
||||
// translators: Default quantity to notify merchants of low stock.
|
||||
__( '%d (store default)', 'woocommerce' ),
|
||||
notifyLowStockAmount
|
||||
) }
|
||||
onChange={ setLowStockAmount }
|
||||
onBlur={ validateLowStockAmount }
|
||||
value={ lowStockAmount }
|
||||
type="number"
|
||||
min={ 0 }
|
||||
/>
|
||||
</BaseControl>
|
||||
|
|
|
@ -1,22 +1,29 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { BlockConfiguration } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { initBlock } from '../../utils';
|
||||
import metadata from './block.json';
|
||||
import { initBlock } from '../../utils/init-blocks';
|
||||
import blockConfiguration from './block.json';
|
||||
import { Edit } from './edit';
|
||||
import { InventoryEmailBlockAttributes } from './types';
|
||||
|
||||
const { name } = metadata;
|
||||
const { name, ...metadata } =
|
||||
blockConfiguration as BlockConfiguration< InventoryEmailBlockAttributes >;
|
||||
|
||||
export { metadata, name };
|
||||
|
||||
export const settings = {
|
||||
export const settings: Partial<
|
||||
BlockConfiguration< InventoryEmailBlockAttributes >
|
||||
> = {
|
||||
example: {},
|
||||
edit: Edit,
|
||||
};
|
||||
|
||||
export const init = () =>
|
||||
initBlock( {
|
||||
name,
|
||||
metadata: metadata as never,
|
||||
settings,
|
||||
} );
|
||||
export function init() {
|
||||
return initBlock( { name, metadata, settings } );
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockAttributes } from '@wordpress/blocks';
|
||||
|
||||
export interface InventoryEmailBlockAttributes extends BlockAttributes {
|
||||
name: string;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Product } from '@woocommerce/data';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
|
@ -17,10 +18,11 @@ import {
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { TrackInventoryBlockAttributes } from './types';
|
||||
import { useValidation } from '../../contexts/validation-context';
|
||||
|
||||
import { useValidation } from '../../hooks/use-validation';
|
||||
|
||||
export function Edit( {}: BlockEditProps< TrackInventoryBlockAttributes > ) {
|
||||
export function Edit( {
|
||||
clientId,
|
||||
}: BlockEditProps< TrackInventoryBlockAttributes > ) {
|
||||
const blockProps = useBlockProps();
|
||||
|
||||
const [ manageStock ] = useEntityProp< boolean >(
|
||||
|
@ -40,16 +42,21 @@ export function Edit( {}: BlockEditProps< TrackInventoryBlockAttributes > ) {
|
|||
'product_stock_quantity'
|
||||
) as string;
|
||||
|
||||
const stockQuantityValidationError = useValidation(
|
||||
'product/stock_quantity',
|
||||
function stockQuantityValidator() {
|
||||
const {
|
||||
ref: stockQuantityRef,
|
||||
error: stockQuantityValidationError,
|
||||
validate: validateStockQuantity,
|
||||
} = useValidation< Product >(
|
||||
`stock_quantity-${ clientId }`,
|
||||
async function stockQuantityValidator() {
|
||||
if ( manageStock && stockQuantity && stockQuantity < 0 ) {
|
||||
return __(
|
||||
'Stock quantity must be a positive number.',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[ manageStock, stockQuantity ]
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
|
@ -72,9 +79,11 @@ export function Edit( {}: BlockEditProps< TrackInventoryBlockAttributes > ) {
|
|||
<InputControl
|
||||
id={ stockQuantityId }
|
||||
name="stock_quantity"
|
||||
ref={ stockQuantityRef }
|
||||
label={ __( 'Available quantity', 'woocommerce' ) }
|
||||
value={ stockQuantity }
|
||||
onChange={ setStockQuantity }
|
||||
onBlur={ validateStockQuantity }
|
||||
type="number"
|
||||
min={ 0 }
|
||||
/>
|
||||
|
|
|
@ -35,7 +35,7 @@ import { useEntityProp, useEntityId } from '@wordpress/core-data';
|
|||
*/
|
||||
import { AUTO_DRAFT_NAME } from '../../utils';
|
||||
import { EditProductLinkModal } from '../../components/edit-product-link-modal';
|
||||
import { useValidation } from '../../hooks/use-validation';
|
||||
import { useValidation } from '../../contexts/validation-context';
|
||||
|
||||
export function Edit() {
|
||||
const blockProps = useBlockProps();
|
||||
|
@ -77,9 +77,13 @@ export function Edit() {
|
|||
}
|
||||
);
|
||||
|
||||
const nameValidationError = useValidation(
|
||||
'product/name',
|
||||
function nameValidator() {
|
||||
const {
|
||||
ref: nameRef,
|
||||
error: nameValidationError,
|
||||
validate: validateName,
|
||||
} = useValidation< Product >(
|
||||
'name',
|
||||
async function nameValidator() {
|
||||
if ( ! name || name === AUTO_DRAFT_NAME ) {
|
||||
return __( 'This field is required.', 'woocommerce' );
|
||||
}
|
||||
|
@ -90,7 +94,8 @@ export function Edit() {
|
|||
'woocommerce'
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[ name ]
|
||||
);
|
||||
|
||||
const setSkuIfEmpty = () => {
|
||||
|
@ -153,6 +158,7 @@ export function Edit() {
|
|||
>
|
||||
<InputControl
|
||||
id={ nameControlId }
|
||||
ref={ nameRef }
|
||||
name="name"
|
||||
placeholder={ __(
|
||||
'e.g. 12 oz Coffee Mug',
|
||||
|
@ -160,7 +166,10 @@ export function Edit() {
|
|||
) }
|
||||
onChange={ setName }
|
||||
value={ name && name !== AUTO_DRAFT_NAME ? name : '' }
|
||||
onBlur={ setSkuIfEmpty }
|
||||
onBlur={ () => {
|
||||
setSkuIfEmpty();
|
||||
validateName();
|
||||
} }
|
||||
/>
|
||||
</BaseControl>
|
||||
|
||||
|
|
|
@ -2,18 +2,13 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { CurrencyContext } from '@woocommerce/currency';
|
||||
import { getNewPath } from '@woocommerce/navigation';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import {
|
||||
createElement,
|
||||
useContext,
|
||||
createInterpolateElement,
|
||||
} from '@wordpress/element';
|
||||
import { createElement, createInterpolateElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
BaseControl,
|
||||
|
@ -25,7 +20,6 @@ import {
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { useCurrencyInputProps } from '../../hooks/use-currency-input-props';
|
||||
import { formatCurrencyDisplayValue } from '../../utils';
|
||||
import { PricingBlockAttributes } from './types';
|
||||
|
||||
export function Edit( {
|
||||
|
@ -38,12 +32,9 @@ export function Edit( {
|
|||
'product',
|
||||
name
|
||||
);
|
||||
const context = useContext( CurrencyContext );
|
||||
const { getCurrencyConfig, formatAmount } = context;
|
||||
const currencyConfig = getCurrencyConfig();
|
||||
const inputProps = useCurrencyInputProps( {
|
||||
value: price,
|
||||
setValue: setPrice,
|
||||
onChange: setPrice,
|
||||
} );
|
||||
|
||||
const interpolatedHelp = help
|
||||
|
@ -71,13 +62,7 @@ export function Edit( {
|
|||
{ ...inputProps }
|
||||
id={ priceId }
|
||||
name={ name }
|
||||
onChange={ setPrice }
|
||||
label={ label || __( 'Price', 'woocommerce' ) }
|
||||
value={ formatCurrencyDisplayValue(
|
||||
String( price ),
|
||||
currencyConfig,
|
||||
formatAmount
|
||||
) }
|
||||
/>
|
||||
</BaseControl>
|
||||
</div>
|
||||
|
|
|
@ -3,18 +3,14 @@
|
|||
*/
|
||||
import classNames from 'classnames';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { CurrencyContext } from '@woocommerce/currency';
|
||||
import { Product } from '@woocommerce/data';
|
||||
import { getNewPath } from '@woocommerce/navigation';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import {
|
||||
createElement,
|
||||
useContext,
|
||||
createInterpolateElement,
|
||||
} from '@wordpress/element';
|
||||
import { createElement, createInterpolateElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
BaseControl,
|
||||
|
@ -25,13 +21,13 @@ import {
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useValidation } from '../../contexts/validation-context';
|
||||
import { useCurrencyInputProps } from '../../hooks/use-currency-input-props';
|
||||
import { formatCurrencyDisplayValue } from '../../utils';
|
||||
import { SalePriceBlockAttributes } from './types';
|
||||
import { useValidation } from '../../hooks/use-validation';
|
||||
|
||||
export function Edit( {
|
||||
attributes,
|
||||
clientId,
|
||||
}: BlockEditProps< SalePriceBlockAttributes > ) {
|
||||
const blockProps = useBlockProps();
|
||||
const { label, help } = attributes;
|
||||
|
@ -45,12 +41,9 @@ export function Edit( {
|
|||
'product',
|
||||
'sale_price'
|
||||
);
|
||||
const context = useContext( CurrencyContext );
|
||||
const { getCurrencyConfig, formatAmount } = context;
|
||||
const currencyConfig = getCurrencyConfig();
|
||||
const inputProps = useCurrencyInputProps( {
|
||||
value: regularPrice,
|
||||
setValue: setRegularPrice,
|
||||
onChange: setRegularPrice,
|
||||
} );
|
||||
|
||||
const interpolatedHelp = help
|
||||
|
@ -71,9 +64,13 @@ export function Edit( {
|
|||
'wp-block-woocommerce-product-regular-price-field'
|
||||
) as string;
|
||||
|
||||
const regularPriceValidationError = useValidation(
|
||||
'product/regular_price',
|
||||
function regularPriceValidator() {
|
||||
const {
|
||||
ref: regularPriceRef,
|
||||
error: regularPriceValidationError,
|
||||
validate: validateRegularPrice,
|
||||
} = useValidation< Product >(
|
||||
`regular_price-${ clientId }`,
|
||||
async function regularPriceValidator() {
|
||||
const listPrice = Number.parseFloat( regularPrice );
|
||||
if ( listPrice ) {
|
||||
if ( listPrice < 0 ) {
|
||||
|
@ -92,7 +89,8 @@ export function Edit( {
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[ regularPrice, salePrice ]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -112,13 +110,9 @@ export function Edit( {
|
|||
{ ...inputProps }
|
||||
id={ regularPriceId }
|
||||
name={ 'regular_price' }
|
||||
ref={ regularPriceRef }
|
||||
label={ label }
|
||||
value={ formatCurrencyDisplayValue(
|
||||
String( regularPrice ),
|
||||
currencyConfig,
|
||||
formatAmount
|
||||
) }
|
||||
onChange={ setRegularPrice }
|
||||
onBlur={ validateRegularPrice }
|
||||
/>
|
||||
</BaseControl>
|
||||
</div>
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import classNames from 'classnames';
|
||||
import { CurrencyContext } from '@woocommerce/currency';
|
||||
import { Product } from '@woocommerce/data';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { createElement, useContext } from '@wordpress/element';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
BaseControl,
|
||||
|
@ -18,13 +18,13 @@ import {
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useValidation } from '../../contexts/validation-context';
|
||||
import { useCurrencyInputProps } from '../../hooks/use-currency-input-props';
|
||||
import { formatCurrencyDisplayValue } from '../../utils';
|
||||
import { SalePriceBlockAttributes } from './types';
|
||||
import { useValidation } from '../../hooks/use-validation';
|
||||
|
||||
export function Edit( {
|
||||
attributes,
|
||||
clientId,
|
||||
}: BlockEditProps< SalePriceBlockAttributes > ) {
|
||||
const blockProps = useBlockProps();
|
||||
const { label, help } = attributes;
|
||||
|
@ -38,12 +38,9 @@ export function Edit( {
|
|||
'product',
|
||||
'sale_price'
|
||||
);
|
||||
const context = useContext( CurrencyContext );
|
||||
const { getCurrencyConfig, formatAmount } = context;
|
||||
const currencyConfig = getCurrencyConfig();
|
||||
const inputProps = useCurrencyInputProps( {
|
||||
value: salePrice,
|
||||
setValue: setSalePrice,
|
||||
onChange: setSalePrice,
|
||||
} );
|
||||
|
||||
const salePriceId = useInstanceId(
|
||||
|
@ -51,9 +48,13 @@ export function Edit( {
|
|||
'wp-block-woocommerce-product-sale-price-field'
|
||||
) as string;
|
||||
|
||||
const salePriceValidationError = useValidation(
|
||||
'product/sale_price',
|
||||
function salePriceValidator() {
|
||||
const {
|
||||
ref: salePriceRef,
|
||||
error: salePriceValidationError,
|
||||
validate: validateSalePrice,
|
||||
} = useValidation< Product >(
|
||||
`sale-price-${ clientId }`,
|
||||
async function salePriceValidator() {
|
||||
if ( salePrice ) {
|
||||
if ( Number.parseFloat( salePrice ) < 0 ) {
|
||||
return __(
|
||||
|
@ -72,7 +73,8 @@ export function Edit( {
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[ regularPrice, salePrice ]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -90,13 +92,9 @@ export function Edit( {
|
|||
{ ...inputProps }
|
||||
id={ salePriceId }
|
||||
name={ 'sale_price' }
|
||||
onChange={ setSalePrice }
|
||||
ref={ salePriceRef }
|
||||
label={ label }
|
||||
value={ formatCurrencyDisplayValue(
|
||||
String( salePrice ),
|
||||
currencyConfig,
|
||||
formatAmount
|
||||
) }
|
||||
onBlur={ validateSalePrice }
|
||||
/>
|
||||
</BaseControl>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { DateTimePickerControl } from '@woocommerce/components';
|
||||
import { Product } from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
|
@ -19,9 +20,11 @@ import { getSettings } from '@wordpress/date';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { ScheduleSalePricingBlockAttributes } from './types';
|
||||
import { useValidation } from '../../hooks/use-validation';
|
||||
import { useValidation } from '../../contexts/validation-context';
|
||||
|
||||
export function Edit( {}: BlockEditProps< ScheduleSalePricingBlockAttributes > ) {
|
||||
export function Edit( {
|
||||
clientId,
|
||||
}: BlockEditProps< ScheduleSalePricingBlockAttributes > ) {
|
||||
const blockProps = useBlockProps();
|
||||
|
||||
const dateTimeFormat = getSettings().formats.datetime;
|
||||
|
@ -84,9 +87,13 @@ export function Edit( {}: BlockEditProps< ScheduleSalePricingBlockAttributes > )
|
|||
const _dateOnSaleFrom = moment( dateOnSaleFromGmt, moment.ISO_8601, true );
|
||||
const _dateOnSaleTo = moment( dateOnSaleToGmt, moment.ISO_8601, true );
|
||||
|
||||
const dateOnSaleFromGmtValidationError = useValidation(
|
||||
'product/date_on_sale_from_gmt',
|
||||
function dateOnSaleFromValidator() {
|
||||
const {
|
||||
ref: dateOnSaleFromGmtRef,
|
||||
error: dateOnSaleFromGmtValidationError,
|
||||
validate: validateDateOnSaleFromGmt,
|
||||
} = useValidation< Product >(
|
||||
`date_on_sale_from_gmt-${ clientId }`,
|
||||
async function dateOnSaleFromValidator() {
|
||||
if ( showScheduleSale && dateOnSaleFromGmt ) {
|
||||
if ( ! _dateOnSaleFrom.isValid() ) {
|
||||
return __( 'Please enter a valid date.', 'woocommerce' );
|
||||
|
@ -99,12 +106,17 @@ export function Edit( {}: BlockEditProps< ScheduleSalePricingBlockAttributes > )
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[ showScheduleSale, dateOnSaleFromGmt, _dateOnSaleFrom, _dateOnSaleTo ]
|
||||
);
|
||||
|
||||
const dateOnSaleToGmtValidationError = useValidation(
|
||||
'product/date_on_sale_to_gmt',
|
||||
function dateOnSaleToValidator() {
|
||||
const {
|
||||
ref: dateOnSaleToGmtRef,
|
||||
error: dateOnSaleToGmtValidationError,
|
||||
validate: validateDateOnSaleToGmt,
|
||||
} = useValidation< Product >(
|
||||
`date_on_sale_to_gmt-${ clientId }`,
|
||||
async function dateOnSaleToValidator() {
|
||||
if ( showScheduleSale && dateOnSaleToGmt ) {
|
||||
if ( ! _dateOnSaleTo.isValid() ) {
|
||||
return __( 'Please enter a valid date.', 'woocommerce' );
|
||||
|
@ -117,7 +129,8 @@ export function Edit( {}: BlockEditProps< ScheduleSalePricingBlockAttributes > )
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[ showScheduleSale, dateOnSaleFromGmt, _dateOnSaleFrom, _dateOnSaleTo ]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -133,6 +146,9 @@ export function Edit( {}: BlockEditProps< ScheduleSalePricingBlockAttributes > )
|
|||
<div className="wp-block-columns">
|
||||
<div className="wp-block-column">
|
||||
<DateTimePickerControl
|
||||
ref={
|
||||
dateOnSaleFromGmtRef as React.Ref< HTMLInputElement >
|
||||
}
|
||||
label={ __( 'From', 'woocommerce' ) }
|
||||
placeholder={ __(
|
||||
'Sale start date and time (optional)',
|
||||
|
@ -145,11 +161,15 @@ export function Edit( {}: BlockEditProps< ScheduleSalePricingBlockAttributes > )
|
|||
dateOnSaleFromGmtValidationError && 'has-error'
|
||||
}
|
||||
help={ dateOnSaleFromGmtValidationError as string }
|
||||
onBlur={ validateDateOnSaleFromGmt }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="wp-block-column">
|
||||
<DateTimePickerControl
|
||||
ref={
|
||||
dateOnSaleToGmtRef as React.Ref< HTMLInputElement >
|
||||
}
|
||||
label={ __( 'To', 'woocommerce' ) }
|
||||
placeholder={ __(
|
||||
'Sale end date and time (optional)',
|
||||
|
@ -164,6 +184,7 @@ export function Edit( {}: BlockEditProps< ScheduleSalePricingBlockAttributes > )
|
|||
.toISOString()
|
||||
)
|
||||
}
|
||||
onBlur={ validateDateOnSaleToGmt }
|
||||
className={
|
||||
dateOnSaleToGmtValidationError && 'has-error'
|
||||
}
|
||||
|
|
|
@ -14,9 +14,6 @@
|
|||
"description": {
|
||||
"type": "string",
|
||||
"__experimentalRole": "content"
|
||||
},
|
||||
"icon": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
|
|
|
@ -8,13 +8,11 @@ import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { BlockIcon } from '../../components/block-icon';
|
||||
import { SectionBlockAttributes } from './types';
|
||||
import { sanitizeHTML } from '../../utils/sanitize-html';
|
||||
|
||||
export function Edit( {
|
||||
attributes,
|
||||
clientId,
|
||||
}: BlockEditProps< SectionBlockAttributes > ) {
|
||||
const blockProps = useBlockProps();
|
||||
const { description, title } = attributes;
|
||||
|
@ -22,7 +20,6 @@ export function Edit( {
|
|||
return (
|
||||
<div { ...blockProps }>
|
||||
<h2 className="wp-block-woocommerce-product-section__title">
|
||||
<BlockIcon clientId={ clientId } />
|
||||
<span>{ title }</span>
|
||||
</h2>
|
||||
{ description && (
|
||||
|
|
|
@ -9,27 +9,18 @@
|
|||
.wp-block-woocommerce-product-section__title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: $gray-900;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.block-editor-block-icon {
|
||||
margin-right: 14px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin-top: $gap-small;
|
||||
font-size: 13px;
|
||||
color: $gray-900;
|
||||
color: $gray-700;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "woocommerce/product-shipping-fee-fields",
|
||||
"title": "Product shipping fee fields",
|
||||
"name": "woocommerce/product-shipping-class-field",
|
||||
"title": "Product shipping class field",
|
||||
"category": "woocommerce",
|
||||
"description": "The product shipping fee fields.",
|
||||
"keywords": [ "products", "shipping", "fee" ],
|
||||
"description": "The product shipping class field.",
|
||||
"keywords": [ "products", "shipping", "class" ],
|
||||
"textdomain": "default",
|
||||
"attributes": {
|
||||
"title": {
|
|
@ -18,19 +18,15 @@ import {
|
|||
Fragment,
|
||||
createElement,
|
||||
createInterpolateElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ShippingFeeBlockAttributes } from './types';
|
||||
import { useValidation } from '../../hooks/use-validation';
|
||||
import { RadioField } from '../../components/radio-field';
|
||||
import { ShippingClassBlockAttributes } from './types';
|
||||
import { AddNewShippingClassModal } from '../../components';
|
||||
import { ADD_NEW_SHIPPING_CLASS_OPTION_VALUE } from '../../constants';
|
||||
|
||||
|
@ -38,9 +34,6 @@ type ServerErrorResponse = {
|
|||
code: string;
|
||||
};
|
||||
|
||||
const FOLLOW_CLASS_OPTION_VALUE = 'follow_class';
|
||||
const FREE_SHIPPING_OPTION_VALUE = 'free_shipping';
|
||||
|
||||
export const DEFAULT_SHIPPING_CLASS_OPTIONS: SelectControl.Option[] = [
|
||||
{ value: '', label: __( 'No shipping class', 'woocommerce' ) },
|
||||
{
|
||||
|
@ -58,17 +51,6 @@ function mapShippingClassToSelectOption(
|
|||
} ) );
|
||||
}
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: __( 'Follow class', 'woocommerce' ),
|
||||
value: FOLLOW_CLASS_OPTION_VALUE,
|
||||
},
|
||||
{
|
||||
label: __( 'Free shipping', 'woocommerce' ),
|
||||
value: FREE_SHIPPING_OPTION_VALUE,
|
||||
},
|
||||
];
|
||||
|
||||
function extractDefaultShippingClassFromProduct(
|
||||
categories?: PartialProduct[ 'categories' ],
|
||||
shippingClasses?: ProductShippingClass[]
|
||||
|
@ -87,19 +69,12 @@ function extractDefaultShippingClassFromProduct(
|
|||
}
|
||||
}
|
||||
|
||||
export function Edit( {
|
||||
attributes,
|
||||
}: BlockEditProps< ShippingFeeBlockAttributes > ) {
|
||||
const { title } = attributes;
|
||||
export function Edit( {}: BlockEditProps< ShippingClassBlockAttributes > ) {
|
||||
const [ showShippingClassModal, setShowShippingClassModal ] =
|
||||
useState( false );
|
||||
|
||||
const blockProps = useBlockProps();
|
||||
|
||||
const [ option, setOption ] = useState< string >(
|
||||
FREE_SHIPPING_OPTION_VALUE
|
||||
);
|
||||
|
||||
const { createProductShippingClass, invalidateResolution } = useDispatch(
|
||||
EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME
|
||||
);
|
||||
|
@ -149,114 +124,71 @@ export function Edit( {
|
|||
};
|
||||
}, [] );
|
||||
|
||||
const shippingClassControlId = useInstanceId( BaseControl ) as string;
|
||||
|
||||
const shippingClassValidationError = useValidation(
|
||||
'product/shipping_class',
|
||||
function shippingClassValidator() {
|
||||
if ( option === FOLLOW_CLASS_OPTION_VALUE && ! shippingClass ) {
|
||||
return __( 'The shipping class is required.', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function handleOptionChange( value: string ) {
|
||||
setOption( value );
|
||||
|
||||
if ( value === FOLLOW_CLASS_OPTION_VALUE ) {
|
||||
const [ firstShippingClass ] = shippingClasses;
|
||||
setShippingClass( firstShippingClass?.slug ?? '' );
|
||||
} else {
|
||||
setShippingClass( '' );
|
||||
}
|
||||
}
|
||||
|
||||
useEffect( () => {
|
||||
if ( shippingClass ) {
|
||||
setOption( FOLLOW_CLASS_OPTION_VALUE );
|
||||
}
|
||||
}, [ shippingClass ] );
|
||||
const shippingClassControlId = useInstanceId(
|
||||
BaseControl,
|
||||
'wp-block-woocommerce-product-shipping-class-field'
|
||||
) as string;
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<div className="wp-block-columns">
|
||||
<div className="wp-block-column">
|
||||
<RadioField
|
||||
title={ title }
|
||||
selected={ option }
|
||||
options={ options }
|
||||
onChange={ handleOptionChange }
|
||||
<SelectControl
|
||||
id={ shippingClassControlId }
|
||||
name="shipping_class"
|
||||
value={ shippingClass }
|
||||
onChange={ ( value: string ) => {
|
||||
if (
|
||||
value === ADD_NEW_SHIPPING_CLASS_OPTION_VALUE
|
||||
) {
|
||||
setShowShippingClassModal( true );
|
||||
return;
|
||||
}
|
||||
setShippingClass( value );
|
||||
} }
|
||||
label={ __( 'Shipping class', 'woocommerce' ) }
|
||||
options={ [
|
||||
...DEFAULT_SHIPPING_CLASS_OPTIONS,
|
||||
...mapShippingClassToSelectOption(
|
||||
shippingClasses ?? []
|
||||
),
|
||||
] }
|
||||
help={ createInterpolateElement(
|
||||
__(
|
||||
'Manage shipping classes and rates in <Link>global settings</Link>.',
|
||||
'woocommerce'
|
||||
),
|
||||
{
|
||||
Link: (
|
||||
<Link
|
||||
href={ getNewPath(
|
||||
{
|
||||
tab: 'shipping',
|
||||
section: 'classes',
|
||||
},
|
||||
'',
|
||||
{},
|
||||
'wc-settings'
|
||||
) }
|
||||
target="_blank"
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_shipping_global_settings_link_click'
|
||||
);
|
||||
} }
|
||||
>
|
||||
<Fragment />
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="wp-block-column"></div>
|
||||
</div>
|
||||
|
||||
{ option === FOLLOW_CLASS_OPTION_VALUE && (
|
||||
<div className="wp-block-columns">
|
||||
<div
|
||||
className={ classNames( 'wp-block-column', {
|
||||
'has-error': shippingClassValidationError,
|
||||
} ) }
|
||||
>
|
||||
<SelectControl
|
||||
id={ shippingClassControlId }
|
||||
name="shipping_class"
|
||||
value={ shippingClass }
|
||||
onChange={ ( value: string ) => {
|
||||
if (
|
||||
value ===
|
||||
ADD_NEW_SHIPPING_CLASS_OPTION_VALUE
|
||||
) {
|
||||
setShowShippingClassModal( true );
|
||||
return;
|
||||
}
|
||||
setShippingClass( value );
|
||||
} }
|
||||
label={ __( 'Shipping class', 'woocommerce' ) }
|
||||
options={ [
|
||||
...DEFAULT_SHIPPING_CLASS_OPTIONS,
|
||||
...mapShippingClassToSelectOption(
|
||||
shippingClasses ?? []
|
||||
),
|
||||
] }
|
||||
help={
|
||||
shippingClassValidationError ||
|
||||
createInterpolateElement(
|
||||
__(
|
||||
'Manage shipping classes and rates in <Link>global settings</Link>.',
|
||||
'woocommerce'
|
||||
),
|
||||
{
|
||||
Link: (
|
||||
<Link
|
||||
href={ getNewPath(
|
||||
{
|
||||
tab: 'shipping',
|
||||
section: 'classes',
|
||||
},
|
||||
'',
|
||||
{},
|
||||
'wc-settings'
|
||||
) }
|
||||
target="_blank"
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_shipping_global_settings_link_click'
|
||||
);
|
||||
} }
|
||||
>
|
||||
<Fragment />
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="wp-block-column"></div>
|
||||
</div>
|
||||
) }
|
||||
{ showShippingClassModal && (
|
||||
<AddNewShippingClassModal
|
||||
shippingClass={ extractDefaultShippingClassFromProduct(
|
|
@ -10,15 +10,15 @@ import { BlockConfiguration } from '@wordpress/blocks';
|
|||
import { initBlock } from '../../utils/init-blocks';
|
||||
import blockConfiguration from './block.json';
|
||||
import { Edit } from './edit';
|
||||
import { ShippingFeeBlockAttributes } from './types';
|
||||
import { ShippingClassBlockAttributes } from './types';
|
||||
|
||||
const { name, ...metadata } =
|
||||
blockConfiguration as BlockConfiguration< ShippingFeeBlockAttributes >;
|
||||
blockConfiguration as BlockConfiguration< ShippingClassBlockAttributes >;
|
||||
|
||||
export { metadata, name };
|
||||
|
||||
export const settings: Partial<
|
||||
BlockConfiguration< ShippingFeeBlockAttributes >
|
||||
BlockConfiguration< ShippingClassBlockAttributes >
|
||||
> = {
|
||||
example: {},
|
||||
edit: Edit,
|
|
@ -3,6 +3,6 @@
|
|||
*/
|
||||
import { BlockAttributes } from '@wordpress/blocks';
|
||||
|
||||
export interface ShippingFeeBlockAttributes extends BlockAttributes {
|
||||
export interface ShippingClassBlockAttributes extends BlockAttributes {
|
||||
title: string;
|
||||
}
|
|
@ -3,7 +3,11 @@
|
|||
*/
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
import { OPTIONS_STORE_NAME, ProductDimensions } from '@woocommerce/data';
|
||||
import {
|
||||
OPTIONS_STORE_NAME,
|
||||
Product,
|
||||
ProductDimensions,
|
||||
} from '@woocommerce/data';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
|
@ -29,9 +33,11 @@ import {
|
|||
HighlightSides,
|
||||
ShippingDimensionsImage,
|
||||
} from '../../components/shipping-dimensions-image';
|
||||
import { useValidation } from '../../hooks/use-validation';
|
||||
import { useValidation } from '../../contexts/validation-context';
|
||||
|
||||
export function Edit( {}: BlockEditProps< ShippingDimensionsBlockAttributes > ) {
|
||||
export function Edit( {
|
||||
clientId,
|
||||
}: BlockEditProps< ShippingDimensionsBlockAttributes > ) {
|
||||
const blockProps = useBlockProps();
|
||||
|
||||
const [ dimensions, setDimensions ] =
|
||||
|
@ -79,12 +85,70 @@ export function Edit( {}: BlockEditProps< ShippingDimensionsBlockAttributes > )
|
|||
};
|
||||
}
|
||||
|
||||
const {
|
||||
ref: dimensionsWidthRef,
|
||||
error: dimensionsWidthValidationError,
|
||||
validate: validateDimensionsWidth,
|
||||
} = useValidation< Product >(
|
||||
`dimensions_width-${ clientId }`,
|
||||
async function dimensionsWidthValidator() {
|
||||
if ( dimensions?.width && +dimensions.width <= 0 ) {
|
||||
return __( 'Width must be greater than zero.', 'woocommerce' );
|
||||
}
|
||||
},
|
||||
[ dimensions?.width ]
|
||||
);
|
||||
|
||||
const {
|
||||
ref: dimensionsLengthRef,
|
||||
error: dimensionsLengthValidationError,
|
||||
validate: validateDimensionsLength,
|
||||
} = useValidation< Product >(
|
||||
`dimensions_length-${ clientId }`,
|
||||
async function dimensionsLengthValidator() {
|
||||
if ( dimensions?.length && +dimensions.length <= 0 ) {
|
||||
return __( 'Length must be greater than zero.', 'woocommerce' );
|
||||
}
|
||||
},
|
||||
[ dimensions?.length ]
|
||||
);
|
||||
|
||||
const {
|
||||
ref: dimensionsHeightRef,
|
||||
error: dimensionsHeightValidationError,
|
||||
validate: validateDimensionsHeight,
|
||||
} = useValidation< Product >(
|
||||
`dimensions_height-${ clientId }`,
|
||||
async function dimensionsHeightValidator() {
|
||||
if ( dimensions?.height && +dimensions.height <= 0 ) {
|
||||
return __( 'Height must be greater than zero.', 'woocommerce' );
|
||||
}
|
||||
},
|
||||
[ dimensions?.height ]
|
||||
);
|
||||
|
||||
const {
|
||||
ref: weightRef,
|
||||
error: weightValidationError,
|
||||
validate: validateWeight,
|
||||
} = useValidation< Product >(
|
||||
`weight-${ clientId }`,
|
||||
async function weightValidator() {
|
||||
if ( weight && +weight <= 0 ) {
|
||||
return __( 'Weight must be greater than zero.', 'woocommerce' );
|
||||
}
|
||||
},
|
||||
[ weight ]
|
||||
);
|
||||
|
||||
const dimensionsWidthProps = {
|
||||
...getDimensionsControlProps( 'width', 'A' ),
|
||||
id: useInstanceId(
|
||||
BaseControl,
|
||||
`product_shipping_dimensions_width`
|
||||
) as string,
|
||||
ref: dimensionsWidthRef,
|
||||
onBlur: validateDimensionsWidth,
|
||||
};
|
||||
const dimensionsLengthProps = {
|
||||
...getDimensionsControlProps( 'length', 'B' ),
|
||||
|
@ -92,6 +156,8 @@ export function Edit( {}: BlockEditProps< ShippingDimensionsBlockAttributes > )
|
|||
BaseControl,
|
||||
`product_shipping_dimensions_length`
|
||||
) as string,
|
||||
ref: dimensionsLengthRef,
|
||||
onBlur: validateDimensionsLength,
|
||||
};
|
||||
const dimensionsHeightProps = {
|
||||
...getDimensionsControlProps( 'height', 'C' ),
|
||||
|
@ -99,6 +165,8 @@ export function Edit( {}: BlockEditProps< ShippingDimensionsBlockAttributes > )
|
|||
BaseControl,
|
||||
`product_shipping_dimensions_height`
|
||||
) as string,
|
||||
ref: dimensionsHeightRef,
|
||||
onBlur: validateDimensionsHeight,
|
||||
};
|
||||
const weightProps = {
|
||||
id: useInstanceId( BaseControl, `product_shipping_weight` ) as string,
|
||||
|
@ -106,41 +174,10 @@ export function Edit( {}: BlockEditProps< ShippingDimensionsBlockAttributes > )
|
|||
value: formatNumber( String( weight ) ),
|
||||
onChange: ( value: string ) => setWeight( parseNumber( value ) ),
|
||||
suffix: weightUnit,
|
||||
ref: weightRef,
|
||||
onBlur: validateWeight,
|
||||
};
|
||||
|
||||
const dimensionsWidthValidationError = useValidation(
|
||||
'product/dimensions/width',
|
||||
function dimensionsWidthValidator() {
|
||||
if ( dimensions?.width && +dimensions.width <= 0 ) {
|
||||
return __( 'Width must be greater than zero.', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
);
|
||||
const dimensionsLengthValidationError = useValidation(
|
||||
'product/dimensions/length',
|
||||
function dimensionsLengthValidator() {
|
||||
if ( dimensions?.length && +dimensions.length <= 0 ) {
|
||||
return __( 'Length must be greater than zero.', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
);
|
||||
const dimensionsHeightValidationError = useValidation(
|
||||
'product/dimensions/height',
|
||||
function dimensionsHeightValidator() {
|
||||
if ( dimensions?.height && +dimensions.height <= 0 ) {
|
||||
return __( 'Height must be greater than zero.', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
);
|
||||
const weightValidationError = useValidation(
|
||||
'product/weight',
|
||||
function weightValidator() {
|
||||
if ( weight && +weight <= 0 ) {
|
||||
return __( 'Weight must be greater than zero.', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<h4>{ __( 'Dimensions', 'woocommerce' ) }</h4>
|
||||
|
|
|
@ -41,6 +41,10 @@
|
|||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-bottom: 128px;
|
||||
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.components-input-control {
|
||||
|
@ -101,9 +105,3 @@
|
|||
display: none; // use important or increase specificity.
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-layout:has( .woocommerce-product-block-editor ) {
|
||||
.woocommerce-layout__header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,31 +55,41 @@ function getSelectedWithParents(
|
|||
return selected;
|
||||
}
|
||||
|
||||
function mapFromCategoryType(
|
||||
categories: ProductCategoryNode[]
|
||||
): TreeItemType[] {
|
||||
return categories.map( ( val ) =>
|
||||
val.parent
|
||||
? {
|
||||
value: String( val.id ),
|
||||
label: val.name,
|
||||
parent: String( val.parent ),
|
||||
}
|
||||
: {
|
||||
value: String( val.id ),
|
||||
label: val.name,
|
||||
}
|
||||
);
|
||||
export function mapFromCategoryToTreeItem(
|
||||
val: ProductCategoryNode
|
||||
): TreeItemType {
|
||||
return val.parent
|
||||
? {
|
||||
value: String( val.id ),
|
||||
label: val.name,
|
||||
parent: String( val.parent ),
|
||||
}
|
||||
: {
|
||||
value: String( val.id ),
|
||||
label: val.name,
|
||||
};
|
||||
}
|
||||
|
||||
function mapToCategoryType(
|
||||
export function mapFromTreeItemToCategory(
|
||||
val: TreeItemType
|
||||
): ProductCategoryNode {
|
||||
return {
|
||||
id: +val.value,
|
||||
name: val.label,
|
||||
parent: val.parent ? +val.parent : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapFromCategoriesToTreeItems(
|
||||
categories: ProductCategoryNode[]
|
||||
): TreeItemType[] {
|
||||
return categories.map( mapFromCategoryToTreeItem );
|
||||
}
|
||||
|
||||
export function mapFromTreeItemsToCategories(
|
||||
categories: TreeItemType[]
|
||||
): ProductCategoryNode[] {
|
||||
return categories.map( ( cat ) => ( {
|
||||
id: +cat.value,
|
||||
name: cat.label,
|
||||
parent: cat.parent ? +cat.parent : 0,
|
||||
} ) );
|
||||
return categories.map( mapFromTreeItemToCategory );
|
||||
}
|
||||
|
||||
export const CategoryField: React.FC< CategoryFieldProps > = ( {
|
||||
|
@ -129,15 +139,15 @@ export const CategoryField: React.FC< CategoryFieldProps > = ( {
|
|||
) === -1
|
||||
}
|
||||
items={ getFilteredItemsForSelectTree(
|
||||
mapFromCategoryType( categoriesSelectList ),
|
||||
mapFromCategoriesToTreeItems( categoriesSelectList ),
|
||||
searchValue,
|
||||
mapFromCategoryType( value )
|
||||
mapFromCategoriesToTreeItems( value )
|
||||
) }
|
||||
selected={ mapFromCategoryType( value ) }
|
||||
selected={ mapFromCategoriesToTreeItems( value ) }
|
||||
onSelect={ ( selectedItems ) => {
|
||||
if ( Array.isArray( selectedItems ) ) {
|
||||
const newItems: ProductCategoryNode[] =
|
||||
mapToCategoryType(
|
||||
mapFromTreeItemsToCategories(
|
||||
selectedItems.filter(
|
||||
( { value: selectedItemValue } ) =>
|
||||
! value.some(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.woocommerce-create-new-category-modal {
|
||||
min-width: 650px;
|
||||
overflow: visible;
|
||||
|
||||
&__buttons {
|
||||
margin-top: $gap-larger;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button, Modal, Spinner, TextControl } from '@wordpress/components';
|
||||
import { Button, Modal, TextControl } from '@wordpress/components';
|
||||
import { useDebounce } from '@wordpress/compose';
|
||||
import {
|
||||
useState,
|
||||
|
@ -11,8 +11,8 @@ import {
|
|||
} from '@wordpress/element';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import {
|
||||
__experimentalSelectControl as SelectControl,
|
||||
__experimentalSelectControlMenu as Menu,
|
||||
__experimentalSelectTreeControl as SelectTree,
|
||||
TreeItemType as Item,
|
||||
} from '@woocommerce/components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import {
|
||||
|
@ -24,7 +24,10 @@ import {
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { ProductCategoryNode, useCategorySearch } from './use-category-search';
|
||||
import { CategoryFieldItem } from './category-field-item';
|
||||
import {
|
||||
mapFromCategoriesToTreeItems,
|
||||
mapFromCategoryToTreeItem,
|
||||
} from './category-field';
|
||||
|
||||
type CreateCategoryModalProps = {
|
||||
initialCategoryName?: string;
|
||||
|
@ -32,15 +35,6 @@ type CreateCategoryModalProps = {
|
|||
onCreate: ( newCategory: ProductCategory ) => void;
|
||||
};
|
||||
|
||||
function getCategoryItemLabel( item: ProductCategoryNode | null ): string {
|
||||
return item?.name || '';
|
||||
}
|
||||
function getCategoryItemValue(
|
||||
item: ProductCategoryNode | null
|
||||
): string | number {
|
||||
return item?.id || '';
|
||||
}
|
||||
|
||||
export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( {
|
||||
initialCategoryName,
|
||||
onCancel,
|
||||
|
@ -49,9 +43,8 @@ export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( {
|
|||
const {
|
||||
categoriesSelectList,
|
||||
isSearching,
|
||||
categoryTreeKeyValues,
|
||||
searchCategories,
|
||||
getFilteredItems,
|
||||
getFilteredItemsForSelectTree,
|
||||
} = useCategorySearch();
|
||||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
const [ isCreating, setIsCreating ] = useState( false );
|
||||
|
@ -63,6 +56,9 @@ export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( {
|
|||
const [ categoryParent, setCategoryParent ] =
|
||||
useState< ProductCategoryNode | null >( null );
|
||||
|
||||
const [ categoryParentTypedValue, setCategoryParentTypedValue ] =
|
||||
useState( '' );
|
||||
|
||||
const onSave = async () => {
|
||||
recordEvent( 'product_category_add', {
|
||||
new_product_page: true,
|
||||
|
@ -101,8 +97,7 @@ export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( {
|
|||
value={ categoryName }
|
||||
onChange={ setCategoryName }
|
||||
/>
|
||||
<SelectControl< ProductCategoryNode >
|
||||
items={ categoriesSelectList }
|
||||
<SelectTree
|
||||
label={ createInterpolateElement(
|
||||
__( 'Parent category <optional/>', 'woocommerce' ),
|
||||
{
|
||||
|
@ -113,78 +108,34 @@ export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( {
|
|||
),
|
||||
}
|
||||
) }
|
||||
selected={ categoryParent }
|
||||
onSelect={ ( item ) => item && setCategoryParent( item ) }
|
||||
id="parent-category-field"
|
||||
isLoading={ isSearching }
|
||||
items={ getFilteredItemsForSelectTree(
|
||||
mapFromCategoriesToTreeItems( categoriesSelectList ),
|
||||
categoryParentTypedValue,
|
||||
[]
|
||||
) }
|
||||
shouldNotRecursivelySelect
|
||||
selected={
|
||||
categoryParent
|
||||
? mapFromCategoryToTreeItem( categoryParent )
|
||||
: undefined
|
||||
}
|
||||
onSelect={ ( item: Item ) =>
|
||||
item &&
|
||||
setCategoryParent( {
|
||||
id: +item.value,
|
||||
name: item.label,
|
||||
parent: item.parent ? +item.parent : 0,
|
||||
} )
|
||||
}
|
||||
onRemove={ () => setCategoryParent( null ) }
|
||||
onInputChange={ debouncedSearch }
|
||||
getFilteredItems={ getFilteredItems }
|
||||
getItemLabel={ getCategoryItemLabel }
|
||||
getItemValue={ getCategoryItemValue }
|
||||
>
|
||||
{ ( {
|
||||
items,
|
||||
isOpen,
|
||||
getMenuProps,
|
||||
highlightedIndex,
|
||||
getItemProps,
|
||||
} ) => {
|
||||
return (
|
||||
<Menu
|
||||
isOpen={ isOpen }
|
||||
getMenuProps={ getMenuProps }
|
||||
className="woocommerce-category-field-dropdown__menu"
|
||||
>
|
||||
{ [
|
||||
isSearching ? (
|
||||
<div
|
||||
key="loading-spinner"
|
||||
className="woocommerce-category-field-dropdown__item"
|
||||
>
|
||||
<div className="woocommerce-category-field-dropdown__item-content">
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
) : null,
|
||||
...items
|
||||
.filter(
|
||||
( item ) =>
|
||||
categoryTreeKeyValues[ item.id ]
|
||||
?.parentID === 0
|
||||
)
|
||||
.map( ( item ) => {
|
||||
return (
|
||||
<CategoryFieldItem
|
||||
key={ `${ item.id }` }
|
||||
item={
|
||||
categoryTreeKeyValues[
|
||||
item.id
|
||||
]
|
||||
}
|
||||
selectedIds={
|
||||
categoryParent
|
||||
? [
|
||||
categoryParent.id,
|
||||
]
|
||||
: []
|
||||
}
|
||||
items={ items }
|
||||
highlightedIndex={
|
||||
highlightedIndex
|
||||
}
|
||||
getItemProps={
|
||||
getItemProps
|
||||
}
|
||||
/>
|
||||
);
|
||||
} ),
|
||||
].filter(
|
||||
( item ): item is JSX.Element =>
|
||||
item !== null
|
||||
) }
|
||||
</Menu>
|
||||
);
|
||||
onInputChange={ ( value ) => {
|
||||
debouncedSearch( value );
|
||||
setCategoryParentTypedValue( value || '' );
|
||||
} }
|
||||
</SelectControl>
|
||||
createValue={ categoryParentTypedValue }
|
||||
/>
|
||||
<div className="woocommerce-create-new-category-modal__buttons">
|
||||
<Button
|
||||
isSecondary
|
||||
|
|
|
@ -8,6 +8,7 @@ import { useSelect, resolveSelect } from '@wordpress/data';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { useCategorySearch } from '../use-category-search';
|
||||
import { mapFromCategoriesToTreeItems } from '../../details-categories-field/category-field';
|
||||
|
||||
jest.mock( '@wordpress/data', () => ( {
|
||||
...jest.requireActual( '@wordpress/data' ),
|
||||
|
@ -179,7 +180,7 @@ describe( 'useCategorySearch', () => {
|
|||
);
|
||||
} );
|
||||
|
||||
describe( 'getFilteredItems', () => {
|
||||
describe( 'getFilteredItemsForSelectTree', () => {
|
||||
it( 'should filter items by label, matching input value, and if selected', async () => {
|
||||
getProductCategoriesMock.mockReturnValue( [ ...mockCategoryList ] );
|
||||
|
||||
|
@ -191,13 +192,15 @@ describe( 'useCategorySearch', () => {
|
|||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
const filteredItems = result.current.getFilteredItems(
|
||||
result.current.categoriesSelectList,
|
||||
const filteredItems = result.current.getFilteredItemsForSelectTree(
|
||||
mapFromCategoriesToTreeItems(
|
||||
result.current.categoriesSelectList
|
||||
),
|
||||
'Rain',
|
||||
[]
|
||||
);
|
||||
expect( filteredItems.length ).toEqual( 1 );
|
||||
expect( filteredItems[ 0 ].name ).toEqual( 'Rain gear' );
|
||||
expect( filteredItems[ 0 ].label ).toEqual( 'Rain gear' );
|
||||
} );
|
||||
|
||||
it( 'should filter items by isOpen as well, keeping them if isOpen is true', async () => {
|
||||
|
@ -215,15 +218,17 @@ describe( 'useCategorySearch', () => {
|
|||
} );
|
||||
await waitForNextUpdate();
|
||||
expect( result.current.categoriesSelectList.length ).toEqual( 6 );
|
||||
const filteredItems = result.current.getFilteredItems(
|
||||
result.current.categoriesSelectList,
|
||||
const filteredItems = result.current.getFilteredItemsForSelectTree(
|
||||
mapFromCategoriesToTreeItems(
|
||||
result.current.categoriesSelectList
|
||||
),
|
||||
'Bel',
|
||||
[]
|
||||
);
|
||||
expect( filteredItems.length ).toEqual( 3 );
|
||||
expect( filteredItems[ 0 ].name ).toEqual( 'Clothing' );
|
||||
expect( filteredItems[ 1 ].name ).toEqual( 'Accessories' );
|
||||
expect( filteredItems[ 2 ].name ).toEqual( 'Belts' );
|
||||
expect( filteredItems[ 0 ].label ).toEqual( 'Clothing' );
|
||||
expect( filteredItems[ 1 ].label ).toEqual( 'Accessories' );
|
||||
expect( filteredItems[ 2 ].label ).toEqual( 'Belts' );
|
||||
} );
|
||||
} );
|
||||
|
||||
|
|
|
@ -257,32 +257,6 @@ export const useCategorySearch = () => {
|
|||
|
||||
const categoryTreeKeyValues = categoriesAndNewItem[ 2 ];
|
||||
|
||||
/**
|
||||
* getFilteredItems callback for use in the SelectControl component.
|
||||
*/
|
||||
const getFilteredItems = useCallback(
|
||||
(
|
||||
allItems: ProductCategoryNode[],
|
||||
inputValue: string,
|
||||
selectedItems: ProductCategoryNode[]
|
||||
) => {
|
||||
const searchRegex = new RegExp( escapeRegExp( inputValue ), 'i' );
|
||||
return allItems.filter(
|
||||
( item ) =>
|
||||
selectedItems.indexOf( item ) < 0 &&
|
||||
( searchRegex.test( item.name ) ||
|
||||
( categoryTreeKeyValues[ item.id ] &&
|
||||
categoryTreeKeyValues[ item.id ].isOpen ) )
|
||||
);
|
||||
},
|
||||
[ categoriesAndNewItem ]
|
||||
);
|
||||
|
||||
/**
|
||||
* this is the same as getFilteredItems but for the SelectTree component, where item id is a string.
|
||||
* After all the occurrences of getFilteredItems are migrated to use SelectTree,
|
||||
* this can become the standard version
|
||||
*/
|
||||
const getFilteredItemsForSelectTree = useCallback(
|
||||
(
|
||||
allItems: TreeItemType[],
|
||||
|
@ -303,7 +277,6 @@ export const useCategorySearch = () => {
|
|||
|
||||
return {
|
||||
searchCategories,
|
||||
getFilteredItems,
|
||||
getFilteredItemsForSelectTree,
|
||||
categoriesSelectList: categoriesAndNewItem[ 0 ],
|
||||
categories: categoriesAndNewItem[ 1 ],
|
||||
|
|
|
@ -36,9 +36,7 @@ import { FullscreenMode, InterfaceSkeleton } from '@wordpress/interface';
|
|||
*/
|
||||
import { Header } from '../header';
|
||||
import { BlockEditor } from '../block-editor';
|
||||
import { initBlocks } from './init-blocks';
|
||||
|
||||
initBlocks();
|
||||
import { ValidationProvider } from '../../contexts/validation-context';
|
||||
|
||||
export type ProductEditorSettings = Partial<
|
||||
EditorSettings & EditorBlockListSettings
|
||||
|
@ -65,31 +63,33 @@ export function Editor( { product, settings }: EditorProps ) {
|
|||
<ShortcutProvider>
|
||||
<FullscreenMode isActive={ false } />
|
||||
<SlotFillProvider>
|
||||
<InterfaceSkeleton
|
||||
header={
|
||||
<Header
|
||||
productName={ product.name }
|
||||
onTabSelect={ setSelectedTab }
|
||||
/>
|
||||
}
|
||||
content={
|
||||
<>
|
||||
<BlockEditor
|
||||
settings={ settings }
|
||||
product={ product }
|
||||
context={ {
|
||||
selectedTab,
|
||||
postType: 'product',
|
||||
postId: product.id,
|
||||
} }
|
||||
<ValidationProvider initialValue={ product }>
|
||||
<InterfaceSkeleton
|
||||
header={
|
||||
<Header
|
||||
productName={ product.name }
|
||||
onTabSelect={ setSelectedTab }
|
||||
/>
|
||||
{ /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
|
||||
<PluginArea scope="woocommerce-product-block-editor" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
content={
|
||||
<>
|
||||
<BlockEditor
|
||||
settings={ settings }
|
||||
product={ product }
|
||||
context={ {
|
||||
selectedTab,
|
||||
postType: 'product',
|
||||
postId: product.id,
|
||||
} }
|
||||
/>
|
||||
{ /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
|
||||
<PluginArea scope="woocommerce-product-block-editor" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Popover.Slot />
|
||||
<Popover.Slot />
|
||||
</ValidationProvider>
|
||||
</SlotFillProvider>
|
||||
</ShortcutProvider>
|
||||
</EntityProvider>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue