diff --git a/.github/actions/tests/run-api-tests/action.yml b/.github/actions/tests/run-api-tests/action.yml index f569e12afcc..fab3e83cca3 100644 --- a/.github/actions/tests/run-api-tests/action.yml +++ b/.github/actions/tests/run-api-tests/action.yml @@ -1,6 +1,5 @@ name: Run API tests description: Runs the WooCommerce Core API tests and generates Allure report. -permissions: {} inputs: report-name: @@ -8,6 +7,9 @@ inputs: required: true tests: description: Specific tests to run, separated by single whitespace. See https://playwright.dev/docs/test-cli + playwright-config: + description: Playwright config file to be used + default: playwright.config.js outputs: result: @@ -23,7 +25,7 @@ runs: shell: bash run: | pnpm exec playwright test \ - --config=tests/api-core-tests/playwright.config.js \ + --config=tests/api-core-tests/${{ inputs.playwright-config }} \ ${{ inputs.tests }} - name: Generate Test report. diff --git a/.github/project-pr-labeler.yml b/.github/project-pr-labeler.yml index ad5c471c0d4..87d89fa1056 100644 --- a/.github/project-pr-labeler.yml +++ b/.github/project-pr-labeler.yml @@ -67,11 +67,11 @@ 'plugin: woo-ai': - plugins/woo-ai/**/* -'focus: performance tests [team:Solaris]': +'focus: performance tests': - plugins/woocommerce/tests/performance/**/* -'focus: api tests [team:Solaris]': +'focus: api tests': - plugins/woocommerce/tests/api-core-tests/**/* -'focus: e2e tests [team:Solaris]': +'focus: e2e tests': - plugins/woocommerce/tests/e2e-pw/**/* diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml index f96715d5dd0..c9519aeb277 100644 --- a/.github/workflows/cherry-pick.yml +++ b/.github/workflows/cherry-pick.yml @@ -294,6 +294,15 @@ jobs: core.setOutput( 'cherry-pick-pr', pr.data.html_url ) + // label PR + + const label = await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.data.number, + labels: ["metric: code freeze exception"], + }); + - name: Checkout trunk branch if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null run: git checkout trunk diff --git a/.github/workflows/cot-pr-build-and-e2e-tests.yml b/.github/workflows/non-cot-pr-build-and-e2e-tests.yml similarity index 87% rename from .github/workflows/cot-pr-build-and-e2e-tests.yml rename to .github/workflows/non-cot-pr-build-and-e2e-tests.yml index 02cfb498434..f54fc1dce89 100644 --- a/.github/workflows/cot-pr-build-and-e2e-tests.yml +++ b/.github/workflows/non-cot-pr-build-and-e2e-tests.yml @@ -1,4 +1,4 @@ -name: Run tests against PR in an environment with COT enabled +name: Run tests against PR in an environment with HPOS disabled on: pull_request: workflow_dispatch: @@ -10,8 +10,8 @@ concurrency: permissions: {} jobs: - cot-e2e-tests-run: - name: Runs E2E tests with COT enabled. + non-hpos-e2e-tests-run: + name: Runs E2E tests with HPOS disabled. runs-on: ubuntu-20.04 permissions: contents: read @@ -24,11 +24,9 @@ jobs: - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo - - name: Load docker images and start containers with COT enabled. + - name: Load docker images and start containers working-directory: plugins/woocommerce - env: - ENABLE_HPOS: 1 - run: pnpm env:test:cot --filter=woocommerce + run: pnpm env:test --filter=woocommerce - name: Download and install Chromium browser. working-directory: plugins/woocommerce @@ -39,6 +37,7 @@ jobs: id: run_playwright_e2e_tests env: USE_WP_ENV: 1 + ENABLE_HPOS: 0 working-directory: plugins/woocommerce run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js @@ -66,8 +65,8 @@ jobs: if-no-files-found: ignore retention-days: 5 - cot-api-tests-run: - name: Runs API tests with COT enabled. + non-hpos-api-tests-run: + name: Runs API tests with HPOS disabled. runs-on: ubuntu-20.04 permissions: contents: read @@ -80,11 +79,9 @@ jobs: - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo - - name: Load docker images and start containers with COT enabled. + - name: Load docker images and start containers working-directory: plugins/woocommerce - env: - ENABLE_HPOS: 1 - run: pnpm env:test:cot --filter=woocommerce + run: pnpm env:test --filter=woocommerce - name: Run Playwright API tests. id: run_playwright_api_tests @@ -93,6 +90,7 @@ jobs: BASE_URL: http://localhost:8086 USER_KEY: admin USER_SECRET: password + ENABLE_HPOS: 0 run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js - name: Generate Playwright API Test report. @@ -117,4 +115,4 @@ jobs: ${{ env.ALLURE_RESULTS_DIR }} ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore - retention-days: 5 + retention-days: 5 \ No newline at end of file diff --git a/.github/workflows/pr-build-and-e2e-tests.yml b/.github/workflows/pr-build-and-e2e-tests.yml index bcb64f055a0..0a2b8d2ae60 100644 --- a/.github/workflows/pr-build-and-e2e-tests.yml +++ b/.github/workflows/pr-build-and-e2e-tests.yml @@ -31,7 +31,6 @@ jobs: - name: Load docker images and start containers. working-directory: plugins/woocommerce env: - ENABLE_HPOS: 0 WP_ENV_PHP_VERSION: 7.4 run: pnpm run env:test @@ -265,4 +264,4 @@ jobs: -f pr_number=$PR_NUMBER \ -f commit_sha=$COMMIT_SHA \ -f s3_root=public \ - --repo woocommerce/woocommerce-test-reports + --repo woocommerce/woocommerce-test-reports \ No newline at end of file diff --git a/.github/workflows/smoke-test-release.yml b/.github/workflows/smoke-test-release.yml index e9892a9892a..554f92da5d2 100644 --- a/.github/workflows/smoke-test-release.yml +++ b/.github/workflows/smoke-test-release.yml @@ -155,16 +155,22 @@ jobs: install-filters: woocommerce build: false + - name: Download and install Chromium browser. + working-directory: plugins/woocommerce + run: pnpm exec playwright install chromium + - name: Run API tests id: run-api-composite-action uses: ./.github/actions/tests/run-api-tests with: report-name: ${{ env.API_WP_LATEST_ARTIFACT }} tests: hello + playwright-config: ci-release.playwright.config.js env: API_BASE_URL: ${{ secrets.RELEASE_TEST_URL }} USER_KEY: ${{ secrets.RELEASE_TEST_ADMIN_USER }} USER_SECRET: ${{ secrets.RELEASE_TEST_ADMIN_PASSWORD }} + UPDATE_WC: ${{ needs.get-tag.outputs.tag }} - name: Upload Allure artifacts to bucket if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' ) @@ -331,7 +337,7 @@ jobs: 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: @@ -341,7 +347,7 @@ jobs: working-directory: plugins/woocommerce run: pnpm run env:test env: - WP_ENV_CORE: WordPress/WordPress#${{ steps.get-wp-latest-1.outputs.version }} + WP_ENV_CORE: WordPress/WordPress#${{ steps.get-wp-latest-1.outputs.version }} - name: Download release zip env: diff --git a/.github/workflows/test-assistant-api-rest-change-tracker.yml b/.github/workflows/test-assistant-api-rest-change-tracker.yml new file mode 100644 index 00000000000..4dc2796943b --- /dev/null +++ b/.github/workflows/test-assistant-api-rest-change-tracker.yml @@ -0,0 +1,88 @@ +name: Send a Slack notification when a PR contains rest api changes + +on: + pull_request_target: + types: [labeled] + +permissions: {} + +jobs: + send-slack-notification-when-pr-contains-rest-api-changes: + if: "${{ github.event.label.name == 'contains: rest api change' && (github.event.pull_request.state == 'open' || github.event.pull_request.merged) }}" + runs-on: ubuntu-20.04 + steps: + - name: Wait 2 minutes for other labelling jobs to finish + run: sleep 2m + shell: bash + + - name: Calculate test date + id: calculate_date + run: | + #!/bin/bash + + # Get the day of the week of the merged PR (0 for Sunday, 1 for Monday, etc.) + MERGE_DAY_OF_WEEK=$(date -u -d "${{ github.event.pull_request.merged_at }}" +"%u") + + # Calculate days until the next Thursday after the merge + # If the merge is on Thursday, this will give 7 (i.e., next week's Thursday) + DAYS_UNTIL_NEXT_THURSDAY=$(( (4 + 7 - MERGE_DAY_OF_WEEK) % 7 )) + + # If DAYS_UNTIL_NEXT_THURSDAY is 0, the merge was on a Thursday, so we set it to 7 to get the next Thursday + if [ $DAYS_UNTIL_NEXT_THURSDAY -eq 0 ]; then + DAYS_UNTIL_NEXT_THURSDAY=7 + fi + + # Calculate the date for the next Thursday after the merge + THURSDAY_AFTER_MERGE=$(date -u -d "${{ github.event.pull_request.merged_at }} + $DAYS_UNTIL_NEXT_THURSDAY days" +"%Y-%m-%d") + WOOAF_RELEASE_DATE=$(date -u -d "${THURSDAY_AFTER_MERGE} + 6 days" +"%Y-%m-%d") + TEST_DATE_MESSAGE="Thursday, $THURSDAY_AFTER_MERGE. (Targeting release on $WOOAF_RELEASE_DATE)" + echo "TEST_DATE_MESSAGE=${TEST_DATE_MESSAGE}" >> $GITHUB_ENV + + - name: Determine Milestone Date + id: get_milestone_date + run: | + #!/bin/bash + + MILESTONE_TITLE="${{ github.event.pull_request.milestone.title }}" + MILESTONE_DATE="Undefined" + + # Mapping of milestone titles to release dates + declare -A MILESTONE_DATES + MILESTONE_DATES=( + ["8.0.0"]="2023-08-08" + ["8.1.0"]="2023-09-12" + ["8.2.0"]="2023-10-10" + ["8.3.0"]="2023-11-14" + ["8.4.0"]="2023-12-12" + ["8.5.0"]="2024-01-09" + ["8.6.0"]="2024-02-13" + ["8.7.0"]="2024-03-12" + ["8.8.0"]="2024-04-09" + ["8.9.0"]="2024-05-14" + ["9.0.0"]="2024-06-11" + ) + + # Check if the milestone title exists in our predefined list and get the date + if [[ -v "MILESTONE_DATES[${MILESTONE_TITLE}]" ]]; then + MILESTONE_DATE=${MILESTONE_DATES[${MILESTONE_TITLE}]} + fi + + # Export for later steps + echo "MILESTONE_DATE=${MILESTONE_DATE}" >> $GITHUB_ENV + + # Notify Slack Step + - name: Notify Slack + uses: archive/github-actions-slack@d9dae40827adf93bddf939db6552d1e392259d7d + id: notify + with: + slack-bot-user-oauth-access-token: ${{ secrets.TEST_ASSISTANCE_BOT_TOKEN }} + slack-channel: ${{ secrets.WOO_CORE_REST_API_CHANGES_SLACK_CHANNEL }} + slack-text: | + <${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}> + *Labels:* ${{ join(github.event.pull_request.labels.*.name, ', ') }} + *Monthly Release Milestone:* <${{ github.event.pull_request.milestone.html_url }}|${{ github.event.pull_request.milestone.title }}> (Release Date: ${{ env.MILESTONE_DATE }}) + *WooAF (weekly) Timeline: this PR can be tested from:* ${{ env.TEST_DATE_MESSAGE }} + Please visit the <#${{ secrets.WOO_CORE_RELEASES_SLACK_CHANNEL }}> to obtain the latest WooAF build for testing. + slack-optional-unfurl_links: false + slack-optional-unfurl_media: false + diff --git a/.markdownlint.json b/.markdownlint.json index 57085409db7..5e29a079a84 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -4,6 +4,7 @@ "MD007": { "indent": 4 }, "MD013": { "line_length": 9999 }, "MD024": { "allow_different_nesting": true }, + "MD033": { "allowed_elements": ["video"] }, "no-hard-tabs": false, "whitespace": false } diff --git a/changelog.txt b/changelog.txt index a68743b594e..a850fbf287a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,134 @@ == Changelog == += 8.2.0 2023-10-13 = + +**WooCommerce** + +* Fix - Correctly set 'created_via' for HPOS orders created on the admin. [#40469](https://github.com/woocommerce/woocommerce/pull/40469) +* Fix - Fix backwards compatibility issue with `wc_get_orders()` when HPOS is active and the pagination bit is set. [#40551](https://github.com/woocommerce/woocommerce/pull/40551) +* Fix - Save hpos order data before clearing the order from cache [#40282](https://github.com/woocommerce/woocommerce/pull/40282) +* Fix - Disable WP's post lock on HPOS order edit screen. [#40355](https://github.com/woocommerce/woocommerce/pull/40355) +* Fix - Enqueue media scripts for Images block within the product editor, as is required for Images block. [#40356](https://github.com/woocommerce/woocommerce/pull/40356) +* Fix - Addressed visual tweaks for CYS in response to feedback from 12th Sept [#40155](https://github.com/woocommerce/woocommerce/pull/40155) +* Fix - Address missing order type handling in HPOS compatibility mode sync. [#40279](https://github.com/woocommerce/woocommerce/pull/40279) +* Fix - Add Variation options section back to the product blocks template [#39914](https://github.com/woocommerce/woocommerce/pull/39914) +* Fix - Avoid a fatal error on the order received page if the order ID is not for a valid order. [#39876](https://github.com/woocommerce/woocommerce/pull/39876) +* Fix - Avoid string<>int comparison in products bought query to avoid results with customer_id = 0. [#40030](https://github.com/woocommerce/woocommerce/pull/40030) +* Fix - Changed Tax task completion criteria so that it considers both boolean and stringly typed values as expected [#39983](https://github.com/woocommerce/woocommerce/pull/39983) +* Fix - Display search results subtitle in HPOS list table view. [#40270](https://github.com/woocommerce/woocommerce/pull/40270) +* Fix - Eliminate an unnecessary redirect when the geo hash isalready set to the correct value. [#39634](https://github.com/woocommerce/woocommerce/pull/39634) +* Fix - Fix a bug where updating store location doesn't update store currency. [#40142](https://github.com/woocommerce/woocommerce/pull/40142) +* Fix - Fix cached refund not deleted when the refund is deleted with HPOS active [#40197](https://github.com/woocommerce/woocommerce/pull/40197) +* Fix - Fix changes in order custom fields made from admin not being applied when using the order Update button with HPOS active. [#40278](https://github.com/woocommerce/woocommerce/pull/40278) +* Fix - Fix customize store white screen bug in WP 6.3 [#40031](https://github.com/woocommerce/woocommerce/pull/40031) +* Fix - Fix customize your store task header button [#40031](https://github.com/woocommerce/woocommerce/pull/40031) +* Fix - Fix CYS UI issues [#40269](https://github.com/woocommerce/woocommerce/pull/40269) +* Fix - Fix CYS `__experimentalReapplyBlockTypeFilters` is not a function [#40104](https://github.com/woocommerce/woocommerce/pull/40104) +* Fix - Fixed missed lint error in Assembler Hub [#39964](https://github.com/woocommerce/woocommerce/pull/39964) +* Fix - Fix minor layout shift in the core profiler. [#39898](https://github.com/woocommerce/woocommerce/pull/39898) +* Fix - Fix product e2e tests [#39823](https://github.com/woocommerce/woocommerce/pull/39823) +* Fix - Fix undismissable notice when using localization for certain messages like "Coupon management has moved" [#39913](https://github.com/woocommerce/woocommerce/pull/39913) +* Fix - FIx WC Admin pages are empty for WP 6.2 and below. [#39995](https://github.com/woocommerce/woocommerce/pull/39995) +* Fix - Properly convert local time date queries to UTC in the HPOS datastore. [#40146](https://github.com/woocommerce/woocommerce/pull/40146) +* Fix - Redirect to Jetpack connect when jetpack-boost is selected. [#40261](https://github.com/woocommerce/woocommerce/pull/40261) +* Fix - Remove COT enable requirement from sync and verify command. [#39998](https://github.com/woocommerce/woocommerce/pull/39998) +* Fix - Removed references to the un-used Purchase task item in the onboarding task list. [#40121](https://github.com/woocommerce/woocommerce/pull/40121) +* Fix - Remove use of woocommerce-page class within WooCommerce Admin pages, replaced with woocommerce-admin-page. [#40218](https://github.com/woocommerce/woocommerce/pull/40218) +* Fix - Restore moving to trash functionality within HPOS order edit screen. [#39693](https://github.com/woocommerce/woocommerce/pull/39693) +* Fix - update the SqlQuery filter prefix in data.md [#39319](https://github.com/woocommerce/woocommerce/pull/39319) +* Fix - Use correct object reference when cloning a cart [#39282](https://github.com/woocommerce/woocommerce/pull/39282) +* Fix - [HPOS]Fix duplicate meta handling by passing meta_value|unique in post calls [#40088](https://github.com/woocommerce/woocommerce/pull/40088) +* Fix - [HPOS] Modify query to have less characters before the `FROM` keyword. [#40109](https://github.com/woocommerce/woocommerce/pull/40109) +* Fix - [HPOS] Support deleting metadata just by meta id. [#40064](https://github.com/woocommerce/woocommerce/pull/40064) +* Fix - [HPOS] Use objects method instead of calling datastore directly. [#40158](https://github.com/woocommerce/woocommerce/pull/40158) +* Add - Add ability to remove blocks from templates. [#39900](https://github.com/woocommerce/woocommerce/pull/39900) +* Add - Add a filter to OrdersTableQuery to allow overriding of HPOS queries. [#39945](https://github.com/woocommerce/woocommerce/pull/39945) +* Add - Add after_add_block and after_remove block hooks to the block template API. [#40139](https://github.com/woocommerce/woocommerce/pull/40139) +* Add - Add AI wizard business info step for Customize Your Store task [#39979](https://github.com/woocommerce/woocommerce/pull/39979) +* Add - Add component to Customize Your Store task. [#40140](https://github.com/woocommerce/woocommerce/pull/40140) +* Add - Add customize store - fonts [#40082](https://github.com/woocommerce/woocommerce/pull/40082) +* Add - Add customize store AI wizard call for best colour palette suggestions. [#40295](https://github.com/woocommerce/woocommerce/pull/40295) +* Add - Add customize store AI wizard call for color palette suggestion [#40237](https://github.com/woocommerce/woocommerce/pull/40237) +* Add - Add customize store AI wizard call for font pairing suggestion [#40240](https://github.com/woocommerce/woocommerce/pull/40240) +* Add - Add customize store assembler hub onboarding tour [#39981](https://github.com/woocommerce/woocommerce/pull/39981) +* Add - Add customize store assembler hub [#39843](https://github.com/woocommerce/woocommerce/pull/39843) +* Add - Add customize store color palettes [#40051](https://github.com/woocommerce/woocommerce/pull/40051) +* Add - Add customize store transitional screen [#40122](https://github.com/woocommerce/woocommerce/pull/40122) +* Add - Added URL navigation support to customize-store feature [#40068](https://github.com/woocommerce/woocommerce/pull/40068) +* Add - Add filter woocommerce_hpos_enable_sync_on_read to disable sync on read with HPOS sync enabled. [#40039](https://github.com/woocommerce/woocommerce/pull/40039) +* Add - Add has_price param to the variations REST API query. [#40281](https://github.com/woocommerce/woocommerce/pull/40281) +* Add - Add header customization to the Assembler Hub [#40107](https://github.com/woocommerce/woocommerce/pull/40107) +* Add - Add help text to Name field in Create new category modal [#40059](https://github.com/woocommerce/woocommerce/pull/40059) +* Add - Add new e2e test for Shopper My Account Downloads section [#40100](https://github.com/woocommerce/woocommerce/pull/40100) +* Add - Add new e2e test to cover My Account Addresses section [#40114](https://github.com/woocommerce/woocommerce/pull/40114) +* Add - Add sidebar to customize your store task. [#40136](https://github.com/woocommerce/woocommerce/pull/40136) +* Add - Adds new action hook `woocommerce_pay_order_before_payment` to the `checkout/form-pay.php` template. [#37588](https://github.com/woocommerce/woocommerce/pull/37588) +* Add - Add support for slug auto generation to the create attribute endpoint [#39827](https://github.com/woocommerce/woocommerce/pull/39827) +* Add - Add tags (or general taxonomy ) block [#39966](https://github.com/woocommerce/woocommerce/pull/39966) +* Add - Add track events to customize store AI wizard [#40144](https://github.com/woocommerce/woocommerce/pull/40144) +* Add - Add track events to customize store transitional page [#40143](https://github.com/woocommerce/woocommerce/pull/40143) +* Add - Add Tracks events to Appearance > Themes screen [#40193](https://github.com/woocommerce/woocommerce/pull/40193) +* Add - Add tracks to CYS assembler-hub and hide pages sidebar screen [#40156](https://github.com/woocommerce/woocommerce/pull/40156) +* Add - Add variable product experiment [#40177](https://github.com/woocommerce/woocommerce/pull/40177) +* Add - Add woocommerce_block_template_register action. [#39915](https://github.com/woocommerce/woocommerce/pull/39915) +* Add - Create a plugin to enable Variations feature #40027 [#40027](https://github.com/woocommerce/woocommerce/pull/40027) +* Add - Implement customize store assembler hub - logo feature [#39932](https://github.com/woocommerce/woocommerce/pull/39932) +* Add - Implemented loader design for Customize your store - Design with AI [#40083](https://github.com/woocommerce/woocommerce/pull/40083) +* Add - Made ai completion for look and tone more robust and added tracks [#40052](https://github.com/woocommerce/woocommerce/pull/40052) +* Add - Records plugin API requests and installation errors to coreprofiler_install_plugin_error separately for the core profiler. [#39899](https://github.com/woocommerce/woocommerce/pull/39899) +* Update - Added Marketplace class as basis for Reactified marketplace. [#39121](https://github.com/woocommerce/woocommerce/pull/39121) +* Update - Added xstate scaffolding for AI Wizard in customize your store feature [#39863](https://github.com/woocommerce/woocommerce/pull/39863) +* Update - Display a custom WooPayments onboarding task header content, when an incentive with server based header is available. [#40034](https://github.com/woocommerce/woocommerce/pull/40034) +* Update - Implement customize your store task completion logic [#40267](https://github.com/woocommerce/woocommerce/pull/40267) +* Update - Optimize customize store preview frame resize performance [#39930](https://github.com/woocommerce/woocommerce/pull/39930) +* Update - Remove core-profiler checks from the tests -- core profiler is enabled by default now. [#40260](https://github.com/woocommerce/woocommerce/pull/40260) +* Update - Replace Personalize Your Store task with Choose Your Theme [#40239](https://github.com/woocommerce/woocommerce/pull/40239) +* Update - Track coreprofiler_store_extensions_installed_and_activated when async installation is complete [#39921](https://github.com/woocommerce/woocommerce/pull/39921) +* Update - Turn off the experimental flag for HPOS. [#39846](https://github.com/woocommerce/woocommerce/pull/39846) +* Update - Update Action Scheduler to 3.6.3 [#40147](https://github.com/woocommerce/woocommerce/pull/40147) +* Update - Update intro screen for the new Customize Your Store task [#40293](https://github.com/woocommerce/woocommerce/pull/40293) +* Update - Update Remote Inbox Notifications to add in and !in comparison operators for comparing values against arrays [#40084](https://github.com/woocommerce/woocommerce/pull/40084) +* Update - Update the simple product template implementation to use the product form template API. [#39814](https://github.com/woocommerce/woocommerce/pull/39814) +* Update - Update use of preventLeavingProductForm with new function changes. [#40225](https://github.com/woocommerce/woocommerce/pull/40225) +* Update - Update WooCommerce Blocks to 10.9.3 [#39895](https://github.com/woocommerce/woocommerce/pull/39895) +* Update - Update WooCommerce Blocks to 11.0.0 [#39971](https://github.com/woocommerce/woocommerce/pull/39971) +* Update - Update WooCommerce Blocks to 11.1.0 [#40141](https://github.com/woocommerce/woocommerce/pull/40141) +* Update - Update WooCommerce Blocks to 11.1.1 [#40300](https://github.com/woocommerce/woocommerce/pull/40300) +* Update - Update WooCommerce Blocks to 11.1.2 [#40475](https://github.com/woocommerce/woocommerce/pull/40475) +* Update - We have completely redesigned the In-app Marketplace. [#39121](https://github.com/woocommerce/woocommerce/pull/39121) +* Dev - Added documentation for the Core Profiler [#39963](https://github.com/woocommerce/woocommerce/pull/39963) +* Dev - Add job to post Slack summary of plugin test results in "Smoke test daily" workflow. [#39838](https://github.com/woocommerce/woocommerce/pull/39838) +* Dev - Add new E2E test covering shopper product page and make Product-related tests granular (separated test files) [#40132](https://github.com/woocommerce/woocommerce/pull/40132) +* Dev - Add notice to "track inventory" toggle #40011 [#40011](https://github.com/woocommerce/woocommerce/pull/40011) +* Dev - Add some basic E2E tests for Assembler Hub [#40235](https://github.com/woocommerce/woocommerce/pull/40235) +* Dev - Adds test to check required fields on checkout [#40099](https://github.com/woocommerce/woocommerce/pull/40099) +* Dev - Bump required PHP version to 7.4 [#39820](https://github.com/woocommerce/woocommerce/pull/39820) +* Dev - Cleanup: remove the unused is_feature_visible and show_feature methods. [#39931](https://github.com/woocommerce/woocommerce/pull/39931) +* Dev - Fixes and enables API test suite to run on daily CI run against alternate host [#39858](https://github.com/woocommerce/woocommerce/pull/39858) +* Dev - Fix flakiness around the `Turn off the new product form` menu item. [#39957](https://github.com/woocommerce/woocommerce/pull/39957) +* Dev - Fix for a couple of flaky API tests on daily runs [#39918](https://github.com/woocommerce/woocommerce/pull/39918) +* Dev - Improve documentation for the `is_checkout()` function. [#40258](https://github.com/woocommerce/woocommerce/pull/40258) +* Dev - Refactored core profiler loader to be more generalizable and moved to @woocommerce/onboarding [#39735](https://github.com/woocommerce/woocommerce/pull/39735) +* Dev - Remove "WP Latest-2" from release tests. [#40012](https://github.com/woocommerce/woocommerce/pull/40012) +* Dev - Remove legacy PHP version update checks [#39845](https://github.com/woocommerce/woocommerce/pull/39845) +* Dev - Run a full reset on API daily test site [#40061](https://github.com/woocommerce/woocommerce/pull/40061) +* Dev - Updates Playwright from 1.33 to 1.37.1 [#39815](https://github.com/woocommerce/woocommerce/pull/39815) +* Tweak - Add order property to every block in SimpleProductTemplate [#39946](https://github.com/woocommerce/woocommerce/pull/39946) +* Tweak - Adds an informative tooltip to the Account Details section of the Direct Bank Transfer settings. [#39860](https://github.com/woocommerce/woocommerce/pull/39860) +* Tweak - Fix a minor code typo, no change in functionality [#36402](https://github.com/woocommerce/woocommerce/pull/36402) +* Tweak - Make it easier to disable email verification checks for the order confirmation and order pay pages. [#40050](https://github.com/woocommerce/woocommerce/pull/40050) +* Tweak - Migrate category field to woocommerce/product-taxonomy-field block [#40021](https://github.com/woocommerce/woocommerce/pull/40021) +* Tweak - tweak some of the HPOS Settings UI. [#39912](https://github.com/woocommerce/woocommerce/pull/39912) +* Tweak - Tweak tasklist description color to darker [#39903](https://github.com/woocommerce/woocommerce/pull/39903) +* Enhancement - Add CLI commands to enable or disable HPOS. [#39865](https://github.com/woocommerce/woocommerce/pull/39865) +* Enhancement - Design enhancements for the Attributes tab. [#39987](https://github.com/woocommerce/woocommerce/pull/39987) +* Enhancement - Design enhancements for the Inventory tab. [#39962](https://github.com/woocommerce/woocommerce/pull/39962) +* Enhancement - Enable HPOS by default for new installs. [#40296](https://github.com/woocommerce/woocommerce/pull/40296) +* Enhancement - Improve the existing E2E test to verify one more element on each page load. [#40008](https://github.com/woocommerce/woocommerce/pull/40008) +* Enhancement - Update the default setting for the task list progress bar from 0 to 0.25, which gives the progress better visual context when no tasks have been completed. [#39369](https://github.com/woocommerce/woocommerce/pull/39369) +* Enhancement - Update Venezuelan currency: Bolívar (Bs.). [#29380](https://github.com/woocommerce/woocommerce/pull/29380) + + = 8.1.1 2023-09-18 = **WooCommerce** diff --git a/docs/docs-manifest.json b/docs/docs-manifest.json new file mode 100644 index 00000000000..690a79691bc --- /dev/null +++ b/docs/docs-manifest.json @@ -0,0 +1,449 @@ +{ + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/style-guide.md", + "hash": "e81f1f926568e1792b6814c735c0321dee1356b4f8c053b7a5ee1770440e8052", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/style-guide.md", + "id": "211490a474b1b2ce6a6fc670b159f3497d6a9054" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/README.md", + "hash": "b1a13c53d16ba2a1b089a0cf19c5c37a8181f33bdbe0c6ee106cd1c80aef9fa0", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/README.md", + "id": "2c20e49a2bfd5bfcbb318dedbec96ada77784f14", + "links": { + "extension-development/building-your-first-extension.md": "bfb30a2379ec5e7c7960192b0a9175191e39aef7", + "extension-development/how-to-design-a-simple-extension.md": "c2f4612f31cc3892bfb19c6e9859f2daccdb6fa0", + "style-guide.md": "211490a474b1b2ce6a6fc670b159f3497d6a9054", + "../plugins/woocommerce/README.md": "827066d08695e5002bf059cd2e9d4d2a0fa58df8", + "../plugins/woocommerce/i18n/languages/README.md": "826a4400174812dfabb978c87ff2742bfdf28d62", + "../plugins/woocommerce/includes/README.md": "3d07aabeb0926e4c675e5770e78a0cfa537d2f56", + "../plugins/woocommerce/lib/README.md": "7c7e05959e4e9dcde4ac0e3e2a13258d7521e731", + "../plugins/woocommerce/packages/README.md": "740c206346a48e9dcb2e70efd5a3221992c389dc", + "../plugins/woocommerce/src/README.md": "c7444c322c5bb1ff755b2bf3e961babf3a879f4e", + "../plugins/woocommerce/src/Admin/RemoteInboxNotifications/README.md": "a46d35b9e8a8c15b89082e0c2c04d83c852d545f", + "../plugins/woocommerce/src/Admin/RemoteInboxNotifications/Transformers/README.md": "bbccc83ae18c86679a3b632e7203f6c395987967", + "../plugins/woocommerce/src/Blocks/README.md": "532a6602e970797759269b4c588c724551379214", + "../plugins/woocommerce/src/Internal/README.md": "67d669be90b641e2273194796e3d9bdfdbd69a7e", + "../plugins/woocommerce/src/Internal/Admin/ProductForm/README.md": "1d691e34fd20dd268e8e9f8283f46a595758c33b", + "../plugins/woocommerce/tests/README.md": "722532b12b14b2f5c0b8efdd96cca1854bba38c4", + "../plugins/woocommerce/tests/api-core-tests/README.md": "b000db6a9a2807a49a3bb47c57bb78326a865c78", + "../plugins/woocommerce/tests/e2e/README.md": "c15296a46be7331ed23d791073cdebeb0f4c48c4", + "../plugins/woocommerce/tests/e2e-pw/README.md": "103a9a613a34a031b34ca48f0895640dc9fc2b10", + "../plugins/woocommerce/tests/performance/README.md": "8c14e3b9fac89bced565b78e96d3a3a89e0a568e", + "../plugins/woocommerce/tests/Tools/CodeHacking/README.md": "231d9fc132423f1ecc391aaf1bcb57a3cd749d24", + "../plugins/woocommerce-admin/README.md": "9ba7a7e2c4411e01c70d866f8b8b5604484368d8", + "../plugins/woocommerce-admin/client/activity-panel/README.md": "2de9812a153a9c9ed90588feacd9ece41ecff93c", + "../plugins/woocommerce-admin/client/activity-panel/activity-card/README.md": "c34672cf87665e3ff157c6df3ddf1993fd33e7a4", + "../plugins/woocommerce-admin/client/activity-panel/activity-header/README.md": "13e379fd37082d3cba36e76046acfc63ff70ab8a", + "../plugins/woocommerce-admin/client/analytics/report/README.md": "84430b89912cb95d31240cd2d00a400b2d904ec4", + "../plugins/woocommerce-admin/client/analytics/settings/README.md": "421781cef9f3d0160c1e3892488b21b3f4a2a7f5", + "../plugins/woocommerce-admin/client/dashboard/README.md": "f7d6ff3c0f18554161afaa94698fb127f098986f", + "../plugins/woocommerce-admin/client/header/README.md": "39d3152fc1be21beee488dddd3f5a90e6af2502a", + "../plugins/woocommerce-admin/client/marketing/README.md": "54f1eb4cf5ec67713ce2fdf2b2f1dc28b03e35ef", + "../plugins/woocommerce-admin/client/marketing/components/product-icon/README.md": "3fa5081872b79078ad320dd0d9e218ed4148dc4e", + "../plugins/woocommerce-admin/client/utils/README.md": "0486d6a8f01b89bf171e21678df5dae9b5791a99", + "../plugins/woocommerce-admin/client/wp-admin-scripts/README.md": "641ab0c131d812f0969631b44ed9e8fe3fdb0cba", + "../plugins/woocommerce-admin/docs/README.md": "25155a38fc7599a01bfb30eb31a1edb098b59219", + "../plugins/woocommerce-admin/docs/examples/README.md": "b5ca126b81199d9da6ae62d5d26c093f88405a06", + "../plugins/woocommerce-admin/docs/examples/extensions/README.md": "42a90d07061074d0de1b15aeaf80531f83b2a258", + "../plugins/woocommerce-admin/docs/features/README.md": "1ee849cb8dc013ffbfe8ebcb54b8cc83f39d72e4", + "../plugins/woocommerce-admin/docs/woocommerce.com/README.md": "ee127fc0f256c3ae546869c602e94b4a6e258be8", + "../plugins/woocommerce-beta-tester/README.md": "081da0cc3caba15e926b606082120c2692b71ad4", + "../plugins/woocommerce-beta-tester/src/tools/README.md": "aedc38bbe8b29e676a15d252bcb1be38acc86f3c", + "../plugins/woocommerce-beta-tester/userscripts/README.md": "d3d94172608c29c4968ee73858ee3575a2cc11ce" + } + } + ], + "categories": [ + { + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/unhook--remove-woocommerce-emails.md", + "hash": "a22ec5c7c7c670e97e34a438d5e13dc3709716484efbecbf49e5917b6da9079d", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/unhook--remove-woocommerce-emails.md", + "id": "517b5bdeb798c1832c3ba76670e728b7b922d5d1" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/rename-a-country.md", + "hash": "3929e2145dcaad30a83fda6d82d973028e4463eeb2f6a6ba6ff9cb70c6b52fd5", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/rename-a-country.md", + "id": "2197550f7fdfbce39155865ba36f1f396a35e4a6" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/number-of-products-per-row.md", + "hash": "507e7be407716b56fe577109b296b55cdcb48cb2b2e339a761446b4df2d730e9", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/number-of-products-per-row.md", + "id": "7da8ddcdbc3f427d14b040015fb1de9efde79d19" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/change-a-currency-symbol.md", + "hash": "4485b212ff7bdf7fd8c23dc589dbbb6263a530623b939b384297f2377b11056f", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/change-a-currency-symbol.md", + "id": "de0e6701b0e15a209ac6ed3c100b34f67237ea96" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/before-login--register-form.md", + "hash": "3435f3859ec047566696ae5474df697c99a6e1ab04168ae8f1ca831abc519d67", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/before-login--register-form.md", + "id": "5acf55bdb90174bc00a10816ca71a8846bd7040f" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/adjust-quantity-input-values.md", + "hash": "056fb597c07e0e82b92ca8a2c484166f807d3d513fa950cb5c46a8db087fe986", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/adjust-quantity-input-values.md", + "id": "06d0ddda56838dc22ad94716aca95a1f2a9ad67c" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/add-or-modify-states.md", + "hash": "d61d5051439b0cbf949be64958c8101a7c6487672a7b79dd3f528e9c1a5b408e", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/add-or-modify-states.md", + "id": "10568c46b0baa2f8ea01a8cd9dd6b2d65d35aafc" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/add-a-currency-symbol.md", + "hash": "0053a87328ee04a168c5657df26cf18f219d30669b000300ace4284ea2333186", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/add-a-currency-symbol.md", + "id": "6d63d431fa1d66af7b3b47861ccc940775eba397" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/add-a-country.md", + "hash": "9305a5ed51cc0174e19e68ebc94801c5e7d6e62e820d106f35175540b0531d10", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/add-a-country.md", + "id": "7c591c3bd31cb2942fdcb4f77da167bc9ec36207" + } + ], + "categories": [] + }, + { + "category_title": "Contributing", + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/contributing/deciding-pr-high-impact.md", + "hash": "6d9ddea0e44e1ab9f7b12183069c7fde51df26c9bcad15d8b6f9d3ab17ee1fe0", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/contributing/deciding-pr-high-impact.md", + "id": "cee3a746887c88f92966bb72ec1847aa5d97cebd" + } + ], + "categories": [] + }, + { + "category_title": "Data-management", + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/data-management/data-stores.md", + "hash": "7af9424c5b3fdfa5b30b658f0c3a452ac12d67390b58e9c23ef3bc71412b2a92", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/data-management/data-stores.md", + "id": "72609f6c4fecccf2881c76bb8f21fc6a2d3cfef5" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/data-management/crud-objects.md", + "hash": "9b687cd323893ed0fc012ad82e67a329ea0f41c312017bfdf3015ab7ef51a23a", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/data-management/crud-objects.md", + "id": "0214be4dc9e0ddcc6d6a175fcd2f2b9fd4c8b042" + } + ], + "categories": [] + }, + { + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/tools-for-low-code-development.md", + "hash": "5989c60346c29c019332b7e2faee0dfcddf063db2d12f22f7755525e99c8ba4a", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/tools-for-low-code-development.md", + "id": "9db13086ff88d75fc5d5cd7bd36820b2d372b288" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/readme.md", + "hash": "e22a09b0d39adf37cd19fa57263e1195646f766396a36571411c54c64c6ba5a1", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/readme.md", + "id": "4946a26f3f2ba63d5db65033a90b6dda3a43a4ee" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/how-to-design-a-simple-extension.md", + "hash": "893c9361b6a1f6cd8a83dc9a4500c9b3050d66ef6d567fddf56d68de01d182a1", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/how-to-design-a-simple-extension.md", + "id": "c2f4612f31cc3892bfb19c6e9859f2daccdb6fa0" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/how-to-add-your-own-store-management-links.md", + "hash": "59dfa1f941a2fafaccedf2dd9be5bc1ee8c34b7320c6589c860ad98403bcd273", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/how-to-add-your-own-store-management-links.md", + "id": "461423141956d0653582aa15956a7b0401301cf6" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/handling-merchant-onboarding.md", + "hash": "e4a8358de815e378beb07426fd5687a0e91792f70a697574edd5031bf073c08d", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/handling-merchant-onboarding.md", + "id": "878874b744cea792a439935c67ac833a5f7d8796" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/handling-deactivation-and-uninstallation.md", + "hash": "cfc8cdf9e9a41e69cf40dcc1548cf7b0231c51c33c504ee800dd7525afe6d483", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/handling-deactivation-and-uninstallation.md", + "id": "93a623f5b7ff808c92541941787bdc2101518f5a" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/extension-developer-handbook.md", + "hash": "ff87298651dff8672b0c0da8693b53cb19d1fe7e65f04a947029aeec9cc9896f", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/extension-developer-handbook.md", + "id": "9f22e410b1ff4f6e5c22f6b4567c1a676d037295" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/development-environment.md", + "hash": "811f43ef03b40b48ad72a9d787031cb10d5a17b5f4ef4a2d5c0fa4977b2d43df", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/development-environment.md", + "id": "188b18434a7e844978d4d25a4b1fbc74004294d5" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/building-your-first-extension.md", + "hash": "2aad1fe83142ff55197f3bc560fafbe2c1279ccf9bb727fea5257eed447ba694", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/building-your-first-extension.md", + "id": "bfb30a2379ec5e7c7960192b0a9175191e39aef7" + } + ], + "categories": [] + }, + { + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/readme.md", + "hash": "8aa7cd66683478dff1f37c767f154d35e6b78a738b604d0bc2ae31a10f920ba6", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/readme.md", + "id": "2145ad4acea5e37b1ff4354bf44a8af9d10c750a" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/developer-tools.md", + "hash": "860d227350f472b610351bf416953bf9401bf0df296ee8ebe2af9c7a9d20fc57", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/developer-tools.md", + "id": "3d8c7c1a9187dd7c96d2561bccfcfa88a8d0102a" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/developer-resources.md", + "hash": "e5417528667f6afb2de5ed1b4f7073a63268e72e163bd67a8a1daeda1e623eb0", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/developer-resources.md", + "id": "0923cb0175b15041bcecf8f9dbc60828052e1fec" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/getting-started/debugging.md", + "hash": "e8407732d22ffa183db83e5875e6709bdc8dd7ac47a05280590c4b5f1021fea9", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/getting-started/debugging.md", + "id": "b28ccc6fa293efebd716b510df0331fb222b8a1b", + "links": { + "../utilities/logging.md": "f673f50bdd83d5d0d7fd8d9976828f8736809510" + } + } + ], + "categories": [] + }, + { + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/high-performance-order-storage/readme.md", + "hash": "aada6bfad51c9f68ddb61e6475aae137e6b2d91839afa001aa3862ecd651093b", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/high-performance-order-storage/readme.md", + "id": "214c00235e4efed0a786fd704c4bfde25952a669" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/high-performance-order-storage/hpos-overview.md", + "hash": "a7504d828aed93aa68fbaff338406682fa6169f9401b2c4de6e750d7f51c7416", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/high-performance-order-storage/hpos-overview.md", + "id": "380a11e440eb17bc76fb606fecce4ce82fc9b0cb" + } + ], + "categories": [] + }, + { + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-editor-development/common-tasks.md", + "hash": "a7725479ff8b34f8069f3a8262502dc82354ea50d6ccd3760c5c7f7a9ae58fe2", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-editor-development/common-tasks.md", + "id": "c09542004d78254458250704f3a68cfeb9fa7a7c" + } + ], + "categories": [] + }, + { + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/readme.md", + "hash": "7afc464833ea0eb33127e98cb92f951bcb4a5d9006898e56db9d1f36006970cf", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/readme.md", + "id": "f12729345d42760bd61a4a4cfdcebf6dbf2daac4" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/performance-optimization.md", + "hash": "cc54a9af29f80e413536f46e70bc17512c92de536375bded23e4893a3e95e262", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/performance-optimization.md", + "id": "280d9d16382ef5374226629751cff7fceefbfa62" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/grammar-punctuation-capitalization.md", + "hash": "cea26a41342f5e67bb3313c935726da68647827a60c2fb034f3e64dd783496bd", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/grammar-punctuation-capitalization.md", + "id": "85d910730d1daa03c86c42f2d4cbd2e146cec83c" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/quality-and-best-practices/coding-standards.md", + "hash": "332efc3cccdde767237087abba302fcc371d510e5f287d439de49fe99a5445cc", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/quality-and-best-practices/coding-standards.md", + "id": "3a47d9e0da6c415606b7ee87bb65ed24f4630be1" + } + ], + "categories": [] + }, + { + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/reference-code/readme.md", + "hash": "49a289229235989dcb8fa990af7e5017ae37569d6103dfd044ee2d95f250f596", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/reference-code/readme.md", + "id": "1934f96989f9b62e2caf7fd0ac7deb2292f161ce" + } + ], + "categories": [] + }, + { + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/reporting/readme.md", + "hash": "117ccaf1bf931f7a98486ac0b506c48dd4834a2f2acd76f8936a9bf6ba93171b", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/reporting/readme.md", + "id": "fc4ef6d8e26fc46ef6c9da5e0d860cf051928e12" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/reporting/extending-woocommerce-admin-reports.md", + "hash": "860c37ab2d1de9744d838e6d3b11eeadb6e6cb11afffa8dfc11021240661d843", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/reporting/extending-woocommerce-admin-reports.md", + "id": "9960520d4548fd3647c5bbc7ed4d4fcc6a76cfce" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/reporting/adding-columns-to-analytics-reports-and-csv-downloads.md", + "hash": "e8265405615834b04a02f5406c38e3be8acdd01116b66f08f25f99a86a580948", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/reporting/adding-columns-to-analytics-reports-and-csv-downloads.md", + "id": "10cabd954ae223ac83f84223611f6d82361f9d9d", + "links": { + "extending-woocommerce-admin-reports.md": "9960520d4548fd3647c5bbc7ed4d4fcc6a76cfce" + } + } + ], + "categories": [] + }, + { + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/rest-api/readme.md", + "hash": "895a1986fd30f437c1bc2aaa618d6a06a98c65511cd257077302c2d1225787d9", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/rest-api/readme.md", + "id": "128b41ed5e9a0ed13a51de3196bd06c0297b2f33" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/rest-api/getting-started.md", + "hash": "b0bb28f6eed57c7d57dde75e8093771450db4666cbfab16d35387c39cd8d1e20", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/rest-api/getting-started.md", + "id": "10f52db2464ea2f373eff9a45ce9e8ff40214122", + "links": { + "_media/permalinks.webp": "5ac10f14cd0ca3ae45a9c089d659fee3ea4d9f22", + "_media/keys.png": "7a29fcb3f0b565f4f6983f1e12200d2c4d62907b", + "_media/postman.png": "b100bbf70f8889c26fcde6a6daca29be796071de", + "_media/insomnia.png": "a50683984998ab518b4b05433ffb1803f2ac135f", + "_media/sslerror.png": "5b6825fb0aba7ed55e1641880701c2afd1744ce1", + "_media/postman-ssl.png": "7e3523b0340acf908e98ac320f822d00a0710ac5", + "_media/insomnia-ssl.png": "84e6406407498b9936d6ef1df5e4c4cc78f75b00" + } + } + ], + "categories": [ + { + "category_title": "_media", + "categories": [] + } + ] + }, + { + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/security/security-best-practices.md", + "hash": "ded14bf9c1b5402c9896c795c4c4cbe6165d465de56009c32559f854446bafda", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/security/security-best-practices.md", + "id": "ee399e729785fec50b90d73b231d321b34ab5ff9" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/security/reporting-security-issues.md", + "hash": "4a748990a0e3207c9f0828c585c1d2edce14bafee9afc331d43bda0b8ee09aa6", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/security/reporting-security-issues.md", + "id": "48caba58b4e5b44fc418bb80cf03aa48f87e4cc8" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/security/readme.md", + "hash": "b484a4886068a33cd8114066262e95a2c8036f1e11d09f0dc63141cd36d85e95", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/security/readme.md", + "id": "8b4aa089616cbeeedfec301eee17b5a00f669154" + } + ], + "categories": [] + }, + { + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/theme-development/theme-design-ux-guidelines.md", + "hash": "bcab4deb86172df46ff775a23b921f8fb4aadd5b5c96085451609a3863642efc", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/theme-development/theme-design-ux-guidelines.md", + "id": "1abe0b1ff5e3990387c7da7fa5a147430b3282f7" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/theme-development/marketplace-guidelines.md", + "hash": "5ca562b2f4cabefe47b0d08334f39d56e8d3e6dc31ea347000d3bb221e51d0a1", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/theme-development/marketplace-guidelines.md", + "id": "f39a348d787fd0cf4a390e70e6f0cbd36b27a283" + } + ], + "categories": [] + }, + { + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/tutorials/readme.md", + "hash": "2ca52b9289180adad5d59865ca45c50098119b420662d727d739243e6a98131a", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/tutorials/readme.md", + "id": "0741650e7ad7567b84f776db7ddad2b8f5a598ea" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/tutorials/adding-actions-and-filters.md", + "hash": "fa6bc021c918aa168023a90e683832455dfa35abbb2561470b08002f1281356f", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/tutorials/adding-actions-and-filters.md", + "id": "3f4d6dcdbc181b11c97c438132ec13a0a8484524" + }, + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/tutorials/adding-a-custom-field-to-variable-products.md", + "hash": "a6b654234e7c7f1fc3f9be49b7d00187d6f271e2d699971bade3cf9a728a780a", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/tutorials/adding-a-custom-field-to-variable-products.md", + "id": "f0a7a4a194a6e0aee6f939791f3eaebf36aebb27" + } + ], + "categories": [] + }, + { + "category_title": "Utilities", + "posts": [ + { + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/utilities/logging.md", + "hash": "8912094df57a66c047fe2177be815a40024b461a10ddd61ed297c4b2c2919d64", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/utilities/logging.md", + "id": "f673f50bdd83d5d0d7fd8d9976828f8736809510", + "links": { + "_media/log-critical.jpg": "cc802f94ac4cf125a66bc6bb77bf95b80bf4fbed" + } + } + ], + "categories": [ + { + "category_title": "_media", + "categories": [] + } + ] + } + ], + "hash": "fc6ee378adfa92b706b9d0813f541bf6e8029634c0dd6623000f76047316f0af" +} \ No newline at end of file diff --git a/docs/extension-development/settings-api.md b/docs/extension-development/settings-api.md new file mode 100644 index 00000000000..35f1dce1d72 --- /dev/null +++ b/docs/extension-development/settings-api.md @@ -0,0 +1,110 @@ +# Settings API + +The WooCommerce Settings API is used by extensions to display, save, and load settings. The best way to make use of the API in your extension is to create a class that extends the `WC_Settings_API` class: + +```php +class My_Extension_Settings extends WC_Settings_API { + // +} +``` + +## Defining form fields + +You can define your fields using a method called `init_form_fields` in your class constructor: + +```php +$this->init_form_fields(); +``` + +You must have your settings defined before you can load them. Setting definitions go in the `form_fields` array: + +```php +/** + * Initialise gateway settings form fields. + */ +function init_form_fields() { + $this->form_fields = array( + 'title' => array( + 'title' => __( 'Title', 'your-text-domain' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'your-text-domain' ), + 'default' => __( 'PayPal', 'your-text-domain' ) + ), + 'description' => array( + 'title' => __( 'Description', 'your-text-domain' ), + 'type' => 'textarea', + 'description' => __( 'This controls the description which the user sees during checkout.', 'your-text-domain' ), + 'default' => __( "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account", 'your-text-domain' ) + ) + ); +} // End init_form_fields() +``` + +(Make sure your class initializes the `form_fields` property so that the "Creation of dynamic property" error is not thrown in PHP 8.2+) + +In the above example we define two settings, Title and Description. Title is a text box, whereas Description is a textarea. Notice how you can define a default value and a description for the setting itself. + +Setting definitions use the following format: + +```php +'setting_name' => array( + 'title' => 'Title for your setting shown on the settings page', + 'description' => 'Description for your setting shown on the settings page', + 'type' => 'text|password|textarea|checkbox|select|multiselect', + 'default' => 'Default value for the setting', + 'class' => 'Class for the input element', + 'css' => 'CSS rules added inline on the input element', + 'label' => 'Label', // For checkbox inputs only. + 'options' => array( // Array of options for select/multiselect inputs only. + 'key' => 'value' + ), +) +``` + +## Displaying your settings + +Create a method called `admin_options` containing the following: + +```php +function admin_options() { + ?> +

+ + generate_settings_html(); ?> +
+ init_settings(); +``` + +After that you can load your settings from the settings API. The `init_settings` method above populates the settings variable for you: + +```php +// Define user set variables +$this->title = $this->settings['title']; +$this->description = $this->settings['description']; +``` diff --git a/docs/product-editor-development/README.md b/docs/product-editor-development/README.md new file mode 100644 index 00000000000..1bc62be54ef --- /dev/null +++ b/docs/product-editor-development/README.md @@ -0,0 +1,38 @@ +# Product Editor Development Handbook + +> ⚠️ **Notice:** This documentation is currently a **work in progress**. Please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions! + +This handbook is a guide for extension developers looking to add support for the new product editor in their extensions. The product editor uses [Gutenberg's Block Editor](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-editor), which is going to help WooCommerce evolve alongside the WordPress ecosystem. + +The product editor's UI consists of Groups (currently rendered as tabs), Sections, and Fields, which are all blocks. + +![Product editor structure](https://woocommerce.files.wordpress.com/2023/09/groups-sections-fields.jpg) + +The form's structure is defined in PHP using a Template, which is a tree structure of blocks. The template can be modified by using the Template API to add new Groups, Sections, and Fields as well as remove existing ones. + +Many extensibility implementations can be done using only the PHP-based Block Template API alongside our library of [generic blocks](../../packages/js/product-editor/src/blocks/generic/README.md). More complex interactivity can be implemented using JavaScript and React (the same library used to implement the core blocks used in the product editor). [@woocommerce/create-product-editor-block](../../packages/js/create-product-editor-block/README.md) can help scaffold a development environment with JavaScript and React. + +## Declaring compatibility with the product editor + +To declare compatibility with the product editor, add the following to your plugin's root PHP file: + +```php +use Automattic\WooCommerce\Utilities\FeaturesUtil; + +add_action( + 'before_woocommerce_init', + function() { + if ( class_exists( FeaturesUtil::class ) ) { + FeaturesUtil::declare_compatibility( 'product_block_editor', plugin_basename( __FILE__ ), true ); + } + } +); +``` + +Please note that this check is currently not being enforced: the product editor can still be enabled even without this declaration. However, we might enforce this in the future, so it is recommended to declare compatibility as soon as possible. + +## Related documentation + +- [Examples on Template API usage](../../plugins/woocommerce/src/Admin/Features/ProductBlockEditor/ProductTemplates/README.md) +- [Related hooks and Template API documentation](../../plugins/woocommerce/src/Admin/BlockTemplates/README.md) +- [Generic blocks documentation](../../packages/js/product-editor/src/blocks/generic/README.md) diff --git a/docs/quality-and-best-practices/css-sass-naming-conventions.md b/docs/quality-and-best-practices/css-sass-naming-conventions.md new file mode 100644 index 00000000000..568374cb742 --- /dev/null +++ b/docs/quality-and-best-practices/css-sass-naming-conventions.md @@ -0,0 +1,62 @@ +# CSS/Sass Naming Conventions + +Table of Contents: + +- [Introduction](#introduction) +- [Prefixing](#prefixing) +- [Class names](#class-names) + - [Example](#example) +- [TL;DR](#tldr) + +## Introduction + +Our guidelines are based on those used in [Calypso](https://github.com/Automattic/wp-calypso), which itself follows the [BEM methodology](https://getbem.com/). + +Refer to the [Calypso CSS/Sass Coding Guidelines](https://wpcalypso.wordpress.com/devdocs/docs/coding-guidelines/css.md) for full details. + +Read more about [BEM key concepts](https://en.bem.info/methodology/key-concepts/). + +There are a few differences in WooCommerce which are outlined below. + +## Prefixing + +As a WordPress plugin WooCommerce has to play nicely with WordPress core and other plugins/themes. To minimize conflict potential, all classes should be prefixed with `.woocommerce-`. + +## Class names + +When naming classes, remember: + +- **Block** - Standalone entity that is meaningful on its own. Such as the name of a component. +- **Element** - Parts of a block and have no standalone meaning. They are semantically tied to its block. +- **Modifier** - Flags on blocks or elements. Use them to change appearance or behavior. + +### Example + +```css +/* Block */ +.woocommerce-loop {} + +/* Nested block */ +.woocommerce-loop-product {} + +/* Modifier */ +.woocommerce-loop-product--sale {} + +/* Element */ +.woocommerce-loop-product__link {} + +/* Element */ +.woocommerce-loop-product__button-add-to-cart {} + +/* Modifier */ +.woocommerce-loop-product__button-add-to-cart--added {} +``` + +**Note:** `.woocommerce-loop-product` is not named as such because the block is nested within `.woocommerce-loop`. It's to be specific so that we can have separate classes for single products, cart products, etc. **Nested blocks do not need to inherit their parents full name.** + +## TL;DR + +- Follow the [WordPress Coding standards for CSS](https://make.wordpress.org/core/handbook/best-practices/coding-standards/css/) unless it contradicts anything here. +- Follow [Calypso guidelines for CSS](https://wpcalypso.wordpress.com/devdocs/docs/coding-guidelines/css.md). +- Use BEM for [class names](https://en.bem.info/methodology/naming-convention/). +- Prefix all class names. diff --git a/docs/quality-and-best-practices/naming-conventions.md b/docs/quality-and-best-practices/naming-conventions.md new file mode 100644 index 00000000000..9400c6a389a --- /dev/null +++ b/docs/quality-and-best-practices/naming-conventions.md @@ -0,0 +1,88 @@ +# Naming Conventions + +Table of Contents: + +- [PHP](#php) + - [`/src`](#src) + - [`/includes`](#includes) +- [JS](#js) +- [CSS and SASS](#css-and-sass) + +## PHP + +WooCommerce core generally follows [WordPress PHP naming conventions](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions). + +There are some additional conventions that apply, depending on the location of the code. + +### `/src` + +Classes defined inside `/src` follow the [PSR-4](https://www.php-fig.org/psr/psr-4/) standard. See the [README for `/src`](../../plugins/woocommerce/src/README.md) for more information. + +The following conventions apply to this directory: + +- No class name prefix is needed, as all classes in this location live within the `Automattic\WooCommerce` namespace. +- Classes are named using `CamelCase` convention. +- Functions are named using `snake_case` convention. +- Class file names should match the class name. They do not need a `class-` prefix. +- The namespace should match the directory structure. +- Hooks are prefixed with `woocommerce_`. +- Hooks are named using `snake_case` convention. + +For example, the class defined in `src/Util/StringUtil.php` should be named `StringUtil` and should be in the `Automattic\WooCommerce\Util` namespace. + +### `/includes` + +The `/includes` directory contains legacy code that does not follow the PSR-4 standard. See the [README for `/includes`](../../plugins/woocommerce/includes/README.md) for more information. + +The following conventions apply to this directory: + +- Class names are prefixed with `WC_`. +- Classes are named using `Upper_Snake_Case` convention. +- Functions are prefixed with `wc_`. +- Functions are named using `snake_case` convention. +- Hooks are prefixed with `woocommerce_`. +- Hooks are named using `snake_case` convention. + +Class name examples: + +- `WC_Cache_Helper` +- `WC_Cart` + +Function name examples: + +- `wc_get_product()` +- `wc_is_active_theme()` + +Hook name examples (actions or filters): + +- `woocommerce_after_checkout_validation` +- `woocommerce_get_formatted_order_total` + +## JS + +WooCommerce core follows [WordPress JS naming conventions](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/javascript/#naming-conventions). + +As with PHP, function, class, and hook names should be prefixed, but the convention for JS is slightly different. + +- Global class names are prefixed with `WC`. Class names exported from modules are not prefixed. +- Classes are named using `UpperCamelCase` convention. +- Global function names are prefixed with `wc`. Function names exported from modules are not prefixed. +- Functions are named using `camelCase` convention. +- Hooks names are prefixed with `woocommerce`. +- Hooks are named using `camelCase` convention. + +Global class name example: + +- `WCOrdersTable` + +Global function name example: + +- `wcSettings()` + +Hook name example (actions or filters): + +- `woocommerceTracksEventProperties` + +## CSS and SASS + +See [CSS/Sass Naming Conventions](./css-sass-naming-conventions.md). diff --git a/docs/quality-and-best-practices/writing-high-quality-testing-instructions.md b/docs/quality-and-best-practices/writing-high-quality-testing-instructions.md new file mode 100644 index 00000000000..06567f41eab --- /dev/null +++ b/docs/quality-and-best-practices/writing-high-quality-testing-instructions.md @@ -0,0 +1,138 @@ +# Writing high quality testing instructions + +## Introduction + +Having clear testing Instructions on pull requests is the first level of quality engineering in WooCommerce, which is key for testing early and minimizing the impact of unexpected effects in the upcoming versions of WooCommerce. + +This page contains the following sections: + +- [What is a test?](#what-is-a-test) +- [What to cover with the testing instructions](#what-to-cover-with-the-testing-instructions) +- [Flow to write good testing instructions](#flow-to-write-good-testing-instructions) +- [Examples](#examples) + +## What is a test? + +A test is a method that we can use to check that something meets certain criteria. It is typically defined as a procedure which contains the steps required to put the system under test in a certain state before executing the action to be checked. Therefore, a test consists of the following stages: + +- **Preconditions:** All the steps that need to be performed to put the system in the desired state before executing the action we want to check. A test could have many preconditions. +- **Action:** This is the exact step that causes the change we want to check in the system. It should be only one because each test should ideally cover one thing at a time. +- **Validation:** Relates to the steps to be performed in order to validate the result of performing the action in the system. A test could validate more than one thing. + +For example, in the process of adding an item to the cart: + +- The **preconditions** would be all the steps involved in: + - The product creation process. + - Logging as a shopper. + - Heading to the shop page where the products are listed. +- The **action** would be clicking the _"Add to cart"_ button in the desired product. +- The **validation** stage would include checking that the cart icon (if any) shows 1 more item and the product we selected is now included in the cart. + +Specifying the preconditions, actions and validations can be quite beneficial when understanding the scope of a test, because: + +- The **preconditions** describe what we have to do so that we can execute the test successfully. +- The **action** lets us know the purpose of the test, in other words, it is the key to understanding what we need to test. +- The **validation** stage lets us know what to expect when executing the test. + +In this context, we will refer to testing instructions as the tests we need to execute in order to validate that the changes delivered in a pull request or release work as expected. This means the testing instructions could refer to a test or more, involving the happy path and potential edge cases. + +## What to cover with the testing instructions + +As stated in the previous section, a test (in our context, a testing instruction) is a method to check that a new change or set of changes meets certain criteria. + +Therefore, a PR could have testing instructions for multiple scenarios, in fact, it is recommended to include testing instructions for as many scenarios as needed to cover the changes introduced in the PR. In other words, please **add as many testing instructions as needed to cover the acceptance criteria**, understanding acceptance criteria as _the conditions that a software product must satisfy to be accepted by a user, customer or other stakeholders_ or, in the context of a PR, the conditions that this PR must satisfy to be accepted by users, developers and the WooCommerce community as per requirements. + +## Flow to write good testing instructions + +1. **Outline the user flows** you want to cover. +2. **Define the environment** where the testing instructions should be executed (server, PHP version, WP version, required plugins, etc), and start writing the testing instructions as if you were starting from a fresh install. +3. Identify the **preconditions**, **action** and **validation** steps. +4. Write **as many preconditions as you need** to explain how to set up the state of WooCommerce so that you can execute the desired action to test every flow. + 1. Try to be detailed when explaining the interactions the user needs to perform in WooCommerce. + 2. If there are several preconditions for a user flow that is explained in a public guide, feel free to simply link the guide in the testing instructions instead of writing several steps. For example, _"Enable dev mode in WooCommerce Payments by following the steps mentioned [here](https://woocommerce.com/document/woocommerce-payments/testing-and-troubleshooting/dev-mode/)"_. +5. Write **the action step**, which should cover the specific action that we want to test as part of this user flow. +6. Write **as many validation steps** as needed in order to assess that the actual result meets expectations. + 1. Bear in mind to check only the steps needed to validate that this change works. + +### Considerations for writing high-quality testing instructions + +- Define the testing instructions in a way that they can be **understood and followed by everybody**, even for people new to WooCommerce. +- Make sure to describe every detail and **avoid assuming knowledge**, the spectrum of readers might be wide and some people would not know the concepts behind what is being assumed. For example, instead of saying _“Enable the [x] experiment”_, say something like: + +```text +- Install the WooCommerce Beta Tester plugin. +- Go to `Tools > WCA Test Helper > Experiments`. +- Toggle the [x] experiment. +``` + +- Always try to explain in detail **where the user should head to**, for example instead of saying “Go to the Orders page as admin”, say “Go to [url]” or even “Go to WooCommerce > Orders”. +- Try to use real test data. For example, instead of saying _"Enter a name for the product"_, say something like _"Enter 'Blue T-Shirt' as the product name"_. This will make it more self-explanatory and remove potential doubts related to assuming knowledge. +- Make sure you **keep your testing instructions updated** if they become obsolete as part of a new commit. +- If the testing instructions require to add custom code, please **provide the code snippet**. +- If the testing instructions require to install a plugin, please **provide a link to this plugin, or the zip file** to install it. +- If the testing instructions require to hit an API endpoint, please provide the **link to the endpoint documentation**. +- Ideally **provide screenshots and/or videos** that supported what the testing instructions are explaining. If you are using links to collaborative tools then also provide an equivalent screenshot/video for those who do not have access. + +## Examples + +### Good quality testing instructions + +#### Example 1 + +![Sample of good quality instructions](https://woocommerce.files.wordpress.com/2023/10/213682695-3dc51613-b836-4e7e-93ef-f75078ab48ac.png) + +#### Example 2 + +![Another sample of good quality instructions](https://woocommerce.files.wordpress.com/2023/10/213682778-b552ab07-a518-48a7-9358-16adc5762aca.png) + +### Improving real testing instructions + +In this section, you will see some real examples of testing instructions that have room for improvement (before) and how we can tweak them (after). + +Before: + +![Instructions needing improvement](https://woocommerce.files.wordpress.com/2023/10/213682262-25bec5c3-154c-45ec-aa3d-d3e07f52669e.png) + +After: + +![Improved instructions](https://woocommerce.files.wordpress.com/2023/10/213682303-1b12ab97-f27a-41cb-a8db-da8a78d18840.png) + +Improvements: + +![Changes made](https://woocommerce.files.wordpress.com/2023/10/213682323-0ecc998d-69ab-4201-8daa-820b948315e8.png) + +Before: + +![Instructions needing improvement](https://woocommerce.files.wordpress.com/2023/10/213682396-8c52d20e-1fca-4ac1-8345-f381c15a102a.png) + +After: + +![Improved instructions](https://woocommerce.files.wordpress.com/2023/10/213682480-c01e0e84-5969-4456-8f43-70cbb8509e8d.png) + +Improvements: + +![Changes made](https://woocommerce.files.wordpress.com/2023/10/213682597-8d06e638-35dd-4ff8-9236-63c6ec5d05b8.jpg) + +Before: + +![Screenshot 2023-02-02 at 16 07 29](https://woocommerce.files.wordpress.com/2023/10/216365611-b540a814-3b8f-40f3-ae64-81018b9f97fb.png) + +After: + +![Screenshot 2023-02-02 at 16 22 31](https://woocommerce.files.wordpress.com/2023/10/216366043-967e5daa-6a23-4ab8-adda-5f3082d1ebf7.png) + +Improvements: + +![Screenshot 2023-02-02 at 16 09 24](https://woocommerce.files.wordpress.com/2023/10/216366152-b331648d-bcef-443b-b126-de2621a20862.png) + +Before: + +![Screenshot 2023-02-02 at 17 25 07](https://woocommerce.files.wordpress.com/2023/10/216388785-8806ea74-62e6-42da-8887-c8e291e7dfe2-1.png) + +After: + +![Screenshot 2023-02-02 at 17 49 22](https://woocommerce.files.wordpress.com/2023/10/216388842-e5ab433e-d288-4306-862f-72f6f81ab2cd.png) + +Improvements: + +![Screenshot 2023-02-02 at 17 39 23](https://woocommerce.files.wordpress.com/2023/10/216388874-c5b21fc3-f693-4a7e-a58a-c5d1b6606682.png) diff --git a/docs/style-guide.md b/docs/style-guide.md index ad90ffe7007..9bff55bbc2d 100644 --- a/docs/style-guide.md +++ b/docs/style-guide.md @@ -52,6 +52,56 @@ How-to guides are focused and specific, providing instructions on how to accompl > > [Divio Framework on How-to-Guide Writing](https://documentation.divio.com/how-to-guides/) +## Custom Linting Rules + +At WooCommerce, we're dedicated to maintaining a consistent and high-quality standard for our technical documentation. Our documents primarily adhere to the linting rules provided by `markdownlint`. To assist our contributors, we've detailed our custom configurations and exceptions below. + +Note: While we've outlined specific rules above, all other default linting rules from `markdownlint` apply unless otherwise stated. We've only highlighted custom configurations or exceptions here. For a complete list of `markdownlint` rules, you can refer to [this link](https://github.com/DavidAnson/markdownlint/blob/3561fc3f38b05b3c55f44e371c2cd9bda194598a/doc/Rules.md). + +1. **Headings Style**: + - Use the ATX-style (`#`) for headers. + + ```markdown + # This is an H1 + ## This is an H2 + ``` + + [Reference: MD003](https://github.com/DavidAnson/markdownlint/blob/3561fc3f38b05b3c55f44e371c2cd9bda194598a/doc/Rules.md#md003---heading-style) + +2. **List Indentation**: + - Indent list items with 4 spaces. + + ```markdown + - Item 1 + - Subitem 1.1 + ``` + + [Reference: MD007]([https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md](https://github.com/DavidAnson/markdownlint/blob/3561fc3f38b05b3c55f44e371c2cd9bda194598a/doc/Rules.md)#md007---unordered-list-indentation) + +3. **Line Length**: + - No specific restriction on the line length, but keep paragraphs and sentences readable. + + [Reference: MD013](https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md013---line-length) + +4. **Multiple Headings with the Same Content**: + - Multiple headings with the same content are permissible as long as they are not siblings. + + [Reference: MD024](https://github.com/DavidAnson/markdownlint/blob/3561fc3f38b05b3c55f44e371c2cd9bda194598a/doc/Rules.md#md024---no-multiple-headings-with-the-same-content) + +5. **Inline HTML**: + - Only the `video` element is allowed when using inline HTML. + + ```markdown + + ``` + + [Reference: MD033](https://github.com/DavidAnson/markdownlint/blob/3561fc3f38b05b3c55f44e371c2cd9bda194598a/doc/Rules.md#md033---inline-html) + +6. **Tabs and Whitespace**: + - We're flexible with the use of hard tabs and trailing whitespace. However, for consistency, we recommend using spaces over tabs and avoiding trailing whitespaces. + + [Reference: no-hard-tabs & whitespace](https://github.com/DavidAnson/markdownlint/blob/3561fc3f38b05b3c55f44e371c2cd9bda194598a/doc/Rules.md) + ## Formatting ### Visual style diff --git a/packages/js/ai/changelog/update-separate-php-and-js-tests b/packages/js/ai/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/ai/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/ai/package.json b/packages/js/ai/package.json index b13cf1ecc13..fc18941a737 100644 --- a/packages/js/ai/package.json +++ b/packages/js/ai/package.json @@ -78,7 +78,8 @@ "changelog": "composer exec -- changelogger", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name --", "lint": "eslint --output-file eslint_report.json --format json src", "build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json", "build:css": "webpack", diff --git a/packages/js/api/changelog/update-separate-php-and-js-tests b/packages/js/api/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/api/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/api/package.json b/packages/js/api/package.json index d647ddf60c8..6dd6ec27b88 100644 --- a/packages/js/api/package.json +++ b/packages/js/api/package.json @@ -32,7 +32,8 @@ "scripts": { "turbo:build": "pnpm run clean && npm run compile", "turbo:test": "jest", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "prepare": "composer install", "changelog": "composer exec -- changelogger", "clean": "rm -rf ./dist ./tsconfig.tsbuildinfo", diff --git a/packages/js/block-templates/changelog/update-separate-php-and-js-tests b/packages/js/block-templates/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/block-templates/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/block-templates/package.json b/packages/js/block-templates/package.json index b4a32b6943b..db48c18cee1 100644 --- a/packages/js/block-templates/package.json +++ b/packages/js/block-templates/package.json @@ -65,7 +65,8 @@ "changelog": "composer exec -- changelogger", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "lint": "eslint --output-file eslint_report.json --format json src", "build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json", "build:css": "webpack", diff --git a/packages/js/components/changelog/add-35142 b/packages/js/components/changelog/add-35142 new file mode 100644 index 00000000000..a363cabf8fb --- /dev/null +++ b/packages/js/components/changelog/add-35142 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Set button optional in MediaUploader component diff --git a/packages/js/components/changelog/add-40592 b/packages/js/components/changelog/add-40592 new file mode 100644 index 00000000000..1bd4cbb8f34 --- /dev/null +++ b/packages/js/components/changelog/add-40592 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Image gallery and media uploader now support initial selected images. diff --git a/packages/js/components/changelog/add-40593_pricing_tab_for_variations b/packages/js/components/changelog/add-40593_pricing_tab_for_variations new file mode 100644 index 00000000000..58dca500033 --- /dev/null +++ b/packages/js/components/changelog/add-40593_pricing_tab_for_variations @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Small condition change in the date time picker to avoid edge case where inputControl is null. diff --git a/packages/js/components/changelog/fix-40171 b/packages/js/components/changelog/fix-40171 new file mode 100644 index 00000000000..5e2452b721d --- /dev/null +++ b/packages/js/components/changelog/fix-40171 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix invalid focus state of the experimental select control diff --git a/packages/js/components/changelog/fix-cys-ui-issues b/packages/js/components/changelog/fix-cys-ui-issues new file mode 100644 index 00000000000..1dbe1004f3f --- /dev/null +++ b/packages/js/components/changelog/fix-cys-ui-issues @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Add z-index=1 to tour-kit close btn to ensure it's clickable diff --git a/packages/js/components/changelog/fix-dropdown-css-conflict b/packages/js/components/changelog/fix-dropdown-css-conflict new file mode 100644 index 00000000000..328af18b070 --- /dev/null +++ b/packages/js/components/changelog/fix-dropdown-css-conflict @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add class back in for increase specificity of css for dropdown button. diff --git a/packages/js/components/changelog/update-separate-php-and-js-tests b/packages/js/components/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/components/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/components/package.json b/packages/js/components/package.json index c84f58dd95a..f9a6257df9d 100644 --- a/packages/js/components/package.json +++ b/packages/js/components/package.json @@ -153,7 +153,8 @@ "prepare": "composer install", "changelog": "composer exec -- changelogger", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "lint": "eslint --output-file eslint_report.json --format json --ext=js,ts,tsx src", "build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json", "build:css": "webpack", diff --git a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx index c414f3f09c7..82ed87d32b5 100644 --- a/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx +++ b/packages/js/components/src/date-time-picker-control/date-time-picker-control.tsx @@ -265,7 +265,11 @@ export const DateTimePickerControl = forwardRef( }, [ onBlur ] ); const callOnBlurIfDropdownIsNotOpening = useCallback( ( willOpen ) => { - if ( ! willOpen && typeof onBlurRef.current === 'function' ) { + if ( + ! willOpen && + typeof onBlurRef.current === 'function' && + inputControl.current + ) { // in case the component is blurred before a debounced // change has been processed, immediately set the input string // to the current value of the input field, so that diff --git a/packages/js/components/src/dropdown-button/style.scss b/packages/js/components/src/dropdown-button/style.scss index 3c38e8da238..d265f38c834 100644 --- a/packages/js/components/src/dropdown-button/style.scss +++ b/packages/js/components/src/dropdown-button/style.scss @@ -1,4 +1,4 @@ -.woocommerce-dropdown-button { +.components-button.woocommerce-dropdown-button { background-color: $studio-white; position: relative; border: 1px solid $gray-700; diff --git a/packages/js/components/src/experimental-select-control/menu.scss b/packages/js/components/src/experimental-select-control/menu.scss index 1fa08495c05..0b776a80709 100644 --- a/packages/js/components/src/experimental-select-control/menu.scss +++ b/packages/js/components/src/experimental-select-control/menu.scss @@ -17,13 +17,14 @@ } .woocommerce-experimental-select-control__popover-menu { .components-popover__content { - max-height: 300px; - overflow-y: scroll; + overflow: hidden; } } .woocommerce-experimental-select-control__popover-menu-container { margin: 0; width: 100%; + max-height: 300px; + overflow-y: scroll; > .category-field-dropdown__item:not(:first-child) { .category-field-dropdown__item-content { diff --git a/packages/js/components/src/experimental-select-control/select-control.tsx b/packages/js/components/src/experimental-select-control/select-control.tsx index 691550c06b4..12dd34d9902 100644 --- a/packages/js/components/src/experimental-select-control/select-control.tsx +++ b/packages/js/components/src/experimental-select-control/select-control.tsx @@ -15,6 +15,7 @@ import { useEffect, createElement, Fragment, + useRef, } from '@wordpress/element'; import { chevronDown } from '@wordpress/icons'; @@ -138,11 +139,13 @@ function SelectControl< ItemType = DefaultItemType >( { const instanceId = useInstanceId( SelectControl, 'woocommerce-experimental-select-control' - ); + ) as string; const innerInputClassName = 'woocommerce-experimental-select-control__input'; + const selectControlWrapperRef = useRef< HTMLDivElement >( null ); + let selectedItems = selected === null ? [] : selected; selectedItems = Array.isArray( selectedItems ) ? selectedItems @@ -186,6 +189,7 @@ function SelectControl< ItemType = DefaultItemType >( { openMenu, closeMenu, } = useCombobox< ItemType | null >( { + id: instanceId, initialSelectedItem: singleSelectedItem, inputValue, items: filteredItems, @@ -246,12 +250,15 @@ function SelectControl< ItemType = DefaultItemType >( { } ); const isEventOutside = ( event: React.FocusEvent ) => { - const inputClasses = event?.target?.className; + const selectControlWrapperElement = selectControlWrapperRef.current; + const menuElement = document.getElementById( `${ instanceId }-menu` ); + const parentPopoverMenuElement = menuElement?.closest( + '.woocommerce-experimental-select-control__popover-menu' + ); + return ( - ! document - .querySelector( '.' + instanceId ) - ?.contains( event.relatedTarget ) && - ! inputClasses.includes( innerInputClassName ) + ! selectControlWrapperElement?.contains( event.relatedTarget ) && + ! parentPopoverMenuElement?.contains( event.relatedTarget ) ); }; @@ -276,10 +283,11 @@ function SelectControl< ItemType = DefaultItemType >( { return (
= ( { id, alt, isCover = false, + isDraggable = true, src, className = '', onClick = () => null, @@ -33,7 +35,7 @@ export const ImageGalleryItem: React.FC< ImageGalleryItemProps > = ( { children, }: ImageGalleryItemProps ) => ( ( { wrappedChildren } ) } @@ -48,15 +50,15 @@ export const ImageGalleryItem: React.FC< ImageGalleryItemProps > = ( { > { children } - { isCover ? ( - <> - { __( 'Cover', 'woocommerce' ) } - { - - ) : ( + { isDraggable ? ( { + ) : ( + <> + { isCover && { __( 'Cover', 'woocommerce' ) } } + { + ) }
diff --git a/packages/js/components/src/image-gallery/image-gallery-toolbar.tsx b/packages/js/components/src/image-gallery/image-gallery-toolbar.tsx index a87155b2a66..176acc78b3a 100644 --- a/packages/js/components/src/image-gallery/image-gallery-toolbar.tsx +++ b/packages/js/components/src/image-gallery/image-gallery-toolbar.tsx @@ -25,6 +25,7 @@ import { ImageGalleryToolbarDropdown } from './image-gallery-toolbar-dropdown'; export type ImageGalleryToolbarProps = { childIndex: number; allowDragging?: boolean; + value?: number; moveItem: ( fromIndex: number, toIndex: number ) => void; removeItem: ( removeIndex: number ) => void; replaceItem: ( @@ -44,6 +45,7 @@ export const ImageGalleryToolbar: React.FC< ImageGalleryToolbarProps > = ( { replaceItem, setToolBarItem, lastChild, + value, MediaUploadComponent = MediaUpload, }: ImageGalleryToolbarProps ) => { const moveNext = () => { @@ -105,6 +107,7 @@ export const ImageGalleryToolbar: React.FC< ImageGalleryToolbarProps > = ( { { isCoverItem && ( replaceItem( childIndex, media as MediaItem ) } diff --git a/packages/js/components/src/image-gallery/image-gallery.tsx b/packages/js/components/src/image-gallery/image-gallery.tsx index f75f4c77847..01dcd589955 100644 --- a/packages/js/components/src/image-gallery/image-gallery.tsx +++ b/packages/js/components/src/image-gallery/image-gallery.tsx @@ -100,12 +100,11 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( { > { orderedChildren.map( ( child, childIndex ) => { const isToolbarVisible = child.key === activeToolbarKey; - const isCoverItem = ( childIndex === 0 ) as boolean; return cloneElement( child, { - isCover: isCoverItem, + isDraggable: allowDragging && ! child.props.isCover, className: classnames( { 'is-toolbar-visible': isToolbarVisible, } ), @@ -152,6 +151,7 @@ export const ImageGallery: React.FC< ImageGalleryProps > = ( { }, isToolbarVisible ? ( ) => JSX.Element; multipleSelect?: boolean | string; + value?: number | number[]; onSelect?: ( // eslint-disable-next-line @typescript-eslint/no-explicit-any value: ( { id: number } & { [ k: string ]: any } ) | MediaItem[] @@ -35,8 +36,8 @@ type MediaUploaderProps = { file: File; } ) => void; onMediaGalleryOpen?: () => void; - onUpload?: ( files: MediaItem[] ) => void; - onFileUploadChange?: ( files: MediaItem[] ) => void; + onUpload?: ( files: MediaItem | MediaItem[] ) => void; + onFileUploadChange?: ( files: MediaItem | MediaItem[] ) => void; uploadMedia?: ( options: UploadMediaOptions ) => Promise< void >; }; @@ -48,6 +49,7 @@ export const MediaUploader = ( { maxUploadFileSize = 10000000, MediaUploadComponent = MediaUpload, multipleSelect = false, + value, onError = () => null, onFileUploadChange = () => null, onMediaGalleryOpen = () => null, @@ -55,19 +57,21 @@ export const MediaUploader = ( { onSelect = () => null, uploadMedia = wpUploadMedia, }: MediaUploaderProps ) => { - const getFormFileUploadAcceptedFiles = () => - allowedMediaTypes.map( ( type ) => `${ type }/*` ); + const multiple = Boolean( multipleSelect ); return ( { uploadMedia( { + allowedTypes: allowedMediaTypes, filesList: currentTarget.files as FileList, - onError, - onFileChange: onFileUploadChange, maxUploadFileSize, + onError, + onFileChange( files ) { + onFileUploadChange( multiple ? files : files[ 0 ] ); + }, } ); } } render={ ( { openFileDialog } ) => ( @@ -94,31 +98,41 @@ export const MediaUploader = ( { ( - - ) } + render={ ( { open } ) => + buttonText ? ( + + ) : ( + + ) + } /> { hasDropZone && ( + onFilesDrop={ ( droppedFiles ) => uploadMedia( { - filesList: files, - onError, - onFileChange: onUpload, + allowedTypes: allowedMediaTypes, + filesList: droppedFiles, maxUploadFileSize, + onError, + onFileChange( files ) { + onUpload( + multiple ? files : files[ 0 ] + ); + }, } ) } /> diff --git a/packages/js/components/src/tour-kit/style.scss b/packages/js/components/src/tour-kit/style.scss index 135a774d82e..e2a7bc15da1 100644 --- a/packages/js/components/src/tour-kit/style.scss +++ b/packages/js/components/src/tour-kit/style.scss @@ -40,6 +40,8 @@ height: 16px; min-width: 16px; padding: 0; + z-index: 1; + svg { fill: #1e1e1e; } diff --git a/packages/js/csv-export/changelog/update-separate-php-and-js-tests b/packages/js/csv-export/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/csv-export/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/csv-export/package.json b/packages/js/csv-export/package.json index 2796700ad95..90a122f87f5 100644 --- a/packages/js/csv-export/package.json +++ b/packages/js/csv-export/package.json @@ -40,7 +40,8 @@ "changelog": "composer exec -- changelogger", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "lint": "eslint --output-file eslint_report.json --format json src", "start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\"", "prepack": "pnpm run clean && pnpm run build", diff --git a/packages/js/currency/changelog/update-separate-php-and-js-tests b/packages/js/currency/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/currency/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/currency/package.json b/packages/js/currency/package.json index e89fa8ebb22..8c7770e2bed 100644 --- a/packages/js/currency/package.json +++ b/packages/js/currency/package.json @@ -43,7 +43,8 @@ "changelog": "composer exec -- changelogger", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "lint": "eslint --output-file eslint_report.json --format json src", "start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\"", "prepack": "pnpm run clean && pnpm run build", diff --git a/packages/js/customer-effort-score/changelog/update-separate-php-and-js-tests b/packages/js/customer-effort-score/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/customer-effort-score/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/customer-effort-score/package.json b/packages/js/customer-effort-score/package.json index 88304dfd4df..dd8f46fcea6 100644 --- a/packages/js/customer-effort-score/package.json +++ b/packages/js/customer-effort-score/package.json @@ -79,7 +79,8 @@ "changelog": "composer exec -- changelogger", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "lint": "eslint --output-file eslint_report.json --format json src", "build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json", "build:css": "webpack", diff --git a/packages/js/data/changelog/add-40598 b/packages/js/data/changelog/add-40598 new file mode 100644 index 00000000000..912a3cd30cc --- /dev/null +++ b/packages/js/data/changelog/add-40598 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add name and parent_id to the ProductVariation type definition diff --git a/packages/js/data/changelog/update-separate-php-and-js-tests b/packages/js/data/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/data/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/data/package.json b/packages/js/data/package.json index b4235253e43..f091ecf69af 100644 --- a/packages/js/data/package.json +++ b/packages/js/data/package.json @@ -86,7 +86,8 @@ "changelog": "composer exec -- changelogger", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "lint": "eslint --output-file eslint_report.json --format json src", "start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\"", "prepack": "pnpm run clean && pnpm run build", diff --git a/packages/js/data/src/product-variations/types.ts b/packages/js/data/src/product-variations/types.ts index 563d48fbd19..4c3f7b569bb 100644 --- a/packages/js/data/src/product-variations/types.ts +++ b/packages/js/data/src/product-variations/types.ts @@ -55,7 +55,7 @@ export interface ProductVariationImage { export type ProductVariation = Omit< Product, - 'name' | 'slug' | 'attributes' | 'images' | 'manage_stock' + 'slug' | 'attributes' | 'images' | 'manage_stock' > & { attributes: ProductVariationAttribute[]; /** @@ -70,6 +70,10 @@ export type ProductVariation = Omit< * @default false */ manage_stock: boolean | 'parent'; + /** + * The product id this variation belongs to + */ + parent_id: number; }; type Query = Omit< ProductQuery, 'name' >; diff --git a/packages/js/date/changelog/update-separate-php-and-js-tests b/packages/js/date/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/date/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/date/package.json b/packages/js/date/package.json index f4702b0132f..c3a4474370c 100644 --- a/packages/js/date/package.json +++ b/packages/js/date/package.json @@ -63,7 +63,8 @@ "changelog": "composer exec -- changelogger", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "lint": "eslint --output-file eslint_report.json --format json src", "start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\"", "prepack": "pnpm run clean && pnpm run build", diff --git a/packages/js/experimental/changelog/update-separate-php-and-js-tests b/packages/js/experimental/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/experimental/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/experimental/package.json b/packages/js/experimental/package.json index 935891113bb..8cf6c8b762f 100644 --- a/packages/js/experimental/package.json +++ b/packages/js/experimental/package.json @@ -89,7 +89,8 @@ "changelog": "composer exec -- changelogger", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "lint": "eslint --output-file eslint_report.json --format json src", "build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json", "build:css": "webpack", diff --git a/packages/js/explat/changelog/update-separate-php-and-js-tests b/packages/js/explat/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/explat/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/explat/package.json b/packages/js/explat/package.json index 6bd11158294..54930fab577 100644 --- a/packages/js/explat/package.json +++ b/packages/js/explat/package.json @@ -61,7 +61,8 @@ "changelog": "composer exec -- changelogger", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "lint": "eslint --output-file eslint_report.json --format json src", "start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\"", "prepack": "pnpm run clean && pnpm run build", diff --git a/packages/js/expression-evaluation/.eslintrc.js b/packages/js/expression-evaluation/.eslintrc.js new file mode 100644 index 00000000000..e4d185d8cd1 --- /dev/null +++ b/packages/js/expression-evaluation/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + extends: [ 'plugin:@woocommerce/eslint-plugin/recommended' ], + root: true, +}; diff --git a/packages/js/expression-evaluation/.npmrc b/packages/js/expression-evaluation/.npmrc new file mode 100644 index 00000000000..43c97e719a5 --- /dev/null +++ b/packages/js/expression-evaluation/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/js/expression-evaluation/CHANGELOG.md b/packages/js/expression-evaluation/CHANGELOG.md new file mode 100644 index 00000000000..f7aac6be3b3 --- /dev/null +++ b/packages/js/expression-evaluation/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/packages/js/expression-evaluation/README.md b/packages/js/expression-evaluation/README.md new file mode 100644 index 00000000000..583c87b9b2b --- /dev/null +++ b/packages/js/expression-evaluation/README.md @@ -0,0 +1,314 @@ +# @woocommerce/expression-evaluation + +Evaluation of JavaScript-like expressions in an optional context. + +Examples of simple expressions: + +```js +1 + 2 +``` + +```js +foo === 'bar' +``` + +```js +foo ? 'bar' : 'baz' +``` + +Examples of complex expressions: + +```js +foo.bar.baz === 'qux' +``` + +```js +foo.bar + && ( foo.bar.baz === 'qux' || foo.baz === 'quux' ) +``` + +```js +foo.bar + && ( foo.baz === "qux" || foo.baz === "quux" ) + && ( foo.quux > 1 && foo.quux <= 5 ) +``` + +```js +foo.bar + && ( foo.baz === "qux" || foo.baz === "quux" ) + && ( foo.quux > 1 && foo.quux <= 5 ) + ? "boo" + : "baa" +``` + +```js +foo + + 5 + /* This is a comment */ + * ( bar ? baz : qux ) +``` + +## API + +### evaluate + +Evaluates an expression in an optional context. + +#### Usage + +```js +import { evaluate } from '@woocommerce/expression-evaluation'; + +const result = evaluate( '1 + foo', { foo: 2 } ); + +console.log( result ); // 3 +``` + +#### Parameters + +- _expression_ `string`: The expression to evaluate. +- _context_ `Object`: Optional. The context to evaluate the expression in. Variables in the expression will be looked up in this object. + +#### Returns + +- `any`: The result of the expression evaluation. + +## Expression syntax + +### Grammar and types + +The expression syntax is based on JavaScript. The formal grammar is defined in [parser.ts](./src/parser.ts). + +An expression consists of a single statement. + +Features like `if` statements, `for` loops, function calls, and variable assignments, are not supported. + +The following types are supported: + +- `null` +- Boolean: `true` and `false` +- Number: An integer or floating point number. +- String: A sequence of characters that represent text. + +### Literals + +Values in an expression can be written as literals. + +#### null + +```js +null +``` + +#### Boolean + +```js +true +false +``` + +#### Number + +```js +1 +5.23 +-9 +``` + +#### String + +String literals can be written with single or double quotes. This can be helpful if the string contains a single or double quote. + +```js +'foo' +"foo" +'foo "bar"' +"foo 'bar'" +``` + +Quotes can be escaped with a backslash. + +```js +'foo \'bar\'' +"foo \"bar\"" +``` + +### Context variables + +Variables can be used in an expression. The value of a variable is looked up in the context. + +```js +const result = evaluate( 'foo', { foo: 1 } ); + +console.log( result ); // 1 +``` + +Nested properties can be accessed with the dot operator. + +```js +const result = evaluate( 'foo.bar', { foo: { bar: 1 } } ); + +console.log( result ); // 1 +``` + +### Operators + +The following operators are supported. + +#### Comparison operators + +##### Equal (`==`) + +Returns `true` if the operands are equal. + +```js +1 == 1 +``` + +##### Not equal (`!=`) + +Returns `true` if the operands are not equal. + +```js +1 != 2 +``` + +##### Strict equal (`===`) + +Returns `true` if the operands are equal and of the same type. + +```js +1 === 1 +``` + +##### Strict not equal (`!==`) + +Returns `true` if the operands are not equal and/or not of the same type. + +```js +1 !== "1" +``` + +##### Greater than (`>`) + +Returns `true` if the left operand is greater than the right operand. + +```js +2 > 1 +``` + +##### Greater than or equal (`>=`) + +Returns `true` if the left operand is greater than or equal to the right operand. + +```js +2 >= 2 +``` + +##### Less than (`<`) + +Returns `true` if the left operand is less than the right operand. + +```js +1 < 2 +``` + +##### Less than or equal (`<=`) + +Returns `true` if the left operand is less than or equal to the right operand. + +```js +2 <= 2 +``` + +#### Arithmetic operators + +##### Addition (`+`) + +Returns the sum of two operands. + +```js +1 + 2 +``` + +##### Subtraction (`-`) + +Returns the difference of two operands. + +```js +2 - 1 +``` + +##### Multiplication (`*`) + +Returns the product of two operands. + +```js +2 * 3 +``` + +##### Division (`/`) + +Returns the quotient of two operands. + +```js +6 / 2 +``` + +##### Modulus (`%`) + +Returns the remainder of two operands. + +```js +5 % 2 +``` + +##### Negation (`-`) + +Returns the negation of an operand. + +```js +-1 +``` + +#### Logical operators + +##### Logical AND (`&&`) + +Returns `true` if both operands are `true`. + +```js +true && true +``` + +##### Logical OR (`||`) + +Returns `true` if either operand is `true`. + +```js +true || false +``` + +##### Logical NOT (`!`) + +Returns `true` if the operand is `false`. + +```js +!false +``` + +#### Conditional (ternary) operator + +Returns the first value if the condition is `true`, otherwise it returns the second value. + +```js +true ? 1 : 2 +``` + +### Comments + +Comments can be used to document an expression. Comments are treated as whitespace and are ignored by the parser. + +```js +/* This is a comment */ +``` diff --git a/packages/js/expression-evaluation/babel.config.js b/packages/js/expression-evaluation/babel.config.js new file mode 100644 index 00000000000..f73e04467aa --- /dev/null +++ b/packages/js/expression-evaluation/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: '../internal-js-tests/babel.config.js', +}; diff --git a/packages/js/expression-evaluation/changelog/add-expression-evaluation-package b/packages/js/expression-evaluation/changelog/add-expression-evaluation-package new file mode 100644 index 00000000000..3ad65acf293 --- /dev/null +++ b/packages/js/expression-evaluation/changelog/add-expression-evaluation-package @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Initial @woocommerce/expression-evaluation package. diff --git a/packages/js/expression-evaluation/composer.json b/packages/js/expression-evaluation/composer.json new file mode 100644 index 00000000000..b7e276fcbea --- /dev/null +++ b/packages/js/expression-evaluation/composer.json @@ -0,0 +1,32 @@ +{ + "name": "woocommerce/expression-evaluation", + "description": "WooCommerce expression evaluation library", + "type": "library", + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { + "automattic/jetpack-changelogger": "3.3.0" + }, + "config": { + "platform": { + "php": "7.2" + } + }, + "extra": { + "changelogger": { + "formatter": { + "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", + "add": "Adds functionality", + "update": "Update existing functionality", + "dev": "Development related task", + "tweak": "A minor adjustment to the codebase", + "performance": "Address performance issues", + "enhancement": "Improve existing functionality" + }, + "changelog": "CHANGELOG.md" + } + } +} diff --git a/packages/js/expression-evaluation/composer.lock b/packages/js/expression-evaluation/composer.lock new file mode 100644 index 00000000000..0448aa63f09 --- /dev/null +++ b/packages/js/expression-evaluation/composer.lock @@ -0,0 +1,483 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "76226c5737d8666789d8a162710469da", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", + "reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/8f63c829b8d1b0d7b1d5de93510d78523ed18959", + "reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "symfony/console": "^3.4 || ^5.2 || ^6.0", + "symfony/process": "^3.4 || ^5.2 || ^6.0", + "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "yoast/phpunit-polyfills": "1.0.4" + }, + "bin": [ + "bin/changelogger" + ], + "type": "project", + "extra": { + "autotagger": true, + "branch-alias": { + "dev-trunk": "3.3.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { + "::VERSION": "src/Application.php" + }, + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-changelogger/compare/${old}...${new}" + } + }, + "autoload": { + "psr-4": { + "Automattic\\Jetpack\\Changelog\\": "lib", + "Automattic\\Jetpack\\Changelogger\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { + "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.3.0" + }, + "time": "2022-12-26T13:49:01+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "symfony/console", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/a10b1da6fc93080c180bba7219b5ff5b7518fe81", + "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/console/tree/3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/debug", + "version": "4.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "1a692492190773c5310bc7877cb590c04c2f05be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/1a692492190773c5310bc7877cb590c04c2f05be", + "reference": "1a692492190773c5310bc7877cb590c04c2f05be", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "symfony/http-kernel": "<3.4" + }, + "require-dev": { + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/debug/tree/v4.4.44" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "abandoned": "symfony/error-handler", + "time": "2022-07-28T16:29:46+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-28T09:04:16+00:00" + }, + { + "name": "symfony/process", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T10:57:07+00:00" + }, + { + "name": "wikimedia/at-ease", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/wikimedia/at-ease.git", + "reference": "013ac61929797839c80a111a3f1a4710d8248e7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wikimedia/at-ease/zipball/013ac61929797839c80a111a3f1a4710d8248e7a", + "reference": "013ac61929797839c80a111a3f1a4710d8248e7a", + "shasum": "" + }, + "require": { + "php": ">=5.6.99" + }, + "require-dev": { + "jakub-onderka/php-console-highlighter": "0.3.2", + "jakub-onderka/php-parallel-lint": "1.0.0", + "mediawiki/mediawiki-codesniffer": "22.0.0", + "mediawiki/minus-x": "0.3.1", + "ockcyp/covers-validator": "0.5.1 || 0.6.1", + "phpunit/phpunit": "4.8.36 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/Wikimedia/Functions.php" + ], + "psr-4": { + "Wikimedia\\AtEase\\": "src/Wikimedia/AtEase/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Tim Starling", + "email": "tstarling@wikimedia.org" + }, + { + "name": "MediaWiki developers", + "email": "wikitech-l@lists.wikimedia.org" + } + ], + "description": "Safe replacement to @ for suppressing warnings.", + "homepage": "https://www.mediawiki.org/wiki/at-ease", + "support": { + "source": "https://github.com/wikimedia/at-ease/tree/master" + }, + "time": "2018-10-10T15:39:06+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "platform-overrides": { + "php": "7.2" + }, + "plugin-api-version": "2.6.0" +} diff --git a/packages/js/expression-evaluation/jest.config.json b/packages/js/expression-evaluation/jest.config.json new file mode 100644 index 00000000000..3d8108048f6 --- /dev/null +++ b/packages/js/expression-evaluation/jest.config.json @@ -0,0 +1,4 @@ +{ + "rootDir": "./src", + "preset": "../node_modules/@woocommerce/internal-js-tests/jest-preset.js" +} diff --git a/packages/js/expression-evaluation/package.json b/packages/js/expression-evaluation/package.json new file mode 100644 index 00000000000..949a3b3cba7 --- /dev/null +++ b/packages/js/expression-evaluation/package.json @@ -0,0 +1,69 @@ +{ + "name": "@woocommerce/expression-evaluation", + "version": "0.0.1", + "description": "Library for evaluating expressions.", + "author": "Automattic", + "license": "GPL-3.0-or-later", + "keywords": [ + "wordpress", + "woocommerce", + "expression", + "evalution" + ], + "engines": { + "node": "^16.14.1", + "pnpm": "^8.6.7" + }, + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/expression-evaluation/README.md", + "repository": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce.git" + }, + "bugs": { + "url": "https://github.com/woocommerce/woocommerce/issues" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "types": "build-types", + "react-native": "src/index", + "dependencies": { + "@wordpress/i18n": "wp-6.0", + "peggy": "^3.0.2" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "turbo:build": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json", + "turbo:test": "jest --config ./jest.config.json", + "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", + "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", + "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "lint": "eslint --output-file eslint_report.json --format json src", + "start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\"", + "prepack": "pnpm run clean && pnpm run build", + "lint:fix": "eslint src --fix", + "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "devDependencies": { + "@babel/core": "^7.17.5", + "@types/jest": "^27.4.1", + "@woocommerce/eslint-plugin": "workspace:*", + "@woocommerce/internal-js-tests": "workspace:*", + "concurrently": "^7.0.0", + "eslint": "^8.32.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^5.1.6" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "pnpm lint:fix", + "pnpm test-staged" + ] + } +} diff --git a/packages/js/expression-evaluation/src/index.ts b/packages/js/expression-evaluation/src/index.ts new file mode 100644 index 00000000000..0d499c72029 --- /dev/null +++ b/packages/js/expression-evaluation/src/index.ts @@ -0,0 +1,9 @@ +/** + * Internal dependencies + */ + +import { parser } from './parser'; + +export function evaluate( expression: string, context = {} ) { + return parser.parse( expression, { context } ); +} diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts new file mode 100644 index 00000000000..2fdb9470f0c --- /dev/null +++ b/packages/js/expression-evaluation/src/parser.ts @@ -0,0 +1,404 @@ +/** + * External dependencies + */ +import * as peggy from 'peggy'; + +const grammar = ` +{{ + function evaluateUnaryExpression( operator, operand ) { + switch ( operator ) { + case '!': + return !operand; + break; + case '-': + return -operand; + break; + case '+': + return +operand; + break; + default: + return undefined; + break; + } + } + + function evaluateBinaryExpression( head, tail ) { + return tail.reduce( ( leftOperand, tailElement ) => { + const operator = tailElement[ 1 ]; + const rightOperand = tailElement[ 3 ]; + + switch ( operator ) { + case '&&': + return leftOperand && rightOperand; + break; + case '||': + return leftOperand || rightOperand; + break; + case '===': + return leftOperand === rightOperand; + break; + case '!==': + return leftOperand !== rightOperand; + break; + case '==': + return leftOperand == rightOperand; + break; + case '!=': + return leftOperand != rightOperand; + break; + case '<=': + return leftOperand <= rightOperand; + break; + case '<': + return leftOperand < rightOperand; + break; + case '>=': + return leftOperand >= rightOperand; + break; + case '>': + return leftOperand > rightOperand; + break; + case '+': + return leftOperand + rightOperand; + break; + case '-': + return leftOperand - rightOperand; + break; + case '*': + return leftOperand * rightOperand; + break; + case '/': + return leftOperand / rightOperand; + break; + case '%': + return leftOperand % rightOperand; + break; + default: + return undefined; + break; + } + }, head ); + } +}} + +Start + = Expression + +SourceCharacter + = . + +WhiteSpace + = " " + / "\\t" + +LineTerminator + = "\\n" + / "\\r" + / "\\u2028" + / "\\u2029" + +LineTerminatorSequence + = "\\n" + / "\\r\\n" + / "\\r" + / "\\u2028" + / "\\u2029" + +Comment "comment" + = MultiLineComment + +MultiLineComment + = "/*" (!"*/" SourceCharacter)* "*/" + +__ "skipped" + = (WhiteSpace / LineTerminatorSequence / Comment)* + +IdentifierPath + = variable:Identifier accessor:(__ "." __ Identifier)* { + const path = variable.split( '.' ); + let result = path.reduce( ( nextObject, propertyName ) => nextObject[ propertyName ], options.context ); + + for ( let i = 0; i < accessor.length; i++ ) { + result = result[ accessor[ i ][ 3 ] ]; + } + + return result; + } + +Identifier + = !ReservedWord name:IdentifierName { + return name; + } + +IdentifierName + = first:IdentifierStart rest:IdentifierPart* { + return text(); + } + +IdentifierStart + = [a-zA-Z] + / "_" + / "$" + +IdentifierPart + = IdentifierStart + +ReservedWord + = NullLiteral + / BooleanLiteral + +// Literals + +Literal + = NullLiteral + / BooleanLiteral + / NumericLiteral + / StringLiteral + +NullLiteral + = NullToken { return null; } + +BooleanLiteral + = "true" { return true; } + / "false" { return false; } + +NumericLiteral + = literal:HexIntegerLiteral !(IdentifierStart / DecimalDigit) { + return literal; + } + / literal:DecimalLiteral !(IdentifierStart / DecimalDigit) { + return literal; + } + +HexIntegerLiteral + = "0x"i digits:$HexDigit+ { + return parseInt( digits, 16 ); + } + +HexDigit + = [0-9a-f]i + +DecimalLiteral + = DecimalIntegerLiteral "." DecimalDigit* ExponentPart? { + return parseFloat( text() ); + } + / "." DecimalDigit+ ExponentPart? { + return parseFloat( text() ); + } + / DecimalIntegerLiteral ExponentPart? { + return parseFloat( text() ); + } + +DecimalIntegerLiteral + = "0" + / NonZeroDigit DecimalDigit* + +DecimalDigit + = [0-9] + +NonZeroDigit + = [1-9] + +ExponentPart + = ExponentIndicator SignedInteger + +ExponentIndicator + = "e"i + +SignedInteger + = [+-]? DecimalDigit+ + +StringLiteral + = '"' chars:DoubleQuotedStringCharacter* '"' { + return chars.join( '' ); + } + / "'" chars:SingleQuotedStringCharacter* "'" { + return chars.join( '' ); + } + +DoubleQuotedStringCharacter + = !('"' / "\\\\" / LineTerminator) SourceCharacter { + return text(); + } + / "\\\\" escapeSequence:EscapeSequence { + return escapeSequence; + } + / LineContinuation + +SingleQuotedStringCharacter + = !("'" / "\\\\" / LineTerminator) SourceCharacter { + return text(); + } + / "\\\\" escapeSequence:EscapeSequence { + return escapeSequence; + } + / LineContinuation + +LineContinuation + = "\\\\" LineTerminatorSequence { + return ''; + } + +EscapeSequence + = CharacterEscapeSequence + / "0" !DecimalDigit { + return "\\0"; + } + / HexEscapeSequence + / UnicodeEscapeSequence + +CharacterEscapeSequence + = SingleEscapeCharacter + / NonEscapeCharacter + +SingleEscapeCharacter + = "'" + / '"' + / "\\\\" + / "b" { + return "\\b"; + } + / "f" { + return "\\f"; + } + / "n" { + return "\\n"; + } + / "r" { + return "\\r"; + } + / "t" { + return "\\t"; + } + / "v" { + return "\\v"; + } + +NonEscapeCharacter + = (!EscapeCharacter / LineTerminator) SourceCharacter { + return text(); + } + +EscapeCharacter + = SingleEscapeCharacter + / DecimalDigit + / "x" + / "u" + +HexEscapeSequence + = "x" digits:$(HexDigit HexDigit) { + return String.fromCharCode( parseInt( digits, 16 ) ); + } + +UnicodeEscapeSequence + = "u" digits:$(HexDigit HexDigit HexDigit HexDigit) { + return String.fromCharCode( parseInt( digits, 16 ) ); + } + +// Tokens + +NullToken + = "null" !IdentifierPart + +TrueToken + = "true" !IdentifierPart + +FalseToken + = "false" !IdentifierPart + +// Expressions + +PrimaryExpression + = IdentifierPath + / Literal + / "(" __ expression:Expression __ ")" { + return expression; + } + +UnaryExpression + = PrimaryExpression + / operator:UnaryOperator __ operand:UnaryExpression { + return evaluateUnaryExpression( operator, operand ); + } + +UnaryOperator + = "!" + / "-" + / "+" + +MultiplicativeExpression + = head:UnaryExpression tail:(__ MultiplicativeOperator __ UnaryExpression)* { + return evaluateBinaryExpression( head, tail ); + } + +MultiplicativeOperator + = "*" + / "/" + / "%" + +AdditiveExpression + = head:MultiplicativeExpression tail:(__ AdditiveOperator __ MultiplicativeExpression)* { + return evaluateBinaryExpression( head, tail ); + } + +AdditiveOperator + = "+" + / "-" + +RelationalExpression + = head:AdditiveExpression tail:(__ RelationalOperator __ AdditiveExpression)* { + return evaluateBinaryExpression( head, tail ); + } + +RelationalOperator + = "<=" + / "<" + / ">=" + / ">" + +EqualityExpression + = head:RelationalExpression tail:(__ EqualityOperator __ RelationalExpression)* { + return evaluateBinaryExpression( head, tail ); + } + +EqualityOperator + = "===" + / "!==" + / "==" + / "!=" + +LogicalAndExpression + = head:EqualityExpression tail:(__ LogicalAndOperator __ EqualityExpression)* { + return evaluateBinaryExpression( head, tail ); + } + +LogicalAndOperator + = "&&" + +LogicalOrExpression + = head:LogicalAndExpression tail:(__ LogicalOrOperator __ LogicalAndExpression)* { + return evaluateBinaryExpression( head, tail ); + } + +LogicalOrOperator + = "||" + +ConditionalExpression + = condition:LogicalOrExpression __ ConditionalTrueOperator __ expressionIfTrue:ConditionalExpression __ ConditionalFalseOperator __ expressionIfFalse:ConditionalExpression { + return condition ? expressionIfTrue : expressionIfFalse; + } + / LogicalOrExpression + +ConditionalTrueOperator + = "?" + +ConditionalFalseOperator + = ":" + +Expression + = __ expression:ConditionalExpression __ { + return expression; + } +`; + +export const parser = peggy.generate( grammar ); diff --git a/packages/js/expression-evaluation/src/test/index.test.ts b/packages/js/expression-evaluation/src/test/index.test.ts new file mode 100644 index 00000000000..7b5d3dc4284 --- /dev/null +++ b/packages/js/expression-evaluation/src/test/index.test.ts @@ -0,0 +1,482 @@ +/** + * Internal dependencies + */ + +import { evaluate } from '../'; + +describe( 'evaluate', () => { + it( 'should evaluate a null literal', () => { + const result = evaluate( 'null' ); + + expect( result ).toEqual( null ); + } ); + + it( 'should evaluate a boolean true literal', () => { + const result = evaluate( 'true' ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate a boolean false literal', () => { + const result = evaluate( 'false' ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a numeric integer literal', () => { + const result = evaluate( '23' ); + + expect( result ).toEqual( 23 ); + } ); + + it( 'should evaluate a signed negative integer literal', () => { + const result = evaluate( '-1' ); + + expect( result ).toEqual( -1 ); + } ); + + it( 'should evaluate a signed positive integer literal', () => { + const result = evaluate( '+1' ); + + expect( result ).toEqual( 1 ); + } ); + + it( 'should evaluate a numeric floating point literal', () => { + const result = evaluate( '5.23' ); + + expect( result ).toEqual( 5.23 ); + } ); + + it( 'should evaluate a signed negative floating point literal', () => { + const result = evaluate( '-9.95' ); + + expect( result ).toEqual( -9.95 ); + } ); + + it( 'should evaluate a signed positive floating point literal', () => { + const result = evaluate( '+9.95' ); + + expect( result ).toEqual( 9.95 ); + } ); + + it( 'should evaluate a numeric hexadecimal literal', () => { + const result = evaluate( '0x23' ); + + expect( result ).toEqual( 35 ); + } ); + + it( 'should evaluate a string literal with double quotes', () => { + const result = evaluate( '"foo"' ); + + expect( result ).toEqual( 'foo' ); + } ); + + it( 'should evaluate a string literal with double quotes and single quotes', () => { + const result = evaluate( '"foo \'bar\'"' ); + + expect( result ).toEqual( "foo 'bar'" ); + } ); + + it( 'should evaluate a string literal with double quotes and escaped double quotes', () => { + const result = evaluate( '"foo \\"bar\\""' ); + + expect( result ).toEqual( 'foo "bar"' ); + } ); + + it( 'should evaluate a string literal with double quotes and escaped backslashes', () => { + // eslint-disable-next-line prettier/prettier + const result = evaluate( '"foo \\\\\\"bar\\\\\\""' ); + + expect( result ).toEqual( 'foo \\"bar\\"' ); + } ); + + it( 'should evaluate a string literal with single quotes', () => { + const result = evaluate( "'foo'" ); + + expect( result ).toEqual( 'foo' ); + } ); + + it( 'should evaluate a string literal with single quotes and double quotes', () => { + // eslint-disable-next-line prettier/prettier + const result = evaluate( "'foo \"bar\"'" ); + + expect( result ).toEqual( 'foo "bar"' ); + } ); + + it( 'should evaluate a string literal with single quotes and escaped single quotes', () => { + const result = evaluate( "'foo \\'bar\\''" ); + + expect( result ).toEqual( "foo 'bar'" ); + } ); + + it( 'should evaluate a string literal with single quotes and escaped backslashes', () => { + // eslint-disable-next-line prettier/prettier + const result = evaluate( "'foo \\\\\\'bar\\\\\\''" ); + + expect( result ).toEqual( "foo \\'bar\\'" ); + } ); + + it( 'should evaluate a literal with whitespace around it', () => { + const result = evaluate( ' 23 ' ); + + expect( result ).toEqual( 23 ); + } ); + + it( 'should evaluate a top-level context property', () => { + const result = evaluate( 'foo', { + foo: 'bar', + } ); + + expect( result ).toEqual( 'bar' ); + } ); + + it( 'should evaluate a top-level context property with whitespace', () => { + const result = evaluate( ' foo ', { + foo: 'bar', + } ); + + expect( result ).toEqual( 'bar' ); + } ); + + it( 'should evaluate a nested context property', () => { + const result = evaluate( 'foo.bar', { + foo: { + bar: 'baz', + }, + } ); + + expect( result ).toEqual( 'baz' ); + } ); + + it( 'should evaluate a nested context property with whitespace', () => { + const result = evaluate( 'foo. bar', { + foo: { + bar: 'baz', + }, + } ); + + expect( result ).toEqual( 'baz' ); + } ); + + it( 'should evaluate a nested context property with multiple lines', () => { + const result = evaluate( + `foo. + bar`, + { + foo: { + bar: 'baz', + }, + } + ); + + expect( result ).toEqual( 'baz' ); + } ); + + it( 'should evaluate a NOT expression', () => { + const result = evaluate( '!foo', { + foo: true, + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a double NOT expression', () => { + const result = evaluate( '!!foo', { + foo: true, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate a NOT expression with parentheses', () => { + const result = evaluate( '!( foo )', { + foo: true, + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a NOT expression with parentheses and spaces', () => { + const result = evaluate( '! ( foo ) ', { + foo: true, + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a multiplication expression', () => { + const result = evaluate( 'foo * 2', { + foo: 2, + } ); + + expect( result ).toEqual( 4 ); + } ); + + it( 'should evaluate a division expression', () => { + const result = evaluate( 'foo / 2', { + foo: 4, + } ); + + expect( result ).toEqual( 2 ); + } ); + + it( 'should evaluate a modulo expression', () => { + const result = evaluate( 'foo % 2', { + foo: 5, + } ); + + expect( result ).toEqual( 1 ); + } ); + + it( 'should evaluate an addition expression', () => { + const result = evaluate( 'foo + 2', { + foo: 3, + } ); + + expect( result ).toEqual( 5 ); + } ); + + it( 'should evaluate a subtraction expression', () => { + const result = evaluate( 'foo - 2', { + foo: 5, + } ); + + expect( result ).toEqual( 3 ); + } ); + + it( 'should evaluate a complex arithmetic expression', () => { + const result = evaluate( 'foo * 2 + 1', { + foo: 3, + } ); + + expect( result ).toEqual( 7 ); + } ); + + it( 'should evaluate a complex arithmetic expression with parenthesis', () => { + const result = evaluate( 'foo * (2 + 1)', { + foo: 3, + } ); + + expect( result ).toEqual( 9 ); + } ); + + it( 'should evaluate a less than or equal expression', () => { + const result = evaluate( 'foo <= 1', { + foo: 1, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate a less than expression', () => { + const result = evaluate( 'foo < 1', { + foo: 1, + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a greater than or equal expression', () => { + const result = evaluate( 'foo >= 1', { + foo: 1, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate a greater than expression', () => { + const result = evaluate( 'foo > 1', { + foo: 1, + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate an strict equality expression', () => { + const result = evaluate( 'foo === "bar"', { + foo: 'bar', + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate an strict inequality expression', () => { + const result = evaluate( 'foo !== "bar"', { + foo: 'bar', + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate an equality expression', () => { + const result = evaluate( 'foo == "bar"', { + foo: 'bar', + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate an inequality expression', () => { + const result = evaluate( 'foo != "bar"', { + foo: 'bar', + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a conditional expression that is true', () => { + const result = evaluate( 'foo ? "bar" : "baz"', { + foo: true, + } ); + + expect( result ).toEqual( 'bar' ); + } ); + + it( 'should evaluate a conditional expression that is false', () => { + const result = evaluate( 'foo ? "bar" : "baz"', { + foo: false, + } ); + + expect( result ).toEqual( 'baz' ); + } ); + + it( 'should evaluate a logical OR expression', () => { + const result = evaluate( 'foo || bar', { + foo: true, + bar: false, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate a logical AND expression', () => { + const result = evaluate( 'foo && bar', { + foo: true, + bar: false, + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a multiline expression', () => { + const result = evaluate( + `foo + || bar + || baz`, + { + foo: false, + bar: false, + baz: true, + } + ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate a complex expression', () => { + const result = evaluate( + `foo.bar + && ( foo.baz === "qux" || foo.baz === "quux" )`, + { + foo: { + bar: true, + baz: 'quux', + }, + } + ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate a complex expression with arithmetic, relational, and logical operators', () => { + const result = evaluate( + `foo.bar + && ( foo.baz === "qux" || foo.baz === "quux" ) + && ( foo.quux > 1 && foo.quux <= 5 )`, + { + foo: { + bar: true, + baz: 'quux', + quux: 10, + }, + } + ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a complex expression with conditional, arithmetic, relational, and logical operators', () => { + const result = evaluate( + `foo.bar + && ( foo.baz === "qux" || foo.baz === "quux" ) + && ( foo.quux > 1 && foo.quux <= 5 ) + ? "boo" + : "baa"`, + { + foo: { + bar: true, + baz: 'quux', + quux: 10, + }, + } + ); + + expect( result ).toEqual( 'baa' ); + } ); + + it( 'should evaluate an expression with needless parentheses', () => { + const result = evaluate( '(((foo)))', { + foo: true, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate an expression with a multiline comment at the end', () => { + const result = evaluate( 'foo /* + 23 */', { + foo: 5, + } ); + + expect( result ).toEqual( 5 ); + } ); + + it( 'should evaluate an expression with a multiline comment at the beginning', () => { + const result = evaluate( '/* 23 + */ foo', { + foo: 5, + } ); + + expect( result ).toEqual( 5 ); + } ); + + it( 'should evaluate an expression with a multiline comment in the middle', () => { + const result = evaluate( 'foo + /* 23 */ bar', { + foo: 5, + bar: 3, + } ); + + expect( result ).toEqual( 8 ); + } ); + + it( 'should evaluate a multiline expression with a multiline comment', () => { + const result = evaluate( + `foo + /* + + bar + + boo + */ + + baz`, + { + foo: 5, + bar: 23, + boo: 6, + baz: 3, + } + ); + + expect( result ).toEqual( 8 ); + } ); + + it( 'should throw an error if the expression is invalid', () => { + expect( () => evaluate( '= 1' ) ).toThrow(); + } ); +} ); diff --git a/packages/js/expression-evaluation/tsconfig-cjs.json b/packages/js/expression-evaluation/tsconfig-cjs.json new file mode 100644 index 00000000000..61782c90442 --- /dev/null +++ b/packages/js/expression-evaluation/tsconfig-cjs.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig-cjs", + "compilerOptions": { + "declaration": true, + "outDir": "build", + "typeRoots": [ + "./typings", + "./node_modules/@types" + ] + } +} diff --git a/packages/js/expression-evaluation/tsconfig.json b/packages/js/expression-evaluation/tsconfig.json new file mode 100644 index 00000000000..c5f351a60cc --- /dev/null +++ b/packages/js/expression-evaluation/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig", + "compilerOptions": { + "rootDir": "src", + "outDir": "build-module", + "declaration": true, + "declarationMap": true, + "declarationDir": "./build-types", + "typeRoots": [ + "./typings", + "./node_modules/@types" + ] + } +} diff --git a/packages/js/integrate-plugin/.eslintrc.js b/packages/js/integrate-plugin/.eslintrc.js new file mode 100644 index 00000000000..e7aa0662678 --- /dev/null +++ b/packages/js/integrate-plugin/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + extends: [ 'plugin:@woocommerce/eslint-plugin/recommended' ], + root: true, + overrides: [ + { + files: [ '**/*.js', '**/*.jsx', '**/*.tsx' ], + rules: { + 'react/react-in-jsx-scope': 'off', + }, + }, + ], +}; diff --git a/packages/js/integrate-plugin/.npmrc b/packages/js/integrate-plugin/.npmrc new file mode 100644 index 00000000000..43c97e719a5 --- /dev/null +++ b/packages/js/integrate-plugin/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/js/integrate-plugin/README.md b/packages/js/integrate-plugin/README.md new file mode 100644 index 00000000000..ee6c804e231 --- /dev/null +++ b/packages/js/integrate-plugin/README.md @@ -0,0 +1,4 @@ +# @woocommerce/integrate-plugin + +Integrate plugin is a tool to help existing WordPress plugins get set up with JavaScript & React in order to extend and create Blocks. +The tool can also be used for scaffolding block examples. diff --git a/packages/js/integrate-plugin/babel.config.js b/packages/js/integrate-plugin/babel.config.js new file mode 100644 index 00000000000..f73e04467aa --- /dev/null +++ b/packages/js/integrate-plugin/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: '../internal-js-tests/babel.config.js', +}; diff --git a/packages/js/integrate-plugin/bin/index.js b/packages/js/integrate-plugin/bin/index.js new file mode 100644 index 00000000000..bc84d7a7b24 --- /dev/null +++ b/packages/js/integrate-plugin/bin/index.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require( '../build' ); diff --git a/packages/js/integrate-plugin/changelog.md b/packages/js/integrate-plugin/changelog.md new file mode 100644 index 00000000000..3783eb0da3d --- /dev/null +++ b/packages/js/integrate-plugin/changelog.md @@ -0,0 +1,3 @@ +# Changelog + +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/packages/js/integrate-plugin/changelog/add-40311_initial_migration_script b/packages/js/integrate-plugin/changelog/add-40311_initial_migration_script new file mode 100644 index 00000000000..ed15963a633 --- /dev/null +++ b/packages/js/integrate-plugin/changelog/add-40311_initial_migration_script @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add initial scripts for integrate plugin to get plugin details. diff --git a/packages/js/integrate-plugin/changelog/add-expose-block-id-and-order b/packages/js/integrate-plugin/changelog/add-expose-block-id-and-order new file mode 100644 index 00000000000..c7319d42c20 --- /dev/null +++ b/packages/js/integrate-plugin/changelog/add-expose-block-id-and-order @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Initial version of @woocommerce/block-templates package. Adds registerWooBlockType and useWooBlockProps. diff --git a/packages/js/integrate-plugin/changelog/update-separate-php-and-js-tests b/packages/js/integrate-plugin/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/integrate-plugin/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/integrate-plugin/composer.json b/packages/js/integrate-plugin/composer.json new file mode 100644 index 00000000000..4821be4a660 --- /dev/null +++ b/packages/js/integrate-plugin/composer.json @@ -0,0 +1,32 @@ +{ + "name": "woocommerce/block-templates", + "description": "WooCommerce Admin block templates component library", + "type": "library", + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { + "automattic/jetpack-changelogger": "3.3.0" + }, + "config": { + "platform": { + "php": "7.2" + } + }, + "extra": { + "changelogger": { + "formatter": { + "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", + "add": "Adds functionality", + "update": "Update existing functionality", + "dev": "Development related task", + "tweak": "A minor adjustment to the codebase", + "performance": "Address performance issues", + "enhancement": "Improve existing functionality" + }, + "changelog": "CHANGELOG.md" + } + } +} diff --git a/packages/js/integrate-plugin/composer.lock b/packages/js/integrate-plugin/composer.lock new file mode 100644 index 00000000000..96339a01d0a --- /dev/null +++ b/packages/js/integrate-plugin/composer.lock @@ -0,0 +1,483 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "2d316cec186ab12385e8dda54f6df0f6", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", + "reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/8f63c829b8d1b0d7b1d5de93510d78523ed18959", + "reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "symfony/console": "^3.4 || ^5.2 || ^6.0", + "symfony/process": "^3.4 || ^5.2 || ^6.0", + "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "yoast/phpunit-polyfills": "1.0.4" + }, + "bin": [ + "bin/changelogger" + ], + "type": "project", + "extra": { + "autotagger": true, + "branch-alias": { + "dev-trunk": "3.3.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { + "::VERSION": "src/Application.php" + }, + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-changelogger/compare/${old}...${new}" + } + }, + "autoload": { + "psr-4": { + "Automattic\\Jetpack\\Changelog\\": "lib", + "Automattic\\Jetpack\\Changelogger\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { + "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.3.0" + }, + "time": "2022-12-26T13:49:01+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "symfony/console", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/a10b1da6fc93080c180bba7219b5ff5b7518fe81", + "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/console/tree/3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/debug", + "version": "4.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "1a692492190773c5310bc7877cb590c04c2f05be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/1a692492190773c5310bc7877cb590c04c2f05be", + "reference": "1a692492190773c5310bc7877cb590c04c2f05be", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "symfony/http-kernel": "<3.4" + }, + "require-dev": { + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/debug/tree/v4.4.44" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "abandoned": "symfony/error-handler", + "time": "2022-07-28T16:29:46+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-28T09:04:16+00:00" + }, + { + "name": "symfony/process", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T10:57:07+00:00" + }, + { + "name": "wikimedia/at-ease", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/wikimedia/at-ease.git", + "reference": "013ac61929797839c80a111a3f1a4710d8248e7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wikimedia/at-ease/zipball/013ac61929797839c80a111a3f1a4710d8248e7a", + "reference": "013ac61929797839c80a111a3f1a4710d8248e7a", + "shasum": "" + }, + "require": { + "php": ">=5.6.99" + }, + "require-dev": { + "jakub-onderka/php-console-highlighter": "0.3.2", + "jakub-onderka/php-parallel-lint": "1.0.0", + "mediawiki/mediawiki-codesniffer": "22.0.0", + "mediawiki/minus-x": "0.3.1", + "ockcyp/covers-validator": "0.5.1 || 0.6.1", + "phpunit/phpunit": "4.8.36 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/Wikimedia/Functions.php" + ], + "psr-4": { + "Wikimedia\\AtEase\\": "src/Wikimedia/AtEase/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Tim Starling", + "email": "tstarling@wikimedia.org" + }, + { + "name": "MediaWiki developers", + "email": "wikitech-l@lists.wikimedia.org" + } + ], + "description": "Safe replacement to @ for suppressing warnings.", + "homepage": "https://www.mediawiki.org/wiki/at-ease", + "support": { + "source": "https://github.com/wikimedia/at-ease/tree/master" + }, + "time": "2018-10-10T15:39:06+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "platform-overrides": { + "php": "7.2" + }, + "plugin-api-version": "2.6.0" +} diff --git a/packages/js/integrate-plugin/jest.config.json b/packages/js/integrate-plugin/jest.config.json new file mode 100644 index 00000000000..3d8108048f6 --- /dev/null +++ b/packages/js/integrate-plugin/jest.config.json @@ -0,0 +1,4 @@ +{ + "rootDir": "./src", + "preset": "../node_modules/@woocommerce/internal-js-tests/jest-preset.js" +} diff --git a/packages/js/integrate-plugin/package.json b/packages/js/integrate-plugin/package.json new file mode 100644 index 00000000000..806a24cad10 --- /dev/null +++ b/packages/js/integrate-plugin/package.json @@ -0,0 +1,79 @@ +{ + "name": "@woocommerce/integrate-plugin", + "version": "0.1.0", + "description": "WooCommerce plugin integration scripts.", + "author": "Automattic", + "license": "GPL-3.0-or-later", + "keywords": [ + "wordpress", + "woocommerce", + "plugin" + ], + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/integrate-plugin/README.md", + "repository": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce.git" + }, + "bugs": { + "url": "https://github.com/woocommerce/woocommerce/issues" + }, + "main": "build/index.js", + "bin": { + "woo-integrate-plugin": "./build/index.js" + }, + "types": "build-types", + "react-native": "src/index", + "sideEffects": [], + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@wordpress/create-block": "wp-6.0", + "chalk": "^4.1.2", + "change-case": "^4.1.2", + "commander": "^9.2.0", + "execa": "^4.0.2", + "fast-glob": "^3.2.7", + "inquirer": "^7.1.0", + "npm-package-arg": "^8.1.5", + "rimraf": "^3.0.2", + "write-pkg": "^4.0.0" + }, + "devDependencies": { + "@babel/core": "^7.21.3", + "@babel/runtime": "^7.17.2", + "@testing-library/jest-dom": "^5.16.2", + "@testing-library/react-hooks": "^8.0.1", + "@types/jest": "^27.4.1", + "@types/node": "^16.18.18", + "@types/testing-library__jest-dom": "^5.14.3", + "@woocommerce/eslint-plugin": "workspace:*", + "@woocommerce/internal-js-tests": "workspace:*", + "@wordpress/browserslist-config": "wp-6.0", + "copy-webpack-plugin": "^9.1.0", + "css-loader": "^3.6.0", + "eslint": "^8.32.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^5.1.6", + "webpack": "^5.70.0", + "webpack-cli": "^3.3.12" + }, + "scripts": { + "turbo:build": "pnpm run build:js", + "turbo:test": "jest --config ./jest.config.json", + "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", + "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "lint": "eslint --output-file eslint_report.json --format json src", + "build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json", + "start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\" \"webpack --watch\"", + "prepack": "pnpm run clean && pnpm run build", + "lint:fix": "eslint src --fix" + } +} diff --git a/packages/js/integrate-plugin/src/get-plugin-config.ts b/packages/js/integrate-plugin/src/get-plugin-config.ts new file mode 100644 index 00000000000..e20f9fe6545 --- /dev/null +++ b/packages/js/integrate-plugin/src/get-plugin-config.ts @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import { existsSync, readFileSync, promises } from 'fs'; +import { join } from 'path'; + +/** + * Internal dependencies + */ +import { info } from './log'; + +const { writeFile } = promises; +function getUniqueItems( arr: string[] ) { + const uniqueObject = arr.reduce( ( unique, item ) => { + unique[ item ] = true; + return unique; + }, {} as Record< string, boolean > ); + + return Object.keys( uniqueObject ); +} + +const getPluginConfig = () => { + const cwd = join( process.cwd() ); + + if ( ! existsSync( join( cwd, '.woo-plugin.json' ) ) ) { + return {}; + } + + return JSON.parse( + readFileSync( join( cwd, '.woo-plugin.json' ), 'utf8' ) + ); +}; + +const updateConfig = async ( { modules }: { modules: string[] } ) => { + const cwd = join( process.cwd() ); + const config = getPluginConfig(); + + const uniqueModules = modules.reduce( ( unique, module ) => { + unique[ module ] = true; + return unique; + }, {} as Record< string, boolean > ); + + config.modules = Object.keys( uniqueModules ); + + info( '' ); + info( 'Updating plugin config file.' ); + + await writeFile( + join( cwd, '.woo-plugin.json' ), + JSON.stringify( config, null, 4 ) + ); +}; + +export { getPluginConfig, getUniqueItems, updateConfig }; diff --git a/packages/js/integrate-plugin/src/get-plugin-data.ts b/packages/js/integrate-plugin/src/get-plugin-data.ts new file mode 100644 index 00000000000..0f4310f69da --- /dev/null +++ b/packages/js/integrate-plugin/src/get-plugin-data.ts @@ -0,0 +1,40 @@ +/** + * External dependencies + */ +import { readdirSync, readFileSync } from 'fs'; +import CLIError from '@wordpress/create-block/lib/cli-error'; +import path from 'path'; + +/** + * Get the plugin data. + * + * @return {Object} - Plugin data as key value pairs. + */ +export function getPluginData() { + const files = readdirSync( process.cwd() ); + for ( let i = 0; i < files.length; i++ ) { + const file = path.join( process.cwd(), files[ i ] ); + if ( path.extname( file ) !== '.php' ) { + continue; + } + const content = readFileSync( file, 'utf8' ); + const name = content.match( /^\s+\*\s*Plugin Name:\s*(.*)/m ); + if ( name && name.length > 1 ) { + const description = content.match( + /^\s+\*\s+Description:\s*(.*)/m + ); + const textdomain = content.match( /^\s+\*\s*Text Domain:\s*(.*)/m ); + const version = content.match( /^\s+\*\s*Version:\s*(.*)/m ); + + return { + description: description && description[ 1 ].trim(), + name: name[ 1 ].trim(), + textdomain: textdomain && textdomain[ 1 ].trim(), + version: version && version[ 1 ].trim(), + namespace: textdomain && textdomain[ 1 ].trim(), + }; + } + } + + throw new CLIError( 'Plugin file not found.' ); +} diff --git a/packages/js/integrate-plugin/src/index.ts b/packages/js/integrate-plugin/src/index.ts new file mode 100644 index 00000000000..21ea94119d9 --- /dev/null +++ b/packages/js/integrate-plugin/src/index.ts @@ -0,0 +1,76 @@ +#!/usr/bin/env node + +/** + * External dependencies + */ +import { Command } from 'commander'; +import CLIError from '@wordpress/create-block/lib/cli-error'; + +/** + * Internal dependencies + */ +import * as log from './log'; +import { getPluginData } from './get-plugin-data'; +import { getPluginConfig } from './get-plugin-config'; + +//add the following line +const program = new Command(); + +const commandName = `woo-integrate-plugin`; +program + .name( commandName ) + .description( + 'Integrates a plugin with WooCommerce build scripts and dependencies.\n\n' + + 'The provided build scripts provide an easy way to build in a modern ' + + 'JS environment and automatically assist in building block assets. ' + ) + .version( '0.1.0' ) + .option( + '--wp-scripts', + 'enable integration with `@wordpress/scripts` package' + ) + .option( + '--no-wp-scripts', + 'disable integration with `@wordpress/scripts` package' + ) + .option( + '-t, --template ', + 'project template type name; allowed values: "standard", "es5", the name of an external npm package, or the path to a local directory', + 'standard' + ) + .option( '--variant ', 'the variant of the template to use' ) + .option( '--wp-env', 'enable integration with `@wordpress/env` package' ) + .option( + '--includes-dir ', + 'the path to the includes directory with backend logic' + ) + .option( + '--src-dir ', + 'the path to the src directory with client-side logic' + ) + .option( '--namespace ', 'internal namespace for the plugin' ) + .action( async () => { + try { + const pluginData = getPluginData(); + const pluginConfig = getPluginConfig(); + log.info( JSON.stringify( pluginData ) ); + log.info( JSON.stringify( pluginConfig ) ); + } catch ( error ) { + if ( error instanceof CLIError ) { + log.error( error.message ); + process.exit( 1 ); + } else { + throw error; + } + } + } ) + .on( '--help', () => { + log.info( '' ); + log.info( 'Examples:' ); + log.info( ` $ ${ commandName }` ); + log.info( ` $ ${ commandName } todo-list` ); + log.info( + ` $ ${ commandName } todo-list --template es5 --title "TODO List"` + ); + } ) + .parse( process.argv ); diff --git a/packages/js/integrate-plugin/src/log.ts b/packages/js/integrate-plugin/src/log.ts new file mode 100644 index 00000000000..9835f5da489 --- /dev/null +++ b/packages/js/integrate-plugin/src/log.ts @@ -0,0 +1,23 @@ +/* eslint-disable no-console */ +/** + * External dependencies + */ +import chalk from 'chalk'; + +const code = ( input: string ) => { + console.log( chalk.cyan( input ) ); +}; + +const error = ( input: string ) => { + console.log( chalk.bold.red( input ) ); +}; + +const info = ( input: string ) => { + console.log( input ); +}; +const success = ( input: string ) => { + console.log( chalk.bold.green( input ) ); +}; + +export { code, error, info, success }; +/* eslint-enable no-console */ diff --git a/packages/js/integrate-plugin/tsconfig-cjs.json b/packages/js/integrate-plugin/tsconfig-cjs.json new file mode 100644 index 00000000000..92bfe004f21 --- /dev/null +++ b/packages/js/integrate-plugin/tsconfig-cjs.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig-cjs", + "include": [ + "**/*.d.ts", + "src/**/*", + "src/**/*.json" + ], + "compilerOptions": { + "outDir": "build", + "resolveJsonModule": true, + "typeRoots": [ + "./typings", + "./node_modules/@types" + ] + } +} diff --git a/packages/js/integrate-plugin/tsconfig.json b/packages/js/integrate-plugin/tsconfig.json new file mode 100644 index 00000000000..1cd9fd20005 --- /dev/null +++ b/packages/js/integrate-plugin/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig", + "compilerOptions": { + "rootDir": "src", + "outDir": "build-module", + "declaration": true, + "declarationMap": true, + "declarationDir": "./build-types", + "resolveJsonModule": true, + "typeRoots": [ "./typings", "./node_modules/@types" ] + }, + "include": [ "**/*.d.ts", "src/**/*", "src/**/*.json" ] +} diff --git a/packages/js/integrate-plugin/typings/index.d.ts b/packages/js/integrate-plugin/typings/index.d.ts new file mode 100644 index 00000000000..117e9edd673 --- /dev/null +++ b/packages/js/integrate-plugin/typings/index.d.ts @@ -0,0 +1,3 @@ +declare module '@wordpress/create-block/lib/cli-error' { + export default Error; +} diff --git a/packages/js/navigation/changelog/update-separate-php-and-js-tests b/packages/js/navigation/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/navigation/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/navigation/package.json b/packages/js/navigation/package.json index 4f5f84da9ff..1db264e3e20 100644 --- a/packages/js/navigation/package.json +++ b/packages/js/navigation/package.json @@ -51,7 +51,8 @@ "changelog": "composer exec -- changelogger", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "lint": "eslint --output-file eslint_report.json --format json src", "start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\"", "prepack": "pnpm run clean && pnpm run build", diff --git a/packages/js/number/changelog/update-separate-php-and-js-tests b/packages/js/number/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/number/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/number/package.json b/packages/js/number/package.json index ea3ddffece2..2527f2dbcb5 100644 --- a/packages/js/number/package.json +++ b/packages/js/number/package.json @@ -37,7 +37,8 @@ "changelog": "composer exec -- changelogger", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "lint": "eslint --output-file eslint_report.json --format json src", "start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\"", "prepack": "pnpm run clean && pnpm run build", diff --git a/packages/js/onboarding/changelog/update-separate-php-and-js-tests b/packages/js/onboarding/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/onboarding/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/onboarding/package.json b/packages/js/onboarding/package.json index 3efbf46491c..89ad9b8ba36 100644 --- a/packages/js/onboarding/package.json +++ b/packages/js/onboarding/package.json @@ -79,7 +79,8 @@ "build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json", "build:css": "webpack", "start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\" \"webpack --watch\"", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "prepack": "pnpm run clean && pnpm run build", "lint:fix": "eslint src --fix", "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" diff --git a/packages/js/product-editor/changelog/add-35142 b/packages/js/product-editor/changelog/add-35142 new file mode 100644 index 00000000000..9fceac00f39 --- /dev/null +++ b/packages/js/product-editor/changelog/add-35142 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add download file list product block diff --git a/packages/js/product-editor/changelog/add-35145 b/packages/js/product-editor/changelog/add-35145 new file mode 100644 index 00000000000..d463be068a9 --- /dev/null +++ b/packages/js/product-editor/changelog/add-35145 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Set shipping disabled when the product is virtual diff --git a/packages/js/product-editor/changelog/add-39688 b/packages/js/product-editor/changelog/add-39688 new file mode 100644 index 00000000000..67da99dbb8c --- /dev/null +++ b/packages/js/product-editor/changelog/add-39688 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Show a summary when adding or removing variation option values diff --git a/packages/js/product-editor/changelog/add-40250 b/packages/js/product-editor/changelog/add-40250 new file mode 100644 index 00000000000..cd12031f88c --- /dev/null +++ b/packages/js/product-editor/changelog/add-40250 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add virtual and downloads related controls to variation management quick actions diff --git a/packages/js/product-editor/changelog/add-40493 b/packages/js/product-editor/changelog/add-40493 new file mode 100644 index 00000000000..2347ef7990f --- /dev/null +++ b/packages/js/product-editor/changelog/add-40493 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add new file dropdown menu diff --git a/packages/js/product-editor/changelog/add-40573_product_variation_edit_page b/packages/js/product-editor/changelog/add-40573_product_variation_edit_page new file mode 100644 index 00000000000..5c2a12abfeb --- /dev/null +++ b/packages/js/product-editor/changelog/add-40573_product_variation_edit_page @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update Editor and BlockEditor components to add support for multiple product/post types. diff --git a/packages/js/product-editor/changelog/add-40591_variation_switching b/packages/js/product-editor/changelog/add-40591_variation_switching new file mode 100644 index 00000000000..3cadbf8e0b9 --- /dev/null +++ b/packages/js/product-editor/changelog/add-40591_variation_switching @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add new VariationSwitcherFooter component for switching variations on edit variation page. diff --git a/packages/js/product-editor/changelog/add-40592 b/packages/js/product-editor/changelog/add-40592 new file mode 100644 index 00000000000..9869aded28d --- /dev/null +++ b/packages/js/product-editor/changelog/add-40592 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Images block now supports one or multiple images. Checkbox block now supports another property type. diff --git a/packages/js/product-editor/changelog/add-40593_pricing_tab_for_variations b/packages/js/product-editor/changelog/add-40593_pricing_tab_for_variations new file mode 100644 index 00000000000..ed24753f879 --- /dev/null +++ b/packages/js/product-editor/changelog/add-40593_pricing_tab_for_variations @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update pricing, and schedule field blocks to use postType context value. diff --git a/packages/js/product-editor/changelog/add-40594 b/packages/js/product-editor/changelog/add-40594 new file mode 100644 index 00000000000..3cf70f74ad2 --- /dev/null +++ b/packages/js/product-editor/changelog/add-40594 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add suport for context post type to blocks related to the ProductVariationTemplate diff --git a/packages/js/product-editor/changelog/add-40595 b/packages/js/product-editor/changelog/add-40595 new file mode 100644 index 00000000000..a6a0756e955 --- /dev/null +++ b/packages/js/product-editor/changelog/add-40595 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add support for context post type to blocks related to the ProductVariationTemplate diff --git a/packages/js/product-editor/changelog/add-40598 b/packages/js/product-editor/changelog/add-40598 new file mode 100644 index 00000000000..5bda7fdb2d4 --- /dev/null +++ b/packages/js/product-editor/changelog/add-40598 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Export RemoveConfirmationModal to be used outside of the product editor package diff --git a/packages/js/product-editor/changelog/add-40708 b/packages/js/product-editor/changelog/add-40708 new file mode 100644 index 00000000000..227e2786e48 --- /dev/null +++ b/packages/js/product-editor/changelog/add-40708 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add edit button to each variation to redirect to the single variation page diff --git a/packages/js/product-editor/changelog/add-edited-product-in-context b/packages/js/product-editor/changelog/add-edited-product-in-context new file mode 100644 index 00000000000..81040c48b28 --- /dev/null +++ b/packages/js/product-editor/changelog/add-edited-product-in-context @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Store edited product in product editor context. diff --git a/packages/js/product-editor/changelog/add-input-field b/packages/js/product-editor/changelog/add-input-field new file mode 100644 index 00000000000..2fa35121d8f --- /dev/null +++ b/packages/js/product-editor/changelog/add-input-field @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add woocommerce/product-text-field block diff --git a/packages/js/product-editor/changelog/add-number-block b/packages/js/product-editor/changelog/add-number-block new file mode 100644 index 00000000000..81aa1cced10 --- /dev/null +++ b/packages/js/product-editor/changelog/add-number-block @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add woocommerce/product-number-field block diff --git a/packages/js/product-editor/changelog/add-use-prod-entity-prop b/packages/js/product-editor/changelog/add-use-prod-entity-prop new file mode 100644 index 00000000000..1438f6005f2 --- /dev/null +++ b/packages/js/product-editor/changelog/add-use-prod-entity-prop @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add support for persisting automatically in post_meta for generic blocks diff --git a/packages/js/product-editor/changelog/dev-40203_edit_attribute_modal_styles b/packages/js/product-editor/changelog/dev-40203_edit_attribute_modal_styles new file mode 100644 index 00000000000..40ed406ffd1 --- /dev/null +++ b/packages/js/product-editor/changelog/dev-40203_edit_attribute_modal_styles @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Fix field label styling in the edit attribute modal #40449 diff --git a/packages/js/product-editor/changelog/dev-40216_remove_confirmation_modal_for_dismissing_attributes_variations b/packages/js/product-editor/changelog/dev-40216_remove_confirmation_modal_for_dismissing_attributes_variations new file mode 100644 index 00000000000..f3d54d4f7c4 --- /dev/null +++ b/packages/js/product-editor/changelog/dev-40216_remove_confirmation_modal_for_dismissing_attributes_variations @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Remove confirmation modal for dismissing unsaved attributes or variation options #40500 diff --git a/packages/js/product-editor/changelog/dev-40245_improve_copy_for_variation_management_notifications b/packages/js/product-editor/changelog/dev-40245_improve_copy_for_variation_management_notifications new file mode 100644 index 00000000000..8b2580e948c --- /dev/null +++ b/packages/js/product-editor/changelog/dev-40245_improve_copy_for_variation_management_notifications @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Improve copy in variation management notifications #40467 diff --git a/packages/js/product-editor/changelog/dev-40590_change_header_to_support_variation b/packages/js/product-editor/changelog/dev-40590_change_header_to_support_variation new file mode 100644 index 00000000000..cc38d9fb810 --- /dev/null +++ b/packages/js/product-editor/changelog/dev-40590_change_header_to_support_variation @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Change the blocks editor header to support variations #40606 diff --git a/packages/js/product-editor/changelog/doc-handbook-1.1 b/packages/js/product-editor/changelog/doc-handbook-1.1 new file mode 100644 index 00000000000..885d3748f6d --- /dev/null +++ b/packages/js/product-editor/changelog/doc-handbook-1.1 @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Add blocks documentation + + diff --git a/packages/js/product-editor/changelog/doc-more-reusable-blocks b/packages/js/product-editor/changelog/doc-more-reusable-blocks new file mode 100644 index 00000000000..5ac79cfaf04 --- /dev/null +++ b/packages/js/product-editor/changelog/doc-more-reusable-blocks @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Add reusable blocks documentation + + diff --git a/packages/js/product-editor/changelog/fix-40171 b/packages/js/product-editor/changelog/fix-40171 new file mode 100644 index 00000000000..4e79051fa67 --- /dev/null +++ b/packages/js/product-editor/changelog/fix-40171 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix regression setting readOnlyWhenClosed to false by default on attribute term input field diff --git a/packages/js/product-editor/changelog/fix-40199 b/packages/js/product-editor/changelog/fix-40199 new file mode 100644 index 00000000000..5d0dcec8984 --- /dev/null +++ b/packages/js/product-editor/changelog/fix-40199 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix the position and sizing of the pagination controls in variations table diff --git a/packages/js/product-editor/changelog/fix-40202 b/packages/js/product-editor/changelog/fix-40202 new file mode 100644 index 00000000000..51036430cf4 --- /dev/null +++ b/packages/js/product-editor/changelog/fix-40202 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix font sizes difference between chips used in the variations options table and variations list diff --git a/packages/js/product-editor/changelog/fix-40210_dropdown_and_list_behavior_in_attributes_variation_modals b/packages/js/product-editor/changelog/fix-40210_dropdown_and_list_behavior_in_attributes_variation_modals new file mode 100644 index 00000000000..003be99adfc --- /dev/null +++ b/packages/js/product-editor/changelog/fix-40210_dropdown_and_list_behavior_in_attributes_variation_modals @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix dropdown and list behavior in attributes variation modals #40496 diff --git a/packages/js/product-editor/changelog/fix-40502_record_events_for_variation_options_modal b/packages/js/product-editor/changelog/fix-40502_record_events_for_variation_options_modal new file mode 100644 index 00000000000..ca1b6b8bc3e --- /dev/null +++ b/packages/js/product-editor/changelog/fix-40502_record_events_for_variation_options_modal @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Add missing Tracks events to attribute modals #40517 diff --git a/packages/js/product-editor/changelog/tweak-refactor-blocks-org b/packages/js/product-editor/changelog/tweak-refactor-blocks-org new file mode 100644 index 00000000000..39876fe7b38 --- /dev/null +++ b/packages/js/product-editor/changelog/tweak-refactor-blocks-org @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Split product editor blocks into 'generic' and 'product-fields' directories diff --git a/packages/js/product-editor/changelog/update-40591_validation b/packages/js/product-editor/changelog/update-40591_validation new file mode 100644 index 00000000000..edde3d20f16 --- /dev/null +++ b/packages/js/product-editor/changelog/update-40591_validation @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add unregister function to the validation provider and trigger this from the useValidation hook. diff --git a/packages/js/product-editor/changelog/update-40591_variation_switching_when_deleting b/packages/js/product-editor/changelog/update-40591_variation_switching_when_deleting new file mode 100644 index 00000000000..41c6961fd08 --- /dev/null +++ b/packages/js/product-editor/changelog/update-40591_variation_switching_when_deleting @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add useVariationSwitcher hook and make use of it in the VariationSwitcherFooter. diff --git a/packages/js/product-editor/changelog/update-separate-php-and-js-tests b/packages/js/product-editor/changelog/update-separate-php-and-js-tests new file mode 100644 index 00000000000..12fd177d1fa --- /dev/null +++ b/packages/js/product-editor/changelog/update-separate-php-and-js-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This is just a change to developer commands. + diff --git a/packages/js/product-editor/config/block-entry-points.js b/packages/js/product-editor/config/block-entry-points.js index c23f6b3b3d1..dd8ba8e60dd 100644 --- a/packages/js/product-editor/config/block-entry-points.js +++ b/packages/js/product-editor/config/block-entry-points.js @@ -64,7 +64,9 @@ const getBlockName = ( blockMetaData ) => { * @return {string} The entry point name. */ const getEntryPointName = ( entryFilePath, blockMetaData ) => { - const filePathParts = entryFilePath.split( '/' ); + const filePathParts = entryFilePath + .split( '/' ) + .filter( ( dir ) => dir !== 'blocks' ); filePathParts[ filePathParts.length - 2 ] = getBlockName( blockMetaData ); return filePathParts .join( '/' ) diff --git a/packages/js/product-editor/package.json b/packages/js/product-editor/package.json index 68c843549ea..c099824a1dc 100644 --- a/packages/js/product-editor/package.json +++ b/packages/js/product-editor/package.json @@ -122,7 +122,8 @@ "changelog": "composer exec -- changelogger", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", - "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "test": "pnpm test:js", + "test:js": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", "lint": "eslint --output-file eslint_report.json --format json src", "build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json", "build:css": "webpack", diff --git a/packages/js/product-editor/src/blocks/catalog-visibility/index.ts b/packages/js/product-editor/src/blocks/catalog-visibility/index.ts deleted file mode 100644 index 5b30cbce748..00000000000 --- a/packages/js/product-editor/src/blocks/catalog-visibility/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { CatalogVisibilityBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< CatalogVisibilityBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< - BlockConfiguration< CatalogVisibilityBlockAttributes > -> = { - example: {}, - edit: Edit, -}; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/checkbox/README.md b/packages/js/product-editor/src/blocks/checkbox/README.md deleted file mode 100644 index ddbad959b38..00000000000 --- a/packages/js/product-editor/src/blocks/checkbox/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# woocommerce/product-checkbox-field - -A reusable checkbox for the product editor. - -![Product checkbox field screenshot](https://woocommerce.files.wordpress.com/2023/09/checkbox.png) - -_Please note that to persist a custom field in the product it also needs to be added to the WooCommerce REST API._ - -## Attributes - -### title - -- **Type:** `String` -- **Required:** `No` - -Header that appears above the checkbox. - -### label - -- **Type:** `String` -- **Required:** `No` - -Label that appears at the side of the checkbox. - -### property - -- **Type:** `String` -- **Required:** `Yes` - -Property in which the checkbox value is stored. - -### tooltip - -- **Type:** `String` -- **Required:** `No` - -Tooltip text that is shown when hovering the icon at the side of the label. - -## Example - -Here's an example on the code that is used for the 'sold_individually' field in the Inventory section: - -```php -$parent_container->add_block( - [ - 'id' => 'product-limit-purchase', - 'blockName' => 'woocommerce/product-checkbox-field', - 'order' => 20, - 'attributes' => [ - 'title' => __( - 'Restrictions', - 'woocommerce' - ), - 'label' => __( - 'Limit purchases to 1 item per order', - 'woocommerce' - ), - 'property' => 'sold_individually', - 'tooltip' => __( - 'When checked, customers will be able to purchase only 1 item in a single order. This is particularly useful for items that have limited quantity, like art or handmade goods.', - 'woocommerce' - ), - ], - ] -); -``` - diff --git a/packages/js/product-editor/src/blocks/checkbox/index.ts b/packages/js/product-editor/src/blocks/checkbox/index.ts deleted file mode 100644 index 6b7a2f17c22..00000000000 --- a/packages/js/product-editor/src/blocks/checkbox/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; - -const { name, ...metadata } = blockConfiguration as BlockConfiguration; - -export { metadata, name }; - -export const settings = { - example: {}, - edit: Edit, -}; - -export const init = () => registerWooBlockType( { name, metadata, settings } ); diff --git a/packages/js/product-editor/src/blocks/collapsible/index.ts b/packages/js/product-editor/src/blocks/collapsible/index.ts deleted file mode 100644 index 6af10d47939..00000000000 --- a/packages/js/product-editor/src/blocks/collapsible/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * External dependencies - */ -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import metadata from './block.json'; -import { Edit } from './edit'; - -const { name } = metadata; - -export { metadata, name }; - -export const settings = { - example: {}, - edit: Edit, -}; - -export const init = () => - registerWooBlockType( { name, metadata: metadata as never, settings } ); diff --git a/packages/js/product-editor/src/blocks/conditional/index.ts b/packages/js/product-editor/src/blocks/conditional/index.ts deleted file mode 100644 index 6af10d47939..00000000000 --- a/packages/js/product-editor/src/blocks/conditional/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * External dependencies - */ -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import metadata from './block.json'; -import { Edit } from './edit'; - -const { name } = metadata; - -export { metadata, name }; - -export const settings = { - example: {}, - edit: Edit, -}; - -export const init = () => - registerWooBlockType( { name, metadata: metadata as never, settings } ); diff --git a/packages/js/product-editor/src/blocks/description/index.ts b/packages/js/product-editor/src/blocks/description/index.ts deleted file mode 100644 index 6b7a2f17c22..00000000000 --- a/packages/js/product-editor/src/blocks/description/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; - -const { name, ...metadata } = blockConfiguration as BlockConfiguration; - -export { metadata, name }; - -export const settings = { - example: {}, - edit: Edit, -}; - -export const init = () => registerWooBlockType( { name, metadata, settings } ); diff --git a/packages/js/product-editor/src/blocks/generic/README.md b/packages/js/product-editor/src/blocks/generic/README.md new file mode 100644 index 00000000000..4be98408a0f --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/README.md @@ -0,0 +1,19 @@ +# Product Editor Generic Blocks + +This directory contains generic blocks for the product editor. + +## Blocks list + +### [woocommerce/product-checkbox-field](checkbox/README.md) + +### [woocommerce/product-collapsible](collapsible/README.md) + +### [woocommerce/conditional](conditional/README.md) + +### [woocommerce/product-pricing-field](pricing/README.md) + +### [woocommerce/product-radio-field](radio/README.md) + +### [woocommerce/product-text-field](text/README.md) + +### [woocommerce/product-toggle-field](toggle/README.md) diff --git a/packages/js/product-editor/src/blocks/generic/checkbox/README.md b/packages/js/product-editor/src/blocks/generic/checkbox/README.md new file mode 100644 index 00000000000..9ce91ec7a54 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/checkbox/README.md @@ -0,0 +1,106 @@ +# woocommerce/product-checkbox-field + +A reusable checkbox for the product editor. + +![Product checkbox field screenshot](https://woocommerce.files.wordpress.com/2023/09/checkbox.png) + +_Please note that to persist a custom field in the product it also needs to be added to the WooCommerce REST API._ + +## Attributes + +### title + +- **Type:** `String` +- **Required:** `No` + +Header that appears above the checkbox. + +### label + +- **Type:** `String` +- **Required:** `No` + +Label that appears at the side of the checkbox. + +### property + +- **Type:** `String` +- **Required:** `Yes` + +Property in which the checkbox value is stored. + +### tooltip + +- **Type:** `String` +- **Required:** `No` + +Tooltip text that is shown when hovering the icon at the side of the label. + +### checkedValue + +- **Type:** `String` +- **Required:** `No` + +If it is set, the checked state will be `property` === `checkedValue`. When `onChange` is fired with the checked value set to `true` then the `property` value will be set to the one stored in `checkedValue`. + +This is needed for cases where the `property` type is not a `boolean`. + +### uncheckedValue + +- **Type:** `String` +- **Required:** `No` + +If it is set, the unchecked state will be `property` !== `checkedValue`. When `onChange` is fired with the checked value set to `false` then the `property` value will be set to the one stored in `uncheckedValue`. + +This is needed for cases where the `property` type is not a `boolean`. + +## Usage + +Here's an example on the code that is used for the `sold_individually` field in the Inventory section: + +```php +$parent_container->add_block( + [ + 'id' => 'product-limit-purchase', + 'blockName' => 'woocommerce/product-checkbox-field', + 'order' => 20, + 'attributes' => [ + 'title' => __( + 'Restrictions', + 'woocommerce' + ), + 'label' => __( + 'Limit purchases to 1 item per order', + 'woocommerce' + ), + 'property' => 'sold_individually', + 'tooltip' => __( + 'When checked, customers will be able to purchase only 1 item in a single order. This is particularly useful for items that have limited quantity, like art or handmade goods.', + 'woocommerce' + ), + ], + ] +); +``` + +--- + +Here's an example that is used to toggle the product variation `status` from `publish` to `private`: + +> In this case the checkbox will be checked when the variation `status === 'private'`, changing the checked state of the checkbox will toggle the `status` value from `private` to `publish` and not from `true` to `false` like in the previous example: + +```php +$parent_container->add_block( + [ + 'id' => 'product-variation-visibility', + 'blockName' => 'woocommerce/product-checkbox-field', + 'order' => 30, + 'attributes' => [ + 'property' => 'status', + 'label' => __( 'Hide in product catalog', 'woocommerce' ), + 'checkedValue' => 'private', + 'uncheckedValue' => 'publish', + ], + ] +); +``` diff --git a/packages/js/product-editor/src/blocks/checkbox/block.json b/packages/js/product-editor/src/blocks/generic/checkbox/block.json similarity index 81% rename from packages/js/product-editor/src/blocks/checkbox/block.json rename to packages/js/product-editor/src/blocks/generic/checkbox/block.json index 88d0fafa767..49b4d26080f 100644 --- a/packages/js/product-editor/src/blocks/checkbox/block.json +++ b/packages/js/product-editor/src/blocks/generic/checkbox/block.json @@ -20,6 +20,12 @@ }, "tooltip": { "type": "string" + }, + "checkedValue": { + "type": "string" + }, + "uncheckedValue": { + "type": "string" } }, "supports": { @@ -31,5 +37,6 @@ "lock": false, "__experimentalToolbar": false }, - "editorStyle": "file:./editor.css" + "editorStyle": "file:./editor.css", + "usesContext": [ "postType" ] } diff --git a/packages/js/product-editor/src/blocks/checkbox/edit.tsx b/packages/js/product-editor/src/blocks/generic/checkbox/edit.tsx similarity index 52% rename from packages/js/product-editor/src/blocks/checkbox/edit.tsx rename to packages/js/product-editor/src/blocks/generic/checkbox/edit.tsx index 5efb60dad99..5e52c221706 100644 --- a/packages/js/product-editor/src/blocks/checkbox/edit.tsx +++ b/packages/js/product-editor/src/blocks/generic/checkbox/edit.tsx @@ -2,36 +2,60 @@ * External dependencies */ import { createElement } from '@wordpress/element'; -import type { BlockAttributes } from '@wordpress/blocks'; import { CheckboxControl, Tooltip } from '@wordpress/components'; -import { useEntityProp } from '@wordpress/core-data'; import { Icon, help } from '@wordpress/icons'; import { useWooBlockProps } from '@woocommerce/block-templates'; /** * Internal dependencies */ +import { ProductEditorBlockEditProps } from '../../../types'; +import useProductEntityProp from '../../../hooks/use-product-entity-prop'; +import { CheckboxBlockAttributes } from './types'; + +export function Edit( { + attributes, + context: { postType }, +}: ProductEditorBlockEditProps< CheckboxBlockAttributes > ) { + const { property, title, label, tooltip, checkedValue, uncheckedValue } = + attributes; -export function Edit( { attributes }: { attributes: BlockAttributes } ) { const blockProps = useWooBlockProps( { className: 'woocommerce-product-form__checkbox', ...attributes, } ); - const { property, title, label, tooltip } = attributes; - const [ value, setValue ] = useEntityProp< boolean >( - 'postType', - 'product', - property + + const [ value, setValue ] = useProductEntityProp< boolean | string | null >( + property, + { + postType, + fallbackValue: false, + } ); + function isChecked() { + if ( checkedValue !== undefined ) { + return checkedValue === value; + } + return value as boolean; + } + + function handleChange( checked: boolean ) { + if ( checked ) { + setValue( checkedValue !== undefined ? checkedValue : checked ); + } else { + setValue( uncheckedValue !== undefined ? uncheckedValue : checked ); + } + } + return (
-

{ title }

+ { title &&

{ title }

}
setValue( selected ) } + checked={ isChecked() } + onChange={ handleChange } /> { tooltip && ( + registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); diff --git a/packages/js/product-editor/src/blocks/generic/checkbox/types.ts b/packages/js/product-editor/src/blocks/generic/checkbox/types.ts new file mode 100644 index 00000000000..1830073f674 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/checkbox/types.ts @@ -0,0 +1,13 @@ +/** + * External dependencies + */ +import type { BlockAttributes } from '@wordpress/blocks'; + +export interface CheckboxBlockAttributes extends BlockAttributes { + property: string; + title?: string; + label?: string; + tooltip?: string; + checkedValue?: string | null; + uncheckedValue?: string | null; +} diff --git a/packages/js/product-editor/src/blocks/generic/collapsible/README.md b/packages/js/product-editor/src/blocks/generic/collapsible/README.md new file mode 100644 index 00000000000..c1e5ff276fc --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/collapsible/README.md @@ -0,0 +1,51 @@ +# woocommerce/product-collapsible + +Container with collapsible inner blocks. + +![Collapsible](https://woocommerce.files.wordpress.com/2023/09/woocommerceproduct-collapsible.png) + +## Attributes + +### toggleText + +- **Type**: `string` +- **Required**: ` Yes` + +The text to display on the toggle button. + +### initialCollapsed + +- **Type**: `boolean` +- **Required**: ` Yes` + +Controls if the content is collapsed by default. + +### persistRender + +- **Type**: `boolean` +- **Required**: ` Yes` + +Controls if content is rendered to the DOM even when collapsed. + +## Usage + +Here's the code that was used to create the example in the screenshot above: + +```php +$product_inventory_advanced = $product_inventory_section->add_block( + [ + 'id' => 'product-inventory-advanced', + 'blockName' => 'woocommerce/product-collapsible', + 'attributes' => [ + 'toggleText' => __( 'Advanced', 'woocommerce' ), + 'initialCollapsed' => true, + 'persistRender' => true, + ], + ] +); +$product_inventory_advanced->add_block( + [ + // add block information here + ] +) +``` diff --git a/packages/js/product-editor/src/blocks/collapsible/block.json b/packages/js/product-editor/src/blocks/generic/collapsible/block.json similarity index 100% rename from packages/js/product-editor/src/blocks/collapsible/block.json rename to packages/js/product-editor/src/blocks/generic/collapsible/block.json diff --git a/packages/js/product-editor/src/blocks/collapsible/edit.tsx b/packages/js/product-editor/src/blocks/generic/collapsible/edit.tsx similarity index 79% rename from packages/js/product-editor/src/blocks/collapsible/edit.tsx rename to packages/js/product-editor/src/blocks/generic/collapsible/edit.tsx index 17dc9b86db5..2e0107011f5 100644 --- a/packages/js/product-editor/src/blocks/collapsible/edit.tsx +++ b/packages/js/product-editor/src/blocks/generic/collapsible/edit.tsx @@ -7,7 +7,14 @@ import type { BlockAttributes } from '@wordpress/blocks'; import { createElement } from '@wordpress/element'; import { InnerBlocks } from '@wordpress/block-editor'; -export function Edit( { attributes }: { attributes: BlockAttributes } ) { +/** + * Internal dependencies + */ +import { ProductEditorBlockEditProps } from '../../../types'; + +export function Edit( { + attributes, +}: ProductEditorBlockEditProps< BlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const { toggleText, initialCollapsed, persistRender = true } = attributes; diff --git a/packages/js/product-editor/src/blocks/attributes/index.ts b/packages/js/product-editor/src/blocks/generic/collapsible/index.ts similarity index 93% rename from packages/js/product-editor/src/blocks/attributes/index.ts rename to packages/js/product-editor/src/blocks/generic/collapsible/index.ts index 37f321821db..5322eeb221e 100644 --- a/packages/js/product-editor/src/blocks/attributes/index.ts +++ b/packages/js/product-editor/src/blocks/generic/collapsible/index.ts @@ -22,5 +22,5 @@ export const init = () => registerWooBlockType( { name, metadata: metadata as never, - settings, + settings: settings as never, } ); diff --git a/packages/js/product-editor/src/blocks/generic/conditional/README.md b/packages/js/product-editor/src/blocks/generic/conditional/README.md new file mode 100644 index 00000000000..c1b778b55d5 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/conditional/README.md @@ -0,0 +1,45 @@ +# woocommerce/conditional + +Container to only conditionally render inner blocks. + + + +## Attributes + +### mustMatch + +- **Type**: `Record< string, Array< string > >` +- **Required**: `Yes` + +A list of requirements that must be met for the inner blocks to be rendered. The keys should reference properties from the product, and the values are possible values for that property so that the inner blocks are rendered. + +## Usage + +Here's the code that was used to create the example in the video above: + +```php +$wrapper = $product_summary_field->get_parent()->add_block( + [ + 'id' => 'example-conditional-wrapper', + 'blockName' => 'woocommerce/conditional', + 'order' => $product_summary_field->get_order() + 5, + 'attributes' => [ + 'mustMatch' => [ + 'name' => [ 'Car', 'Bike' ] + ], + ], + ] +); +$wrapper->add_block( + [ + 'id' => 'example-pricing-field', + 'blockName' => 'woocommerce/product-pricing-field', + 'order' => $product_summary_field->get_order() + 5, + 'attributes' => [ + 'label' => __( 'Example price field', 'woocommerce'), + 'property' => 'custom_price', + 'help' => 'This is a help text', + ], + ] +); +``` diff --git a/packages/js/product-editor/src/blocks/conditional/block.json b/packages/js/product-editor/src/blocks/generic/conditional/block.json similarity index 94% rename from packages/js/product-editor/src/blocks/conditional/block.json rename to packages/js/product-editor/src/blocks/generic/conditional/block.json index 5afff2f5865..c3ddd9c0581 100644 --- a/packages/js/product-editor/src/blocks/conditional/block.json +++ b/packages/js/product-editor/src/blocks/generic/conditional/block.json @@ -16,6 +16,7 @@ "default": [] } }, + "usesContext": [ "editedProduct" ], "supports": { "align": false, "html": false, diff --git a/packages/js/product-editor/src/blocks/conditional/edit.tsx b/packages/js/product-editor/src/blocks/generic/conditional/edit.tsx similarity index 59% rename from packages/js/product-editor/src/blocks/conditional/edit.tsx rename to packages/js/product-editor/src/blocks/generic/conditional/edit.tsx index 0c088010354..7ad3afd18ca 100644 --- a/packages/js/product-editor/src/blocks/conditional/edit.tsx +++ b/packages/js/product-editor/src/blocks/generic/conditional/edit.tsx @@ -4,33 +4,26 @@ import type { BlockAttributes } from '@wordpress/blocks'; import { createElement, useMemo } from '@wordpress/element'; import { InnerBlocks } from '@wordpress/block-editor'; -import { useSelect } from '@wordpress/data'; import { useWooBlockProps } from '@woocommerce/block-templates'; import { DisplayState } from '@woocommerce/components'; -import { Product } from '@woocommerce/data'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore No types for this exist yet. -// eslint-disable-next-line @woocommerce/dependency-group -import { useEntityId } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { ProductEditorBlockEditProps } from '../../../types'; + +export interface ConditionalBlockAttributes extends BlockAttributes { + mustMatch: Record< string, Array< string > >; +} export function Edit( { attributes, -}: { - attributes: BlockAttributes & { - mustMatch: Record< string, Array< string > >; - }; -} ) { + context, +}: ProductEditorBlockEditProps< ConditionalBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const { mustMatch } = attributes; - const productId = useEntityId( 'postType', 'product' ); - const product: Product = useSelect( ( select ) => - select( 'core' ).getEditedEntityRecord( - 'postType', - 'product', - productId - ) - ); + const product = context.editedProduct; const displayBlocks = useMemo( () => { for ( const [ prop, values ] of Object.entries( mustMatch ) ) { diff --git a/packages/js/product-editor/src/blocks/name/index.ts b/packages/js/product-editor/src/blocks/generic/conditional/index.ts similarity index 93% rename from packages/js/product-editor/src/blocks/name/index.ts rename to packages/js/product-editor/src/blocks/generic/conditional/index.ts index 37f321821db..5322eeb221e 100644 --- a/packages/js/product-editor/src/blocks/name/index.ts +++ b/packages/js/product-editor/src/blocks/generic/conditional/index.ts @@ -22,5 +22,5 @@ export const init = () => registerWooBlockType( { name, metadata: metadata as never, - settings, + settings: settings as never, } ); diff --git a/packages/js/product-editor/src/blocks/generic/number/README.md b/packages/js/product-editor/src/blocks/generic/number/README.md new file mode 100644 index 00000000000..c80f311f1b9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/number/README.md @@ -0,0 +1,65 @@ +# woocommerce/product-number-field + +A reusable number field for the product editor. + +![Product number field screenshot](https://woocommerce.files.wordpress.com/2023/10/woocommerceproduct-number-field.png) + +## Attributes + +### label + +- **Type:** `String` +- **Required:** `Yes` + +Label that appears on top of the field. + +### property + +- **Type:** `String` +- **Required:** `Yes` + +Property in which the value is stored. + + +### help + +- **Type:** `String` +- **Required:** `No` + +Help text that appears below the field. + +### suffix + +- **Type:** `String` +- **Required:** `No` + +Suffix that can be used for a unit of measure, as an example. + + +### placeholder + +- **Type:** `String` +- **Required:** `No` + +Placeholder text that appears in the field when it's empty. + +## Usage + +Here's a snippet that adds a field similar to the previous screenshot: + +```php +$section->add_block( + [ + 'id' => 'example-number-meta', + 'blockName' => 'woocommerce/product-number-field', + 'attributes' => [ + 'label' => 'Label', + 'property' => 'meta_data.number_field', + 'suffix' => 'suffix', + 'help' => 'Add additional information here', + 'placeholder' => 'Placeholder', + ] + ], +); +``` + diff --git a/packages/js/product-editor/src/blocks/generic/number/block.json b/packages/js/product-editor/src/blocks/generic/number/block.json new file mode 100644 index 00000000000..0c67f1c5771 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/number/block.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "woocommerce/product-number-field", + "title": "Product number control", + "category": "woocommerce", + "description": "A reusable number field for the product editor.", + "keywords": [ "products", "number", "input" ], + "textdomain": "default", + "attributes": { + "label": { + "type": "string", + "__experimentalRole": "content" + }, + "property": { + "type": "string" + }, + "suffix": { + "type": "string" + }, + "help": { + "type": "string" + }, + "placeholder": { + "type": "string" + } + }, + "supports": { + "align": false, + "html": false, + "multiple": true, + "reusable": false, + "inserter": false, + "lock": false, + "__experimentalToolbar": false + }, + "editorStyle": "file:./editor.css" +} diff --git a/packages/js/product-editor/src/blocks/generic/number/edit.tsx b/packages/js/product-editor/src/blocks/generic/number/edit.tsx new file mode 100644 index 00000000000..94686da66f1 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/number/edit.tsx @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; +import { useWooBlockProps } from '@woocommerce/block-templates'; +import { + // @ts-expect-error `__experimentalInputControl` does exist. + __experimentalInputControl as InputControl, +} from '@wordpress/components'; +/** + * Internal dependencies + */ +import { ProductEditorBlockEditProps } from '../../../types'; +import useProductEntityProp from '../../../hooks/use-product-entity-prop'; +import { useNumberInputProps } from '../../../hooks/use-number-input-props'; +import { NumberBlockAttributes } from './types'; + +export function Edit( { + attributes, + context: { postType }, +}: ProductEditorBlockEditProps< NumberBlockAttributes > ) { + const blockProps = useWooBlockProps( attributes ); + const { label, property, suffix, placeholder, help } = attributes; + const [ value, setValue ] = useProductEntityProp( property, { + postType, + fallbackValue: '', + } ); + + const inputProps = useNumberInputProps( { + value: value || '', + onChange: setValue, + } ); + + return ( +
+ +
+ ); +} diff --git a/packages/js/product-editor/src/blocks/generic/number/editor.scss b/packages/js/product-editor/src/blocks/generic/number/editor.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/js/product-editor/src/blocks/generic/number/index.ts b/packages/js/product-editor/src/blocks/generic/number/index.ts new file mode 100644 index 00000000000..d670004d84d --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/number/index.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => + registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); diff --git a/packages/js/product-editor/src/blocks/generic/number/types.ts b/packages/js/product-editor/src/blocks/generic/number/types.ts new file mode 100644 index 00000000000..d4e80ac53c4 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/number/types.ts @@ -0,0 +1,12 @@ +/** + * External dependencies + */ +import { BlockAttributes } from '@wordpress/blocks'; + +export interface NumberBlockAttributes extends BlockAttributes { + label: string; + property: string; + help?: string; + suffix?: string; + placeholder?: string; +} diff --git a/packages/js/product-editor/src/blocks/generic/pricing/README.md b/packages/js/product-editor/src/blocks/generic/pricing/README.md new file mode 100644 index 00000000000..4cc92be24cf --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/pricing/README.md @@ -0,0 +1,64 @@ +# woocommerce/product-pricing-field + +A product price block with currency display. + +![Product pricing field](https://woocommerce.files.wordpress.com/2023/09/woocommerceproduct-pricing-field.png) + +## Attributes + +### property + +- **Type:** `String` +- **Required:** `Yes` + +Property in which the price value is stored. + +### label + +- **Type:** `String` +- **Required:** `Yes` + +Label that appears on top of the price field. + +### help + +- **Type:** `String` +- **Required:** `No` + +Help text that appears below the price field. + +## Usage + +Here's the code that adds the field from the screenshot after the Summary field: + +```php +get_parent()->add_block( + [ + 'id' => 'example-pricing-field', + 'blockName' => 'woocommerce/product-pricing-field', + 'order' => $product_summary_field->get_order() + 5, + 'attributes' => [ + 'label' => __( 'Example price field', 'woocommerce'), + 'property' => 'custom_price', + 'help' => 'This is a help text', + ], + ] + ); + } +} + +if ( ! function_exists( 'example_hook_up_block_template_modifications_pricing' ) ) { + function example_hook_up_block_template_modifications_pricing() { + add_action( + 'woocommerce_block_template_area_product-form_after_add_block_product-summary', + 'add_pricing_field' + ); + } +} + +add_action( 'init', 'example_hook_up_block_template_modifications_pricing', 0 ); +``` diff --git a/packages/js/product-editor/src/blocks/pricing/block.json b/packages/js/product-editor/src/blocks/generic/pricing/block.json similarity index 88% rename from packages/js/product-editor/src/blocks/pricing/block.json rename to packages/js/product-editor/src/blocks/generic/pricing/block.json index 0b0bece7105..f1afd30e580 100644 --- a/packages/js/product-editor/src/blocks/pricing/block.json +++ b/packages/js/product-editor/src/blocks/generic/pricing/block.json @@ -8,7 +8,7 @@ "keywords": [ "products", "price" ], "textdomain": "default", "attributes": { - "name": { + "property": { "type": "string", "__experimentalRole": "content" }, @@ -28,5 +28,6 @@ "lock": false, "__experimentalToolbar": false }, - "editorStyle": "file:./editor.css" + "editorStyle": "file:./editor.css", + "usesContext": [ "postType" ] } diff --git a/packages/js/product-editor/src/blocks/pricing/edit.tsx b/packages/js/product-editor/src/blocks/generic/pricing/edit.tsx similarity index 74% rename from packages/js/product-editor/src/blocks/pricing/edit.tsx rename to packages/js/product-editor/src/blocks/generic/pricing/edit.tsx index 366a7370d41..f5c6e9a9835 100644 --- a/packages/js/product-editor/src/blocks/pricing/edit.tsx +++ b/packages/js/product-editor/src/blocks/generic/pricing/edit.tsx @@ -5,9 +5,7 @@ import { useWooBlockProps } from '@woocommerce/block-templates'; import { Link } from '@woocommerce/components'; import { getNewPath } from '@woocommerce/navigation'; import { recordEvent } from '@woocommerce/tracks'; -import { BlockEditProps } from '@wordpress/blocks'; import { useInstanceId } from '@wordpress/compose'; -import { useEntityProp } from '@wordpress/core-data'; import { createElement, createInterpolateElement } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { @@ -19,21 +17,23 @@ import { /** * Internal dependencies */ -import { useCurrencyInputProps } from '../../hooks/use-currency-input-props'; +import { useCurrencyInputProps } from '../../../hooks/use-currency-input-props'; import { PricingBlockAttributes } from './types'; +import { ProductEditorBlockEditProps } from '../../../types'; +import useProductEntityProp from '../../../hooks/use-product-entity-prop'; export function Edit( { attributes, -}: BlockEditProps< PricingBlockAttributes > ) { + context: { postType }, +}: ProductEditorBlockEditProps< PricingBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); - const { name, label, help } = attributes; - const [ price, setPrice ] = useEntityProp< string >( - 'postType', - 'product', - name - ); + const { property, label, help } = attributes; + const [ price, setPrice ] = useProductEntityProp< string >( property, { + postType, + fallbackValue: '', + } ); const inputProps = useCurrencyInputProps( { - value: price, + value: price || '', onChange: setPrice, } ); @@ -61,7 +61,7 @@ export function Edit( { diff --git a/packages/js/product-editor/src/blocks/pricing/editor.scss b/packages/js/product-editor/src/blocks/generic/pricing/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/pricing/editor.scss rename to packages/js/product-editor/src/blocks/generic/pricing/editor.scss diff --git a/packages/js/product-editor/src/blocks/generic/pricing/index.ts b/packages/js/product-editor/src/blocks/generic/pricing/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/pricing/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/pricing/types.ts b/packages/js/product-editor/src/blocks/generic/pricing/types.ts similarity index 90% rename from packages/js/product-editor/src/blocks/pricing/types.ts rename to packages/js/product-editor/src/blocks/generic/pricing/types.ts index ae5ec68b128..46f4de43562 100644 --- a/packages/js/product-editor/src/blocks/pricing/types.ts +++ b/packages/js/product-editor/src/blocks/generic/pricing/types.ts @@ -4,7 +4,7 @@ import { BlockAttributes } from '@wordpress/blocks'; export interface PricingBlockAttributes extends BlockAttributes { - name: string; + property: string; label: string; help?: string; } diff --git a/packages/js/product-editor/src/blocks/generic/radio/README.md b/packages/js/product-editor/src/blocks/generic/radio/README.md new file mode 100644 index 00000000000..4bb31e8c26e --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/radio/README.md @@ -0,0 +1,60 @@ +# woocommerce/product-radio-field block + +Radio button field for the product editor. + +![Product radio field](https://woocommerce.files.wordpress.com/2023/09/woocommerceproduct-radio-field.png) + +## Attributes + +### title + +- **Type:** `String` +- **Required:** `Yes` + +### description + +- **Type:** `String` +- **Required:** `No` + + +### property + +- **Type:** `String` +- **Required:** `Yes` + +### options + +- **Type:** `Array` +- **Required:** `Yes` + +## Usage + +Here's an example of the usage on the "Charge sales tax on" field in the Pricing section: + +```php +$product_pricing_section->add_block( + [ + 'id' => 'product-sale-tax', + 'blockName' => 'woocommerce/product-radio-field', + 'order' => 30, + 'attributes' => [ + 'title' => __( 'Charge sales tax on', 'woocommerce' ), + 'property' => 'tax_status', + 'options' => [ + [ + 'label' => __( 'Product and shipping', 'woocommerce' ), + 'value' => 'taxable', + ], + [ + 'label' => __( 'Only shipping', 'woocommerce' ), + 'value' => 'shipping', + ], + [ + 'label' => __( "Don't charge tax", 'woocommerce' ), + 'value' => 'none', + ], + ], + ], + ] +); +``` diff --git a/packages/js/product-editor/src/blocks/radio/block.json b/packages/js/product-editor/src/blocks/generic/radio/block.json similarity index 95% rename from packages/js/product-editor/src/blocks/radio/block.json rename to packages/js/product-editor/src/blocks/generic/radio/block.json index 234b073c31d..76030c8da35 100644 --- a/packages/js/product-editor/src/blocks/radio/block.json +++ b/packages/js/product-editor/src/blocks/generic/radio/block.json @@ -34,5 +34,6 @@ "inserter": false, "lock": false, "__experimentalToolbar": false - } + }, + "usesContext": [ "postType" ] } diff --git a/packages/js/product-editor/src/blocks/radio/edit.tsx b/packages/js/product-editor/src/blocks/generic/radio/edit.tsx similarity index 57% rename from packages/js/product-editor/src/blocks/radio/edit.tsx rename to packages/js/product-editor/src/blocks/generic/radio/edit.tsx index b8ba2c2506d..5bba087cd35 100644 --- a/packages/js/product-editor/src/blocks/radio/edit.tsx +++ b/packages/js/product-editor/src/blocks/generic/radio/edit.tsx @@ -2,24 +2,26 @@ * External dependencies */ import { createElement } from '@wordpress/element'; -import { BlockEditProps } from '@wordpress/blocks'; -import { useEntityProp } from '@wordpress/core-data'; import { useWooBlockProps } from '@woocommerce/block-templates'; /** * Internal dependencies */ -import { RadioField } from '../../components/radio-field'; +import { RadioField } from '../../../components/radio-field'; import { RadioBlockAttributes } from './types'; +import { ProductEditorBlockEditProps } from '../../../types'; +import useProductEntityProp from '../../../hooks/use-product-entity-prop'; -export function Edit( { attributes }: BlockEditProps< RadioBlockAttributes > ) { +export function Edit( { + attributes, + context: { postType }, +}: ProductEditorBlockEditProps< RadioBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const { description, options, property, title } = attributes; - const [ value, setValue ] = useEntityProp< string >( - 'postType', - 'product', - property - ); + const [ value, setValue ] = useProductEntityProp< string >( property, { + postType, + fallbackValue: '', + } ); return (
diff --git a/packages/js/product-editor/src/blocks/generic/radio/index.ts b/packages/js/product-editor/src/blocks/generic/radio/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/radio/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/radio/types.ts b/packages/js/product-editor/src/blocks/generic/radio/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/radio/types.ts rename to packages/js/product-editor/src/blocks/generic/radio/types.ts diff --git a/packages/js/product-editor/src/blocks/section/block.json b/packages/js/product-editor/src/blocks/generic/section/block.json similarity index 100% rename from packages/js/product-editor/src/blocks/section/block.json rename to packages/js/product-editor/src/blocks/generic/section/block.json diff --git a/packages/js/product-editor/src/blocks/section/edit.tsx b/packages/js/product-editor/src/blocks/generic/section/edit.tsx similarity index 88% rename from packages/js/product-editor/src/blocks/section/edit.tsx rename to packages/js/product-editor/src/blocks/generic/section/edit.tsx index 3f18c455e4f..c1ae5274773 100644 --- a/packages/js/product-editor/src/blocks/section/edit.tsx +++ b/packages/js/product-editor/src/blocks/generic/section/edit.tsx @@ -3,7 +3,6 @@ */ import classNames from 'classnames'; import { createElement } from '@wordpress/element'; -import type { BlockEditProps } from '@wordpress/blocks'; import { useWooBlockProps } from '@woocommerce/block-templates'; import { // @ts-expect-error no exported member. @@ -13,12 +12,13 @@ import { /** * Internal dependencies */ -import { sanitizeHTML } from '../../utils/sanitize-html'; +import { sanitizeHTML } from '../../../utils/sanitize-html'; import { SectionBlockAttributes } from './types'; +import { ProductEditorBlockEditProps } from '../../../types'; export function Edit( { attributes, -}: BlockEditProps< SectionBlockAttributes > ) { +}: ProductEditorBlockEditProps< SectionBlockAttributes > ) { const { description, title, blockGap } = attributes; const blockProps = useWooBlockProps( attributes ); const innerBlockProps = useInnerBlocksProps( diff --git a/packages/js/product-editor/src/blocks/section/editor.scss b/packages/js/product-editor/src/blocks/generic/section/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/section/editor.scss rename to packages/js/product-editor/src/blocks/generic/section/editor.scss diff --git a/packages/js/product-editor/src/blocks/generic/section/index.ts b/packages/js/product-editor/src/blocks/generic/section/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/section/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/section/types.ts b/packages/js/product-editor/src/blocks/generic/section/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/section/types.ts rename to packages/js/product-editor/src/blocks/generic/section/types.ts diff --git a/packages/js/product-editor/src/blocks/tab/block.json b/packages/js/product-editor/src/blocks/generic/tab/block.json similarity index 100% rename from packages/js/product-editor/src/blocks/tab/block.json rename to packages/js/product-editor/src/blocks/generic/tab/block.json diff --git a/packages/js/product-editor/src/blocks/tab/edit.tsx b/packages/js/product-editor/src/blocks/generic/tab/edit.tsx similarity index 85% rename from packages/js/product-editor/src/blocks/tab/edit.tsx rename to packages/js/product-editor/src/blocks/generic/tab/edit.tsx index d9af3352387..02655e0c72f 100644 --- a/packages/js/product-editor/src/blocks/tab/edit.tsx +++ b/packages/js/product-editor/src/blocks/generic/tab/edit.tsx @@ -4,13 +4,14 @@ import { InnerBlocks } from '@wordpress/block-editor'; import classnames from 'classnames'; import { createElement } from '@wordpress/element'; -import type { BlockAttributes, BlockEditProps } from '@wordpress/blocks'; +import type { BlockAttributes } from '@wordpress/blocks'; import { useWooBlockProps } from '@woocommerce/block-templates'; /** * Internal dependencies */ import { TabButton } from './tab-button'; +import { ProductEditorBlockEditProps } from '../../../types'; export interface TabBlockAttributes extends BlockAttributes { id: string; @@ -23,14 +24,10 @@ export function Edit( { setAttributes, attributes, context, -}: BlockEditProps< TabBlockAttributes > & { - context?: { - selectedTab?: string | null; - }; -} ) { +}: ProductEditorBlockEditProps< TabBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const { id, title, order, isSelected: contextIsSelected } = attributes; - const isSelected = context?.selectedTab === id; + const isSelected = context.selectedTab === id; if ( isSelected !== contextIsSelected ) { setAttributes( { isSelected } ); } diff --git a/packages/js/product-editor/src/blocks/tab/editor.scss b/packages/js/product-editor/src/blocks/generic/tab/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/tab/editor.scss rename to packages/js/product-editor/src/blocks/generic/tab/editor.scss diff --git a/packages/js/product-editor/src/blocks/generic/tab/index.ts b/packages/js/product-editor/src/blocks/generic/tab/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/tab/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/tab/tab-button.tsx b/packages/js/product-editor/src/blocks/generic/tab/tab-button.tsx similarity index 90% rename from packages/js/product-editor/src/blocks/tab/tab-button.tsx rename to packages/js/product-editor/src/blocks/generic/tab/tab-button.tsx index 6c8490c84a4..32dd0073fe9 100644 --- a/packages/js/product-editor/src/blocks/tab/tab-button.tsx +++ b/packages/js/product-editor/src/blocks/generic/tab/tab-button.tsx @@ -8,8 +8,8 @@ import { createElement, Fragment } from '@wordpress/element'; /** * Internal dependencies */ -import { TABS_SLOT_NAME } from '../../components/tabs/constants'; -import { TabsFillProps } from '../../components/tabs'; +import { TABS_SLOT_NAME } from '../../../components/tabs/constants'; +import { TabsFillProps } from '../../../components/tabs'; export const DEFAULT_TAB_ORDER = 100; diff --git a/packages/js/product-editor/src/blocks/taxonomy/README.md b/packages/js/product-editor/src/blocks/generic/taxonomy/README.md similarity index 51% rename from packages/js/product-editor/src/blocks/taxonomy/README.md rename to packages/js/product-editor/src/blocks/generic/taxonomy/README.md index 60f9c97e595..7b19e17a2b1 100644 --- a/packages/js/product-editor/src/blocks/taxonomy/README.md +++ b/packages/js/product-editor/src/blocks/generic/taxonomy/README.md @@ -2,6 +2,55 @@ This is a block that displays a taxonomy field, allowing searching, selection, and creation of new items, to be used in a product context. +![Taxonomy block](https://woocommerce.files.wordpress.com/2023/09/woocommerceproduct-taxonomy-field.png) + +## Attributes + +### slug + +- **Type:** `String` +- **Required:** `Yes` + +The taxonomy slug. + +### property + +- **Type:** `String` +- **Required:** `Yes` + +Property name in which the taxonomy value is stored in the product. + +### label + +- **Type:** `String` +- **Required:** `No` + +Label that appears on top of the field. + +### createTitle + +- **Type:** `String` +- **Required:** `No` + +Title of the dialog that appears when creating a new taxonomy. + +### dialogNameHelpText + +- **Type:** `String` +- **Required:** `No` + +Help text that appears for the name field in the dialog that appears when creating a new taxonomy. + +### parentTaxonomyText + +- **Type:** `String` +- **Required:** `No` + +Label for the parent taxonomy field in the dialog that appears when creating a new taxonomy. + + +## Usage + Please note that to use this block you need to have the custom taxonomy registered in the backend, attached to the products post type and added to the REST API. Here's a snippet that shows how to add an already registered taxonomy to the REST API: ```php @@ -49,3 +98,23 @@ function YOUR_PREFIX_rest_api_add_custom1_to_product( $product, $request, $creat add_filter( 'woocommerce_rest_insert_product_object', 'YOUR_PREFIX_rest_api_add_custom1_to_product', 10, 3 ); ``` + +Here's a snippet that shows how it is used for the Categories field: + +```php +$product_catalog_section->add_block( + [ + 'id' => 'product-categories', + 'blockName' => 'woocommerce/product-taxonomy-field', + 'order' => 10, + 'attributes' => [ + 'slug' => 'product_cat', + 'property' => 'categories', + 'label' => __( 'Categories', 'woocommerce' ), + 'createTitle' => __( 'Create new category', 'woocommerce' ), + 'dialogNameHelpText' => __( 'Shown to customers on the product page.', 'woocommerce' ), + 'parentTaxonomyText' => __( 'Parent category', 'woocommerce' ), + ], + ] +); +``` diff --git a/packages/js/product-editor/src/blocks/taxonomy/block.json b/packages/js/product-editor/src/blocks/generic/taxonomy/block.json similarity index 93% rename from packages/js/product-editor/src/blocks/taxonomy/block.json rename to packages/js/product-editor/src/blocks/generic/taxonomy/block.json index cff52e4791a..f0d8a2550ff 100644 --- a/packages/js/product-editor/src/blocks/taxonomy/block.json +++ b/packages/js/product-editor/src/blocks/generic/taxonomy/block.json @@ -42,5 +42,6 @@ "lock": false, "__experimentalToolbar": false }, - "editorStyle": "file:./editor.css" + "editorStyle": "file:./editor.css", + "usesContext": [ "postType" ] } diff --git a/packages/js/product-editor/src/blocks/taxonomy/create-taxonomy-modal.tsx b/packages/js/product-editor/src/blocks/generic/taxonomy/create-taxonomy-modal.tsx similarity index 100% rename from packages/js/product-editor/src/blocks/taxonomy/create-taxonomy-modal.tsx rename to packages/js/product-editor/src/blocks/generic/taxonomy/create-taxonomy-modal.tsx diff --git a/packages/js/product-editor/src/blocks/taxonomy/edit.tsx b/packages/js/product-editor/src/blocks/generic/taxonomy/edit.tsx similarity index 87% rename from packages/js/product-editor/src/blocks/taxonomy/edit.tsx rename to packages/js/product-editor/src/blocks/generic/taxonomy/edit.tsx index 33e3a1fe570..33c4b38a6e1 100644 --- a/packages/js/product-editor/src/blocks/taxonomy/edit.tsx +++ b/packages/js/product-editor/src/blocks/generic/taxonomy/edit.tsx @@ -12,7 +12,6 @@ import { import '@woocommerce/settings'; import { useWooBlockProps } from '@woocommerce/block-templates'; import { __experimentalSelectTreeControl as SelectTreeControl } from '@woocommerce/components'; -import { useEntityProp } from '@wordpress/core-data'; import { useDebounce, useInstanceId } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; @@ -22,6 +21,8 @@ import { useSelect } from '@wordpress/data'; import { CreateTaxonomyModal } from './create-taxonomy-modal'; import { Taxonomy, TaxonomyMetadata } from './types'; import useTaxonomySearch from './use-taxonomy-search'; +import { ProductEditorBlockEditProps } from '../../../types'; +import useProductEntityProp from '../../../hooks/use-product-entity-prop'; interface TaxonomyBlockAttributes extends BlockAttributes { label: string; @@ -34,9 +35,8 @@ interface TaxonomyBlockAttributes extends BlockAttributes { export function Edit( { attributes, -}: { - attributes: TaxonomyBlockAttributes; -} ) { + context: { postType }, +}: ProductEditorBlockEditProps< TaxonomyBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const { hierarchical }: TaxonomyMetadata = useSelect( ( select ) => @@ -73,13 +73,11 @@ export function Edit( { searchDelayed( '' ); }, [] ); - const [ selectedEntries, setSelectedEntries ] = useEntityProp< Taxonomy[] >( - 'postType', - 'product', - property - ); + const [ selectedEntries, setSelectedEntries ] = useProductEntityProp< + Taxonomy[] + >( property, { postType, fallbackValue: [] } ); - const mappedEntries = selectedEntries.map( ( b ) => ( { + const mappedEntries = ( selectedEntries || [] ).map( ( b ) => ( { value: String( b.id ), label: b.name, } ) ); @@ -130,7 +128,7 @@ export function Edit( { name: i.label, parent: +( i.parent || 0 ), } ) ), - ...selectedEntries, + ...( selectedEntries || [] ), ] ); } else { setSelectedEntries( [ @@ -139,14 +137,14 @@ export function Edit( { name: selectedItems.label, parent: +( selectedItems.parent || 0 ), }, - ...selectedEntries, + ...( selectedEntries || [] ), ] ); } } } onRemove={ ( removedItems ) => { if ( Array.isArray( removedItems ) ) { setSelectedEntries( - selectedEntries.filter( + ( selectedEntries || [] ).filter( ( taxonomy ) => ! removedItems.find( ( item ) => @@ -157,7 +155,7 @@ export function Edit( { ); } else { setSelectedEntries( - selectedEntries.filter( + ( selectedEntries || [] ).filter( ( taxonomy ) => String( taxonomy.id ) !== removedItems.value @@ -183,7 +181,7 @@ export function Edit( { name: taxonomy.name, parent: taxonomy.parent, }, - ...selectedEntries, + ...( selectedEntries || [] ), ] ); } } initialName={ searchValue } diff --git a/packages/js/product-editor/src/blocks/taxonomy/editor.scss b/packages/js/product-editor/src/blocks/generic/taxonomy/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/taxonomy/editor.scss rename to packages/js/product-editor/src/blocks/generic/taxonomy/editor.scss diff --git a/packages/js/product-editor/src/blocks/taxonomy/index.ts b/packages/js/product-editor/src/blocks/generic/taxonomy/index.ts similarity index 93% rename from packages/js/product-editor/src/blocks/taxonomy/index.ts rename to packages/js/product-editor/src/blocks/generic/taxonomy/index.ts index 37f321821db..5322eeb221e 100644 --- a/packages/js/product-editor/src/blocks/taxonomy/index.ts +++ b/packages/js/product-editor/src/blocks/generic/taxonomy/index.ts @@ -22,5 +22,5 @@ export const init = () => registerWooBlockType( { name, metadata: metadata as never, - settings, + settings: settings as never, } ); diff --git a/packages/js/product-editor/src/blocks/taxonomy/types.ts b/packages/js/product-editor/src/blocks/generic/taxonomy/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/taxonomy/types.ts rename to packages/js/product-editor/src/blocks/generic/taxonomy/types.ts diff --git a/packages/js/product-editor/src/blocks/taxonomy/use-taxonomy-search.ts b/packages/js/product-editor/src/blocks/generic/taxonomy/use-taxonomy-search.ts similarity index 100% rename from packages/js/product-editor/src/blocks/taxonomy/use-taxonomy-search.ts rename to packages/js/product-editor/src/blocks/generic/taxonomy/use-taxonomy-search.ts diff --git a/packages/js/product-editor/src/blocks/generic/text/block.json b/packages/js/product-editor/src/blocks/generic/text/block.json new file mode 100644 index 00000000000..edf42709d23 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/text/block.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "woocommerce/product-text-field", + "title": "Product text field", + "category": "woocommerce", + "description": "A text field for use in the product editor.", + "keywords": [ + "products", + "text" + ], + "textdomain": "default", + "attributes": { + "label": { + "type": "string", + "__experimentalRole": "content" + }, + "property": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": false + }, + "validationRegex": { + "type": "string" + }, + "validationErrorMessage": { + "type": "string" + }, + "minLength": { + "type": "number" + }, + "maxLength": { + "type": "number" + } + }, + "supports": { + "align": false, + "html": false, + "multiple": true, + "reusable": false, + "inserter": false, + "lock": false, + "__experimentalToolbar": false + }, + "editorStyle": "file:./editor.css", + "usesContext": [ "postType" ] +} diff --git a/packages/js/product-editor/src/blocks/generic/text/edit.tsx b/packages/js/product-editor/src/blocks/generic/text/edit.tsx new file mode 100644 index 00000000000..1925f6d1c55 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/text/edit.tsx @@ -0,0 +1,121 @@ +/** + * External dependencies + */ +import { createElement, createInterpolateElement } from '@wordpress/element'; +import { useInstanceId } from '@wordpress/compose'; +import { __, sprintf } from '@wordpress/i18n'; +import { Product } from '@woocommerce/data'; +import { useWooBlockProps } from '@woocommerce/block-templates'; +import classNames from 'classnames'; +import { + BaseControl, + // @ts-expect-error `__experimentalInputControl` does exist. + __experimentalInputControl as InputControl, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { useValidation } from '../../../contexts/validation-context'; +import useProductEntityProp from '../../../hooks/use-product-entity-prop'; +import { TextBlockAttributes } from './types'; +import { ProductEditorBlockEditProps } from '../../../types'; + +export function Edit( { + attributes, + context: { postType }, +}: ProductEditorBlockEditProps< TextBlockAttributes > ) { + const blockProps = useWooBlockProps( attributes ); + const { + property, + label, + placeholder, + required, + validationRegex, + validationErrorMessage, + minLength, + maxLength, + } = attributes; + const [ value, setValue ] = useProductEntityProp< string >( property, { + postType, + fallbackValue: '', + } ); + const nameControlId = useInstanceId( BaseControl, property ) as string; + + const { error, validate } = useValidation< Product >( + property, + async function validator() { + if ( typeof value !== 'string' ) { + return __( + 'Unexpected property type assigned to field.', + 'woocommerce' + ); + } + if ( required && ! value ) { + return __( 'This field is required.', 'woocommerce' ); + } + if ( validationRegex ) { + const regExp = new RegExp( validationRegex ); + if ( ! regExp.test( value ) ) { + return ( + validationErrorMessage || + __( 'Invalid value for the field.', 'woocommerce' ) + ); + } + } + if ( typeof minLength === 'number' && value.length < minLength ) { + return sprintf( + /* translators: %d: minimum length */ + __( + 'The minimum length of the field is %d', + 'woocommerce' + ), + minLength + ); + } + if ( typeof maxLength === 'number' && value.length > maxLength ) { + return sprintf( + /* translators: %d: maximum length */ + __( + 'The maximum length of the field is %d', + 'woocommerce' + ), + maxLength + ); + } + }, + [ value ] + ); + + return ( +
+ `, { + required: ( + + { /* translators: field 'required' indicator */ } + { __( '*', 'woocommerce' ) } + + ), + } ) + : label + } + className={ classNames( { + 'has-error': error, + } ) } + help={ error } + > + + +
+ ); +} diff --git a/packages/js/product-editor/src/blocks/generic/text/editor.scss b/packages/js/product-editor/src/blocks/generic/text/editor.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/js/product-editor/src/blocks/summary/index.ts b/packages/js/product-editor/src/blocks/generic/text/index.ts similarity index 59% rename from packages/js/product-editor/src/blocks/summary/index.ts rename to packages/js/product-editor/src/blocks/generic/text/index.ts index 634fcfa6837..4a21cafe137 100644 --- a/packages/js/product-editor/src/blocks/summary/index.ts +++ b/packages/js/product-editor/src/blocks/generic/text/index.ts @@ -9,22 +9,21 @@ import { registerWooBlockType } from '@woocommerce/block-templates'; */ import blockConfiguration from './block.json'; import { Edit } from './edit'; -import { SummaryAttributes } from './types'; +import { TextBlockAttributes } from './types'; const { name, ...metadata } = - blockConfiguration as BlockConfiguration< SummaryAttributes >; + blockConfiguration as BlockConfiguration< TextBlockAttributes >; -export { name, metadata }; +export { metadata, name }; export const settings = { example: {}, edit: Edit, }; -export function init() { - return registerWooBlockType< SummaryAttributes >( { +export const init = () => + registerWooBlockType( { name, - metadata, - settings, + metadata: metadata as never, + settings: settings as never, } ); -} diff --git a/packages/js/product-editor/src/blocks/generic/text/types.ts b/packages/js/product-editor/src/blocks/generic/text/types.ts new file mode 100644 index 00000000000..3ed584e8e77 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/text/types.ts @@ -0,0 +1,15 @@ +/** + * External dependencies + */ +import type { BlockAttributes } from '@wordpress/blocks'; + +export interface TextBlockAttributes extends BlockAttributes { + property: string; + label?: string; + placeholder?: string; + required: boolean; + validationRegex?: string; + validationErrorMessage?: string; + minLength?: number; + maxLength?: number; +} diff --git a/packages/js/product-editor/src/blocks/toggle/block.json b/packages/js/product-editor/src/blocks/generic/toggle/block.json similarity index 95% rename from packages/js/product-editor/src/blocks/toggle/block.json rename to packages/js/product-editor/src/blocks/generic/toggle/block.json index 1c10e09a95d..7db3610b691 100644 --- a/packages/js/product-editor/src/blocks/toggle/block.json +++ b/packages/js/product-editor/src/blocks/generic/toggle/block.json @@ -32,5 +32,6 @@ "inserter": false, "lock": false, "__experimentalToolbar": false - } + }, + "usesContext": [ "postType" ] } diff --git a/packages/js/product-editor/src/blocks/toggle/edit.tsx b/packages/js/product-editor/src/blocks/generic/toggle/edit.tsx similarity index 66% rename from packages/js/product-editor/src/blocks/toggle/edit.tsx rename to packages/js/product-editor/src/blocks/generic/toggle/edit.tsx index 6b9a46dddb5..6a1e735ea05 100644 --- a/packages/js/product-editor/src/blocks/toggle/edit.tsx +++ b/packages/js/product-editor/src/blocks/generic/toggle/edit.tsx @@ -2,8 +2,6 @@ * External dependencies */ import { createElement } from '@wordpress/element'; -import { BlockEditProps } from '@wordpress/blocks'; -import { useEntityProp } from '@wordpress/core-data'; import { ToggleControl } from '@wordpress/components'; import { useWooBlockProps } from '@woocommerce/block-templates'; @@ -11,18 +9,20 @@ import { useWooBlockProps } from '@woocommerce/block-templates'; * Internal dependencies */ import { ToggleBlockAttributes } from './types'; -import { sanitizeHTML } from '../../utils/sanitize-html'; +import { sanitizeHTML } from '../../../utils/sanitize-html'; +import { ProductEditorBlockEditProps } from '../../../types'; +import useProductEntityProp from '../../../hooks/use-product-entity-prop'; export function Edit( { attributes, -}: BlockEditProps< ToggleBlockAttributes > ) { + context: { postType }, +}: ProductEditorBlockEditProps< ToggleBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const { label, property, disabled, disabledCopy } = attributes; - const [ value, setValue ] = useEntityProp< boolean >( - 'postType', - 'product', - property - ); + const [ value, setValue ] = useProductEntityProp< boolean >( property, { + postType, + fallbackValue: false, + } ); return (
diff --git a/packages/js/product-editor/src/blocks/toggle/editor.scss b/packages/js/product-editor/src/blocks/generic/toggle/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/toggle/editor.scss rename to packages/js/product-editor/src/blocks/generic/toggle/editor.scss diff --git a/packages/js/product-editor/src/blocks/generic/toggle/index.ts b/packages/js/product-editor/src/blocks/generic/toggle/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/toggle/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/toggle/types.ts b/packages/js/product-editor/src/blocks/generic/toggle/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/toggle/types.ts rename to packages/js/product-editor/src/blocks/generic/toggle/types.ts diff --git a/packages/js/product-editor/src/blocks/images/edit.tsx b/packages/js/product-editor/src/blocks/images/edit.tsx deleted file mode 100644 index ca4f3aff59c..00000000000 --- a/packages/js/product-editor/src/blocks/images/edit.tsx +++ /dev/null @@ -1,183 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { BlockEditProps, BlockAttributes } from '@wordpress/blocks'; -import { DropZone } from '@wordpress/components'; -import classnames from 'classnames'; -import { createElement, useState } from '@wordpress/element'; -import { Icon, trash } from '@wordpress/icons'; -import { MediaItem } from '@wordpress/media-utils'; -import { useWooBlockProps } from '@woocommerce/block-templates'; -import { - MediaUploader, - ImageGallery, - ImageGalleryItem, -} from '@woocommerce/components'; -import { recordEvent } from '@woocommerce/tracks'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore No types for this exist yet. -// eslint-disable-next-line @woocommerce/dependency-group -import { useEntityProp } from '@wordpress/core-data'; - -type Image = MediaItem & { - src: string; -}; - -export function Edit( { attributes }: BlockEditProps< BlockAttributes > ) { - const [ images, setImages ] = useEntityProp< MediaItem[] >( - 'postType', - 'product', - 'images' - ); - const [ isRemovingZoneVisible, setIsRemovingZoneVisible ] = - useState< boolean >( false ); - const [ isRemoving, setIsRemoving ] = useState< boolean >( false ); - const [ draggedImageId, setDraggedImageId ] = useState< number | null >( - null - ); - - const blockProps = useWooBlockProps( attributes, { - className: classnames( { - 'has-images': images.length > 0, - } ), - } ); - - const toggleRemoveZone = () => { - setIsRemovingZoneVisible( ! isRemovingZoneVisible ); - }; - - const orderImages = ( newOrder: JSX.Element[] ) => { - const orderedImages = newOrder.map( ( image ) => { - return images.find( - ( file ) => file.id === parseInt( image?.props?.id, 10 ) - ); - } ); - recordEvent( 'product_images_change_image_order_via_image_gallery' ); - setImages( orderedImages as MediaItem[] ); - }; - - const onFileUpload = ( files: MediaItem[] ) => { - if ( files[ 0 ].id ) { - recordEvent( 'product_images_add_via_file_upload_area' ); - setImages( [ ...images, ...files ] ); - } - }; - - return ( -
-
- { isRemovingZoneVisible ? ( -
- - - { __( 'Drop here to remove', 'woocommerce' ) } - - setIsRemoving( true ) } - onDrop={ () => setIsRemoving( true ) } - label={ __( 'Drop here to remove', 'woocommerce' ) } - /> -
- ) : ( - null } - onFileUploadChange={ onFileUpload } - onMediaGalleryOpen={ () => { - recordEvent( 'product_images_media_gallery_open' ); - } } - onSelect={ ( files ) => { - const newImages = files.filter( - ( img: Image ) => - ! images.find( - ( image ) => image.id === img.id - ) - ); - if ( newImages.length > 0 ) { - recordEvent( - 'product_images_add_via_media_library' - ); - setImages( [ ...images, ...newImages ] ); - } - } } - onUpload={ ( files ) => { - if ( files[ 0 ].id ) { - recordEvent( - 'product_images_add_via_drag_and_drop_upload' - ); - setImages( [ ...images, ...files ] ); - } - } } - label={ '' } - /> - ) } -
- { - const { id: imageId, dataset } = - event.target as HTMLElement; - if ( imageId ) { - setDraggedImageId( parseInt( imageId, 10 ) ); - } else { - const index = dataset?.index; - if ( index ) { - setDraggedImageId( - images[ parseInt( index, 10 ) ]?.id - ); - } - } - toggleRemoveZone(); - } } - onDragEnd={ () => { - if ( isRemoving && draggedImageId ) { - recordEvent( - 'product_images_remove_image_button_click' - ); - setImages( - images.filter( - ( img ) => img.id !== draggedImageId - ) - ); - setIsRemoving( false ); - setDraggedImageId( null ); - } - toggleRemoveZone(); - } } - onOrderChange={ orderImages } - onReplace={ ( { replaceIndex, media } ) => { - if ( - images.find( ( img ) => media.id === img.id ) === - undefined - ) { - const newImages = [ ...images ]; - newImages[ replaceIndex ] = media as MediaItem; - recordEvent( - 'product_images_replace_image_button_click' - ); - setImages( newImages ); - } - } } - onSelectAsCover={ () => - recordEvent( - 'product_images_select_image_as_cover_button_click' - ) - } - > - { images.map( ( image ) => ( - - ) ) } - -
- ); -} diff --git a/packages/js/product-editor/src/blocks/images/editor.scss b/packages/js/product-editor/src/blocks/images/editor.scss deleted file mode 100644 index 5ab57eb05e6..00000000000 --- a/packages/js/product-editor/src/blocks/images/editor.scss +++ /dev/null @@ -1,24 +0,0 @@ -.wp-block-woocommerce-product-images-field { - .woocommerce-media-uploader { - text-align: left; - } - .woocommerce-media-uploader__label { - display: none; - } - .woocommerce-sortable { - margin-top: 0; - padding: 0; - } - - &.has-images { - .woocommerce-image-gallery { - margin-top: $gap-largest; - } - } - - &:not(.has-images) { - .woocommerce-sortable { - display: none; - } - } -} diff --git a/packages/js/product-editor/src/blocks/index.ts b/packages/js/product-editor/src/blocks/index.ts index 182ad1c1f65..ad6bccfbe0b 100644 --- a/packages/js/product-editor/src/blocks/index.ts +++ b/packages/js/product-editor/src/blocks/index.ts @@ -1,29 +1,32 @@ -export { init as initCatalogVisibility } from './catalog-visibility'; -export { init as initCheckbox } from './checkbox'; -export { init as initCollapsible } from './collapsible'; -export { init as initConditional } from './conditional'; -export { init as initDescription } from './description'; -export { init as initImages } from './images'; -export { init as initLowStockQty } from './inventory-email'; -export { init as initSku } from './inventory-sku'; -export { init as initName } from './name'; -export { init as initPricing } from './pricing'; -export { init as initRadio } from './radio'; -export { init as initRegularPrice } from './regular-price'; -export { init as initSalePrice } from './sale-price'; -export { init as initScheduleSale } from './schedule-sale'; -export { init as initSection } from './section'; -export { init as initShippingClass } from './shipping-class'; -export { init as initShippingDimensions } from './shipping-dimensions'; -export { init as 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'; -export { init as initVariations } from './variations'; -export { init as initRequirePassword } from './password'; -export { init as initVariationItems } from './variation-items'; -export { init as initVariationOptions } from './variation-options'; -export { init as initNotice } from './notice'; -export { init as initTaxonomy } from './taxonomy'; +export { init as initCatalogVisibility } from './product-fields/catalog-visibility'; +export { init as initCheckbox } from './generic/checkbox'; +export { init as initCollapsible } from './generic/collapsible'; +export { init as initConditional } from './generic/conditional'; +export { init as initDescription } from './product-fields/description'; +export { init as initDownloads } from './product-fields/downloads'; +export { init as initImages } from './product-fields/images'; +export { init as initLowStockQty } from './product-fields/inventory-email'; +export { init as initSku } from './product-fields/inventory-sku'; +export { init as initName } from './product-fields/name'; +export { init as initPricing } from './generic/pricing'; +export { init as initRadio } from './generic/radio'; +export { init as initRegularPrice } from './product-fields/regular-price'; +export { init as initSalePrice } from './product-fields/sale-price'; +export { init as initScheduleSale } from './product-fields/schedule-sale'; +export { init as initSection } from './generic/section'; +export { init as initShippingClass } from './product-fields/shipping-class'; +export { init as initShippingDimensions } from './product-fields/shipping-dimensions'; +export { init as initSummary } from './product-fields/summary'; +export { init as initTab } from './generic/tab'; +export { init as initTag } from './product-fields/tag'; +export { init as initInventoryQuantity } from './product-fields/inventory-quantity'; +export { init as initToggle } from './generic/toggle'; +export { init as attributesInit } from './product-fields/attributes'; +export { init as initVariations } from './product-fields/variations'; +export { init as initRequirePassword } from './product-fields/password'; +export { init as initVariationItems } from './product-fields/variation-items'; +export { init as initVariationOptions } from './product-fields/variation-options'; +export { init as initNotice } from './product-fields/notice'; +export { init as initTaxonomy } from './generic/taxonomy'; +export { init as initText } from './generic/text'; +export { init as initNumber } from './generic/number'; diff --git a/packages/js/product-editor/src/blocks/inventory-email/index.ts b/packages/js/product-editor/src/blocks/inventory-email/index.ts deleted file mode 100644 index db814b62b9e..00000000000 --- a/packages/js/product-editor/src/blocks/inventory-email/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { InventoryEmailBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< InventoryEmailBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< - BlockConfiguration< InventoryEmailBlockAttributes > -> = { - example: {}, - edit: Edit, -}; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/inventory-quantity/index.ts b/packages/js/product-editor/src/blocks/inventory-quantity/index.ts deleted file mode 100644 index 98fb0c23aad..00000000000 --- a/packages/js/product-editor/src/blocks/inventory-quantity/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { TrackInventoryBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< TrackInventoryBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< - BlockConfiguration< TrackInventoryBlockAttributes > -> = { - example: {}, - edit: Edit, -}; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/inventory-sku/index.ts b/packages/js/product-editor/src/blocks/inventory-sku/index.ts deleted file mode 100644 index 6b7a2f17c22..00000000000 --- a/packages/js/product-editor/src/blocks/inventory-sku/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; - -const { name, ...metadata } = blockConfiguration as BlockConfiguration; - -export { metadata, name }; - -export const settings = { - example: {}, - edit: Edit, -}; - -export const init = () => registerWooBlockType( { name, metadata, settings } ); diff --git a/packages/js/product-editor/src/blocks/notice/index.ts b/packages/js/product-editor/src/blocks/notice/index.ts deleted file mode 100644 index 72691b07798..00000000000 --- a/packages/js/product-editor/src/blocks/notice/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit, NoticeBlockAttributes } from './edit'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< NoticeBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< BlockConfiguration< NoticeBlockAttributes > > = - { - example: {}, - edit: Edit, - }; - -export function init() { - registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/password/index.ts b/packages/js/product-editor/src/blocks/password/index.ts deleted file mode 100644 index a25088682e6..00000000000 --- a/packages/js/product-editor/src/blocks/password/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { RequirePasswordBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< RequirePasswordBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< - BlockConfiguration< RequirePasswordBlockAttributes > -> = { - edit: Edit, -}; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/attributes/block.json b/packages/js/product-editor/src/blocks/product-fields/attributes/block.json similarity index 100% rename from packages/js/product-editor/src/blocks/attributes/block.json rename to packages/js/product-editor/src/blocks/product-fields/attributes/block.json diff --git a/packages/js/product-editor/src/blocks/attributes/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/attributes/edit.tsx similarity index 74% rename from packages/js/product-editor/src/blocks/attributes/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/attributes/edit.tsx index 044777a0d90..963cea94de4 100644 --- a/packages/js/product-editor/src/blocks/attributes/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/attributes/edit.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { BlockEditProps, BlockAttributes } from '@wordpress/blocks'; +import { BlockAttributes } from '@wordpress/blocks'; import { createElement } from '@wordpress/element'; import { useWooBlockProps } from '@woocommerce/block-templates'; import { ProductAttribute } from '@woocommerce/data'; @@ -13,9 +13,12 @@ import { useEntityProp, useEntityId } from '@wordpress/core-data'; /** * Internal dependencies */ -import { Attributes as AttributesContainer } from '../../components/attributes/attributes'; +import { Attributes as AttributesContainer } from '../../../components/attributes/attributes'; +import { ProductEditorBlockEditProps } from '../../../types'; -export function Edit( { attributes }: BlockEditProps< BlockAttributes > ) { +export function Edit( { + attributes, +}: ProductEditorBlockEditProps< BlockAttributes > ) { const [ entityAttributes, setEntityAttributes ] = useEntityProp< ProductAttribute[] >( 'postType', 'product', 'attributes' ); diff --git a/packages/js/product-editor/src/blocks/attributes/editor.scss b/packages/js/product-editor/src/blocks/product-fields/attributes/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/attributes/editor.scss rename to packages/js/product-editor/src/blocks/product-fields/attributes/editor.scss diff --git a/packages/js/product-editor/src/blocks/images/index.ts b/packages/js/product-editor/src/blocks/product-fields/attributes/index.ts similarity index 93% rename from packages/js/product-editor/src/blocks/images/index.ts rename to packages/js/product-editor/src/blocks/product-fields/attributes/index.ts index 37f321821db..5322eeb221e 100644 --- a/packages/js/product-editor/src/blocks/images/index.ts +++ b/packages/js/product-editor/src/blocks/product-fields/attributes/index.ts @@ -22,5 +22,5 @@ export const init = () => registerWooBlockType( { name, metadata: metadata as never, - settings, + settings: settings as never, } ); diff --git a/packages/js/product-editor/src/blocks/catalog-visibility/block.json b/packages/js/product-editor/src/blocks/product-fields/catalog-visibility/block.json similarity index 94% rename from packages/js/product-editor/src/blocks/catalog-visibility/block.json rename to packages/js/product-editor/src/blocks/product-fields/catalog-visibility/block.json index 4ccc7f89192..cd9aecb69e2 100644 --- a/packages/js/product-editor/src/blocks/catalog-visibility/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/catalog-visibility/block.json @@ -26,6 +26,5 @@ "inserter": false, "lock": false, "__experimentalToolbar": false - }, - "editorStyle": "file:./editor.css" + } } diff --git a/packages/js/product-editor/src/blocks/catalog-visibility/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/catalog-visibility/edit.tsx similarity index 91% rename from packages/js/product-editor/src/blocks/catalog-visibility/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/catalog-visibility/edit.tsx index 7ec4e7a30d9..a72fc435e60 100644 --- a/packages/js/product-editor/src/blocks/catalog-visibility/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/catalog-visibility/edit.tsx @@ -11,12 +11,11 @@ import { Product } from '@woocommerce/data'; * Internal dependencies */ import { CatalogVisibilityBlockAttributes } from './types'; +import { ProductEditorBlockEditProps } from '../../../types'; export function Edit( { attributes, -}: { - attributes: CatalogVisibilityBlockAttributes; -} ) { +}: ProductEditorBlockEditProps< CatalogVisibilityBlockAttributes > ) { const { label, visibility } = attributes; const blockProps = useWooBlockProps( attributes ); diff --git a/packages/js/product-editor/src/blocks/product-fields/catalog-visibility/index.ts b/packages/js/product-editor/src/blocks/product-fields/catalog-visibility/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/catalog-visibility/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/catalog-visibility/types.ts b/packages/js/product-editor/src/blocks/product-fields/catalog-visibility/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/catalog-visibility/types.ts rename to packages/js/product-editor/src/blocks/product-fields/catalog-visibility/types.ts diff --git a/packages/js/product-editor/src/blocks/description/block.json b/packages/js/product-editor/src/blocks/product-fields/description/block.json similarity index 100% rename from packages/js/product-editor/src/blocks/description/block.json rename to packages/js/product-editor/src/blocks/product-fields/description/block.json diff --git a/packages/js/product-editor/src/blocks/description/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/description/edit.tsx similarity index 87% rename from packages/js/product-editor/src/blocks/description/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/description/edit.tsx index 769d713f9c7..dc2e3e0e1e6 100644 --- a/packages/js/product-editor/src/blocks/description/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/description/edit.tsx @@ -4,7 +4,6 @@ import { __ } from '@wordpress/i18n'; import { createElement, useState } from '@wordpress/element'; import { - BlockEditProps, BlockAttributes, BlockInstance, parse, @@ -18,8 +17,9 @@ import { useEntityProp } from '@wordpress/core-data'; /** * Internal dependencies */ -import { ContentPreview } from '../../components/content-preview'; -import { ModalEditor } from '../../components/modal-editor'; +import { ContentPreview } from '../../../components/content-preview'; +import { ModalEditor } from '../../../components/modal-editor'; +import { ProductEditorBlockEditProps } from '../../../types'; /** * Internal dependencies @@ -45,7 +45,9 @@ function clearDescriptionIfEmpty( blocks: BlockInstance[] ) { return blocks; } -export function Edit( { attributes }: BlockEditProps< BlockAttributes > ) { +export function Edit( { + attributes, +}: ProductEditorBlockEditProps< BlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const [ isModalOpen, setIsModalOpen ] = useState( false ); const [ description, setDescription ] = useEntityProp< string >( diff --git a/packages/js/product-editor/src/blocks/product-fields/description/index.ts b/packages/js/product-editor/src/blocks/product-fields/description/index.ts new file mode 100644 index 00000000000..d670004d84d --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/description/index.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => + registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/block.json b/packages/js/product-editor/src/blocks/product-fields/downloads/block.json new file mode 100644 index 00000000000..e96d0bea93f --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/block.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "woocommerce/product-downloads-field", + "title": "Product downloads", + "category": "widgets", + "description": "The product downloads.", + "keywords": [ "products", "downloads" ], + "textdomain": "default", + "attributes": { + "name": { + "type": "string", + "__experimentalRole": "content" + } + }, + "supports": { + "align": false, + "html": false, + "multiple": false, + "reusable": false, + "inserter": false, + "lock": false, + "__experimentalToolbar": false + }, + "editorStyle": "file:./editor.css" +} diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/downloads-menu.tsx b/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/downloads-menu.tsx new file mode 100644 index 00000000000..00a37c0d65f --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/downloads-menu.tsx @@ -0,0 +1,71 @@ +/** + * External dependencies + */ +import { Button, Dropdown, MenuGroup } from '@wordpress/components'; +import { createElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { chevronDown, chevronUp } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { DownloadsMenuProps } from './types'; +import { UploadFilesMenuItem } from '../upload-files-menu-item'; +import { MediaLibraryMenuItem } from '../media-library-menu-item'; +import { InsertUrlMenuItem } from '../insert-url-menu-item'; + +export function DownloadsMenu( { + allowedTypes, + maxUploadFileSize, + onUploadSuccess, + onUploadError, +}: DownloadsMenuProps ) { + return ( + ( + + ) } + renderContent={ ( { onClose } ) => ( +
+ + { + onUploadSuccess( files ); + onClose(); + } } + onUploadError={ onUploadError } + /> + + { + onUploadSuccess( files ); + onClose(); + } } + /> + + { + onUploadSuccess( files ); + onClose(); + } } + onUploadError={ onUploadError } + /> + +
+ ) } + /> + ); +} diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/index.ts b/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/index.ts new file mode 100644 index 00000000000..0e59caf644f --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/index.ts @@ -0,0 +1,2 @@ +export * from './downloads-menu'; +export * from './types'; diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/style.scss b/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/style.scss new file mode 100644 index 00000000000..fe56bd2c248 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/style.scss @@ -0,0 +1,15 @@ +.woocommerce-downloads-menu { + &__toogle { + flex-direction: row-reverse; + + > span { + margin: 0 6px; + } + } + + &__menu-content { + .components-menu-item__item { + min-width: 172px; + } + } +} diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/types.ts b/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/types.ts new file mode 100644 index 00000000000..b7bd2323026 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/types.ts @@ -0,0 +1,11 @@ +/** + * External dependencies + */ +import { MediaItem } from '@wordpress/media-utils'; + +export type DownloadsMenuProps = { + allowedTypes?: string[]; + maxUploadFileSize?: number; + onUploadSuccess( files: MediaItem[] ): void; + onUploadError( error: unknown ): void; +}; diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/downloads/edit.tsx new file mode 100644 index 00000000000..10d7773e1b1 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/edit.tsx @@ -0,0 +1,242 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { BlockEditProps } from '@wordpress/blocks'; +import { Button, Spinner } from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { + createElement, + Fragment, + createInterpolateElement, +} from '@wordpress/element'; +import { closeSmall } from '@wordpress/icons'; +import { MediaItem } from '@wordpress/media-utils'; +import { useWooBlockProps } from '@woocommerce/block-templates'; +import { ListItem, MediaUploader, Sortable } from '@woocommerce/components'; +import { Product, ProductDownload } from '@woocommerce/data'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { useEntityProp } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { UploadsBlockAttributes } from './types'; +import { UploadImage } from './upload-image'; +import { DownloadsMenu } from './downloads-menu'; + +function getFileName( url?: string ) { + const [ name ] = url?.split( '/' ).reverse() ?? []; + return name; +} + +function stringifyId< ID >( id?: ID ): string { + return id ? String( id ) : ''; +} + +function stringifyEntityId< ID, T extends { id?: ID } >( entity: T ): T { + return { ...entity, id: stringifyId( entity.id ) }; +} + +export function Edit( { + attributes, +}: BlockEditProps< UploadsBlockAttributes > ) { + const blockProps = useWooBlockProps( attributes ); + const [ , setDownloadable ] = useEntityProp< Product[ 'downloadable' ] >( + 'postType', + 'product', + 'downloadable' + ); + const [ downloads, setDownloads ] = useEntityProp< Product[ 'downloads' ] >( + 'postType', + 'product', + 'downloads' + ); + + const { allowedMimeTypes } = useSelect( ( select ) => { + const { getEditorSettings } = select( 'core/editor' ); + return getEditorSettings(); + } ); + + const allowedTypes = allowedMimeTypes + ? Object.values( allowedMimeTypes ) + : []; + + const { createErrorNotice } = useDispatch( 'core/notices' ); + + function handleFileUpload( files: MediaItem | MediaItem[] ) { + if ( ! Array.isArray( files ) ) return; + + const newFiles = files.filter( + ( file ) => + ! downloads.some( ( download ) => download.file === file.url ) + ); + + if ( newFiles.length !== files.length ) { + createErrorNotice( + files.length === 1 + ? __( 'This file has already been added', 'woocommerce' ) + : __( + 'Some of these files have already been added', + 'woocommerce' + ) + ); + } + + if ( newFiles.length ) { + if ( ! downloads.length ) { + setDownloadable( true ); + } + + const uploadedFiles = newFiles.map( ( file ) => ( { + id: stringifyId( file.id ), + file: file.url, + name: + file.title || + file.alt || + file.caption || + getFileName( file.url ), + } ) ); + + const stringifyIds = downloads.map( stringifyEntityId ); + + stringifyIds.push( ...uploadedFiles ); + + setDownloads( stringifyIds ); + } + } + + function removeHandler( download: ProductDownload ) { + return function handleRemoveClick() { + const otherDownloads = downloads.reduce< ProductDownload[] >( + function removeDownload( others, current ) { + if ( current.file === download.file ) { + return others; + } + return [ ...others, stringifyEntityId( current ) ]; + }, + [] + ); + + if ( ! otherDownloads.length ) { + setDownloadable( false ); + } + + setDownloads( otherDownloads ); + }; + } + + function handleUploadError( error: unknown ) { + createErrorNotice( + typeof error === 'string' + ? error + : __( 'There was an error uploading files', 'woocommerce' ) + ); + } + + return ( +
+
+ +
+ +
+ + +

+ { createInterpolateElement( + __( + 'Supported file types: and more. View all', + 'woocommerce' + ), + { + Types: ( + + PNG, JPG, PDF, PPT, DOC, + MP3, MP4 + + ), + link: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + event.stopPropagation() + } + /> + ), + } + ) } +

+
+ ) : ( + '' + ) + } + buttonText="" + allowedMediaTypes={ allowedTypes } + multipleSelect={ 'add' } + onUpload={ handleFileUpload } + onFileUploadChange={ handleFileUpload } + onError={ handleUploadError } + /> + + { Boolean( downloads.length ) && ( + + { downloads.map( ( download ) => { + const nameFromUrl = getFileName( download.file ); + const isUploading = + download.file.startsWith( 'blob' ); + + return ( + +
+ { download.name } + { download.name !== nameFromUrl && ( + + { nameFromUrl } + + ) } +
+ +
+ { isUploading && ( + + ) } +
+
+ ); + } ) } +
+ ) } +
+
+ ); +} diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/editor.scss b/packages/js/product-editor/src/blocks/product-fields/downloads/editor.scss new file mode 100644 index 00000000000..74275f12a22 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/editor.scss @@ -0,0 +1,86 @@ +@import "./downloads-menu/style.scss"; +@import "./media-library-menu-item/style.scss"; +@import "./insert-url-menu-item/style.scss"; + +$fixed-section-height: 224px; + +.wp-block-woocommerce-product-downloads-field { + &__header { + display: flex; + justify-content: flex-end; + margin-bottom: $grid-unit-30; + } + + &__body { + position: relative; + } + + &__table { + height: $fixed-section-height; + overflow: auto; + margin: 0; + flex: 1 0 auto; + } + + &__table-filename { + display: flex; + flex-direction: column; + gap: 2px; + } + + &__table-filename-description { + color: $gray-700; + } + + &__table-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: $grid-unit; + padding-right: 2px; + + .components-button.has-icon { + color: $gray-700; + width: $grid-unit-30; + height: $grid-unit-30; + min-width: inherit; + padding: 0; + } + } + + &__drop-zone-content { + border: 1px dashed $gray-400; + height: $fixed-section-height; + border-radius: 2px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + gap: $grid-unit-30; + } + + &__drop-zone-label { + color: $gray-700; + font-weight: normal; + margin: 0; + } + + .woocommerce-sortable { + &__item .woocommerce-list-item { + display: grid; + grid-template-columns: auto 88px; + padding: 0; + min-height: calc($grid-unit-80 + $grid-unit-05); + border: none; + } + + &__item:not(:last-child) .woocommerce-list-item { + border-bottom: 1px solid $gray-200; + } + + &__handle { + display: none; + } + } +} diff --git a/packages/js/product-editor/src/blocks/pricing/index.ts b/packages/js/product-editor/src/blocks/product-fields/downloads/index.ts similarity index 72% rename from packages/js/product-editor/src/blocks/pricing/index.ts rename to packages/js/product-editor/src/blocks/product-fields/downloads/index.ts index 4d22a025cd8..8cbd59a1d06 100644 --- a/packages/js/product-editor/src/blocks/pricing/index.ts +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/index.ts @@ -9,14 +9,14 @@ import { registerWooBlockType } from '@woocommerce/block-templates'; */ import blockConfiguration from './block.json'; import { Edit } from './edit'; -import { PricingBlockAttributes } from './types'; +import { UploadsBlockAttributes } from './types'; const { name, ...metadata } = - blockConfiguration as BlockConfiguration< PricingBlockAttributes >; + blockConfiguration as BlockConfiguration< UploadsBlockAttributes >; export { metadata, name }; -export const settings: Partial< BlockConfiguration< PricingBlockAttributes > > = +export const settings: Partial< BlockConfiguration< UploadsBlockAttributes > > = { example: {}, edit: Edit, diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/insert-url-menu-item/index.ts b/packages/js/product-editor/src/blocks/product-fields/downloads/insert-url-menu-item/index.ts new file mode 100644 index 00000000000..5451dae9cc7 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/insert-url-menu-item/index.ts @@ -0,0 +1,2 @@ +export * from './insert-url-menu-item'; +export * from './types'; diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/insert-url-menu-item/insert-url-menu-item.tsx b/packages/js/product-editor/src/blocks/product-fields/downloads/insert-url-menu-item/insert-url-menu-item.tsx new file mode 100644 index 00000000000..458880ff707 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/insert-url-menu-item/insert-url-menu-item.tsx @@ -0,0 +1,110 @@ +/** + * External dependencies + */ +import { FocusEvent, FormEvent } from 'react'; +import { createElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { customLink, keyboardReturn } from '@wordpress/icons'; +import { MediaItem } from '@wordpress/media-utils'; +import { + Button, + Dropdown, + MenuItem, + // @ts-expect-error `__experimentalInputControl` does exist. + __experimentalInputControl as InputControl, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { InsertUrlMenuItemProps } from './types'; + +function validateInput( input: HTMLInputElement ) { + input.required = true; + input.setCustomValidity( '' ); + + if ( input.validity.valueMissing ) { + input.setCustomValidity( __( 'The URL is required', 'woocommerce' ) ); + } + + if ( input.validity.typeMismatch ) { + input.setCustomValidity( __( 'Insert a valid URL', 'woocommerce' ) ); + } +} + +export function InsertUrlMenuItem( { + onUploadSuccess, + onUploadError, +}: InsertUrlMenuItemProps ) { + function handleSubmit( event: FormEvent< HTMLFormElement > ) { + event.preventDefault(); + + const form = event.currentTarget; + + const urlInput: HTMLInputElement = form.url; + validateInput( urlInput ); + + if ( form.checkValidity() ) { + const url = form.url.value; + const mediaItem = { + url, + } as MediaItem; + + onUploadSuccess( [ mediaItem ] ); + } else { + onUploadError( urlInput.validationMessage ); + } + } + + function handleInput( event: FormEvent< HTMLInputElement > ) { + const urlInput = event.target as HTMLInputElement; + + validateInput( urlInput ); + } + + function handleBlur( event: FocusEvent< HTMLInputElement > ) { + const urlInput = event.target; + + validateInput( urlInput ); + } + + return ( + ( + + { __( 'Insert from URL', 'woocommerce' ) } + + ) } + renderContent={ () => ( +
+ + } + className="woocommerce-inert-url-menu-item__input" + aria-label={ __( 'Insert URL', 'woocommerce' ) } + onInput={ handleInput } + onBlur={ handleBlur } + /> + + ) } + /> + ); +} diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/insert-url-menu-item/style.scss b/packages/js/product-editor/src/blocks/product-fields/downloads/insert-url-menu-item/style.scss new file mode 100644 index 00000000000..d0d456e0b16 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/insert-url-menu-item/style.scss @@ -0,0 +1,13 @@ +.woocommerce-inert-url-menu-item { + &__input { + min-width: 300px; + + .components-base-control__field + .components-input-control__container + > .components-input-control__input:invalid + + .components-input-control__suffix + + .components-input-control__backdrop { + border-color: red; + } + } +} diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/insert-url-menu-item/types.ts b/packages/js/product-editor/src/blocks/product-fields/downloads/insert-url-menu-item/types.ts new file mode 100644 index 00000000000..72c94fff28a --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/insert-url-menu-item/types.ts @@ -0,0 +1,9 @@ +/** + * External dependencies + */ +import { MediaItem } from '@wordpress/media-utils'; + +export type InsertUrlMenuItemProps = { + onUploadSuccess( files: MediaItem[] ): void; + onUploadError( error: unknown ): void; +}; diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/media-library-menu-item/index.ts b/packages/js/product-editor/src/blocks/product-fields/downloads/media-library-menu-item/index.ts new file mode 100644 index 00000000000..154d679d17a --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/media-library-menu-item/index.ts @@ -0,0 +1,2 @@ +export * from './media-library-menu-item'; +export * from './types'; diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/media-library-menu-item/media-library-menu-item.tsx b/packages/js/product-editor/src/blocks/product-fields/downloads/media-library-menu-item/media-library-menu-item.tsx new file mode 100644 index 00000000000..80dd80a6426 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/media-library-menu-item/media-library-menu-item.tsx @@ -0,0 +1,72 @@ +/** + * External dependencies + */ + +import { MenuItem } from '@wordpress/components'; +import { createElement, useEffect, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { media } from '@wordpress/icons'; +import { MediaItem, MediaUpload } from '@wordpress/media-utils'; + +/** + * Internal dependencies + */ +import { MediaLibraryMenuItemProps } from './types'; + +const MODAL_CLASS_NAME = + 'woocommerce-media-library-menu-item__upload_files_modal'; +const MODAL_WRAPPER_CLASS_NAME = + 'woocommerce-media-library-menu-item__upload_files_modal_wrapper'; + +export function MediaLibraryMenuItem( { + allowedTypes, + onUploadSuccess, +}: MediaLibraryMenuItemProps ) { + const [ uploadFilesModalOpen, setUploadFilesModalOpen ] = useState( false ); + + useEffect( + function addUploadModalClass() { + const modal = document.querySelector( `.${ MODAL_CLASS_NAME }` ); + const dialog = modal?.closest( '[role="dialog"]' ); + const wrapper = dialog?.parentElement; + + wrapper?.classList.add( MODAL_WRAPPER_CLASS_NAME ); + + return () => { + wrapper?.classList.remove( MODAL_WRAPPER_CLASS_NAME ); + }; + }, + [ uploadFilesModalOpen ] + ); + + function handleMediaUploadSelect( value: unknown ) { + onUploadSuccess( value as MediaItem[] ); + } + + function uploadFilesClickHandler( openMediaUploadModal: () => void ) { + return function handleUploadFilesClick() { + openMediaUploadModal(); + setUploadFilesModalOpen( true ); + }; + } + + return ( + ( + + { __( 'Media Library', 'woocommerce' ) } + + ) } + /> + ); +} diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/media-library-menu-item/style.scss b/packages/js/product-editor/src/blocks/product-fields/downloads/media-library-menu-item/style.scss new file mode 100644 index 00000000000..940bed87edd --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/media-library-menu-item/style.scss @@ -0,0 +1,14 @@ +$media-modal-z-index: calc(z-index(".components-popover") + 10000); +$media-modal-backdrop-z-index: calc($media-modal-z-index - 1000); + +.woocommerce-media-library-menu-item { + &__upload_files_modal_wrapper { + .media-modal { + z-index: $media-modal-z-index; + } + + .media-modal-backdrop { + z-index: $media-modal-backdrop-z-index; + } + } +} diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/media-library-menu-item/types.ts b/packages/js/product-editor/src/blocks/product-fields/downloads/media-library-menu-item/types.ts new file mode 100644 index 00000000000..2db2e75899f --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/media-library-menu-item/types.ts @@ -0,0 +1,9 @@ +/** + * External dependencies + */ +import { MediaItem } from '@wordpress/media-utils'; + +export type MediaLibraryMenuItemProps = { + allowedTypes?: string[]; + onUploadSuccess( files: MediaItem[] ): void; +}; diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/types.ts b/packages/js/product-editor/src/blocks/product-fields/downloads/types.ts new file mode 100644 index 00000000000..04f462d8372 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/types.ts @@ -0,0 +1,15 @@ +/** + * External dependencies + */ +import { ProductDownload } from '@woocommerce/data'; +import { BlockAttributes } from '@wordpress/blocks'; + +export interface UploadsBlockAttributes extends BlockAttributes { + name: string; +} + +export type DownloadableFileItem = { + key: string; + download: ProductDownload; + uploading?: boolean; +}; diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/upload-files-menu-item/index.ts b/packages/js/product-editor/src/blocks/product-fields/downloads/upload-files-menu-item/index.ts new file mode 100644 index 00000000000..022460cde79 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/upload-files-menu-item/index.ts @@ -0,0 +1,2 @@ +export * from './upload-files-menu-item'; +export * from './types'; diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/upload-files-menu-item/types.ts b/packages/js/product-editor/src/blocks/product-fields/downloads/upload-files-menu-item/types.ts new file mode 100644 index 00000000000..bab34bb3598 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/upload-files-menu-item/types.ts @@ -0,0 +1,11 @@ +/** + * External dependencies + */ +import { MediaItem } from '@wordpress/media-utils'; + +export type UploadFilesMenuItemProps = { + allowedTypes?: string[]; + maxUploadFileSize?: number; + onUploadSuccess( files: MediaItem[] ): void; + onUploadError( error: unknown ): void; +}; diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/upload-files-menu-item/upload-files-menu-item.tsx b/packages/js/product-editor/src/blocks/product-fields/downloads/upload-files-menu-item/upload-files-menu-item.tsx new file mode 100644 index 00000000000..788c34024eb --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/upload-files-menu-item/upload-files-menu-item.tsx @@ -0,0 +1,55 @@ +/** + * External dependencies + */ +import { ChangeEvent } from 'react'; +import { FormFileUpload, MenuItem } from '@wordpress/components'; +import { createElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { upload } from '@wordpress/icons'; +import { uploadMedia } from '@wordpress/media-utils'; + +/** + * Internal dependencies + */ +import { UploadFilesMenuItemProps } from './types'; + +export function UploadFilesMenuItem( { + allowedTypes, + maxUploadFileSize = 10000000, + onUploadSuccess, + onUploadError, +}: UploadFilesMenuItemProps ) { + function handleFormFileUploadChange( + event: ChangeEvent< HTMLInputElement > + ) { + const filesList = event.currentTarget.files as FileList; + + uploadMedia( { + allowedTypes, + filesList, + maxUploadFileSize, + onFileChange: onUploadSuccess, + onError: onUploadError, + } ); + } + + return ( + ( + + { __( 'Upload', 'woocommerce' ) } + + ) } + /> + ); +} diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/upload-image.tsx b/packages/js/product-editor/src/blocks/product-fields/downloads/upload-image.tsx new file mode 100644 index 00000000000..35ad6ca0289 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/upload-image.tsx @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; + +export function UploadImage() { + return ( + + + + ); +} diff --git a/packages/js/product-editor/src/blocks/images/block.json b/packages/js/product-editor/src/blocks/product-fields/images/block.json similarity index 80% rename from packages/js/product-editor/src/blocks/images/block.json rename to packages/js/product-editor/src/blocks/product-fields/images/block.json index 758ab2d45a3..e36cb7c5da4 100644 --- a/packages/js/product-editor/src/blocks/images/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/images/block.json @@ -12,6 +12,13 @@ "type": "number", "__experimentalRole": "content" }, + "property": { + "type": "string" + }, + "multiple": { + "type": "boolean", + "default": true + }, "images": { "__experimentalRole": "content", "type": "array", @@ -30,5 +37,6 @@ "lock": false, "__experimentalToolbar": false }, - "editorStyle": "file:./editor.css" + "editorStyle": "file:./editor.css", + "usesContext": [ "postType" ] } diff --git a/packages/js/product-editor/src/blocks/product-fields/images/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/images/edit.tsx new file mode 100644 index 00000000000..3e36af49eed --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/images/edit.tsx @@ -0,0 +1,269 @@ +/** + * External dependencies + */ +import { DragEvent } from 'react'; +import { __ } from '@wordpress/i18n'; +import { BlockAttributes } from '@wordpress/blocks'; +import { DropZone } from '@wordpress/components'; +import classnames from 'classnames'; +import { createElement, useState } from '@wordpress/element'; +import { Icon, trash } from '@wordpress/icons'; +import { MediaItem } from '@wordpress/media-utils'; +import { useWooBlockProps } from '@woocommerce/block-templates'; +import { + MediaUploader, + ImageGallery, + ImageGalleryItem, +} from '@woocommerce/components'; +import { recordEvent } from '@woocommerce/tracks'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { useEntityProp } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { ProductEditorBlockEditProps } from '../../../types'; + +type UploadImage = { + id?: number; +} & Record< string, string >; + +export interface Image { + id: number; + src: string; + name: string; + alt: string; +} + +function mapUploadImageToImage( upload: UploadImage ): Image | null { + if ( ! upload.id ) return null; + return { + id: upload.id, + name: upload.title, + src: upload.url, + alt: upload.alt, + }; +} + +export function Edit( { + attributes, + context, +}: ProductEditorBlockEditProps< BlockAttributes > ) { + const { property, multiple } = attributes; + const [ propertyValue, setPropertyValue ] = useEntityProp< + Image | Image[] | null + >( 'postType', context.postType, property ); + const [ isRemovingZoneVisible, setIsRemovingZoneVisible ] = + useState< boolean >( false ); + const [ isRemoving, setIsRemoving ] = useState< boolean >( false ); + const [ draggedImageId, setDraggedImageId ] = useState< number | null >( + null + ); + + const blockProps = useWooBlockProps( attributes, { + className: classnames( { + 'has-images': Array.isArray( propertyValue ) + ? propertyValue.length > 0 + : Boolean( propertyValue ), + } ), + } ); + + function orderImages( newOrder: JSX.Element[] ) { + if ( Array.isArray( propertyValue ) ) { + const memoIds = propertyValue.reduce< Record< string, Image > >( + ( current, item ) => ( { + ...current, + [ `${ item.id }` ]: item, + } ), + {} + ); + const orderedImages = newOrder + .filter( ( image ) => image?.props?.id in memoIds ) + .map( ( image ) => memoIds[ image?.props?.id ] ); + + recordEvent( + 'product_images_change_image_order_via_image_gallery' + ); + setPropertyValue( orderedImages ); + } + } + + function uploadHandler( eventName: string ) { + return function handleFileUpload( upload: MediaItem | MediaItem[] ) { + recordEvent( eventName ); + + if ( Array.isArray( upload ) ) { + const images: Image[] = upload + .filter( ( image ) => image.id ) + .map( ( image ) => ( { + id: image.id, + name: image.title, + src: image.url, + alt: image.alt, + } ) ); + if ( upload[ 0 ]?.id ) { + setPropertyValue( [ + ...( propertyValue as Image[] ), + ...images, + ] ); + } + } else if ( upload.id ) { + setPropertyValue( mapUploadImageToImage( upload ) ); + } + }; + } + + function handleSelect( selection: UploadImage | UploadImage[] ) { + recordEvent( 'product_images_add_via_media_library' ); + + if ( Array.isArray( selection ) ) { + const images = selection + .map( mapUploadImageToImage ) + .filter( ( image ) => image !== null ); + + setPropertyValue( images as Image[] ); + } else { + setPropertyValue( mapUploadImageToImage( selection ) ); + } + } + + function handleDragStart( event: DragEvent< HTMLDivElement > ) { + if ( Array.isArray( propertyValue ) ) { + const { id: imageId, dataset } = event.target as HTMLElement; + if ( imageId ) { + setDraggedImageId( parseInt( imageId, 10 ) ); + } else if ( dataset?.index ) { + const index = parseInt( dataset.index, 10 ); + setDraggedImageId( propertyValue[ index ]?.id ?? null ); + } + setIsRemovingZoneVisible( ( current ) => ! current ); + } + } + + function handleDragEnd() { + if ( Array.isArray( propertyValue ) ) { + if ( isRemoving && draggedImageId ) { + recordEvent( 'product_images_remove_image_button_click' ); + setPropertyValue( + propertyValue.filter( ( img ) => img.id !== draggedImageId ) + ); + setIsRemoving( false ); + setDraggedImageId( null ); + } + setIsRemovingZoneVisible( ( current ) => ! current ); + } + } + + function handleReplace( { + replaceIndex, + media, + }: { + replaceIndex: number; + media: UploadImage; + } ) { + recordEvent( 'product_images_replace_image_button_click' ); + + if ( + Array.isArray( propertyValue ) && + ! propertyValue.some( ( img ) => media.id === img.id ) + ) { + const image = mapUploadImageToImage( media ); + if ( image ) { + const newImages = [ ...propertyValue ]; + newImages[ replaceIndex ] = image; + setPropertyValue( newImages ); + } + } else { + setPropertyValue( mapUploadImageToImage( media ) ); + } + } + + function handleRemove( { removedItem }: { removedItem: JSX.Element } ) { + recordEvent( 'product_images_remove_image_button_click' ); + + if ( Array.isArray( propertyValue ) ) { + const remainingImages = propertyValue.filter( + ( image ) => image.id === removedItem.props.id + ); + setPropertyValue( remainingImages ); + } else { + setPropertyValue( null ); + } + } + + return ( +
+
+ { isRemovingZoneVisible ? ( +
+ + + { __( 'Drop here to remove', 'woocommerce' ) } + + setIsRemoving( true ) } + onDrop={ () => setIsRemoving( true ) } + label={ __( 'Drop here to remove', 'woocommerce' ) } + /> +
+ ) : ( + id ) + : propertyValue?.id ?? undefined + } + multipleSelect={ multiple ? 'add' : false } + onError={ () => null } + onFileUploadChange={ uploadHandler( + 'product_images_add_via_file_upload_area' + ) } + onMediaGalleryOpen={ () => { + recordEvent( 'product_images_media_gallery_open' ); + } } + onSelect={ handleSelect } + onUpload={ uploadHandler( + 'product_images_add_via_drag_and_drop_upload' + ) } + label={ '' } + buttonText={ __( 'Choose an image', 'woocommerce' ) } + /> + ) } +
+ { propertyValue !== null && propertyValue !== undefined && ( + + recordEvent( + 'product_images_select_image_as_cover_button_click' + ) + } + > + { ( Array.isArray( propertyValue ) + ? propertyValue + : [ propertyValue ] + ).map( ( image, index ) => ( + + ) ) } + + ) } +
+ ); +} diff --git a/packages/js/product-editor/src/blocks/product-fields/images/editor.scss b/packages/js/product-editor/src/blocks/product-fields/images/editor.scss new file mode 100644 index 00000000000..dd257860981 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/images/editor.scss @@ -0,0 +1,30 @@ +.wp-block-woocommerce-product-images-field { + .woocommerce-media-uploader { + text-align: left; + + .components-drop-zone { + min-height: 222px; + } + } + + .woocommerce-media-uploader__label { + display: none; + } + + .woocommerce-sortable { + margin-top: 0; + padding: 0; + } + + &.has-images { + .woocommerce-image-gallery { + margin-top: $gap-largest; + } + } + + &:not(.has-images) { + .woocommerce-sortable { + display: none; + } + } +} diff --git a/packages/js/product-editor/src/blocks/product-fields/images/index.ts b/packages/js/product-editor/src/blocks/product-fields/images/index.ts new file mode 100644 index 00000000000..5322eeb221e --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/images/index.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { Edit } from './edit'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => + registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); diff --git a/packages/js/product-editor/src/blocks/inventory-email/block.json b/packages/js/product-editor/src/blocks/product-fields/inventory-email/block.json similarity index 100% rename from packages/js/product-editor/src/blocks/inventory-email/block.json rename to packages/js/product-editor/src/blocks/product-fields/inventory-email/block.json diff --git a/packages/js/product-editor/src/blocks/inventory-email/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/inventory-email/edit.tsx similarity index 93% rename from packages/js/product-editor/src/blocks/inventory-email/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/inventory-email/edit.tsx index 8c16acde552..ff03ea6f8eb 100644 --- a/packages/js/product-editor/src/blocks/inventory-email/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/inventory-email/edit.tsx @@ -11,7 +11,6 @@ import { createInterpolateElement, } from '@wordpress/element'; import { getSetting } from '@woocommerce/settings'; -import { BlockEditProps } from '@wordpress/blocks'; import { useInstanceId } from '@wordpress/compose'; import { BaseControl, @@ -26,13 +25,14 @@ import { useEntityProp } from '@wordpress/core-data'; /** * Internal dependencies */ -import { useValidation } from '../../contexts/validation-context'; +import { useValidation } from '../../../contexts/validation-context'; import { InventoryEmailBlockAttributes } from './types'; +import { ProductEditorBlockEditProps } from '../../../types'; export function Edit( { attributes, clientId, -}: BlockEditProps< InventoryEmailBlockAttributes > ) { +}: ProductEditorBlockEditProps< InventoryEmailBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const notifyLowStockAmount = getSetting( 'notifyLowStockAmount', 2 ); diff --git a/packages/js/product-editor/src/blocks/inventory-email/editor.scss b/packages/js/product-editor/src/blocks/product-fields/inventory-email/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/inventory-email/editor.scss rename to packages/js/product-editor/src/blocks/product-fields/inventory-email/editor.scss diff --git a/packages/js/product-editor/src/blocks/product-fields/inventory-email/index.ts b/packages/js/product-editor/src/blocks/product-fields/inventory-email/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/inventory-email/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/inventory-email/types.ts b/packages/js/product-editor/src/blocks/product-fields/inventory-email/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/inventory-email/types.ts rename to packages/js/product-editor/src/blocks/product-fields/inventory-email/types.ts diff --git a/packages/js/product-editor/src/blocks/inventory-quantity/block.json b/packages/js/product-editor/src/blocks/product-fields/inventory-quantity/block.json similarity index 94% rename from packages/js/product-editor/src/blocks/inventory-quantity/block.json rename to packages/js/product-editor/src/blocks/product-fields/inventory-quantity/block.json index d34c3a80d8c..eede24ab1a2 100644 --- a/packages/js/product-editor/src/blocks/inventory-quantity/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/inventory-quantity/block.json @@ -22,5 +22,5 @@ "lock": false, "__experimentalToolbar": false }, - "editorStyle": "file:./editor.css" + "usesContext": [ "postType" ] } diff --git a/packages/js/product-editor/src/blocks/inventory-quantity/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/inventory-quantity/edit.tsx similarity index 90% rename from packages/js/product-editor/src/blocks/inventory-quantity/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/inventory-quantity/edit.tsx index 8f3e81bee31..a3944ff93e7 100644 --- a/packages/js/product-editor/src/blocks/inventory-quantity/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/inventory-quantity/edit.tsx @@ -3,7 +3,6 @@ */ import { useWooBlockProps } from '@woocommerce/block-templates'; import { Product } from '@woocommerce/data'; -import { BlockEditProps } from '@wordpress/blocks'; import { useInstanceId } from '@wordpress/compose'; import { useEntityProp } from '@wordpress/core-data'; import { createElement, useEffect } from '@wordpress/element'; @@ -18,23 +17,25 @@ import { * Internal dependencies */ import { TrackInventoryBlockAttributes } from './types'; -import { useValidation } from '../../contexts/validation-context'; +import { useValidation } from '../../../contexts/validation-context'; +import { ProductEditorBlockEditProps } from '../../../types'; export function Edit( { attributes, clientId, -}: BlockEditProps< TrackInventoryBlockAttributes > ) { + context, +}: ProductEditorBlockEditProps< TrackInventoryBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const [ manageStock ] = useEntityProp< boolean >( 'postType', - 'product', + context.postType, 'manage_stock' ); const [ stockQuantity, setStockQuantity ] = useEntityProp< number | null >( 'postType', - 'product', + context.postType, 'stock_quantity' ); diff --git a/packages/js/product-editor/src/blocks/product-fields/inventory-quantity/index.ts b/packages/js/product-editor/src/blocks/product-fields/inventory-quantity/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/inventory-quantity/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/inventory-quantity/types.ts b/packages/js/product-editor/src/blocks/product-fields/inventory-quantity/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/inventory-quantity/types.ts rename to packages/js/product-editor/src/blocks/product-fields/inventory-quantity/types.ts diff --git a/packages/js/product-editor/src/blocks/inventory-sku/block.json b/packages/js/product-editor/src/blocks/product-fields/inventory-sku/block.json similarity index 88% rename from packages/js/product-editor/src/blocks/inventory-sku/block.json rename to packages/js/product-editor/src/blocks/product-fields/inventory-sku/block.json index 9a50ccdf140..45b120192a7 100644 --- a/packages/js/product-editor/src/blocks/inventory-sku/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/inventory-sku/block.json @@ -22,5 +22,6 @@ "lock": false, "__experimentalToolbar": false }, - "editorStyle": "file:./editor.css" + "editorStyle": "file:./editor.css", + "usesContext": [ "postType" ] } diff --git a/packages/js/product-editor/src/blocks/inventory-sku/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/inventory-sku/edit.tsx similarity index 78% rename from packages/js/product-editor/src/blocks/inventory-sku/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/inventory-sku/edit.tsx index 0ac680df75d..32aba6971bb 100644 --- a/packages/js/product-editor/src/blocks/inventory-sku/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/inventory-sku/edit.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { BlockEditProps, BlockAttributes } from '@wordpress/blocks'; +import { BlockAttributes } from '@wordpress/blocks'; import { createElement, createInterpolateElement } from '@wordpress/element'; import { useWooBlockProps } from '@woocommerce/block-templates'; @@ -19,11 +19,23 @@ import { useEntityProp } from '@wordpress/core-data'; /** * Internal dependencies */ +import { ProductEditorBlockEditProps } from '../../../types'; -export function Edit( { attributes }: BlockEditProps< BlockAttributes > ) { +/** + * Internal dependencies + */ + +export function Edit( { + attributes, + context, +}: ProductEditorBlockEditProps< BlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); - const [ sku, setSku ] = useEntityProp( 'postType', 'product', 'sku' ); + const [ sku, setSku ] = useEntityProp( + 'postType', + context.postType, + 'sku' + ); return (
diff --git a/packages/js/product-editor/src/blocks/inventory-sku/editor.scss b/packages/js/product-editor/src/blocks/product-fields/inventory-sku/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/inventory-sku/editor.scss rename to packages/js/product-editor/src/blocks/product-fields/inventory-sku/editor.scss diff --git a/packages/js/product-editor/src/blocks/product-fields/inventory-sku/index.ts b/packages/js/product-editor/src/blocks/product-fields/inventory-sku/index.ts new file mode 100644 index 00000000000..d670004d84d --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/inventory-sku/index.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => + registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); diff --git a/packages/js/product-editor/src/blocks/name/block.json b/packages/js/product-editor/src/blocks/product-fields/name/block.json similarity index 100% rename from packages/js/product-editor/src/blocks/name/block.json rename to packages/js/product-editor/src/blocks/product-fields/name/block.json diff --git a/packages/js/product-editor/src/blocks/name/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/name/edit.tsx similarity index 92% rename from packages/js/product-editor/src/blocks/name/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/name/edit.tsx index ac5ff97cb6e..4f679bcf902 100644 --- a/packages/js/product-editor/src/blocks/name/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/name/edit.tsx @@ -9,7 +9,6 @@ import { useState, } from '@wordpress/element'; -import { BlockEditProps } from '@wordpress/blocks'; import { useInstanceId } from '@wordpress/compose'; import { cleanForSlug } from '@wordpress/url'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -34,13 +33,16 @@ import { useEntityProp, useEntityId } from '@wordpress/core-data'; /** * Internal dependencies */ -import { AUTO_DRAFT_NAME } from '../../utils'; -import { EditProductLinkModal } from '../../components/edit-product-link-modal'; -import { useValidation } from '../../contexts/validation-context'; +import { AUTO_DRAFT_NAME } from '../../../utils'; +import { EditProductLinkModal } from '../../../components/edit-product-link-modal'; +import { useValidation } from '../../../contexts/validation-context'; import { NameBlockAttributes } from './types'; -import { useProductEdits } from '../../hooks/use-product-edits'; +import { useProductEdits } from '../../../hooks/use-product-edits'; +import { ProductEditorBlockEditProps } from '../../../types'; -export function Edit( { attributes }: BlockEditProps< NameBlockAttributes > ) { +export function Edit( { + attributes, +}: ProductEditorBlockEditProps< NameBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const { editEntityRecord, saveEntityRecord } = useDispatch( 'core' ); diff --git a/packages/js/product-editor/src/blocks/name/editor.scss b/packages/js/product-editor/src/blocks/product-fields/name/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/name/editor.scss rename to packages/js/product-editor/src/blocks/product-fields/name/editor.scss diff --git a/packages/js/product-editor/src/blocks/product-fields/name/index.ts b/packages/js/product-editor/src/blocks/product-fields/name/index.ts new file mode 100644 index 00000000000..5322eeb221e --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/name/index.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { Edit } from './edit'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => + registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); diff --git a/packages/js/product-editor/src/blocks/name/types.ts b/packages/js/product-editor/src/blocks/product-fields/name/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/name/types.ts rename to packages/js/product-editor/src/blocks/product-fields/name/types.ts diff --git a/packages/js/product-editor/src/blocks/notice/block.json b/packages/js/product-editor/src/blocks/product-fields/notice/block.json similarity index 100% rename from packages/js/product-editor/src/blocks/notice/block.json rename to packages/js/product-editor/src/blocks/product-fields/notice/block.json diff --git a/packages/js/product-editor/src/blocks/notice/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/notice/edit.tsx similarity index 81% rename from packages/js/product-editor/src/blocks/notice/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/notice/edit.tsx index 61be266772b..44fab4ccc71 100644 --- a/packages/js/product-editor/src/blocks/notice/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/notice/edit.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { createElement } from '@wordpress/element'; -import type { BlockAttributes, BlockEditProps } from '@wordpress/blocks'; +import type { BlockAttributes } from '@wordpress/blocks'; import { Button } from '@wordpress/components'; import { useWooBlockProps } from '@woocommerce/block-templates'; import { getNewPath, navigateTo } from '@woocommerce/navigation'; @@ -12,8 +12,9 @@ import { useEntityProp } from '@wordpress/core-data'; /** * Internal dependencies */ -import { Notice } from '../../components/notice'; -import { hasAttributesUsedForVariations } from '../../utils'; +import { Notice } from '../../../components/notice'; +import { hasAttributesUsedForVariations } from '../../../utils'; +import { ProductEditorBlockEditProps } from '../../../types'; export interface NoticeBlockAttributes extends BlockAttributes { buttonText: string; @@ -24,7 +25,7 @@ export interface NoticeBlockAttributes extends BlockAttributes { export function Edit( { attributes, -}: BlockEditProps< NoticeBlockAttributes > ) { +}: ProductEditorBlockEditProps< NoticeBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const { buttonText, content, title, type = 'info' } = attributes; diff --git a/packages/js/product-editor/src/blocks/notice/editor.scss b/packages/js/product-editor/src/blocks/product-fields/notice/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/notice/editor.scss rename to packages/js/product-editor/src/blocks/product-fields/notice/editor.scss diff --git a/packages/js/product-editor/src/blocks/product-fields/notice/index.ts b/packages/js/product-editor/src/blocks/product-fields/notice/index.ts new file mode 100644 index 00000000000..c9d5881a968 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/notice/index.ts @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { example: {}, edit: Edit }; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/password/block.json b/packages/js/product-editor/src/blocks/product-fields/password/block.json similarity index 100% rename from packages/js/product-editor/src/blocks/password/block.json rename to packages/js/product-editor/src/blocks/product-fields/password/block.json diff --git a/packages/js/product-editor/src/blocks/password/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/password/edit.tsx similarity index 92% rename from packages/js/product-editor/src/blocks/password/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/password/edit.tsx index 7fdcd717175..7d19d850a16 100644 --- a/packages/js/product-editor/src/blocks/password/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/password/edit.tsx @@ -1,7 +1,6 @@ /** * External dependencies */ -import { BlockEditProps } from '@wordpress/blocks'; import { useInstanceId } from '@wordpress/compose'; import { useEntityProp } from '@wordpress/core-data'; import { createElement, useState } from '@wordpress/element'; @@ -19,10 +18,11 @@ import { */ import { RequirePasswordBlockAttributes } from './types'; +import { ProductEditorBlockEditProps } from '../../../types'; export function Edit( { attributes, -}: BlockEditProps< RequirePasswordBlockAttributes > ) { +}: ProductEditorBlockEditProps< RequirePasswordBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const { label } = attributes; diff --git a/packages/js/product-editor/src/blocks/password/editor.scss b/packages/js/product-editor/src/blocks/product-fields/password/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/password/editor.scss rename to packages/js/product-editor/src/blocks/product-fields/password/editor.scss diff --git a/packages/js/product-editor/src/blocks/product-fields/password/index.ts b/packages/js/product-editor/src/blocks/product-fields/password/index.ts new file mode 100644 index 00000000000..d36ff2b8fc6 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/password/index.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/password/types.ts b/packages/js/product-editor/src/blocks/product-fields/password/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/password/types.ts rename to packages/js/product-editor/src/blocks/product-fields/password/types.ts diff --git a/packages/js/product-editor/src/blocks/regular-price/block.json b/packages/js/product-editor/src/blocks/product-fields/regular-price/block.json similarity index 87% rename from packages/js/product-editor/src/blocks/regular-price/block.json rename to packages/js/product-editor/src/blocks/product-fields/regular-price/block.json index 70c3c995030..b8e759226e7 100644 --- a/packages/js/product-editor/src/blocks/regular-price/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/regular-price/block.json @@ -14,6 +14,10 @@ }, "help": { "type": "string" + }, + "isRequired": { + "type": "boolean", + "default": false } }, "supports": { @@ -25,5 +29,6 @@ "lock": false, "__experimentalToolbar": false }, + "usesContext": [ "postType" ], "editorStyle": "file:./editor.css" } diff --git a/packages/js/product-editor/src/blocks/regular-price/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/regular-price/edit.tsx similarity index 77% rename from packages/js/product-editor/src/blocks/regular-price/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/regular-price/edit.tsx index 78655a8335b..0762b954fd9 100644 --- a/packages/js/product-editor/src/blocks/regular-price/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/regular-price/edit.tsx @@ -7,11 +7,14 @@ import { Link } from '@woocommerce/components'; import { Product } from '@woocommerce/data'; import { getNewPath } from '@woocommerce/navigation'; import { recordEvent } from '@woocommerce/tracks'; -import { BlockEditProps } from '@wordpress/blocks'; import { useInstanceId } from '@wordpress/compose'; import { useEntityProp } from '@wordpress/core-data'; -import { createElement, createInterpolateElement } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { + createElement, + createInterpolateElement, + useEffect, +} from '@wordpress/element'; +import { sprintf, __ } from '@wordpress/i18n'; import { BaseControl, // @ts-expect-error `__experimentalInputControl` does exist. @@ -21,24 +24,26 @@ import { /** * Internal dependencies */ -import { useValidation } from '../../contexts/validation-context'; -import { useCurrencyInputProps } from '../../hooks/use-currency-input-props'; +import { useValidation } from '../../../contexts/validation-context'; +import { useCurrencyInputProps } from '../../../hooks/use-currency-input-props'; import { SalePriceBlockAttributes } from './types'; +import { ProductEditorBlockEditProps } from '../../../types'; export function Edit( { attributes, clientId, -}: BlockEditProps< SalePriceBlockAttributes > ) { + context, +}: ProductEditorBlockEditProps< SalePriceBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); - const { label, help } = attributes; + const { label, help, isRequired } = attributes; const [ regularPrice, setRegularPrice ] = useEntityProp< string >( 'postType', - 'product', + context.postType || 'product', 'regular_price' ); const [ salePrice ] = useEntityProp< string >( 'postType', - 'product', + context.postType || 'product', 'sale_price' ); const inputProps = useCurrencyInputProps( { @@ -88,11 +93,20 @@ export function Edit( { 'woocommerce' ); } + } else if ( isRequired ) { + /* translators: label of required field. */ + return sprintf( __( '%s is required.', 'woocommerce' ), label ); } }, [ regularPrice, salePrice ] ); + useEffect( () => { + if ( isRequired ) { + validateRegularPrice(); + } + }, [] ); + return (
) { + context, +}: ProductEditorBlockEditProps< SalePriceBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const { label, help } = attributes; const [ regularPrice ] = useEntityProp< string >( 'postType', - 'product', + context.postType || 'product', 'regular_price' ); const [ salePrice, setSalePrice ] = useEntityProp< string >( 'postType', - 'product', + context.postType || 'product', 'sale_price' ); const inputProps = useCurrencyInputProps( { diff --git a/packages/js/product-editor/src/blocks/sale-price/editor.scss b/packages/js/product-editor/src/blocks/product-fields/sale-price/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/sale-price/editor.scss rename to packages/js/product-editor/src/blocks/product-fields/sale-price/editor.scss diff --git a/packages/js/product-editor/src/blocks/product-fields/sale-price/index.ts b/packages/js/product-editor/src/blocks/product-fields/sale-price/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/sale-price/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/regular-price/types.ts b/packages/js/product-editor/src/blocks/product-fields/sale-price/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/regular-price/types.ts rename to packages/js/product-editor/src/blocks/product-fields/sale-price/types.ts diff --git a/packages/js/product-editor/src/blocks/schedule-sale/block.json b/packages/js/product-editor/src/blocks/product-fields/schedule-sale/block.json similarity index 89% rename from packages/js/product-editor/src/blocks/schedule-sale/block.json rename to packages/js/product-editor/src/blocks/product-fields/schedule-sale/block.json index d54f9354f0f..fe4a0d8dc6f 100644 --- a/packages/js/product-editor/src/blocks/schedule-sale/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/schedule-sale/block.json @@ -22,5 +22,6 @@ "lock": false, "__experimentalToolbar": false }, - "editorStyle": "file:./editor.css" + "editorStyle": "file:./editor.css", + "usesContext": [ "postType" ] } diff --git a/packages/js/product-editor/src/blocks/schedule-sale/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/schedule-sale/edit.tsx similarity index 92% rename from packages/js/product-editor/src/blocks/schedule-sale/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/schedule-sale/edit.tsx index daa587f855e..ef0a6b438d9 100644 --- a/packages/js/product-editor/src/blocks/schedule-sale/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/schedule-sale/edit.tsx @@ -5,7 +5,6 @@ import { useWooBlockProps } from '@woocommerce/block-templates'; import { DateTimePickerControl } from '@woocommerce/components'; import { Product } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; -import { BlockEditProps } from '@wordpress/blocks'; import { ToggleControl } from '@wordpress/components'; import { useEntityProp } from '@wordpress/core-data'; import { createElement, useEffect, useState } from '@wordpress/element'; @@ -20,13 +19,15 @@ import { getSettings } from '@wordpress/date'; * Internal dependencies */ import { ScheduleSalePricingBlockAttributes } from './types'; -import { useProductEdits } from '../../hooks/use-product-edits'; -import { useValidation } from '../../contexts/validation-context'; +import { useProductEdits } from '../../../hooks/use-product-edits'; +import { useValidation } from '../../../contexts/validation-context'; +import { ProductEditorBlockEditProps } from '../../../types'; export function Edit( { attributes, clientId, -}: BlockEditProps< ScheduleSalePricingBlockAttributes > ) { + context, +}: ProductEditorBlockEditProps< ScheduleSalePricingBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const { hasEdit } = useProductEdits(); @@ -36,7 +37,7 @@ export function Edit( { const [ salePrice ] = useEntityProp< string | null >( 'postType', - 'product', + context.postType || 'product', 'sale_price' ); @@ -45,11 +46,11 @@ export function Edit( { const [ dateOnSaleFromGmt, setDateOnSaleFromGmt ] = useEntityProp< string | null - >( 'postType', 'product', 'date_on_sale_from_gmt' ); + >( 'postType', context.postType || 'product', 'date_on_sale_from_gmt' ); const [ dateOnSaleToGmt, setDateOnSaleToGmt ] = useEntityProp< string | null - >( 'postType', 'product', 'date_on_sale_to_gmt' ); + >( 'postType', context.postType || 'product', 'date_on_sale_to_gmt' ); const today = moment().startOf( 'minute' ).toISOString(); diff --git a/packages/js/product-editor/src/blocks/schedule-sale/editor.scss b/packages/js/product-editor/src/blocks/product-fields/schedule-sale/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/schedule-sale/editor.scss rename to packages/js/product-editor/src/blocks/product-fields/schedule-sale/editor.scss diff --git a/packages/js/product-editor/src/blocks/product-fields/schedule-sale/index.ts b/packages/js/product-editor/src/blocks/product-fields/schedule-sale/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/schedule-sale/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/schedule-sale/types.ts b/packages/js/product-editor/src/blocks/product-fields/schedule-sale/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/schedule-sale/types.ts rename to packages/js/product-editor/src/blocks/product-fields/schedule-sale/types.ts diff --git a/packages/js/product-editor/src/blocks/shipping-class/block.json b/packages/js/product-editor/src/blocks/product-fields/shipping-class/block.json similarity index 94% rename from packages/js/product-editor/src/blocks/shipping-class/block.json rename to packages/js/product-editor/src/blocks/product-fields/shipping-class/block.json index 124054818db..09a39cee5e0 100644 --- a/packages/js/product-editor/src/blocks/shipping-class/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/shipping-class/block.json @@ -22,5 +22,5 @@ "lock": false, "__experimentalToolbar": false }, - "editorStyle": "file:./editor.css" + "usesContext": [ "postType" ] } diff --git a/packages/js/product-editor/src/blocks/shipping-class/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/shipping-class/edit.tsx similarity index 92% rename from packages/js/product-editor/src/blocks/shipping-class/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/shipping-class/edit.tsx index 8575360178c..9d29d26644f 100644 --- a/packages/js/product-editor/src/blocks/shipping-class/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/shipping-class/edit.tsx @@ -1,7 +1,6 @@ /** * External dependencies */ -import { BlockEditProps } from '@wordpress/blocks'; import { useWooBlockProps } from '@woocommerce/block-templates'; import { Link } from '@woocommerce/components'; import { @@ -27,8 +26,9 @@ import { useEntityProp } from '@wordpress/core-data'; * Internal dependencies */ import { ShippingClassBlockAttributes } from './types'; -import { AddNewShippingClassModal } from '../../components'; -import { ADD_NEW_SHIPPING_CLASS_OPTION_VALUE } from '../../constants'; +import { AddNewShippingClassModal } from '../../../components'; +import { ADD_NEW_SHIPPING_CLASS_OPTION_VALUE } from '../../../constants'; +import { ProductEditorBlockEditProps } from '../../../types'; type ServerErrorResponse = { code: string; @@ -71,7 +71,8 @@ function extractDefaultShippingClassFromProduct( export function Edit( { attributes, -}: BlockEditProps< ShippingClassBlockAttributes > ) { + context, +}: ProductEditorBlockEditProps< ShippingClassBlockAttributes > ) { const [ showShippingClassModal, setShowShippingClassModal ] = useState( false ); @@ -90,9 +91,14 @@ export function Edit( { ); const [ shippingClass, setShippingClass ] = useEntityProp< string >( 'postType', - 'product', + context.postType, 'shipping_class' ); + const [ virtual ] = useEntityProp< boolean >( + 'postType', + context.postType, + 'virtual' + ); function handleShippingClassServerError( error: ServerErrorResponse @@ -155,6 +161,7 @@ export function Edit( { shippingClasses ?? [] ), ] } + disabled={ virtual } help={ createInterpolateElement( __( 'Manage shipping classes and rates in global settings.', diff --git a/packages/js/product-editor/src/blocks/product-fields/shipping-class/index.ts b/packages/js/product-editor/src/blocks/product-fields/shipping-class/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/shipping-class/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/shipping-class/types.ts b/packages/js/product-editor/src/blocks/product-fields/shipping-class/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/shipping-class/types.ts rename to packages/js/product-editor/src/blocks/product-fields/shipping-class/types.ts diff --git a/packages/js/product-editor/src/blocks/shipping-dimensions/block.json b/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/block.json similarity index 90% rename from packages/js/product-editor/src/blocks/shipping-dimensions/block.json rename to packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/block.json index a0778f86e5d..d66452bd781 100644 --- a/packages/js/product-editor/src/blocks/shipping-dimensions/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/block.json @@ -22,5 +22,6 @@ "lock": false, "__experimentalToolbar": false }, - "editorStyle": "file:./editor.css" + "editorStyle": "file:./editor.css", + "usesContext": [ "postType" ] } diff --git a/packages/js/product-editor/src/blocks/shipping-dimensions/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/edit.tsx similarity index 92% rename from packages/js/product-editor/src/blocks/shipping-dimensions/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/edit.tsx index 0938e223813..a5c245e5440 100644 --- a/packages/js/product-editor/src/blocks/shipping-dimensions/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/edit.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { useWooBlockProps } from '@woocommerce/block-templates'; -import { BlockEditProps } from '@wordpress/blocks'; import { OPTIONS_STORE_NAME, Product, @@ -28,32 +27,40 @@ import { * Internal dependencies */ import { ShippingDimensionsBlockAttributes } from './types'; -import { useProductHelper } from '../../hooks/use-product-helper'; +import { useProductHelper } from '../../../hooks/use-product-helper'; import { HighlightSides, ShippingDimensionsImage, -} from '../../components/shipping-dimensions-image'; -import { useValidation } from '../../contexts/validation-context'; +} from '../../../components/shipping-dimensions-image'; +import { useValidation } from '../../../contexts/validation-context'; +import { ProductEditorBlockEditProps } from '../../../types'; export function Edit( { attributes, clientId, -}: BlockEditProps< ShippingDimensionsBlockAttributes > ) { + context, +}: ProductEditorBlockEditProps< ShippingDimensionsBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); const [ dimensions, setDimensions ] = useEntityProp< Partial< ProductDimensions > | null >( 'postType', - 'product', + context.postType, 'dimensions' ); const [ weight, setWeight ] = useEntityProp< string | null >( 'postType', - 'product', + context.postType, 'weight' ); + const [ virtual ] = useEntityProp< boolean >( + 'postType', + context.postType, + 'virtual' + ); + const [ highlightSide, setHighlightSide ] = useState< HighlightSides >(); const { formatNumber, parseNumber } = useProductHelper(); @@ -83,6 +90,7 @@ export function Edit( { onFocus: () => setHighlightSide( side ), onBlur: () => setHighlightSide( undefined ), suffix: dimensionUnit, + disabled: virtual, }; } @@ -177,6 +185,7 @@ export function Edit( { suffix: weightUnit, ref: weightRef, onBlur: validateWeight, + disabled: virtual, }; return ( diff --git a/packages/js/product-editor/src/blocks/shipping-dimensions/editor.scss b/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/shipping-dimensions/editor.scss rename to packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/editor.scss diff --git a/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/index.ts b/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/shipping-dimensions/types.ts b/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/shipping-dimensions/types.ts rename to packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/types.ts diff --git a/packages/js/product-editor/src/blocks/summary/block.json b/packages/js/product-editor/src/blocks/product-fields/summary/block.json similarity index 86% rename from packages/js/product-editor/src/blocks/summary/block.json rename to packages/js/product-editor/src/blocks/product-fields/summary/block.json index 2cb25d1ad98..b1859bc2ffa 100644 --- a/packages/js/product-editor/src/blocks/summary/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/summary/block.json @@ -8,6 +8,9 @@ "keywords": [ "products", "summary", "excerpt" ], "textdomain": "default", "attributes": { + "property": { + "type": "string" + }, "align": { "type": "string" }, @@ -33,6 +36,9 @@ "label": { "type": "string" }, + "helpText": { + "type": "string" + }, "content": { "type": "string", "__experimentalRole": "content" @@ -46,5 +52,6 @@ "inserter": false, "lock": false }, - "editorStyle": "file:./editor.css" + "editorStyle": "file:./editor.css", + "usesContext": [ "postType" ] } diff --git a/packages/js/product-editor/src/blocks/summary/constants.ts b/packages/js/product-editor/src/blocks/product-fields/summary/constants.ts similarity index 100% rename from packages/js/product-editor/src/blocks/summary/constants.ts rename to packages/js/product-editor/src/blocks/product-fields/summary/constants.ts diff --git a/packages/js/product-editor/src/blocks/summary/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/summary/edit.tsx similarity index 77% rename from packages/js/product-editor/src/blocks/summary/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/summary/edit.tsx index bc72958de1b..049bf46d765 100644 --- a/packages/js/product-editor/src/blocks/summary/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/summary/edit.tsx @@ -3,8 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { useWooBlockProps } from '@woocommerce/block-templates'; -import { createElement } from '@wordpress/element'; -import { BlockEditProps } from '@wordpress/blocks'; +import { createElement, createInterpolateElement } from '@wordpress/element'; import { BaseControl } from '@wordpress/components'; import { useDispatch } from '@wordpress/data'; import { useEntityProp } from '@wordpress/core-data'; @@ -25,12 +24,14 @@ import { import { ParagraphRTLControl } from './paragraph-rtl-control'; import { SummaryAttributes } from './types'; import { ALIGNMENT_CONTROLS } from './constants'; +import { ProductEditorBlockEditProps } from '../../../types'; export function Edit( { attributes, setAttributes, -}: BlockEditProps< SummaryAttributes > ) { - const { align, allowedFormats, direction, label } = attributes; + context, +}: ProductEditorBlockEditProps< SummaryAttributes > ) { + const { align, allowedFormats, direction, label, helpText } = attributes; const blockProps = useWooBlockProps( attributes, { style: { direction }, } ); @@ -40,8 +41,8 @@ export function Edit( { ); const [ summary, setSummary ] = useEntityProp< string >( 'postType', - 'product', - 'short_description' + context.postType || 'product', + attributes.property ); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore No types for this exist yet. @@ -87,11 +88,24 @@ export function Edit( { + { __( '(OPTIONAL)', 'woocommerce' ) } + + ), + } ) } + help={ + typeof helpText === 'string' + ? helpText + : __( + "Summarize this product in 1-2 short sentences. We'll show it at the top of the page.", + 'woocommerce' + ) + } >
) { const blockProps = useWooBlockProps( attributes ); const { name, label, placeholder } = attributes; const [ tags, setTags ] = useEntityProp< Pick< ProductTag, 'id' | 'name' >[] - >( 'postType', context?.postType || 'product', name || 'tags' ); + >( 'postType', context.postType || 'product', name || 'tags' ); const tagFieldId = useInstanceId( BaseControl, 'tag-field' ) as string; diff --git a/packages/js/product-editor/src/blocks/tag/editor.scss b/packages/js/product-editor/src/blocks/product-fields/tag/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/tag/editor.scss rename to packages/js/product-editor/src/blocks/product-fields/tag/editor.scss diff --git a/packages/js/product-editor/src/blocks/product-fields/tag/index.ts b/packages/js/product-editor/src/blocks/product-fields/tag/index.ts new file mode 100644 index 00000000000..5322eeb221e --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/tag/index.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { Edit } from './edit'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => + registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); diff --git a/packages/js/product-editor/src/blocks/variation-items/block.json b/packages/js/product-editor/src/blocks/product-fields/variation-items/block.json similarity index 100% rename from packages/js/product-editor/src/blocks/variation-items/block.json rename to packages/js/product-editor/src/blocks/product-fields/variation-items/block.json diff --git a/packages/js/product-editor/src/blocks/variation-items/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/variation-items/edit.tsx similarity index 91% rename from packages/js/product-editor/src/blocks/variation-items/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/variation-items/edit.tsx index b57528f19f2..6c448954005 100644 --- a/packages/js/product-editor/src/blocks/variation-items/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/variation-items/edit.tsx @@ -10,7 +10,6 @@ import { } from '@woocommerce/data'; import { useWooBlockProps } from '@woocommerce/block-templates'; import { recordEvent } from '@woocommerce/tracks'; -import { BlockEditProps } from '@wordpress/blocks'; import { createElement, useMemo, useRef } from '@wordpress/element'; import { resolveSelect, useDispatch, useSelect } from '@wordpress/data'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -21,18 +20,19 @@ import { useEntityId, useEntityProp } from '@wordpress/core-data'; /** * Internal dependencies */ -import { VariationsTable } from '../../components/variations-table'; -import { useValidation } from '../../contexts/validation-context'; +import { VariationsTable } from '../../../components/variations-table'; +import { useValidation } from '../../../contexts/validation-context'; import { VariationOptionsBlockAttributes } from './types'; import { VariableProductTour } from './variable-product-tour'; -import { TRACKS_SOURCE } from '../../constants'; -import { handlePrompt } from '../../utils/handle-prompt'; +import { TRACKS_SOURCE } from '../../../constants'; +import { handlePrompt } from '../../../utils/handle-prompt'; +import { ProductEditorBlockEditProps } from '../../../types'; export function Edit( { attributes, context, -}: BlockEditProps< VariationOptionsBlockAttributes > & { - context?: { +}: ProductEditorBlockEditProps< VariationOptionsBlockAttributes > & { + context: { isInSelectedTab?: boolean; }; } ) { @@ -198,7 +198,7 @@ export function Edit( { } } } /> - { context?.isInSelectedTab && } + { context.isInSelectedTab && }
); } diff --git a/packages/js/product-editor/src/blocks/variation-items/editor.scss b/packages/js/product-editor/src/blocks/product-fields/variation-items/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/variation-items/editor.scss rename to packages/js/product-editor/src/blocks/product-fields/variation-items/editor.scss diff --git a/packages/js/product-editor/src/blocks/product-fields/variation-items/index.ts b/packages/js/product-editor/src/blocks/product-fields/variation-items/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/variation-items/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/variation-items/types.ts b/packages/js/product-editor/src/blocks/product-fields/variation-items/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/variation-items/types.ts rename to packages/js/product-editor/src/blocks/product-fields/variation-items/types.ts diff --git a/packages/js/product-editor/src/blocks/variation-items/variable-product-tour.tsx b/packages/js/product-editor/src/blocks/product-fields/variation-items/variable-product-tour.tsx similarity index 97% rename from packages/js/product-editor/src/blocks/variation-items/variable-product-tour.tsx rename to packages/js/product-editor/src/blocks/product-fields/variation-items/variable-product-tour.tsx index 0b02be482ea..0fb67d78625 100644 --- a/packages/js/product-editor/src/blocks/variation-items/variable-product-tour.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/variation-items/variable-product-tour.tsx @@ -24,7 +24,7 @@ import { useEntityId } from '@wordpress/core-data'; /** * Internal dependencies */ -import { DEFAULT_VARIATION_PER_PAGE_OPTION } from '../../constants'; +import { DEFAULT_VARIATION_PER_PAGE_OPTION } from '../../../constants'; export const VariableProductTour: React.FC = () => { const [ isTourOpen, setIsTourOpen ] = useState( false ); diff --git a/packages/js/product-editor/src/blocks/variation-options/block.json b/packages/js/product-editor/src/blocks/product-fields/variation-options/block.json similarity index 100% rename from packages/js/product-editor/src/blocks/variation-options/block.json rename to packages/js/product-editor/src/blocks/product-fields/variation-options/block.json diff --git a/packages/js/product-editor/src/blocks/variation-options/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/variation-options/edit.tsx similarity index 77% rename from packages/js/product-editor/src/blocks/variation-options/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/variation-options/edit.tsx index f6aa0586da6..87cbe8add0e 100644 --- a/packages/js/product-editor/src/blocks/variation-options/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/variation-options/edit.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { BlockEditProps, BlockAttributes } from '@wordpress/blocks'; +import { BlockAttributes } from '@wordpress/blocks'; import { createElement, createInterpolateElement, @@ -14,6 +14,7 @@ import { ProductAttribute, useUserPreferences, } from '@woocommerce/data'; +import { recordEvent } from '@woocommerce/tracks'; import { Link } from '@woocommerce/components'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore No types for this exist yet. @@ -23,13 +24,14 @@ import { useEntityProp, useEntityId } from '@wordpress/core-data'; /** * Internal dependencies */ -import { useProductAttributes } from '../../hooks/use-product-attributes'; -import { AttributeControl } from '../../components/attribute-control'; -import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper'; +import { useProductAttributes } from '../../../hooks/use-product-attributes'; +import { AttributeControl } from '../../../components/attribute-control'; +import { useProductVariationsHelper } from '../../../hooks/use-product-variations-helper'; +import { ProductEditorBlockEditProps } from '../../../types'; export function Edit( { attributes: blockAttributes, -}: BlockEditProps< BlockAttributes > ) { +}: ProductEditorBlockEditProps< BlockAttributes > ) { const blockProps = useWooBlockProps( blockAttributes ); const { generateProductVariations } = useProductVariationsHelper(); const { @@ -112,6 +114,9 @@ export function Edit( { attributes, entityDefaultAttributes, ] ) } + onAdd={ () => { + recordEvent( 'product_options_modal_add_button_click' ); + } } onChange={ handleChange } createNewAttributesAsGlobal={ true } useRemoveConfirmationModal={ true } @@ -120,6 +125,32 @@ export function Edit( { product_block_variable_options_notice_dismissed: 'yes', } ) } + onAddAnother={ () => { + recordEvent( + 'product_add_options_modal_add_another_option_button_click' + ); + } } + onNewModalCancel={ () => { + recordEvent( 'product_options_modal_cancel_button_click' ); + } } + onNewModalOpen={ () => { + recordEvent( 'product_options_add_option' ); + } } + onRemoveItem={ () => { + recordEvent( + 'product_add_options_modal_remove_option_button_click' + ); + } } + onRemove={ () => + recordEvent( + 'product_remove_option_confirmation_confirm_click' + ) + } + onRemoveCancel={ () => + recordEvent( + 'product_remove_option_confirmation_cancel_click' + ) + } disabledAttributeIds={ entityAttributes .filter( ( attr ) => ! attr.variation ) .map( ( attr ) => attr.id ) } diff --git a/packages/js/product-editor/src/blocks/variation-options/editor.scss b/packages/js/product-editor/src/blocks/product-fields/variation-options/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/variation-options/editor.scss rename to packages/js/product-editor/src/blocks/product-fields/variation-options/editor.scss diff --git a/packages/js/product-editor/src/blocks/product-fields/variation-options/index.ts b/packages/js/product-editor/src/blocks/product-fields/variation-options/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/variation-options/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/variation-options/types.ts b/packages/js/product-editor/src/blocks/product-fields/variation-options/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/variation-options/types.ts rename to packages/js/product-editor/src/blocks/product-fields/variation-options/types.ts diff --git a/packages/js/product-editor/src/blocks/variations/block.json b/packages/js/product-editor/src/blocks/product-fields/variations/block.json similarity index 100% rename from packages/js/product-editor/src/blocks/variations/block.json rename to packages/js/product-editor/src/blocks/product-fields/variations/block.json diff --git a/packages/js/product-editor/src/blocks/variations/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/variations/edit.tsx similarity index 81% rename from packages/js/product-editor/src/blocks/variations/edit.tsx rename to packages/js/product-editor/src/blocks/product-fields/variations/edit.tsx index 0ffaa045f6b..5e9257bbc6a 100644 --- a/packages/js/product-editor/src/blocks/variations/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/variations/edit.tsx @@ -2,7 +2,6 @@ * External dependencies */ import classNames from 'classnames'; -import type { BlockEditProps } from '@wordpress/blocks'; import { Button } from '@wordpress/components'; import { useWooBlockProps } from '@woocommerce/block-templates'; import { Product, ProductAttribute } from '@woocommerce/data'; @@ -21,22 +20,23 @@ import { useEntityProp, useEntityId } from '@wordpress/core-data'; /** * Internal dependencies */ -import { sanitizeHTML } from '../../utils/sanitize-html'; +import { sanitizeHTML } from '../../../utils/sanitize-html'; import { VariationsBlockAttributes } from './types'; import { EmptyVariationsImage } from './empty-variations-image'; -import { NewAttributeModal } from '../../components/attribute-control/new-attribute-modal'; +import { NewAttributeModal } from '../../../components/attribute-control/new-attribute-modal'; import { EnhancedProductAttribute, useProductAttributes, -} from '../../hooks/use-product-attributes'; -import { getAttributeId } from '../../components/attribute-control/utils'; -import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper'; -import { hasAttributesUsedForVariations } from '../../utils'; -import { TRACKS_SOURCE } from '../../constants'; +} from '../../../hooks/use-product-attributes'; +import { getAttributeId } from '../../../components/attribute-control/utils'; +import { useProductVariationsHelper } from '../../../hooks/use-product-variations-helper'; +import { hasAttributesUsedForVariations } from '../../../utils'; +import { TRACKS_SOURCE } from '../../../constants'; +import { ProductEditorBlockEditProps } from '../../../types'; export function Edit( { attributes, -}: BlockEditProps< VariationsBlockAttributes > ) { +}: ProductEditorBlockEditProps< VariationsBlockAttributes > ) { const { description } = attributes; const { generateProductVariations } = useProductVariationsHelper(); @@ -79,6 +79,7 @@ export function Edit( { const openNewModal = () => { setIsNewModalVisible( true ); + recordEvent( 'product_options_add_first_option' ); }; const closeNewModal = () => { @@ -133,9 +134,22 @@ export function Edit( { createNewAttributesAsGlobal={ true } notice={ '' } onCancel={ () => { + recordEvent( + 'product_options_modal_cancel_button_click' + ); closeNewModal(); } } onAdd={ handleAdd } + onAddAnother={ () => { + recordEvent( + 'product_add_options_modal_add_another_option_button_click' + ); + } } + onRemoveItem={ () => { + recordEvent( + 'product_add_options_modal_remove_option_button_click' + ); + } } selectedAttributeIds={ variationOptions.map( ( attr ) => attr.id ) } diff --git a/packages/js/product-editor/src/blocks/variations/editor.scss b/packages/js/product-editor/src/blocks/product-fields/variations/editor.scss similarity index 100% rename from packages/js/product-editor/src/blocks/variations/editor.scss rename to packages/js/product-editor/src/blocks/product-fields/variations/editor.scss diff --git a/packages/js/product-editor/src/blocks/variations/empty-variations-image.tsx b/packages/js/product-editor/src/blocks/product-fields/variations/empty-variations-image.tsx similarity index 100% rename from packages/js/product-editor/src/blocks/variations/empty-variations-image.tsx rename to packages/js/product-editor/src/blocks/product-fields/variations/empty-variations-image.tsx diff --git a/packages/js/product-editor/src/blocks/product-fields/variations/index.ts b/packages/js/product-editor/src/blocks/product-fields/variations/index.ts new file mode 100644 index 00000000000..7829fccedb9 --- /dev/null +++ b/packages/js/product-editor/src/blocks/product-fields/variations/index.ts @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { registerWooBlockType } from '@woocommerce/block-templates'; + +/** + * Internal dependencies + */ +import blockConfiguration from './block.json'; +import { Edit } from './edit'; + +const { name, ...metadata } = blockConfiguration; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export function init() { + return registerWooBlockType( { + name, + metadata: metadata as never, + settings: settings as never, + } ); +} diff --git a/packages/js/product-editor/src/blocks/variations/types.ts b/packages/js/product-editor/src/blocks/product-fields/variations/types.ts similarity index 100% rename from packages/js/product-editor/src/blocks/variations/types.ts rename to packages/js/product-editor/src/blocks/product-fields/variations/types.ts diff --git a/packages/js/product-editor/src/blocks/radio/index.ts b/packages/js/product-editor/src/blocks/radio/index.ts deleted file mode 100644 index 247c3376403..00000000000 --- a/packages/js/product-editor/src/blocks/radio/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { RadioBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< RadioBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< BlockConfiguration< RadioBlockAttributes > > = { - example: {}, - edit: Edit, -}; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/regular-price/index.ts b/packages/js/product-editor/src/blocks/regular-price/index.ts deleted file mode 100644 index d52482c6070..00000000000 --- a/packages/js/product-editor/src/blocks/regular-price/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { SalePriceBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< SalePriceBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< - BlockConfiguration< SalePriceBlockAttributes > -> = { - example: {}, - edit: Edit, -}; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/sale-price/index.ts b/packages/js/product-editor/src/blocks/sale-price/index.ts deleted file mode 100644 index d52482c6070..00000000000 --- a/packages/js/product-editor/src/blocks/sale-price/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { SalePriceBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< SalePriceBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< - BlockConfiguration< SalePriceBlockAttributes > -> = { - example: {}, - edit: Edit, -}; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/schedule-sale/index.ts b/packages/js/product-editor/src/blocks/schedule-sale/index.ts deleted file mode 100644 index a503751f2d1..00000000000 --- a/packages/js/product-editor/src/blocks/schedule-sale/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { ScheduleSalePricingBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< ScheduleSalePricingBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< - BlockConfiguration< ScheduleSalePricingBlockAttributes > -> = { - example: {}, - edit: Edit, -}; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/section/index.ts b/packages/js/product-editor/src/blocks/section/index.ts deleted file mode 100644 index addbd405737..00000000000 --- a/packages/js/product-editor/src/blocks/section/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { SectionBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< SectionBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< BlockConfiguration< SectionBlockAttributes > > = - { - example: {}, - edit: Edit, - }; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/shipping-class/index.ts b/packages/js/product-editor/src/blocks/shipping-class/index.ts deleted file mode 100644 index c61e625622d..00000000000 --- a/packages/js/product-editor/src/blocks/shipping-class/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { ShippingClassBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< ShippingClassBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< - BlockConfiguration< ShippingClassBlockAttributes > -> = { - example: {}, - edit: Edit, -}; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/shipping-dimensions/index.ts b/packages/js/product-editor/src/blocks/shipping-dimensions/index.ts deleted file mode 100644 index 7bf5acf1c12..00000000000 --- a/packages/js/product-editor/src/blocks/shipping-dimensions/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { ShippingDimensionsBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< ShippingDimensionsBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< - BlockConfiguration< ShippingDimensionsBlockAttributes > -> = { - example: {}, - edit: Edit, -}; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/style.scss b/packages/js/product-editor/src/blocks/style.scss index 2a8dbaffb6b..25bcc80e836 100644 --- a/packages/js/product-editor/src/blocks/style.scss +++ b/packages/js/product-editor/src/blocks/style.scss @@ -1,21 +1,24 @@ -@import 'attributes/editor.scss'; -@import 'checkbox/editor.scss'; -@import 'images/editor.scss'; -@import 'inventory-email/editor.scss'; -@import 'inventory-sku/editor.scss'; -@import 'name/editor.scss'; -@import 'notice/editor.scss'; -@import 'pricing/editor.scss'; -@import 'regular-price/editor.scss'; -@import 'sale-price/editor.scss'; -@import 'schedule-sale/editor.scss'; -@import 'section/editor.scss'; -@import 'shipping-dimensions/editor.scss'; -@import 'summary/editor.scss'; -@import 'tab/editor.scss'; -@import 'variations/editor.scss'; -@import 'password/editor.scss'; -@import 'variation-items/editor.scss'; -@import 'variation-options/editor.scss'; -@import 'taxonomy/editor.scss'; -@import 'toggle/editor.scss'; +@import "product-fields/attributes/editor.scss"; +@import "generic/checkbox/editor.scss"; +@import "product-fields/downloads/editor.scss"; +@import "product-fields/images/editor.scss"; +@import "product-fields/inventory-email/editor.scss"; +@import "product-fields/inventory-sku/editor.scss"; +@import "product-fields/name/editor.scss"; +@import "product-fields/notice/editor.scss"; +@import "generic/pricing/editor.scss"; +@import "product-fields/regular-price/editor.scss"; +@import "product-fields/sale-price/editor.scss"; +@import "product-fields/schedule-sale/editor.scss"; +@import "generic/section/editor.scss"; +@import "product-fields/shipping-dimensions/editor.scss"; +@import "product-fields/summary/editor.scss"; +@import "generic/tab/editor.scss"; +@import "product-fields/variations/editor.scss"; +@import "product-fields/password/editor.scss"; +@import "product-fields/variation-items/editor.scss"; +@import "product-fields/variation-options/editor.scss"; +@import "generic/taxonomy/editor.scss"; +@import "generic/toggle/editor.scss"; +@import "generic/text/editor.scss"; +@import "generic/number/editor.scss"; diff --git a/packages/js/product-editor/src/blocks/tab/index.ts b/packages/js/product-editor/src/blocks/tab/index.ts deleted file mode 100644 index a72be689268..00000000000 --- a/packages/js/product-editor/src/blocks/tab/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit, TabBlockAttributes } from './edit'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< TabBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< BlockConfiguration< TabBlockAttributes > > = { - example: {}, - edit: Edit, -}; - -export function init() { - registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/tag/index.ts b/packages/js/product-editor/src/blocks/tag/index.ts deleted file mode 100644 index 6af10d47939..00000000000 --- a/packages/js/product-editor/src/blocks/tag/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * External dependencies - */ -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import metadata from './block.json'; -import { Edit } from './edit'; - -const { name } = metadata; - -export { metadata, name }; - -export const settings = { - example: {}, - edit: Edit, -}; - -export const init = () => - registerWooBlockType( { name, metadata: metadata as never, settings } ); diff --git a/packages/js/product-editor/src/blocks/toggle/index.ts b/packages/js/product-editor/src/blocks/toggle/index.ts deleted file mode 100644 index 43d9b3fa945..00000000000 --- a/packages/js/product-editor/src/blocks/toggle/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { ToggleBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< ToggleBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< BlockConfiguration< ToggleBlockAttributes > > = - { - example: {}, - edit: Edit, - }; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/variation-items/index.ts b/packages/js/product-editor/src/blocks/variation-items/index.ts deleted file mode 100644 index 17a6b007a32..00000000000 --- a/packages/js/product-editor/src/blocks/variation-items/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { VariationOptionsBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< VariationOptionsBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< - BlockConfiguration< VariationOptionsBlockAttributes > -> = { - example: {}, - edit: Edit, -}; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/variation-options/index.ts b/packages/js/product-editor/src/blocks/variation-options/index.ts deleted file mode 100644 index 17a6b007a32..00000000000 --- a/packages/js/product-editor/src/blocks/variation-options/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { VariationOptionsBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< VariationOptionsBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< - BlockConfiguration< VariationOptionsBlockAttributes > -> = { - example: {}, - edit: Edit, -}; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/blocks/variations/index.ts b/packages/js/product-editor/src/blocks/variations/index.ts deleted file mode 100644 index 6f2dfb0dc1a..00000000000 --- a/packages/js/product-editor/src/blocks/variations/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import { BlockConfiguration } from '@wordpress/blocks'; -import { registerWooBlockType } from '@woocommerce/block-templates'; - -/** - * Internal dependencies - */ -import blockConfiguration from './block.json'; -import { Edit } from './edit'; -import { VariationsBlockAttributes } from './types'; - -const { name, ...metadata } = - blockConfiguration as BlockConfiguration< VariationsBlockAttributes >; - -export { metadata, name }; - -export const settings: Partial< - BlockConfiguration< VariationsBlockAttributes > -> = { - example: {}, - edit: Edit, -}; - -export function init() { - return registerWooBlockType( { name, metadata, settings } ); -} diff --git a/packages/js/product-editor/src/components/attribute-control/attribute-control.tsx b/packages/js/product-editor/src/components/attribute-control/attribute-control.tsx index 6c890b3a703..f20ce408abe 100644 --- a/packages/js/product-editor/src/components/attribute-control/attribute-control.tsx +++ b/packages/js/product-editor/src/components/attribute-control/attribute-control.tsx @@ -30,12 +30,14 @@ import { } from './utils'; import { AttributeListItem } from '../attribute-list-item'; import { NewAttributeModal } from './new-attribute-modal'; -import { RemoveConfirmationModal } from './remove-confirmation-modal'; +import { RemoveConfirmationModal } from '../remove-confirmation-modal'; import { TRACKS_SOURCE } from '../../constants'; type AttributeControlProps = { value: EnhancedProductAttribute[]; onAdd?: ( attribute: EnhancedProductAttribute[] ) => void; + onAddAnother?: () => void; + onRemoveItem?: () => void; onChange: ( value: ProductAttribute[] ) => void; onEdit?: ( attribute: ProductAttribute ) => void; onRemove?: ( attribute: ProductAttribute ) => void; @@ -71,6 +73,8 @@ type AttributeControlProps = { export const AttributeControl: React.FC< AttributeControlProps > = ( { value, onAdd = () => {}, + onAddAnother = () => {}, + onRemoveItem = () => {}, onChange, onEdit = () => {}, onNewModalCancel = () => {}, @@ -183,13 +187,6 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( { getAttributeId( newAttr ) === getAttributeId( current ) ) ); - recordEvent( 'product_options_add', { - source: TRACKS_SOURCE, - options: addedAttributesOnly.map( ( attribute ) => ( { - attribute: attribute.name, - values: attribute.options, - } ) ), - } ); handleChange( [ ...value, ...addedAttributesOnly ] ); onAdd( newAttributes ); closeNewModal(); @@ -244,9 +241,6 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( { className="woocommerce-add-attribute-list-item__add-button" onClick={ () => { openNewModal(); - recordEvent( 'product_options_add_button_click', { - source: TRACKS_SOURCE, - } ); } } > { uiStrings.newAttributeListItemLabel } @@ -305,6 +299,8 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( { onNewModalCancel(); } } onAdd={ handleAdd } + onAddAnother={ onAddAnother } + onRemoveItem={ onRemoveItem } selectedAttributeIds={ value.map( ( attr ) => attr.id ) } createNewAttributesAsGlobal={ createNewAttributesAsGlobal } disabledAttributeIds={ disabledAttributeIds } @@ -354,6 +350,7 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( { handleEdit( updatedAttribute ); } } attribute={ currentAttribute } + attributes={ value } /> ) } { removingAttribute && ( @@ -363,7 +360,11 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( { { attributeName: removingAttribute.name } ) } description={ - uiStrings.attributeRemoveConfirmationModalMessage +

+ { + uiStrings.attributeRemoveConfirmationModalMessage + } +

} onRemove={ () => handleRemove( removingAttribute ) } onCancel={ () => { diff --git a/packages/js/product-editor/src/components/attribute-control/edit-attribute-modal.tsx b/packages/js/product-editor/src/components/attribute-control/edit-attribute-modal.tsx index 6e774430d43..281a1765068 100644 --- a/packages/js/product-editor/src/components/attribute-control/edit-attribute-modal.tsx +++ b/packages/js/product-editor/src/components/attribute-control/edit-attribute-modal.tsx @@ -1,14 +1,14 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { Button, Modal, CheckboxControl, TextControl, } from '@wordpress/components'; -import { useState, createElement, Fragment } from '@wordpress/element'; +import { useState, createElement, Fragment, useMemo } from '@wordpress/element'; import { __experimentalTooltip as Tooltip, __experimentalSelectControlMenuSlot as SelectControlMenuSlot, @@ -22,6 +22,8 @@ import { CustomAttributeTermInputField, } from '../attribute-term-input-field'; import { EnhancedProductAttribute } from '../../hooks/use-product-attributes'; +import { Notice } from '../notice'; +import { getAttributeId } from './utils'; type EditAttributeModalProps = { title?: string; @@ -43,6 +45,7 @@ type EditAttributeModalProps = { onCancel: () => void; onEdit: ( alteredAttribute: EnhancedProductAttribute ) => void; attribute: EnhancedProductAttribute; + attributes: EnhancedProductAttribute[]; }; export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( { @@ -77,6 +80,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( { onCancel, onEdit, attribute, + attributes, } ) => { const [ editableAttribute, setEditableAttribute ] = useState< EnhancedProductAttribute | undefined @@ -84,6 +88,71 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( { const isCustomAttribute = editableAttribute?.id === 0; + const { additions, deletions } = useMemo( () => { + if ( ! attribute.variation ) { + return {}; + } + + const variationsSubTotal = attributes + .filter( + ( otherAttribute ) => + getAttributeId( otherAttribute ) !== + getAttributeId( attribute ) + ) + .reduce( + ( subTotal, { terms } ) => subTotal * ( terms?.length ?? 1 ), + 1 + ); + + const currentAttributeTermsCount = attribute.terms?.length ?? 0; + const variationsTotal = variationsSubTotal * currentAttributeTermsCount; + + const addedTermsCount = + editableAttribute?.terms?.filter( + ( editedTerm ) => + ! attribute.terms?.some( + ( currentTerm ) => currentTerm.id === editedTerm.id + ) + )?.length ?? 0; + const addedTermsTotal = + currentAttributeTermsCount + addedTermsCount || 1; + + const remainedTermsCount = + attribute.terms?.filter( ( currentTerm ) => + editableAttribute?.terms?.some( + ( editedTerm ) => currentTerm.id === editedTerm.id + ) + )?.length ?? 0; + + return { + additions: Math.abs( + variationsTotal - variationsSubTotal * addedTermsTotal + ), + deletions: Math.abs( + variationsTotal - variationsSubTotal * remainedTermsCount + ), + }; + }, [ attributes, attribute, editableAttribute ] ); + + function getNoticeMessage() { + const additionsMessage = sprintf( + // translators: %d is the amount of variations to be added + __( '%d variations will be added', 'woocommerce' ), + additions + ); + const deletionsMessage = sprintf( + // translators: %d is the amount of variations to be removed + __( '%d variations will be removed', 'woocommerce' ), + deletions + ); + if ( additions && deletions ) { + return sprintf( '%s, %s.', additionsMessage, deletionsMessage ); + } else if ( additions ) { + return sprintf( '%s.', additionsMessage ); + } + return sprintf( '%s.', deletionsMessage ); + } + return ( <> = ( { { attribute.terms ? ( 0 + ? '' + : termsPlaceholder + } value={ editableAttribute?.terms } attributeId={ editableAttribute?.id } onChange={ ( val ) => { @@ -128,7 +202,12 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( { ) : ( 0 + ? '' + : termsPlaceholder + } disabled={ ! attribute?.name } value={ editableAttribute?.options } onChange={ ( val ) => { @@ -185,6 +264,10 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
) }
+ + { Boolean( additions || deletions ) && ( + { getNoticeMessage() } + ) }
@@ -525,16 +503,6 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { { /* Add slot so select control menu renders correctly within Modal */ } - { showConfirmClose && ( - setShowConfirmClose( false ) } - onConfirm={ onCancel } - > - { confirmMessage } - - ) } ); }; diff --git a/packages/js/product-editor/src/components/attribute-control/remove-confirmation-modal.scss b/packages/js/product-editor/src/components/attribute-control/remove-confirmation-modal.scss deleted file mode 100644 index 1e832ded9b4..00000000000 --- a/packages/js/product-editor/src/components/attribute-control/remove-confirmation-modal.scss +++ /dev/null @@ -1,11 +0,0 @@ -.woocommerce-remove-attribute-modal { - max-width: 650px; - - &__buttons { - margin-top: $gap-larger; - display: flex; - flex-direction: row; - gap: 8px; - justify-content: flex-end; - } -} diff --git a/packages/js/product-editor/src/components/attribute-control/remove-confirmation-modal.tsx b/packages/js/product-editor/src/components/attribute-control/remove-confirmation-modal.tsx deleted file mode 100644 index 876ef370457..00000000000 --- a/packages/js/product-editor/src/components/attribute-control/remove-confirmation-modal.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { createElement } from '@wordpress/element'; -import { Button, Modal } from '@wordpress/components'; - -type RemoveConfirmationModalProps = { - title?: string; - description?: string | React.ReactElement; - onCancel: () => void; - onRemove: () => void; -}; - -export const RemoveConfirmationModal: React.FC< - RemoveConfirmationModalProps -> = ( { - title = __( 'Add attributes', 'woocommerce' ), - description = '', - onCancel, - onRemove, -} ) => { - return ( - - | React.MouseEvent< Element > - | React.FocusEvent< Element > - ) => { - if ( ! event.isPropagationStopped() ) { - onCancel(); - } - } } - className="woocommerce-remove-attribute-modal" - > - { description &&

{ description }

} - -
- - -
-
- ); -}; diff --git a/packages/js/product-editor/src/components/attribute-control/test/attribute-field.spec.tsx b/packages/js/product-editor/src/components/attribute-control/test/attribute-field.spec.tsx index 56f131f853a..13ba918b3f3 100644 --- a/packages/js/product-editor/src/components/attribute-control/test/attribute-field.spec.tsx +++ b/packages/js/product-editor/src/components/attribute-control/test/attribute-field.spec.tsx @@ -79,6 +79,7 @@ jest.mock( '@woocommerce/components', () => ( { __esModule: true, __experimentalSelectControlMenuSlot: () =>
, ListItem: ( { children }: { children: JSX.Element } ) => children, + Tag: ( { label }: { label: string } ) => { label }, Sortable: ( { onOrderChange, children, diff --git a/packages/js/product-editor/src/components/attribute-list-item/attribute-list-item.scss b/packages/js/product-editor/src/components/attribute-list-item/attribute-list-item.scss index bc06bef3f0d..edaeb9c6e24 100644 --- a/packages/js/product-editor/src/components/attribute-list-item/attribute-list-item.scss +++ b/packages/js/product-editor/src/components/attribute-list-item/attribute-list-item.scss @@ -16,20 +16,6 @@ margin-bottom: -1px; } - &__options { - display: flex; - flex-direction: row; - gap: $gap-smallest; - } - - &__option-chip { - padding: $gap-smallest $gap-smaller; - gap: 2px; - - background: $gray-100; - border-radius: 2px; - } - &__actions { display: flex; flex-direction: row; diff --git a/packages/js/product-editor/src/components/attribute-list-item/attribute-list-item.tsx b/packages/js/product-editor/src/components/attribute-list-item/attribute-list-item.tsx index f76e47625a8..32e2c68e030 100644 --- a/packages/js/product-editor/src/components/attribute-list-item/attribute-list-item.tsx +++ b/packages/js/product-editor/src/components/attribute-list-item/attribute-list-item.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { DragEventHandler } from 'react'; -import { ListItem } from '@woocommerce/components'; +import { ListItem, Tag } from '@woocommerce/components'; import { ProductAttribute } from '@woocommerce/data'; import { sprintf, __ } from '@wordpress/i18n'; import { Button, Tooltip } from '@wordpress/components'; @@ -47,24 +47,19 @@ export const AttributeListItem: React.FC< AttributeListItemProps > = ( { onDragEnd={ onDragEnd } >
{ attribute.name }
-
+
{ attribute.options .slice( 0, attribute.options.length > 3 ? 2 : 3 ) - .map( ( option, index ) => ( -
- { option } -
+ .map( ( option ) => ( + ) ) } { attribute.options.length > 3 && ( -
- { sprintf( + + /> ) }
diff --git a/packages/js/product-editor/src/components/attribute-term-input-field/attribute-term-input-field.tsx b/packages/js/product-editor/src/components/attribute-term-input-field/attribute-term-input-field.tsx index f3876b5e64c..1a22767b78a 100644 --- a/packages/js/product-editor/src/components/attribute-term-input-field/attribute-term-input-field.tsx +++ b/packages/js/product-editor/src/components/attribute-term-input-field/attribute-term-input-field.tsx @@ -41,6 +41,7 @@ type AttributeTermInputFieldProps = { disabled?: boolean; label?: string; autoCreateOnSelect?: boolean; + readOnlyWhenClosed?: boolean; }; interface customError extends Error { @@ -60,6 +61,7 @@ export const AttributeTermInputField: React.FC< attributeId, label = '', autoCreateOnSelect = true, + readOnlyWhenClosed = false, } ) => { const attributeTermInputId = useRef( `woocommerce-attribute-term-field-${ ++uniqueId }` @@ -266,6 +268,7 @@ export const AttributeTermInputField: React.FC< selected={ value } onSelect={ onSelect } onRemove={ onRemove } + readOnlyWhenClosed={ readOnlyWhenClosed } className={ 'woocommerce-attribute-term-field ' + attributeTermInputId.current diff --git a/packages/js/product-editor/src/components/attributes/attributes.tsx b/packages/js/product-editor/src/components/attributes/attributes.tsx index 6747ed5e4cd..f7f646d36c2 100644 --- a/packages/js/product-editor/src/components/attributes/attributes.tsx +++ b/packages/js/product-editor/src/components/attributes/attributes.tsx @@ -57,6 +57,16 @@ export const Attributes: React.FC< AttributesProps > = ( { } recordEvent( 'product_add_attribute_button' ); } } + onAddAnother={ () => { + recordEvent( + 'product_add_attributes_modal_add_another_attribute_button_click' + ); + } } + onRemoveItem={ () => { + recordEvent( + 'product_add_attributes_modal_remove_attribute_button_click' + ); + } } onRemove={ () => recordEvent( 'product_remove_attribute_confirmation_confirm_click' diff --git a/packages/js/product-editor/src/components/block-editor/block-editor.tsx b/packages/js/product-editor/src/components/block-editor/block-editor.tsx index 9c069b2b7a7..5bb9dfb893e 100644 --- a/packages/js/product-editor/src/components/block-editor/block-editor.tsx +++ b/packages/js/product-editor/src/components/block-editor/block-editor.tsx @@ -4,7 +4,7 @@ import { synchronizeBlocksWithTemplate, Template } from '@wordpress/blocks'; import { createElement, useMemo, useLayoutEffect } from '@wordpress/element'; import { Product } from '@woocommerce/data'; -import { useSelect, select as WPSelect } from '@wordpress/data'; +import { useDispatch, useSelect, select as WPSelect } from '@wordpress/data'; import { uploadMedia } from '@wordpress/media-utils'; import { // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -32,15 +32,16 @@ import { * Internal dependencies */ import { useConfirmUnsavedProductChanges } from '../../hooks/use-confirm-unsaved-product-changes'; +import { ProductEditorContext } from '../../types'; type BlockEditorProps = { - context: { - [ key: string ]: unknown; - }; - product: Partial< Product >; + context: Partial< ProductEditorContext >; + productType: string; + productId: number; settings: | ( Partial< EditorSettings & EditorBlockListSettings > & { template?: Template[]; + templates: Record< string, Template[] >; } ) | undefined; }; @@ -48,7 +49,8 @@ type BlockEditorProps = { export function BlockEditor( { context, settings: _settings, - product, + productType, + productId, }: BlockEditorProps ) { useConfirmUnsavedProductChanges(); @@ -83,16 +85,28 @@ export function BlockEditor( { const [ blocks, onInput, onChange ] = useEntityBlockEditor( 'postType', - 'product', - { id: product.id } + productType, + { id: productId } ); + const { updateEditorSettings } = useDispatch( 'core/editor' ); + useLayoutEffect( () => { - onChange( - synchronizeBlocksWithTemplate( [], _settings?.template ), - {} - ); - }, [ product.id ] ); + const template = _settings?.templates[ productType ]; + const blockInstances = synchronizeBlocksWithTemplate( [], template ); + + onChange( blockInstances, {} ); + + updateEditorSettings( _settings ?? {} ); + }, [ productType, productId ] ); + + const editedProduct: Product = useSelect( ( select ) => + select( 'core' ).getEditedEntityRecord( + 'postType', + productType, + productId + ) + ); if ( ! blocks ) { return null; @@ -100,7 +114,7 @@ export function BlockEditor( { return (
- + ; +> & { + templates: Record< string, Template[] >; +}; type EditorProps = { - product: Product; + product: Pick< Product, 'id' | 'type' >; + productType?: string; settings: ProductEditorSettings | undefined; }; -export function Editor( { product, settings }: EditorProps ) { +export function Editor( { + product, + productType = 'product', + settings, +}: EditorProps ) { const [ selectedTab, setSelectedTab ] = useState< string | null >( null ); const updatedLayoutContext = useExtendLayout( 'product-block-editor' ); @@ -58,7 +65,7 @@ export function Editor( { product, settings }: EditorProps ) { @@ -69,16 +76,18 @@ export function Editor( { product, settings }: EditorProps ) { header={
} content={ <> @@ -87,16 +96,10 @@ export function Editor( { product, settings }: EditorProps ) { } /> - - { /* We put Footer here instead of in InterfaceSkeleton because Footer uses - WooFooterItem to actually render in the WooFooterItem.Slot defined by - WooCommerce Admin. And, we need to put it outside of the SlotFillProvider - we create in this component. */ } -