Merge branch 'trunk' into add/sync_of_deleted_orders
This commit is contained in:
commit
34a8de217c
|
@ -1,5 +1,5 @@
|
||||||
name: ✨ Enhancement Request
|
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]: "
|
title: "[Enhancement]: "
|
||||||
labels: ["type: enhancement", "status: awaiting triage"]
|
labels: ["type: enhancement", "status: awaiting triage"]
|
||||||
body:
|
body:
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
contact_links:
|
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
|
- name: 🔒 Security issue
|
||||||
url: https://hackerone.com/automattic/
|
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).
|
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:
|
### 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. -->
|
<!-- Describe the changes made to this Pull Request and the reason for such changes. -->
|
||||||
|
|
||||||
Closes # .
|
Closes # .
|
||||||
|
|
|
@ -9,6 +9,11 @@ inputs:
|
||||||
tests:
|
tests:
|
||||||
description: Specific tests to run, separated by single whitespace. See https://playwright.dev/docs/test-cli
|
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:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -12,6 +12,11 @@ inputs:
|
||||||
description: The Playwright configuration file to use.
|
description: The Playwright configuration file to use.
|
||||||
default: playwright.config.js
|
default: playwright.config.js
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
result:
|
||||||
|
description: Whether the test passed or failed.
|
||||||
|
value: ${{ steps.run-e2e-tests.conclusion }}
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
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
|
working-directory: plugins/woocommerce
|
||||||
run: pnpm run build:feature-config
|
run: pnpm run build:feature-config
|
||||||
|
|
||||||
- name: Add PHP8 Compatibility.
|
- id: parseMatrix
|
||||||
run: |
|
name: Parse Matrix Variables
|
||||||
if [ "$(php -r "echo version_compare(PHP_VERSION,'8.0','>=');")" ]; then
|
uses: actions/github-script@v6
|
||||||
cd plugins/woocommerce
|
with:
|
||||||
curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip
|
script: |
|
||||||
unzip -d /tmp/phpunit-7.5-fork /tmp/phpunit-7.5-fork.zip
|
const parseWPVersion = require( './.github/workflows/scripts/parse-wp-version' );
|
||||||
composer bin phpunit config --unset platform
|
parseWPVersion( '${{ matrix.wp }}' ).then( ( version ) => {
|
||||||
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}}'
|
core.setOutput( 'wpVersion', version );
|
||||||
composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-reqs
|
} );
|
||||||
rm -rf ./vendor/phpunit/
|
|
||||||
composer dump-autoload
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Init DB and WP
|
- name: Prepare Testing Environment
|
||||||
working-directory: plugins/woocommerce
|
env:
|
||||||
run: ./tests/bin/install.sh woo_test root root 127.0.0.1 ${{ matrix.wp }}
|
WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }}
|
||||||
|
WP_ENV_PHP_VERSION: ${{ matrix.php }}
|
||||||
|
run: pnpm --filter=woocommerce env:test
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run Tests
|
||||||
working-directory: plugins/woocommerce
|
env:
|
||||||
run: pnpm run test --color
|
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:
|
jobs:
|
||||||
e2e-tests-run:
|
e2e-tests-run:
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
|
|
||||||
name: Runs E2E tests.
|
name: Runs E2E tests.
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
permissions:
|
permissions:
|
||||||
|
@ -85,7 +84,6 @@ jobs:
|
||||||
|
|
||||||
api-tests-run:
|
api-tests-run:
|
||||||
name: Runs API tests.
|
name: Runs API tests.
|
||||||
if: github.event.pull_request.user.login != 'github-actions[bot]'
|
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
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:
|
jobs:
|
||||||
test:
|
test:
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
|
|
||||||
name: Code sniff (PHP 7.4, WP Latest)
|
name: Code sniff (PHP 7.4, WP Latest)
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
|
@ -8,7 +8,7 @@ permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
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
|
name: Check pull request changes to highlight
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
permissions:
|
permissions:
|
||||||
|
|
|
@ -11,7 +11,6 @@ permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
changelogger_used:
|
changelogger_used:
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
|
|
||||||
name: Changelogger use
|
name: Changelogger use
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
permissions:
|
permissions:
|
||||||
|
|
|
@ -12,7 +12,6 @@ permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-test-js:
|
lint-test-js:
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
|
|
||||||
name: Lint and Test JS
|
name: Lint and Test JS
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
permissions:
|
permissions:
|
||||||
|
|
|
@ -14,7 +14,7 @@ permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
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' || '' }}
|
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} ${{ matrix.hpos && 'HPOS' || '' }}
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
@ -52,15 +52,24 @@ jobs:
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php }}
|
php-version: ${{ matrix.php }}
|
||||||
|
|
||||||
- name: Tool versions
|
- id: parseMatrix
|
||||||
run: |
|
name: Parse Matrix Variables
|
||||||
php --version
|
uses: actions/github-script@v6
|
||||||
composer --version
|
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
|
- name: Prepare Testing Environment
|
||||||
working-directory: plugins/woocommerce
|
env:
|
||||||
run: ./tests/bin/install.sh woo_test root root 127.0.0.1 ${{ matrix.wp }}
|
WP_ENV_CORE: ${{ steps.parseMatrix.outputs.wpVersion }}
|
||||||
|
WP_ENV_PHP_VERSION: ${{ matrix.php }}
|
||||||
|
run: pnpm --filter=woocommerce env:test
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run Tests
|
||||||
working-directory: plugins/woocommerce
|
env:
|
||||||
run: pnpm run test --filter=woocommerce --color
|
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:
|
with:
|
||||||
fetch-depth: 0
|
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
|
- name: Install prerequisites
|
||||||
run: |
|
run: |
|
||||||
npm install -g pnpm
|
pnpm install --filter monorepo-utils --ignore-scripts
|
||||||
pnpm install --filter monorepo-utils
|
# 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'
|
- name: 'Check whether today is the code freeze day'
|
||||||
id: check-freeze
|
id: check-freeze
|
||||||
|
@ -71,42 +85,47 @@ jobs:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: pnpm run utils code-freeze version-bump -o ${{ github.repository_owner }} -v ${{ steps.milestone.outputs.nextDevelopmentVersion }}.0-dev
|
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:
|
notify-slack:
|
||||||
name: 'Sends code freeze notification to Slack'
|
name: 'Sends code freeze notification to Slack'
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: code-freeze-prep
|
needs: code-freeze-prep
|
||||||
if: ${{ inputs.skipSlackPing != true }}
|
if: ${{ needs.code-freeze-prep.outputs.freeze == 'true' && inputs.skipSlackPing != true }}
|
||||||
steps:
|
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
|
- name: Slack
|
||||||
uses: archive/github-actions-slack@v2.0.0
|
|
||||||
id: notify
|
id: notify
|
||||||
with:
|
run: |
|
||||||
slack-bot-user-oauth-access-token: ${{ secrets.CODE_FREEZE_BOT_TOKEN }}
|
pnpm utils slack "${{ secrets.CODE_FREEZE_BOT_TOKEN }}" "
|
||||||
slack-channel: ${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }}
|
:warning-8c: ${{ needs.code-freeze-prep.outputs.nextReleaseVersion }} Code Freeze :ice_cube:
|
||||||
slack-text: |
|
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>.
|
||||||
:warning-8c: ${{ needs.code-freeze-prep.outputs.nextReleaseVersion }} Code Freeze :ice_cube:
|
" "${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }}"
|
||||||
|
|
||||||
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 }}"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
|
@ -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.
|
* @param string $branch The name of the branch.
|
||||||
* @return string Returns the name of the branch, or a falsey value on error.
|
* @return string Returns the name of the branch, or a falsey value on error.
|
||||||
|
|
|
@ -48,7 +48,7 @@ jobs:
|
||||||
working-directory: plugins/woocommerce
|
working-directory: plugins/woocommerce
|
||||||
env:
|
env:
|
||||||
UPDATE_WC: nightly
|
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.
|
- name: Run API tests.
|
||||||
working-directory: plugins/woocommerce
|
working-directory: plugins/woocommerce
|
||||||
|
@ -200,15 +200,17 @@ jobs:
|
||||||
- plugin: 'WooCommerce Subscriptions'
|
- plugin: 'WooCommerce Subscriptions'
|
||||||
repo: WC_SUBSCRIPTIONS_REPO
|
repo: WC_SUBSCRIPTIONS_REPO
|
||||||
private: true
|
private: true
|
||||||
- plugin: 'WordPress SEO' # Yoast SEO in the UI, but the slug is wordpress-seo
|
- plugin: 'Gutenberg'
|
||||||
repo: 'Yoast/wordpress-seo'
|
repo: 'WordPress/gutenberg'
|
||||||
- plugin: 'Contact Form 7'
|
- plugin: 'Gutenberg - Nightly'
|
||||||
repo: 'takayukister/contact-form-7'
|
repo: 'bph/gutenberg'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup WooCommerce Monorepo
|
- name: Setup WooCommerce Monorepo
|
||||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||||
|
with:
|
||||||
|
build-filters: woocommerce
|
||||||
|
|
||||||
- name: Launch wp-env e2e environment
|
- name: Launch wp-env e2e environment
|
||||||
working-directory: plugins/woocommerce
|
working-directory: plugins/woocommerce
|
||||||
|
@ -224,13 +226,13 @@ jobs:
|
||||||
PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }}
|
PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }}
|
||||||
PLUGIN_NAME: ${{ matrix.plugin }}
|
PLUGIN_NAME: ${{ matrix.plugin }}
|
||||||
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
|
||||||
run: 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
|
- name: Run the rest of E2E tests
|
||||||
working-directory: plugins/woocommerce
|
working-directory: plugins/woocommerce
|
||||||
env:
|
env:
|
||||||
E2E_MAX_FAILURES: 15
|
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.
|
- name: Generate E2E Test report.
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
|
@ -321,36 +323,29 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- plugin: 'WooCommerce Payments'
|
- plugin: 'WooCommerce Payments'
|
||||||
repo: 'automattic/woocommerce-payments'
|
slug: woocommerce-payments
|
||||||
- plugin: 'WooCommerce PayPal Payments'
|
- plugin: 'WooCommerce PayPal Payments'
|
||||||
repo: 'woocommerce/woocommerce-paypal-payments'
|
slug: woocommerce-paypal-payments
|
||||||
- plugin: 'WooCommerce Shipping & Tax'
|
- plugin: 'WooCommerce Shipping & Tax'
|
||||||
repo: 'automattic/woocommerce-services'
|
slug: woocommerce-services
|
||||||
- plugin: 'WordPress SEO' # Yoast SEO in the UI, but the slug is wordpress-seo
|
- plugin: 'WooCommerce Subscriptions'
|
||||||
repo: 'Yoast/wordpress-seo'
|
slug: woocommerce-subscriptions
|
||||||
- plugin: 'Contact Form 7'
|
- plugin: 'Gutenberg'
|
||||||
repo: 'takayukister/contact-form-7'
|
slug: gutenberg
|
||||||
|
- plugin: 'Gutenberg - Nightly'
|
||||||
|
slug: gutenberg-nightly
|
||||||
steps:
|
steps:
|
||||||
- name: Download test report artifact
|
- name: Download test report artifact
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ env.ARTIFACT }}
|
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
|
- name: Publish reports
|
||||||
run: |
|
run: |
|
||||||
gh workflow run publish-test-reports-daily-plugins.yml \
|
gh workflow run publish-test-reports-daily-plugins.yml \
|
||||||
-f run_id=$RUN_ID \
|
-f run_id=$RUN_ID \
|
||||||
-f artifact="${{ env.ARTIFACT }}" \
|
-f artifact="${{ env.ARTIFACT }}" \
|
||||||
-f plugin="${{ matrix.plugin }}" \
|
-f plugin="${{ matrix.plugin }}" \
|
||||||
-f slug="${{ steps.get-slug.outputs.result }}" \
|
-f slug="${{ matrix.slug }}" \
|
||||||
-f s3_root=public \
|
-f s3_root=public \
|
||||||
--repo woocommerce/woocommerce-test-reports
|
--repo woocommerce/woocommerce-test-reports
|
||||||
|
|
|
@ -14,7 +14,7 @@ permissions: {}
|
||||||
env:
|
env:
|
||||||
E2E_WP_LATEST_ARTIFACT: E2E test on release smoke test site with WP Latest (run ${{ github.run_number }})
|
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 }})
|
E2E_UPDATE_WC_ARTIFACT: WooCommerce version update test on release smoke test site (run ${{ github.run_number }})
|
||||||
|
SLACK_BLOCKS_ARTIFACT: slack-blocks
|
||||||
jobs:
|
jobs:
|
||||||
get-tag:
|
get-tag:
|
||||||
name: Get WooCommerce release tag
|
name: Get WooCommerce release tag
|
||||||
|
@ -122,12 +122,26 @@ jobs:
|
||||||
-f test_type="e2e" \
|
-f test_type="e2e" \
|
||||||
--repo woocommerce/woocommerce-test-reports
|
--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:
|
api-wp-latest:
|
||||||
name: API on WP Latest
|
name: API on WP Latest
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [get-tag, e2e-update-wc]
|
needs: [get-tag, e2e-update-wc]
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
outputs:
|
||||||
|
result: ${{ steps.run-api-composite-action.outputs.result }}
|
||||||
env:
|
env:
|
||||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report
|
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
|
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" \
|
-f test_type="api" \
|
||||||
--repo woocommerce/woocommerce-test-reports
|
--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:
|
e2e-wp-latest:
|
||||||
name: E2E on WP Latest
|
name: E2E on WP Latest
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
@ -268,6 +294,19 @@ jobs:
|
||||||
-f test_type="e2e" \
|
-f test_type="e2e" \
|
||||||
--repo woocommerce/woocommerce-test-reports
|
--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:
|
get-wp-versions:
|
||||||
name: Get WP L-1 & L-2 version numbers
|
name: Get WP L-1 & L-2 version numbers
|
||||||
needs: [get-tag]
|
needs: [get-tag]
|
||||||
|
@ -328,6 +367,8 @@ jobs:
|
||||||
|
|
||||||
- name: Setup WooCommerce Monorepo
|
- name: Setup WooCommerce Monorepo
|
||||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||||
|
with:
|
||||||
|
build-filters: woocommerce
|
||||||
|
|
||||||
- name: Launch WP Env
|
- name: Launch WP Env
|
||||||
working-directory: plugins/woocommerce
|
working-directory: plugins/woocommerce
|
||||||
|
@ -360,7 +401,7 @@ jobs:
|
||||||
uses: ./.github/actions/tests/run-api-tests
|
uses: ./.github/actions/tests/run-api-tests
|
||||||
with:
|
with:
|
||||||
report-name: ${{ env.API_WP_LATEST_X_ARTIFACT }}
|
report-name: ${{ env.API_WP_LATEST_X_ARTIFACT }}
|
||||||
tests: hello
|
tests: hello.test.js
|
||||||
env:
|
env:
|
||||||
ALLURE_RESULTS_DIR: ${{ env.API_ALLURE_RESULTS_DIR }}
|
ALLURE_RESULTS_DIR: ${{ env.API_ALLURE_RESULTS_DIR }}
|
||||||
ALLURE_REPORT_DIR: ${{ env.API_ALLURE_REPORT_DIR }}
|
ALLURE_REPORT_DIR: ${{ env.API_ALLURE_REPORT_DIR }}
|
||||||
|
@ -435,6 +476,22 @@ jobs:
|
||||||
-f test_type="e2e" \
|
-f test_type="e2e" \
|
||||||
--repo woocommerce/woocommerce-test-reports
|
--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:
|
test-php-versions:
|
||||||
name: Test against PHP ${{ matrix.php_version }}
|
name: Test against PHP ${{ matrix.php_version }}
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
@ -456,6 +513,8 @@ jobs:
|
||||||
|
|
||||||
- name: Setup WooCommerce Monorepo
|
- name: Setup WooCommerce Monorepo
|
||||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||||
|
with:
|
||||||
|
build-filters: woocommerce
|
||||||
|
|
||||||
- name: Launch WP Env
|
- name: Launch WP Env
|
||||||
working-directory: plugins/woocommerce
|
working-directory: plugins/woocommerce
|
||||||
|
@ -482,7 +541,7 @@ jobs:
|
||||||
uses: ./.github/actions/tests/run-api-tests
|
uses: ./.github/actions/tests/run-api-tests
|
||||||
with:
|
with:
|
||||||
report-name: ${{ env.API_ARTIFACT }}
|
report-name: ${{ env.API_ARTIFACT }}
|
||||||
tests: hello
|
tests: hello.test.js
|
||||||
env:
|
env:
|
||||||
ALLURE_RESULTS_DIR: ${{ env.API_ALLURE_RESULTS_DIR }}
|
ALLURE_RESULTS_DIR: ${{ env.API_ALLURE_RESULTS_DIR }}
|
||||||
ALLURE_REPORT_DIR: ${{ env.API_ALLURE_REPORT_DIR }}
|
ALLURE_REPORT_DIR: ${{ env.API_ALLURE_REPORT_DIR }}
|
||||||
|
@ -557,6 +616,22 @@ jobs:
|
||||||
-f test_type="e2e" \
|
-f test_type="e2e" \
|
||||||
--repo woocommerce/woocommerce-test-reports
|
--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:
|
test-plugins:
|
||||||
name: With ${{ matrix.plugin }}
|
name: With ${{ matrix.plugin }}
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
@ -582,18 +657,20 @@ jobs:
|
||||||
repo: WC_SUBSCRIPTIONS_REPO
|
repo: WC_SUBSCRIPTIONS_REPO
|
||||||
private: true
|
private: true
|
||||||
env_description: 'woocommerce-subscriptions'
|
env_description: 'woocommerce-subscriptions'
|
||||||
- plugin: 'WordPress SEO' # Yoast SEO in the UI, but the slug is wordpress-seo
|
- plugin: 'Gutenberg'
|
||||||
repo: 'Yoast/wordpress-seo'
|
repo: 'WordPress/gutenberg'
|
||||||
env_description: 'wordpress-seo'
|
env_description: 'gutenberg'
|
||||||
- plugin: 'Contact Form 7'
|
- plugin: 'Gutenberg - Nightly'
|
||||||
repo: 'takayukister/contact-form-7'
|
repo: 'bph/gutenberg'
|
||||||
env_description: 'contact-form-7'
|
env_description: 'gutenberg-nightly'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup WooCommerce Monorepo
|
- name: Setup WooCommerce Monorepo
|
||||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||||
|
with:
|
||||||
|
build-filters: woocommerce
|
||||||
|
|
||||||
- name: Launch WP Env
|
- name: Launch WP Env
|
||||||
working-directory: plugins/woocommerce
|
working-directory: plugins/woocommerce
|
||||||
|
@ -656,3 +733,54 @@ jobs:
|
||||||
-f env_description="${{ matrix.env_description }}" \
|
-f env_description="${{ matrix.env_description }}" \
|
||||||
-f test_type="e2e" \
|
-f test_type="e2e" \
|
||||||
--repo woocommerce/woocommerce-test-reports
|
--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/eslint-plugin",
|
||||||
"@wordpress/babel-plugin-import-jsx-pragma",
|
"@wordpress/babel-plugin-import-jsx-pragma",
|
||||||
"@wordpress/babel-preset-default",
|
"@wordpress/babel-preset-default",
|
||||||
"@wordpress/env",
|
|
||||||
"@wordpress/stylelint-config",
|
"@wordpress/stylelint-config",
|
||||||
"@wordpress/prettier-config",
|
"@wordpress/prettier-config",
|
||||||
"@wordpress/scripts",
|
"@wordpress/scripts",
|
||||||
|
@ -116,6 +115,15 @@
|
||||||
],
|
],
|
||||||
"isIgnored": true
|
"isIgnored": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"dependencies": [
|
||||||
|
"@wordpress/env"
|
||||||
|
],
|
||||||
|
"packages": [
|
||||||
|
"**"
|
||||||
|
],
|
||||||
|
"pinVersion": "^7.0.0"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@wordpress/**"
|
"@wordpress/**"
|
||||||
|
|
|
@ -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.
|
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.
|
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.
|
- [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.
|
- [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.
|
- [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.
|
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 ==
|
== 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 =
|
= 7.6.1 2023-04-26 =
|
||||||
|
|
||||||
**WooCommerce**
|
**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 - 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 - 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 - 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)
|
* 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 "Create a new campaign" modal in Campaigns card in Multichannel Marketing page. [#37044](https://github.com/woocommerce/woocommerce/pull/37044)
|
||||||
* Add - Add a cache for orders, to use when custom order tables are enabled [#35014](https://github.com/woocommerce/woocommerce/pull/35014)
|
* Add - Add 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 - 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 - 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)
|
* 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 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 confirmation modal when the user tries to navigate away with unsaved changes [#35625](https://github.com/woocommerce/woocommerce/pull/35625)
|
||||||
* Enhancement - Add a default placeholder title for newly added attributes and always show remove button for attributes [#35904](https://github.com/woocommerce/woocommerce/pull/35904)
|
* Enhancement - Add 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 - Add protection around func_get_args_call for backwards compatibility. #28677
|
||||||
* Fix - Restore stock_status in REST API V3 response. #28731
|
* 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
|
* 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 - Add support for running a custom initialization script for tests. #28041
|
||||||
* Dev - Use the coenjacobs/mozart package to renamespace vendor packages. #28147
|
* Dev - Use the coenjacobs/mozart package to renamespace vendor packages. #28147
|
||||||
* Dev - Documentation for `wc_get_container`. #28269
|
* 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 - 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 - 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 - 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 - 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 - Apply sale price until end of closing sale date. #22189
|
||||||
* Fix - Allow empty schema again when registering a custom field for the API. #22204
|
* Fix - Allow empty schema again when registering a custom field for the API. #22204
|
||||||
|
@ -6612,7 +6738,7 @@
|
||||||
* Removed internal scroll from log viewer.
|
* Removed internal scroll from log viewer.
|
||||||
* Add reply-to to admin emails.
|
* Add reply-to to admin emails.
|
||||||
* Improved the zone setup flow.
|
* 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().
|
* Use all paid statuses in $customer->get_total_spent().
|
||||||
* Move location of billing email field to work with password managers.
|
* Move location of billing email field to work with password managers.
|
||||||
* Option to restrict selling locations by country.
|
* Option to restrict selling locations by country.
|
||||||
|
@ -8122,7 +8248,7 @@
|
||||||
* Tweak - Flat rate shipping support for percentage factor of additional costs.
|
* 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 - 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 - 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 - Coupon in admin bar new menu #3974
|
||||||
* Tweak - Shortcode tag filters + updated menu names to make white labelling easier.
|
* 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/
|
* 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",
|
"create-extension": "node ./tools/create-extension/index.js",
|
||||||
"cherry-pick": "node ./tools/cherry-pick/bin/run",
|
"cherry-pick": "node ./tools/cherry-pick/bin/run",
|
||||||
"sync-dependencies": "pnpm exec syncpack -- fix-mismatches",
|
"sync-dependencies": "pnpm exec syncpack -- fix-mismatches",
|
||||||
"utils": "./tools/monorepo-utils/bin/run"
|
"utils": "node ./tools/monorepo-utils/dist/index.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-env": "^7.20.2",
|
"@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
|
`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
|
`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
|
`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
|
`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
|
`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
|
`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,
|
PropTypes.func,
|
||||||
] ),
|
] ),
|
||||||
/**
|
/**
|
||||||
* Wether header UI controls must be displayed.
|
* Whether header UI controls must be displayed.
|
||||||
*/
|
*/
|
||||||
showHeaderControls: PropTypes.bool,
|
showHeaderControls: PropTypes.bool,
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
import { Ref } from 'react';
|
||||||
import { format as formatDate } from '@wordpress/date';
|
import { format as formatDate } from '@wordpress/date';
|
||||||
import {
|
import {
|
||||||
createElement,
|
createElement,
|
||||||
|
@ -9,6 +10,7 @@ import {
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
|
forwardRef,
|
||||||
} from '@wordpress/element';
|
} from '@wordpress/element';
|
||||||
import { Icon, calendar } from '@wordpress/icons';
|
import { Icon, calendar } from '@wordpress/icons';
|
||||||
import moment, { Moment } from 'moment';
|
import moment, { Moment } from 'moment';
|
||||||
|
@ -48,306 +50,329 @@ export type DateTimePickerControlProps = {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
help?: string | null;
|
help?: string | null;
|
||||||
onChangeDebounceWait?: number;
|
onChangeDebounceWait?: number;
|
||||||
} & Omit< React.HTMLAttributes< HTMLDivElement >, 'onChange' >;
|
} & Omit< React.HTMLAttributes< HTMLInputElement >, 'onChange' >;
|
||||||
|
|
||||||
export const DateTimePickerControl: React.FC< DateTimePickerControlProps > = ( {
|
export const DateTimePickerControl = forwardRef(
|
||||||
currentDate,
|
function ForwardedDateTimePickerControl(
|
||||||
isDateOnlyPicker = false,
|
{
|
||||||
is12HourPicker = true,
|
currentDate,
|
||||||
timeForDateOnly = 'start-of-day',
|
isDateOnlyPicker = false,
|
||||||
dateTimeFormat,
|
is12HourPicker = true,
|
||||||
disabled = false,
|
timeForDateOnly = 'start-of-day',
|
||||||
onChange,
|
dateTimeFormat,
|
||||||
onBlur,
|
disabled = false,
|
||||||
label,
|
onChange,
|
||||||
placeholder,
|
onBlur,
|
||||||
help,
|
label,
|
||||||
className = '',
|
placeholder,
|
||||||
onChangeDebounceWait = 500,
|
help,
|
||||||
}: DateTimePickerControlProps ) => {
|
className = '',
|
||||||
const instanceId = useInstanceId( DateTimePickerControl );
|
onChangeDebounceWait = 500,
|
||||||
const id = `inspector-date-time-picker-control-${ instanceId }`;
|
...props
|
||||||
const inputControl = useRef< InputControl >();
|
}: DateTimePickerControlProps,
|
||||||
|
ref: Ref< HTMLInputElement >
|
||||||
|
) {
|
||||||
|
const id = useInstanceId(
|
||||||
|
DateTimePickerControl,
|
||||||
|
'inspector-date-time-picker-control',
|
||||||
|
props.id
|
||||||
|
) as string;
|
||||||
|
const inputControl = useRef< InputControl >();
|
||||||
|
|
||||||
const displayFormat = useMemo( () => {
|
const displayFormat = useMemo( () => {
|
||||||
if ( dateTimeFormat ) {
|
if ( dateTimeFormat ) {
|
||||||
return 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' );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedMomentDate;
|
if ( isDateOnlyPicker ) {
|
||||||
},
|
return defaultDateFormat;
|
||||||
[ isDateOnlyPicker, timeForDateOnly ]
|
}
|
||||||
);
|
|
||||||
|
|
||||||
function hasFocusLeftInputAndDropdownContent(
|
if ( is12HourPicker ) {
|
||||||
event: React.FocusEvent< HTMLInputElement >
|
return default12HourDateTimeFormat;
|
||||||
): boolean {
|
}
|
||||||
return ! event.relatedTarget?.closest(
|
|
||||||
'.components-dropdown__content'
|
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
|
`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
|
`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
|
`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 {
|
.woocommerce-experimental-select-control__items-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 2px $gap-smaller;
|
padding: 2px $gap-smaller;
|
||||||
|
|
||||||
|
|
|
@ -75,8 +75,8 @@ export const ComboBox = ( {
|
||||||
<input
|
<input
|
||||||
{ ...inputProps }
|
{ ...inputProps }
|
||||||
ref={ ( node ) => {
|
ref={ ( node ) => {
|
||||||
|
inputRef.current = node;
|
||||||
if ( typeof inputProps.ref === 'function' ) {
|
if ( typeof inputProps.ref === 'function' ) {
|
||||||
inputRef.current = node;
|
|
||||||
(
|
(
|
||||||
inputProps.ref as unknown as (
|
inputProps.ref as unknown as (
|
||||||
node: HTMLInputElement | null
|
node: HTMLInputElement | null
|
||||||
|
|
|
@ -12,6 +12,18 @@
|
||||||
border-color: var( --wp-admin-theme-color );
|
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 {
|
&__label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: $gap-smaller;
|
margin-bottom: $gap-smaller;
|
||||||
|
|
|
@ -9,13 +9,14 @@ import {
|
||||||
useMultipleSelection,
|
useMultipleSelection,
|
||||||
GetInputPropsOptions,
|
GetInputPropsOptions,
|
||||||
} from 'downshift';
|
} from 'downshift';
|
||||||
|
import { useInstanceId } from '@wordpress/compose';
|
||||||
import {
|
import {
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
createElement,
|
createElement,
|
||||||
Fragment,
|
Fragment,
|
||||||
} from '@wordpress/element';
|
} from '@wordpress/element';
|
||||||
import { search } from '@wordpress/icons';
|
import { chevronDown } from '@wordpress/icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -57,6 +58,7 @@ export type SelectControlProps< ItemType > = {
|
||||||
) => void;
|
) => void;
|
||||||
onRemove?: ( item: ItemType ) => void;
|
onRemove?: ( item: ItemType ) => void;
|
||||||
onSelect?: ( selected: ItemType ) => void;
|
onSelect?: ( selected: ItemType ) => void;
|
||||||
|
onKeyDown?: ( e: KeyboardEvent ) => void;
|
||||||
onFocus?: ( data: { inputValue: string } ) => void;
|
onFocus?: ( data: { inputValue: string } ) => void;
|
||||||
stateReducer?: (
|
stateReducer?: (
|
||||||
state: UseComboboxState< ItemType | null >,
|
state: UseComboboxState< ItemType | null >,
|
||||||
|
@ -69,6 +71,8 @@ export type SelectControlProps< ItemType > = {
|
||||||
inputProps?: GetInputPropsOptions;
|
inputProps?: GetInputPropsOptions;
|
||||||
suffix?: JSX.Element | null;
|
suffix?: JSX.Element | null;
|
||||||
showToggleButton?: boolean;
|
showToggleButton?: boolean;
|
||||||
|
readOnlyWhenClosed?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a feature already implemented in downshift@7.0.0 through the
|
* 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
|
* reducer. In order for us to use it this prop is added temporarily until
|
||||||
|
@ -117,18 +121,24 @@ function SelectControl< ItemType = DefaultItemType >( {
|
||||||
onRemove = () => null,
|
onRemove = () => null,
|
||||||
onSelect = () => null,
|
onSelect = () => null,
|
||||||
onFocus = () => null,
|
onFocus = () => null,
|
||||||
|
onKeyDown = () => null,
|
||||||
stateReducer = ( state, actionAndChanges ) => actionAndChanges.changes,
|
stateReducer = ( state, actionAndChanges ) => actionAndChanges.changes,
|
||||||
placeholder,
|
placeholder,
|
||||||
selected,
|
selected,
|
||||||
className,
|
className,
|
||||||
disabled,
|
disabled,
|
||||||
inputProps = {},
|
inputProps = {},
|
||||||
suffix = <SuffixIcon icon={ search } />,
|
suffix = <SuffixIcon icon={ chevronDown } />,
|
||||||
showToggleButton = false,
|
showToggleButton = false,
|
||||||
|
readOnlyWhenClosed = true,
|
||||||
__experimentalOpenMenuOnFocus = false,
|
__experimentalOpenMenuOnFocus = false,
|
||||||
}: SelectControlProps< ItemType > ) {
|
}: SelectControlProps< ItemType > ) {
|
||||||
const [ isFocused, setIsFocused ] = useState( false );
|
const [ isFocused, setIsFocused ] = useState( false );
|
||||||
const [ inputValue, setInputValue ] = useState( '' );
|
const [ inputValue, setInputValue ] = useState( '' );
|
||||||
|
const instanceId = useInstanceId(
|
||||||
|
SelectControl,
|
||||||
|
'woocommerce-experimental-select-control'
|
||||||
|
);
|
||||||
|
|
||||||
let selectedItems = selected === null ? [] : selected;
|
let selectedItems = selected === null ? [] : selected;
|
||||||
selectedItems = Array.isArray( selectedItems )
|
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 ) => {
|
const onRemoveItem = ( item: ItemType ) => {
|
||||||
selectItem( null );
|
selectItem( null );
|
||||||
removeSelectedItem( item );
|
removeSelectedItem( item );
|
||||||
onRemove( item );
|
onRemove( item );
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isReadOnly = readOnlyWhenClosed && ! isOpen && ! isFocused;
|
||||||
|
|
||||||
const selectedItemTags = multiple ? (
|
const selectedItemTags = multiple ? (
|
||||||
<SelectedItems
|
<SelectedItems
|
||||||
items={ selectedItems }
|
items={ selectedItems }
|
||||||
|
isReadOnly={ isReadOnly }
|
||||||
getItemLabel={ getItemLabel }
|
getItemLabel={ getItemLabel }
|
||||||
getItemValue={ getItemValue }
|
getItemValue={ getItemValue }
|
||||||
getSelectedItemProps={ getSelectedItemProps }
|
getSelectedItemProps={ getSelectedItemProps }
|
||||||
|
@ -251,8 +270,12 @@ function SelectControl< ItemType = DefaultItemType >( {
|
||||||
className={ classnames(
|
className={ classnames(
|
||||||
'woocommerce-experimental-select-control',
|
'woocommerce-experimental-select-control',
|
||||||
className,
|
className,
|
||||||
|
instanceId,
|
||||||
{
|
{
|
||||||
|
'is-read-only': isReadOnly,
|
||||||
'is-focused': isFocused,
|
'is-focused': isFocused,
|
||||||
|
'is-multiple': multiple,
|
||||||
|
'has-selected-items': selectedItems.length,
|
||||||
}
|
}
|
||||||
) }
|
) }
|
||||||
>
|
>
|
||||||
|
@ -282,7 +305,12 @@ function SelectControl< ItemType = DefaultItemType >( {
|
||||||
openMenu();
|
openMenu();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onBlur: () => setIsFocused( false ),
|
onBlur: ( event: React.FocusEvent ) => {
|
||||||
|
if ( isEventOutside( event ) ) {
|
||||||
|
setIsFocused( false );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onKeyDown,
|
||||||
placeholder,
|
placeholder,
|
||||||
disabled,
|
disabled,
|
||||||
...inputProps,
|
...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 {
|
.woocommerce-experimental-select-control__selected-item {
|
||||||
margin-right: $gap-smallest;
|
margin-right: $gap-smallest;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { createElement, Fragment } from '@wordpress/element';
|
import classnames from 'classnames';
|
||||||
|
import { createElement } from '@wordpress/element';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -10,6 +11,7 @@ import Tag from '../tag';
|
||||||
import { getItemLabelType, getItemValueType } from './types';
|
import { getItemLabelType, getItemValueType } from './types';
|
||||||
|
|
||||||
type SelectedItemsProps< ItemType > = {
|
type SelectedItemsProps< ItemType > = {
|
||||||
|
isReadOnly: boolean;
|
||||||
items: ItemType[];
|
items: ItemType[];
|
||||||
getItemLabel: getItemLabelType< ItemType >;
|
getItemLabel: getItemLabelType< ItemType >;
|
||||||
getItemValue: getItemValueType< ItemType >;
|
getItemValue: getItemValueType< ItemType >;
|
||||||
|
@ -22,14 +24,34 @@ type SelectedItemsProps< ItemType > = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectedItems = < ItemType, >( {
|
export const SelectedItems = < ItemType, >( {
|
||||||
|
isReadOnly,
|
||||||
items,
|
items,
|
||||||
getItemLabel,
|
getItemLabel,
|
||||||
getItemValue,
|
getItemValue,
|
||||||
getSelectedItemProps,
|
getSelectedItemProps,
|
||||||
onRemove,
|
onRemove,
|
||||||
}: SelectedItemsProps< ItemType > ) => {
|
}: 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 (
|
return (
|
||||||
<>
|
<div className={ classes }>
|
||||||
{ items.map( ( item, index ) => {
|
{ items.map( ( item, index ) => {
|
||||||
return (
|
return (
|
||||||
// Disable reason: We prevent the default action to keep the input focused on click.
|
// Disable reason: We prevent the default action to keep the input focused on click.
|
||||||
|
@ -42,6 +64,9 @@ export const SelectedItems = < ItemType, >( {
|
||||||
selectedItem: item,
|
selectedItem: item,
|
||||||
index,
|
index,
|
||||||
} ) }
|
} ) }
|
||||||
|
onMouseDown={ ( event ) => {
|
||||||
|
event.preventDefault();
|
||||||
|
} }
|
||||||
onClick={ ( event ) => {
|
onClick={ ( event ) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} }
|
} }
|
||||||
|
@ -56,6 +81,6 @@ export const SelectedItems = < ItemType, >( {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} ) }
|
} ) }
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
} from '../experimental-tree-control';
|
} from '../experimental-tree-control';
|
||||||
|
|
||||||
type MenuProps = {
|
type MenuProps = {
|
||||||
|
isEventOutside: ( event: React.FocusEvent ) => boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
position?: Popover.Position;
|
position?: Popover.Position;
|
||||||
|
@ -32,6 +33,7 @@ type MenuProps = {
|
||||||
} & Omit< TreeControlProps, 'items' >;
|
} & Omit< TreeControlProps, 'items' >;
|
||||||
|
|
||||||
export const SelectTreeMenu = ( {
|
export const SelectTreeMenu = ( {
|
||||||
|
isEventOutside,
|
||||||
isLoading,
|
isLoading,
|
||||||
isOpen,
|
isOpen,
|
||||||
className,
|
className,
|
||||||
|
@ -103,8 +105,10 @@ export const SelectTreeMenu = ( {
|
||||||
) }
|
) }
|
||||||
position={ position }
|
position={ position }
|
||||||
animate={ false }
|
animate={ false }
|
||||||
onFocusOutside={ () => {
|
onFocusOutside={ ( event ) => {
|
||||||
onClose();
|
if ( isEventOutside( event ) ) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
{ isOpen && (
|
{ isOpen && (
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { createElement, useState } from '@wordpress/element';
|
import { chevronDown } from '@wordpress/icons';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { search } from '@wordpress/icons';
|
import { createElement, useState } from '@wordpress/element';
|
||||||
import { useInstanceId } from '@wordpress/compose';
|
import { useInstanceId } from '@wordpress/compose';
|
||||||
|
import { BaseControl, TextControl } from '@wordpress/components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -19,8 +19,7 @@ import { SelectTreeMenu } from './select-tree-menu';
|
||||||
|
|
||||||
interface SelectTreeProps extends TreeControlProps {
|
interface SelectTreeProps extends TreeControlProps {
|
||||||
id: string;
|
id: string;
|
||||||
selected?: Item[];
|
selected?: Item | Item[];
|
||||||
getSelectedItemProps?: any;
|
|
||||||
treeRef?: React.ForwardedRef< HTMLOListElement >;
|
treeRef?: React.ForwardedRef< HTMLOListElement >;
|
||||||
suffix?: JSX.Element | null;
|
suffix?: JSX.Element | null;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
@ -30,9 +29,8 @@ interface SelectTreeProps extends TreeControlProps {
|
||||||
|
|
||||||
export const SelectTree = function SelectTree( {
|
export const SelectTree = function SelectTree( {
|
||||||
items,
|
items,
|
||||||
getSelectedItemProps,
|
|
||||||
treeRef: ref,
|
treeRef: ref,
|
||||||
suffix = <SuffixIcon icon={ search } />,
|
suffix = <SuffixIcon icon={ chevronDown } />,
|
||||||
placeholder,
|
placeholder,
|
||||||
isLoading,
|
isLoading,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
|
@ -40,114 +38,171 @@ export const SelectTree = function SelectTree( {
|
||||||
...props
|
...props
|
||||||
}: SelectTreeProps ) {
|
}: SelectTreeProps ) {
|
||||||
const linkedTree = useLinkedTree( items );
|
const linkedTree = useLinkedTree( items );
|
||||||
|
const selectTreeInstanceId = useInstanceId(
|
||||||
|
SelectTree,
|
||||||
|
'woocommerce-experimental-select-tree-control__dropdown'
|
||||||
|
);
|
||||||
const menuInstanceId = useInstanceId(
|
const menuInstanceId = useInstanceId(
|
||||||
SelectTree,
|
SelectTree,
|
||||||
'woocommerce-select-tree-control__menu'
|
'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 [ isFocused, setIsFocused ] = useState( false );
|
||||||
const [ isOpen, setIsOpen ] = 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="woocommerce-experimental-select-tree-control__dropdown"
|
className={ `woocommerce-experimental-select-tree-control__dropdown ${ selectTreeInstanceId }` }
|
||||||
tabIndex={ -1 }
|
tabIndex={ -1 }
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={ classNames(
|
className={ classNames(
|
||||||
'woocommerce-experimental-select-control',
|
'woocommerce-experimental-select-control',
|
||||||
{
|
{
|
||||||
|
'is-read-only': isReadOnly,
|
||||||
'is-focused': isFocused,
|
'is-focused': isFocused,
|
||||||
|
'is-multiple': props.multiple,
|
||||||
|
'has-selected-items':
|
||||||
|
Array.isArray( props.selected ) &&
|
||||||
|
props.selected.length,
|
||||||
}
|
}
|
||||||
) }
|
) }
|
||||||
>
|
>
|
||||||
<label
|
<BaseControl label={ props.label } id={ `${ props.id }-input` }>
|
||||||
htmlFor={ `${ props.id }-input` }
|
{ props.multiple ? (
|
||||||
id={ `${ props.id }-label` }
|
<ComboBox
|
||||||
className="woocommerce-experimental-select-control__label"
|
comboBoxProps={ {
|
||||||
>
|
className:
|
||||||
{ props.label }
|
'woocommerce-experimental-select-control__combo-box-wrapper',
|
||||||
</label>
|
role: 'combobox',
|
||||||
<ComboBox
|
'aria-expanded': isOpen,
|
||||||
comboBoxProps={ {
|
'aria-haspopup': 'tree',
|
||||||
className:
|
'aria-owns': `${ props.id }-menu`,
|
||||||
'woocommerce-experimental-select-control__combo-box-wrapper',
|
} }
|
||||||
role: 'combobox',
|
inputProps={ inputProps }
|
||||||
'aria-expanded': isOpen,
|
suffix={ suffix }
|
||||||
'aria-haspopup': 'tree',
|
>
|
||||||
'aria-labelledby': `${ props.id }-label`,
|
<SelectedItems
|
||||||
'aria-owns': `${ props.id }-menu`,
|
isReadOnly={ isReadOnly }
|
||||||
} }
|
items={ ( props.selected as Item[] ) || [] }
|
||||||
inputProps={ {
|
getItemLabel={ ( item ) => item?.label || '' }
|
||||||
className:
|
getItemValue={ ( item ) => item?.value || '' }
|
||||||
'woocommerce-experimental-select-control__input',
|
onRemove={ ( item ) => {
|
||||||
id: `${ props.id }-input`,
|
if (
|
||||||
'aria-autocomplete': 'list',
|
! Array.isArray( item ) &&
|
||||||
'aria-controls': `${ props.id }-menu`,
|
props.onRemove
|
||||||
autoComplete: 'off',
|
) {
|
||||||
onFocus: () => {
|
props.onRemove( item );
|
||||||
if ( ! isOpen ) {
|
}
|
||||||
setIsOpen( true );
|
} }
|
||||||
}
|
getSelectedItemProps={ () => ( {} ) }
|
||||||
setIsFocused( true );
|
/>
|
||||||
},
|
</ComboBox>
|
||||||
onBlur: ( event ) => {
|
) : (
|
||||||
// if blurring to an element inside the dropdown, don't close it
|
<TextControl
|
||||||
if (
|
{ ...inputProps }
|
||||||
isOpen &&
|
value={ props.createValue || '' }
|
||||||
! document
|
onChange={ ( value ) => {
|
||||||
.querySelector( '.' + menuInstanceId )
|
if ( onInputChange ) onInputChange( value );
|
||||||
?.contains( event.relatedTarget )
|
const item = items.find(
|
||||||
) {
|
( i ) => i.label === value
|
||||||
setIsOpen( false );
|
);
|
||||||
}
|
if ( props.onSelect && item ) {
|
||||||
setIsFocused( false );
|
props.onSelect( item );
|
||||||
},
|
}
|
||||||
onKeyDown: ( event ) => {
|
if ( ! value && props.onRemove ) {
|
||||||
setIsOpen( true );
|
props.onRemove( props.selected as Item );
|
||||||
if ( event.key === 'ArrowDown' ) {
|
}
|
||||||
event.preventDefault();
|
} }
|
||||||
// focus on the first element from the Popover
|
/>
|
||||||
(
|
) }
|
||||||
document.querySelector(
|
</BaseControl>
|
||||||
`.${ menuInstanceId } input, .${ menuInstanceId } button`
|
|
||||||
) as HTMLInputElement | HTMLButtonElement
|
|
||||||
)?.focus();
|
|
||||||
}
|
|
||||||
if ( event.key === 'Tab' ) {
|
|
||||||
setIsOpen( false );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onChange: ( event ) =>
|
|
||||||
onInputChange &&
|
|
||||||
onInputChange( event.target.value ),
|
|
||||||
placeholder,
|
|
||||||
} }
|
|
||||||
suffix={ suffix }
|
|
||||||
>
|
|
||||||
<SelectedItems
|
|
||||||
items={ ( props.selected as Item[] ) || [] }
|
|
||||||
getItemLabel={ ( item ) => item?.label || '' }
|
|
||||||
getItemValue={ ( item ) => item?.value || '' }
|
|
||||||
onRemove={ ( item ) => {
|
|
||||||
if ( ! Array.isArray( item ) && props.onRemove ) {
|
|
||||||
props.onRemove( item );
|
|
||||||
setIsOpen( false );
|
|
||||||
}
|
|
||||||
} }
|
|
||||||
getSelectedItemProps={ () => ( {} ) }
|
|
||||||
/>
|
|
||||||
</ComboBox>
|
|
||||||
</div>
|
</div>
|
||||||
<SelectTreeMenu
|
<SelectTreeMenu
|
||||||
{ ...props }
|
{ ...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` }
|
id={ `${ props.id }-menu` }
|
||||||
className={ menuInstanceId.toString() }
|
className={ menuInstanceId.toString() }
|
||||||
ref={ ref }
|
ref={ ref }
|
||||||
|
isEventOutside={ isEventOutside }
|
||||||
isOpen={ isOpen }
|
isOpen={ isOpen }
|
||||||
items={ linkedTree }
|
items={ linkedTree }
|
||||||
shouldShowCreateButton={ shouldShowCreateButton }
|
shouldShowCreateButton={ shouldShowCreateButton }
|
||||||
onClose={ () => setIsOpen( false ) }
|
onClose={ () => {
|
||||||
|
setIsOpen( false );
|
||||||
|
} }
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 {
|
export default {
|
||||||
title: 'WooCommerce Admin/experimental/SelectTreeControl',
|
title: 'WooCommerce Admin/experimental/SelectTreeControl',
|
||||||
component: SelectTree,
|
component: SelectTree,
|
||||||
|
|
|
@ -122,8 +122,6 @@ export function useSelection( {
|
||||||
if ( item.children.length && ! shouldNotRecursivelySelect ) {
|
if ( item.children.length && ! shouldNotRecursivelySelect ) {
|
||||||
value.push( ...getDeepChildren( item ) );
|
value.push( ...getDeepChildren( item ) );
|
||||||
}
|
}
|
||||||
} else if ( item.children?.length ) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( checked ) {
|
if ( checked ) {
|
||||||
|
|
|
@ -45,10 +45,6 @@ $control-size: $gap-large;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.components-radio-control__input {
|
|
||||||
@include screen-reader-only();
|
|
||||||
}
|
|
||||||
|
|
||||||
.components-checkbox-control__label {
|
.components-checkbox-control__label {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -86,4 +82,8 @@ $control-size: $gap-large;
|
||||||
min-width: $control-size;
|
min-width: $control-size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__checkbox {
|
||||||
|
@include screen-reader-only();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,13 +60,11 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="checkbox"
|
||||||
|
className="experimental-woocommerce-tree-item__checkbox"
|
||||||
checked={ selection.checkedStatus === 'checked' }
|
checked={ selection.checkedStatus === 'checked' }
|
||||||
className="components-radio-control__input"
|
|
||||||
onChange={ ( event ) =>
|
onChange={ ( event ) =>
|
||||||
selection.onSelectChild(
|
selection.onSelectChild( event.target.checked )
|
||||||
event.currentTarget.checked
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
|
|
|
@ -33,34 +33,36 @@ export const Tree = forwardRef( function ForwardedTree(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ol
|
{ items.length || isCreateButtonVisible ? (
|
||||||
{ ...treeProps }
|
<ol
|
||||||
className={ classNames(
|
{ ...treeProps }
|
||||||
treeProps.className,
|
className={ classNames(
|
||||||
'experimental-woocommerce-tree',
|
treeProps.className,
|
||||||
`experimental-woocommerce-tree--level-${ level }`
|
'experimental-woocommerce-tree',
|
||||||
) }
|
`experimental-woocommerce-tree--level-${ level }`
|
||||||
>
|
) }
|
||||||
{ items.map( ( child, index ) => (
|
>
|
||||||
<TreeItem
|
{ items.map( ( child, index ) => (
|
||||||
{ ...treeItemProps }
|
<TreeItem
|
||||||
isExpanded={ props.isExpanded }
|
{ ...treeItemProps }
|
||||||
key={ child.data.value }
|
isExpanded={ props.isExpanded }
|
||||||
item={ child }
|
key={ child.data.value }
|
||||||
index={ index }
|
item={ child }
|
||||||
// Button ref is not working, so need to use CSS directly
|
index={ index }
|
||||||
onLastItemLoop={ () => {
|
// Button ref is not working, so need to use CSS directly
|
||||||
(
|
onLastItemLoop={ () => {
|
||||||
rootListRef.current
|
(
|
||||||
?.closest( 'ol[role="tree"]' )
|
rootListRef.current
|
||||||
?.parentElement?.querySelector(
|
?.closest( 'ol[role="tree"]' )
|
||||||
'.experimental-woocommerce-tree__button'
|
?.parentElement?.querySelector(
|
||||||
) as HTMLButtonElement
|
'.experimental-woocommerce-tree__button'
|
||||||
)?.focus();
|
) as HTMLButtonElement
|
||||||
} }
|
)?.focus();
|
||||||
/>
|
} }
|
||||||
) ) }
|
/>
|
||||||
</ol>
|
) ) }
|
||||||
|
</ol>
|
||||||
|
) : null }
|
||||||
{ isCreateButtonVisible && (
|
{ isCreateButtonVisible && (
|
||||||
<Button
|
<Button
|
||||||
className="experimental-woocommerce-tree__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.
|
- `label`: String - A label above the filter selector.
|
||||||
- `staticParams`: Array - Url parameters to persist when selecting a new filter.
|
- `staticParams`: Array - Url parameters to persist when selecting a new filter.
|
||||||
- `param`: String - The url paramter this filter will modify.
|
- `param`: String - The url parameter this filter will modify.
|
||||||
- `defaultValue`: String - The default paramter value to use instead of 'all'.
|
- `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.
|
- `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.
|
- `filters`: Array - Array of filter objects.
|
||||||
|
|
||||||
|
|
|
@ -374,11 +374,11 @@ FilterPicker.propTypes = {
|
||||||
*/
|
*/
|
||||||
staticParams: PropTypes.array.isRequired,
|
staticParams: PropTypes.array.isRequired,
|
||||||
/**
|
/**
|
||||||
* The url paramter this filter will modify.
|
* The url parameter this filter will modify.
|
||||||
*/
|
*/
|
||||||
param: PropTypes.string.isRequired,
|
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,
|
defaultValue: PropTypes.string,
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { MediaUploadComponentType } from './types';
|
||||||
|
|
||||||
export type ImageGalleryToolbarProps = {
|
export type ImageGalleryToolbarProps = {
|
||||||
childIndex: number;
|
childIndex: number;
|
||||||
|
allowDragging?: boolean;
|
||||||
moveItem: ( fromIndex: number, toIndex: number ) => void;
|
moveItem: ( fromIndex: number, toIndex: number ) => void;
|
||||||
removeItem: ( removeIndex: number ) => void;
|
removeItem: ( removeIndex: number ) => void;
|
||||||
replaceItem: (
|
replaceItem: (
|
||||||
|
@ -29,6 +30,7 @@ export type ImageGalleryToolbarProps = {
|
||||||
|
|
||||||
export const ImageGalleryToolbar: React.FC< ImageGalleryToolbarProps > = ( {
|
export const ImageGalleryToolbar: React.FC< ImageGalleryToolbarProps > = ( {
|
||||||
childIndex,
|
childIndex,
|
||||||
|
allowDragging = true,
|
||||||
moveItem,
|
moveItem,
|
||||||
removeItem,
|
removeItem,
|
||||||
replaceItem,
|
replaceItem,
|
||||||
|
@ -60,12 +62,14 @@ export const ImageGalleryToolbar: React.FC< ImageGalleryToolbarProps > = ( {
|
||||||
>
|
>
|
||||||
{ ! isCoverItem && (
|
{ ! isCoverItem && (
|
||||||
<ToolbarGroup>
|
<ToolbarGroup>
|
||||||
<ToolbarButton
|
{ allowDragging && (
|
||||||
icon={ () => (
|
<ToolbarButton
|
||||||
<SortableHandle itemIndex={ childIndex } />
|
icon={ () => (
|
||||||
) }
|
<SortableHandle itemIndex={ childIndex } />
|
||||||
label={ __( 'Drag to reorder', 'woocommerce' ) }
|
) }
|
||||||
/>
|
label={ __( 'Drag to reorder', 'woocommerce' ) }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
disabled={ childIndex < 2 }
|
disabled={ childIndex < 2 }
|
||||||
onClick={ () => movePrevious() }
|
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;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-sortable {
|
.woocommerce-sortable, &__wrapper {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: inherit;
|
grid-template-columns: inherit;
|
||||||
grid-gap: $gap;
|
grid-gap: $gap;
|
||||||
|
|
|
@ -14,10 +14,11 @@ import { MediaItem, MediaUpload } from '@wordpress/media-utils';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { Sortable, moveIndex } from '../sortable';
|
import { moveIndex } from '../sortable';
|
||||||
import { ImageGalleryToolbar } from './index';
|
import { ImageGalleryToolbar } from './index';
|
||||||
import { ImageGalleryChild, MediaUploadComponentType } from './types';
|
import { ImageGalleryChild, MediaUploadComponentType } from './types';
|
||||||
import { removeItem, replaceItem } from './utils';
|
import { removeItem, replaceItem } from './utils';
|
||||||
|
import { ImageGalleryWrapper } from './image-gallery-wrapper';
|
||||||
|
|
||||||
export type ImageGalleryProps = {
|
export type ImageGalleryProps = {
|
||||||
children: ImageGalleryChild | ImageGalleryChild[];
|
children: ImageGalleryChild | ImageGalleryChild[];
|
||||||
|
@ -30,6 +31,7 @@ export type ImageGalleryProps = {
|
||||||
replaceIndex: number;
|
replaceIndex: number;
|
||||||
media: { id: number } & MediaItem;
|
media: { id: number } & MediaItem;
|
||||||
} ) => void;
|
} ) => void;
|
||||||
|
allowDragging?: boolean;
|
||||||
onSelectAsCover?: ( itemId: string | null ) => void;
|
onSelectAsCover?: ( itemId: string | null ) => void;
|
||||||
onOrderChange?: ( items: ImageGalleryChild[] ) => void;
|
onOrderChange?: ( items: ImageGalleryChild[] ) => void;
|
||||||
MediaUploadComponent?: MediaUploadComponentType;
|
MediaUploadComponent?: MediaUploadComponentType;
|
||||||
|
@ -41,6 +43,7 @@ export type ImageGalleryProps = {
|
||||||
export const ImageGallery: React.FC< ImageGalleryProps > = ( {
|
export const ImageGallery: React.FC< ImageGalleryProps > = ( {
|
||||||
children,
|
children,
|
||||||
columns = 4,
|
columns = 4,
|
||||||
|
allowDragging = true,
|
||||||
onSelectAsCover = () => null,
|
onSelectAsCover = () => null,
|
||||||
onOrderChange = () => null,
|
onOrderChange = () => null,
|
||||||
onRemove = () => null,
|
onRemove = () => null,
|
||||||
|
@ -82,11 +85,9 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( {
|
||||||
gridTemplateColumns: 'min-content '.repeat( columns ),
|
gridTemplateColumns: 'min-content '.repeat( columns ),
|
||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
<Sortable
|
<ImageGalleryWrapper
|
||||||
isHorizontal
|
allowDragging={ allowDragging }
|
||||||
onOrderChange={ ( items ) => {
|
updateOrderedChildren={ updateOrderedChildren }
|
||||||
updateOrderedChildren( items );
|
|
||||||
} }
|
|
||||||
onDragStart={ ( event ) => {
|
onDragStart={ ( event ) => {
|
||||||
setIsDragging( true );
|
setIsDragging( true );
|
||||||
onDragStart( event );
|
onDragStart( event );
|
||||||
|
@ -137,6 +138,7 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( {
|
||||||
},
|
},
|
||||||
isToolbarVisible ? (
|
isToolbarVisible ? (
|
||||||
<ImageGalleryToolbar
|
<ImageGalleryToolbar
|
||||||
|
allowDragging={ allowDragging }
|
||||||
childIndex={ childIndex }
|
childIndex={ childIndex }
|
||||||
lastChild={
|
lastChild={
|
||||||
childIndex === orderedChildren.length - 1
|
childIndex === orderedChildren.length - 1
|
||||||
|
@ -190,7 +192,7 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( {
|
||||||
) : null
|
) : null
|
||||||
);
|
);
|
||||||
} ) }
|
} ) }
|
||||||
</Sortable>
|
</ImageGalleryWrapper>
|
||||||
</div>
|
</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
|
// Changing to a mode other than "edit" ensures that no initial position
|
||||||
// is found and no element gets subsequently focused.
|
// 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
|
// 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 = [
|
const filteredList = [
|
||||||
{ id: 1, name: 'Apricots', parent: 0 },
|
{ id: 1, name: 'Apricots', parent: 0 },
|
||||||
{ id: 3, name: 'Elderberry', parent: 2 },
|
{ 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 { createElement, Component, createRef } from '@wordpress/element';
|
||||||
import { Icon, search } from '@wordpress/icons';
|
import { Icon, search } from '@wordpress/icons';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import { isArray } from 'lodash';
|
||||||
|
import {
|
||||||
|
RefObject,
|
||||||
|
ChangeEvent,
|
||||||
|
FocusEvent,
|
||||||
|
KeyboardEvent,
|
||||||
|
InputHTMLAttributes,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import Tags from './tags';
|
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.
|
* A search control to allow user input to filter the options.
|
||||||
*/
|
*/
|
||||||
class Control extends Component {
|
class Control extends Component< Props, State > {
|
||||||
constructor( props ) {
|
input: RefObject< HTMLInputElement >;
|
||||||
|
|
||||||
|
constructor( props: Props ) {
|
||||||
super( props );
|
super( props );
|
||||||
this.state = {
|
this.state = {
|
||||||
isActive: false,
|
isActive: false,
|
||||||
|
@ -31,13 +162,13 @@ class Control extends Component {
|
||||||
this.onKeyDown = this.onKeyDown.bind( this );
|
this.onKeyDown = this.onKeyDown.bind( this );
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSearch( onSearch ) {
|
updateSearch( onSearch: ( query: string ) => void ) {
|
||||||
return ( event ) => {
|
return ( event: ChangeEvent< HTMLInputElement > ) => {
|
||||||
onSearch( event.target.value );
|
onSearch( event.target.value );
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onFocus( onSearch ) {
|
onFocus( onSearch: ( query: string ) => void ) {
|
||||||
const {
|
const {
|
||||||
isSearchable,
|
isSearchable,
|
||||||
setExpanded,
|
setExpanded,
|
||||||
|
@ -45,7 +176,7 @@ class Control extends Component {
|
||||||
updateSearchOptions,
|
updateSearchOptions,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return ( event ) => {
|
return ( event: FocusEvent< HTMLInputElement > ) => {
|
||||||
this.setState( { isActive: true } );
|
this.setState( { isActive: true } );
|
||||||
if ( isSearchable && showAllOnFocus ) {
|
if ( isSearchable && showAllOnFocus ) {
|
||||||
event.target.select();
|
event.target.select();
|
||||||
|
@ -68,7 +199,7 @@ class Control extends Component {
|
||||||
this.setState( { isActive: false } );
|
this.setState( { isActive: false } );
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown( event ) {
|
onKeyDown( event: KeyboardEvent< HTMLInputElement > ) {
|
||||||
const {
|
const {
|
||||||
decrementSelectedIndex,
|
decrementSelectedIndex,
|
||||||
incrementSelectedIndex,
|
incrementSelectedIndex,
|
||||||
|
@ -78,7 +209,12 @@ class Control extends Component {
|
||||||
setExpanded,
|
setExpanded,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if ( BACKSPACE === event.keyCode && ! query && selected.length ) {
|
if (
|
||||||
|
BACKSPACE === event.keyCode &&
|
||||||
|
! query &&
|
||||||
|
isArray( selected ) &&
|
||||||
|
selected.length
|
||||||
|
) {
|
||||||
onChange( [ ...selected.slice( 0, -1 ) ] );
|
onChange( [ ...selected.slice( 0, -1 ) ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +236,7 @@ class Control extends Component {
|
||||||
renderButton() {
|
renderButton() {
|
||||||
const { multiple, selected } = this.props;
|
const { multiple, selected } = this.props;
|
||||||
|
|
||||||
if ( multiple || ! selected.length ) {
|
if ( multiple || ! isArray( selected ) || ! selected.length ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +287,7 @@ class Control extends Component {
|
||||||
aria-describedby={
|
aria-describedby={
|
||||||
hasTags && inlineTags
|
hasTags && inlineTags
|
||||||
? `search-inline-input-${ instanceId }`
|
? `search-inline-input-${ instanceId }`
|
||||||
: null
|
: undefined
|
||||||
}
|
}
|
||||||
disabled={ disabled }
|
disabled={ disabled }
|
||||||
aria-label={ this.props.ariaLabel ?? this.props.label }
|
aria-label={ this.props.ariaLabel ?? this.props.label }
|
||||||
|
@ -168,7 +304,8 @@ class Control extends Component {
|
||||||
query,
|
query,
|
||||||
selected,
|
selected,
|
||||||
} = this.props;
|
} = 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.
|
// Show the selected value for simple select dropdowns.
|
||||||
if ( ! multiple && ! isFocused && ! inlineTags ) {
|
if ( ! multiple && ! isFocused && ! inlineTags ) {
|
||||||
|
@ -194,6 +331,8 @@ class Control extends Component {
|
||||||
isSearchable,
|
isSearchable,
|
||||||
label,
|
label,
|
||||||
query,
|
query,
|
||||||
|
onChange,
|
||||||
|
showClearButton,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { isActive } = this.state;
|
const { isActive } = this.state;
|
||||||
|
|
||||||
|
@ -213,7 +352,7 @@ class Control extends Component {
|
||||||
empty: ! query || query.length === 0,
|
empty: ! query || query.length === 0,
|
||||||
'is-active': isActive,
|
'is-active': isActive,
|
||||||
'has-tags': inlineTags && hasTags,
|
'has-tags': inlineTags && hasTags,
|
||||||
'with-value': this.getInputValue().length,
|
'with-value': this.getInputValue()?.length,
|
||||||
'has-error': !! help,
|
'has-error': !! help,
|
||||||
'is-disabled': disabled,
|
'is-disabled': disabled,
|
||||||
}
|
}
|
||||||
|
@ -221,8 +360,11 @@ class Control extends Component {
|
||||||
onClick={ ( event ) => {
|
onClick={ ( event ) => {
|
||||||
// Don't focus the input if the click event is from the error message.
|
// Don't focus the input if the click event is from the error message.
|
||||||
if (
|
if (
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore - event.target.className is not in the type definition.
|
||||||
event.target.className !==
|
event.target.className !==
|
||||||
'components-base-control__help'
|
'components-base-control__help' &&
|
||||||
|
this.input.current
|
||||||
) {
|
) {
|
||||||
this.input.current.focus();
|
this.input.current.focus();
|
||||||
}
|
}
|
||||||
|
@ -234,7 +376,13 @@ class Control extends Component {
|
||||||
icon={ search }
|
icon={ search }
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
{ inlineTags && <Tags { ...this.props } /> }
|
{ inlineTags && (
|
||||||
|
<Tags
|
||||||
|
onChange={ onChange }
|
||||||
|
showClearButton={ showClearButton }
|
||||||
|
selected={ this.props.selected }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
|
||||||
<div className="components-base-control__field">
|
<div className="components-base-control__field">
|
||||||
{ !! label && (
|
{ !! 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;
|
export default Control;
|
|
@ -4,26 +4,205 @@
|
||||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Component, createElement } from '@wordpress/element';
|
import { Component, createElement } from '@wordpress/element';
|
||||||
import { debounce, escapeRegExp, identity, noop } from 'lodash';
|
import {
|
||||||
import PropTypes from 'prop-types';
|
debounce,
|
||||||
|
escapeRegExp,
|
||||||
|
identity,
|
||||||
|
isArray,
|
||||||
|
isNumber,
|
||||||
|
noop,
|
||||||
|
} from 'lodash';
|
||||||
import { withFocusOutside, withSpokenMessages } from '@wordpress/components';
|
import { withFocusOutside, withSpokenMessages } from '@wordpress/components';
|
||||||
import { withInstanceId, compose } from '@wordpress/compose';
|
import { withInstanceId, compose } from '@wordpress/compose';
|
||||||
|
import { ChangeEvent, InputHTMLAttributes } from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
import { Option, Selected } from './types';
|
||||||
import List from './list';
|
import List from './list';
|
||||||
import Tags from './tags';
|
import Tags from './tags';
|
||||||
import Control from './control';
|
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,
|
* A search box which filters options while typing,
|
||||||
* allowing a user to select from an option from a filtered list.
|
* allowing a user to select from an option from a filtered list.
|
||||||
*/
|
*/
|
||||||
export class SelectControl extends Component {
|
export class SelectControl extends Component< Props, State > {
|
||||||
constructor( props ) {
|
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 );
|
super( props );
|
||||||
|
|
||||||
const { selected, options, excludeSelectedOptions } = props;
|
const { selected, options, excludeSelectedOptions } = props;
|
||||||
|
@ -50,7 +229,7 @@ export class SelectControl extends Component {
|
||||||
this.setNewValue = this.setNewValue.bind( this );
|
this.setNewValue = this.setNewValue.bind( this );
|
||||||
}
|
}
|
||||||
|
|
||||||
bindNode( node ) {
|
bindNode( node: HTMLDivElement ) {
|
||||||
this.node = node;
|
this.node = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +237,12 @@ export class SelectControl extends Component {
|
||||||
const { multiple, excludeSelectedOptions } = this.props;
|
const { multiple, excludeSelectedOptions } = this.props;
|
||||||
const newState = { ...initialState };
|
const newState = { ...initialState };
|
||||||
// Reset selectedIndex if single selection.
|
// Reset selectedIndex if single selection.
|
||||||
if ( ! multiple && selected.length && selected[ 0 ].key ) {
|
if (
|
||||||
|
! multiple &&
|
||||||
|
isArray( selected ) &&
|
||||||
|
selected.length &&
|
||||||
|
selected[ 0 ].key
|
||||||
|
) {
|
||||||
newState.selectedIndex = ! excludeSelectedOptions
|
newState.selectedIndex = ! excludeSelectedOptions
|
||||||
? this.props.options.findIndex(
|
? this.props.options.findIndex(
|
||||||
( i ) => i.key === selected[ 0 ].key
|
( i ) => i.key === selected[ 0 ].key
|
||||||
|
@ -101,9 +285,12 @@ export class SelectControl extends Component {
|
||||||
return selectedOption ? [ selectedOption ] : [];
|
return selectedOption ? [ selectedOption ] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
selectOption( option ) {
|
selectOption( option: Option ) {
|
||||||
const { multiple, selected } = this.props;
|
const { multiple, selected } = this.props;
|
||||||
const newSelected = multiple ? [ ...selected, option ] : [ option ];
|
const newSelected =
|
||||||
|
multiple && isArray( selected )
|
||||||
|
? [ ...selected, option ]
|
||||||
|
: [ option ];
|
||||||
|
|
||||||
this.reset( newSelected );
|
this.reset( newSelected );
|
||||||
|
|
||||||
|
@ -129,25 +316,24 @@ export class SelectControl extends Component {
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
setNewValue( newValue ) {
|
setNewValue( newValue: Option[] ) {
|
||||||
const { onChange, selected, multiple } = this.props;
|
const { onChange, selected, multiple } = this.props;
|
||||||
const { query } = this.state;
|
const { query } = this.state;
|
||||||
// Trigger a change if the selected value is different and pass back
|
// Trigger a change if the selected value is different and pass back
|
||||||
// an array or string depending on the original value.
|
// an array or string depending on the original value.
|
||||||
if ( multiple || Array.isArray( selected ) ) {
|
if ( multiple || Array.isArray( selected ) ) {
|
||||||
onChange( newValue, query );
|
onChange!( newValue, query );
|
||||||
} else {
|
} else {
|
||||||
onChange( newValue.length > 0 ? newValue[ 0 ].key : '', query );
|
onChange!( newValue.length > 0 ? newValue[ 0 ].key : '', query );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
decrementSelectedIndex() {
|
decrementSelectedIndex() {
|
||||||
const { selectedIndex } = this.state;
|
const { selectedIndex } = this.state;
|
||||||
const options = this.getOptions();
|
const options = this.getOptions();
|
||||||
const nextSelectedIndex =
|
const nextSelectedIndex = isNumber( selectedIndex )
|
||||||
selectedIndex !== null
|
? ( selectedIndex === 0 ? options.length : selectedIndex ) - 1
|
||||||
? ( selectedIndex === 0 ? options.length : selectedIndex ) - 1
|
: options.length - 1;
|
||||||
: options.length - 1;
|
|
||||||
|
|
||||||
this.setState( { selectedIndex: nextSelectedIndex } );
|
this.setState( { selectedIndex: nextSelectedIndex } );
|
||||||
}
|
}
|
||||||
|
@ -155,13 +341,14 @@ export class SelectControl extends Component {
|
||||||
incrementSelectedIndex() {
|
incrementSelectedIndex() {
|
||||||
const { selectedIndex } = this.state;
|
const { selectedIndex } = this.state;
|
||||||
const options = this.getOptions();
|
const options = this.getOptions();
|
||||||
const nextSelectedIndex =
|
const nextSelectedIndex = isNumber( selectedIndex )
|
||||||
selectedIndex !== null ? ( selectedIndex + 1 ) % options.length : 0;
|
? ( selectedIndex + 1 ) % options.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
this.setState( { selectedIndex: nextSelectedIndex } );
|
this.setState( { selectedIndex: nextSelectedIndex } );
|
||||||
}
|
}
|
||||||
|
|
||||||
announce( searchOptions ) {
|
announce( searchOptions: Option[] ) {
|
||||||
const { debouncedSpeak } = this.props;
|
const { debouncedSpeak } = this.props;
|
||||||
if ( ! debouncedSpeak ) {
|
if ( ! debouncedSpeak ) {
|
||||||
return;
|
return;
|
||||||
|
@ -169,6 +356,7 @@ export class SelectControl extends Component {
|
||||||
if ( !! searchOptions.length ) {
|
if ( !! searchOptions.length ) {
|
||||||
debouncedSpeak(
|
debouncedSpeak(
|
||||||
sprintf(
|
sprintf(
|
||||||
|
// translators: %d: number of results.
|
||||||
_n(
|
_n(
|
||||||
'%d result found, use up and down arrow keys to navigate.',
|
'%d result found, use up and down arrow keys to navigate.',
|
||||||
'%d results 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() {
|
getOptions() {
|
||||||
const { isSearchable, options, excludeSelectedOptions } = this.props;
|
const { isSearchable, options, excludeSelectedOptions } = this.props;
|
||||||
const { searchOptions } = this.state;
|
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;
|
const shownOptions = isSearchable ? searchOptions : options;
|
||||||
|
|
||||||
if ( excludeSelectedOptions ) {
|
if ( excludeSelectedOptions ) {
|
||||||
return shownOptions.filter(
|
return shownOptions?.filter(
|
||||||
( option ) => ! selectedKeys.includes( option.key )
|
( option ) => ! selectedKeys.includes( option.key )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return shownOptions;
|
return shownOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getOptionsByQuery( options, query ) {
|
getOptionsByQuery( options: Option[], query: string | null ) {
|
||||||
const { getSearchExpression, maxResults, onFilter } = this.props;
|
const { getSearchExpression, maxResults, onFilter } = this.props;
|
||||||
const filtered = [];
|
const filtered = [];
|
||||||
|
|
||||||
// Create a regular expression to filter the options.
|
// Create a regular expression to filter the options.
|
||||||
const expression = getSearchExpression(
|
const expression = getSearchExpression!(
|
||||||
escapeRegExp( query ? query.trim() : '' )
|
escapeRegExp( query ? query.trim() : '' )
|
||||||
);
|
);
|
||||||
const search = expression ? new RegExp( expression, 'i' ) : /^$/;
|
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 } );
|
this.setState( { isExpanded: value } );
|
||||||
}
|
}
|
||||||
|
|
||||||
search( query ) {
|
search( query: string | null ) {
|
||||||
const cacheSearchOptions = this.cacheSearchOptions || [];
|
const cacheSearchOptions = this.cacheSearchOptions || [];
|
||||||
const searchOptions =
|
const searchOptions =
|
||||||
query !== null && ! query.length && ! this.props.hideBeforeSearch
|
query !== null && ! query.length && ! this.props.hideBeforeSearch
|
||||||
|
@ -252,11 +443,13 @@ export class SelectControl extends Component {
|
||||||
isFocused: true,
|
isFocused: true,
|
||||||
searchOptions,
|
searchOptions,
|
||||||
selectedIndex:
|
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( {
|
this.setState( {
|
||||||
isExpanded: Boolean( this.getOptions().length ),
|
isExpanded: Boolean( this.getOptions()?.length ),
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -264,11 +457,11 @@ export class SelectControl extends Component {
|
||||||
this.updateSearchOptions( query );
|
this.updateSearchOptions( query );
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSearchOptions( query ) {
|
updateSearchOptions( query: string | null ) {
|
||||||
const { hideBeforeSearch, options, onSearch } = this.props;
|
const { hideBeforeSearch, options, onSearch } = this.props;
|
||||||
|
|
||||||
const promise = ( this.activePromise = Promise.resolve(
|
const promise = ( this.activePromise = Promise.resolve(
|
||||||
onSearch( options, query )
|
onSearch!( options, query )
|
||||||
).then( ( promiseOptions ) => {
|
).then( ( promiseOptions ) => {
|
||||||
if ( promise !== this.activePromise ) {
|
if ( promise !== this.activePromise ) {
|
||||||
// Another promise has become active since this one was asked to resolve, so do nothing,
|
// 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,
|
searchOptions,
|
||||||
selectedIndex:
|
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( {
|
this.setState( {
|
||||||
|
@ -300,7 +495,7 @@ export class SelectControl extends Component {
|
||||||
} ) );
|
} ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
onAutofillChange( event ) {
|
onAutofillChange( event: ChangeEvent< HTMLInputElement > ) {
|
||||||
const { options } = this.props;
|
const { options } = this.props;
|
||||||
const searchOptions = this.getOptionsByQuery(
|
const searchOptions = this.getOptionsByQuery(
|
||||||
options,
|
options,
|
||||||
|
@ -327,13 +522,14 @@ export class SelectControl extends Component {
|
||||||
const { isExpanded, isFocused, selectedIndex } = this.state;
|
const { isExpanded, isFocused, selectedIndex } = this.state;
|
||||||
|
|
||||||
const hasMultiple = this.hasMultiple();
|
const hasMultiple = this.hasMultiple();
|
||||||
const { key: selectedKey = '' } = options[ selectedIndex ] || {};
|
const { key: selectedKey = '' } =
|
||||||
|
( isNumber( selectedIndex ) && options[ selectedIndex ] ) || {};
|
||||||
const listboxId = isExpanded
|
const listboxId = isExpanded
|
||||||
? `woocommerce-select-control__listbox-${ instanceId }`
|
? `woocommerce-select-control__listbox-${ instanceId }`
|
||||||
: null;
|
: undefined;
|
||||||
const activeId = isExpanded
|
const activeId = isExpanded
|
||||||
? `woocommerce-select-control__option-${ instanceId }-${ selectedKey }`
|
? `woocommerce-select-control__option-${ instanceId }-${ selectedKey }`
|
||||||
: null;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -354,13 +550,25 @@ export class SelectControl extends Component {
|
||||||
name={ autofill }
|
name={ autofill }
|
||||||
type="text"
|
type="text"
|
||||||
className="woocommerce-select-control__autofill-input"
|
className="woocommerce-select-control__autofill-input"
|
||||||
tabIndex="-1"
|
tabIndex={ -1 }
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
{ children }
|
{ children }
|
||||||
<Control
|
<Control
|
||||||
{ ...this.props }
|
help={ this.props.help }
|
||||||
{ ...this.state }
|
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 }
|
activeId={ activeId }
|
||||||
className={ controlClassName }
|
className={ controlClassName }
|
||||||
disabled={ disabled }
|
disabled={ disabled }
|
||||||
|
@ -374,15 +582,20 @@ export class SelectControl extends Component {
|
||||||
updateSearchOptions={ this.updateSearchOptions }
|
updateSearchOptions={ this.updateSearchOptions }
|
||||||
decrementSelectedIndex={ this.decrementSelectedIndex }
|
decrementSelectedIndex={ this.decrementSelectedIndex }
|
||||||
incrementSelectedIndex={ this.incrementSelectedIndex }
|
incrementSelectedIndex={ this.incrementSelectedIndex }
|
||||||
|
showClearButton={ this.props.showClearButton }
|
||||||
/>
|
/>
|
||||||
{ ! inlineTags && hasMultiple && (
|
{ ! inlineTags && hasMultiple && (
|
||||||
<Tags { ...this.props } selected={ this.getSelected() } />
|
<Tags
|
||||||
|
onChange={ this.props.onChange! }
|
||||||
|
showClearButton={ this.props.showClearButton }
|
||||||
|
selected={ this.getSelected() }
|
||||||
|
/>
|
||||||
) }
|
) }
|
||||||
{ isExpanded && (
|
{ isExpanded && (
|
||||||
<List
|
<List
|
||||||
{ ...this.props }
|
instanceId={ instanceId! }
|
||||||
{ ...this.state }
|
selectedIndex={ selectedIndex }
|
||||||
activeId={ activeId }
|
staticList={ this.props.staticList! }
|
||||||
listboxId={ listboxId }
|
listboxId={ listboxId }
|
||||||
node={ this.node }
|
node={ this.node }
|
||||||
onSelect={ this.selectOption }
|
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(
|
export default compose(
|
||||||
withSpokenMessages,
|
withSpokenMessages,
|
||||||
withInstanceId,
|
withInstanceId,
|
|
@ -2,18 +2,73 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { Button } from '@wordpress/components';
|
import { Button } from '@wordpress/components';
|
||||||
|
import { RefObject } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { createElement, Component, createRef } from '@wordpress/element';
|
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 { 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.
|
* A list box that displays filtered options after search.
|
||||||
*/
|
*/
|
||||||
class List extends Component {
|
class List extends Component< Props > {
|
||||||
constructor() {
|
optionRefs: { [ key: number ]: RefObject< HTMLButtonElement > };
|
||||||
super( ...arguments );
|
listbox: RefObject< HTMLDivElement >;
|
||||||
|
|
||||||
|
constructor( props: Props ) {
|
||||||
|
super( props );
|
||||||
|
|
||||||
this.handleKeyDown = this.handleKeyDown.bind( this );
|
this.handleKeyDown = this.handleKeyDown.bind( this );
|
||||||
this.select = this.select.bind( this );
|
this.select = this.select.bind( this );
|
||||||
|
@ -21,7 +76,7 @@ class List extends Component {
|
||||||
this.listbox = createRef();
|
this.listbox = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate( prevProps ) {
|
componentDidUpdate( prevProps: Props ) {
|
||||||
const { options, selectedIndex } = this.props;
|
const { options, selectedIndex } = this.props;
|
||||||
|
|
||||||
// Remove old option refs to avoid memory leaks.
|
// Remove old option refs to avoid memory leaks.
|
||||||
|
@ -29,12 +84,15 @@ class List extends Component {
|
||||||
this.optionRefs = {};
|
this.optionRefs = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( selectedIndex !== prevProps.selectedIndex ) {
|
if (
|
||||||
|
selectedIndex !== prevProps.selectedIndex &&
|
||||||
|
isNumber( selectedIndex )
|
||||||
|
) {
|
||||||
this.scrollToOption( selectedIndex );
|
this.scrollToOption( selectedIndex );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getOptionRef( index ) {
|
getOptionRef( index: number ) {
|
||||||
if ( ! this.optionRefs.hasOwnProperty( index ) ) {
|
if ( ! this.optionRefs.hasOwnProperty( index ) ) {
|
||||||
this.optionRefs[ index ] = createRef();
|
this.optionRefs[ index ] = createRef();
|
||||||
}
|
}
|
||||||
|
@ -42,7 +100,7 @@ class List extends Component {
|
||||||
return this.optionRefs[ index ];
|
return this.optionRefs[ index ];
|
||||||
}
|
}
|
||||||
|
|
||||||
select( option ) {
|
select( option: Option ) {
|
||||||
const { onSelect } = this.props;
|
const { onSelect } = this.props;
|
||||||
|
|
||||||
if ( option.isDisabled ) {
|
if ( option.isDisabled ) {
|
||||||
|
@ -52,9 +110,13 @@ class List extends Component {
|
||||||
onSelect( option );
|
onSelect( option );
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToOption( index ) {
|
scrollToOption( index: number ) {
|
||||||
const listbox = this.listbox.current;
|
const listbox = this.listbox.current;
|
||||||
|
|
||||||
|
if ( ! listbox ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ( listbox.scrollHeight <= listbox.clientHeight ) {
|
if ( listbox.scrollHeight <= listbox.clientHeight ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -64,6 +126,12 @@ class List extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const option = this.optionRefs[ index ].current;
|
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 scrollBottom = listbox.clientHeight + listbox.scrollTop;
|
||||||
const elementBottom = option.offsetTop + option.offsetHeight;
|
const elementBottom = option.offsetTop + option.offsetHeight;
|
||||||
if ( elementBottom > scrollBottom ) {
|
if ( elementBottom > scrollBottom ) {
|
||||||
|
@ -73,7 +141,7 @@ class List extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown( event ) {
|
handleKeyDown( event: KeyboardEvent ) {
|
||||||
const {
|
const {
|
||||||
decrementSelectedIndex,
|
decrementSelectedIndex,
|
||||||
incrementSelectedIndex,
|
incrementSelectedIndex,
|
||||||
|
@ -100,7 +168,7 @@ class List extends Component {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ENTER:
|
case ENTER:
|
||||||
if ( options[ selectedIndex ] ) {
|
if ( isNumber( selectedIndex ) && options[ selectedIndex ] ) {
|
||||||
this.select( options[ selectedIndex ] );
|
this.select( options[ selectedIndex ] );
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -118,7 +186,7 @@ class List extends Component {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case TAB:
|
case TAB:
|
||||||
if ( options[ selectedIndex ] ) {
|
if ( isNumber( selectedIndex ) && options[ selectedIndex ] ) {
|
||||||
this.select( options[ selectedIndex ] );
|
this.select( options[ selectedIndex ] );
|
||||||
}
|
}
|
||||||
setExpanded( false );
|
setExpanded( false );
|
||||||
|
@ -128,8 +196,14 @@ class List extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleKeyEvents( isListening ) {
|
toggleKeyEvents( isListening: boolean ) {
|
||||||
const { node } = this.props;
|
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.
|
// This exists because we must capture ENTER key presses before RichText.
|
||||||
// It seems that react fires the simulated capturing events after the
|
// It seems that react fires the simulated capturing events after the
|
||||||
// native browser event has already bubbled so we can't stopPropagation
|
// native browser event has already bubbled so we can't stopPropagation
|
||||||
|
@ -138,12 +212,16 @@ class List extends Component {
|
||||||
const handler = isListening
|
const handler = isListening
|
||||||
? 'addEventListener'
|
? 'addEventListener'
|
||||||
: 'removeEventListener';
|
: 'removeEventListener';
|
||||||
node[ handler ]( 'keydown', this.handleKeyDown, true );
|
node[ handler ](
|
||||||
|
'keydown',
|
||||||
|
this.handleKeyDown as ( e: Event ) => void,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { selectedIndex } = this.props;
|
const { selectedIndex } = this.props;
|
||||||
if ( selectedIndex > -1 ) {
|
if ( isNumber( selectedIndex ) && selectedIndex > -1 ) {
|
||||||
this.scrollToOption( selectedIndex );
|
this.scrollToOption( selectedIndex );
|
||||||
}
|
}
|
||||||
this.toggleKeyEvents( true );
|
this.toggleKeyEvents( true );
|
||||||
|
@ -169,7 +247,7 @@ class List extends Component {
|
||||||
id={ listboxId }
|
id={ listboxId }
|
||||||
role="listbox"
|
role="listbox"
|
||||||
className={ listboxClasses }
|
className={ listboxClasses }
|
||||||
tabIndex="-1"
|
tabIndex={ -1 }
|
||||||
>
|
>
|
||||||
{ options.map( ( option, index ) => (
|
{ options.map( ( option, index ) => (
|
||||||
<Button
|
<Button
|
||||||
|
@ -186,7 +264,7 @@ class List extends Component {
|
||||||
}
|
}
|
||||||
) }
|
) }
|
||||||
onClick={ () => this.select( option ) }
|
onClick={ () => this.select( option ) }
|
||||||
tabIndex="-1"
|
tabIndex={ -1 }
|
||||||
>
|
>
|
||||||
{ option.label }
|
{ option.label }
|
||||||
</Button>
|
</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;
|
export default List;
|
|
@ -1,8 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { SelectControl } from '@woocommerce/components';
|
import React from 'react';
|
||||||
import { useState } from '@wordpress/element';
|
import { createElement, useState } from '@wordpress/element';
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import SelectControl from '../';
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
|
@ -5,19 +5,36 @@ import { __, sprintf } from '@wordpress/i18n';
|
||||||
import { Button } from '@wordpress/components';
|
import { Button } from '@wordpress/components';
|
||||||
import { Icon, cancelCircleFilled } from '@wordpress/icons';
|
import { Icon, cancelCircleFilled } from '@wordpress/icons';
|
||||||
import { createElement, Component, Fragment } from '@wordpress/element';
|
import { createElement, Component, Fragment } from '@wordpress/element';
|
||||||
import { findIndex } from 'lodash';
|
import { findIndex, isArray } from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import Tag from '../tag';
|
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.
|
* A list of tags to display selected items.
|
||||||
*/
|
*/
|
||||||
class Tags extends Component {
|
class Tags extends Component< Props > {
|
||||||
constructor( props ) {
|
constructor( props: Props ) {
|
||||||
super( props );
|
super( props );
|
||||||
this.removeAll = this.removeAll.bind( this );
|
this.removeAll = this.removeAll.bind( this );
|
||||||
this.removeResult = this.removeResult.bind( this );
|
this.removeResult = this.removeResult.bind( this );
|
||||||
|
@ -28,9 +45,13 @@ class Tags extends Component {
|
||||||
onChange( [] );
|
onChange( [] );
|
||||||
}
|
}
|
||||||
|
|
||||||
removeResult( key ) {
|
removeResult( key: string | undefined ) {
|
||||||
return () => {
|
return () => {
|
||||||
const { selected, onChange } = this.props;
|
const { selected, onChange } = this.props;
|
||||||
|
if ( ! isArray( selected ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const i = findIndex( selected, { key } );
|
const i = findIndex( selected, { key } );
|
||||||
onChange( [
|
onChange( [
|
||||||
...selected.slice( 0, i ),
|
...selected.slice( 0, i ),
|
||||||
|
@ -41,7 +62,7 @@ class Tags extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { selected, showClearButton } = this.props;
|
const { selected, showClearButton } = this.props;
|
||||||
if ( ! selected.length ) {
|
if ( ! isArray( selected ) || ! selected.length ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +84,7 @@ class Tags extends Component {
|
||||||
key={ item.key }
|
key={ item.key }
|
||||||
id={ item.key }
|
id={ item.key }
|
||||||
label={ item.label }
|
label={ item.label }
|
||||||
|
// @ts-expect-error key is a string or undefined here
|
||||||
remove={ this.removeResult }
|
remove={ this.removeResult }
|
||||||
screenReaderLabel={ screenReaderLabel }
|
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;
|
export default Tags;
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
import React from 'react';
|
||||||
import { render, waitFor } from '@testing-library/react';
|
import { render, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { createElement } from '@wordpress/element';
|
import { createElement } from '@wordpress/element';
|
||||||
|
@ -9,10 +10,11 @@ import { createElement } from '@wordpress/element';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { SelectControl } from '../index';
|
import { SelectControl } from '../index';
|
||||||
|
import { Option } from '../types';
|
||||||
|
|
||||||
describe( 'SelectControl', () => {
|
describe( 'SelectControl', () => {
|
||||||
const query = 'lorem';
|
const query = 'lorem';
|
||||||
const options = [
|
const options: Option[] = [
|
||||||
{ key: '1', label: 'lorem 1', value: { id: '1' } },
|
{ key: '1', label: 'lorem 1', value: { id: '1' } },
|
||||||
{ key: '2', label: 'lorem 2', value: { id: '2' } },
|
{ key: '2', label: 'lorem 2', value: { id: '2' } },
|
||||||
{ key: '3', label: 'bar', value: { id: '3' } },
|
{ key: '3', label: 'bar', value: { id: '3' } },
|
||||||
|
@ -168,9 +170,9 @@ describe( 'SelectControl', () => {
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'changes the options on search', async () => {
|
it( 'changes the options on search', async () => {
|
||||||
const queriedOptions = [];
|
const queriedOptions: Option[] = [];
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const queryOptions = ( options, searchedQuery ) => {
|
const queryOptions = async ( options: Option[], searchedQuery: string | null ) => {
|
||||||
if ( searchedQuery === 'test' ) {
|
if ( searchedQuery === 'test' ) {
|
||||||
queriedOptions.push( {
|
queriedOptions.push( {
|
||||||
key: 'test-option',
|
key: 'test-option',
|
||||||
|
@ -209,7 +211,7 @@ describe( 'SelectControl', () => {
|
||||||
isSearchable
|
isSearchable
|
||||||
showAllOnFocus
|
showAllOnFocus
|
||||||
options={ options }
|
options={ options }
|
||||||
onSearch={ () => options }
|
onSearch={ async () => options }
|
||||||
onFilter={ () => options }
|
onFilter={ () => options }
|
||||||
onChange={ onChangeMock }
|
onChange={ onChangeMock }
|
||||||
/>
|
/>
|
||||||
|
@ -229,7 +231,7 @@ describe( 'SelectControl', () => {
|
||||||
isSearchable
|
isSearchable
|
||||||
selected={ [ { ...options[ 0 ] } ] }
|
selected={ [ { ...options[ 0 ] } ] }
|
||||||
options={ options }
|
options={ options }
|
||||||
onSearch={ () => options }
|
onSearch={ async () => options }
|
||||||
onFilter={ () => options }
|
onFilter={ () => options }
|
||||||
onChange={ onChangeMock }
|
onChange={ onChangeMock }
|
||||||
/>
|
/>
|
||||||
|
@ -258,7 +260,7 @@ describe( 'SelectControl', () => {
|
||||||
isSearchable
|
isSearchable
|
||||||
selected={ options[ 0 ].key }
|
selected={ options[ 0 ].key }
|
||||||
options={ options }
|
options={ options }
|
||||||
onSearch={ () => options }
|
onSearch={ async () => options }
|
||||||
onFilter={ () => options }
|
onFilter={ () => options }
|
||||||
onChange={ onChangeMock }
|
onChange={ onChangeMock }
|
||||||
/>
|
/>
|
||||||
|
@ -289,7 +291,7 @@ describe( 'SelectControl', () => {
|
||||||
showAllOnFocus
|
showAllOnFocus
|
||||||
selected={ options[ 2 ].key }
|
selected={ options[ 2 ].key }
|
||||||
options={ options }
|
options={ options }
|
||||||
onSearch={ () => options }
|
onSearch={ async () => options }
|
||||||
onFilter={ () => options }
|
onFilter={ () => options }
|
||||||
onChange={ onChangeMock }
|
onChange={ onChangeMock }
|
||||||
excludeSelectedOptions={ false }
|
excludeSelectedOptions={ false }
|
||||||
|
@ -316,7 +318,7 @@ describe( 'SelectControl', () => {
|
||||||
showAllOnFocus
|
showAllOnFocus
|
||||||
selected={ options[ 2 ].key }
|
selected={ options[ 2 ].key }
|
||||||
options={ options }
|
options={ options }
|
||||||
onSearch={ () => options }
|
onSearch={ async () => options }
|
||||||
onFilter={ () => options }
|
onFilter={ () => options }
|
||||||
onChange={ onChangeMock }
|
onChange={ onChangeMock }
|
||||||
excludeSelectedOptions={ false }
|
excludeSelectedOptions={ false }
|
||||||
|
@ -364,7 +366,7 @@ describe( 'SelectControl', () => {
|
||||||
isSearchable
|
isSearchable
|
||||||
selected={ [ { ...options[ 0 ] } ] }
|
selected={ [ { ...options[ 0 ] } ] }
|
||||||
options={ options }
|
options={ options }
|
||||||
onSearch={ () => options }
|
onSearch={ async () => options }
|
||||||
onFilter={ () => options }
|
onFilter={ () => options }
|
||||||
onChange={ onChangeMock }
|
onChange={ onChangeMock }
|
||||||
/>
|
/>
|
||||||
|
@ -383,7 +385,7 @@ describe( 'SelectControl', () => {
|
||||||
isSearchable
|
isSearchable
|
||||||
selected={ options[ 0 ].key }
|
selected={ options[ 0 ].key }
|
||||||
options={ options }
|
options={ options }
|
||||||
onSearch={ () => options }
|
onSearch={ async () => options }
|
||||||
onFilter={ () => options }
|
onFilter={ () => options }
|
||||||
onChange={ onChangeMock }
|
onChange={ onChangeMock }
|
||||||
/>
|
/>
|
||||||
|
@ -416,9 +418,8 @@ describe( 'SelectControl', () => {
|
||||||
const { getByRole } = render(
|
const { getByRole } = render(
|
||||||
<SelectControl
|
<SelectControl
|
||||||
options={ options }
|
options={ options }
|
||||||
onSearch={ () => options }
|
onSearch={ async () => options }
|
||||||
onFilter={ () => options }
|
onFilter={ () => options }
|
||||||
selected={ null }
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -440,7 +441,7 @@ describe( 'SelectControl', () => {
|
||||||
const { getByRole } = render(
|
const { getByRole } = render(
|
||||||
<SelectControl
|
<SelectControl
|
||||||
options={ options }
|
options={ options }
|
||||||
onSearch={ () => options }
|
onSearch={ async () => options }
|
||||||
onFilter={ () => options }
|
onFilter={ () => options }
|
||||||
selected={ options[ 1 ].key }
|
selected={ options[ 1 ].key }
|
||||||
excludeSelectedOptions={ false }
|
excludeSelectedOptions={ false }
|
||||||
|
@ -465,7 +466,7 @@ describe( 'SelectControl', () => {
|
||||||
const { getByRole } = render(
|
const { getByRole } = render(
|
||||||
<SelectControl
|
<SelectControl
|
||||||
options={ options }
|
options={ options }
|
||||||
onSearch={ () => options }
|
onSearch={ async () => options }
|
||||||
onFilter={ () => options }
|
onFilter={ () => options }
|
||||||
selected={ options[ options.length - 1 ].key }
|
selected={ options[ options.length - 1 ].key }
|
||||||
excludeSelectedOptions={ false }
|
excludeSelectedOptions={ false }
|
||||||
|
@ -490,9 +491,8 @@ describe( 'SelectControl', () => {
|
||||||
const { getByRole } = render(
|
const { getByRole } = render(
|
||||||
<SelectControl
|
<SelectControl
|
||||||
options={ options }
|
options={ options }
|
||||||
onSearch={ () => options }
|
onSearch={ async () => options }
|
||||||
onFilter={ () => options }
|
onFilter={ () => options }
|
||||||
selected={ null }
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -514,9 +514,8 @@ describe( 'SelectControl', () => {
|
||||||
const { getByRole, queryByRole } = render(
|
const { getByRole, queryByRole } = render(
|
||||||
<SelectControl
|
<SelectControl
|
||||||
options={ options }
|
options={ options }
|
||||||
onSearch={ () => options }
|
onSearch={ async () => options }
|
||||||
onFilter={ () => options }
|
onFilter={ () => options }
|
||||||
selected={ null }
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -537,9 +536,8 @@ describe( 'SelectControl', () => {
|
||||||
const { getByRole } = render(
|
const { getByRole } = render(
|
||||||
<SelectControl
|
<SelectControl
|
||||||
options={ options }
|
options={ options }
|
||||||
onSearch={ () => options }
|
onSearch={ async () => options }
|
||||||
onFilter={ () => options }
|
onFilter={ () => options }
|
||||||
selected={ null }
|
|
||||||
excludeSelectedOptions={ false }
|
excludeSelectedOptions={ false }
|
||||||
onChange={ onChangeMock }
|
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
|
// Items before the current item cause a one off error when
|
||||||
// removed from the old array and spliced into the new array.
|
// 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;
|
let targetIndex = dragIndex < index ? index : index + 1;
|
||||||
if ( isBefore( event, isHorizontal ) ) {
|
if ( isBefore( event, isHorizontal ) ) {
|
||||||
targetIndex--;
|
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 ]
|
`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
|
`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`
|
`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
|
`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
|
`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
|
`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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
onQueryChange?: ( param: string ) => ( ...props: any ) => void;
|
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;
|
onColumnsChange?: ( showCols: Array< string >, key?: string ) => void;
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
## Breaking changes
|
## Breaking changes
|
||||||
|
|
||||||
- Fix the batch fetch logic for the options data store. #7587
|
- 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
|
- 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
|
- Fix race condition in data package's options module. #7947
|
||||||
- Remove dev dependency `@woocommerce/wc-admin-settings`. #8057
|
- 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 * as resolvers from './resolvers';
|
||||||
import reducer, { State } from './reducer';
|
import reducer, { State } from './reducer';
|
||||||
import { WPDataSelectors } from '../types';
|
import { WPDataSelectors } from '../types';
|
||||||
|
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export type { State };
|
export type { State };
|
||||||
|
|
||||||
|
@ -35,4 +36,7 @@ declare module '@wordpress/data' {
|
||||||
function select(
|
function select(
|
||||||
key: typeof STORE_NAME
|
key: typeof STORE_NAME
|
||||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
): 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 reducer, { State } from './reducer';
|
||||||
import { WPDataSelectors } from '../types';
|
import { WPDataSelectors } from '../types';
|
||||||
import controls from '../controls';
|
import controls from '../controls';
|
||||||
|
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export type { State };
|
export type { State };
|
||||||
|
|
||||||
|
@ -33,4 +34,7 @@ declare module '@wordpress/data' {
|
||||||
function select(
|
function select(
|
||||||
key: typeof STORE_NAME
|
key: typeof STORE_NAME
|
||||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
): 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 * as resolvers from './resolvers';
|
||||||
import reducer, { State } from './reducer';
|
import reducer, { State } from './reducer';
|
||||||
import { WPDataSelectors } from '../types';
|
import { WPDataSelectors } from '../types';
|
||||||
|
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export type { State };
|
export type { State };
|
||||||
|
|
||||||
|
@ -35,4 +36,7 @@ declare module '@wordpress/data' {
|
||||||
function select(
|
function select(
|
||||||
key: typeof STORE_NAME
|
key: typeof STORE_NAME
|
||||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
): 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 controls from '../controls';
|
||||||
import { WPDataActions, WPDataSelectors } from '../types';
|
import { WPDataActions, WPDataSelectors } from '../types';
|
||||||
import { getItemsType } from './selectors';
|
import { getItemsType } from './selectors';
|
||||||
|
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export type { State };
|
export type { State };
|
||||||
|
|
||||||
|
@ -40,4 +41,7 @@ declare module '@wordpress/data' {
|
||||||
key: typeof STORE_NAME
|
key: typeof STORE_NAME
|
||||||
): DispatchFromMap< typeof actions & WPDataActions >;
|
): DispatchFromMap< typeof actions & WPDataActions >;
|
||||||
function select( key: typeof STORE_NAME ): ItemsSelector;
|
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 * as resolvers from './resolvers';
|
||||||
import initDispatchers from './dispatchers';
|
import initDispatchers from './dispatchers';
|
||||||
import { WPDataActions, WPDataSelectors } from '../types';
|
import { WPDataActions, WPDataSelectors } from '../types';
|
||||||
|
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||||
|
|
||||||
registerStore< State >( STORE_NAME, {
|
registerStore< State >( STORE_NAME, {
|
||||||
reducer: reducer as Reducer< State, AnyAction >,
|
reducer: reducer as Reducer< State, AnyAction >,
|
||||||
|
@ -35,4 +36,7 @@ declare module '@wordpress/data' {
|
||||||
function select(
|
function select(
|
||||||
key: typeof STORE_NAME
|
key: typeof STORE_NAME
|
||||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
): 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 actions from './actions';
|
||||||
import * as resolvers from './resolvers';
|
import * as resolvers from './resolvers';
|
||||||
import reducer, { State } from './reducer';
|
import reducer, { State } from './reducer';
|
||||||
|
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||||
|
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export type { State };
|
export type { State };
|
||||||
|
@ -36,4 +37,7 @@ declare module '@wordpress/data' {
|
||||||
function select(
|
function select(
|
||||||
key: typeof STORE_NAME
|
key: typeof STORE_NAME
|
||||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
||||||
|
function resolveSelect(
|
||||||
|
key: typeof STORE_NAME
|
||||||
|
): PromiseifySelectors< SelectFromMap< typeof selectors > >;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
ProfileItems,
|
ProfileItems,
|
||||||
TaskListType,
|
TaskListType,
|
||||||
TaskType,
|
TaskType,
|
||||||
OnboardingProductType,
|
OnboardingProductTypes,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { Plugin } from '../plugins/types';
|
import { Plugin } from '../plugins/types';
|
||||||
|
|
||||||
|
@ -267,9 +267,7 @@ export function actionTaskSuccess( task: Partial< TaskType > ) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProductTypesSuccess(
|
export function getProductTypesSuccess( productTypes: OnboardingProductTypes ) {
|
||||||
productTypes: OnboardingProductType[]
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
type: TYPES.GET_PRODUCT_TYPES_SUCCESS,
|
type: TYPES.GET_PRODUCT_TYPES_SUCCESS,
|
||||||
productTypes,
|
productTypes,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import * as actions from './actions';
|
||||||
import * as resolvers from './resolvers';
|
import * as resolvers from './resolvers';
|
||||||
import reducer, { State } from './reducer';
|
import reducer, { State } from './reducer';
|
||||||
import { WPDataActions, WPDataSelectors } from '../types';
|
import { WPDataActions, WPDataSelectors } from '../types';
|
||||||
|
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export type { State };
|
export type { State };
|
||||||
|
|
||||||
|
@ -35,4 +36,7 @@ declare module '@wordpress/data' {
|
||||||
key: typeof STORE_NAME
|
key: typeof STORE_NAME
|
||||||
): DispatchFromMap< typeof actions & WPDataActions >;
|
): DispatchFromMap< typeof actions & WPDataActions >;
|
||||||
function select( key: typeof STORE_NAME ): OnboardingSelector;
|
function select( key: typeof STORE_NAME ): OnboardingSelector;
|
||||||
|
function resolveSelect(
|
||||||
|
key: typeof STORE_NAME
|
||||||
|
): PromiseifySelectors< OnboardingSelector >;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ export const defaultState: OnboardingState = {
|
||||||
},
|
},
|
||||||
emailPrefill: '',
|
emailPrefill: '',
|
||||||
paymentMethods: [],
|
paymentMethods: [],
|
||||||
productTypes: [],
|
productTypes: {},
|
||||||
requesting: {},
|
requesting: {},
|
||||||
taskLists: {},
|
taskLists: {},
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,7 +24,7 @@ import {
|
||||||
import { DeprecatedTasks } from './deprecated-tasks';
|
import { DeprecatedTasks } from './deprecated-tasks';
|
||||||
import {
|
import {
|
||||||
ExtensionList,
|
ExtensionList,
|
||||||
OnboardingProductType,
|
OnboardingProductTypes,
|
||||||
ProfileItems,
|
ProfileItems,
|
||||||
TaskListType,
|
TaskListType,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
@ -126,7 +126,7 @@ export function* getFreeExtensions() {
|
||||||
|
|
||||||
export function* getProductTypes() {
|
export function* getProductTypes() {
|
||||||
try {
|
try {
|
||||||
const results: OnboardingProductType[] = yield apiFetch( {
|
const results: OnboardingProductTypes = yield apiFetch( {
|
||||||
path: WC_ADMIN_NAMESPACE + '/onboarding/product-types',
|
path: WC_ADMIN_NAMESPACE + '/onboarding/product-types',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
OnboardingState,
|
OnboardingState,
|
||||||
ExtensionList,
|
ExtensionList,
|
||||||
ProfileItems,
|
ProfileItems,
|
||||||
OnboardingProductType,
|
|
||||||
} from './types';
|
} from './types';
|
||||||
import { WPDataSelectors } from '../types';
|
import { WPDataSelectors } from '../types';
|
||||||
import { Plugin } from '../plugins/types';
|
import { Plugin } from '../plugins/types';
|
||||||
|
@ -92,10 +91,8 @@ export const getEmailPrefill = ( state: OnboardingState ): string => {
|
||||||
return state.emailPrefill || '';
|
return state.emailPrefill || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductTypes = (
|
export const getProductTypes = ( state: OnboardingState ) => {
|
||||||
state: OnboardingState
|
return state.productTypes || {};
|
||||||
): OnboardingProductType[] => {
|
|
||||||
return state.productTypes || [];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OnboardingSelectors = {
|
export type OnboardingSelectors = {
|
||||||
|
|
|
@ -75,7 +75,7 @@ export type OnboardingState = {
|
||||||
profileItems: ProfileItems;
|
profileItems: ProfileItems;
|
||||||
taskLists: Record< string, TaskListType >;
|
taskLists: Record< string, TaskListType >;
|
||||||
paymentMethods: Plugin[];
|
paymentMethods: Plugin[];
|
||||||
productTypes: OnboardingProductType[];
|
productTypes: OnboardingProductTypes;
|
||||||
emailPrefill: string;
|
emailPrefill: string;
|
||||||
// TODO clarify what the error record's type is
|
// TODO clarify what the error record's type is
|
||||||
errors: Record< string, unknown >;
|
errors: Record< string, unknown >;
|
||||||
|
@ -152,11 +152,21 @@ export type MethodFields = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OnboardingProductType = {
|
export type OnboardingProductType = {
|
||||||
default?: boolean;
|
|
||||||
label: string;
|
label: string;
|
||||||
|
default?: boolean;
|
||||||
product?: number;
|
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 = {
|
export type ExtensionList = {
|
||||||
key: string;
|
key: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
@ -14,6 +14,7 @@ import * as resolvers from './resolvers';
|
||||||
import reducer, { State } from './reducer';
|
import reducer, { State } from './reducer';
|
||||||
import { controls } from './controls';
|
import { controls } from './controls';
|
||||||
import { WPDataActions, WPDataSelectors } from '../types';
|
import { WPDataActions, WPDataSelectors } from '../types';
|
||||||
|
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export type { State };
|
export type { State };
|
||||||
|
|
||||||
|
@ -34,4 +35,7 @@ declare module '@wordpress/data' {
|
||||||
function select(
|
function select(
|
||||||
key: typeof STORE_NAME
|
key: typeof STORE_NAME
|
||||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
): 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 reducer from './reducer';
|
||||||
import { STORE_KEY } from './constants';
|
import { STORE_KEY } from './constants';
|
||||||
import { WPDataActions } from '../types';
|
import { WPDataActions } from '../types';
|
||||||
|
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||||
|
|
||||||
export const PAYMENT_GATEWAYS_STORE_NAME = STORE_KEY;
|
export const PAYMENT_GATEWAYS_STORE_NAME = STORE_KEY;
|
||||||
|
|
||||||
|
@ -33,4 +34,7 @@ declare module '@wordpress/data' {
|
||||||
function select(
|
function select(
|
||||||
key: typeof STORE_KEY
|
key: typeof STORE_KEY
|
||||||
): SelectFromMap< typeof selectors > & WPDataActions;
|
): SelectFromMap< typeof selectors > & WPDataActions;
|
||||||
|
function resolveSelect(
|
||||||
|
key: typeof STORE_KEY
|
||||||
|
): PromiseifySelectors< SelectFromMap< typeof selectors > >;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const pluginNames = {
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
),
|
),
|
||||||
'woocommerce-gateway-stripe': __( 'WooCommerce Stripe', '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-payments': __( 'WooCommerce Payments', 'woocommerce' ),
|
||||||
'woocommerce-services': __( 'WooCommerce Shipping & Tax', 'woocommerce' ),
|
'woocommerce-services': __( 'WooCommerce Shipping & Tax', 'woocommerce' ),
|
||||||
'woocommerce-services:shipping': __(
|
'woocommerce-services:shipping': __(
|
||||||
|
|
|
@ -13,6 +13,7 @@ import * as actions from './actions';
|
||||||
import * as resolvers from './resolvers';
|
import * as resolvers from './resolvers';
|
||||||
import reducer, { State } from './reducer';
|
import reducer, { State } from './reducer';
|
||||||
import { WPDataActions, WPDataSelectors } from '../types';
|
import { WPDataActions, WPDataSelectors } from '../types';
|
||||||
|
import { PromiseifySelectors } from '../types/promiseify-selectors';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export type { State };
|
export type { State };
|
||||||
|
|
||||||
|
@ -33,4 +34,7 @@ declare module '@wordpress/data' {
|
||||||
function select(
|
function select(
|
||||||
key: typeof STORE_NAME
|
key: typeof STORE_NAME
|
||||||
): SelectFromMap< typeof selectors > & WPDataSelectors;
|
): 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