Merge branch 'trunk' into add/sync_of_deleted_orders
This commit is contained in:
commit
34a8de217c
|
@ -1,5 +1,5 @@
|
|||
name: ✨ Enhancement Request
|
||||
description: If you have an idea to improve an existing feature in core or need something for development (such as a new hook) please let us know or submit a Pull Request!
|
||||
description: If you have an idea to improve existing functionality in core or need something for development (such as a new hook) please let us know or submit a Pull Request!
|
||||
title: "[Enhancement]: "
|
||||
labels: ["type: enhancement", "status: awaiting triage"]
|
||||
body:
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Feature Requests
|
||||
url: https://woocommerce.com/feature-requests/woocommerce/
|
||||
about: If you have an idea for a new feature that you wished existed in WooCommerce, take a look at our Feature Requests and vote, or open a new Feature Request yourself!
|
||||
- name: 🔒 Security issue
|
||||
url: https://hackerone.com/automattic/
|
||||
about: For security reasons, please report all security issues via HackerOne. If the issue is valid, a bug bounty will be paid out to you. Please disclose responsibly and not via GitHub (which allows for exploiting issues in the wild before the patch is released).
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -9,6 +9,11 @@ inputs:
|
|||
tests:
|
||||
description: Specific tests to run, separated by single whitespace. See https://playwright.dev/docs/test-cli
|
||||
|
||||
outputs:
|
||||
result:
|
||||
description: Whether the test passed or failed.
|
||||
value: ${{ steps.run-api-tests.conclusion }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
|
|
|
@ -12,6 +12,11 @@ inputs:
|
|||
description: The Playwright configuration file to use.
|
||||
default: playwright.config.js
|
||||
|
||||
outputs:
|
||||
result:
|
||||
description: Whether the test passed or failed.
|
||||
value: ${{ steps.run-e2e-tests.conclusion }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
name: Compose a Slack block for release tests
|
||||
description: Create a Slack block that shows the API and E2E test results from one of the release tests, and upload it as an artifact.
|
||||
permissions: {}
|
||||
|
||||
inputs:
|
||||
test-name:
|
||||
required: true
|
||||
api-result:
|
||||
required: true
|
||||
type: choice
|
||||
default: skipped
|
||||
options:
|
||||
- success
|
||||
- failure
|
||||
- cancelled
|
||||
- skipped
|
||||
e2e-result:
|
||||
required: true
|
||||
type: choice
|
||||
default: skipped
|
||||
options:
|
||||
- success
|
||||
- failure
|
||||
- cancelled
|
||||
- skipped
|
||||
env-slug:
|
||||
required: true
|
||||
release-version:
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Create context block as a JSON object
|
||||
id: generate-json
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/actions/tests/slack-summary-on-release/slack-blocks/scripts/create-result-block');
|
||||
return script();
|
||||
env:
|
||||
API_RESULT: ${{ inputs.api-result }}
|
||||
E2E_RESULT: ${{ inputs.e2e-result }}
|
||||
ENV_SLUG: ${{ inputs.env-slug }}
|
||||
TEST_NAME: ${{ inputs.test-name }}
|
||||
RELEASE_VERSION: ${{ inputs.release-version }}
|
||||
|
||||
- name: Write JSON file
|
||||
working-directory: /tmp
|
||||
shell: bash
|
||||
env:
|
||||
CONTEXT_JSON: ${{ toJSON(steps.generate-json.outputs.result) }}
|
||||
run: echo ${{ env.CONTEXT_JSON }} > "${{ inputs.test-name }}.json"
|
||||
|
||||
- name: Upload JSON file as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.SLACK_BLOCKS_ARTIFACT }}
|
||||
path: /tmp/${{ inputs.test-name }}.json
|
31
.github/actions/tests/slack-summary-on-release/slack-blocks/scripts/create-result-block/index.js
vendored
Normal file
31
.github/actions/tests/slack-summary-on-release/slack-blocks/scripts/create-result-block/index.js
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
module.exports = () => {
|
||||
const { API_RESULT, E2E_RESULT, ENV_SLUG, TEST_NAME, RELEASE_VERSION } =
|
||||
process.env;
|
||||
const { setElementText } = require( './utils' );
|
||||
|
||||
const apiLinkText = setElementText( {
|
||||
testType: 'API',
|
||||
result: API_RESULT,
|
||||
envSlug: ENV_SLUG,
|
||||
releaseVersion: RELEASE_VERSION,
|
||||
} );
|
||||
const e2eLinkText = setElementText( {
|
||||
testType: 'E2E',
|
||||
result: E2E_RESULT,
|
||||
envSlug: ENV_SLUG,
|
||||
releaseVersion: RELEASE_VERSION,
|
||||
} );
|
||||
const elementText = `*${ TEST_NAME }*\n ${ apiLinkText } ${ e2eLinkText }`;
|
||||
|
||||
const contextBlock = {
|
||||
type: 'context',
|
||||
elements: [
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: elementText,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return contextBlock;
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
const { setElementText } = require( './set-element-text' );
|
||||
|
||||
module.exports = {
|
||||
setElementText,
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
const emojis = {
|
||||
PASSED: ':workflow-passed:',
|
||||
FAILED: ':workflow-failed:',
|
||||
SKIPPED: ':workflow-skipped:',
|
||||
CANCELLED: ':workflow-cancelled:',
|
||||
UNKNOWN: ':grey_question:',
|
||||
};
|
||||
|
||||
const selectEmoji = ( result ) => {
|
||||
switch ( result ) {
|
||||
case 'success':
|
||||
return emojis.PASSED;
|
||||
case 'failure':
|
||||
return emojis.FAILED;
|
||||
case 'skipped':
|
||||
return emojis.SKIPPED;
|
||||
case 'cancelled':
|
||||
return emojis.CANCELLED;
|
||||
default:
|
||||
return emojis.UNKNOWN;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
selectEmoji,
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
const setElementText = ( { testType, result, envSlug, releaseVersion } ) => {
|
||||
const { selectEmoji } = require( './select-emoji' );
|
||||
const allureReportURL = `https://woocommerce.github.io/woocommerce-test-reports/release/${ releaseVersion }/${ envSlug }/${ testType.toLowerCase() }`;
|
||||
const emoji = selectEmoji( result );
|
||||
const textValue = `<${ allureReportURL }|${ testType.toUpperCase() } ${ emoji }>`;
|
||||
|
||||
return textValue;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
setElementText,
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
name: Combine all Slack blocks
|
||||
description: Combine all Slack blocks to construct the payload for the Slack GitHub action
|
||||
permissions: {}
|
||||
|
||||
inputs:
|
||||
release-version:
|
||||
required: true
|
||||
blocks-dir:
|
||||
require: true
|
||||
|
||||
outputs:
|
||||
payload:
|
||||
value: ${{ steps.payload.outputs.result }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Construct payload from all blocks
|
||||
id: payload
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
RELEASE_VERSION: ${{ inputs.release-version }}
|
||||
BLOCKS_DIR: ${{ inputs.blocks-dir }}
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/actions/tests/slack-summary-on-release/slack-payload/scripts/construct-payload');
|
||||
return script();
|
36
.github/actions/tests/slack-summary-on-release/slack-payload/scripts/construct-payload/index.js
vendored
Normal file
36
.github/actions/tests/slack-summary-on-release/slack-payload/scripts/construct-payload/index.js
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
module.exports = () => {
|
||||
const { RELEASE_VERSION, BLOCKS_DIR } = process.env;
|
||||
const {
|
||||
filterContextBlocks,
|
||||
readContextBlocksFromJsonFiles,
|
||||
} = require( './utils' );
|
||||
|
||||
const headerText = `Test summary for ${ RELEASE_VERSION }`;
|
||||
const headerBlock = {
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: headerText,
|
||||
emoji: true,
|
||||
},
|
||||
};
|
||||
|
||||
const blocks_all = readContextBlocksFromJsonFiles( BLOCKS_DIR );
|
||||
const blocks_wcUpdate = filterContextBlocks( blocks_all, 'WC Update' );
|
||||
const blocks_wpVersions = filterContextBlocks( blocks_all, 'WP Latest' );
|
||||
const blocks_phpVersions = filterContextBlocks( blocks_all, 'PHP' );
|
||||
const blocks_plugins = filterContextBlocks( blocks_all, 'With' );
|
||||
|
||||
const blocksPayload = [ headerBlock ]
|
||||
.concat( blocks_wcUpdate )
|
||||
.concat( blocks_wpVersions )
|
||||
.concat( blocks_phpVersions )
|
||||
.concat( blocks_plugins );
|
||||
|
||||
const payload = {
|
||||
text: headerText,
|
||||
blocks: blocksPayload,
|
||||
};
|
||||
|
||||
return payload;
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
const fs = require( 'fs' );
|
||||
const path = require( 'path' );
|
||||
|
||||
const readContextBlocksFromJsonFiles = ( blocksDir ) => {
|
||||
const jsonsDir = path.resolve( blocksDir );
|
||||
const jsons = fs.readdirSync( jsonsDir );
|
||||
|
||||
let contextBlocks = [];
|
||||
|
||||
for ( const json of jsons ) {
|
||||
const jsonPath = path.resolve( jsonsDir, json );
|
||||
const contextBlock = require( jsonPath );
|
||||
|
||||
contextBlocks.push( contextBlock );
|
||||
}
|
||||
|
||||
return contextBlocks;
|
||||
};
|
||||
|
||||
const filterContextBlocks = ( blocks, testName ) => {
|
||||
const divider = {
|
||||
type: 'divider',
|
||||
};
|
||||
|
||||
let filteredBlocks = [];
|
||||
|
||||
const matchingBlocks = blocks.filter( ( { elements } ) =>
|
||||
elements[ 0 ].text.includes( testName )
|
||||
);
|
||||
|
||||
matchingBlocks.forEach( ( block ) => {
|
||||
filteredBlocks.push( block );
|
||||
filteredBlocks.push( divider );
|
||||
} );
|
||||
|
||||
return filteredBlocks;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
filterContextBlocks,
|
||||
readContextBlocksFromJsonFiles,
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
const {
|
||||
filterContextBlocks,
|
||||
readContextBlocksFromJsonFiles,
|
||||
} = require( './get-context-blocks' );
|
||||
|
||||
module.exports = { filterContextBlocks, readContextBlocksFromJsonFiles };
|
|
@ -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: ${{ inputs.skipSlackPing != true }}
|
||||
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 );
|
||||
} );
|
||||
} );
|
||||
}
|
|
@ -135,7 +135,7 @@ function get_latest_version_with_release() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Function to retreive the sha1 reference for a given branch name.
|
||||
* Function to retrieve the sha1 reference for a given branch name.
|
||||
*
|
||||
* @param string $branch The name of the branch.
|
||||
* @return string Returns the name of the branch, or a falsey value on 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
|
||||
|
|
|
@ -14,7 +14,7 @@ permissions: {}
|
|||
env:
|
||||
E2E_WP_LATEST_ARTIFACT: E2E test on release smoke test site with WP Latest (run ${{ github.run_number }})
|
||||
E2E_UPDATE_WC_ARTIFACT: WooCommerce version update test on release smoke test site (run ${{ github.run_number }})
|
||||
|
||||
SLACK_BLOCKS_ARTIFACT: slack-blocks
|
||||
jobs:
|
||||
get-tag:
|
||||
name: Get WooCommerce release tag
|
||||
|
@ -122,12 +122,26 @@ jobs:
|
|||
-f test_type="e2e" \
|
||||
--repo woocommerce/woocommerce-test-reports
|
||||
|
||||
- name: Create Slack block
|
||||
if: |
|
||||
success() || (
|
||||
failure() && steps.run-e2e-composite-action.outputs.result == 'failure'
|
||||
)
|
||||
uses: ./.github/actions/tests/slack-summary-on-release/slack-blocks
|
||||
with:
|
||||
test-name: WC Update test
|
||||
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
|
||||
env-slug: wp-latest
|
||||
release-version: ${{ needs.get-tag.outputs.tag }}
|
||||
|
||||
api-wp-latest:
|
||||
name: API on WP Latest
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [get-tag, e2e-update-wc]
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
result: ${{ steps.run-api-composite-action.outputs.result }}
|
||||
env:
|
||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report
|
||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results
|
||||
|
@ -178,6 +192,18 @@ jobs:
|
|||
-f test_type="api" \
|
||||
--repo woocommerce/woocommerce-test-reports
|
||||
|
||||
- name: Create Slack block
|
||||
if: |
|
||||
success() || (
|
||||
failure() && steps.run-api-composite-action.outputs.result == 'failure'
|
||||
)
|
||||
uses: ./.github/actions/tests/slack-summary-on-release/slack-blocks
|
||||
with:
|
||||
test-name: WP Latest
|
||||
api-result: ${{ steps.run-api-composite-action.outputs.result }}
|
||||
env-slug: wp-latest
|
||||
release-version: ${{ needs.get-tag.outputs.tag }}
|
||||
|
||||
e2e-wp-latest:
|
||||
name: E2E on WP Latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
@ -268,6 +294,19 @@ jobs:
|
|||
-f test_type="e2e" \
|
||||
--repo woocommerce/woocommerce-test-reports
|
||||
|
||||
- name: Create Slack block
|
||||
if: |
|
||||
success() || (
|
||||
failure() && steps.run-e2e-composite-action.outputs.result == 'failure'
|
||||
)
|
||||
uses: ./.github/actions/tests/slack-summary-on-release/slack-blocks
|
||||
with:
|
||||
test-name: WP Latest
|
||||
api-result: ${{ needs.api-wp-latest.outputs.result }}
|
||||
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
|
||||
env-slug: wp-latest
|
||||
release-version: ${{ needs.get-tag.outputs.tag }}
|
||||
|
||||
get-wp-versions:
|
||||
name: Get WP L-1 & L-2 version numbers
|
||||
needs: [get-tag]
|
||||
|
@ -328,6 +367,8 @@ jobs:
|
|||
|
||||
- name: Setup WooCommerce Monorepo
|
||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||
with:
|
||||
build-filters: woocommerce
|
||||
|
||||
- name: Launch WP Env
|
||||
working-directory: plugins/woocommerce
|
||||
|
@ -360,7 +401,7 @@ jobs:
|
|||
uses: ./.github/actions/tests/run-api-tests
|
||||
with:
|
||||
report-name: ${{ env.API_WP_LATEST_X_ARTIFACT }}
|
||||
tests: hello
|
||||
tests: hello.test.js
|
||||
env:
|
||||
ALLURE_RESULTS_DIR: ${{ env.API_ALLURE_RESULTS_DIR }}
|
||||
ALLURE_REPORT_DIR: ${{ env.API_ALLURE_REPORT_DIR }}
|
||||
|
@ -435,6 +476,22 @@ jobs:
|
|||
-f test_type="e2e" \
|
||||
--repo woocommerce/woocommerce-test-reports
|
||||
|
||||
- name: Create Slack block
|
||||
if: |
|
||||
success() || (
|
||||
failure() && (
|
||||
steps.run-api-composite-action.outputs.result == 'failure' ||
|
||||
steps.run-e2e-composite-action.outputs.result == 'failure'
|
||||
)
|
||||
)
|
||||
uses: ./.github/actions/tests/slack-summary-on-release/slack-blocks
|
||||
with:
|
||||
test-name: ${{ matrix.version.description }} (${{ matrix.version.number }})
|
||||
api-result: ${{ steps.run-api-composite-action.outputs.result }}
|
||||
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
|
||||
env-slug: ${{ matrix.version.env_description }}
|
||||
release-version: ${{ needs.get-wp-versions.outputs.tag }}
|
||||
|
||||
test-php-versions:
|
||||
name: Test against PHP ${{ matrix.php_version }}
|
||||
runs-on: ubuntu-20.04
|
||||
|
@ -456,6 +513,8 @@ jobs:
|
|||
|
||||
- name: Setup WooCommerce Monorepo
|
||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||
with:
|
||||
build-filters: woocommerce
|
||||
|
||||
- name: Launch WP Env
|
||||
working-directory: plugins/woocommerce
|
||||
|
@ -482,7 +541,7 @@ jobs:
|
|||
uses: ./.github/actions/tests/run-api-tests
|
||||
with:
|
||||
report-name: ${{ env.API_ARTIFACT }}
|
||||
tests: hello
|
||||
tests: hello.test.js
|
||||
env:
|
||||
ALLURE_RESULTS_DIR: ${{ env.API_ALLURE_RESULTS_DIR }}
|
||||
ALLURE_REPORT_DIR: ${{ env.API_ALLURE_REPORT_DIR }}
|
||||
|
@ -557,6 +616,22 @@ jobs:
|
|||
-f test_type="e2e" \
|
||||
--repo woocommerce/woocommerce-test-reports
|
||||
|
||||
- name: Create Slack block
|
||||
if: |
|
||||
success() || (
|
||||
failure() && (
|
||||
steps.run-api-composite-action.outputs.result == 'failure' ||
|
||||
steps.run-e2e-composite-action.outputs.result == 'failure'
|
||||
)
|
||||
)
|
||||
uses: ./.github/actions/tests/slack-summary-on-release/slack-blocks
|
||||
with:
|
||||
test-name: PHP ${{ matrix.php_version }}
|
||||
api-result: ${{ steps.run-api-composite-action.outputs.result }}
|
||||
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
|
||||
env-slug: php-${{ matrix.php_version }}
|
||||
release-version: ${{ needs.get-tag.outputs.tag }}
|
||||
|
||||
test-plugins:
|
||||
name: With ${{ matrix.plugin }}
|
||||
runs-on: ubuntu-20.04
|
||||
|
@ -582,18 +657,20 @@ 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
|
||||
|
||||
- name: Setup WooCommerce Monorepo
|
||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||
with:
|
||||
build-filters: woocommerce
|
||||
|
||||
- name: Launch WP Env
|
||||
working-directory: plugins/woocommerce
|
||||
|
@ -656,3 +733,54 @@ jobs:
|
|||
-f env_description="${{ matrix.env_description }}" \
|
||||
-f test_type="e2e" \
|
||||
--repo woocommerce/woocommerce-test-reports
|
||||
|
||||
- name: Create Slack block
|
||||
if: |
|
||||
success() || (
|
||||
failure() && steps.run-e2e-composite-action.outputs.result == 'failure' )
|
||||
uses: ./.github/actions/tests/slack-summary-on-release/slack-blocks
|
||||
with:
|
||||
test-name: With ${{ matrix.plugin }}
|
||||
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
|
||||
env-slug: ${{ matrix.env_description }}
|
||||
release-version: ${{ needs.get-tag.outputs.tag }}
|
||||
|
||||
post-slack-summary:
|
||||
name: Post Slack summary
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: read
|
||||
if: |
|
||||
success() || (
|
||||
failure() && contains( needs.*.result, 'failure' )
|
||||
)
|
||||
needs:
|
||||
- e2e-wp-latest
|
||||
- get-tag
|
||||
- test-php-versions
|
||||
- test-plugins
|
||||
- test-wp-versions
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download all slack blocks
|
||||
id: download-slack-blocks
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ env.SLACK_BLOCKS_ARTIFACT }}
|
||||
path: /tmp/slack-payload
|
||||
|
||||
- name: Construct payload from all blocks
|
||||
id: run-payload-action
|
||||
uses: ./.github/actions/tests/slack-summary-on-release/slack-payload
|
||||
with:
|
||||
release-version: ${{ needs.get-tag.outputs.tag }}
|
||||
blocks-dir: ${{ steps.download-slack-blocks.outputs.download-path }}
|
||||
|
||||
- name: Send Slack message
|
||||
uses: slackapi/slack-github-action@v1.23.0
|
||||
with:
|
||||
channel-id: ${{ secrets.RELEASE_TEST_SLACK_CHANNEL }}
|
||||
payload: ${{ steps.run-payload-action.outputs.payload }}
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.E2E_SLACK_TOKEN }}
|
||||
|
|
10
.syncpackrc
10
.syncpackrc
|
@ -78,7 +78,6 @@
|
|||
"@wordpress/eslint-plugin",
|
||||
"@wordpress/babel-plugin-import-jsx-pragma",
|
||||
"@wordpress/babel-preset-default",
|
||||
"@wordpress/env",
|
||||
"@wordpress/stylelint-config",
|
||||
"@wordpress/prettier-config",
|
||||
"@wordpress/scripts",
|
||||
|
@ -116,6 +115,15 @@
|
|||
],
|
||||
"isIgnored": true
|
||||
},
|
||||
{
|
||||
"dependencies": [
|
||||
"@wordpress/env"
|
||||
],
|
||||
"packages": [
|
||||
"**"
|
||||
],
|
||||
"pinVersion": "^7.0.0"
|
||||
},
|
||||
{
|
||||
"dependencies": [
|
||||
"@wordpress/**"
|
||||
|
|
|
@ -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.
|
||||
|
|
138
changelog.txt
138
changelog.txt
|
@ -1,5 +1,131 @@
|
|||
== Changelog ==
|
||||
|
||||
= 7.7.0 2023-05-10 =
|
||||
|
||||
**WooCommerce**
|
||||
|
||||
* Fix - Removing auto-draft as wc post type to resolve publish time bug. [#38099](https://github.com/woocommerce/woocommerce/pull/38099)
|
||||
* Fix - Handle updating customer when user_registered is 0000-00-00 00:00:00. [#37907](https://github.com/woocommerce/woocommerce/pull/37907)
|
||||
* Fix - Sync up date_column_name default for orders table, between stats and table data. [#37927](https://github.com/woocommerce/woocommerce/pull/37927)
|
||||
* Fix - Fix regression in supporting nested date query arguments in HPOS. [#37827](https://github.com/woocommerce/woocommerce/pull/37827)
|
||||
* Fix - Fix disabled "Save attributes" button [#37790](https://github.com/woocommerce/woocommerce/pull/37790)
|
||||
* Fix - Accessibility update for product_categories shortcode. [#37445](https://github.com/woocommerce/woocommerce/pull/37445)
|
||||
* Fix - Add sort order to migration script for consistency. [#37545](https://github.com/woocommerce/woocommerce/pull/37545)
|
||||
* Fix - Add support for end_at ID to allow partial verification. [#37446](https://github.com/woocommerce/woocommerce/pull/37446)
|
||||
* Fix - Avoid over-aggressive escaping of the payment gateway title in the context of the checkout thank you page. [#37481](https://github.com/woocommerce/woocommerce/pull/37481)
|
||||
* Fix - Corrects imported ContainerInterface. It was not replaced because of the leading backslash. [#37334](https://github.com/woocommerce/woocommerce/pull/37334)
|
||||
* Fix - Delete shipping zone count transient on woocommerce_shipping_zone_method_added and woocommerce_after_shipping_zone_object_save [#37693](https://github.com/woocommerce/woocommerce/pull/37693)
|
||||
* Fix - Delete tax lookup and order stats database records when an order is deleted while orders table is authoritative [#36601](https://github.com/woocommerce/woocommerce/pull/36601)
|
||||
* Fix - fixed bug where adjust_download_permissions was being scheduled on variable products without downloadable variations [#34828](https://github.com/woocommerce/woocommerce/pull/34828)
|
||||
* Fix - Fixed the attributes table styling in TT2 tabs content area [#37639](https://github.com/woocommerce/woocommerce/pull/37639)
|
||||
* Fix - Fix ellipsis menu overlaps on small screen [#37583](https://github.com/woocommerce/woocommerce/pull/37583)
|
||||
* Fix - Fixes a failing e2e test for product variations [#37246](https://github.com/woocommerce/woocommerce/pull/37246)
|
||||
* Fix - Fix global button aria-disabled style [#37461](https://github.com/woocommerce/woocommerce/pull/37461)
|
||||
* Fix - Fix incorrect variable name in api-core-tests [#37388](https://github.com/woocommerce/woocommerce/pull/37388)
|
||||
* Fix - Fix missing result prop in wcadmin_install_plugin_error track [#37466](https://github.com/woocommerce/woocommerce/pull/37466)
|
||||
* Fix - Fix special characters not rendered in admin titles [#37546](https://github.com/woocommerce/woocommerce/pull/37546)
|
||||
* Fix - Fix table alias issue in order field queries. [#37560](https://github.com/woocommerce/woocommerce/pull/37560)
|
||||
* Fix - Fix the type for order items in the schema definition of REST API v1 and V2 [#35940](https://github.com/woocommerce/woocommerce/pull/35940)
|
||||
* Fix - Lock the product block editor template root [#37685](https://github.com/woocommerce/woocommerce/pull/37685)
|
||||
* Fix - Make migration more strict by removing IGNORE [#37595](https://github.com/woocommerce/woocommerce/pull/37595)
|
||||
* Fix - Minor fixup for getting order ids in verify db command. [#37576](https://github.com/woocommerce/woocommerce/pull/37576)
|
||||
* Fix - Refetch data for "Installed extensions" card after installing a recommended marketing channel. [#37300](https://github.com/woocommerce/woocommerce/pull/37300)
|
||||
* Fix - Remove double checking for woocommerce_tax_display_cart filter. [#37617](https://github.com/woocommerce/woocommerce/pull/37617)
|
||||
* Fix - Remove unique constraint from order_key, since orders can be created with empty order keys, which then conflict with the constraint. [#37594](https://github.com/woocommerce/woocommerce/pull/37594)
|
||||
* Fix - Removing modification to rest_namespace on post type and replacing with middleware. [#37621](https://github.com/woocommerce/woocommerce/pull/37621)
|
||||
* Fix - Replace information_schema queries in favor of create table parsing to remove foreign key constraints during updates [#37299](https://github.com/woocommerce/woocommerce/pull/37299)
|
||||
* Fix - Restores comments (reviews) to the product editor. [#37457](https://github.com/woocommerce/woocommerce/pull/37457)
|
||||
* Fix - Revert changes to use window.fetch in legacy cart JS [#37463](https://github.com/woocommerce/woocommerce/pull/37463)
|
||||
* Fix - Synchronized SSR from template to REST API. [#37425](https://github.com/woocommerce/woocommerce/pull/37425)
|
||||
* Fix - Updates an e2e variable name to be more descriptive [#37448](https://github.com/woocommerce/woocommerce/pull/37448)
|
||||
* Fix - update select all to checkbox in menu editor [#37562](https://github.com/woocommerce/woocommerce/pull/37562)
|
||||
* Fix - Use first meta value for HPOS migration when there are duplicates for flat column. [#37676](https://github.com/woocommerce/woocommerce/pull/37676)
|
||||
* Fix - Hide stock status field if stock management is enabled. [#37890](https://github.com/woocommerce/woocommerce/pull/37890)
|
||||
* Add - Add plugin installer version independent of WP cron. [#37753](https://github.com/woocommerce/woocommerce/pull/37753)
|
||||
* Add - Add a category for product editor blocks [#37347](https://github.com/woocommerce/woocommerce/pull/37347)
|
||||
* Add - Add country query param to payment gateway data sources [#37443](https://github.com/woocommerce/woocommerce/pull/37443)
|
||||
* Add - Add image configuration to the product block template [#37340](https://github.com/woocommerce/woocommerce/pull/37340)
|
||||
* Add - Add images block to product editor template [#37455](https://github.com/woocommerce/woocommerce/pull/37455)
|
||||
* Add - add import webp support [#37307](https://github.com/woocommerce/woocommerce/pull/37307)
|
||||
* Add - Adding charge sales tax field to product block editor template. [#37582](https://github.com/woocommerce/woocommerce/pull/37582)
|
||||
* Add - Adding checkbox, conditional and inventory email blocks to product blocks editor. [#37646](https://github.com/woocommerce/woocommerce/pull/37646)
|
||||
* Add - Adding inventory section and sku blocks to product block editor. [#37623](https://github.com/woocommerce/woocommerce/pull/37623)
|
||||
* Add - Add method delete_meta_data_value to WC_Data objects [#37667](https://github.com/woocommerce/woocommerce/pull/37667)
|
||||
* Add - Add methods to OrderUtil to get the names of order database tables [#37624](https://github.com/woocommerce/woocommerce/pull/37624)
|
||||
* Add - Add pricing section to the pricing tab [#37513](https://github.com/woocommerce/woocommerce/pull/37513)
|
||||
* Add - Add product editor blocks to assets build folder [#37318](https://github.com/woocommerce/woocommerce/pull/37318)
|
||||
* Add - Add product schedule sale pricing block to blocks template definition [#37567](https://github.com/woocommerce/woocommerce/pull/37567)
|
||||
* Add - Add product shipping fee block to blocks template definition [#37642](https://github.com/woocommerce/woocommerce/pull/37642)
|
||||
* Add - Add summary block [#37302](https://github.com/woocommerce/woocommerce/pull/37302)
|
||||
* Add - Add tax class to product editor template [#37529](https://github.com/woocommerce/woocommerce/pull/37529)
|
||||
* Add - Add tracks events to attributes tab [#37622](https://github.com/woocommerce/woocommerce/pull/37622)
|
||||
* Add - Add tracks events to variations tab [#37607](https://github.com/woocommerce/woocommerce/pull/37607)
|
||||
* Add - Add Woo Payments feature slotfill on homepage [#37768](https://github.com/woocommerce/woocommerce/pull/37768)
|
||||
* Add - Allows the WP, WC & PHP version to be specified in .wp-env.json for e2e and api tests [#37587](https://github.com/woocommerce/woocommerce/pull/37587)
|
||||
* Add - Change variations dropdown menu visibility [#37558](https://github.com/woocommerce/woocommerce/pull/37558)
|
||||
* Add - Register product track inventory block [#37585](https://github.com/woocommerce/woocommerce/pull/37585)
|
||||
* Add - Register woocommerce/product-shipping-dimensions-fields block [#37683](https://github.com/woocommerce/woocommerce/pull/37683)
|
||||
* Update - Update WooCommerce Blocks to 10.0.2 [#37818](https://github.com/woocommerce/woocommerce/pull/37818)
|
||||
* Update - Update WC Blocks to include changes from 9.9.0, 10.0.0 and 10.0.1 [#37662](https://github.com/woocommerce/woocommerce/pull/37662)
|
||||
* Update - Add default priority for countries that are not in the payment recommendation map [#37590](https://github.com/woocommerce/woocommerce/pull/37590)
|
||||
* Update - Add Payoneer, zipco payment gateways and update Klarna available countries [#37329](https://github.com/woocommerce/woocommerce/pull/37329)
|
||||
* Update - Filter out marketing channels in "Installed extensions" and "Discover more marketing tools" cards. [#37126](https://github.com/woocommerce/woocommerce/pull/37126)
|
||||
* Update - FlexSlider always uses Web Animations API for "slide" animations. [#36987](https://github.com/woocommerce/woocommerce/pull/36987)
|
||||
* Update - Make Multichannel Marketing the default new UI for Marketing page; remove classic Marketing page and unused code. [#37430](https://github.com/woocommerce/woocommerce/pull/37430)
|
||||
* Update - Migrate steps location task to TS [#37257](https://github.com/woocommerce/woocommerce/pull/37257)
|
||||
* Update - Refactoring product editor more menu items, and using in block editor slot fills. [#37255](https://github.com/woocommerce/woocommerce/pull/37255)
|
||||
* Update - Register product editor blocks server-side [#37339](https://github.com/woocommerce/woocommerce/pull/37339)
|
||||
* Update - Remove multichannel marketing info from WC Tracker [#37438](https://github.com/woocommerce/woocommerce/pull/37438)
|
||||
* Update - Remove theme step from onboarding wizard [#37671](https://github.com/woocommerce/woocommerce/pull/37671)
|
||||
* Update - Replacing multiple components on the block product page with a single hook. [#37283](https://github.com/woocommerce/woocommerce/pull/37283)
|
||||
* Update - Show different error message when deleting an attribute used in variations [#37527](https://github.com/woocommerce/woocommerce/pull/37527)
|
||||
* Update - Show tooltip in Save attributes button instead of using title attribute [#37345](https://github.com/woocommerce/woocommerce/pull/37345)
|
||||
* Update - Support min_php_version and min_wp_version for the free extensions feed [#37694](https://github.com/woocommerce/woocommerce/pull/37694)
|
||||
* Update - Update payment gateway recommendation priority [#37442](https://github.com/woocommerce/woocommerce/pull/37442)
|
||||
* Update - Update textdomain in woocommerce-blocks *.json files to `woocommerce` [#37234](https://github.com/woocommerce/woocommerce/pull/37234)
|
||||
* Dev - Fix recent failures in "Smoke test release" workflow. [#37783](https://github.com/woocommerce/woocommerce/pull/37783)
|
||||
* Dev - Add composer scripts for linting with phpcs-changed [#37465](https://github.com/woocommerce/woocommerce/pull/37465)
|
||||
* Dev - Add tracks event to gather onboarding heuristics [#37767](https://github.com/woocommerce/woocommerce/pull/37767)
|
||||
* Dev - Bump required PHP version to 7.3 and PHPUnit version to 9 [#37366](https://github.com/woocommerce/woocommerce/pull/37366)
|
||||
* Dev - Code refactor on marketing components. [#37444](https://github.com/woocommerce/woocommerce/pull/37444)
|
||||
* Dev - Dev - Add customer object parameter to taxable address filter [#37426](https://github.com/woocommerce/woocommerce/pull/37426)
|
||||
* Dev - Dev - Allow to filter wc_help_tip [#37485](https://github.com/woocommerce/woocommerce/pull/37485)
|
||||
* Dev - Fix WP latest-2 version retrieval in the "Smoke test release" workflow. [#37675](https://github.com/woocommerce/woocommerce/pull/37675)
|
||||
* Dev - Item controls for attribute creation are always visible [#37620](https://github.com/woocommerce/woocommerce/pull/37620)
|
||||
* Dev - Migrate woocommerce-payments task to TS and remove connect.js from task fills [#37308](https://github.com/woocommerce/woocommerce/pull/37308)
|
||||
* Dev - Move additional CES-related components to @woocommerce/customer-effort-score. [#37316](https://github.com/woocommerce/woocommerce/pull/37316)
|
||||
* Dev - Move components to @woocommerce/product-editor [#37131](https://github.com/woocommerce/woocommerce/pull/37131)
|
||||
* Dev - New empty state for variations - no variations have been created yet [#37411](https://github.com/woocommerce/woocommerce/pull/37411)
|
||||
* Dev - Reduce flakiness on E2E test setup. [#37410](https://github.com/woocommerce/woocommerce/pull/37410)
|
||||
* Dev - Rename the default placeholder in the new attribute form header to New attribute [#37645](https://github.com/woocommerce/woocommerce/pull/37645)
|
||||
* Dev - Replaced `example.org` in tests with WP_TESTS_DOMAIN for consistency with WordPress Core. [#37742](https://github.com/woocommerce/woocommerce/pull/37742)
|
||||
* Dev - Reset variable product tour after running e2e tests. [#37680](https://github.com/woocommerce/woocommerce/pull/37680)
|
||||
* Dev - Run E2E tests on PR merge to trunk. [#37033](https://github.com/woocommerce/woocommerce/pull/37033)
|
||||
* Dev - Set quantity value when stock tracking is enabled [#37304](https://github.com/woocommerce/woocommerce/pull/37304)
|
||||
* Dev - Simplify boolean expression before && in Marketing page. [#37452](https://github.com/woocommerce/woocommerce/pull/37452)
|
||||
* Dev - Smoke test WooCommerce with plugins installed on releases. [#37361](https://github.com/woocommerce/woocommerce/pull/37361)
|
||||
* Dev - Split can create product, attributes and variations, edit variations and delete variations into smaller tests to avoid timing out [#37733](https://github.com/woocommerce/woocommerce/pull/37733)
|
||||
* Dev - Update webpack config to use @woocommerce/internal-style-build's parser config [#37195](https://github.com/woocommerce/woocommerce/pull/37195)
|
||||
* Tweak - Update plugin listing description [#38074](https://github.com/woocommerce/woocommerce/pull/38074)
|
||||
* Tweak - Changed label for button to add a new global attribute value from the product screen. [#37414](https://github.com/woocommerce/woocommerce/pull/37414)
|
||||
* Tweak - Default to sorting orders by date (desc) when HPOS is active. [#37565](https://github.com/woocommerce/woocommerce/pull/37565)
|
||||
* Tweak - Exclude empty attributes from the attribute count when tracking product updates. [#37718](https://github.com/woocommerce/woocommerce/pull/37718)
|
||||
* Tweak - Fix typo in a function comment. [#37746](https://github.com/woocommerce/woocommerce/pull/37746)
|
||||
* Tweak - Fix typo in Stats controller [#37407](https://github.com/woocommerce/woocommerce/pull/37407)
|
||||
* Tweak - Fix typos in comments in REST API customers controller [#37405](https://github.com/woocommerce/woocommerce/pull/37405)
|
||||
* Tweak - Remove the multichannel marketing feature flag from database since it's the default option now. [#37454](https://github.com/woocommerce/woocommerce/pull/37454)
|
||||
* Tweak - Remove timeouts in e2e tests for variable products and analytics. [#37335](https://github.com/woocommerce/woocommerce/pull/37335)
|
||||
* Tweak - Update mobile app image resolution [#37506](https://github.com/woocommerce/woocommerce/pull/37506)
|
||||
* Tweak - Update style of product attributes tab empty state. [#37429](https://github.com/woocommerce/woocommerce/pull/37429)
|
||||
* Tweak - Update to the merchant variable product e2e test [#37714](https://github.com/woocommerce/woocommerce/pull/37714)
|
||||
* Performance - Improve search count query performance by avoiding LEFT JOIN in favor of subquery. [#36688](https://github.com/woocommerce/woocommerce/pull/36688)
|
||||
* Enhancement - Added a button to download SSR to a file. [#38110](https://github.com/woocommerce/woocommerce/pull/38110)
|
||||
* Enhancement - Added a woocommerce_disable_api_access_log filter to disable last access logging for rest api. [#37332](https://github.com/woocommerce/woocommerce/pull/37332)
|
||||
* Enhancement - Fix rounding difference on refunds with per-line taxes [#34641](https://github.com/woocommerce/woocommerce/pull/34641)
|
||||
* Enhancement - Show info message when on variations tab and no attributes have been assigned to product. [#37352](https://github.com/woocommerce/woocommerce/pull/37352)
|
||||
* Enhancement - Show tour when product type is changed to variable. [#37413](https://github.com/woocommerce/woocommerce/pull/37413)
|
||||
|
||||
|
||||
= 7.6.1 2023-04-26 =
|
||||
|
||||
**WooCommerce**
|
||||
|
@ -54,7 +180,7 @@
|
|||
* Fix - Prevent possible warning arising from use of woocommerce_wp_* family of functions. [#37026](https://github.com/woocommerce/woocommerce/pull/37026)
|
||||
* Fix - Record values for toggled checkboxes/features in settings [#37242](https://github.com/woocommerce/woocommerce/pull/37242)
|
||||
* Fix - Restore the sort order when orders are cached. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
|
||||
* Fix - Treat order as seperate resource when validating for webhook since it's not necessarily a CPT anymore. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
|
||||
* Fix - Treat order as separate resource when validating for webhook since it's not necessarily a CPT anymore. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
|
||||
* Fix - Update Customers report with latest user data after editing user. [#37237](https://github.com/woocommerce/woocommerce/pull/37237)
|
||||
* Add - Add "Create a new campaign" modal in Campaigns card in Multichannel Marketing page. [#37044](https://github.com/woocommerce/woocommerce/pull/37044)
|
||||
* Add - Add a cache for orders, to use when custom order tables are enabled [#35014](https://github.com/woocommerce/woocommerce/pull/35014)
|
||||
|
@ -432,7 +558,7 @@
|
|||
* Tweak - Resolve an error in the product tracking code by testing to see if the `post_type` query var is set before checking its value. [#34501](https://github.com/woocommerce/woocommerce/pull/34501)
|
||||
* Tweak - Simplify wording within the customer emails for on-hold orders. [#31886](https://github.com/woocommerce/woocommerce/pull/31886)
|
||||
* Tweak - WooCommerce has now been tested up to WordPress 6.1.x. [#35985](https://github.com/woocommerce/woocommerce/pull/35985)
|
||||
* Performance - Split CALC_FOUND_ROW query into seperate count query for better performance. [#35468](https://github.com/woocommerce/woocommerce/pull/35468)
|
||||
* Performance - Split CALC_FOUND_ROW query into separate count query for better performance. [#35468](https://github.com/woocommerce/woocommerce/pull/35468)
|
||||
* Enhancement - Add a bottom padding to the whole form [#35721](https://github.com/woocommerce/woocommerce/pull/35721)
|
||||
* Enhancement - Add a confirmation modal when the user tries to navigate away with unsaved changes [#35625](https://github.com/woocommerce/woocommerce/pull/35625)
|
||||
* Enhancement - Add a default placeholder title for newly added attributes and always show remove button for attributes [#35904](https://github.com/woocommerce/woocommerce/pull/35904)
|
||||
|
@ -3371,7 +3497,7 @@
|
|||
* Fix - Add protection around func_get_args_call for backwards compatibility. #28677
|
||||
* Fix - Restore stock_status in REST API V3 response. #28731
|
||||
* Fix - Revert some of the changes related to perf enhancements (27735) as it was breaking stock_status filter. #28745
|
||||
* Dev - Hook for intializing price slider in frontend. #28014
|
||||
* Dev - Hook for initializing price slider in frontend. #28014
|
||||
* Dev - Add support for running a custom initialization script for tests. #28041
|
||||
* Dev - Use the coenjacobs/mozart package to renamespace vendor packages. #28147
|
||||
* Dev - Documentation for `wc_get_container`. #28269
|
||||
|
@ -5290,7 +5416,7 @@
|
|||
* Fix - Fix edge case where `get_plugins` would not have the custom WooCommerce plugin headers if `get_plugins` was called early. #21669
|
||||
* Fix - Prevent PHP warning when deprecated user meta starts with uppercase. #21943
|
||||
* Fix - Fixed support for multiple query parameters translated to meta queries via REST API requests. #22108
|
||||
* Fix - Prevent PHP errors when trying to access non-existant report tabs. #22183
|
||||
* Fix - Prevent PHP errors when trying to access non-existent report tabs. #22183
|
||||
* Fix - Filter by attributes dropdown placeholder text should not be wrapped in quotes. #22185
|
||||
* Fix - Apply sale price until end of closing sale date. #22189
|
||||
* Fix - Allow empty schema again when registering a custom field for the API. #22204
|
||||
|
@ -6612,7 +6738,7 @@
|
|||
* Removed internal scroll from log viewer.
|
||||
* Add reply-to to admin emails.
|
||||
* Improved the zone setup flow.
|
||||
* Made wc_get_wildcard_postcodes return the orignal postcode plus * since wildcards should match empty strings too.
|
||||
* Made wc_get_wildcard_postcodes return the original postcode plus * since wildcards should match empty strings too.
|
||||
* Use all paid statuses in $customer->get_total_spent().
|
||||
* Move location of billing email field to work with password managers.
|
||||
* Option to restrict selling locations by country.
|
||||
|
@ -8122,7 +8248,7 @@
|
|||
* Tweak - Flat rate shipping support for percentage factor of additional costs.
|
||||
* Tweak - local delivery _ pattern matching for postcodes. e.g. NG1___ would match NG1 1AA but not NG10 1AA.
|
||||
* Tweak - Improved layered nav OR count logic
|
||||
* Tweak - Make shipping methods check if taxable, so when customer is VAT excempt taxes are not included in price.
|
||||
* Tweak - Make shipping methods check if taxable, so when customer is VAT exempt taxes are not included in price.
|
||||
* Tweak - Coupon in admin bar new menu #3974
|
||||
* Tweak - Shortcode tag filters + updated menu names to make white labelling easier.
|
||||
* Tweak - Removed placeholder polyfill. Use this plugin to replace functionality if required: https://wordpress.org/plugins/html5-placeholder-polyfill/
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"create-extension": "node ./tools/create-extension/index.js",
|
||||
"cherry-pick": "node ./tools/cherry-pick/bin/run",
|
||||
"sync-dependencies": "pnpm exec syncpack -- fix-mismatches",
|
||||
"utils": "./tools/monorepo-utils/bin/run"
|
||||
"utils": "node ./tools/monorepo-utils/dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
|
|
|
@ -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: add
|
||||
|
||||
Add single selection mode to SelectTree
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Migrate select control component to TS
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Correct spelling errors
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Show comma separated list in ready only mode of select tree control
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add allowDragging option to ImageGallery to support disabling drag and drop of images.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
TreeControl: Fix a bug where items with children were not selected in single mode and fix a bug where navigation between items with tab was not working properly.
|
|
@ -94,7 +94,7 @@ Name | Type | Default | Description
|
|||
`legendPosition` | One of: 'bottom', 'side', 'top', 'hidden' | `null` | Position the legend must be displayed in. If it's not defined, it's calculated depending on the viewport width and the mode
|
||||
`legendTotals` | Object | `null` | Values to overwrite the legend totals. If not defined, the sum of all line values will be used
|
||||
`screenReaderFormat` | One of type: string, func | `'%B %-d, %Y'` | A datetime formatting string or overriding function to format the screen reader labels
|
||||
`showHeaderControls` | Boolean | `true` | Wether header UI controls must be displayed
|
||||
`showHeaderControls` | Boolean | `true` | Whether header UI controls must be displayed
|
||||
`title` | String | `null` | A title describing this chart
|
||||
`tooltipLabelFormat` | One of type: string, func | `'%B %-d, %Y'` | A datetime formatting string or overriding function to format the tooltip label
|
||||
`tooltipValueFormat` | One of type: string, func | `','` | A number formatting string or function to format the value displayed in the tooltips
|
||||
|
|
|
@ -577,7 +577,7 @@ Chart.propTypes = {
|
|||
PropTypes.func,
|
||||
] ),
|
||||
/**
|
||||
* Wether header UI controls must be displayed.
|
||||
* Whether header UI controls must be displayed.
|
||||
*/
|
||||
showHeaderControls: PropTypes.bool,
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -75,8 +75,8 @@ export const ComboBox = ( {
|
|||
<input
|
||||
{ ...inputProps }
|
||||
ref={ ( node ) => {
|
||||
inputRef.current = node;
|
||||
if ( typeof inputProps.ref === 'function' ) {
|
||||
inputRef.current = node;
|
||||
(
|
||||
inputProps.ref as unknown as (
|
||||
node: HTMLInputElement | null
|
||||
|
|
|
@ -12,6 +12,18 @@
|
|||
border-color: var( --wp-admin-theme-color );
|
||||
}
|
||||
|
||||
&.is-read-only.is-multiple.has-selected-items {
|
||||
.woocommerce-experimental-select-control__combo-box-wrapper {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.woocommerce-experimental-select-control__input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: inline-block;
|
||||
margin-bottom: $gap-smaller;
|
||||
|
|
|
@ -9,13 +9,14 @@ import {
|
|||
useMultipleSelection,
|
||||
GetInputPropsOptions,
|
||||
} from 'downshift';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
createElement,
|
||||
Fragment,
|
||||
} from '@wordpress/element';
|
||||
import { search } from '@wordpress/icons';
|
||||
import { chevronDown } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -57,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 >,
|
||||
|
@ -69,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
|
||||
|
@ -117,18 +121,24 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
onRemove = () => null,
|
||||
onSelect = () => null,
|
||||
onFocus = () => null,
|
||||
onKeyDown = () => null,
|
||||
stateReducer = ( state, actionAndChanges ) => actionAndChanges.changes,
|
||||
placeholder,
|
||||
selected,
|
||||
className,
|
||||
disabled,
|
||||
inputProps = {},
|
||||
suffix = <SuffixIcon icon={ search } />,
|
||||
suffix = <SuffixIcon icon={ chevronDown } />,
|
||||
showToggleButton = false,
|
||||
readOnlyWhenClosed = true,
|
||||
__experimentalOpenMenuOnFocus = false,
|
||||
}: SelectControlProps< ItemType > ) {
|
||||
const [ isFocused, setIsFocused ] = useState( false );
|
||||
const [ inputValue, setInputValue ] = useState( '' );
|
||||
const instanceId = useInstanceId(
|
||||
SelectControl,
|
||||
'woocommerce-experimental-select-control'
|
||||
);
|
||||
|
||||
let selectedItems = selected === null ? [] : selected;
|
||||
selectedItems = Array.isArray( selectedItems )
|
||||
|
@ -230,15 +240,24 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
},
|
||||
} );
|
||||
|
||||
const isEventOutside = ( event: React.FocusEvent ) => {
|
||||
return ! document
|
||||
.querySelector( '.' + instanceId )
|
||||
?.contains( event.relatedTarget );
|
||||
};
|
||||
|
||||
const onRemoveItem = ( item: ItemType ) => {
|
||||
selectItem( null );
|
||||
removeSelectedItem( item );
|
||||
onRemove( item );
|
||||
};
|
||||
|
||||
const isReadOnly = readOnlyWhenClosed && ! isOpen && ! isFocused;
|
||||
|
||||
const selectedItemTags = multiple ? (
|
||||
<SelectedItems
|
||||
items={ selectedItems }
|
||||
isReadOnly={ isReadOnly }
|
||||
getItemLabel={ getItemLabel }
|
||||
getItemValue={ getItemValue }
|
||||
getSelectedItemProps={ getSelectedItemProps }
|
||||
|
@ -251,8 +270,12 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
className={ classnames(
|
||||
'woocommerce-experimental-select-control',
|
||||
className,
|
||||
instanceId,
|
||||
{
|
||||
'is-read-only': isReadOnly,
|
||||
'is-focused': isFocused,
|
||||
'is-multiple': multiple,
|
||||
'has-selected-items': selectedItems.length,
|
||||
}
|
||||
) }
|
||||
>
|
||||
|
@ -282,7 +305,12 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
openMenu();
|
||||
}
|
||||
},
|
||||
onBlur: () => setIsFocused( false ),
|
||||
onBlur: ( event: React.FocusEvent ) => {
|
||||
if ( isEventOutside( event ) ) {
|
||||
setIsFocused( false );
|
||||
}
|
||||
},
|
||||
onKeyDown,
|
||||
placeholder,
|
||||
disabled,
|
||||
...inputProps,
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
.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 {
|
||||
margin-right: $gap-smallest;
|
||||
margin-top: 2px;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement, Fragment } from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -10,6 +11,7 @@ import Tag from '../tag';
|
|||
import { getItemLabelType, getItemValueType } from './types';
|
||||
|
||||
type SelectedItemsProps< ItemType > = {
|
||||
isReadOnly: boolean;
|
||||
items: ItemType[];
|
||||
getItemLabel: getItemLabelType< ItemType >;
|
||||
getItemValue: getItemValueType< ItemType >;
|
||||
|
@ -22,14 +24,34 @@ type SelectedItemsProps< ItemType > = {
|
|||
};
|
||||
|
||||
export const SelectedItems = < ItemType, >( {
|
||||
isReadOnly,
|
||||
items,
|
||||
getItemLabel,
|
||||
getItemValue,
|
||||
getSelectedItemProps,
|
||||
onRemove,
|
||||
}: SelectedItemsProps< ItemType > ) => {
|
||||
const classes = classnames(
|
||||
'woocommerce-experimental-select-control__selected-items',
|
||||
{
|
||||
'is-read-only': isReadOnly,
|
||||
}
|
||||
);
|
||||
|
||||
if ( isReadOnly ) {
|
||||
return (
|
||||
<div className={ classes }>
|
||||
{ items
|
||||
.map( ( item ) => {
|
||||
return getItemLabel( item );
|
||||
} )
|
||||
.join( ', ' ) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={ classes }>
|
||||
{ items.map( ( item, index ) => {
|
||||
return (
|
||||
// Disable reason: We prevent the default action to keep the input focused on click.
|
||||
|
@ -42,6 +64,9 @@ export const SelectedItems = < ItemType, >( {
|
|||
selectedItem: item,
|
||||
index,
|
||||
} ) }
|
||||
onMouseDown={ ( event ) => {
|
||||
event.preventDefault();
|
||||
} }
|
||||
onClick={ ( event ) => {
|
||||
event.preventDefault();
|
||||
} }
|
||||
|
@ -56,6 +81,6 @@ export const SelectedItems = < ItemType, >( {
|
|||
</div>
|
||||
);
|
||||
} ) }
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from '../experimental-tree-control';
|
||||
|
||||
type MenuProps = {
|
||||
isEventOutside: ( event: React.FocusEvent ) => boolean;
|
||||
isOpen: boolean;
|
||||
isLoading?: boolean;
|
||||
position?: Popover.Position;
|
||||
|
@ -32,6 +33,7 @@ type MenuProps = {
|
|||
} & Omit< TreeControlProps, 'items' >;
|
||||
|
||||
export const SelectTreeMenu = ( {
|
||||
isEventOutside,
|
||||
isLoading,
|
||||
isOpen,
|
||||
className,
|
||||
|
@ -103,8 +105,10 @@ export const SelectTreeMenu = ( {
|
|||
) }
|
||||
position={ position }
|
||||
animate={ false }
|
||||
onFocusOutside={ () => {
|
||||
onClose();
|
||||
onFocusOutside={ ( event ) => {
|
||||
if ( isEventOutside( event ) ) {
|
||||
onClose();
|
||||
}
|
||||
} }
|
||||
>
|
||||
{ isOpen && (
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement, useState } from '@wordpress/element';
|
||||
import { chevronDown } from '@wordpress/icons';
|
||||
import classNames from 'classnames';
|
||||
import { search } from '@wordpress/icons';
|
||||
import { createElement, useState } from '@wordpress/element';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { BaseControl, TextControl } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -19,8 +19,7 @@ import { SelectTreeMenu } from './select-tree-menu';
|
|||
|
||||
interface SelectTreeProps extends TreeControlProps {
|
||||
id: string;
|
||||
selected?: Item[];
|
||||
getSelectedItemProps?: any;
|
||||
selected?: Item | Item[];
|
||||
treeRef?: React.ForwardedRef< HTMLOListElement >;
|
||||
suffix?: JSX.Element | null;
|
||||
isLoading?: boolean;
|
||||
|
@ -30,9 +29,8 @@ interface SelectTreeProps extends TreeControlProps {
|
|||
|
||||
export const SelectTree = function SelectTree( {
|
||||
items,
|
||||
getSelectedItemProps,
|
||||
treeRef: ref,
|
||||
suffix = <SuffixIcon icon={ search } />,
|
||||
suffix = <SuffixIcon icon={ chevronDown } />,
|
||||
placeholder,
|
||||
isLoading,
|
||||
onInputChange,
|
||||
|
@ -40,114 +38,171 @@ export const SelectTree = function SelectTree( {
|
|||
...props
|
||||
}: SelectTreeProps ) {
|
||||
const linkedTree = useLinkedTree( items );
|
||||
const selectTreeInstanceId = useInstanceId(
|
||||
SelectTree,
|
||||
'woocommerce-experimental-select-tree-control__dropdown'
|
||||
);
|
||||
const menuInstanceId = useInstanceId(
|
||||
SelectTree,
|
||||
'woocommerce-select-tree-control__menu'
|
||||
);
|
||||
const isEventOutside = ( event: React.FocusEvent ) => {
|
||||
return ! document
|
||||
.querySelector( '.' + selectTreeInstanceId )
|
||||
?.contains( event.relatedTarget );
|
||||
};
|
||||
|
||||
const recalculateInputValue = () => {
|
||||
if ( onInputChange ) {
|
||||
if ( ! props.multiple && props.selected ) {
|
||||
onInputChange( ( props.selected as Item ).label );
|
||||
} else {
|
||||
onInputChange( '' );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const focusOnInput = () => {
|
||||
(
|
||||
document.querySelector( `#${ props.id }-input` ) as HTMLInputElement
|
||||
)?.focus();
|
||||
};
|
||||
|
||||
const [ isFocused, setIsFocused ] = useState( false );
|
||||
const [ isOpen, setIsOpen ] = useState( false );
|
||||
const isReadOnly = ! isOpen && ! isFocused;
|
||||
|
||||
const inputProps: React.InputHTMLAttributes< HTMLInputElement > = {
|
||||
className: 'woocommerce-experimental-select-control__input',
|
||||
id: `${ props.id }-input`,
|
||||
'aria-autocomplete': 'list',
|
||||
'aria-controls': `${ props.id }-menu`,
|
||||
autoComplete: 'off',
|
||||
onFocus: () => {
|
||||
if ( ! isOpen ) {
|
||||
setIsOpen( true );
|
||||
}
|
||||
setIsFocused( true );
|
||||
},
|
||||
onBlur: ( event ) => {
|
||||
if ( isOpen && isEventOutside( event ) ) {
|
||||
setIsOpen( false );
|
||||
recalculateInputValue();
|
||||
}
|
||||
setIsFocused( false );
|
||||
},
|
||||
onKeyDown: ( event ) => {
|
||||
setIsOpen( true );
|
||||
if ( event.key === 'ArrowDown' ) {
|
||||
event.preventDefault();
|
||||
// focus on the first element from the Popover
|
||||
(
|
||||
document.querySelector(
|
||||
`.${ menuInstanceId } input, .${ menuInstanceId } button`
|
||||
) as HTMLInputElement | HTMLButtonElement
|
||||
)?.focus();
|
||||
}
|
||||
if ( event.key === 'Tab' ) {
|
||||
setIsOpen( false );
|
||||
recalculateInputValue();
|
||||
}
|
||||
},
|
||||
onChange: ( event ) =>
|
||||
onInputChange && onInputChange( event.target.value ),
|
||||
placeholder,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="woocommerce-experimental-select-tree-control__dropdown"
|
||||
className={ `woocommerce-experimental-select-tree-control__dropdown ${ selectTreeInstanceId }` }
|
||||
tabIndex={ -1 }
|
||||
>
|
||||
<div
|
||||
className={ classNames(
|
||||
'woocommerce-experimental-select-control',
|
||||
{
|
||||
'is-read-only': isReadOnly,
|
||||
'is-focused': isFocused,
|
||||
'is-multiple': props.multiple,
|
||||
'has-selected-items':
|
||||
Array.isArray( props.selected ) &&
|
||||
props.selected.length,
|
||||
}
|
||||
) }
|
||||
>
|
||||
<label
|
||||
htmlFor={ `${ props.id }-input` }
|
||||
id={ `${ props.id }-label` }
|
||||
className="woocommerce-experimental-select-control__label"
|
||||
>
|
||||
{ props.label }
|
||||
</label>
|
||||
<ComboBox
|
||||
comboBoxProps={ {
|
||||
className:
|
||||
'woocommerce-experimental-select-control__combo-box-wrapper',
|
||||
role: 'combobox',
|
||||
'aria-expanded': isOpen,
|
||||
'aria-haspopup': 'tree',
|
||||
'aria-labelledby': `${ props.id }-label`,
|
||||
'aria-owns': `${ props.id }-menu`,
|
||||
} }
|
||||
inputProps={ {
|
||||
className:
|
||||
'woocommerce-experimental-select-control__input',
|
||||
id: `${ props.id }-input`,
|
||||
'aria-autocomplete': 'list',
|
||||
'aria-controls': `${ props.id }-menu`,
|
||||
autoComplete: 'off',
|
||||
onFocus: () => {
|
||||
if ( ! isOpen ) {
|
||||
setIsOpen( true );
|
||||
}
|
||||
setIsFocused( true );
|
||||
},
|
||||
onBlur: ( event ) => {
|
||||
// if blurring to an element inside the dropdown, don't close it
|
||||
if (
|
||||
isOpen &&
|
||||
! document
|
||||
.querySelector( '.' + menuInstanceId )
|
||||
?.contains( event.relatedTarget )
|
||||
) {
|
||||
setIsOpen( false );
|
||||
}
|
||||
setIsFocused( false );
|
||||
},
|
||||
onKeyDown: ( event ) => {
|
||||
setIsOpen( true );
|
||||
if ( event.key === 'ArrowDown' ) {
|
||||
event.preventDefault();
|
||||
// focus on the first element from the Popover
|
||||
(
|
||||
document.querySelector(
|
||||
`.${ menuInstanceId } input, .${ menuInstanceId } button`
|
||||
) as HTMLInputElement | HTMLButtonElement
|
||||
)?.focus();
|
||||
}
|
||||
if ( event.key === 'Tab' ) {
|
||||
setIsOpen( false );
|
||||
}
|
||||
},
|
||||
onChange: ( event ) =>
|
||||
onInputChange &&
|
||||
onInputChange( event.target.value ),
|
||||
placeholder,
|
||||
} }
|
||||
suffix={ suffix }
|
||||
>
|
||||
<SelectedItems
|
||||
items={ ( props.selected as Item[] ) || [] }
|
||||
getItemLabel={ ( item ) => item?.label || '' }
|
||||
getItemValue={ ( item ) => item?.value || '' }
|
||||
onRemove={ ( item ) => {
|
||||
if ( ! Array.isArray( item ) && props.onRemove ) {
|
||||
props.onRemove( item );
|
||||
setIsOpen( false );
|
||||
}
|
||||
} }
|
||||
getSelectedItemProps={ () => ( {} ) }
|
||||
/>
|
||||
</ComboBox>
|
||||
<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 );
|
||||
}
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</BaseControl>
|
||||
</div>
|
||||
<SelectTreeMenu
|
||||
{ ...props }
|
||||
onSelect={ ( item ) => {
|
||||
if ( ! props.multiple && onInputChange ) {
|
||||
onInputChange( ( item as Item ).label );
|
||||
setIsOpen( false );
|
||||
setIsFocused( false );
|
||||
focusOnInput();
|
||||
}
|
||||
if ( props.onSelect ) {
|
||||
props.onSelect( item );
|
||||
}
|
||||
} }
|
||||
id={ `${ props.id }-menu` }
|
||||
className={ menuInstanceId.toString() }
|
||||
ref={ ref }
|
||||
isEventOutside={ isEventOutside }
|
||||
isOpen={ isOpen }
|
||||
items={ linkedTree }
|
||||
shouldShowCreateButton={ shouldShowCreateButton }
|
||||
onClose={ () => setIsOpen( false ) }
|
||||
onClose={ () => {
|
||||
setIsOpen( false );
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -162,6 +162,36 @@ export const SingleWithinModalUsingBodyDropdownPlacement: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export const SingleSelectTree: React.FC = () => {
|
||||
const [ value, setValue ] = React.useState( '' );
|
||||
const [ selected, setSelected ] = React.useState< Item | undefined >();
|
||||
|
||||
const items = filterItems( listItems, value );
|
||||
|
||||
return (
|
||||
<SelectTree
|
||||
id="single-select-tree"
|
||||
label="Single Select Tree"
|
||||
items={ items }
|
||||
selected={ selected }
|
||||
shouldNotRecursivelySelect
|
||||
shouldShowCreateButton={ ( typedValue ) =>
|
||||
! value ||
|
||||
listItems.findIndex( ( item ) => item.label === typedValue ) ===
|
||||
-1
|
||||
}
|
||||
createValue={ value }
|
||||
// eslint-disable-next-line no-alert
|
||||
onCreateNew={ () => alert( 'create new called' ) }
|
||||
onInputChange={ ( a ) => setValue( a || '' ) }
|
||||
onSelect={ ( selectedItems ) => {
|
||||
setSelected( selectedItems as Item );
|
||||
} }
|
||||
onRemove={ () => setSelected( undefined ) }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'WooCommerce Admin/experimental/SelectTreeControl',
|
||||
component: SelectTree,
|
||||
|
|
|
@ -122,8 +122,6 @@ export function useSelection( {
|
|||
if ( item.children.length && ! shouldNotRecursivelySelect ) {
|
||||
value.push( ...getDeepChildren( item ) );
|
||||
}
|
||||
} else if ( item.children?.length ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( checked ) {
|
||||
|
|
|
@ -45,10 +45,6 @@ $control-size: $gap-large;
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.components-radio-control__input {
|
||||
@include screen-reader-only();
|
||||
}
|
||||
|
||||
.components-checkbox-control__label {
|
||||
display: none;
|
||||
}
|
||||
|
@ -86,4 +82,8 @@ $control-size: $gap-large;
|
|||
min-width: $control-size;
|
||||
}
|
||||
}
|
||||
|
||||
&__checkbox {
|
||||
@include screen-reader-only();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,13 +60,11 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
|
|||
/>
|
||||
) : (
|
||||
<input
|
||||
type="radio"
|
||||
type="checkbox"
|
||||
className="experimental-woocommerce-tree-item__checkbox"
|
||||
checked={ selection.checkedStatus === 'checked' }
|
||||
className="components-radio-control__input"
|
||||
onChange={ ( event ) =>
|
||||
selection.onSelectChild(
|
||||
event.currentTarget.checked
|
||||
)
|
||||
selection.onSelectChild( event.target.checked )
|
||||
}
|
||||
/>
|
||||
) }
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -65,8 +65,8 @@ The `config` prop has the following structure:
|
|||
|
||||
- `label`: String - A label above the filter selector.
|
||||
- `staticParams`: Array - Url parameters to persist when selecting a new filter.
|
||||
- `param`: String - The url paramter this filter will modify.
|
||||
- `defaultValue`: String - The default paramter value to use instead of 'all'.
|
||||
- `param`: String - The url parameter this filter will modify.
|
||||
- `defaultValue`: String - The default parameter value to use instead of 'all'.
|
||||
- `showFilters`: Function - Determine if the filter should be shown. Supply a function with the query object as an argument returning a boolean.
|
||||
- `filters`: Array - Array of filter objects.
|
||||
|
||||
|
|
|
@ -374,11 +374,11 @@ FilterPicker.propTypes = {
|
|||
*/
|
||||
staticParams: PropTypes.array.isRequired,
|
||||
/**
|
||||
* The url paramter this filter will modify.
|
||||
* The url parameter this filter will modify.
|
||||
*/
|
||||
param: PropTypes.string.isRequired,
|
||||
/**
|
||||
* The default paramter value to use instead of 'all'.
|
||||
* The default parameter value to use instead of 'all'.
|
||||
*/
|
||||
defaultValue: PropTypes.string,
|
||||
/**
|
||||
|
|
|
@ -16,6 +16,7 @@ import { MediaUploadComponentType } from './types';
|
|||
|
||||
export type ImageGalleryToolbarProps = {
|
||||
childIndex: number;
|
||||
allowDragging?: boolean;
|
||||
moveItem: ( fromIndex: number, toIndex: number ) => void;
|
||||
removeItem: ( removeIndex: number ) => void;
|
||||
replaceItem: (
|
||||
|
@ -29,6 +30,7 @@ export type ImageGalleryToolbarProps = {
|
|||
|
||||
export const ImageGalleryToolbar: React.FC< ImageGalleryToolbarProps > = ( {
|
||||
childIndex,
|
||||
allowDragging = true,
|
||||
moveItem,
|
||||
removeItem,
|
||||
replaceItem,
|
||||
|
@ -60,12 +62,14 @@ export const ImageGalleryToolbar: React.FC< ImageGalleryToolbarProps > = ( {
|
|||
>
|
||||
{ ! isCoverItem && (
|
||||
<ToolbarGroup>
|
||||
<ToolbarButton
|
||||
icon={ () => (
|
||||
<SortableHandle itemIndex={ childIndex } />
|
||||
) }
|
||||
label={ __( 'Drag to reorder', 'woocommerce' ) }
|
||||
/>
|
||||
{ allowDragging && (
|
||||
<ToolbarButton
|
||||
icon={ () => (
|
||||
<SortableHandle itemIndex={ childIndex } />
|
||||
) }
|
||||
label={ __( 'Drag to reorder', 'woocommerce' ) }
|
||||
/>
|
||||
) }
|
||||
<ToolbarButton
|
||||
disabled={ childIndex < 2 }
|
||||
onClick={ () => movePrevious() }
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { DragEventHandler } from 'react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Sortable } from '../sortable';
|
||||
import { ImageGalleryChild } from './types';
|
||||
|
||||
export type ImageGalleryWrapperProps = {
|
||||
children: JSX.Element[];
|
||||
allowDragging?: boolean;
|
||||
onDragStart?: DragEventHandler< HTMLDivElement >;
|
||||
onDragEnd?: DragEventHandler< HTMLDivElement >;
|
||||
onDragOver?: DragEventHandler< HTMLLIElement >;
|
||||
updateOrderedChildren?: ( items: ImageGalleryChild[] ) => void;
|
||||
};
|
||||
|
||||
export const ImageGalleryWrapper: React.FC< ImageGalleryWrapperProps > = ( {
|
||||
children,
|
||||
allowDragging = true,
|
||||
onDragStart = () => null,
|
||||
onDragEnd = () => null,
|
||||
onDragOver = () => null,
|
||||
updateOrderedChildren = () => null,
|
||||
} ) => {
|
||||
if ( allowDragging ) {
|
||||
return (
|
||||
<Sortable
|
||||
isHorizontal
|
||||
onOrderChange={ ( items ) => {
|
||||
updateOrderedChildren( items );
|
||||
} }
|
||||
onDragStart={ ( event ) => {
|
||||
onDragStart( event );
|
||||
} }
|
||||
onDragEnd={ ( event ) => {
|
||||
onDragEnd( event );
|
||||
} }
|
||||
onDragOver={ onDragOver }
|
||||
>
|
||||
{ children }
|
||||
</Sortable>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="woocommerce-image-gallery__wrapper">{ children }</div>
|
||||
);
|
||||
};
|
|
@ -3,7 +3,7 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.woocommerce-sortable {
|
||||
.woocommerce-sortable, &__wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: inherit;
|
||||
grid-gap: $gap;
|
||||
|
|
|
@ -14,10 +14,11 @@ import { MediaItem, MediaUpload } from '@wordpress/media-utils';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Sortable, moveIndex } from '../sortable';
|
||||
import { moveIndex } from '../sortable';
|
||||
import { ImageGalleryToolbar } from './index';
|
||||
import { ImageGalleryChild, MediaUploadComponentType } from './types';
|
||||
import { removeItem, replaceItem } from './utils';
|
||||
import { ImageGalleryWrapper } from './image-gallery-wrapper';
|
||||
|
||||
export type ImageGalleryProps = {
|
||||
children: ImageGalleryChild | ImageGalleryChild[];
|
||||
|
@ -30,6 +31,7 @@ export type ImageGalleryProps = {
|
|||
replaceIndex: number;
|
||||
media: { id: number } & MediaItem;
|
||||
} ) => void;
|
||||
allowDragging?: boolean;
|
||||
onSelectAsCover?: ( itemId: string | null ) => void;
|
||||
onOrderChange?: ( items: ImageGalleryChild[] ) => void;
|
||||
MediaUploadComponent?: MediaUploadComponentType;
|
||||
|
@ -41,6 +43,7 @@ export type ImageGalleryProps = {
|
|||
export const ImageGallery: React.FC< ImageGalleryProps > = ( {
|
||||
children,
|
||||
columns = 4,
|
||||
allowDragging = true,
|
||||
onSelectAsCover = () => null,
|
||||
onOrderChange = () => null,
|
||||
onRemove = () => null,
|
||||
|
@ -82,11 +85,9 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( {
|
|||
gridTemplateColumns: 'min-content '.repeat( columns ),
|
||||
} }
|
||||
>
|
||||
<Sortable
|
||||
isHorizontal
|
||||
onOrderChange={ ( items ) => {
|
||||
updateOrderedChildren( items );
|
||||
} }
|
||||
<ImageGalleryWrapper
|
||||
allowDragging={ allowDragging }
|
||||
updateOrderedChildren={ updateOrderedChildren }
|
||||
onDragStart={ ( event ) => {
|
||||
setIsDragging( true );
|
||||
onDragStart( event );
|
||||
|
@ -137,6 +138,7 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( {
|
|||
},
|
||||
isToolbarVisible ? (
|
||||
<ImageGalleryToolbar
|
||||
allowDragging={ allowDragging }
|
||||
childIndex={ childIndex }
|
||||
lastChild={
|
||||
childIndex === orderedChildren.length - 1
|
||||
|
@ -190,7 +192,7 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( {
|
|||
) : null
|
||||
);
|
||||
} ) }
|
||||
</Sortable>
|
||||
</ImageGalleryWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -45,7 +45,7 @@ export const EditorWritingFlow = ( {
|
|||
};
|
||||
} );
|
||||
|
||||
// This is a workaround to prevent focusing the block on intialization.
|
||||
// This is a workaround to prevent focusing the block on initialization.
|
||||
// Changing to a mode other than "edit" ensures that no initial position
|
||||
// is found and no element gets subsequently focused.
|
||||
// See https://github.com/WordPress/gutenberg/blob/411b6eee8376e31bf9db4c15c92a80524ae38e9b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js#L42
|
||||
|
|
|
@ -153,7 +153,7 @@ describe( 'buildTermsTree', () => {
|
|||
] );
|
||||
} );
|
||||
|
||||
test( 'should return a tree of items, with orphan categories appended to the end, with children of thier own', () => {
|
||||
test( 'should return a tree of items, with orphan categories appended to the end, with children of their own', () => {
|
||||
const filteredList = [
|
||||
{ id: 1, name: 'Apricots', parent: 0 },
|
||||
{ id: 3, name: 'Elderberry', parent: 2 },
|
||||
|
|
|
@ -6,18 +6,149 @@ import { BACKSPACE, DOWN, UP } from '@wordpress/keycodes';
|
|||
import { createElement, Component, createRef } from '@wordpress/element';
|
||||
import { Icon, search } from '@wordpress/icons';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isArray } from 'lodash';
|
||||
import {
|
||||
RefObject,
|
||||
ChangeEvent,
|
||||
FocusEvent,
|
||||
KeyboardEvent,
|
||||
InputHTMLAttributes,
|
||||
} from 'react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Tags from './tags';
|
||||
import { Selected, Option } from './types';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Bool to determine if tags should be rendered.
|
||||
*/
|
||||
hasTags?: boolean;
|
||||
/**
|
||||
* Help text to be appended beneath the input.
|
||||
*/
|
||||
help?: string | JSX.Element;
|
||||
/**
|
||||
* Render tags inside input, otherwise render below input.
|
||||
*/
|
||||
inlineTags?: boolean;
|
||||
/**
|
||||
* Allow the select options to be filtered by search input.
|
||||
*/
|
||||
isSearchable?: boolean;
|
||||
/**
|
||||
* ID of the main SelectControl instance.
|
||||
*/
|
||||
instanceId?: number;
|
||||
/**
|
||||
* A label to use for the main input.
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* ID used for a11y in the listbox.
|
||||
*/
|
||||
listboxId?: string;
|
||||
/**
|
||||
* Function called when the input is blurred.
|
||||
*/
|
||||
onBlur?: () => void;
|
||||
/**
|
||||
* Function called when selected results change, passed result list.
|
||||
*/
|
||||
onChange: ( selected: Option[] ) => void;
|
||||
/**
|
||||
* Function called when input field is changed or focused.
|
||||
*/
|
||||
onSearch: ( query: string ) => void;
|
||||
/**
|
||||
* A placeholder for the search input.
|
||||
*/
|
||||
placeholder?: string;
|
||||
/**
|
||||
* Search query entered by user.
|
||||
*/
|
||||
query?: string | null;
|
||||
/**
|
||||
* An array of objects describing selected values. If the label of the selected
|
||||
* value is omitted, the Tag of that value will not be rendered inside the
|
||||
* search box.
|
||||
*/
|
||||
selected?: Selected;
|
||||
/**
|
||||
* Show all options on focusing, even if a query exists.
|
||||
*/
|
||||
showAllOnFocus?: boolean;
|
||||
/**
|
||||
* Control input autocomplete field, defaults: off.
|
||||
*/
|
||||
autoComplete?: string;
|
||||
/**
|
||||
* Function to execute when the control should be expanded or collapsed.
|
||||
*/
|
||||
setExpanded: ( expanded: boolean ) => void;
|
||||
/**
|
||||
* Function to execute when the search value changes.
|
||||
*/
|
||||
updateSearchOptions: ( query: string ) => void;
|
||||
/**
|
||||
* Function to execute when keyboard navigation should decrement the selected index.
|
||||
*/
|
||||
decrementSelectedIndex: () => void;
|
||||
/**
|
||||
* Function to execute when keyboard navigation should increment the selected index.
|
||||
*/
|
||||
incrementSelectedIndex: () => void;
|
||||
/**
|
||||
* Multi-select mode allows multiple options to be selected.
|
||||
*/
|
||||
multiple?: boolean;
|
||||
/**
|
||||
* Is the control currently focused.
|
||||
*/
|
||||
isFocused?: boolean;
|
||||
/**
|
||||
* ID for accessibility purposes. aria-activedescendant will be set to this value.
|
||||
*/
|
||||
activeId?: string;
|
||||
/**
|
||||
* Disable the control.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Is the control currently expanded. This is for accessibility purposes.
|
||||
*/
|
||||
isExpanded?: boolean;
|
||||
/**
|
||||
* The type of input to use for the search field.
|
||||
*/
|
||||
searchInputType?: InputHTMLAttributes< HTMLInputElement >[ 'type' ];
|
||||
/**
|
||||
* The aria label for the search input.
|
||||
*/
|
||||
ariaLabel?: string;
|
||||
/**
|
||||
* Class name to be added to the input.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Show the clear button.
|
||||
*/
|
||||
showClearButton?: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A search control to allow user input to filter the options.
|
||||
*/
|
||||
class Control extends Component {
|
||||
constructor( props ) {
|
||||
class Control extends Component< Props, State > {
|
||||
input: RefObject< HTMLInputElement >;
|
||||
|
||||
constructor( props: Props ) {
|
||||
super( props );
|
||||
this.state = {
|
||||
isActive: false,
|
||||
|
@ -31,13 +162,13 @@ class Control extends Component {
|
|||
this.onKeyDown = this.onKeyDown.bind( this );
|
||||
}
|
||||
|
||||
updateSearch( onSearch ) {
|
||||
return ( event ) => {
|
||||
updateSearch( onSearch: ( query: string ) => void ) {
|
||||
return ( event: ChangeEvent< HTMLInputElement > ) => {
|
||||
onSearch( event.target.value );
|
||||
};
|
||||
}
|
||||
|
||||
onFocus( onSearch ) {
|
||||
onFocus( onSearch: ( query: string ) => void ) {
|
||||
const {
|
||||
isSearchable,
|
||||
setExpanded,
|
||||
|
@ -45,7 +176,7 @@ class Control extends Component {
|
|||
updateSearchOptions,
|
||||
} = this.props;
|
||||
|
||||
return ( event ) => {
|
||||
return ( event: FocusEvent< HTMLInputElement > ) => {
|
||||
this.setState( { isActive: true } );
|
||||
if ( isSearchable && showAllOnFocus ) {
|
||||
event.target.select();
|
||||
|
@ -68,7 +199,7 @@ class Control extends Component {
|
|||
this.setState( { isActive: false } );
|
||||
}
|
||||
|
||||
onKeyDown( event ) {
|
||||
onKeyDown( event: KeyboardEvent< HTMLInputElement > ) {
|
||||
const {
|
||||
decrementSelectedIndex,
|
||||
incrementSelectedIndex,
|
||||
|
@ -78,7 +209,12 @@ class Control extends Component {
|
|||
setExpanded,
|
||||
} = this.props;
|
||||
|
||||
if ( BACKSPACE === event.keyCode && ! query && selected.length ) {
|
||||
if (
|
||||
BACKSPACE === event.keyCode &&
|
||||
! query &&
|
||||
isArray( selected ) &&
|
||||
selected.length
|
||||
) {
|
||||
onChange( [ ...selected.slice( 0, -1 ) ] );
|
||||
}
|
||||
|
||||
|
@ -100,7 +236,7 @@ class Control extends Component {
|
|||
renderButton() {
|
||||
const { multiple, selected } = this.props;
|
||||
|
||||
if ( multiple || ! selected.length ) {
|
||||
if ( multiple || ! isArray( selected ) || ! selected.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -151,7 +287,7 @@ class Control extends Component {
|
|||
aria-describedby={
|
||||
hasTags && inlineTags
|
||||
? `search-inline-input-${ instanceId }`
|
||||
: null
|
||||
: undefined
|
||||
}
|
||||
disabled={ disabled }
|
||||
aria-label={ this.props.ariaLabel ?? this.props.label }
|
||||
|
@ -168,7 +304,8 @@ class Control extends Component {
|
|||
query,
|
||||
selected,
|
||||
} = this.props;
|
||||
const selectedValue = selected.length ? selected[ 0 ].label : '';
|
||||
const selectedValue =
|
||||
isArray( selected ) && selected.length ? selected[ 0 ].label : '';
|
||||
|
||||
// Show the selected value for simple select dropdowns.
|
||||
if ( ! multiple && ! isFocused && ! inlineTags ) {
|
||||
|
@ -194,6 +331,8 @@ class Control extends Component {
|
|||
isSearchable,
|
||||
label,
|
||||
query,
|
||||
onChange,
|
||||
showClearButton,
|
||||
} = this.props;
|
||||
const { isActive } = this.state;
|
||||
|
||||
|
@ -213,7 +352,7 @@ class Control extends Component {
|
|||
empty: ! query || query.length === 0,
|
||||
'is-active': isActive,
|
||||
'has-tags': inlineTags && hasTags,
|
||||
'with-value': this.getInputValue().length,
|
||||
'with-value': this.getInputValue()?.length,
|
||||
'has-error': !! help,
|
||||
'is-disabled': disabled,
|
||||
}
|
||||
|
@ -221,8 +360,11 @@ class Control extends Component {
|
|||
onClick={ ( event ) => {
|
||||
// Don't focus the input if the click event is from the error message.
|
||||
if (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - event.target.className is not in the type definition.
|
||||
event.target.className !==
|
||||
'components-base-control__help'
|
||||
'components-base-control__help' &&
|
||||
this.input.current
|
||||
) {
|
||||
this.input.current.focus();
|
||||
}
|
||||
|
@ -234,7 +376,13 @@ class Control extends Component {
|
|||
icon={ search }
|
||||
/>
|
||||
) }
|
||||
{ inlineTags && <Tags { ...this.props } /> }
|
||||
{ inlineTags && (
|
||||
<Tags
|
||||
onChange={ onChange }
|
||||
showClearButton={ showClearButton }
|
||||
selected={ this.props.selected }
|
||||
/>
|
||||
) }
|
||||
|
||||
<div className="components-base-control__field">
|
||||
{ !! label && (
|
||||
|
@ -272,75 +420,4 @@ class Control extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
Control.propTypes = {
|
||||
/**
|
||||
* Bool to determine if tags should be rendered.
|
||||
*/
|
||||
hasTags: PropTypes.bool,
|
||||
/**
|
||||
* Help text to be appended beneath the input.
|
||||
*/
|
||||
help: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
|
||||
/**
|
||||
* Render tags inside input, otherwise render below input.
|
||||
*/
|
||||
inlineTags: PropTypes.bool,
|
||||
/**
|
||||
* Allow the select options to be filtered by search input.
|
||||
*/
|
||||
isSearchable: PropTypes.bool,
|
||||
/**
|
||||
* ID of the main SelectControl instance.
|
||||
*/
|
||||
instanceId: PropTypes.number,
|
||||
/**
|
||||
* A label to use for the main input.
|
||||
*/
|
||||
label: PropTypes.string,
|
||||
/**
|
||||
* ID used for a11y in the listbox.
|
||||
*/
|
||||
listboxId: PropTypes.string,
|
||||
/**
|
||||
* Function called when the input is blurred.
|
||||
*/
|
||||
onBlur: PropTypes.func,
|
||||
/**
|
||||
* Function called when selected results change, passed result list.
|
||||
*/
|
||||
onChange: PropTypes.func,
|
||||
/**
|
||||
* Function called when input field is changed or focused.
|
||||
*/
|
||||
onSearch: PropTypes.func,
|
||||
/**
|
||||
* A placeholder for the search input.
|
||||
*/
|
||||
placeholder: PropTypes.string,
|
||||
/**
|
||||
* Search query entered by user.
|
||||
*/
|
||||
query: PropTypes.string,
|
||||
/**
|
||||
* An array of objects describing selected values. If the label of the selected
|
||||
* value is omitted, the Tag of that value will not be rendered inside the
|
||||
* search box.
|
||||
*/
|
||||
selected: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] )
|
||||
.isRequired,
|
||||
label: PropTypes.string,
|
||||
} )
|
||||
),
|
||||
/**
|
||||
* Show all options on focusing, even if a query exists.
|
||||
*/
|
||||
showAllOnFocus: PropTypes.bool,
|
||||
/**
|
||||
* Control input autocomplete field, defaults: off.
|
||||
*/
|
||||
autoComplete: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Control;
|
|
@ -4,26 +4,205 @@
|
|||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import { Component, createElement } from '@wordpress/element';
|
||||
import { debounce, escapeRegExp, identity, noop } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
debounce,
|
||||
escapeRegExp,
|
||||
identity,
|
||||
isArray,
|
||||
isNumber,
|
||||
noop,
|
||||
} from 'lodash';
|
||||
import { withFocusOutside, withSpokenMessages } from '@wordpress/components';
|
||||
import { withInstanceId, compose } from '@wordpress/compose';
|
||||
import { ChangeEvent, InputHTMLAttributes } from 'react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Option, Selected } from './types';
|
||||
import List from './list';
|
||||
import Tags from './tags';
|
||||
import Control from './control';
|
||||
|
||||
const initialState = { isExpanded: false, isFocused: false, query: '' };
|
||||
type Props = {
|
||||
/**
|
||||
* Name to use for the autofill field, not used if no string is passed.
|
||||
*/
|
||||
autofill?: string;
|
||||
/**
|
||||
* A renderable component (or string) which will be displayed before the `Control` of this component.
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
/**
|
||||
* Class name applied to parent div.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Class name applied to control wrapper.
|
||||
*/
|
||||
controlClassName?: string;
|
||||
/**
|
||||
* Allow the select options to be disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Exclude already selected options from the options list.
|
||||
*/
|
||||
excludeSelectedOptions?: boolean;
|
||||
/**
|
||||
* Add or remove items to the list of options after filtering,
|
||||
* passed the array of filtered options and should return an array of options.
|
||||
*/
|
||||
onFilter?: (
|
||||
options: Array< Option >,
|
||||
query: string | null
|
||||
) => Array< Option >;
|
||||
/**
|
||||
* Function to add regex expression to the filter the results, passed the search query.
|
||||
*/
|
||||
getSearchExpression?: ( query: string ) => RegExp;
|
||||
/**
|
||||
* Help text to be appended beneath the input.
|
||||
*/
|
||||
help?: string | JSX.Element;
|
||||
/**
|
||||
* Render tags inside input, otherwise render below input.
|
||||
*/
|
||||
inlineTags?: boolean;
|
||||
/**
|
||||
* Allow the select options to be filtered by search input.
|
||||
*/
|
||||
isSearchable?: boolean;
|
||||
/**
|
||||
* A label to use for the main input.
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* Function called when selected results change, passed result list.
|
||||
*/
|
||||
onChange?: ( selected: string | Option[], query?: string | null ) => void;
|
||||
/**
|
||||
* Function run after search query is updated, passed previousOptions and query,
|
||||
* should return a promise with an array of updated options.
|
||||
*/
|
||||
onSearch?: (
|
||||
previousOptions: Array< Option >,
|
||||
query: string | null
|
||||
) => Promise< Array< Option > >;
|
||||
/**
|
||||
* An array of objects for the options list. The option along with its key, label and
|
||||
* value will be returned in the onChange event.
|
||||
*/
|
||||
options: Option[];
|
||||
/**
|
||||
* A placeholder for the search input.
|
||||
*/
|
||||
placeholder?: string;
|
||||
/**
|
||||
* Time in milliseconds to debounce the search function after typing.
|
||||
*/
|
||||
searchDebounceTime?: number;
|
||||
/**
|
||||
* An array of objects describing selected values or optionally a string for a single value.
|
||||
* If the label of the selected value is omitted, the Tag of that value will not
|
||||
* be rendered inside the search box.
|
||||
*/
|
||||
selected?: Selected;
|
||||
/**
|
||||
* A limit for the number of results shown in the options menu. Set to 0 for no limit.
|
||||
*/
|
||||
maxResults?: number;
|
||||
/**
|
||||
* Allow multiple option selections.
|
||||
*/
|
||||
multiple?: boolean;
|
||||
/**
|
||||
* Render a 'Clear' button next to the input box to remove its contents.
|
||||
*/
|
||||
showClearButton?: boolean;
|
||||
/**
|
||||
* The input type for the search box control.
|
||||
*/
|
||||
searchInputType?: InputHTMLAttributes< HTMLInputElement >[ 'type' ];
|
||||
/**
|
||||
* Only show list options after typing a search query.
|
||||
*/
|
||||
hideBeforeSearch?: boolean;
|
||||
/**
|
||||
* Show all options on focusing, even if a query exists.
|
||||
*/
|
||||
showAllOnFocus?: boolean;
|
||||
/**
|
||||
* Render results list positioned statically instead of absolutely.
|
||||
*/
|
||||
staticList?: boolean;
|
||||
/**
|
||||
* autocomplete prop for the Control input field.
|
||||
*/
|
||||
autoComplete?: string;
|
||||
/**
|
||||
* Instance ID for the component.
|
||||
*/
|
||||
instanceId?: number;
|
||||
/**
|
||||
* From withSpokenMessages
|
||||
*/
|
||||
debouncedSpeak?: ( message: string, assertive?: string ) => void;
|
||||
/**
|
||||
* aria-label for the search input.
|
||||
*/
|
||||
ariaLabel?: string;
|
||||
/**
|
||||
* On Blur callback.
|
||||
*/
|
||||
onBlur?: () => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
isExpanded: boolean;
|
||||
isFocused: boolean;
|
||||
query: string | null;
|
||||
searchOptions: Option[];
|
||||
selectedIndex?: number | null;
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
isExpanded: false,
|
||||
isFocused: false,
|
||||
query: '',
|
||||
searchOptions: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* A search box which filters options while typing,
|
||||
* allowing a user to select from an option from a filtered list.
|
||||
*/
|
||||
export class SelectControl extends Component {
|
||||
constructor( props ) {
|
||||
export class SelectControl extends Component< Props, State > {
|
||||
static defaultProps: Partial< Props > = {
|
||||
excludeSelectedOptions: true,
|
||||
getSearchExpression: identity,
|
||||
inlineTags: false,
|
||||
isSearchable: false,
|
||||
onChange: noop,
|
||||
onFilter: identity,
|
||||
onSearch: ( options: Option[] ) => Promise.resolve( options ),
|
||||
maxResults: 0,
|
||||
multiple: false,
|
||||
searchDebounceTime: 0,
|
||||
searchInputType: 'search',
|
||||
selected: [],
|
||||
showAllOnFocus: false,
|
||||
showClearButton: false,
|
||||
hideBeforeSearch: false,
|
||||
staticList: false,
|
||||
autoComplete: 'off',
|
||||
};
|
||||
|
||||
node: HTMLDivElement | null = null;
|
||||
activePromise: Promise< void | Option[] > | null = null;
|
||||
cacheSearchOptions: Option[] = [];
|
||||
|
||||
constructor( props: Props ) {
|
||||
super( props );
|
||||
|
||||
const { selected, options, excludeSelectedOptions } = props;
|
||||
|
@ -50,7 +229,7 @@ export class SelectControl extends Component {
|
|||
this.setNewValue = this.setNewValue.bind( this );
|
||||
}
|
||||
|
||||
bindNode( node ) {
|
||||
bindNode( node: HTMLDivElement ) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
|
@ -58,7 +237,12 @@ export class SelectControl extends Component {
|
|||
const { multiple, excludeSelectedOptions } = this.props;
|
||||
const newState = { ...initialState };
|
||||
// Reset selectedIndex if single selection.
|
||||
if ( ! multiple && selected.length && selected[ 0 ].key ) {
|
||||
if (
|
||||
! multiple &&
|
||||
isArray( selected ) &&
|
||||
selected.length &&
|
||||
selected[ 0 ].key
|
||||
) {
|
||||
newState.selectedIndex = ! excludeSelectedOptions
|
||||
? this.props.options.findIndex(
|
||||
( i ) => i.key === selected[ 0 ].key
|
||||
|
@ -101,9 +285,12 @@ export class SelectControl extends Component {
|
|||
return selectedOption ? [ selectedOption ] : [];
|
||||
}
|
||||
|
||||
selectOption( option ) {
|
||||
selectOption( option: Option ) {
|
||||
const { multiple, selected } = this.props;
|
||||
const newSelected = multiple ? [ ...selected, option ] : [ option ];
|
||||
const newSelected =
|
||||
multiple && isArray( selected )
|
||||
? [ ...selected, option ]
|
||||
: [ option ];
|
||||
|
||||
this.reset( newSelected );
|
||||
|
||||
|
@ -129,25 +316,24 @@ export class SelectControl extends Component {
|
|||
} );
|
||||
}
|
||||
|
||||
setNewValue( newValue ) {
|
||||
setNewValue( newValue: Option[] ) {
|
||||
const { onChange, selected, multiple } = this.props;
|
||||
const { query } = this.state;
|
||||
// Trigger a change if the selected value is different and pass back
|
||||
// an array or string depending on the original value.
|
||||
if ( multiple || Array.isArray( selected ) ) {
|
||||
onChange( newValue, query );
|
||||
onChange!( newValue, query );
|
||||
} else {
|
||||
onChange( newValue.length > 0 ? newValue[ 0 ].key : '', query );
|
||||
onChange!( newValue.length > 0 ? newValue[ 0 ].key : '', query );
|
||||
}
|
||||
}
|
||||
|
||||
decrementSelectedIndex() {
|
||||
const { selectedIndex } = this.state;
|
||||
const options = this.getOptions();
|
||||
const nextSelectedIndex =
|
||||
selectedIndex !== null
|
||||
? ( selectedIndex === 0 ? options.length : selectedIndex ) - 1
|
||||
: options.length - 1;
|
||||
const nextSelectedIndex = isNumber( selectedIndex )
|
||||
? ( selectedIndex === 0 ? options.length : selectedIndex ) - 1
|
||||
: options.length - 1;
|
||||
|
||||
this.setState( { selectedIndex: nextSelectedIndex } );
|
||||
}
|
||||
|
@ -155,13 +341,14 @@ export class SelectControl extends Component {
|
|||
incrementSelectedIndex() {
|
||||
const { selectedIndex } = this.state;
|
||||
const options = this.getOptions();
|
||||
const nextSelectedIndex =
|
||||
selectedIndex !== null ? ( selectedIndex + 1 ) % options.length : 0;
|
||||
const nextSelectedIndex = isNumber( selectedIndex )
|
||||
? ( selectedIndex + 1 ) % options.length
|
||||
: 0;
|
||||
|
||||
this.setState( { selectedIndex: nextSelectedIndex } );
|
||||
}
|
||||
|
||||
announce( searchOptions ) {
|
||||
announce( searchOptions: Option[] ) {
|
||||
const { debouncedSpeak } = this.props;
|
||||
if ( ! debouncedSpeak ) {
|
||||
return;
|
||||
|
@ -169,6 +356,7 @@ export class SelectControl extends Component {
|
|||
if ( !! searchOptions.length ) {
|
||||
debouncedSpeak(
|
||||
sprintf(
|
||||
// translators: %d: number of results.
|
||||
_n(
|
||||
'%d result found, use up and down arrow keys to navigate.',
|
||||
'%d results found, use up and down arrow keys to navigate.',
|
||||
|
@ -187,23 +375,26 @@ export class SelectControl extends Component {
|
|||
getOptions() {
|
||||
const { isSearchable, options, excludeSelectedOptions } = this.props;
|
||||
const { searchOptions } = this.state;
|
||||
const selectedKeys = this.getSelected().map( ( option ) => option.key );
|
||||
const selected = this.getSelected();
|
||||
const selectedKeys = isArray( selected )
|
||||
? selected.map( ( option ) => option.key )
|
||||
: [];
|
||||
const shownOptions = isSearchable ? searchOptions : options;
|
||||
|
||||
if ( excludeSelectedOptions ) {
|
||||
return shownOptions.filter(
|
||||
return shownOptions?.filter(
|
||||
( option ) => ! selectedKeys.includes( option.key )
|
||||
);
|
||||
}
|
||||
return shownOptions;
|
||||
}
|
||||
|
||||
getOptionsByQuery( options, query ) {
|
||||
getOptionsByQuery( options: Option[], query: string | null ) {
|
||||
const { getSearchExpression, maxResults, onFilter } = this.props;
|
||||
const filtered = [];
|
||||
|
||||
// Create a regular expression to filter the options.
|
||||
const expression = getSearchExpression(
|
||||
const expression = getSearchExpression!(
|
||||
escapeRegExp( query ? query.trim() : '' )
|
||||
);
|
||||
const search = expression ? new RegExp( expression, 'i' ) : /^$/;
|
||||
|
@ -232,14 +423,14 @@ export class SelectControl extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
return onFilter( filtered, query );
|
||||
return onFilter!( filtered, query );
|
||||
}
|
||||
|
||||
setExpanded( value ) {
|
||||
setExpanded( value: boolean ) {
|
||||
this.setState( { isExpanded: value } );
|
||||
}
|
||||
|
||||
search( query ) {
|
||||
search( query: string | null ) {
|
||||
const cacheSearchOptions = this.cacheSearchOptions || [];
|
||||
const searchOptions =
|
||||
query !== null && ! query.length && ! this.props.hideBeforeSearch
|
||||
|
@ -252,11 +443,13 @@ export class SelectControl extends Component {
|
|||
isFocused: true,
|
||||
searchOptions,
|
||||
selectedIndex:
|
||||
query?.length > 0 ? null : this.state.selectedIndex, // Only reset selectedIndex if we're actually searching.
|
||||
query && query?.length > 0
|
||||
? null
|
||||
: this.state.selectedIndex, // Only reset selectedIndex if we're actually searching.
|
||||
},
|
||||
() => {
|
||||
this.setState( {
|
||||
isExpanded: Boolean( this.getOptions().length ),
|
||||
isExpanded: Boolean( this.getOptions()?.length ),
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
@ -264,11 +457,11 @@ export class SelectControl extends Component {
|
|||
this.updateSearchOptions( query );
|
||||
}
|
||||
|
||||
updateSearchOptions( query ) {
|
||||
updateSearchOptions( query: string | null ) {
|
||||
const { hideBeforeSearch, options, onSearch } = this.props;
|
||||
|
||||
const promise = ( this.activePromise = Promise.resolve(
|
||||
onSearch( options, query )
|
||||
onSearch!( options, query )
|
||||
).then( ( promiseOptions ) => {
|
||||
if ( promise !== this.activePromise ) {
|
||||
// Another promise has become active since this one was asked to resolve, so do nothing,
|
||||
|
@ -288,7 +481,9 @@ export class SelectControl extends Component {
|
|||
{
|
||||
searchOptions,
|
||||
selectedIndex:
|
||||
query?.length > 0 ? null : this.state.selectedIndex, // Only reset selectedIndex if we're actually searching.
|
||||
query && query?.length > 0
|
||||
? null
|
||||
: this.state.selectedIndex, // Only reset selectedIndex if we're actually searching.
|
||||
},
|
||||
() => {
|
||||
this.setState( {
|
||||
|
@ -300,7 +495,7 @@ export class SelectControl extends Component {
|
|||
} ) );
|
||||
}
|
||||
|
||||
onAutofillChange( event ) {
|
||||
onAutofillChange( event: ChangeEvent< HTMLInputElement > ) {
|
||||
const { options } = this.props;
|
||||
const searchOptions = this.getOptionsByQuery(
|
||||
options,
|
||||
|
@ -327,13 +522,14 @@ export class SelectControl extends Component {
|
|||
const { isExpanded, isFocused, selectedIndex } = this.state;
|
||||
|
||||
const hasMultiple = this.hasMultiple();
|
||||
const { key: selectedKey = '' } = options[ selectedIndex ] || {};
|
||||
const { key: selectedKey = '' } =
|
||||
( isNumber( selectedIndex ) && options[ selectedIndex ] ) || {};
|
||||
const listboxId = isExpanded
|
||||
? `woocommerce-select-control__listbox-${ instanceId }`
|
||||
: null;
|
||||
: undefined;
|
||||
const activeId = isExpanded
|
||||
? `woocommerce-select-control__option-${ instanceId }-${ selectedKey }`
|
||||
: null;
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -354,13 +550,25 @@ export class SelectControl extends Component {
|
|||
name={ autofill }
|
||||
type="text"
|
||||
className="woocommerce-select-control__autofill-input"
|
||||
tabIndex="-1"
|
||||
tabIndex={ -1 }
|
||||
/>
|
||||
) }
|
||||
{ children }
|
||||
<Control
|
||||
{ ...this.props }
|
||||
{ ...this.state }
|
||||
help={ this.props.help }
|
||||
label={ this.props.label }
|
||||
inlineTags={ inlineTags }
|
||||
isSearchable={ isSearchable }
|
||||
isFocused={ isFocused }
|
||||
instanceId={ instanceId }
|
||||
searchInputType={ this.props.searchInputType }
|
||||
query={ this.state.query }
|
||||
placeholder={ this.props.placeholder }
|
||||
autoComplete={ this.props.autoComplete }
|
||||
multiple={ this.props.multiple }
|
||||
ariaLabel={ this.props.ariaLabel }
|
||||
onBlur={ this.props.onBlur }
|
||||
showAllOnFocus={ this.props.showAllOnFocus }
|
||||
activeId={ activeId }
|
||||
className={ controlClassName }
|
||||
disabled={ disabled }
|
||||
|
@ -374,15 +582,20 @@ export class SelectControl extends Component {
|
|||
updateSearchOptions={ this.updateSearchOptions }
|
||||
decrementSelectedIndex={ this.decrementSelectedIndex }
|
||||
incrementSelectedIndex={ this.incrementSelectedIndex }
|
||||
showClearButton={ this.props.showClearButton }
|
||||
/>
|
||||
{ ! inlineTags && hasMultiple && (
|
||||
<Tags { ...this.props } selected={ this.getSelected() } />
|
||||
<Tags
|
||||
onChange={ this.props.onChange! }
|
||||
showClearButton={ this.props.showClearButton }
|
||||
selected={ this.getSelected() }
|
||||
/>
|
||||
) }
|
||||
{ isExpanded && (
|
||||
<List
|
||||
{ ...this.props }
|
||||
{ ...this.state }
|
||||
activeId={ activeId }
|
||||
instanceId={ instanceId! }
|
||||
selectedIndex={ selectedIndex }
|
||||
staticList={ this.props.staticList! }
|
||||
listboxId={ listboxId }
|
||||
node={ this.node }
|
||||
onSelect={ this.selectOption }
|
||||
|
@ -398,171 +611,6 @@ export class SelectControl extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
SelectControl.propTypes = {
|
||||
/**
|
||||
* Name to use for the autofill field, not used if no string is passed.
|
||||
*/
|
||||
autofill: PropTypes.string,
|
||||
/**
|
||||
* A renderable component (or string) which will be displayed before the `Control` of this component.
|
||||
*/
|
||||
children: PropTypes.node,
|
||||
/**
|
||||
* Class name applied to parent div.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* Class name applied to control wrapper.
|
||||
*/
|
||||
controlClassName: PropTypes.string,
|
||||
/**
|
||||
* Allow the select options to be disabled.
|
||||
*/
|
||||
disabled: PropTypes.bool,
|
||||
/**
|
||||
* Exclude already selected options from the options list.
|
||||
*/
|
||||
excludeSelectedOptions: PropTypes.bool,
|
||||
/**
|
||||
* Add or remove items to the list of options after filtering,
|
||||
* passed the array of filtered options and should return an array of options.
|
||||
*/
|
||||
onFilter: PropTypes.func,
|
||||
/**
|
||||
* Function to add regex expression to the filter the results, passed the search query.
|
||||
*/
|
||||
getSearchExpression: PropTypes.func,
|
||||
/**
|
||||
* Help text to be appended beneath the input.
|
||||
*/
|
||||
help: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
|
||||
/**
|
||||
* Render tags inside input, otherwise render below input.
|
||||
*/
|
||||
inlineTags: PropTypes.bool,
|
||||
/**
|
||||
* Allow the select options to be filtered by search input.
|
||||
*/
|
||||
isSearchable: PropTypes.bool,
|
||||
/**
|
||||
* A label to use for the main input.
|
||||
*/
|
||||
label: PropTypes.string,
|
||||
/**
|
||||
* Function called when selected results change, passed result list.
|
||||
*/
|
||||
onChange: PropTypes.func,
|
||||
/**
|
||||
* Function run after search query is updated, passed previousOptions and query,
|
||||
* should return a promise with an array of updated options.
|
||||
*/
|
||||
onSearch: PropTypes.func,
|
||||
/**
|
||||
* An array of objects for the options list. The option along with its key, label and
|
||||
* value will be returned in the onChange event.
|
||||
*/
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
isDisabled: PropTypes.bool,
|
||||
key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] )
|
||||
.isRequired,
|
||||
keywords: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType( [ PropTypes.string, PropTypes.number ] )
|
||||
),
|
||||
label: PropTypes.oneOfType( [
|
||||
PropTypes.string,
|
||||
PropTypes.object,
|
||||
] ),
|
||||
value: PropTypes.any,
|
||||
} )
|
||||
).isRequired,
|
||||
/**
|
||||
* A placeholder for the search input.
|
||||
*/
|
||||
placeholder: PropTypes.string,
|
||||
/**
|
||||
* Time in milliseconds to debounce the search function after typing.
|
||||
*/
|
||||
searchDebounceTime: PropTypes.number,
|
||||
/**
|
||||
* An array of objects describing selected values or optionally a string for a single value.
|
||||
* If the label of the selected value is omitted, the Tag of that value will not
|
||||
* be rendered inside the search box.
|
||||
*/
|
||||
selected: PropTypes.oneOfType( [
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
key: PropTypes.oneOfType( [
|
||||
PropTypes.number,
|
||||
PropTypes.string,
|
||||
] ).isRequired,
|
||||
label: PropTypes.string,
|
||||
} )
|
||||
),
|
||||
] ),
|
||||
/**
|
||||
* A limit for the number of results shown in the options menu. Set to 0 for no limit.
|
||||
*/
|
||||
maxResults: PropTypes.number,
|
||||
/**
|
||||
* Allow multiple option selections.
|
||||
*/
|
||||
multiple: PropTypes.bool,
|
||||
/**
|
||||
* Render a 'Clear' button next to the input box to remove its contents.
|
||||
*/
|
||||
showClearButton: PropTypes.bool,
|
||||
/**
|
||||
* The input type for the search box control.
|
||||
*/
|
||||
searchInputType: PropTypes.oneOf( [
|
||||
'text',
|
||||
'search',
|
||||
'number',
|
||||
'email',
|
||||
'tel',
|
||||
'url',
|
||||
] ),
|
||||
/**
|
||||
* Only show list options after typing a search query.
|
||||
*/
|
||||
hideBeforeSearch: PropTypes.bool,
|
||||
/**
|
||||
* Show all options on focusing, even if a query exists.
|
||||
*/
|
||||
showAllOnFocus: PropTypes.bool,
|
||||
/**
|
||||
* Render results list positioned statically instead of absolutely.
|
||||
*/
|
||||
staticList: PropTypes.bool,
|
||||
/**
|
||||
* autocomplete prop for the Control input field.
|
||||
*/
|
||||
autoComplete: PropTypes.string,
|
||||
};
|
||||
|
||||
SelectControl.defaultProps = {
|
||||
autofill: null,
|
||||
excludeSelectedOptions: true,
|
||||
getSearchExpression: identity,
|
||||
inlineTags: false,
|
||||
isSearchable: false,
|
||||
onChange: noop,
|
||||
onFilter: identity,
|
||||
onSearch: ( options ) => Promise.resolve( options ),
|
||||
maxResults: 0,
|
||||
multiple: false,
|
||||
searchDebounceTime: 0,
|
||||
searchInputType: 'search',
|
||||
selected: [],
|
||||
showAllOnFocus: false,
|
||||
showClearButton: false,
|
||||
hideBeforeSearch: false,
|
||||
staticList: false,
|
||||
autoComplete: 'off',
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withSpokenMessages,
|
||||
withInstanceId,
|
|
@ -2,18 +2,73 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { Button } from '@wordpress/components';
|
||||
import { RefObject } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { createElement, Component, createRef } from '@wordpress/element';
|
||||
import { isEqual } from 'lodash';
|
||||
import { isEqual, isNumber } from 'lodash';
|
||||
import { ENTER, ESCAPE, UP, DOWN, LEFT, RIGHT, TAB } from '@wordpress/keycodes';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Option } from './types';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* ID of the main SelectControl instance.
|
||||
*/
|
||||
listboxId?: string;
|
||||
/**
|
||||
* ID used for a11y in the listbox.
|
||||
*/
|
||||
instanceId: number;
|
||||
/**
|
||||
* Parent node to bind keyboard events to.
|
||||
*/
|
||||
node: HTMLElement | null;
|
||||
/**
|
||||
* Function to execute when an option is selected.
|
||||
*/
|
||||
onSelect: ( option: Option ) => void;
|
||||
/**
|
||||
* Array of options to display.
|
||||
*/
|
||||
options: Array< Option >;
|
||||
/**
|
||||
* Integer for the currently selected item.
|
||||
*/
|
||||
selectedIndex: number | null | undefined;
|
||||
/**
|
||||
* Bool to determine if the list should be positioned absolutely or staticly.
|
||||
*/
|
||||
staticList: boolean;
|
||||
/**
|
||||
* Function to execute when keyboard navigation should decrement the selected index.
|
||||
*/
|
||||
decrementSelectedIndex: () => void;
|
||||
/**
|
||||
* Function to execute when keyboard navigation should increment the selected index.
|
||||
*/
|
||||
incrementSelectedIndex: () => void;
|
||||
/**
|
||||
* Function to execute when the search value changes.
|
||||
*/
|
||||
onSearch: ( option: string | null ) => void;
|
||||
/**
|
||||
* Function to execute when the list should be expanded or collapsed.
|
||||
*/
|
||||
setExpanded: ( expanded: boolean ) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A list box that displays filtered options after search.
|
||||
*/
|
||||
class List extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
class List extends Component< Props > {
|
||||
optionRefs: { [ key: number ]: RefObject< HTMLButtonElement > };
|
||||
listbox: RefObject< HTMLDivElement >;
|
||||
|
||||
constructor( props: Props ) {
|
||||
super( props );
|
||||
|
||||
this.handleKeyDown = this.handleKeyDown.bind( this );
|
||||
this.select = this.select.bind( this );
|
||||
|
@ -21,7 +76,7 @@ class List extends Component {
|
|||
this.listbox = createRef();
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
componentDidUpdate( prevProps: Props ) {
|
||||
const { options, selectedIndex } = this.props;
|
||||
|
||||
// Remove old option refs to avoid memory leaks.
|
||||
|
@ -29,12 +84,15 @@ class List extends Component {
|
|||
this.optionRefs = {};
|
||||
}
|
||||
|
||||
if ( selectedIndex !== prevProps.selectedIndex ) {
|
||||
if (
|
||||
selectedIndex !== prevProps.selectedIndex &&
|
||||
isNumber( selectedIndex )
|
||||
) {
|
||||
this.scrollToOption( selectedIndex );
|
||||
}
|
||||
}
|
||||
|
||||
getOptionRef( index ) {
|
||||
getOptionRef( index: number ) {
|
||||
if ( ! this.optionRefs.hasOwnProperty( index ) ) {
|
||||
this.optionRefs[ index ] = createRef();
|
||||
}
|
||||
|
@ -42,7 +100,7 @@ class List extends Component {
|
|||
return this.optionRefs[ index ];
|
||||
}
|
||||
|
||||
select( option ) {
|
||||
select( option: Option ) {
|
||||
const { onSelect } = this.props;
|
||||
|
||||
if ( option.isDisabled ) {
|
||||
|
@ -52,9 +110,13 @@ class List extends Component {
|
|||
onSelect( option );
|
||||
}
|
||||
|
||||
scrollToOption( index ) {
|
||||
scrollToOption( index: number ) {
|
||||
const listbox = this.listbox.current;
|
||||
|
||||
if ( ! listbox ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( listbox.scrollHeight <= listbox.clientHeight ) {
|
||||
return;
|
||||
}
|
||||
|
@ -64,6 +126,12 @@ class List extends Component {
|
|||
}
|
||||
|
||||
const option = this.optionRefs[ index ].current;
|
||||
if ( ! option ) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn( 'Option not found, index:', index );
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollBottom = listbox.clientHeight + listbox.scrollTop;
|
||||
const elementBottom = option.offsetTop + option.offsetHeight;
|
||||
if ( elementBottom > scrollBottom ) {
|
||||
|
@ -73,7 +141,7 @@ class List extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleKeyDown( event ) {
|
||||
handleKeyDown( event: KeyboardEvent ) {
|
||||
const {
|
||||
decrementSelectedIndex,
|
||||
incrementSelectedIndex,
|
||||
|
@ -100,7 +168,7 @@ class List extends Component {
|
|||
break;
|
||||
|
||||
case ENTER:
|
||||
if ( options[ selectedIndex ] ) {
|
||||
if ( isNumber( selectedIndex ) && options[ selectedIndex ] ) {
|
||||
this.select( options[ selectedIndex ] );
|
||||
}
|
||||
event.preventDefault();
|
||||
|
@ -118,7 +186,7 @@ class List extends Component {
|
|||
return;
|
||||
|
||||
case TAB:
|
||||
if ( options[ selectedIndex ] ) {
|
||||
if ( isNumber( selectedIndex ) && options[ selectedIndex ] ) {
|
||||
this.select( options[ selectedIndex ] );
|
||||
}
|
||||
setExpanded( false );
|
||||
|
@ -128,8 +196,14 @@ class List extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
toggleKeyEvents( isListening ) {
|
||||
toggleKeyEvents( isListening: boolean ) {
|
||||
const { node } = this.props;
|
||||
if ( ! node ) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn( 'No node to bind events to.' );
|
||||
return;
|
||||
}
|
||||
|
||||
// This exists because we must capture ENTER key presses before RichText.
|
||||
// It seems that react fires the simulated capturing events after the
|
||||
// native browser event has already bubbled so we can't stopPropagation
|
||||
|
@ -138,12 +212,16 @@ class List extends Component {
|
|||
const handler = isListening
|
||||
? 'addEventListener'
|
||||
: 'removeEventListener';
|
||||
node[ handler ]( 'keydown', this.handleKeyDown, true );
|
||||
node[ handler ](
|
||||
'keydown',
|
||||
this.handleKeyDown as ( e: Event ) => void,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { selectedIndex } = this.props;
|
||||
if ( selectedIndex > -1 ) {
|
||||
if ( isNumber( selectedIndex ) && selectedIndex > -1 ) {
|
||||
this.scrollToOption( selectedIndex );
|
||||
}
|
||||
this.toggleKeyEvents( true );
|
||||
|
@ -169,7 +247,7 @@ class List extends Component {
|
|||
id={ listboxId }
|
||||
role="listbox"
|
||||
className={ listboxClasses }
|
||||
tabIndex="-1"
|
||||
tabIndex={ -1 }
|
||||
>
|
||||
{ options.map( ( option, index ) => (
|
||||
<Button
|
||||
|
@ -186,7 +264,7 @@ class List extends Component {
|
|||
}
|
||||
) }
|
||||
onClick={ () => this.select( option ) }
|
||||
tabIndex="-1"
|
||||
tabIndex={ -1 }
|
||||
>
|
||||
{ option.label }
|
||||
</Button>
|
||||
|
@ -196,50 +274,4 @@ class List extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
List.propTypes = {
|
||||
/**
|
||||
* ID of the main SelectControl instance.
|
||||
*/
|
||||
instanceId: PropTypes.number,
|
||||
/**
|
||||
* ID used for a11y in the listbox.
|
||||
*/
|
||||
listboxId: PropTypes.string,
|
||||
/**
|
||||
* Parent node to bind keyboard events to.
|
||||
*/
|
||||
// eslint-disable-next-line no-undef
|
||||
node: PropTypes.instanceOf( Element ).isRequired,
|
||||
/**
|
||||
* Function to execute when an option is selected.
|
||||
*/
|
||||
onSelect: PropTypes.func,
|
||||
/**
|
||||
* Array of options to display.
|
||||
*/
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
isDisabled: PropTypes.bool,
|
||||
key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] )
|
||||
.isRequired,
|
||||
keywords: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType( [ PropTypes.string, PropTypes.number ] )
|
||||
),
|
||||
label: PropTypes.oneOfType( [
|
||||
PropTypes.string,
|
||||
PropTypes.object,
|
||||
] ),
|
||||
value: PropTypes.any,
|
||||
} )
|
||||
).isRequired,
|
||||
/**
|
||||
* Integer for the currently selected item.
|
||||
*/
|
||||
selectedIndex: PropTypes.number,
|
||||
/**
|
||||
* Bool to determine if the list should be positioned absolutely or staticly.
|
||||
*/
|
||||
staticList: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default List;
|
|
@ -1,8 +1,12 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { SelectControl } from '@woocommerce/components';
|
||||
import { useState } from '@wordpress/element';
|
||||
import React from 'react';
|
||||
import { createElement, useState } from '@wordpress/element';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import SelectControl from '../';
|
||||
|
||||
const options = [
|
||||
{
|
|
@ -5,19 +5,36 @@ import { __, sprintf } from '@wordpress/i18n';
|
|||
import { Button } from '@wordpress/components';
|
||||
import { Icon, cancelCircleFilled } from '@wordpress/icons';
|
||||
import { createElement, Component, Fragment } from '@wordpress/element';
|
||||
import { findIndex } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { findIndex, isArray } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Tag from '../tag';
|
||||
import { Option, Selected } from './types';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Function called when selected results change, passed result list.
|
||||
*/
|
||||
onChange: ( selected: Option[] ) => void;
|
||||
/**
|
||||
* An array of objects describing selected values. If the label of the selected
|
||||
* value is omitted, the Tag of that value will not be rendered inside the
|
||||
* search box.
|
||||
*/
|
||||
selected?: Selected;
|
||||
/**
|
||||
* Render a 'Clear' button next to the input box to remove its contents.
|
||||
*/
|
||||
showClearButton?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A list of tags to display selected items.
|
||||
*/
|
||||
class Tags extends Component {
|
||||
constructor( props ) {
|
||||
class Tags extends Component< Props > {
|
||||
constructor( props: Props ) {
|
||||
super( props );
|
||||
this.removeAll = this.removeAll.bind( this );
|
||||
this.removeResult = this.removeResult.bind( this );
|
||||
|
@ -28,9 +45,13 @@ class Tags extends Component {
|
|||
onChange( [] );
|
||||
}
|
||||
|
||||
removeResult( key ) {
|
||||
removeResult( key: string | undefined ) {
|
||||
return () => {
|
||||
const { selected, onChange } = this.props;
|
||||
if ( ! isArray( selected ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const i = findIndex( selected, { key } );
|
||||
onChange( [
|
||||
...selected.slice( 0, i ),
|
||||
|
@ -41,7 +62,7 @@ class Tags extends Component {
|
|||
|
||||
render() {
|
||||
const { selected, showClearButton } = this.props;
|
||||
if ( ! selected.length ) {
|
||||
if ( ! isArray( selected ) || ! selected.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -63,6 +84,7 @@ class Tags extends Component {
|
|||
key={ item.key }
|
||||
id={ item.key }
|
||||
label={ item.label }
|
||||
// @ts-expect-error key is a string or undefined here
|
||||
remove={ this.removeResult }
|
||||
screenReaderLabel={ screenReaderLabel }
|
||||
/>
|
||||
|
@ -89,31 +111,4 @@ class Tags extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
Tags.propTypes = {
|
||||
/**
|
||||
* Function called when selected results change, passed result list.
|
||||
*/
|
||||
onChange: PropTypes.func,
|
||||
/**
|
||||
* Function to execute when an option is selected.
|
||||
*/
|
||||
onSelect: PropTypes.func,
|
||||
/**
|
||||
* An array of objects describing selected values. If the label of the selected
|
||||
* value is omitted, the Tag of that value will not be rendered inside the
|
||||
* search box.
|
||||
*/
|
||||
selected: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] )
|
||||
.isRequired,
|
||||
label: PropTypes.string,
|
||||
} )
|
||||
),
|
||||
/**
|
||||
* Render a 'Clear' button next to the input box to remove its contents.
|
||||
*/
|
||||
showClearButton: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Tags;
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
@ -9,10 +10,11 @@ import { createElement } from '@wordpress/element';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { SelectControl } from '../index';
|
||||
import { Option } from '../types';
|
||||
|
||||
describe( 'SelectControl', () => {
|
||||
const query = 'lorem';
|
||||
const options = [
|
||||
const options: Option[] = [
|
||||
{ key: '1', label: 'lorem 1', value: { id: '1' } },
|
||||
{ key: '2', label: 'lorem 2', value: { id: '2' } },
|
||||
{ key: '3', label: 'bar', value: { id: '3' } },
|
||||
|
@ -168,9 +170,9 @@ describe( 'SelectControl', () => {
|
|||
} );
|
||||
|
||||
it( 'changes the options on search', async () => {
|
||||
const queriedOptions = [];
|
||||
const queriedOptions: Option[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const queryOptions = ( options, searchedQuery ) => {
|
||||
const queryOptions = async ( options: Option[], searchedQuery: string | null ) => {
|
||||
if ( searchedQuery === 'test' ) {
|
||||
queriedOptions.push( {
|
||||
key: 'test-option',
|
||||
|
@ -209,7 +211,7 @@ describe( 'SelectControl', () => {
|
|||
isSearchable
|
||||
showAllOnFocus
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
/>
|
||||
|
@ -229,7 +231,7 @@ describe( 'SelectControl', () => {
|
|||
isSearchable
|
||||
selected={ [ { ...options[ 0 ] } ] }
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
/>
|
||||
|
@ -258,7 +260,7 @@ describe( 'SelectControl', () => {
|
|||
isSearchable
|
||||
selected={ options[ 0 ].key }
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
/>
|
||||
|
@ -289,7 +291,7 @@ describe( 'SelectControl', () => {
|
|||
showAllOnFocus
|
||||
selected={ options[ 2 ].key }
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
excludeSelectedOptions={ false }
|
||||
|
@ -316,7 +318,7 @@ describe( 'SelectControl', () => {
|
|||
showAllOnFocus
|
||||
selected={ options[ 2 ].key }
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
excludeSelectedOptions={ false }
|
||||
|
@ -364,7 +366,7 @@ describe( 'SelectControl', () => {
|
|||
isSearchable
|
||||
selected={ [ { ...options[ 0 ] } ] }
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
/>
|
||||
|
@ -383,7 +385,7 @@ describe( 'SelectControl', () => {
|
|||
isSearchable
|
||||
selected={ options[ 0 ].key }
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
/>
|
||||
|
@ -416,9 +418,8 @@ describe( 'SelectControl', () => {
|
|||
const { getByRole } = render(
|
||||
<SelectControl
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
selected={ null }
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -440,7 +441,7 @@ describe( 'SelectControl', () => {
|
|||
const { getByRole } = render(
|
||||
<SelectControl
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
selected={ options[ 1 ].key }
|
||||
excludeSelectedOptions={ false }
|
||||
|
@ -465,7 +466,7 @@ describe( 'SelectControl', () => {
|
|||
const { getByRole } = render(
|
||||
<SelectControl
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
selected={ options[ options.length - 1 ].key }
|
||||
excludeSelectedOptions={ false }
|
||||
|
@ -490,9 +491,8 @@ describe( 'SelectControl', () => {
|
|||
const { getByRole } = render(
|
||||
<SelectControl
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
selected={ null }
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -514,9 +514,8 @@ describe( 'SelectControl', () => {
|
|||
const { getByRole, queryByRole } = render(
|
||||
<SelectControl
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
selected={ null }
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -537,9 +536,8 @@ describe( 'SelectControl', () => {
|
|||
const { getByRole } = render(
|
||||
<SelectControl
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
selected={ null }
|
||||
excludeSelectedOptions={ false }
|
||||
onChange={ onChangeMock }
|
||||
/>
|
|
@ -0,0 +1,9 @@
|
|||
export type Option = {
|
||||
key: string;
|
||||
label: string;
|
||||
isDisabled?: boolean;
|
||||
keywords?: Array< string >;
|
||||
value?: unknown;
|
||||
};
|
||||
|
||||
export type Selected = string | Option[];
|
|
@ -110,7 +110,7 @@ export const Sortable = ( {
|
|||
|
||||
// Items before the current item cause a one off error when
|
||||
// removed from the old array and spliced into the new array.
|
||||
// TODO: Issue with dragging into same position having to do with isBefore returning true intially.
|
||||
// TODO: Issue with dragging into same position having to do with isBefore returning true initially.
|
||||
let targetIndex = dragIndex < index ? index : index + 1;
|
||||
if ( isBefore( event, isHorizontal ) ) {
|
||||
targetIndex--;
|
||||
|
|
|
@ -61,7 +61,7 @@ Name | Type | Default | Description
|
|||
`ids` | Array | `null` | A list of IDs, matching to the row list so that ids[ 0 ] contains the object ID for the object displayed in row[ 0 ]
|
||||
`isLoading` | Boolean | `false` | Defines if the table contents are loading. It will display `TablePlaceholder` component instead of `Table` if that's the case
|
||||
`onQueryChange` | Function | `noop` | A function which returns a callback function to update the query string for a given `param`
|
||||
`onColumnsChange` | Function | `noop` | A function which returns a callback function which is called upon the user changing the visiblity of columns
|
||||
`onColumnsChange` | Function | `noop` | A function which returns a callback function which is called upon the user changing the visibility of columns
|
||||
`onSearch` | Function | `noop` | A function which is called upon the user searching in the table header
|
||||
`onSort` | Function | `undefined` | A function which is called upon the user changing the sorting of the table
|
||||
`downloadable` | Boolean | `false` | Whether the table must be downloadable. If true, the download button will appear
|
||||
|
|
|
@ -158,7 +158,7 @@ export type TableCardProps = CommonTableProps & {
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onQueryChange?: ( param: string ) => ( ...props: any ) => void;
|
||||
/**
|
||||
* A function which returns a callback function which is called upon the user changing the visiblity of columns.
|
||||
* A function which returns a callback function which is called upon the user changing the visibility of columns.
|
||||
*/
|
||||
onColumnsChange?: ( showCols: Array< string >, key?: string ) => void;
|
||||
/**
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
## Breaking changes
|
||||
|
||||
- Fix the batch fetch logic for the options data store. #7587
|
||||
- Add backwards compability for old function format. #7688
|
||||
- Add backwards compatibility for old function format. #7688
|
||||
- Add console warning for inbox note contents exceeding 320 characters and add dompurify dependency. #7869
|
||||
- Fix race condition in data package's options module. #7947
|
||||
- Remove dev dependency `@woocommerce/wc-admin-settings`. #8057
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Added types for resolveSelect where applicable.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Correct spelling errors
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Fix onboarding productTypes TS define type
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Update Payfast title
|
|
@ -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 > >;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
ProfileItems,
|
||||
TaskListType,
|
||||
TaskType,
|
||||
OnboardingProductType,
|
||||
OnboardingProductTypes,
|
||||
} from './types';
|
||||
import { Plugin } from '../plugins/types';
|
||||
|
||||
|
@ -267,9 +267,7 @@ export function actionTaskSuccess( task: Partial< TaskType > ) {
|
|||
};
|
||||
}
|
||||
|
||||
export function getProductTypesSuccess(
|
||||
productTypes: OnboardingProductType[]
|
||||
) {
|
||||
export function getProductTypesSuccess( productTypes: OnboardingProductTypes ) {
|
||||
return {
|
||||
type: TYPES.GET_PRODUCT_TYPES_SUCCESS,
|
||||
productTypes,
|
||||
|
|
|
@ -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 >;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ export const defaultState: OnboardingState = {
|
|||
},
|
||||
emailPrefill: '',
|
||||
paymentMethods: [],
|
||||
productTypes: [],
|
||||
productTypes: {},
|
||||
requesting: {},
|
||||
taskLists: {},
|
||||
};
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
import { DeprecatedTasks } from './deprecated-tasks';
|
||||
import {
|
||||
ExtensionList,
|
||||
OnboardingProductType,
|
||||
OnboardingProductTypes,
|
||||
ProfileItems,
|
||||
TaskListType,
|
||||
} from './types';
|
||||
|
@ -126,7 +126,7 @@ export function* getFreeExtensions() {
|
|||
|
||||
export function* getProductTypes() {
|
||||
try {
|
||||
const results: OnboardingProductType[] = yield apiFetch( {
|
||||
const results: OnboardingProductTypes = yield apiFetch( {
|
||||
path: WC_ADMIN_NAMESPACE + '/onboarding/product-types',
|
||||
method: 'GET',
|
||||
} );
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
OnboardingState,
|
||||
ExtensionList,
|
||||
ProfileItems,
|
||||
OnboardingProductType,
|
||||
} from './types';
|
||||
import { WPDataSelectors } from '../types';
|
||||
import { Plugin } from '../plugins/types';
|
||||
|
@ -92,10 +91,8 @@ export const getEmailPrefill = ( state: OnboardingState ): string => {
|
|||
return state.emailPrefill || '';
|
||||
};
|
||||
|
||||
export const getProductTypes = (
|
||||
state: OnboardingState
|
||||
): OnboardingProductType[] => {
|
||||
return state.productTypes || [];
|
||||
export const getProductTypes = ( state: OnboardingState ) => {
|
||||
return state.productTypes || {};
|
||||
};
|
||||
|
||||
export type OnboardingSelectors = {
|
||||
|
|
|
@ -75,7 +75,7 @@ export type OnboardingState = {
|
|||
profileItems: ProfileItems;
|
||||
taskLists: Record< string, TaskListType >;
|
||||
paymentMethods: Plugin[];
|
||||
productTypes: OnboardingProductType[];
|
||||
productTypes: OnboardingProductTypes;
|
||||
emailPrefill: string;
|
||||
// TODO clarify what the error record's type is
|
||||
errors: Record< string, unknown >;
|
||||
|
@ -152,11 +152,21 @@ export type MethodFields = {
|
|||
};
|
||||
|
||||
export type OnboardingProductType = {
|
||||
default?: boolean;
|
||||
label: string;
|
||||
default?: boolean;
|
||||
product?: number;
|
||||
id?: number;
|
||||
title?: string;
|
||||
yearly_price?: number;
|
||||
description?: string;
|
||||
more_url?: string;
|
||||
slug?: string;
|
||||
};
|
||||
|
||||
export type OnboardingProductTypes =
|
||||
| Record< ProductTypeSlug, OnboardingProductType >
|
||||
| Record< string, never >;
|
||||
|
||||
export type ExtensionList = {
|
||||
key: string;
|
||||
title: string;
|
||||
|
|
|
@ -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 > >;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ export const pluginNames = {
|
|||
'woocommerce'
|
||||
),
|
||||
'woocommerce-gateway-stripe': __( 'WooCommerce Stripe', 'woocommerce' ),
|
||||
'woocommerce-payfast-gateway': __( 'WooCommerce PayFast', 'woocommerce' ),
|
||||
'woocommerce-payfast-gateway': __( 'WooCommerce Payfast', 'woocommerce' ),
|
||||
'woocommerce-payments': __( 'WooCommerce Payments', 'woocommerce' ),
|
||||
'woocommerce-services': __( 'WooCommerce Shipping & Tax', 'woocommerce' ),
|
||||
'woocommerce-services:shipping': __(
|
||||
|
|
|
@ -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 > >;
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue