Merge branch 'trunk' into fix/wccom-18029-suppress-double-scrollbar
This commit is contained in:
commit
da7755a7c1
|
@ -37,6 +37,10 @@ Please take a moment to review the [project readme](https://github.com/woocommer
|
|||
- Run our build process described in the document on [how to set up WooCommerce development environment](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment), it will install our pre-commit hook, code sniffs, dependencies, and more.
|
||||
- Before pushing commits to GitHub, check your code against our code standards. For PHP code in the WooCommerce Core project you can do this by running `pnpm --filter=woocommerce run lint:php:changes:branch`.
|
||||
- Whenever possible, please fix pre-existing code standards errors in code that you change.
|
||||
- Please consider adding appropriate tests related to your change if applicable such as unit, API and E2E tests. You can check the following guides for this purpose:
|
||||
- [Writing unit tests](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/README.md#guide-for-writing-unit-tests).
|
||||
- [Writing API tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests#guide-for-writing-api-tests).
|
||||
- [Writing E2E tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/e2e-pw#guide-for-writing-e2e-tests).
|
||||
- Ensure you use LF line endings in your code editor. Use [EditorConfig](http://editorconfig.org/) if your editor supports it so that indentation, line endings and other settings are auto configured.
|
||||
- When committing, reference your issue number (#1234) and include a note about the fix.
|
||||
- Ensure that your code supports the minimum supported versions of PHP and WordPress; this is shown at the top of the `readme.txt` file.
|
||||
|
|
|
@ -67,11 +67,6 @@
|
|||
'plugin: woo-ai':
|
||||
- plugins/woo-ai/**/*
|
||||
|
||||
'focus: react admin [team:Ghidorah]':
|
||||
- plugins/woocommerce/src/Admin/**/*
|
||||
- plugins/woocommerce/src/Internal/Admin/**/*
|
||||
- plugins/woocommerce-admin/**/*
|
||||
|
||||
'focus: performance tests [team:Solaris]':
|
||||
- plugins/woocommerce/tests/performance/**/*
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ jobs:
|
|||
CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }}
|
||||
CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }}
|
||||
DEFAULT_TIMEOUT_OVERRIDE: 120000
|
||||
UPDATE_WC: 'nightly'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
@ -45,12 +46,6 @@ jobs:
|
|||
working-directory: plugins/woocommerce
|
||||
run: pnpm exec playwright install chromium
|
||||
|
||||
- name: Run 'Update WooCommerce' test.
|
||||
working-directory: plugins/woocommerce
|
||||
env:
|
||||
UPDATE_WC: nightly
|
||||
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js update-woocommerce.spec.js
|
||||
|
||||
- name: Run API tests.
|
||||
working-directory: plugins/woocommerce
|
||||
env:
|
||||
|
@ -79,7 +74,7 @@ jobs:
|
|||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: read
|
||||
needs: [api-tests]
|
||||
# needs: [api-tests]
|
||||
env:
|
||||
ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }}
|
||||
ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }}
|
||||
|
@ -349,4 +344,4 @@ jobs:
|
|||
-f plugin="${{ matrix.plugin }}" \
|
||||
-f slug="${{ matrix.slug }}" \
|
||||
-f s3_root=public \
|
||||
--repo woocommerce/woocommerce-test-reports
|
||||
--repo woocommerce/woocommerce-test-reports
|
||||
|
|
|
@ -307,64 +307,31 @@ jobs:
|
|||
env-slug: wp-latest
|
||||
release-version: ${{ needs.get-tag.outputs.tag }}
|
||||
|
||||
get-wp-versions:
|
||||
name: Get WP L-1 & L-2 version numbers
|
||||
needs: [get-tag]
|
||||
test-wp-latest-1:
|
||||
name: Test against WP Latest-1
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
matrix: ${{ steps.get-versions.outputs.versions }}
|
||||
tag: ${{ needs.get-tag.outputs.tag }}
|
||||
created: ${{ needs.get-tag.outputs.created }}
|
||||
steps:
|
||||
- name: Create dirs
|
||||
run: |
|
||||
mkdir script
|
||||
mkdir repo
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: repo
|
||||
|
||||
- name: Copy script to get previous WP versions
|
||||
run: cp repo/plugins/woocommerce/tests/e2e-pw/utils/wordpress.js script
|
||||
|
||||
- name: Install axios
|
||||
working-directory: script
|
||||
run: npm install axios
|
||||
|
||||
- name: Get version numbers
|
||||
id: get-versions
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { getPreviousTwoVersions } = require('./script/wordpress');
|
||||
const versions = await getPreviousTwoVersions();
|
||||
console.log(versions);
|
||||
core.setOutput('versions', versions);
|
||||
|
||||
test-wp-versions:
|
||||
name: Test against ${{ matrix.version.description }} (${{ matrix.version.number }})
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [get-wp-versions]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.get-wp-versions.outputs.matrix) }}
|
||||
needs: [ get-tag ]
|
||||
env:
|
||||
API_ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/api/allure-report
|
||||
API_ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/api/allure-results
|
||||
API_WP_LATEST_X_ARTIFACT: API test on wp-env with WordPress ${{ matrix.version.number }} (run ${{ github.run_number }})
|
||||
API_WP_LATEST_X_ARTIFACT: API test on wp-env with WordPress L-1 (run ${{ github.run_number }})
|
||||
E2E_ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/e2e/allure-report
|
||||
E2E_ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/e2e/allure-results
|
||||
E2E_WP_LATEST_X_ARTIFACT: E2E test on wp-env with WordPress ${{ matrix.version.number }} (run ${{ github.run_number }})
|
||||
E2E_WP_LATEST_X_ARTIFACT: E2E test on wp-env with WordPress L-1 (run ${{ github.run_number }})
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout WooCommerce repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get WP Latest-1 version number
|
||||
id: get-wp-latest-1
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { getVersionWPLatestMinusOne } = require( './plugins/woocommerce/tests/e2e-pw/utils/wordpress' );
|
||||
await getVersionWPLatestMinusOne( { core, github } );
|
||||
|
||||
- name: Setup WooCommerce Monorepo
|
||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||
with:
|
||||
|
@ -373,29 +340,17 @@ jobs:
|
|||
- name: Launch WP Env
|
||||
working-directory: plugins/woocommerce
|
||||
run: pnpm run env:test
|
||||
env:
|
||||
WP_ENV_CORE: WordPress/WordPress#${{ steps.get-wp-latest-1.outputs.version }}
|
||||
|
||||
- name: Download release zip
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
|
||||
run: gh release download ${{ needs.get-wp-versions.outputs.tag }} --dir tmp
|
||||
run: gh release download ${{ steps.get-wp-latest-1.outputs.version }} --dir tmp
|
||||
|
||||
- name: Replace `plugins/woocommerce` with unzipped woocommerce release build
|
||||
run: unzip -d plugins -o tmp/woocommerce.zip
|
||||
|
||||
- name: Downgrade WordPress version to ${{ matrix.version.number }}
|
||||
working-directory: plugins/woocommerce
|
||||
run: |
|
||||
pnpm exec wp-env run tests-cli -- wp core update --version=${{ matrix.version.number }} --force
|
||||
pnpm exec wp-env run tests-cli wp core update-db
|
||||
|
||||
- name: Verify environment details
|
||||
working-directory: plugins/woocommerce
|
||||
run: |
|
||||
pnpm exec wp-env run tests-cli wp core version
|
||||
pnpm exec wp-env run tests-cli wp plugin list
|
||||
pnpm exec wp-env run tests-cli wp theme list
|
||||
pnpm exec wp-env run tests-cli wp user list
|
||||
|
||||
- name: Run API tests
|
||||
id: run-api-composite-action
|
||||
uses: ./.github/actions/tests/run-api-tests
|
||||
|
@ -423,13 +378,13 @@ jobs:
|
|||
if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' )
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }}
|
||||
ENV_DESCRIPTION: ${{ matrix.version.env_description }}
|
||||
ENV_DESCRIPTION: wp-latest-1
|
||||
run: |
|
||||
gh workflow run publish-test-reports-release.yml \
|
||||
-f created_at="${{ needs.get-wp-versions.outputs.created }}" \
|
||||
-f created_at="${{ needs.get-tag.outputs.created }}" \
|
||||
-f run_id=${{ github.run_id }} \
|
||||
-f run_number=${{ github.run_number }} \
|
||||
-f release_tag=${{ needs.get-wp-versions.outputs.tag }} \
|
||||
-f release_tag=${{ needs.get-tag.outputs.tag }} \
|
||||
-f artifact="${{ env.API_WP_LATEST_X_ARTIFACT }}" \
|
||||
-f env_description="${{ env.ENV_DESCRIPTION }}" \
|
||||
-f test_type="api" \
|
||||
|
@ -464,13 +419,13 @@ jobs:
|
|||
if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' )
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }}
|
||||
ENV_DESCRIPTION: ${{ matrix.version.env_description }}
|
||||
ENV_DESCRIPTION: wp-latest-1
|
||||
run: |
|
||||
gh workflow run publish-test-reports-release.yml \
|
||||
-f created_at="${{ needs.get-wp-versions.outputs.created }}" \
|
||||
-f created_at="${{ needs.get-tag.outputs.created }}" \
|
||||
-f run_id=${{ github.run_id }} \
|
||||
-f run_number=${{ github.run_number }} \
|
||||
-f release_tag=${{ needs.get-wp-versions.outputs.tag }} \
|
||||
-f release_tag=${{ needs.get-tag.outputs.tag }} \
|
||||
-f artifact="${{ env.E2E_WP_LATEST_X_ARTIFACT }}" \
|
||||
-f env_description="${{ env.ENV_DESCRIPTION }}" \
|
||||
-f test_type="e2e" \
|
||||
|
@ -486,11 +441,11 @@ jobs:
|
|||
)
|
||||
uses: ./.github/actions/tests/slack-summary-on-release/slack-blocks
|
||||
with:
|
||||
test-name: ${{ matrix.version.description }} (${{ matrix.version.number }})
|
||||
test-name: WP Latest-1 (${{ steps.get-wp-latest-1.outputs.version }})
|
||||
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 }}
|
||||
env-slug: wp-latest-1
|
||||
release-version: ${{ needs.get-tag.outputs.tag }}
|
||||
|
||||
test-php-versions:
|
||||
name: Test against PHP ${{ matrix.php_version }}
|
||||
|
@ -759,7 +714,7 @@ jobs:
|
|||
- get-tag
|
||||
- test-php-versions
|
||||
- test-plugins
|
||||
- test-wp-versions
|
||||
- test-wp-latest-1
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
|
|
@ -1,5 +1,95 @@
|
|||
== Changelog ==
|
||||
|
||||
= 8.1.0 2023-09-12 =
|
||||
|
||||
**WooCommerce**
|
||||
|
||||
* Fix - Update modified date when a metadata is saved for HPOS. [#39911](https://github.com/woocommerce/woocommerce/pull/39911)
|
||||
* Fix - Fix edgecase performance issues around incentives caching. [#39958](https://github.com/woocommerce/woocommerce/pull/39958)
|
||||
* Fix - Add migration to move incorrectly stored payment token IDS to HPOS tables from postmeta. [#39724](https://github.com/woocommerce/woocommerce/pull/39724)
|
||||
* Fix - Address more PHP 8.1+ deprecation warnings in wc-admin code. [#38774](https://github.com/woocommerce/woocommerce/pull/38774)
|
||||
* Fix - Adds display of postcodes to Vietnam addresses. [#39403](https://github.com/woocommerce/woocommerce/pull/39403)
|
||||
* Fix - Always return bool values from WPCacheEngine functions when expected. [#39819](https://github.com/woocommerce/woocommerce/pull/39819)
|
||||
* Fix - Be more precise when checking submission data from the email verification form on the order confirmation screen. [#39479](https://github.com/woocommerce/woocommerce/pull/39479)
|
||||
* Fix - Bring HPOS order hooks in line with the posts implementation. [#39694](https://github.com/woocommerce/woocommerce/pull/39694)
|
||||
* Fix - Connect WC_Install's create_tables to HPOS tables when its active. [#39682](https://github.com/woocommerce/woocommerce/pull/39682)
|
||||
* Fix - Disable read on sync while backfilling. [#39450](https://github.com/woocommerce/woocommerce/pull/39450)
|
||||
* Fix - Ensure refund meta data is saved correctly when HPOS is enabled. [#39700](https://github.com/woocommerce/woocommerce/pull/39700)
|
||||
* Fix - Ensure that the full discount is ignored in free shipping minimum order calculations when ignore_discount setting is enabled [#39155](https://github.com/woocommerce/woocommerce/pull/39155)
|
||||
* Fix - Fixed a race condition that was causing page views on intro-opt-in page to be sent before tracks was enabled. [#39508](https://github.com/woocommerce/woocommerce/pull/39508)
|
||||
* Fix - Fixes WooCommerce knowledge base API returning empty posts. [#39809](https://github.com/woocommerce/woocommerce/pull/39809)
|
||||
* Fix - Fix failure due to multiple h2 tags in the Product Vendors plugin [#38717](https://github.com/woocommerce/woocommerce/pull/38717)
|
||||
* Fix - Fix Storefront recommendation link and missing image in Marketplace [#39294](https://github.com/woocommerce/woocommerce/pull/39294)
|
||||
* Fix - include post_ID in HPOS order edit screen [#39321](https://github.com/woocommerce/woocommerce/pull/39321)
|
||||
* Fix - Limit index length to 191 characters by default, additionally connect HPOS to verify DB tooling. [#39250](https://github.com/woocommerce/woocommerce/pull/39250)
|
||||
* Fix - Onboarding payments task not completed after setting up WooPayments [#39786](https://github.com/woocommerce/woocommerce/pull/39786)
|
||||
* Fix - Prevent possible error when refreshing order edit locks. [#39498](https://github.com/woocommerce/woocommerce/pull/39498)
|
||||
* Fix - Prevent possible fatal error when edit lock is held on deleted order. [#39497](https://github.com/woocommerce/woocommerce/pull/39497)
|
||||
* Fix - Store transactional data in order tables with HPOS. [#39381](https://github.com/woocommerce/woocommerce/pull/39381)
|
||||
* Fix - Support inserting NULL values for strict DB mode for DataBase Util's insert_on_duplicate_key_update method. [#39396](https://github.com/woocommerce/woocommerce/pull/39396)
|
||||
* Fix - Update CSS prop 'end' to 'flex-end' when using flexbox. [#39419](https://github.com/woocommerce/woocommerce/pull/39419)
|
||||
* Fix - Use admin theme color for select2, instead of hardcoded theme values. [#39451](https://github.com/woocommerce/woocommerce/pull/39451)
|
||||
* Fix - Use admin theme color instead of old pink. Update old pink to the new brand color. [#39182](https://github.com/woocommerce/woocommerce/pull/39182)
|
||||
* Fix - [Product Block Editor] remove digital products from the target list #39769 [#39801](https://github.com/woocommerce/woocommerce/pull/39801)
|
||||
* Add - Add block template registry and controller [#39698](https://github.com/woocommerce/woocommerce/pull/39698)
|
||||
* Add - Add delete option to generate variations API, to auto delete unmatched variations. [#39733](https://github.com/woocommerce/woocommerce/pull/39733)
|
||||
* Add - Added feature flag that removes store appearance task and adds customize store task when enabled [#39397](https://github.com/woocommerce/woocommerce/pull/39397)
|
||||
* Add - Add filter for adding new user preference option for notice to user data fields. [#39685](https://github.com/woocommerce/woocommerce/pull/39685)
|
||||
* Add - Add plugin installation request track for core profiler [#39533](https://github.com/woocommerce/woocommerce/pull/39533)
|
||||
* Add - Add post_password for products for REST API V3 [#39438](https://github.com/woocommerce/woocommerce/pull/39438)
|
||||
* Add - Add support for Japan and UAE in WooPayments [#39431](https://github.com/woocommerce/woocommerce/pull/39431)
|
||||
* Add - Add woocommerce/product-password-field block to new product editor [#39464](https://github.com/woocommerce/woocommerce/pull/39464)
|
||||
* Add - API for block-based templates. [#39470](https://github.com/woocommerce/woocommerce/pull/39470)
|
||||
* Add - Register product catalog and search visibility blocks [#39477](https://github.com/woocommerce/woocommerce/pull/39477)
|
||||
* Add - Register the product variation items block [#39657](https://github.com/woocommerce/woocommerce/pull/39657)
|
||||
* Add - [E2E test coverage]: Disable block product editor #39417 [#39493](https://github.com/woocommerce/woocommerce/pull/39493)
|
||||
* Add - [E2E test coverage]: Enable new product management experience [#39463](https://github.com/woocommerce/woocommerce/pull/39463)
|
||||
* Add - [E2E test coverage]: General tab #39411 [#39493](https://github.com/woocommerce/woocommerce/pull/39493)
|
||||
* Update - Add 'variable' to supported post types for product block editor [#39256](https://github.com/woocommerce/woocommerce/pull/39256)
|
||||
* Update - Added xstate scaffolding for customize your store feature [#39619](https://github.com/woocommerce/woocommerce/pull/39619)
|
||||
* Update - add time support to product import on sale dates [#39372](https://github.com/woocommerce/woocommerce/pull/39372)
|
||||
* Update - On the order confirmation screen, show the 'thank you' message regardless of whether the viewer is verified or not. [#39758](https://github.com/woocommerce/woocommerce/pull/39758)
|
||||
* Update - Support `first_used` and `installation_date` mobile usage data for WCTracker. [#39605](https://github.com/woocommerce/woocommerce/pull/39605)
|
||||
* Update - Updates Action Scheduler to 3.6.2 (bug fixes and improvements to help debug problems). [#39665](https://github.com/woocommerce/woocommerce/pull/39665)
|
||||
* Update - Update task list to show a spinner on item click [#39270](https://github.com/woocommerce/woocommerce/pull/39270)
|
||||
* Update - Update WCPay banners for WooPay in eligible countries. [#39596](https://github.com/woocommerce/woocommerce/pull/39596)
|
||||
* Update - Update WooCommerce Blocks to 10.9.0 [#39783](https://github.com/woocommerce/woocommerce/pull/39783)
|
||||
* Update - Update WooCommerce Blocks to 10.9.2 [#39828](https://github.com/woocommerce/woocommerce/pull/39828)
|
||||
* Update - Use the same checkbox style on the platform selctor [#39469](https://github.com/woocommerce/woocommerce/pull/39469)
|
||||
* Dev - Added a unit test for plugin feature compatibility data in WC Tracker [#38931](https://github.com/woocommerce/woocommerce/pull/38931)
|
||||
* Dev - Added storybook for core profiler pages [#39046](https://github.com/woocommerce/woocommerce/pull/39046)
|
||||
* Dev - Fixed TS type error for state machine Context in Core Profiler that only got caught after TS5 upgrade [#39749](https://github.com/woocommerce/woocommerce/pull/39749)
|
||||
* Dev - Fixes a failing e2e test in our daily test runs. [#39674](https://github.com/woocommerce/woocommerce/pull/39674)
|
||||
* Dev - Fix flaky E2E tests in analytics-overview.spec.js. [#39308](https://github.com/woocommerce/woocommerce/pull/39308)
|
||||
* Dev - Optimized the System Status Report unit tests. [#39363](https://github.com/woocommerce/woocommerce/pull/39363)
|
||||
* Dev - Refactored some core profiler utils out to reuse them in customise your store. [#39581](https://github.com/woocommerce/woocommerce/pull/39581)
|
||||
* Dev - Remove dependency on e2e-environment and e2e-utils in wc-admin. [#39746](https://github.com/woocommerce/woocommerce/pull/39746)
|
||||
* Dev - Remove the non-existing method from TaskList docs. [#39454](https://github.com/woocommerce/woocommerce/pull/39454)
|
||||
* Dev - Remove unused variation option components [#39673](https://github.com/woocommerce/woocommerce/pull/39673)
|
||||
* Dev - Runs all API tests on daily run. Skips failing tests on CI. [#39351](https://github.com/woocommerce/woocommerce/pull/39351)
|
||||
* Dev - Shard the unit tests into two test suites. [#39362](https://github.com/woocommerce/woocommerce/pull/39362)
|
||||
* Dev - Simplify user id retrieval in analytics-overview.spec.js. [#39472](https://github.com/woocommerce/woocommerce/pull/39472)
|
||||
* Dev - Update pnpm to 8.6.7 [#39245](https://github.com/woocommerce/woocommerce/pull/39245)
|
||||
* Dev - Upgrade TypeScript to 5.1.6 [#39531](https://github.com/woocommerce/woocommerce/pull/39531)
|
||||
* Dev - [Product Block Editor] Disable tabs in parent product page with variations #39459 [#39675](https://github.com/woocommerce/woocommerce/pull/39675)
|
||||
* Dev - [Product Block Editor] Disable the new editor for variable products. [#39780](https://github.com/woocommerce/woocommerce/pull/39780)
|
||||
* Tweak - Add loading indicator when submitting location in Tax task [#39613](https://github.com/woocommerce/woocommerce/pull/39613)
|
||||
* Tweak - Center align checkbox, logo, and the title on the plugins page (core profiler) [#39394](https://github.com/woocommerce/woocommerce/pull/39394)
|
||||
* Tweak - Do not run 'woocommerce_process_shop_order_meta' for order post when HPOS is authoritative. [#39587](https://github.com/woocommerce/woocommerce/pull/39587)
|
||||
* Tweak - Fix TikTok naming. [#39748](https://github.com/woocommerce/woocommerce/pull/39748)
|
||||
* Tweak - Modified the error message shown to customers in the event that no payment gateways are available. [#39348](https://github.com/woocommerce/woocommerce/pull/39348)
|
||||
* Tweak - Remove subheading letter-spacing from the core profiler pages. [#39526](https://github.com/woocommerce/woocommerce/pull/39526)
|
||||
* Tweak - Run A/B test on the core profiler plugins page -- suggest Jetpack or Jetpack Boost [#39799](https://github.com/woocommerce/woocommerce/pull/39799)
|
||||
* Tweak - Safety measures to prevent future breakages when executing bulk actions in the order list table (HPOS). [#39524](https://github.com/woocommerce/woocommerce/pull/39524)
|
||||
* Tweak - When all plugins are deselected, but Jetpack is already installed and not connected, redirect users to the Jetpack Connect page. [#39109](https://github.com/woocommerce/woocommerce/pull/39109)
|
||||
* Tweak - When HPOS is authoritative, execute order update logic earlier during the request. [#39590](https://github.com/woocommerce/woocommerce/pull/39590)
|
||||
* Performance - Use direct meta calls for backfilling instead of expensive object update. [#39450](https://github.com/woocommerce/woocommerce/pull/39450)
|
||||
* Enhancement - Add filter `woocommerce_pre_delete_{object_type}` which allows preventing deletion.' [#39650](https://github.com/woocommerce/woocommerce/pull/39650)
|
||||
* Enhancement - Create the Organization tab [#39232](https://github.com/woocommerce/woocommerce/pull/39232)
|
||||
* Enhancement - Modify order index to also include date for faster order list query. [#39682](https://github.com/woocommerce/woocommerce/pull/39682)
|
||||
* Enhancement - Update the admin's menu remaining tasks bubble CSS class and handling [#39273](https://github.com/woocommerce/woocommerce/pull/39273)
|
||||
|
||||
|
||||
= 8.0.3 2023-08-29 =
|
||||
|
||||
* Update - Bump WooCommerce Blocks to 10.6.6. [#39853](https://github.com/woocommerce/woocommerce/pull/39853)
|
||||
|
|
|
@ -63,7 +63,7 @@ Starting with these three broad lifecycle areas, you can begin to break your ext
|
|||
|
||||
## Handling activation and deactivation
|
||||
|
||||
A common pattern in WooCommerce extensions is to create dedicated functions in your main PHP file to serve as activation and deactivation hooks. You then register these hooks with WordPress using the applicable registration function. This tells WordPess to call the function when the plugin is activated or deactivated. Consider the following examples:
|
||||
A common pattern in WooCommerce extensions is to create dedicated functions in your main PHP file to serve as activation and deactivation hooks. You then register these hooks with WordPress using the applicable registration function. This tells WordPress to call the function when the plugin is activated or deactivated. Consider the following examples:
|
||||
|
||||
```php
|
||||
function my_extension_activate() {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Allow jest to pass with no tests
|
|
@ -73,7 +73,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"turbo:build": "pnpm run build:js && pnpm run build:css",
|
||||
"turbo:test": "jest --config ./jest.config.json",
|
||||
"turbo:test": "jest --config ./jest.config.json --passWithNoTests",
|
||||
"prepare": "composer install",
|
||||
"changelog": "composer exec -- changelogger",
|
||||
"clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*",
|
||||
|
|
|
@ -10,8 +10,6 @@ Install the module
|
|||
pnpm install @woocommerce/components --save
|
||||
```
|
||||
|
||||
View [the full Component documentation](https://woocommerce.github.io/woocommerce-admin/#/components/) for usage information.
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add tags (or general taxonomy ) block
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add AI wizard business info step for Customize Your Store task
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add ProgressBar component
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: tweak
|
||||
|
||||
Remove unnecessary use of woocommerce-page selector for DropdownButton styling.
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
Comment: Decode HTML escaped string for tree-item and selected-items components
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
Comment: Just a minor README change.
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.woocommerce-page .woocommerce-dropdown-button {
|
||||
.woocommerce-dropdown-button {
|
||||
background-color: $studio-white;
|
||||
position: relative;
|
||||
border: 1px solid $gray-700;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
import classnames from 'classnames';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -43,7 +44,7 @@ export const SelectedItems = < ItemType, >( {
|
|||
<div className={ classes }>
|
||||
{ items
|
||||
.map( ( item ) => {
|
||||
return getItemLabel( item );
|
||||
return decodeEntities( getItemLabel( item ) );
|
||||
} )
|
||||
.join( ', ' ) }
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
import { chevronDown } from '@wordpress/icons';
|
||||
import classNames from 'classnames';
|
||||
import { createElement, useState } from '@wordpress/element';
|
||||
import { createElement, useEffect, useState } from '@wordpress/element';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { BaseControl, TextControl } from '@wordpress/components';
|
||||
|
||||
|
@ -25,6 +25,7 @@ interface SelectTreeProps extends TreeControlProps {
|
|||
isLoading?: boolean;
|
||||
label: string | JSX.Element;
|
||||
onInputChange?: ( value: string | undefined ) => void;
|
||||
initialInputValue?: string | undefined;
|
||||
}
|
||||
|
||||
export const SelectTree = function SelectTree( {
|
||||
|
@ -33,6 +34,7 @@ export const SelectTree = function SelectTree( {
|
|||
suffix = <SuffixIcon icon={ chevronDown } />,
|
||||
placeholder,
|
||||
isLoading,
|
||||
initialInputValue,
|
||||
onInputChange,
|
||||
shouldShowCreateButton,
|
||||
...props
|
||||
|
@ -73,6 +75,12 @@ export const SelectTree = function SelectTree( {
|
|||
const [ inputValue, setInputValue ] = useState( '' );
|
||||
const isReadOnly = ! isOpen && ! isFocused;
|
||||
|
||||
useEffect( () => {
|
||||
if ( initialInputValue !== undefined && isFocused ) {
|
||||
setInputValue( initialInputValue as string );
|
||||
}
|
||||
}, [ isFocused ] );
|
||||
|
||||
const inputProps: React.InputHTMLAttributes< HTMLInputElement > = {
|
||||
className: 'woocommerce-experimental-select-control__input',
|
||||
id: `${ props.id }-input`,
|
||||
|
@ -211,6 +219,7 @@ export const SelectTree = function SelectTree( {
|
|||
className={ menuInstanceId.toString() }
|
||||
ref={ ref }
|
||||
isEventOutside={ isEventOutside }
|
||||
isLoading={ isLoading }
|
||||
isOpen={ isOpen }
|
||||
items={ linkedTree }
|
||||
shouldShowCreateButton={ shouldShowCreateButton }
|
||||
|
|
|
@ -6,6 +6,7 @@ import { __ } from '@wordpress/i18n';
|
|||
import { chevronDown, chevronUp } from '@wordpress/icons';
|
||||
import classNames from 'classnames';
|
||||
import { createElement, forwardRef } from 'react';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -72,7 +73,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
|
|||
{ typeof getLabel === 'function' ? (
|
||||
getLabel( item )
|
||||
) : (
|
||||
<span>{ item.data.label }</span>
|
||||
<span>{ decodeEntities( item.data.label ) }</span>
|
||||
) }
|
||||
</label>
|
||||
|
||||
|
|
|
@ -111,3 +111,4 @@ export {
|
|||
ProductFieldSection as __experimentalProductFieldSection,
|
||||
} from './product-section-layout';
|
||||
export { DisplayState } from './display-state';
|
||||
export { ProgressBar } from './progress-bar';
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
||||
import { HTMLAttributes, createElement } from 'react';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
||||
type ProgressBarProps = {
|
||||
className?: string;
|
||||
percent?: number;
|
||||
color?: string;
|
||||
bgcolor?: string;
|
||||
};
|
||||
|
||||
export const ProgressBar = ( {
|
||||
className = '',
|
||||
percent = 0,
|
||||
color = '#674399',
|
||||
bgcolor = 'var(--wp-admin-theme-color)',
|
||||
}: ProgressBarProps ) => {
|
||||
const containerStyles = {
|
||||
backgroundColor: bgcolor,
|
||||
};
|
||||
|
||||
const fillerStyles: HTMLAttributes< HTMLDivElement >[ 'style' ] = {
|
||||
backgroundColor: color,
|
||||
width: `${ percent }%`,
|
||||
display: percent === 0 ? 'none' : 'inherit',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={ `woocommerce-progress-bar ${ className }` }>
|
||||
<div
|
||||
className="woocommerce-progress-bar__container"
|
||||
style={ containerStyles }
|
||||
>
|
||||
<div
|
||||
className="woocommerce-progress-bar__filler"
|
||||
style={ fillerStyles }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { ProgressBar } from '@woocommerce/components';
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
||||
export const Basic = () => (
|
||||
<div style={ { background: '#fff', height: '200px', padding: '20px' } }>
|
||||
<ProgressBar percent={ 20 } bgcolor={ '#eeeeee' } color={ '#007cba' } />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default {
|
||||
title: 'WooCommerce Admin/components/ProgressBar',
|
||||
component: ProgressBar,
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
.woocommerce-progress-bar__container {
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Min width equal to height. This means small values look like each other, but all bars have a consistent radius.
|
||||
.woocommerce-progress-bar__filler {
|
||||
height: 100%;
|
||||
min-width: 8px;
|
||||
}
|
|
@ -59,3 +59,4 @@
|
|||
@import 'experimental-select-tree-control/select-tree.scss';
|
||||
@import 'product-section-layout/style.scss';
|
||||
@import 'tree-select-control/index.scss';
|
||||
@import 'progress-bar/style.scss';
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Update to use the template API.
|
|
@ -31,6 +31,9 @@
|
|||
* @package {{namespace}}
|
||||
*/
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\ProductFormTemplateInterface;
|
||||
|
||||
/**
|
||||
* Registers the block using the metadata loaded from the `block.json` file.
|
||||
* Behind the scenes, it registers also all assets so they can be enqueued
|
||||
|
@ -45,53 +48,22 @@ function {{namespaceSnakeCase}}_{{slugSnakeCase}}_block_init() {
|
|||
}
|
||||
add_action( 'init', '{{namespaceSnakeCase}}_{{slugSnakeCase}}_block_init' );
|
||||
|
||||
function {{namespaceSnakeCase}}_{{slugSnakeCase}}_add_block_to_product_editor( $args ) {
|
||||
// if the product block editor is not enabled, return the args as-is
|
||||
if ( ! class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ||
|
||||
! \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
|
||||
return $args;
|
||||
}
|
||||
function {{namespaceSnakeCase}}_{{slugSnakeCase}}_add_block_to_product_editor( BlockTemplateInterface $template ) {
|
||||
if ( $template instanceof ProductFormTemplateInterface && 'simple-product' === $template->get_id() ) {
|
||||
$basic_details = $template->get_section_by_id( 'basic-details' );
|
||||
|
||||
// if the template is not set or is not an array, return the args as-is
|
||||
if ( ! isset( $args['template'] ) || ! is_array( $args['template'] ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$template = $args['template'];
|
||||
|
||||
// find the 'Basic details' section and add our block to the end of it
|
||||
foreach ( $template as $tab_index => $tab ) {
|
||||
$tab_properties = $tab[1];
|
||||
|
||||
if ( 'general' === $tab_properties['id'] ) {
|
||||
$tab_sections = $tab[2];
|
||||
|
||||
foreach ( $tab_sections as $section_index => $section ) {
|
||||
$section_properties = $section[1];
|
||||
|
||||
// TODO: this is not the right way to do this, since it is checking a localized string.
|
||||
if ( 'Basic details' === $section_properties['title'] ) {
|
||||
$section_fields = $section[2];
|
||||
|
||||
// add our block to the end of the section
|
||||
$section_fields[] = [
|
||||
'{{namespace}}/{{slug}}',
|
||||
[
|
||||
'message' => '{{title}}',
|
||||
]
|
||||
];
|
||||
|
||||
// update the template with our new block
|
||||
$args[ 'template' ][ $tab_index ][2][ $section_index ][2] = $section_fields;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
if ( $basic_details ) {
|
||||
$basic_details->add_block(
|
||||
[
|
||||
'id' => '{{namespace}}-{{slug}}',
|
||||
'order' => 40,
|
||||
'blockName' => '{{namespace}}/{{slug}}',
|
||||
'attributes' => [
|
||||
'message' => '{{title}}',
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
add_filter( 'woocommerce_register_post_type_product', '{{namespaceSnakeCase}}_{{slugSnakeCase}}_add_block_to_product_editor', 100 );
|
||||
add_filter( 'woocommerce_block_template_register', '{{namespaceSnakeCase}}_{{slugSnakeCase}}_add_block_to_product_editor', 100 );
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add tags (or general taxonomy ) block
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Update TaskItem type to include `badge` prop.
|
|
@ -107,6 +107,7 @@ export {
|
|||
ProductCategorySelectors,
|
||||
} from './product-categories/types';
|
||||
export { TaxClass } from './tax-classes/types';
|
||||
export { ProductTag, Query } from './product-tags/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
|
|
@ -26,6 +26,7 @@ export type TaskType = {
|
|||
eventPrefix: string;
|
||||
level: number;
|
||||
recordViewEvent: boolean;
|
||||
badge?: string;
|
||||
additionalData?: {
|
||||
woocommerceTaxCountries?: string[];
|
||||
taxJarActivated?: boolean;
|
||||
|
|
|
@ -8,7 +8,7 @@ import { DispatchFromMap } from '@automattic/data-stores';
|
|||
*/
|
||||
import { CrudActions, CrudSelectors } from '../crud/types';
|
||||
|
||||
type ProductTag = {
|
||||
export type ProductTag = {
|
||||
id: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
|
@ -17,7 +17,7 @@ type ProductTag = {
|
|||
count: number;
|
||||
};
|
||||
|
||||
type Query = {
|
||||
export type Query = {
|
||||
context?: string;
|
||||
page: number;
|
||||
per_page: number;
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Schema } from '@wordpress/core-data';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { ProductCategory } from '../product-categories/types';
|
||||
import { ProductTag } from '../product-tags/types';
|
||||
import { BaseQueryParams } from '../types';
|
||||
|
||||
export type ProductType = 'simple' | 'grouped' | 'external' | 'variable';
|
||||
|
@ -120,6 +121,7 @@ export type Product< Status = ProductStatus, Type = ProductType > = Omit<
|
|||
status: Status;
|
||||
stock_quantity: number;
|
||||
stock_status: 'instock' | 'outofstock' | 'onbackorder';
|
||||
tags: Pick< ProductTag, 'id' | 'name' >[];
|
||||
tax_class: 'standard' | 'reduced-rate' | 'zero-rate' | undefined;
|
||||
tax_status: 'taxable' | 'shipping' | 'none';
|
||||
total_sales: number;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Update TaskItem to include a badge next to the title. Update also related components TaskList and SetupTaskList, as well as docs, storybook, and tests.
|
|
@ -140,6 +140,7 @@ export const TaskItemExample: Story = ( args ) => (
|
|||
}
|
||||
showActionButton={ false }
|
||||
title="A high-priority task without `Primary action`"
|
||||
badge="Badge content"
|
||||
/>
|
||||
<TaskItem
|
||||
action={ () => {} }
|
||||
|
|
|
@ -117,6 +117,10 @@ a.woocommerce-experimental-list__item {
|
|||
color: $foreground-color-hover;
|
||||
}
|
||||
|
||||
.woocommerce-task-list__item-badge {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.woocommerce-list__item-before > svg {
|
||||
fill: $foreground-color-hover;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ Use `TaskItem` to display a task item.
|
|||
action={ () => alert( '"My action" button has been clicked' ) }
|
||||
actionLabel="My action"
|
||||
additionalInfo="Additional task information"
|
||||
badge="Task badge"
|
||||
completed={ true }
|
||||
content="Task content"
|
||||
expandable={ false }
|
||||
|
@ -33,6 +34,7 @@ Use `TaskItem` to display a task item.
|
|||
| `action` | Function | `null` | A function to be called when the primary action is triggered |
|
||||
| `actionLabel` | String | `null` | Primary action label |
|
||||
| `additionalInfo` | String | `null` | Additional task information |
|
||||
| `badge` | String | `null` | Task badge to show next to title |
|
||||
| `completed` | Boolean | `null` | Whether the task is completed or not |
|
||||
| `content` | String | `null` | Task content |
|
||||
| `expandable` | Boolean | `false` | Whether it's an expandable task |
|
||||
|
|
|
@ -45,6 +45,7 @@ type TaskItemProps = {
|
|||
onDismiss?: () => void;
|
||||
onSnooze?: () => void;
|
||||
onExpand?: () => void;
|
||||
badge?: string;
|
||||
additionalInfo?: string;
|
||||
time?: string;
|
||||
content: string;
|
||||
|
@ -107,6 +108,7 @@ const OptionalExpansionWrapper: React.FC< {
|
|||
export const TaskItem: React.FC< TaskItemProps > = ( {
|
||||
completed,
|
||||
title,
|
||||
badge,
|
||||
onDelete,
|
||||
onCollapse,
|
||||
onDismiss,
|
||||
|
@ -185,6 +187,11 @@ export const TaskItem: React.FC< TaskItemProps > = ( {
|
|||
>
|
||||
<span className="woocommerce-task-list__item-title">
|
||||
{ title }
|
||||
{ badge && (
|
||||
<span className="woocommerce-task-list__item-badge">
|
||||
{ badge }
|
||||
</span>
|
||||
) }
|
||||
</span>
|
||||
<OptionalExpansionWrapper
|
||||
expandable={ expandable }
|
||||
|
|
|
@ -39,6 +39,20 @@ $task-alert-yellow: #f0b849;
|
|||
|
||||
.woocommerce-task-list__item-title {
|
||||
color: $foreground-color;
|
||||
display: flex;
|
||||
column-gap: $gap-small;
|
||||
row-gap: $gap-smallest;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.woocommerce-task-list__item-badge {
|
||||
padding: 0 10px;
|
||||
background-color: #f6f7f7;
|
||||
border-radius: 2px;
|
||||
color: $gray-800;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.woocommerce-task__additional-info,
|
||||
|
|
|
@ -3,6 +3,16 @@
|
|||
*/
|
||||
const path = require( 'path' );
|
||||
|
||||
// These modules need to be transformed because they are not transpiled to CommonJS.
|
||||
const transformModules = [ 'is-plain-obj', 'memize' ];
|
||||
// Ignore all node_modules except for the ones we need to transform.
|
||||
const transformIgnorePatterns = [
|
||||
`node_modules/(?!.pnpm/${ transformModules.join(
|
||||
'|.pnpm/'
|
||||
) }|${ transformModules.join( '|' ) })`,
|
||||
'/build/',
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
moduleNameMapper: {
|
||||
tinymce: path.resolve( __dirname, 'build/mocks/tinymce' ),
|
||||
|
@ -44,12 +54,10 @@ module.exports = {
|
|||
'<rootDir>/.*/build-module/',
|
||||
'<rootDir>/tests/e2e/',
|
||||
],
|
||||
transformIgnorePatterns: [
|
||||
`node_modules/(?!.pnpm/is-plain-obj|is-plain-obj)`,
|
||||
'/build/',
|
||||
],
|
||||
transformIgnorePatterns,
|
||||
transform: {
|
||||
'^.+\\is-plain-obj/index\\.js$': 'babel-jest',
|
||||
'^.+\\memize/dist/index\\.js$': 'babel-jest',
|
||||
'^.+\\.[jt]sx?$': 'ts-jest',
|
||||
},
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add tags (or general taxonomy ) block
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Auto select one or more attribute terms when selecting an attribute
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add 'Show in product details' checkbox under Edit attribute modal
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Show sale price and list price in each variation row when the variation is on sale
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
|
||||
Add dialogNameHelpText attribute to product-taxonomy-field block
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Remove use of head prop with IFrame component as it has been removed.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Fix delete variation in block product editor
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Fix infinite category loading state
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Rename woocommerce/taxonomy-field to woocommerce/product-taxonomy-field
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Remove __experimentalDetailsCategoriesField and woocommerce/product-category-field block
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Remove visibility toggle from variations table and remove with tooltip icon.
|
|
@ -1,5 +1,4 @@
|
|||
export { init as initCatalogVisibility } from './catalog-visibility';
|
||||
export { init as initCategory } from './category';
|
||||
export { init as initCheckbox } from './checkbox';
|
||||
export { init as initCollapsible } from './collapsible';
|
||||
export { init as initConditional } from './conditional';
|
||||
|
@ -18,6 +17,7 @@ export { init as initShippingClass } from './shipping-class';
|
|||
export { init as initShippingDimensions } from './shipping-dimensions';
|
||||
export { init as initSummary } from './summary';
|
||||
export { init as initTab } from './tab';
|
||||
export { init as initTag } from './tag';
|
||||
export { init as initInventoryQuantity } from './inventory-quantity';
|
||||
export { init as initToggle } from './toggle';
|
||||
export { init as attributesInit } from './attributes';
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
@import 'attributes/editor.scss';
|
||||
@import 'category/editor.scss';
|
||||
@import 'checkbox/editor.scss';
|
||||
@import 'images/editor.scss';
|
||||
@import 'inventory-email/editor.scss';
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "woocommerce/product-category-field",
|
||||
"title": "Product Category",
|
||||
"name": "woocommerce/product-tag-field",
|
||||
"title": "Product Tag",
|
||||
"category": "widgets",
|
||||
"description": "A field to select product categories.",
|
||||
"keywords": [ "products", "category" ],
|
||||
"description": "A field to select product tags.",
|
||||
"keywords": [ "products", "tag" ],
|
||||
"textdomain": "default",
|
||||
"attributes": {
|
||||
"name": {
|
|
@ -5,7 +5,9 @@ import { __ } from '@wordpress/i18n';
|
|||
import { createElement } from '@wordpress/element';
|
||||
import { BlockAttributes } from '@wordpress/blocks';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { ProductCategory } from '@woocommerce/data';
|
||||
import { BaseControl } from '@wordpress/components';
|
||||
import { ProductTag } from '@woocommerce/data';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore No types for this exist yet.
|
||||
// eslint-disable-next-line @woocommerce/dependency-group
|
||||
|
@ -14,7 +16,7 @@ import { useEntityProp } from '@wordpress/core-data';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CategoryField } from '../../components/details-categories-field';
|
||||
import { TagField } from '../../components/tags-field';
|
||||
|
||||
export function Edit( {
|
||||
attributes,
|
||||
|
@ -25,21 +27,26 @@ export function Edit( {
|
|||
} ) {
|
||||
const blockProps = useBlockProps();
|
||||
const { name, label, placeholder } = attributes;
|
||||
const [ categories, setCategories ] = useEntityProp<
|
||||
Pick< ProductCategory, 'name' | 'id' | 'parent' >[]
|
||||
>( 'postType', context?.postType || 'product', name || 'categories' );
|
||||
const [ tags, setTags ] = useEntityProp<
|
||||
Pick< ProductTag, 'id' | 'name' >[]
|
||||
>( 'postType', context?.postType || 'product', name || 'tags' );
|
||||
|
||||
const tagFieldId = useInstanceId( BaseControl, 'tag-field' ) as string;
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<CategoryField
|
||||
label={ label || __( 'Categories', 'woocommerce' ) }
|
||||
placeholder={
|
||||
placeholder ||
|
||||
__( 'Search or create category…', 'woocommerce' )
|
||||
}
|
||||
onChange={ setCategories }
|
||||
value={ categories || [] }
|
||||
/>
|
||||
{
|
||||
<TagField
|
||||
id={ tagFieldId }
|
||||
label={ label || __( 'Tags', 'woocommerce' ) }
|
||||
placeholder={
|
||||
placeholder ||
|
||||
__( 'Search or create tags…', 'woocommerce' )
|
||||
}
|
||||
onChange={ setTags }
|
||||
value={ tags || [] }
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
.wp-block-woocommerce-product-category-field {
|
||||
.wp-block-woocommerce-product-tag-field {
|
||||
.woocommerce-experimental-select-tree-control__menu {
|
||||
.experimental-woocommerce-tree-item {
|
||||
font-size: 13px;
|
||||
.components-checkbox-control__input-container, input[type='checkbox'], .components-checkbox-control__checked {
|
||||
.components-checkbox-control__input-container,
|
||||
input[type="checkbox"],
|
||||
.components-checkbox-control__checked {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
# woocommerce/taxonomy-field block
|
||||
# woocommerce/product-taxonomy-field block
|
||||
|
||||
This is a block that displays a taxonomy field, allowing searching, selection, and creation of new items, to be used in a product context.
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "woocommerce/taxonomy-field",
|
||||
"name": "woocommerce/product-taxonomy-field",
|
||||
"title": "Taxonomy",
|
||||
"category": "widgets",
|
||||
"description": "A block that displays a taxonomy field, allowing searching, selection, and creation of new items",
|
||||
|
@ -23,6 +23,14 @@
|
|||
"createTitle": {
|
||||
"type": "string",
|
||||
"__experimentalRole": "content"
|
||||
},
|
||||
"dialogNameHelpText": {
|
||||
"type": "string",
|
||||
"__experimentalRole": "content"
|
||||
},
|
||||
"parentTaxonomyText": {
|
||||
"type": "string",
|
||||
"__experimentalRole": "content"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
|
|
|
@ -26,6 +26,8 @@ import useTaxonomySearch from './use-taxonomy-search';
|
|||
|
||||
type CreateTaxonomyModalProps = {
|
||||
initialName?: string;
|
||||
dialogNameHelpText?: string;
|
||||
parentTaxonomyText?: string;
|
||||
hierarchical: boolean;
|
||||
slug: string;
|
||||
title: string;
|
||||
|
@ -39,6 +41,8 @@ export const CreateTaxonomyModal: React.FC< CreateTaxonomyModalProps > = ( {
|
|||
initialName,
|
||||
slug,
|
||||
hierarchical,
|
||||
dialogNameHelpText,
|
||||
parentTaxonomyText,
|
||||
title,
|
||||
} ) => {
|
||||
const [ categoryParentTypedValue, setCategoryParentTypedValue ] =
|
||||
|
@ -67,6 +71,7 @@ export const CreateTaxonomyModal: React.FC< CreateTaxonomyModalProps > = ( {
|
|||
|
||||
const onSave = async () => {
|
||||
setErrorMessage( null );
|
||||
setIsCreating( true );
|
||||
try {
|
||||
const newTaxonomy: Taxonomy = await saveEntityRecord(
|
||||
'taxonomy',
|
||||
|
@ -79,6 +84,7 @@ export const CreateTaxonomyModal: React.FC< CreateTaxonomyModalProps > = ( {
|
|||
throwOnError: true,
|
||||
}
|
||||
);
|
||||
setIsCreating( false );
|
||||
onCreate( newTaxonomy );
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch ( e: any ) {
|
||||
|
@ -111,7 +117,7 @@ export const CreateTaxonomyModal: React.FC< CreateTaxonomyModalProps > = ( {
|
|||
<BaseControl
|
||||
id={ id }
|
||||
label={ __( 'Name', 'woocommerce' ) }
|
||||
help={ errorMessage }
|
||||
help={ errorMessage || dialogNameHelpText }
|
||||
className={ classNames( {
|
||||
'has-error': errorMessage,
|
||||
} ) }
|
||||
|
@ -126,10 +132,13 @@ export const CreateTaxonomyModal: React.FC< CreateTaxonomyModalProps > = ( {
|
|||
<SelectTree
|
||||
isLoading={ isResolving }
|
||||
label={ createInterpolateElement(
|
||||
__( 'Parent <optional/>', 'woocommerce' ),
|
||||
`${
|
||||
parentTaxonomyText ||
|
||||
__( 'Parent', 'woocommerce' )
|
||||
} <optional/>`,
|
||||
{
|
||||
optional: (
|
||||
<span className="woocommerce-product-form__optional-input">
|
||||
<span className="woocommerce-create-new-taxonomy-modal__optional">
|
||||
{ __( '(optional)', 'woocommerce' ) }
|
||||
</span>
|
||||
),
|
||||
|
@ -171,19 +180,19 @@ export const CreateTaxonomyModal: React.FC< CreateTaxonomyModalProps > = ( {
|
|||
) }
|
||||
<div className="woocommerce-create-new-taxonomy-modal__buttons">
|
||||
<Button
|
||||
isSecondary
|
||||
variant="tertiary"
|
||||
onClick={ onCancel }
|
||||
disabled={ isCreating }
|
||||
>
|
||||
{ __( 'Cancel', 'woocommerce' ) }
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary
|
||||
variant="primary"
|
||||
disabled={ name.length === 0 || isCreating }
|
||||
isBusy={ isCreating }
|
||||
onClick={ onSave }
|
||||
>
|
||||
{ __( 'Save', 'woocommerce' ) }
|
||||
{ __( 'Create', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -28,6 +28,8 @@ interface TaxonomyBlockAttributes extends BlockAttributes {
|
|||
slug: string;
|
||||
property: string;
|
||||
createTitle: string;
|
||||
dialogNameHelpText?: string;
|
||||
parentTaxonomyText?: string;
|
||||
}
|
||||
|
||||
export function Edit( {
|
||||
|
@ -42,7 +44,14 @@ export function Edit( {
|
|||
hierarchical: false,
|
||||
}
|
||||
);
|
||||
const { label, slug, property, createTitle } = attributes;
|
||||
const {
|
||||
label,
|
||||
slug,
|
||||
property,
|
||||
createTitle,
|
||||
dialogNameHelpText,
|
||||
parentTaxonomyText,
|
||||
} = attributes;
|
||||
const [ searchValue, setSearchValue ] = useState( '' );
|
||||
const [ allEntries, setAllEntries ] = useState< Taxonomy[] >( [] );
|
||||
|
||||
|
@ -162,6 +171,8 @@ export function Edit( {
|
|||
slug={ slug }
|
||||
hierarchical={ hierarchical }
|
||||
title={ createTitle }
|
||||
dialogNameHelpText={ dialogNameHelpText }
|
||||
parentTaxonomyText={ parentTaxonomyText }
|
||||
onCancel={ () => setShowCreateNewModal( false ) }
|
||||
onCreate={ ( taxonomy ) => {
|
||||
setShowCreateNewModal( false );
|
||||
|
|
|
@ -10,7 +10,21 @@
|
|||
gap: $gap-smaller;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__optional {
|
||||
color: $gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
.components-base-control {
|
||||
margin-bottom: $gap;
|
||||
&__field {
|
||||
.components-base-control {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.has-error {
|
||||
.components-text-control__input {
|
||||
border-color: $studio-red-50;
|
||||
|
|
|
@ -69,7 +69,7 @@ const useTaxonomySearch = (
|
|||
taxonomyName
|
||||
);
|
||||
}
|
||||
} catch ( e ) {
|
||||
} finally {
|
||||
setIsSearching( false );
|
||||
}
|
||||
return taxonomies;
|
||||
|
|
|
@ -120,6 +120,7 @@ export function Edit() {
|
|||
disabledAttributeIds={ entityAttributes
|
||||
.filter( ( attr ) => ! attr.variation )
|
||||
.map( ( attr ) => attr.id ) }
|
||||
termsAutoSelection="all"
|
||||
uiStrings={ {
|
||||
notice,
|
||||
globalAttributeHelperMessage: '',
|
||||
|
|
|
@ -158,6 +158,7 @@ export function Edit( {
|
|||
disabledAttributeIds={ productAttributes
|
||||
.filter( ( attr ) => ! attr.variation )
|
||||
.map( ( attr ) => attr.id ) }
|
||||
termsAutoSelection="all"
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
|
|
|
@ -50,6 +50,8 @@ type AttributeControlProps = {
|
|||
createNewAttributesAsGlobal?: boolean;
|
||||
useRemoveConfirmationModal?: boolean;
|
||||
disabledAttributeIds?: number[];
|
||||
termsAutoSelection?: 'first' | 'all';
|
||||
defaultVisibility?: boolean;
|
||||
uiStrings?: {
|
||||
notice?: string | React.ReactElement;
|
||||
emptyStateSubtitle?: string;
|
||||
|
@ -84,6 +86,8 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
|
|||
createNewAttributesAsGlobal = false,
|
||||
useRemoveConfirmationModal = false,
|
||||
disabledAttributeIds = [],
|
||||
termsAutoSelection,
|
||||
defaultVisibility = false,
|
||||
} ) => {
|
||||
uiStrings = {
|
||||
newAttributeListItemLabel: __( 'Add new', 'woocommerce' ),
|
||||
|
@ -307,6 +311,8 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
|
|||
disabledAttributeMessage={
|
||||
uiStrings.disabledAttributeMessage
|
||||
}
|
||||
termsAutoSelection={ termsAutoSelection }
|
||||
defaultVisibility={ defaultVisibility }
|
||||
/>
|
||||
) }
|
||||
<SelectControlMenuSlot />
|
||||
|
|
|
@ -65,9 +65,9 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
|||
'Check to allow customers to search and filter by this option in your store.',
|
||||
'woocommerce'
|
||||
),
|
||||
visibleLabel = __( 'Visible to customers', 'woocommerce' ),
|
||||
visibleLabel = __( 'Show in product details', 'woocommerce' ),
|
||||
visibleTooltip = __(
|
||||
'Show or hide this attribute on the product page',
|
||||
'Check to show this option and its values in the product details section on the product page.',
|
||||
'woocommerce'
|
||||
),
|
||||
cancelAccessibleLabel = __( 'Cancel', 'woocommerce' ),
|
||||
|
|
|
@ -8,12 +8,17 @@ import {
|
|||
Fragment,
|
||||
useEffect,
|
||||
} from '@wordpress/element';
|
||||
import { resolveSelect } from '@wordpress/data';
|
||||
import { trash } from '@wordpress/icons';
|
||||
import {
|
||||
Form,
|
||||
__experimentalSelectControlMenuSlot as SelectControlMenuSlot,
|
||||
} from '@woocommerce/components';
|
||||
import { ProductAttributeTerm } from '@woocommerce/data';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME,
|
||||
ProductAttribute,
|
||||
ProductAttributeTerm,
|
||||
} from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import {
|
||||
Button,
|
||||
|
@ -57,6 +62,8 @@ type NewAttributeModalProps = {
|
|||
createNewAttributesAsGlobal?: boolean;
|
||||
disabledAttributeIds?: number[];
|
||||
disabledAttributeMessage?: string;
|
||||
termsAutoSelection?: 'first' | 'all';
|
||||
defaultVisibility?: boolean;
|
||||
};
|
||||
|
||||
type AttributeForm = {
|
||||
|
@ -95,6 +102,8 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( {
|
|||
'Already used in Attributes',
|
||||
'woocommerce'
|
||||
),
|
||||
termsAutoSelection,
|
||||
defaultVisibility = false,
|
||||
} ) => {
|
||||
const scrollAttributeIntoView = ( index: number ) => {
|
||||
setTimeout( () => {
|
||||
|
@ -152,7 +161,7 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( {
|
|||
};
|
||||
|
||||
const getVisibleOrTrue = ( attribute: EnhancedProductAttribute ) =>
|
||||
attribute.visible !== undefined ? attribute.visible : true;
|
||||
attribute.visible !== undefined ? attribute.visible : defaultVisibility;
|
||||
|
||||
const onAddingAttributes = ( values: AttributeForm ) => {
|
||||
const newAttributesToAdd: EnhancedProductAttribute[] = [];
|
||||
|
@ -242,6 +251,61 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( {
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
setValue: ( name: string, value: any ) => void;
|
||||
} ) => {
|
||||
function getAttributeOnChange( index: number ) {
|
||||
return function handleAttributeChange(
|
||||
value?:
|
||||
| Omit<
|
||||
ProductAttribute,
|
||||
'position' | 'visible' | 'variation'
|
||||
>
|
||||
| string
|
||||
) {
|
||||
if (
|
||||
termsAutoSelection &&
|
||||
value &&
|
||||
! ( typeof value === 'string' )
|
||||
) {
|
||||
resolveSelect(
|
||||
EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME
|
||||
)
|
||||
.getProductAttributeTerms<
|
||||
ProductAttributeTerm[]
|
||||
>( {
|
||||
// Send search parameter as empty to avoid a second
|
||||
// request when focusing the attribute-term-input-field
|
||||
// which perform the same request to get all the terms
|
||||
search: '',
|
||||
attribute_id: value.id,
|
||||
} )
|
||||
.then( ( terms ) => {
|
||||
const selectedAttribute =
|
||||
getProductAttributeObject(
|
||||
value
|
||||
) as EnhancedProductAttribute;
|
||||
if ( termsAutoSelection === 'all' ) {
|
||||
selectedAttribute.terms = terms;
|
||||
} else if ( terms.length > 0 ) {
|
||||
selectedAttribute.terms = [
|
||||
terms[ 0 ],
|
||||
];
|
||||
}
|
||||
setValue( 'attributes[' + index + ']', {
|
||||
...selectedAttribute,
|
||||
} );
|
||||
focusValueField( index );
|
||||
} );
|
||||
} else {
|
||||
setValue(
|
||||
'attributes[' + index + ']',
|
||||
value && getProductAttributeObject( value )
|
||||
);
|
||||
if ( value ) {
|
||||
focusValueField( index );
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={ title }
|
||||
|
@ -289,24 +353,9 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( {
|
|||
label={
|
||||
attributeLabel
|
||||
}
|
||||
onChange={ (
|
||||
val
|
||||
) => {
|
||||
setValue(
|
||||
'attributes[' +
|
||||
index +
|
||||
']',
|
||||
val &&
|
||||
getProductAttributeObject(
|
||||
val
|
||||
)
|
||||
);
|
||||
if ( val ) {
|
||||
focusValueField(
|
||||
index
|
||||
);
|
||||
}
|
||||
} }
|
||||
onChange={ getAttributeOnChange(
|
||||
index
|
||||
) }
|
||||
ignoredAttributeIds={ [
|
||||
...selectedAttributeIds,
|
||||
...values.attributes
|
||||
|
@ -336,7 +385,7 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( {
|
|||
/>
|
||||
</td>
|
||||
<td className="woocommerce-new-attribute-modal__table-attribute-value-column">
|
||||
{ attribute === null ||
|
||||
{ ! attribute ||
|
||||
attribute.id !== 0 ? (
|
||||
<AttributeTermInputField
|
||||
placeholder={
|
||||
|
|
|
@ -12,9 +12,9 @@ import { createElement } from '@wordpress/element';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import HiddenIcon from '../variations-table/hidden-icon';
|
||||
import HelpIcon from './help-icon';
|
||||
import HelpIcon from '../../icons/help-icon';
|
||||
import NotFilterableIcon from './not-filterable-icon';
|
||||
import HiddenWithHelpIcon from '../../icons/hidden-with-help-icon';
|
||||
|
||||
type AttributeListItemProps = {
|
||||
attribute: ProductAttribute;
|
||||
|
@ -89,10 +89,7 @@ export const AttributeListItem: React.FC< AttributeListItemProps > = ( {
|
|||
position="top center"
|
||||
text={ NOT_VISIBLE_TEXT }
|
||||
>
|
||||
<div className="woocommerce-attribute-list-item__actions-icon-wrapper">
|
||||
<HiddenIcon className="woocommerce-attribute-list-item__actions-icon-wrapper-icon" />
|
||||
<HelpIcon className="woocommerce-attribute-list-item__actions-icon-wrapper-help-icon" />
|
||||
</div>
|
||||
<HiddenWithHelpIcon />
|
||||
</Tooltip>
|
||||
) }
|
||||
{ typeof onEditClick === 'function' && (
|
||||
|
|
|
@ -67,6 +67,8 @@ export const Attributes: React.FC< AttributesProps > = ( {
|
|||
'product_remove_attribute_confirmation_cancel_click'
|
||||
)
|
||||
}
|
||||
termsAutoSelection="first"
|
||||
defaultVisibility={ true }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -67,26 +67,22 @@ export function ContentPreview( { content }: ContentPreviewProps ) {
|
|||
|
||||
return (
|
||||
<div className="woocommerce-content-preview">
|
||||
<Iframe
|
||||
head={
|
||||
<>
|
||||
<EditorStyles styles={ parentEditorSettings?.styles } />
|
||||
<style>
|
||||
{ `body {
|
||||
<Iframe className="woocommerce-content-preview__iframe">
|
||||
<>
|
||||
<EditorStyles styles={ parentEditorSettings?.styles } />
|
||||
<style>
|
||||
{ `body {
|
||||
overflow: hidden;
|
||||
}` }
|
||||
</style>
|
||||
</>
|
||||
}
|
||||
className="woocommerce-content-preview__iframe"
|
||||
>
|
||||
<div
|
||||
className="woocommerce-content-preview__content"
|
||||
dangerouslySetInnerHTML={ sanitizeHTML( content, {
|
||||
tags: CONTENT_TAGS,
|
||||
attr: CONTENT_ATTR,
|
||||
} ) }
|
||||
/>
|
||||
</style>
|
||||
<div
|
||||
className="woocommerce-content-preview__content"
|
||||
dangerouslySetInnerHTML={ sanitizeHTML( content, {
|
||||
tags: CONTENT_TAGS,
|
||||
attr: CONTENT_ATTR,
|
||||
} ) }
|
||||
/>
|
||||
</>
|
||||
</Iframe>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Icon } from '@wordpress/components';
|
||||
import { plus } from '@wordpress/icons';
|
||||
import classNames from 'classnames';
|
||||
import { ProductCategory } from '@woocommerce/data';
|
||||
import { __experimentalSelectControlMenuItemProps as MenuItemProps } from '@woocommerce/components';
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
||||
type CategoryFieldAddNewItemProps = {
|
||||
item: Pick< ProductCategory, 'id' | 'name' >;
|
||||
highlightedIndex: number;
|
||||
items: Pick< ProductCategory, 'id' | 'name' >[];
|
||||
} & Pick<
|
||||
MenuItemProps< Pick< ProductCategory, 'id' | 'name' > >,
|
||||
'getItemProps'
|
||||
>;
|
||||
|
||||
export const CategoryFieldAddNewItem: React.FC<
|
||||
CategoryFieldAddNewItemProps
|
||||
> = ( { item, highlightedIndex, getItemProps, items } ) => {
|
||||
const index = items.findIndex( ( i ) => i.id === item.id );
|
||||
return (
|
||||
<li
|
||||
{ ...getItemProps( {
|
||||
item,
|
||||
index,
|
||||
} ) }
|
||||
className={ classNames(
|
||||
'woocommerce-category-field-dropdown__item is-new',
|
||||
{
|
||||
item_highlighted: highlightedIndex === index,
|
||||
}
|
||||
) }
|
||||
>
|
||||
<div className="woocommerce-category-field-dropdown__item-content">
|
||||
<Icon
|
||||
className="category-field-dropdown__toggle"
|
||||
icon={ plus }
|
||||
size={ 20 }
|
||||
/>
|
||||
{ sprintf( __( 'Create "%s"', 'woocommerce' ), item.name ) }
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
|
@ -1,121 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { CheckboxControl, Icon } from '@wordpress/components';
|
||||
import { useEffect, useState, createElement } from '@wordpress/element';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { chevronDown, chevronUp } from '@wordpress/icons';
|
||||
import { ProductCategory } from '@woocommerce/data';
|
||||
import { __experimentalSelectControlMenuItemProps as MenuItemProps } from '@woocommerce/components';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ProductCategoryNode } from './use-category-search';
|
||||
|
||||
export type CategoryTreeItem = {
|
||||
data: ProductCategory;
|
||||
children: CategoryTreeItem[];
|
||||
parentID: number;
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
type CategoryFieldItemProps = {
|
||||
item: CategoryTreeItem;
|
||||
selectedIds: number[];
|
||||
items: ProductCategoryNode[];
|
||||
highlightedIndex: number;
|
||||
openParent?: () => void;
|
||||
} & Pick< MenuItemProps< ProductCategoryNode >, 'getItemProps' >;
|
||||
|
||||
export const CategoryFieldItem: React.FC< CategoryFieldItemProps > = ( {
|
||||
item,
|
||||
selectedIds = [],
|
||||
items,
|
||||
highlightedIndex,
|
||||
openParent,
|
||||
getItemProps,
|
||||
} ) => {
|
||||
const [ isOpen, setIsOpen ] = useState( item.isOpen || false );
|
||||
const index = items.findIndex( ( i ) => i.id === item.data.id );
|
||||
const children = item.children.filter( ( child ) =>
|
||||
items.includes( child.data )
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
if ( highlightedIndex === index && children.length > 0 && ! isOpen ) {
|
||||
setIsOpen( true );
|
||||
} else if ( highlightedIndex === index && openParent ) {
|
||||
// Make sure the parent is also open when the item is highlighted.
|
||||
openParent();
|
||||
}
|
||||
}, [ highlightedIndex ] );
|
||||
|
||||
useEffect( () => {
|
||||
if ( item.isOpen !== isOpen ) {
|
||||
setIsOpen( item.isOpen );
|
||||
}
|
||||
}, [ item.isOpen ] );
|
||||
|
||||
return (
|
||||
<li
|
||||
className={ classNames(
|
||||
'woocommerce-category-field-dropdown__item',
|
||||
{
|
||||
item_highlighted: index === highlightedIndex,
|
||||
}
|
||||
) }
|
||||
>
|
||||
<div
|
||||
className="woocommerce-category-field-dropdown__item-content"
|
||||
{ ...getItemProps( {
|
||||
item: item.data,
|
||||
index,
|
||||
} ) }
|
||||
>
|
||||
{ children.length > 0 ? (
|
||||
<Icon
|
||||
className="woocommerce-category-field-dropdown__toggle"
|
||||
icon={ isOpen ? chevronUp : chevronDown }
|
||||
size={ 20 }
|
||||
onClick={ ( e: React.MouseEvent ) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen( ! isOpen );
|
||||
} }
|
||||
/>
|
||||
) : (
|
||||
<div className="woocommerce-category-field-dropdown__toggle-placeholder"></div>
|
||||
) }
|
||||
<CheckboxControl
|
||||
label={ decodeEntities( item.data.name ) }
|
||||
checked={ selectedIds.includes( item.data.id ) }
|
||||
onChange={ () => item.data }
|
||||
/>
|
||||
</div>
|
||||
{ children.length > 0 ? (
|
||||
<ul
|
||||
className={ classNames(
|
||||
'woocommerce-category-field-dropdown__item-children',
|
||||
{
|
||||
'woocommerce-category-field-dropdown__item-open':
|
||||
isOpen,
|
||||
}
|
||||
) }
|
||||
>
|
||||
{ children.map( ( child ) => (
|
||||
<CategoryFieldItem
|
||||
key={ child.data.id }
|
||||
item={ child }
|
||||
selectedIds={ selectedIds }
|
||||
items={ items }
|
||||
highlightedIndex={ highlightedIndex }
|
||||
openParent={ () => ! isOpen && setIsOpen( true ) }
|
||||
getItemProps={ getItemProps }
|
||||
/>
|
||||
) ) }
|
||||
</ul>
|
||||
) : null }
|
||||
</li>
|
||||
);
|
||||
};
|
|
@ -1,196 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo, useState, createElement, Fragment } from '@wordpress/element';
|
||||
import {
|
||||
TreeItemType,
|
||||
__experimentalSelectTreeControl as SelectTree,
|
||||
} from '@woocommerce/components';
|
||||
import { ProductCategory } from '@woocommerce/data';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CategoryTreeItem } from './category-field-item';
|
||||
import { useCategorySearch, ProductCategoryNode } from './use-category-search';
|
||||
import { CreateCategoryModal } from './create-category-modal';
|
||||
|
||||
type CategoryFieldProps = {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
value?: ProductCategoryNode[];
|
||||
onChange: ( value: ProductCategoryNode[] ) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursive function that adds the current item to the selected list and all it's parents
|
||||
* if not included already.
|
||||
*/
|
||||
function getSelectedWithParents(
|
||||
selected: ProductCategoryNode[] = [],
|
||||
item: ProductCategory,
|
||||
treeKeyValues: Record< number, CategoryTreeItem >
|
||||
): ProductCategoryNode[] {
|
||||
selected.push( { id: item.id, name: item.name, parent: item.parent } );
|
||||
|
||||
const parentId =
|
||||
item.parent !== undefined
|
||||
? item.parent
|
||||
: treeKeyValues[ item.id ].parentID;
|
||||
if (
|
||||
parentId > 0 &&
|
||||
treeKeyValues[ parentId ] &&
|
||||
! selected.find(
|
||||
( selectedCategory ) => selectedCategory.id === parentId
|
||||
)
|
||||
) {
|
||||
getSelectedWithParents(
|
||||
selected,
|
||||
treeKeyValues[ parentId ].data,
|
||||
treeKeyValues
|
||||
);
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
export function mapFromCategoryToTreeItem(
|
||||
val: ProductCategoryNode
|
||||
): TreeItemType {
|
||||
return val.parent
|
||||
? {
|
||||
value: String( val.id ),
|
||||
label: val.name,
|
||||
parent: String( val.parent ),
|
||||
}
|
||||
: {
|
||||
value: String( val.id ),
|
||||
label: val.name,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapFromTreeItemToCategory(
|
||||
val: TreeItemType
|
||||
): ProductCategoryNode {
|
||||
return {
|
||||
id: +val.value,
|
||||
name: val.label,
|
||||
parent: val.parent ? +val.parent : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapFromCategoriesToTreeItems(
|
||||
categories: ProductCategoryNode[]
|
||||
): TreeItemType[] {
|
||||
return categories.map( mapFromCategoryToTreeItem );
|
||||
}
|
||||
|
||||
export function mapFromTreeItemsToCategories(
|
||||
categories: TreeItemType[]
|
||||
): ProductCategoryNode[] {
|
||||
return categories.map( mapFromTreeItemToCategory );
|
||||
}
|
||||
|
||||
export const CategoryField: React.FC< CategoryFieldProps > = ( {
|
||||
label,
|
||||
placeholder,
|
||||
value = [],
|
||||
onChange,
|
||||
} ) => {
|
||||
const {
|
||||
isSearching,
|
||||
categoriesSelectList,
|
||||
categoryTreeKeyValues,
|
||||
searchCategories,
|
||||
getFilteredItemsForSelectTree,
|
||||
} = useCategorySearch();
|
||||
const [ showCreateNewModal, setShowCreateNewModal ] = useState( false );
|
||||
const [ searchValue, setSearchValue ] = useState( '' );
|
||||
|
||||
const onInputChange = ( searchString?: string ) => {
|
||||
setSearchValue( searchString || '' );
|
||||
searchCategories( searchString || '' );
|
||||
};
|
||||
|
||||
const searchDelayed = useMemo(
|
||||
() => debounce( onInputChange, 150 ),
|
||||
[ onInputChange ]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SelectTree
|
||||
id="category-field"
|
||||
multiple
|
||||
shouldNotRecursivelySelect
|
||||
createValue={ searchValue }
|
||||
label={ label }
|
||||
isLoading={ isSearching }
|
||||
onInputChange={ searchDelayed }
|
||||
placeholder={ value.length === 0 ? placeholder : '' }
|
||||
onCreateNew={ () => {
|
||||
setShowCreateNewModal( true );
|
||||
} }
|
||||
shouldShowCreateButton={ ( typedValue ) =>
|
||||
! typedValue ||
|
||||
categoriesSelectList.findIndex(
|
||||
( item ) => item.name === typedValue
|
||||
) === -1
|
||||
}
|
||||
items={ getFilteredItemsForSelectTree(
|
||||
mapFromCategoriesToTreeItems( categoriesSelectList ),
|
||||
searchValue,
|
||||
mapFromCategoriesToTreeItems( value )
|
||||
) }
|
||||
selected={ mapFromCategoriesToTreeItems( value ) }
|
||||
onSelect={ ( selectedItems ) => {
|
||||
if ( Array.isArray( selectedItems ) ) {
|
||||
const newItems: ProductCategoryNode[] =
|
||||
mapFromTreeItemsToCategories(
|
||||
selectedItems.filter(
|
||||
( { value: selectedItemValue } ) =>
|
||||
! value.some(
|
||||
( item ) =>
|
||||
item.id === +selectedItemValue
|
||||
)
|
||||
)
|
||||
);
|
||||
onChange( [ ...value, ...newItems ] );
|
||||
}
|
||||
} }
|
||||
onRemove={ ( removedItems ) => {
|
||||
const newValues = Array.isArray( removedItems )
|
||||
? value.filter(
|
||||
( item ) =>
|
||||
! removedItems.some(
|
||||
( { value: removedValue } ) =>
|
||||
item.id === +removedValue
|
||||
)
|
||||
)
|
||||
: value.filter(
|
||||
( item ) => item.id !== +removedItems.value
|
||||
);
|
||||
onChange( newValues );
|
||||
} }
|
||||
></SelectTree>
|
||||
{ showCreateNewModal && (
|
||||
<CreateCategoryModal
|
||||
initialCategoryName={ searchValue }
|
||||
onCancel={ () => setShowCreateNewModal( false ) }
|
||||
onCreate={ ( newCategory ) => {
|
||||
onChange(
|
||||
getSelectedWithParents(
|
||||
[ ...value ],
|
||||
newCategory,
|
||||
categoryTreeKeyValues
|
||||
)
|
||||
);
|
||||
setShowCreateNewModal( false );
|
||||
onInputChange( '' );
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,34 +0,0 @@
|
|||
.woocommerce-create-new-category-modal {
|
||||
min-width: 650px;
|
||||
overflow: visible;
|
||||
|
||||
&__buttons {
|
||||
margin-top: $gap-larger;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $gap-smaller;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.category-field-dropdown {
|
||||
&__menu {
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-select-control__popover-menu {
|
||||
margin-top: -$gap-small;
|
||||
}
|
||||
.woocommerce-select-control__popover-menu-container {
|
||||
max-height: 300px;
|
||||
overflow-y: scroll;
|
||||
padding: 0 $gap-smaller 0 $gap-small;
|
||||
|
||||
> .category-field-dropdown__item:not(:first-child) {
|
||||
.category-field-dropdown__item-content {
|
||||
border-top: 1px solid $gray-200;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button, Modal, TextControl } from '@wordpress/components';
|
||||
import { useDebounce } from '@wordpress/compose';
|
||||
import {
|
||||
useState,
|
||||
createElement,
|
||||
createInterpolateElement,
|
||||
} from '@wordpress/element';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import {
|
||||
__experimentalSelectTreeControl as SelectTree,
|
||||
TreeItemType as Item,
|
||||
} from '@woocommerce/components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME,
|
||||
ProductCategory,
|
||||
} from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ProductCategoryNode, useCategorySearch } from './use-category-search';
|
||||
import {
|
||||
mapFromCategoriesToTreeItems,
|
||||
mapFromCategoryToTreeItem,
|
||||
} from './category-field';
|
||||
import { TRACKS_SOURCE } from '../../constants';
|
||||
|
||||
type CreateCategoryModalProps = {
|
||||
initialCategoryName?: string;
|
||||
onCancel: () => void;
|
||||
onCreate: ( newCategory: ProductCategory ) => void;
|
||||
};
|
||||
|
||||
export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( {
|
||||
initialCategoryName,
|
||||
onCancel,
|
||||
onCreate,
|
||||
} ) => {
|
||||
const {
|
||||
categoriesSelectList,
|
||||
isSearching,
|
||||
searchCategories,
|
||||
getFilteredItemsForSelectTree,
|
||||
} = useCategorySearch();
|
||||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
const [ isCreating, setIsCreating ] = useState( false );
|
||||
const { createProductCategory, invalidateResolutionForStoreSelector } =
|
||||
useDispatch( EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME );
|
||||
const [ categoryName, setCategoryName ] = useState(
|
||||
initialCategoryName || ''
|
||||
);
|
||||
const [ categoryParent, setCategoryParent ] =
|
||||
useState< ProductCategoryNode | null >( null );
|
||||
|
||||
const [ categoryParentTypedValue, setCategoryParentTypedValue ] =
|
||||
useState( '' );
|
||||
|
||||
const onSave = async () => {
|
||||
recordEvent( 'product_category_add', {
|
||||
source: TRACKS_SOURCE,
|
||||
} );
|
||||
setIsCreating( true );
|
||||
try {
|
||||
const newCategory: ProductCategory = await createProductCategory( {
|
||||
name: categoryName,
|
||||
parent: categoryParent ? categoryParent.id : undefined,
|
||||
} );
|
||||
invalidateResolutionForStoreSelector( 'getProductCategories' );
|
||||
setIsCreating( false );
|
||||
onCreate( newCategory );
|
||||
} catch ( e ) {
|
||||
createNotice(
|
||||
'error',
|
||||
__( 'Failed to create category.', 'woocommerce' )
|
||||
);
|
||||
setIsCreating( false );
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedSearch = useDebounce( searchCategories, 250 );
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={ __( 'Create category', 'woocommerce' ) }
|
||||
onRequestClose={ () => onCancel() }
|
||||
className="woocommerce-create-new-category-modal"
|
||||
>
|
||||
<div className="woocommerce-create-new-category-modal__wrapper">
|
||||
<TextControl
|
||||
label={ __( 'Name', 'woocommerce' ) }
|
||||
name="Tops"
|
||||
value={ categoryName }
|
||||
onChange={ setCategoryName }
|
||||
/>
|
||||
<SelectTree
|
||||
label={ createInterpolateElement(
|
||||
__( 'Parent category <optional/>', 'woocommerce' ),
|
||||
{
|
||||
optional: (
|
||||
<span className="woocommerce-product-form__optional-input">
|
||||
{ __( '(optional)', 'woocommerce' ) }
|
||||
</span>
|
||||
),
|
||||
}
|
||||
) }
|
||||
id="parent-category-field"
|
||||
isLoading={ isSearching }
|
||||
items={ getFilteredItemsForSelectTree(
|
||||
mapFromCategoriesToTreeItems( categoriesSelectList ),
|
||||
categoryParentTypedValue,
|
||||
[]
|
||||
) }
|
||||
shouldNotRecursivelySelect
|
||||
selected={
|
||||
categoryParent
|
||||
? mapFromCategoryToTreeItem( categoryParent )
|
||||
: undefined
|
||||
}
|
||||
onSelect={ ( item: Item ) =>
|
||||
item &&
|
||||
setCategoryParent( {
|
||||
id: +item.value,
|
||||
name: item.label,
|
||||
parent: item.parent ? +item.parent : 0,
|
||||
} )
|
||||
}
|
||||
onRemove={ () => setCategoryParent( null ) }
|
||||
onInputChange={ ( value ) => {
|
||||
debouncedSearch( value );
|
||||
setCategoryParentTypedValue( value || '' );
|
||||
} }
|
||||
createValue={ categoryParentTypedValue }
|
||||
/>
|
||||
<div className="woocommerce-create-new-category-modal__buttons">
|
||||
<Button
|
||||
isSecondary
|
||||
onClick={ () => onCancel() }
|
||||
disabled={ isCreating }
|
||||
>
|
||||
{ __( 'Cancel', 'woocommerce' ) }
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary
|
||||
disabled={ categoryName.length === 0 || isCreating }
|
||||
isBusy={ isCreating }
|
||||
onClick={ () => {
|
||||
onSave();
|
||||
} }
|
||||
>
|
||||
{ __( 'Save', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import { Product } from '@woocommerce/data';
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CategoryField } from './category-field';
|
||||
import { ProductCategoryNode } from './use-category-search';
|
||||
|
||||
export const DetailsCategoriesField = () => {
|
||||
const { getInputProps } = useFormContext< Product >();
|
||||
|
||||
return (
|
||||
<CategoryField
|
||||
label={ __( 'Categories', 'woocommerce' ) }
|
||||
placeholder={ __( 'Search or create category…', 'woocommerce' ) }
|
||||
{ ...getInputProps< ProductCategoryNode[] >( 'categories' ) }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,2 +0,0 @@
|
|||
export * from './details-categories-field';
|
||||
export * from './category-field';
|
|
@ -1,413 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { useSelect, resolveSelect } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useCategorySearch } from '../use-category-search';
|
||||
import { mapFromCategoriesToTreeItems } from '../../details-categories-field/category-field';
|
||||
|
||||
jest.mock( '@wordpress/data', () => ( {
|
||||
...jest.requireActual( '@wordpress/data' ),
|
||||
useSelect: jest.fn(),
|
||||
resolveSelect: jest.fn(),
|
||||
} ) );
|
||||
|
||||
const mockCategoryList = [
|
||||
{ id: 1, name: 'Clothing', parent: 0, count: 0 },
|
||||
{ id: 2, name: 'Hoodies', parent: 1, count: 0 },
|
||||
{ id: 4, name: 'Accessories', parent: 1, count: 0 },
|
||||
{ id: 5, name: 'Belts', parent: 4, count: 0 },
|
||||
{ id: 3, name: 'Rain gear', parent: 0, count: 0 },
|
||||
{ id: 6, name: 'Furniture', parent: 0, count: 0 },
|
||||
];
|
||||
|
||||
describe( 'useCategorySearch', () => {
|
||||
const getProductCategoriesMock = jest
|
||||
.fn()
|
||||
.mockReturnValue( [ ...mockCategoryList ] );
|
||||
const getProductCategoriesTotalCountMock = jest
|
||||
.fn()
|
||||
.mockReturnValue( mockCategoryList.length );
|
||||
const getProductCategoriesResolveMock = jest.fn();
|
||||
|
||||
beforeEach( () => {
|
||||
jest.clearAllMocks();
|
||||
( useSelect as jest.Mock ).mockImplementation( ( callback ) => {
|
||||
return callback( () => ( {
|
||||
getProductCategories: getProductCategoriesMock,
|
||||
getProductCategoriesTotalCount:
|
||||
getProductCategoriesTotalCountMock,
|
||||
} ) );
|
||||
} );
|
||||
( resolveSelect as jest.Mock ).mockImplementation( () => ( {
|
||||
getProductCategories: getProductCategoriesResolveMock,
|
||||
} ) );
|
||||
} );
|
||||
|
||||
it( 'should retrieve an initial list of product categories and generate a tree', async () => {
|
||||
getProductCategoriesMock.mockReturnValue( undefined );
|
||||
getProductCategoriesTotalCountMock.mockReturnValue(
|
||||
mockCategoryList.length
|
||||
);
|
||||
const { result, rerender, waitForNextUpdate } = renderHook( () =>
|
||||
useCategorySearch()
|
||||
);
|
||||
|
||||
act( () => {
|
||||
getProductCategoriesMock.mockReturnValue( [ ...mockCategoryList ] );
|
||||
rerender();
|
||||
} );
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect( result.current.categoriesSelectList.length ).toEqual(
|
||||
mockCategoryList.length
|
||||
);
|
||||
expect( result.current.categories.length ).toEqual(
|
||||
mockCategoryList.filter( ( c ) => c.parent === 0 ).length
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should return a correct tree for categories with each item containing a childrens property', async () => {
|
||||
getProductCategoriesMock.mockReturnValue( [ ...mockCategoryList ] );
|
||||
getProductCategoriesTotalCountMock.mockReturnValue(
|
||||
mockCategoryList.length
|
||||
);
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useCategorySearch()
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
const clothing = result.current.categories.find(
|
||||
( cat ) => cat.data.name === 'Clothing'
|
||||
);
|
||||
expect( clothing?.children[ 0 ].data.name ).toEqual( 'Accessories' );
|
||||
expect( clothing?.children[ 0 ].children[ 0 ].data.name ).toEqual(
|
||||
'Belts'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should sort items by count first and then alphabetical', async () => {
|
||||
getProductCategoriesMock.mockReturnValue( [
|
||||
...mockCategoryList,
|
||||
{ id: 12, name: 'BB', parent: 0, count: 0 },
|
||||
{ id: 13, name: 'AA', parent: 0, count: 0 },
|
||||
{ id: 11, name: 'ZZZ', parent: 0, count: 20 },
|
||||
] );
|
||||
|
||||
getProductCategoriesTotalCountMock.mockReturnValue(
|
||||
mockCategoryList.length
|
||||
);
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useCategorySearch()
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect( result.current.categoriesSelectList[ 0 ].name ).toEqual(
|
||||
'ZZZ'
|
||||
);
|
||||
expect( result.current.categoriesSelectList[ 1 ].name ).toEqual( 'AA' );
|
||||
expect( result.current.categoriesSelectList[ 2 ].name ).toEqual( 'BB' );
|
||||
} );
|
||||
|
||||
it( 'should also sort children by count first and then alphabetical', async () => {
|
||||
getProductCategoriesMock.mockReturnValue( [
|
||||
...mockCategoryList,
|
||||
{ id: 12, name: 'AB', parent: 1, count: 0 },
|
||||
{ id: 13, name: 'AA', parent: 1, count: 0 },
|
||||
{ id: 11, name: 'ZZZ', parent: 1, count: 20 },
|
||||
] );
|
||||
|
||||
getProductCategoriesTotalCountMock.mockReturnValue(
|
||||
mockCategoryList.length
|
||||
);
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useCategorySearch()
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
const clothing = result.current.categories.find(
|
||||
( cat ) => cat.data.name === 'Clothing'
|
||||
);
|
||||
expect( clothing?.children[ 0 ].data.name ).toEqual( 'ZZZ' );
|
||||
expect( clothing?.children[ 1 ].data.name ).toEqual( 'AA' );
|
||||
expect( clothing?.children[ 2 ].data.name ).toEqual( 'AB' );
|
||||
} );
|
||||
|
||||
it( 'should order the select list by parent, child, nested child, parent', async () => {
|
||||
getProductCategoriesMock.mockReturnValue( [
|
||||
...mockCategoryList,
|
||||
{ id: 13, name: 'AA', parent: 1, count: 0 },
|
||||
{ id: 11, name: 'ZZ', parent: 1, count: 20 },
|
||||
] );
|
||||
|
||||
getProductCategoriesTotalCountMock.mockReturnValue(
|
||||
mockCategoryList.length
|
||||
);
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useCategorySearch()
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect( result.current.categoriesSelectList[ 0 ].name ).toEqual(
|
||||
'Clothing'
|
||||
);
|
||||
// child of clothing.
|
||||
expect( result.current.categoriesSelectList[ 1 ].name ).toEqual( 'ZZ' );
|
||||
expect( result.current.categoriesSelectList[ 2 ].name ).toEqual( 'AA' );
|
||||
expect( result.current.categoriesSelectList[ 3 ].name ).toEqual(
|
||||
'Accessories'
|
||||
);
|
||||
// child of accessories.
|
||||
expect( result.current.categoriesSelectList[ 4 ].name ).toEqual(
|
||||
'Belts'
|
||||
);
|
||||
// child of clothing.
|
||||
expect( result.current.categoriesSelectList[ 5 ].name ).toEqual(
|
||||
'Hoodies'
|
||||
);
|
||||
// top level.
|
||||
expect( result.current.categoriesSelectList[ 6 ].name ).toEqual(
|
||||
'Furniture'
|
||||
);
|
||||
} );
|
||||
|
||||
describe( 'getFilteredItemsForSelectTree', () => {
|
||||
it( 'should filter items by label, matching input value, and if selected', async () => {
|
||||
getProductCategoriesMock.mockReturnValue( [ ...mockCategoryList ] );
|
||||
|
||||
getProductCategoriesTotalCountMock.mockReturnValue(
|
||||
mockCategoryList.length
|
||||
);
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useCategorySearch()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
const filteredItems = result.current.getFilteredItemsForSelectTree(
|
||||
mapFromCategoriesToTreeItems(
|
||||
result.current.categoriesSelectList
|
||||
),
|
||||
'Rain',
|
||||
[]
|
||||
);
|
||||
expect( filteredItems.length ).toEqual( 1 );
|
||||
expect( filteredItems[ 0 ].label ).toEqual( 'Rain gear' );
|
||||
} );
|
||||
|
||||
it( 'should filter items by isOpen as well, keeping them if isOpen is true', async () => {
|
||||
getProductCategoriesMock.mockReturnValue( [ ...mockCategoryList ] );
|
||||
|
||||
getProductCategoriesTotalCountMock.mockReturnValue(
|
||||
mockCategoryList.length
|
||||
);
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useCategorySearch()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
act( () => {
|
||||
result.current.searchCategories( 'Bel' );
|
||||
} );
|
||||
await waitForNextUpdate();
|
||||
expect( result.current.categoriesSelectList.length ).toEqual( 6 );
|
||||
const filteredItems = result.current.getFilteredItemsForSelectTree(
|
||||
mapFromCategoriesToTreeItems(
|
||||
result.current.categoriesSelectList
|
||||
),
|
||||
'Bel',
|
||||
[]
|
||||
);
|
||||
expect( filteredItems.length ).toEqual( 3 );
|
||||
expect( filteredItems[ 0 ].label ).toEqual( 'Clothing' );
|
||||
expect( filteredItems[ 1 ].label ).toEqual( 'Accessories' );
|
||||
expect( filteredItems[ 2 ].label ).toEqual( 'Belts' );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'searchCategories', () => {
|
||||
it( 'should not use async when total categories is less then page size', async () => {
|
||||
getProductCategoriesResolveMock.mockClear();
|
||||
getProductCategoriesMock.mockReturnValue( [ ...mockCategoryList ] );
|
||||
|
||||
getProductCategoriesTotalCountMock.mockReturnValue(
|
||||
mockCategoryList.length
|
||||
);
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useCategorySearch()
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
act( () => {
|
||||
result.current.searchCategories( 'Clo' );
|
||||
} );
|
||||
|
||||
await waitForNextUpdate();
|
||||
expect( getProductCategoriesResolveMock ).not.toHaveBeenCalled();
|
||||
} );
|
||||
|
||||
it( 'should use async when total categories is more then page size', async () => {
|
||||
getProductCategoriesResolveMock
|
||||
.mockClear()
|
||||
.mockResolvedValue(
|
||||
mockCategoryList.filter( ( c ) => c.name === 'Clothing' )
|
||||
);
|
||||
getProductCategoriesMock.mockReturnValue( [ ...mockCategoryList ] );
|
||||
|
||||
getProductCategoriesTotalCountMock.mockReturnValue( 200 );
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useCategorySearch()
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
act( () => {
|
||||
result.current.searchCategories( 'Clo' );
|
||||
} );
|
||||
|
||||
await waitForNextUpdate();
|
||||
expect( getProductCategoriesResolveMock ).toHaveBeenCalledWith( {
|
||||
search: 'Clo',
|
||||
per_page: 100,
|
||||
} );
|
||||
expect( result.current.categoriesSelectList.length ).toEqual( 1 );
|
||||
} );
|
||||
|
||||
it( 'should update isSearching when async is enabled', async () => {
|
||||
let finish: () => void = () => {};
|
||||
getProductCategoriesResolveMock.mockClear().mockReturnValue(
|
||||
new Promise( ( resolve ) => {
|
||||
finish = () =>
|
||||
resolve(
|
||||
mockCategoryList.filter(
|
||||
( c ) => c.name === 'Clothing'
|
||||
)
|
||||
);
|
||||
} )
|
||||
);
|
||||
getProductCategoriesMock.mockReturnValue( [ ...mockCategoryList ] );
|
||||
getProductCategoriesTotalCountMock.mockReturnValue( 200 );
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useCategorySearch()
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
act( () => {
|
||||
result.current.searchCategories( 'Clo' );
|
||||
} );
|
||||
expect( result.current.isSearching ).toBe( true );
|
||||
|
||||
act( () => {
|
||||
finish();
|
||||
} );
|
||||
await waitForNextUpdate();
|
||||
expect( result.current.isSearching ).toBe( false );
|
||||
expect( getProductCategoriesResolveMock ).toHaveBeenCalledWith( {
|
||||
search: 'Clo',
|
||||
per_page: 100,
|
||||
} );
|
||||
expect( result.current.categoriesSelectList.length ).toEqual( 1 );
|
||||
} );
|
||||
|
||||
it( 'should set isSearching back to false if search failed and keep last results', async () => {
|
||||
let finish: () => void = () => {};
|
||||
getProductCategoriesResolveMock.mockClear().mockReturnValue(
|
||||
new Promise( ( resolve, reject ) => {
|
||||
finish = () => reject();
|
||||
} )
|
||||
);
|
||||
getProductCategoriesMock.mockReturnValue( [ ...mockCategoryList ] );
|
||||
getProductCategoriesTotalCountMock.mockReturnValue( 200 );
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useCategorySearch()
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
act( () => {
|
||||
result.current.searchCategories( 'Clo' );
|
||||
} );
|
||||
expect( result.current.isSearching ).toBe( true );
|
||||
|
||||
act( () => {
|
||||
finish();
|
||||
} );
|
||||
await waitForNextUpdate();
|
||||
expect( result.current.isSearching ).toBe( false );
|
||||
expect( getProductCategoriesResolveMock ).toHaveBeenCalledWith( {
|
||||
search: 'Clo',
|
||||
per_page: 100,
|
||||
} );
|
||||
expect( result.current.categoriesSelectList.length ).toEqual( 6 );
|
||||
} );
|
||||
|
||||
it( 'should keep parent in the list if only child matches search value', async () => {
|
||||
getProductCategoriesResolveMock
|
||||
.mockClear()
|
||||
.mockResolvedValue( [
|
||||
mockCategoryList.find( ( c ) => c.name === 'Hoodies' ),
|
||||
] );
|
||||
getProductCategoriesMock.mockReturnValue( [ ...mockCategoryList ] );
|
||||
getProductCategoriesTotalCountMock.mockReturnValue( 200 );
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useCategorySearch()
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
act( () => {
|
||||
result.current.searchCategories( 'Hood' );
|
||||
} );
|
||||
await waitForNextUpdate();
|
||||
expect( getProductCategoriesResolveMock ).toHaveBeenCalledWith( {
|
||||
search: 'Hood',
|
||||
per_page: 100,
|
||||
} );
|
||||
expect( result.current.categoriesSelectList.length ).toEqual( 2 );
|
||||
expect( result.current.categoriesSelectList[ 0 ].name ).toEqual(
|
||||
'Clothing'
|
||||
);
|
||||
expect( result.current.categoriesSelectList[ 1 ].name ).toEqual(
|
||||
'Hoodies'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should set parent isOpen to true if child matches search value', async () => {
|
||||
getProductCategoriesResolveMock
|
||||
.mockClear()
|
||||
.mockResolvedValue( [
|
||||
mockCategoryList.find( ( c ) => c.name === 'Hoodies' ),
|
||||
] );
|
||||
getProductCategoriesMock.mockReturnValue( [ ...mockCategoryList ] );
|
||||
getProductCategoriesTotalCountMock.mockReturnValue( 200 );
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useCategorySearch()
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
act( () => {
|
||||
result.current.searchCategories( 'Hood' );
|
||||
} );
|
||||
await waitForNextUpdate();
|
||||
expect( getProductCategoriesResolveMock ).toHaveBeenCalledWith( {
|
||||
search: 'Hood',
|
||||
per_page: 100,
|
||||
} );
|
||||
expect( result.current.categories[ 0 ].isOpen ).toEqual( true );
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -1,286 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
|
||||
import { useSelect, resolveSelect } from '@wordpress/data';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME,
|
||||
WCDataSelector,
|
||||
ProductCategory,
|
||||
} from '@woocommerce/data';
|
||||
import { escapeRegExp } from 'lodash';
|
||||
import { TreeItemType } from '@woocommerce/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CategoryTreeItem } from './category-field-item';
|
||||
|
||||
const PAGE_SIZE = 100;
|
||||
const parentCategoryCache: Record< number, ProductCategory > = {};
|
||||
|
||||
/**
|
||||
* Recursive function to set isOpen to true for all the childrens parents.
|
||||
*/
|
||||
function openParents(
|
||||
treeList: Record< number, CategoryTreeItem >,
|
||||
item: CategoryTreeItem
|
||||
) {
|
||||
if ( treeList[ item.parentID ] ) {
|
||||
treeList[ item.parentID ].isOpen = true;
|
||||
if ( treeList[ item.parentID ].parentID !== 0 ) {
|
||||
openParents( treeList, treeList[ item.parentID ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type ProductCategoryNode = Pick<
|
||||
ProductCategory,
|
||||
'id' | 'name' | 'parent'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Sort function for category tree items, sorts by popularity and then alphabetically.
|
||||
*/
|
||||
export const sortCategoryTreeItems = (
|
||||
menuItems: CategoryTreeItem[]
|
||||
): CategoryTreeItem[] => {
|
||||
return menuItems.sort( ( a, b ) => {
|
||||
if ( a.data.count === b.data.count ) {
|
||||
return a.data.name.localeCompare( b.data.name );
|
||||
}
|
||||
return b.data.count - a.data.count;
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Flattens the category tree into a single list, also sorts the children of any parent tree item.
|
||||
*/
|
||||
function flattenCategoryTreeAndSortChildren(
|
||||
items: ProductCategory[] = [],
|
||||
treeItems: CategoryTreeItem[]
|
||||
) {
|
||||
for ( const treeItem of treeItems ) {
|
||||
items.push( treeItem.data );
|
||||
if ( treeItem.children.length > 0 ) {
|
||||
treeItem.children = sortCategoryTreeItems( treeItem.children );
|
||||
flattenCategoryTreeAndSortChildren( items, treeItem.children );
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive function to turn a category list into a tree and retrieve any missing parents.
|
||||
* It checks if any parents are missing, and then does a single request to retrieve those, running this function again after.
|
||||
*/
|
||||
export async function getCategoriesTreeWithMissingParents(
|
||||
newCategories: ProductCategory[],
|
||||
search: string
|
||||
): Promise<
|
||||
[
|
||||
ProductCategory[],
|
||||
CategoryTreeItem[],
|
||||
Record< number, CategoryTreeItem >
|
||||
]
|
||||
> {
|
||||
const items: Record< number, CategoryTreeItem > = {};
|
||||
const missingParents: number[] = [];
|
||||
|
||||
for ( const cat of newCategories ) {
|
||||
items[ cat.id ] = {
|
||||
data: cat,
|
||||
children: [],
|
||||
parentID: cat.parent,
|
||||
isOpen: false,
|
||||
};
|
||||
}
|
||||
// Loops through each item and adds children to their parents by the use of parentID.
|
||||
Object.keys( items ).forEach( ( key ) => {
|
||||
const item = items[ parseInt( key, 10 ) ];
|
||||
if ( item.parentID !== 0 ) {
|
||||
// Check the parent cache incase the parent was missing and use that instead.
|
||||
if (
|
||||
! items[ item.parentID ] &&
|
||||
parentCategoryCache[ item.parentID ]
|
||||
) {
|
||||
items[ item.parentID ] = {
|
||||
data: parentCategoryCache[ item.parentID ],
|
||||
children: [],
|
||||
parentID: parentCategoryCache[ item.parentID ].parent,
|
||||
isOpen: false,
|
||||
};
|
||||
}
|
||||
if ( items[ item.parentID ] ) {
|
||||
items[ item.parentID ].children.push( item );
|
||||
parentCategoryCache[ item.parentID ] =
|
||||
items[ item.parentID ].data;
|
||||
// Open the parents if the child matches the search string.
|
||||
const searchRegex = new RegExp( escapeRegExp( search ), 'i' );
|
||||
if ( search.length > 0 && searchRegex.test( item.data.name ) ) {
|
||||
openParents( items, item );
|
||||
}
|
||||
} else {
|
||||
missingParents.push( item.parentID );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
// Retrieve the missing parent objects incase not all of them were included.
|
||||
if ( missingParents.length > 0 ) {
|
||||
return (
|
||||
resolveSelect( EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME )
|
||||
.getProductCategories( {
|
||||
include: missingParents,
|
||||
} )
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
.then( ( parentCategories ) => {
|
||||
return getCategoriesTreeWithMissingParents(
|
||||
[
|
||||
...( parentCategories as ProductCategory[] ),
|
||||
...newCategories,
|
||||
],
|
||||
search
|
||||
);
|
||||
} )
|
||||
);
|
||||
}
|
||||
const categoryTreeList = sortCategoryTreeItems(
|
||||
Object.values( items ).filter( ( item ) => item.parentID === 0 )
|
||||
);
|
||||
const categoryCheckboxList = flattenCategoryTreeAndSortChildren(
|
||||
[],
|
||||
categoryTreeList
|
||||
);
|
||||
|
||||
return Promise.resolve( [ categoryCheckboxList, categoryTreeList, items ] );
|
||||
}
|
||||
|
||||
const productCategoryQueryObject = {
|
||||
per_page: PAGE_SIZE,
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook used to handle all the search logic for the category search component.
|
||||
* This hook also handles the data structure and provides a tree like structure see: CategoryTreeItema.
|
||||
*/
|
||||
export const useCategorySearch = () => {
|
||||
const lastSearchValue = useRef( '' );
|
||||
const { initialCategories, totalCount } = useSelect(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
( select: WCDataSelector ) => {
|
||||
const { getProductCategories, getProductCategoriesTotalCount } =
|
||||
select( EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME );
|
||||
return {
|
||||
initialCategories: getProductCategories(
|
||||
productCategoryQueryObject
|
||||
),
|
||||
totalCount: getProductCategoriesTotalCount(
|
||||
productCategoryQueryObject
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
const [ isSearching, setIsSearching ] = useState( true );
|
||||
const [ categoriesAndNewItem, setCategoriesAndNewItem ] = useState<
|
||||
[
|
||||
ProductCategory[],
|
||||
CategoryTreeItem[],
|
||||
Record< number, CategoryTreeItem >
|
||||
]
|
||||
>( [ [], [], {} ] );
|
||||
const isAsync =
|
||||
! initialCategories ||
|
||||
( initialCategories.length > 0 && totalCount > PAGE_SIZE );
|
||||
|
||||
useEffect( () => {
|
||||
if (
|
||||
initialCategories &&
|
||||
initialCategories.length > 0 &&
|
||||
( categoriesAndNewItem[ 0 ].length === 0 ||
|
||||
lastSearchValue.current.length === 0 )
|
||||
) {
|
||||
setIsSearching( true );
|
||||
getCategoriesTreeWithMissingParents(
|
||||
[ ...initialCategories ],
|
||||
''
|
||||
).then(
|
||||
( categoryTree ) => {
|
||||
setCategoriesAndNewItem( categoryTree );
|
||||
setIsSearching( false );
|
||||
},
|
||||
() => {
|
||||
setIsSearching( false );
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [ initialCategories ] );
|
||||
|
||||
const searchCategories = useCallback(
|
||||
async ( search?: string ): Promise< CategoryTreeItem[] > => {
|
||||
lastSearchValue.current = search || '';
|
||||
if ( ! isAsync && initialCategories.length > 0 ) {
|
||||
return getCategoriesTreeWithMissingParents(
|
||||
[ ...initialCategories ],
|
||||
search || ''
|
||||
).then( ( categoryData ) => {
|
||||
setCategoriesAndNewItem( categoryData );
|
||||
return categoryData[ 1 ];
|
||||
} );
|
||||
}
|
||||
setIsSearching( true );
|
||||
try {
|
||||
const newCategories = await resolveSelect(
|
||||
EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME
|
||||
).getProductCategories( {
|
||||
search,
|
||||
per_page: PAGE_SIZE,
|
||||
} );
|
||||
|
||||
const categoryTreeData =
|
||||
await getCategoriesTreeWithMissingParents(
|
||||
newCategories as ProductCategory[],
|
||||
search || ''
|
||||
);
|
||||
setIsSearching( false );
|
||||
setCategoriesAndNewItem( categoryTreeData );
|
||||
return categoryTreeData[ 1 ];
|
||||
} catch ( e ) {
|
||||
setIsSearching( false );
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[ initialCategories ]
|
||||
);
|
||||
|
||||
const categoryTreeKeyValues = categoriesAndNewItem[ 2 ];
|
||||
|
||||
const getFilteredItemsForSelectTree = useCallback(
|
||||
(
|
||||
allItems: TreeItemType[],
|
||||
inputValue: string,
|
||||
selectedItems: TreeItemType[]
|
||||
) => {
|
||||
const searchRegex = new RegExp( escapeRegExp( inputValue ), 'i' );
|
||||
return allItems.filter(
|
||||
( item ) =>
|
||||
selectedItems.indexOf( item ) < 0 &&
|
||||
( searchRegex.test( item.label ) ||
|
||||
( categoryTreeKeyValues[ +item.value ] &&
|
||||
categoryTreeKeyValues[ +item.value ].isOpen ) )
|
||||
);
|
||||
},
|
||||
[ categoriesAndNewItem ]
|
||||
);
|
||||
|
||||
return {
|
||||
searchCategories,
|
||||
getFilteredItemsForSelectTree,
|
||||
categoriesSelectList: categoriesAndNewItem[ 0 ],
|
||||
categories: categoriesAndNewItem[ 1 ],
|
||||
isSearching,
|
||||
categoryTreeKeyValues,
|
||||
};
|
||||
};
|
|
@ -31,37 +31,35 @@ export function EditorCanvas( {
|
|||
const mouseMoveTypingRef = useMouseMoveTypingReset();
|
||||
return (
|
||||
<Iframe
|
||||
head={
|
||||
<>
|
||||
<EditorStyles styles={ settings?.styles } />
|
||||
<style>
|
||||
{
|
||||
// Forming a "block formatting context" to prevent margin collapsing.
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Block_formatting_context
|
||||
`.is-root-container {
|
||||
padding: 36px;
|
||||
display: flow-root;
|
||||
}
|
||||
body { position: relative; }`
|
||||
}
|
||||
</style>
|
||||
{ enableResizing && (
|
||||
<style>
|
||||
{
|
||||
// Some themes will have `min-height: 100vh` for the root container,
|
||||
// which isn't a requirement in auto resize mode.
|
||||
`.is-root-container { min-height: 0 !important; }`
|
||||
}
|
||||
</style>
|
||||
) }
|
||||
</>
|
||||
}
|
||||
ref={ mouseMoveTypingRef }
|
||||
name="editor-canvas"
|
||||
className="edit-site-visual-editor__editor-canvas"
|
||||
{ ...props }
|
||||
>
|
||||
{ children }
|
||||
<>
|
||||
<EditorStyles styles={ settings?.styles } />
|
||||
<style>
|
||||
{
|
||||
// Forming a "block formatting context" to prevent margin collapsing.
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Block_formatting_context
|
||||
`.is-root-container {
|
||||
padding: 36px;
|
||||
display: flow-root;
|
||||
}
|
||||
body { position: relative; }`
|
||||
}
|
||||
</style>
|
||||
{ enableResizing && (
|
||||
<style>
|
||||
{
|
||||
// Some themes will have `min-height: 100vh` for the root container,
|
||||
// which isn't a requirement in auto resize mode.
|
||||
`.is-root-container { min-height: 0 !important; }`
|
||||
}
|
||||
</style>
|
||||
) }
|
||||
{ children }
|
||||
</>
|
||||
</Iframe>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ export { WooProductSectionItem as __experimentalWooProductSectionItem } from './
|
|||
export { WooProductTabItem as __experimentalWooProductTabItem } from './woo-product-tab-item';
|
||||
export { DetailsNameField as __experimentalDetailsNameField } from './details-name-field';
|
||||
export { DetailsFeatureField as __experimentalDetailsFeatureField } from './details-feature-field';
|
||||
export { DetailsCategoriesField as __experimentalDetailsCategoriesField } from './details-categories-field';
|
||||
export { DetailsSummaryField as __experimentalDetailsSummaryField } from './details-summary-field';
|
||||
export { DetailsDescriptionField as __experimentalDetailsDescriptionField } from './details-description-field';
|
||||
export { WooProductMoreMenuItem as __experimentalWooProductMoreMenuItem } from './header';
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
.woocommerce-create-new-tag-modal {
|
||||
min-width: 650px;
|
||||
overflow: visible;
|
||||
|
||||
&__buttons {
|
||||
margin-top: $gap-larger;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $gap-smaller;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button, Modal, TextControl } from '@wordpress/components';
|
||||
import { useState, createElement } from '@wordpress/element';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME,
|
||||
ProductTag,
|
||||
} from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { TRACKS_SOURCE } from '../../constants';
|
||||
|
||||
type CreateTagModalProps = {
|
||||
initialTagName?: string;
|
||||
onCancel: () => void;
|
||||
onCreate: ( newTag: ProductTag ) => void;
|
||||
};
|
||||
|
||||
export const CreateTagModal: React.FC< CreateTagModalProps > = ( {
|
||||
initialTagName,
|
||||
onCancel,
|
||||
onCreate,
|
||||
} ) => {
|
||||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
const [ isCreating, setIsCreating ] = useState( false );
|
||||
const { createProductTag, invalidateResolutionForStoreSelector } =
|
||||
useDispatch( EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME );
|
||||
const [ tagName, setTagName ] = useState( initialTagName || '' );
|
||||
|
||||
const onSave = async () => {
|
||||
recordEvent( 'product_tag_add', {
|
||||
source: TRACKS_SOURCE,
|
||||
} );
|
||||
setIsCreating( true );
|
||||
try {
|
||||
const newTag: ProductTag = await createProductTag( {
|
||||
name: tagName,
|
||||
} );
|
||||
invalidateResolutionForStoreSelector( 'getProductTags' );
|
||||
setIsCreating( false );
|
||||
onCreate( newTag );
|
||||
} catch ( e ) {
|
||||
createNotice(
|
||||
'error',
|
||||
__( 'Failed to create tag.', 'woocommerce' )
|
||||
);
|
||||
setIsCreating( false );
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={ __( 'Create tag', 'woocommerce' ) }
|
||||
onRequestClose={ () => onCancel() }
|
||||
className="woocommerce-create-new-tag-modal"
|
||||
>
|
||||
<div className="woocommerce-create-new-tag-modal__wrapper">
|
||||
<TextControl
|
||||
label={ __( 'Name', 'woocommerce' ) }
|
||||
name="Tops"
|
||||
value={ tagName }
|
||||
onChange={ setTagName }
|
||||
/>
|
||||
<div className="woocommerce-create-new-tag-modal__buttons">
|
||||
<Button
|
||||
isSecondary
|
||||
onClick={ () => onCancel() }
|
||||
disabled={ isCreating }
|
||||
>
|
||||
{ __( 'Cancel', 'woocommerce' ) }
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary
|
||||
disabled={ tagName.length === 0 || isCreating }
|
||||
isBusy={ isCreating }
|
||||
onClick={ () => {
|
||||
onSave();
|
||||
} }
|
||||
>
|
||||
{ __( 'Save', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './tag-field';
|
|
@ -1,4 +1,4 @@
|
|||
.woocommerce-category-field-dropdown {
|
||||
.woocommerce-tag-field-dropdown {
|
||||
.woocommerce-experimental-select-control__input {
|
||||
height: auto;
|
||||
}
|
||||
|
@ -10,8 +10,8 @@
|
|||
max-height: 300px;
|
||||
overflow-y: scroll;
|
||||
|
||||
> .woocommerce-category-field-dropdown__item:not(:first-child) {
|
||||
> .woocommerce-category-field-dropdown__item-content {
|
||||
> .woocommerce-tag-field-dropdown__item:not(:first-child) {
|
||||
> .woocommerce-tag-field-dropdown__item-content {
|
||||
border-top: 1px solid $gray-200;
|
||||
}
|
||||
}
|
||||
|
@ -19,13 +19,13 @@
|
|||
&__item {
|
||||
margin-bottom: 0;
|
||||
|
||||
.woocommerce-category-field-dropdown__item-content {
|
||||
.woocommerce-tag-field-dropdown__item-content {
|
||||
.components-base-control {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
&.is-new {
|
||||
.category-field-dropdown__toggle {
|
||||
.tag-field-dropdown__toggle {
|
||||
margin-right: $gap-smaller;
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +58,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.woocommerce-category-field-dropdown__item.item_highlighted > .woocommerce-category-field-dropdown__item-content {
|
||||
.woocommerce-tag-field-dropdown__item.item_highlighted
|
||||
> .woocommerce-tag-field-dropdown__item-content {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState, createElement, Fragment } from '@wordpress/element';
|
||||
import {
|
||||
TreeItemType,
|
||||
__experimentalSelectTreeControl as SelectTree,
|
||||
} from '@woocommerce/components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME,
|
||||
ProductTag,
|
||||
} from '@woocommerce/data';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { useDebounce } from '@wordpress/compose';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useTagSearch, ProductTagNode } from './use-tag-search';
|
||||
import { TRACKS_SOURCE } from '../../constants';
|
||||
import { CreateTagModal } from './create-tag-modal';
|
||||
|
||||
type TagFieldProps = {
|
||||
id: string;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
value?: ProductTagNode[];
|
||||
onChange: ( value: ProductTagNode[] ) => void;
|
||||
};
|
||||
|
||||
export function mapFromTagToTreeItem( val: ProductTagNode ): TreeItemType {
|
||||
return {
|
||||
value: String( val.id ),
|
||||
label: val.name,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapFromTreeItemToTag( val: TreeItemType ): ProductTagNode {
|
||||
return {
|
||||
id: +val.value,
|
||||
name: val.label,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapFromTagsToTreeItems(
|
||||
tags: ProductTagNode[]
|
||||
): TreeItemType[] {
|
||||
return tags.map( mapFromTagToTreeItem );
|
||||
}
|
||||
|
||||
export function mapFromTreeItemsToTags(
|
||||
tags: TreeItemType[]
|
||||
): ProductTagNode[] {
|
||||
return tags.map( mapFromTreeItemToTag );
|
||||
}
|
||||
|
||||
export const TagField: React.FC< TagFieldProps > = ( {
|
||||
id,
|
||||
label,
|
||||
placeholder,
|
||||
value = [],
|
||||
onChange,
|
||||
} ) => {
|
||||
const { tagsSelectList, searchTags } = useTagSearch();
|
||||
const [ searchValue, setSearchValue ] = useState( '' );
|
||||
const [ isCreating, setIsCreating ] = useState( false );
|
||||
const [ showCreateNewModal, setShowCreateNewModal ] = useState( false );
|
||||
const [ newInputValue, setNewInputValue ] = useState<
|
||||
undefined | string
|
||||
>();
|
||||
const { createProductTag, invalidateResolutionForStoreSelector } =
|
||||
useDispatch( EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME );
|
||||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
|
||||
const onInputChange = ( searchString?: string ) => {
|
||||
setSearchValue( searchString || '' );
|
||||
searchTags( searchString || '' );
|
||||
setNewInputValue( searchString );
|
||||
};
|
||||
|
||||
const searchDelayed = useDebounce( onInputChange, 150 );
|
||||
|
||||
const onSave = async () => {
|
||||
recordEvent( 'product_tag_add', {
|
||||
source: TRACKS_SOURCE,
|
||||
} );
|
||||
setIsCreating( true );
|
||||
try {
|
||||
setNewInputValue( '' );
|
||||
const newTag: ProductTag = await createProductTag( {
|
||||
name: searchValue,
|
||||
} );
|
||||
invalidateResolutionForStoreSelector( 'getProductTags' );
|
||||
setIsCreating( false );
|
||||
onChange( [ ...value, newTag ] );
|
||||
onInputChange( '' );
|
||||
} catch ( e ) {
|
||||
createNotice(
|
||||
'error',
|
||||
__( 'Failed to create tag.', 'woocommerce' )
|
||||
);
|
||||
setIsCreating( false );
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SelectTree
|
||||
id={ id }
|
||||
multiple
|
||||
shouldNotRecursivelySelect
|
||||
createValue={ searchValue }
|
||||
label={ label }
|
||||
isLoading={ isCreating }
|
||||
onInputChange={ searchDelayed }
|
||||
placeholder={ value.length === 0 ? placeholder : '' }
|
||||
initialInputValue={ newInputValue }
|
||||
onCreateNew={
|
||||
searchValue.length === 0
|
||||
? () => setShowCreateNewModal( true )
|
||||
: onSave
|
||||
}
|
||||
shouldShowCreateButton={ ( typedValue ) =>
|
||||
! typedValue ||
|
||||
tagsSelectList.findIndex(
|
||||
( item ) => item.name === typedValue
|
||||
) === -1
|
||||
}
|
||||
items={ mapFromTagsToTreeItems( tagsSelectList ) }
|
||||
selected={ mapFromTagsToTreeItems( value ) }
|
||||
onSelect={ ( selectedItems ) => {
|
||||
if ( Array.isArray( selectedItems ) ) {
|
||||
const newItems: ProductTagNode[] =
|
||||
mapFromTreeItemsToTags(
|
||||
selectedItems.filter(
|
||||
( { value: selectedItemValue } ) =>
|
||||
! value.some(
|
||||
( item ) =>
|
||||
item.id === +selectedItemValue
|
||||
)
|
||||
)
|
||||
);
|
||||
onChange( [ ...value, ...newItems ] );
|
||||
}
|
||||
} }
|
||||
onRemove={ ( removedItems ) => {
|
||||
const newValues = Array.isArray( removedItems )
|
||||
? value.filter(
|
||||
( item ) =>
|
||||
! removedItems.some(
|
||||
( { value: removedValue } ) =>
|
||||
item.id === +removedValue
|
||||
)
|
||||
)
|
||||
: value.filter(
|
||||
( item ) => item.id !== +removedItems.value
|
||||
);
|
||||
onChange( newValues );
|
||||
} }
|
||||
></SelectTree>
|
||||
{ showCreateNewModal && (
|
||||
<CreateTagModal
|
||||
initialTagName={ searchValue }
|
||||
onCancel={ () => setShowCreateNewModal( false ) }
|
||||
onCreate={ ( newTag ) => {
|
||||
onChange( [ ...value, newTag ] );
|
||||
setShowCreateNewModal( false );
|
||||
onInputChange( '' );
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -9,71 +9,66 @@ import { createElement } from '@wordpress/element';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CategoryField } from '../category-field';
|
||||
import { ProductCategoryNode } from '../use-category-search';
|
||||
import { TagField } from '../tag-field';
|
||||
import { ProductTagNode } from '../use-tag-search';
|
||||
|
||||
jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
|
||||
|
||||
jest.mock( '../use-category-search', () => {
|
||||
const originalModule = jest.requireActual( '../use-category-search' );
|
||||
jest.mock( '../use-tag-search', () => {
|
||||
return {
|
||||
getCategoriesTreeWithMissingParents:
|
||||
originalModule.getCategoriesTreeWithMissingParents,
|
||||
useCategorySearch: jest.fn().mockReturnValue( {
|
||||
searchCategories: jest.fn(),
|
||||
useTagSearch: jest.fn().mockReturnValue( {
|
||||
searchTags: jest.fn(),
|
||||
getFilteredItemsForSelectTree: jest.fn().mockReturnValue( [] ),
|
||||
isSearching: false,
|
||||
categoriesSelectList: [],
|
||||
categoryTreeKeyValues: {},
|
||||
tagsSelectList: [],
|
||||
tagTreeKeyValues: {},
|
||||
} ),
|
||||
};
|
||||
} );
|
||||
|
||||
describe( 'CategoryField', () => {
|
||||
describe( 'TagField', () => {
|
||||
beforeEach( () => {
|
||||
jest.clearAllMocks();
|
||||
} );
|
||||
|
||||
it( 'should render a dropdown select control', () => {
|
||||
const { queryByText, queryByPlaceholderText } = render(
|
||||
<Form initialValues={ { categories: [] } }>
|
||||
<Form initialValues={ { tags: [] } }>
|
||||
{ ( { getInputProps }: FormContextType< Product > ) => (
|
||||
<CategoryField
|
||||
label="Categories"
|
||||
placeholder="Search or create category…"
|
||||
{ ...getInputProps< ProductCategoryNode[] >(
|
||||
'categories'
|
||||
) }
|
||||
<TagField
|
||||
id="tag-field"
|
||||
label="Tags"
|
||||
placeholder="Search or create tag…"
|
||||
{ ...getInputProps< ProductTagNode[] >( 'tags' ) }
|
||||
/>
|
||||
) }
|
||||
</Form>
|
||||
);
|
||||
queryByPlaceholderText( 'Search or create category…' )?.focus();
|
||||
queryByPlaceholderText( 'Search or create tag…' )?.focus();
|
||||
expect( queryByText( 'Create new' ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should pass in the selected categories as select control items', () => {
|
||||
it( 'should pass in the selected tags as select control items', () => {
|
||||
const { queryAllByText, queryByPlaceholderText } = render(
|
||||
<Form
|
||||
initialValues={ {
|
||||
categories: [
|
||||
tags: [
|
||||
{ id: 2, name: 'Test' },
|
||||
{ id: 5, name: 'Clothing' },
|
||||
],
|
||||
} }
|
||||
>
|
||||
{ ( { getInputProps }: FormContextType< Product > ) => (
|
||||
<CategoryField
|
||||
label="Categories"
|
||||
placeholder="Search or create category…"
|
||||
{ ...getInputProps< ProductCategoryNode[] >(
|
||||
'categories'
|
||||
) }
|
||||
<TagField
|
||||
id="another-tag-field"
|
||||
label="Tags"
|
||||
placeholder="Search or create tag…"
|
||||
{ ...getInputProps< ProductTagNode[] >( 'tags' ) }
|
||||
/>
|
||||
) }
|
||||
</Form>
|
||||
);
|
||||
queryByPlaceholderText( 'Search or create category…' )?.focus();
|
||||
queryByPlaceholderText( 'Search or create tag…' )?.focus();
|
||||
expect( queryAllByText( 'Test, Clothing' ) ).toHaveLength( 1 );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useEffect, useState } from '@wordpress/element';
|
||||
import { resolveSelect } from '@wordpress/data';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME,
|
||||
ProductTag,
|
||||
} from '@woocommerce/data';
|
||||
|
||||
export type ProductTagNode = Pick< ProductTag, 'id' | 'name' >;
|
||||
|
||||
/**
|
||||
* A hook used to handle all the search logic for the tag search component.
|
||||
*/
|
||||
export const useTagSearch = () => {
|
||||
const [ fetchedTags, setFetchedTags ] = useState< ProductTag[] >( [] );
|
||||
const [ isSearching, setIsSearching ] = useState( true );
|
||||
|
||||
const fetchProductTags = ( search?: string ) => {
|
||||
setIsSearching( true );
|
||||
const query = search !== undefined ? { search } : '';
|
||||
resolveSelect( EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME )
|
||||
.getProductTags( query )
|
||||
.then( ( tags ) => {
|
||||
setFetchedTags( tags as ProductTag[] );
|
||||
} )
|
||||
.finally( () => {
|
||||
setIsSearching( false );
|
||||
} );
|
||||
};
|
||||
|
||||
useEffect( fetchProductTags, [] );
|
||||
|
||||
return {
|
||||
searchTags: fetchProductTags,
|
||||
tagsSelectList: fetchedTags,
|
||||
isSearching,
|
||||
};
|
||||
};
|
|
@ -1,4 +1,5 @@
|
|||
@import "./variations-actions-menu/styles.scss";
|
||||
@import "../../icons/hidden-with-help-icon.scss";
|
||||
|
||||
$table-row-height: calc($grid-unit * 9);
|
||||
|
||||
|
@ -48,6 +49,18 @@ $table-row-height: calc($grid-unit * 9);
|
|||
}
|
||||
}
|
||||
|
||||
&__price {
|
||||
text-align: right;
|
||||
padding-right: $grid-unit-40;
|
||||
}
|
||||
|
||||
&__regular-price--on-sale {
|
||||
text-decoration: line-through;
|
||||
color: $gray-600;
|
||||
margin-left: 6px;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
&__status-dot {
|
||||
margin-right: $gap-smaller;
|
||||
&.green {
|
||||
|
@ -70,6 +83,7 @@ $table-row-height: calc($grid-unit * 9);
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: $gap-smaller;
|
||||
|
||||
&--delete {
|
||||
&.components-button.components-menu-item__button.is-link {
|
||||
|
|
|
@ -78,7 +78,7 @@ export function VariationActionsMenu( {
|
|||
label={ __( 'Delete variation', 'woocommerce' ) }
|
||||
variant="link"
|
||||
onClick={ () => {
|
||||
onDelete( selection.id );
|
||||
onDelete( selection );
|
||||
onClose();
|
||||
} }
|
||||
className="woocommerce-product-variations__actions--delete"
|
||||
|
|
|
@ -40,8 +40,6 @@ import { useEntityId } from '@wordpress/core-data';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import HiddenIcon from './hidden-icon';
|
||||
import VisibleIcon from './visible-icon';
|
||||
import { getProductStockStatus, getProductStockStatusClass } from '../../utils';
|
||||
import {
|
||||
DEFAULT_VARIATION_PER_PAGE_OPTION,
|
||||
|
@ -51,10 +49,9 @@ import {
|
|||
import { VariationActionsMenu } from './variation-actions-menu';
|
||||
import { useSelection } from '../../hooks/use-selection';
|
||||
import { VariationsActionsMenu } from './variations-actions-menu';
|
||||
import HiddenWithHelpIcon from '../../icons/hidden-with-help-icon';
|
||||
|
||||
const NOT_VISIBLE_TEXT = __( 'Not visible to customers', 'woocommerce' );
|
||||
const VISIBLE_TEXT = __( 'Visible to customers', 'woocommerce' );
|
||||
const UPDATING_TEXT = __( 'Updating product variation', 'woocommerce' );
|
||||
|
||||
export function VariationsTable() {
|
||||
const [ currentPage, setCurrentPage ] = useState( 1 );
|
||||
|
@ -378,7 +375,22 @@ export function VariationsTable() {
|
|||
}
|
||||
) }
|
||||
>
|
||||
{ formatAmount( variation.price ) }
|
||||
{ variation.on_sale && (
|
||||
<span className="woocommerce-product-variations__sale-price">
|
||||
{ formatAmount( variation.sale_price ) }
|
||||
</span>
|
||||
) }
|
||||
<span
|
||||
className={ classnames(
|
||||
'woocommerce-product-variations__regular-price',
|
||||
{
|
||||
'woocommerce-product-variations__regular-price--on-sale':
|
||||
variation.on_sale,
|
||||
}
|
||||
) }
|
||||
>
|
||||
{ formatAmount( variation.regular_price ) }
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={ classnames(
|
||||
|
@ -402,63 +414,14 @@ export function VariationsTable() {
|
|||
<div className="woocommerce-product-variations__actions">
|
||||
{ variation.status === 'private' && (
|
||||
<Tooltip
|
||||
// @ts-expect-error className is missing in TS, should remove this when it is included.
|
||||
className="woocommerce-attribute-list-item__actions-tooltip"
|
||||
position="top center"
|
||||
text={ NOT_VISIBLE_TEXT }
|
||||
>
|
||||
<Button
|
||||
className="components-button--hidden"
|
||||
aria-label={
|
||||
isUpdating[ variation.id ]
|
||||
? UPDATING_TEXT
|
||||
: NOT_VISIBLE_TEXT
|
||||
}
|
||||
aria-disabled={
|
||||
isUpdating[ variation.id ]
|
||||
}
|
||||
onClick={ () =>
|
||||
handleVariationChange(
|
||||
variation.id,
|
||||
{ status: 'publish' }
|
||||
)
|
||||
}
|
||||
>
|
||||
{ isUpdating[ variation.id ] ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<HiddenIcon />
|
||||
) }
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) }
|
||||
|
||||
{ variation.status === 'publish' && (
|
||||
<Tooltip
|
||||
position="top center"
|
||||
text={ VISIBLE_TEXT }
|
||||
>
|
||||
<Button
|
||||
className="components-button--visible"
|
||||
aria-label={
|
||||
isUpdating[ variation.id ]
|
||||
? UPDATING_TEXT
|
||||
: VISIBLE_TEXT
|
||||
}
|
||||
aria-disabled={
|
||||
isUpdating[ variation.id ]
|
||||
}
|
||||
onClick={ () =>
|
||||
handleVariationChange(
|
||||
variation.id,
|
||||
{ status: 'private' }
|
||||
)
|
||||
}
|
||||
>
|
||||
{ isUpdating[ variation.id ] ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<VisibleIcon />
|
||||
) }
|
||||
</Button>
|
||||
<div>
|
||||
<HiddenWithHelpIcon />
|
||||
</div>
|
||||
</Tooltip>
|
||||
) }
|
||||
<VariationActionsMenu
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
.woocommerce-hidden-with-help-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
&__hidden-icon {
|
||||
color: $gray-600;
|
||||
}
|
||||
&__help-icon {
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import HelpIcon from './help-icon';
|
||||
import HiddenIcon from './hidden-icon';
|
||||
|
||||
export default function HiddenWithHelpIcon( {
|
||||
width = 24,
|
||||
height = 24,
|
||||
...props
|
||||
}: React.HTMLProps< HTMLDivElement > ) {
|
||||
return (
|
||||
<div className="woocommerce-hidden-with-help-icon" { ...props }>
|
||||
<HiddenIcon
|
||||
className="woocommerce-hidden-with-help-icon__hidden-icon"
|
||||
width={ width }
|
||||
height={ height }
|
||||
/>
|
||||
<HelpIcon className="woocommerce-hidden-with-help-icon__help-icon" />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -17,15 +17,11 @@
|
|||
@import "components/radio-field/style.scss";
|
||||
@import "components/notice/style.scss";
|
||||
@import "components/iframe-editor/style.scss";
|
||||
@import "components/details-categories-field/style.scss";
|
||||
@import "components/details-categories-field/create-category-modal.scss";
|
||||
@import "components/modal-editor/style.scss";
|
||||
@import "components/feedback-bar/style.scss";
|
||||
@import "components/product-mvp-feedback-modal/style.scss";
|
||||
@import "components/edit-product-link-modal/style.scss";
|
||||
@import "components/edit-product-link-modal/style.scss";
|
||||
@import "components/details-categories-field/style.scss";
|
||||
@import "components/details-categories-field/create-category-modal.scss";
|
||||
@import "components/attribute-control/attribute-field.scss";
|
||||
@import "components/attribute-control/edit-attribute-modal.scss";
|
||||
@import "components/attribute-control/new-attribute-modal.scss";
|
||||
|
@ -34,6 +30,8 @@
|
|||
@import "components/attribute-list-item/attribute-list-item.scss";
|
||||
@import "components/attribute-term-input-field/attribute-term-input-field.scss";
|
||||
@import "components/variations-table/styles.scss";
|
||||
@import "components/tags-field/create-tag-modal.scss";
|
||||
@import "components/tags-field/style.scss";
|
||||
|
||||
/* Field Blocks */
|
||||
|
||||
|
|
|
@ -1,5 +1,26 @@
|
|||
# Changelog
|
||||
|
||||
## [0.4](https://github.com/woocommerce/woocommerce/releases/tag/0.4) - 2023-09-12
|
||||
|
||||
- Patch - Add Woo AI Personalization setting and check setting when generating descriptions with AI.
|
||||
- Minor - Suggest product categories using AI
|
||||
- Minor - [Woo AI] Add a Write with AI button for the short description field in product editor.
|
||||
|
||||
## [0.3](https://github.com/woocommerce/woocommerce/releases/tag/0.3) - 2023-08-18
|
||||
|
||||
- Patch - Fix Woo AI settings page fields persistence bug when disabling the feature.
|
||||
- Patch - Woo AI - Fix store branding settings retrieval for use with description generation.
|
||||
- Minor - Adding settings screen for AI centric settings.
|
||||
- Minor - Generating short description after long description on product editor.
|
||||
- Minor - [Woo AI] Add Store Branding data to product description generation prompt.
|
||||
- Minor - Moving text completion hooks into @woocommerce/ai package for reuse.
|
||||
- Minor - Updating AI endpoints for product editing features.
|
||||
- Minor - Use additional product data (categories, tags, and attributes) when generating product descriptions.
|
||||
- Minor - Update pnpm monorepo-wide to 8.6.5
|
||||
- Minor - Update pnpm to 8.6.7
|
||||
- Patch - Update `wp-env` to version 8.2.0.
|
||||
- Minor - Upgrade TypeScript to 5.1.6
|
||||
|
||||
## [0.2](https://github.com/woocommerce/woocommerce/releases/tag/0.2) - 2023-06-28
|
||||
|
||||
- Minor - Adding error handling for a bad token request.
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Adding settings screen for AI centric settings.
|
|
@ -1,4 +0,0 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
[Woo AI] Add Store Branding data to product description generation prompt.
|
|
@ -1,4 +0,0 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Generating short description after long description on product editor.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue