diff --git a/.codecov.yml b/.codecov.yml index bec8da4c270b..907ee344cb6e 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,27 +1,46 @@ codecov: - notify: - require_ci_to_pass: yes + notify: + require_ci_to_pass: yes -coverage: - precision: 2 - round: nearest - range: "50...100" +ignore: + - '**/tests' + - '**/test' + - 'tools/**' + - 'packages/js/admin-e2e-tests' + - 'packages/js/api-core-tests' + - 'packages/js/create-woo-extension' + - 'packages/js/e2e-core-tests' + - 'packages/js/e2e-environment' + - 'packages/js/e2e-utils' + - 'packages/js/eslint-plugin' + - 'packages/js/internal-e2e-builds' + - 'packages/js/internal-js-tests' + - 'packages/js/internal-style-build' + - '**/*.test.*' - status: - project: - default: - informational: true - patch: - default: - informational: true - changes: off +coverage: + precision: 1 + round: nearest + range: '50...80' + status: + project: + default: + target: auto + patch: + default: + target: auto parsers: - gcov: - branch_detection: - conditional: yes - loop: yes - method: no - macro: no + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no -comment: false +comment: + layout: 'reach, diff, flags, files' + behavior: default + require_changes: false + require_base: no + require_head: yes diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7114392b6020..2ad59418aac5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -22,7 +22,9 @@ Closes # . ### How to test the changes in this Pull Request: - + + +- [ ] Have you followed the [Writing high-quality testing instructions guide](https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions)? 1. 2. diff --git a/.github/actions/setup-woocommerce-monorepo/action.yml b/.github/actions/setup-woocommerce-monorepo/action.yml index 32eceb91e7d0..ec3868f9db83 100644 --- a/.github/actions/setup-woocommerce-monorepo/action.yml +++ b/.github/actions/setup-woocommerce-monorepo/action.yml @@ -32,21 +32,21 @@ runs: version: '^7.22.0' - name: Setup Node - uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 + uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c with: node-version-file: .nvmrc cache: pnpm registry-url: 'https://registry.npmjs.org' - name: Setup PHP - uses: shivammathur/setup-php@e04e1d97f0c0481c6e1ba40f8a538454fe5d7709 + uses: shivammathur/setup-php@8e2ac35f639d3e794c1da1f28999385ab6fdf0fc with: php-version: ${{ inputs.php-version }} coverage: none tools: phpcs, sirbrillig/phpcs-changed - name: Cache Composer Dependencies - uses: actions/cache@fd5de65bc895cf536527842281bea11763fefd77 + uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12 with: path: ~/.cache/composer/files key: ${{ runner.os }}-php-${{ inputs.php-version }}-composer-${{ hashFiles('**/composer.lock') }} @@ -59,7 +59,7 @@ runs: pnpm install ${{ steps.parse-input.outputs.INSTALL_FILTERS }} - name: Cache Build Output - uses: actions/cache@fd5de65bc895cf536527842281bea11763fefd77 + uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12 with: path: node_modules/.cache/turbo key: ${{ runner.os }}-build-output-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }} diff --git a/.github/workflows/community-label.yml b/.github/workflows/community-label.yml index b4d772af526c..3361dfa033ba 100644 --- a/.github/workflows/community-label.yml +++ b/.github/workflows/community-label.yml @@ -17,14 +17,14 @@ jobs: name: Verify runs-on: ubuntu-20.04 permissions: - contents: read - pull-requests: write - issues: write + contents: read + pull-requests: write + issues: write steps: - uses: actions/checkout@v3 - name: Setup Node.js - uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 + uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c - name: Install Octokit run: npm --prefix .github/workflows/scripts install @octokit/action diff --git a/.github/workflows/prepare-package-release.yml b/.github/workflows/prepare-package-release.yml index 0f311150cf1c..3eb728e21300 100644 --- a/.github/workflows/prepare-package-release.yml +++ b/.github/workflows/prepare-package-release.yml @@ -14,8 +14,8 @@ jobs: name: Run prepare script runs-on: ubuntu-20.04 permissions: - contents: read - pull-requests: write + contents: write + pull-requests: write steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/pull-request-post-merge-processing.yml b/.github/workflows/pull-request-post-merge-processing.yml index 208cf6736cb9..645d373c8b4f 100644 --- a/.github/workflows/pull-request-post-merge-processing.yml +++ b/.github/workflows/pull-request-post-merge-processing.yml @@ -1,43 +1,47 @@ -name: "Pull request post-merge processing" +name: 'Pull request post-merge processing' on: - pull_request_target: - types: [closed] + pull_request_target: + types: [closed] + paths: + - 'packages/**' + - 'plugins/woocommerce/**' + - 'plugins/woocommerce-admin/**' permissions: {} jobs: - process-pull-request-after-merge: - name: "Process a pull request after it's merged" - if: github.event.pull_request.merged == true - runs-on: ubuntu-20.04 - permissions: - pull-requests: write - steps: - - name: "Get the action scripts" - run: | - scripts="assign-milestone-to-merged-pr.php add-post-merge-comment.php post-request-shared.php" - for script in $scripts - do - curl \ - --silent \ - --fail \ - --header 'Authorization: bearer ${{ secrets.GITHUB_TOKEN }}' \ - --header 'User-Agent: GitHub action to set the milestone for a pull request' \ - --header 'Accept: application/vnd.github.v3.raw' \ - --output $script \ - --location "$GITHUB_API_URL/repos/${{ github.repository }}/contents/.github/workflows/scripts/$script?ref=${{ github.event.pull_request.base.ref }}" - done - env: - GITHUB_API_URL: ${{ env.GITHUB_API_URL }} - - name: "Install PHP" - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - - name: "Run the script to assign a milestone" - if: | - !github.event.pull_request.milestone && - github.event.pull_request.base.ref == 'trunk' - run: php assign-milestone-to-merged-pr.php - env: - PULL_REQUEST_ID: ${{ github.event.pull_request.node_id }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + process-pull-request-after-merge: + name: "Process a pull request after it's merged" + if: github.event.pull_request.merged == true + runs-on: ubuntu-20.04 + permissions: + pull-requests: write + steps: + - name: 'Get the action scripts' + run: | + scripts="assign-milestone-to-merged-pr.php add-post-merge-comment.php post-request-shared.php" + for script in $scripts + do + curl \ + --silent \ + --fail \ + --header 'Authorization: bearer ${{ secrets.GITHUB_TOKEN }}' \ + --header 'User-Agent: GitHub action to set the milestone for a pull request' \ + --header 'Accept: application/vnd.github.v3.raw' \ + --output $script \ + --location "$GITHUB_API_URL/repos/${{ github.repository }}/contents/.github/workflows/scripts/$script?ref=${{ github.event.pull_request.base.ref }}" + done + env: + GITHUB_API_URL: ${{ env.GITHUB_API_URL }} + - name: 'Install PHP' + uses: shivammathur/setup-php@8e2ac35f639d3e794c1da1f28999385ab6fdf0fc + with: + php-version: '7.4' + - name: 'Run the script to assign a milestone' + if: | + !github.event.pull_request.milestone && + github.event.pull_request.base.ref == 'trunk' + run: php assign-milestone-to-merged-pr.php + env: + PULL_REQUEST_ID: ${{ github.event.pull_request.node_id }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-code-freeze.yml b/.github/workflows/release-code-freeze.yml index 1a73fddb62b8..3e49e999aec8 100644 --- a/.github/workflows/release-code-freeze.yml +++ b/.github/workflows/release-code-freeze.yml @@ -30,7 +30,7 @@ jobs: freeze: ${{ steps.check-freeze.outputs.freeze }} steps: - name: 'Install PHP' - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@8e2ac35f639d3e794c1da1f28999385ab6fdf0fc with: php-version: '7.4' diff --git a/.github/workflows/scripts/release-changelog.php b/.github/workflows/scripts/release-changelog.php index c66f641140d3..3dfad7c7480f 100644 --- a/.github/workflows/scripts/release-changelog.php +++ b/.github/workflows/scripts/release-changelog.php @@ -10,8 +10,8 @@ $base_dir = dirname( dirname( dirname( __DIR__ ) ) ); -// The release date is 26 days after the code freeze. -$release_time = strtotime( '+26 days', $now ); +// The release date is 22 days after the code freeze. +$release_time = strtotime( '+22 days', $now ); $release_date = date( 'Y-m-d', $release_time ); $readme_file = $base_dir . '/plugins/woocommerce/readme.txt'; diff --git a/.github/workflows/smoke-test-daily.yml b/.github/workflows/smoke-test-daily.yml index 7c5b9ea78eaf..bf4e89ea759f 100644 --- a/.github/workflows/smoke-test-daily.yml +++ b/.github/workflows/smoke-test-daily.yml @@ -17,22 +17,14 @@ concurrency: permissions: {} jobs: - e2e-tests: - name: E2E tests on nightly build + api-tests: + name: API tests on nightly build runs-on: ubuntu-20.04 permissions: contents: read env: - ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} - ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }} - ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }} - ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-report - ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-results - BASE_URL: ${{ secrets.SMOKE_TEST_URL }} - CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }} - CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }} - DEFAULT_TIMEOUT_OVERRIDE: 120000 - + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-report steps: - uses: actions/checkout@v3 with: @@ -44,50 +36,48 @@ jobs: install-filters: woocommerce build: false - - name: Download and install Chromium browser. - working-directory: plugins/woocommerce - run: pnpm exec playwright install chromium - - - name: Run 'Update WooCommerce' test. - working-directory: plugins/woocommerce - env: - UPDATE_WC: true - run: pnpm exec playwright test --config=tests/e2e-pw/daily.playwright.config.js update-woocommerce.spec.js - - - name: Run the rest of E2E tests. - timeout-minutes: 60 + - name: Run API tests. working-directory: plugins/woocommerce env: - E2E_MAX_FAILURES: 25 - RESET_SITE: true - run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js + BASE_URL: ${{ secrets.SMOKE_TEST_URL }} + USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }} + USER_SECRET: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} + DEFAULT_TIMEOUT_OVERRIDE: 120000 + run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js hello.test.js - - name: Generate Playwright E2E Test report. + - name: Generate API Test report. if: success() || failure() working-directory: plugins/woocommerce run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - - name: Archive E2E test report + - name: Archive API test report if: success() || failure() uses: actions/upload-artifact@v3 with: - name: ${{ env.E2E_ARTIFACT }} + name: ${{ env.API_ARTIFACT }} path: | ${{ env.ALLURE_RESULTS_DIR }} ${{ env.ALLURE_REPORT_DIR }} if-no-files-found: ignore retention-days: 5 - - api-tests: - name: API tests on nightly build + + e2e-tests: + name: E2E tests on nightly build runs-on: ubuntu-20.04 permissions: contents: read - needs: [e2e-tests] - if: success() || failure() + needs: [api-tests] env: - ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-results - ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-report + ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} + ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }} + ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }} + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-report + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-results + BASE_URL: ${{ secrets.SMOKE_TEST_URL }} + CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }} + CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }} + DEFAULT_TIMEOUT_OVERRIDE: 120000 + steps: - uses: actions/checkout@v3 with: @@ -99,25 +89,34 @@ jobs: install-filters: woocommerce build: false - - name: Run API tests. + - name: Download and install Chromium browser. + working-directory: plugins/woocommerce + run: pnpm exec playwright install chromium + + - name: Run 'Update WooCommerce' test. working-directory: plugins/woocommerce env: - BASE_URL: ${{ secrets.SMOKE_TEST_URL }} - USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }} - USER_SECRET: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }} - DEFAULT_TIMEOUT_OVERRIDE: 120000 - run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js hello.test.js + UPDATE_WC: true + run: pnpm exec playwright test --config=tests/e2e-pw/daily.playwright.config.js update-woocommerce.spec.js - - name: Generate API Test report. + - name: Run the rest of E2E tests. + timeout-minutes: 60 + working-directory: plugins/woocommerce + env: + E2E_MAX_FAILURES: 25 + RESET_SITE: true + run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js + + - name: Generate Playwright E2E Test report. if: success() || failure() working-directory: plugins/woocommerce run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - - name: Archive API test report + - name: Archive E2E test report if: success() || failure() uses: actions/upload-artifact@v3 with: - name: ${{ env.API_ARTIFACT }} + name: ${{ env.E2E_ARTIFACT }} path: | ${{ env.ALLURE_RESULTS_DIR }} ${{ env.ALLURE_REPORT_DIR }} @@ -181,6 +180,7 @@ jobs: runs-on: ubuntu-20.04 permissions: contents: read + needs: [api-tests] env: USE_WP_ENV: 1 ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results @@ -256,7 +256,7 @@ jobs: runs-on: ubuntu-20.04 permissions: contents: read - needs: [test-plugins, k6-tests] + needs: [e2e-tests, test-plugins, k6-tests] steps: - name: Create dirs run: | @@ -312,7 +312,7 @@ jobs: ( success() || failure() ) && ! github.event.pull_request.head.repo.fork runs-on: ubuntu-20.04 - needs: [test-plugins, k6-tests] + needs: [e2e-tests, test-plugins, k6-tests] env: GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} RUN_ID: ${{ github.run_id }} diff --git a/.github/workflows/syncpack.yml b/.github/workflows/syncpack.yml index 56fb71d09643..bfa6f58d262b 100644 --- a/.github/workflows/syncpack.yml +++ b/.github/workflows/syncpack.yml @@ -1,37 +1,37 @@ name: Synchronize Dependencies with syncpack on: - # Run whenever a pull request is updated - pull_request: - branches: - - trunk - paths: - - '**/package.json' + # Run whenever a pull request is updated + pull_request: + branches: + - trunk + paths: + - '**/package.json' permissions: {} jobs: - syncpack: - runs-on: ubuntu-latest - permissions: - contents: read - name: syncpack - steps: - - name: 'Checkout' - uses: actions/checkout@v3 - - - name: 'Setup node' - uses: actions/setup-node@v3 - with: - node-version: 16 + syncpack: + runs-on: ubuntu-latest + permissions: + contents: read + name: syncpack + steps: + - name: 'Checkout' + uses: actions/checkout@v3 - - name: 'Install Syncpack' - run: npm install -g syncpack@^8.2.4 - - - name: 'List Mismatches' - run: syncpack list-mismatches - - - name: 'Explain Remedy' - if: failure() - run: | - echo "Dependency version mismatch detected. This can usually be fixed automatically by updating the pinned version in \`.syncpackrc\` and then running: \`pnpm run sync-dependencies\`" - exit 1 + - name: 'Setup node' + uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c + with: + node-version: 16 + + - name: 'Install Syncpack' + run: npm install -g syncpack@^8.2.4 + + - name: 'List Mismatches' + run: syncpack list-mismatches + + - name: 'Explain Remedy' + if: failure() + run: | + echo "Dependency version mismatch detected. This can usually be fixed automatically by updating the pinned version in \`.syncpackrc\` and then running: \`pnpm run sync-dependencies\`" + exit 1 diff --git a/.husky/post-checkout b/.husky/post-checkout new file mode 100755 index 000000000000..1fd4a5b59419 --- /dev/null +++ b/.husky/post-checkout @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +pnpm install diff --git a/.syncpackrc b/.syncpackrc index 4ec2e4ff05ee..8f228343559a 100644 --- a/.syncpackrc +++ b/.syncpackrc @@ -1,6 +1,6 @@ { "dev": true, - "filter": "^(?:react|react-dom|typescript|@typescript-eslint|@types/react).*$", + "filter": "^(?:config|react|react-dom|eslint|typescript|@typescript-eslint|@types/react).*$", "indent": "\t", "overrides": true, "peer": true, @@ -33,6 +33,15 @@ "**" ] }, + { + "dependencies": [ + "config" + ], + "packages": [ + "**" + ], + "pinVersion": "3.3.7" + }, { "dependencies": [ "react", @@ -51,6 +60,18 @@ "**" ], "pinVersion": "^4.8.3" + }, + { + "dependencies": [ + "eslint" + ], + "dependencyTypes": [ + "devDependencies" + ], + "packages": [ + "**" + ], + "pinVersion": "^8.32.0" } ] } diff --git a/bin/pre-push.sh b/bin/pre-push.sh index 024e1f21b3ae..9651af08fda3 100755 --- a/bin/pre-push.sh +++ b/bin/pre-push.sh @@ -31,8 +31,9 @@ if [ $? -ne 0 ]; then exit 1 fi -# Ensure both branches are tracked or check-changelogger-use will fail. -git checkout $PROTECTED_BRANCH --quiet -git checkout $CURRENT_BRANCH --quiet +# Ensure both branches are tracked or check-changelogger-use will fail. Note we pass hooksPath +# to avoid running the pre-commit hook. +git -c core.hooksPath=/dev/null checkout $PROTECTED_BRANCH --quiet +git -c core.hooksPath=/dev/null checkout $CURRENT_BRANCH --quiet php tools/monorepo/check-changelogger-use.php $PROTECTED_BRANCH $CURRENT_BRANCH diff --git a/changelog.txt b/changelog.txt index bb51ad542559..4453a125fe9f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,138 @@ == Changelog == += 7.4.0 2023-02-14 = + +**WooCommerce** + +* Fix - Add support for sorting by includes param. [#36215](https://github.com/woocommerce/woocommerce/pull/36215) +* Fix - Allow product tab navigation without prompting for unsaved changes [#36235](https://github.com/woocommerce/woocommerce/pull/36235) +* Fix - Convert HTML to blocks in product variation description [#36241](https://github.com/woocommerce/woocommerce/pull/36241) +* Fix - Decode HTML entities in CategoryBreadcrumbs. [#36321](https://github.com/woocommerce/woocommerce/pull/36321) +* Fix - Decode HTML entities in CategoryFieldItem. [#36367](https://github.com/woocommerce/woocommerce/pull/36367) +* Fix - Ensure order emails are responsive in most email clients, including when the current language is RTL. [#36310](https://github.com/woocommerce/woocommerce/pull/36310) +* Fix - Ensures product variation sort order is correctly persisted. [#36343](https://github.com/woocommerce/woocommerce/pull/36343) +* Fix - Ensure wc_get_order() works without arguments when HPOS is enabled. [#36496](https://github.com/woocommerce/woocommerce/pull/36496) +* Fix - Fix "Save changes?" modal saves the options after selecting the 'Discard' option [#36160](https://github.com/woocommerce/woocommerce/pull/36160) +* Fix - Fix attributes/options lists corrupt render #36236 [#36236](https://github.com/woocommerce/woocommerce/pull/36236) +* Fix - Fix bug when filtering for customer_id=0. [#36216](https://github.com/woocommerce/woocommerce/pull/36216) +* Fix - Fix deprecated usage of ${var} in strings [#36439](https://github.com/woocommerce/woocommerce/pull/36439) +* Fix - Fix edit attribute modal terms list [#36186](https://github.com/woocommerce/woocommerce/pull/36186) +* Fix - Fixes editing of child product reviews. [#35888](https://github.com/woocommerce/woocommerce/pull/35888) +* Fix - Fix for product filters when 'shop' page is the front page. [#36224](https://github.com/woocommerce/woocommerce/pull/36224) +* Fix - Fix issue where attribute term dropdown was not adhering to sort order setting. [#36047](https://github.com/woocommerce/woocommerce/pull/36047) +* Fix - Fix navigation between variations and tab selection [#36239](https://github.com/woocommerce/woocommerce/pull/36239) +* Fix - Fix notices styling in Twenty Twenty-Three [#36475](https://github.com/woocommerce/woocommerce/pull/36475) +* Fix - Fix overlapping header elements on product page [#36495](https://github.com/woocommerce/woocommerce/pull/36495) +* Fix - Fix product table dropdown issue on mobile. [#36046](https://github.com/woocommerce/woocommerce/pull/36046) +* Fix - Fix reordering list items error [#36296](https://github.com/woocommerce/woocommerce/pull/36296) +* Fix - Fix REST API order refunds enpoint when HPOS is active, and make v2 orders endpoint compatible with HPOS [#36308](https://github.com/woocommerce/woocommerce/pull/36308) +* Fix - Fix settings tables styles [#36531](https://github.com/woocommerce/woocommerce/pull/36531) +* Fix - Fix tax task showing as not completed after setting up tax [#36468](https://github.com/woocommerce/woocommerce/pull/36468) +* Fix - Fix the signature mismatch affecting wc cli commands ability to fetch user subscription data. [#36240](https://github.com/woocommerce/woocommerce/pull/36240) +* Fix - Fix total count query of orders within Analytics reports data store. [#35971](https://github.com/woocommerce/woocommerce/pull/35971) +* Fix - Hide Variations section when it is empty [#36202](https://github.com/woocommerce/woocommerce/pull/36202) +* Fix - Improve accessibility of the coupon code label, in the context of the cart page. [#36247](https://github.com/woocommerce/woocommerce/pull/36247) +* Fix - Improve the way we retrieve the alt text property for product attachments. [#35009](https://github.com/woocommerce/woocommerce/pull/35009) +* Fix - Load wc_empty_cart function for REST API calls. [#36182](https://github.com/woocommerce/woocommerce/pull/36182) +* Fix - Make HPOS UX more consistent with posts UI (so that same e2e tests passes for both). [#36282](https://github.com/woocommerce/woocommerce/pull/36282) +* Fix - Make order edit messages compatible with both posts and theorder object. [#36485](https://github.com/woocommerce/woocommerce/pull/36485) +* Fix - Make sure the tracking shortcode only operates in orders with billing information. [#33735](https://github.com/woocommerce/woocommerce/pull/33735) +* Fix - Remove persisted query on return to parent product from variation [#36365](https://github.com/woocommerce/woocommerce/pull/36365) +* Fix - Reset variation form if a new variation is given [#36078](https://github.com/woocommerce/woocommerce/pull/36078) +* Fix - Restore the pre-7.2.0 behavior for single product quantity inputs. [#36460](https://github.com/woocommerce/woocommerce/pull/36460) +* Fix - Set child orders to be children of current order parent before deleting for consistency. [#36218](https://github.com/woocommerce/woocommerce/pull/36218) +* Fix - Skip custom search for HPOS API queries as it's handled already. [#36213](https://github.com/woocommerce/woocommerce/pull/36213) +* Fix - Use Imagick functions to set parallel thread count instead of direct putenv call as suggested in https://core.trac.wordpress.org/ticket/36534#comment:129. [#35339](https://github.com/woocommerce/woocommerce/pull/35339) +* Fix - When adjusting download permissions, confirm the child products have not been removed. [#36431](https://github.com/woocommerce/woocommerce/pull/36431) +* Add - Add ability to filter variations by local attributes in REST API [#36201](https://github.com/woocommerce/woocommerce/pull/36201) +* Add - Add an admin notice about the upcoming PHP version requirement change for PHP 7.2 users [#36444](https://github.com/woocommerce/woocommerce/pull/36444) +* Add - Added a slot for extending the app with a homescreen header banner [#36467](https://github.com/woocommerce/woocommerce/pull/36467) +* Add - Added a slot for ProgressHeader and ProgressTitle component [#36482](https://github.com/woocommerce/woocommerce/pull/36482) +* Add - Add edit button to variations list items [#36079](https://github.com/woocommerce/woocommerce/pull/36079) +* Add - Added slot for tasklist completion slotfill [#36487](https://github.com/woocommerce/woocommerce/pull/36487) +* Add - Add endpoint to create all product variations [#35980](https://github.com/woocommerce/woocommerce/pull/35980) +* Add - Add exit prompt CES for users editing orders when tracking is enabled. [#35762](https://github.com/woocommerce/woocommerce/pull/35762) +* Add - Adding delayed spotlight to feedback button on current product page. [#35865](https://github.com/woocommerce/woocommerce/pull/35865) +* Add - Adding feedback button to activity bar on classic product page. [#35810](https://github.com/woocommerce/woocommerce/pull/35810) +* Add - Adding JS data store for ProductForm. [#36430](https://github.com/woocommerce/woocommerce/pull/36430) +* Add - Adding the WooProductSectionItem slot within the product editor general tab. [#36331](https://github.com/woocommerce/woocommerce/pull/36331) +* Add - Add initial product form PHP helper class to add new fields. [#36093](https://github.com/woocommerce/woocommerce/pull/36093) +* Add - Additional error logging within the CSV Exporter framework. [#34802](https://github.com/woocommerce/woocommerce/pull/34802) +* Add - Add multichannel marketing API [#36453](https://github.com/woocommerce/woocommerce/pull/36453) +* Add - Add new filter to add additional clauses for SQL statement in Variations report [#36378](https://github.com/woocommerce/woocommerce/pull/36378) +* Add - Add new product form API for extending the new Product Form MVP. [#36165](https://github.com/woocommerce/woocommerce/pull/36165) +* Add - Add Options section to new product experience form. [#35910](https://github.com/woocommerce/woocommerce/pull/35910) +* Add - Add product tour to new product management experience [#36428](https://github.com/woocommerce/woocommerce/pull/36428) +* Add - Add product variation form [#36033](https://github.com/woocommerce/woocommerce/pull/36033) +* Add - Add product variation General section [#36081](https://github.com/woocommerce/woocommerce/pull/36081) +* Add - Add product variation header actions and persistence [#36155](https://github.com/woocommerce/woocommerce/pull/36155) +* Add - Add product variation image [#36133](https://github.com/woocommerce/woocommerce/pull/36133) +* Add - Add product variation navigation component [#36076](https://github.com/woocommerce/woocommerce/pull/36076) +* Add - Add product variations flag to only show work in development [#36311](https://github.com/woocommerce/woocommerce/pull/36311) +* Add - Add product variation title to page header [#36085](https://github.com/woocommerce/woocommerce/pull/36085) +* Add - Add Product variation visibility toggle [#36020](https://github.com/woocommerce/woocommerce/pull/36020) +* Add - Add single product variation sections [#36051](https://github.com/woocommerce/woocommerce/pull/36051) +* Add - Adds support for a 'required' argument when invoking `wc_dropdown_variation_attribute_options()`. [#34579](https://github.com/woocommerce/woocommerce/pull/34579) +* Add - Add support for sorting by order metadata in HPOS queries. [#36403](https://github.com/woocommerce/woocommerce/pull/36403) +* Add - Add WooOnboardingTaskListHeader, woocommerce_admin_experimental_onboarding_tasklists filter, and woocommerce_onboarding_task_list_header Slot to task list [#36519](https://github.com/woocommerce/woocommerce/pull/36519) +* Add - Include tax options in pricing section [#36299](https://github.com/woocommerce/woocommerce/pull/36299) +* Add - Persist active tab on refresh [#36112](https://github.com/woocommerce/woocommerce/pull/36112) +* Add - Persist variations order on product save [#36109](https://github.com/woocommerce/woocommerce/pull/36109) +* Add - Product variation quantity status indicator [#35982](https://github.com/woocommerce/woocommerce/pull/35982) +* Add - Product variations card should have a fixed height. [#36053](https://github.com/woocommerce/woocommerce/pull/36053) +* Add - Remove manage_stock 'parent' value before saving the variation [#36234](https://github.com/woocommerce/woocommerce/pull/36234) +* Add - Run ces exit prompt when product import abandoned. [#35996](https://github.com/woocommerce/woocommerce/pull/35996) +* Add - Scroll newly added product attribute into view in new product management experience [#36447](https://github.com/woocommerce/woocommerce/pull/36447) +* Add - Show product CES footer on product tour close [#36516](https://github.com/woocommerce/woocommerce/pull/36516) +* Add - Truncate attribute option name to a max of 32 chars in variations list [#36134](https://github.com/woocommerce/woocommerce/pull/36134) +* Add - Trying experimental slot context with product editor fills. [#36333](https://github.com/woocommerce/woocommerce/pull/36333) +* Add - Using slotfill to insert attributes section in the product editor. [#36483](https://github.com/woocommerce/woocommerce/pull/36483) +* Add - Using slotfill to insert images section in product editor. [#36461](https://github.com/woocommerce/woocommerce/pull/36461) +* Update - Update woocommerce-blocks to 9.4.3. [#36736](https://github.com/woocommerce/woocommerce/pull/36736) +* Update - Adding WooProductFieldItem slot to product details section. [#36315](https://github.com/woocommerce/woocommerce/pull/36315) +* Update - Add permalink_template and generated_slug to products REST API response. [#36497](https://github.com/woocommerce/woocommerce/pull/36497) +* Update - Auto generate variations on option changes [#36188](https://github.com/woocommerce/woocommerce/pull/36188) +* Update - Bundled version of Action Scheduler updated to 3.5.4. [#36433](https://github.com/woocommerce/woocommerce/pull/36433) +* Update - Customers REST API endpoint will now return user metadata only when requester has an administrator role [#36408](https://github.com/woocommerce/woocommerce/pull/36408) +* Update - Disable irrelevant product tabs when variations exist [#35939](https://github.com/woocommerce/woocommerce/pull/35939) +* Update - Migrate shipping section in product editor to slot fill. [#36534](https://github.com/woocommerce/woocommerce/pull/36534) +* Update - Move product management feature flag down to experimental. [#36552](https://github.com/woocommerce/woocommerce/pull/36552) +* Update - Reimplementing product details fields in product editor as slot fills. [#36368](https://github.com/woocommerce/woocommerce/pull/36368) +* Update - Update api-core-tests readme to include a guide for writing tests [#35978](https://github.com/woocommerce/woocommerce/pull/35978) +* Update - Update store-details test snapshot to reflect updated select-control [#35808](https://github.com/woocommerce/woocommerce/pull/35808) +* Update - Update WooCommerce Blocks to 9.4.0 [#36524](https://github.com/woocommerce/woocommerce/pull/36524) +* Update - Update WooCommerce Blocks to 9.4.1 [#36553](https://github.com/woocommerce/woocommerce/pull/36553) +* Update - Update WooCommerce Blocks to 9.4.2 [#36624](https://github.com/woocommerce/woocommerce/pull/36624) +* Dev - Add advanced setting option [#36380](https://github.com/woocommerce/woocommerce/pull/36380) +* Dev - Add experimental SlotFill for task list footer [#36527](https://github.com/woocommerce/woocommerce/pull/36527) +* Dev - Cleanup product task experiment [#35950](https://github.com/woocommerce/woocommerce/pull/35950) +* Dev - Consistent folder structure for E2E and API test results [#35907](https://github.com/woocommerce/woocommerce/pull/35907) +* Dev - Fix docblock type annotations for $meta_value. [#33853](https://github.com/woocommerce/woocommerce/pull/33853) +* Dev - Fix flakiness of the `can save industry changes when navigating back to "Store Details"` E2E test. [#36260](https://github.com/woocommerce/woocommerce/pull/36260) +* Dev - Make shopper tests passable on daily smoke test site. [#35873](https://github.com/woocommerce/woocommerce/pull/35873) +* Dev - Move product attribute fetching logic into a separate hook [#36354](https://github.com/woocommerce/woocommerce/pull/36354) +* Dev - Update TaskLists::add_task() to reflect changes in TaskList::add_task() [#36104](https://github.com/woocommerce/woocommerce/pull/36104) +* Dev - Update the browserslist config for legacy client JS to match Wordpress. [#36264](https://github.com/woocommerce/woocommerce/pull/36264) +* Dev - Upgrade PHPUnit to v8 [#36273](https://github.com/woocommerce/woocommerce/pull/36273) +* Tweak - Corrects a typo in the i18n/states.php file, relating to our list of Iranian states. [#36457](https://github.com/woocommerce/woocommerce/pull/36457) +* Tweak - Derive product type from product attributes [#36243](https://github.com/woocommerce/woocommerce/pull/36243) +* Tweak - Fix typo in a function comment. [#36122](https://github.com/woocommerce/woocommerce/pull/36122) +* Tweak - Fix units in function doc comment [#36353](https://github.com/woocommerce/woocommerce/pull/36353) +* Tweak - Make related products check more robust against wrong transients. [#34742](https://github.com/woocommerce/woocommerce/pull/34742) +* Tweak - Makes it possible to use an `add_meta_boxes_` style hook in the HPOS editor, for parity with the traditional post editor. [#35999](https://github.com/woocommerce/woocommerce/pull/35999) +* Tweak - Minor adjustments to the ProductForm API [#36414](https://github.com/woocommerce/woocommerce/pull/36414) +* Tweak - Redirect to new product experience when in experiment group [#36381](https://github.com/woocommerce/woocommerce/pull/36381) +* Tweak - Refactor AttributeField into sub-components. [#35997](https://github.com/woocommerce/woocommerce/pull/35997) +* Tweak - Update product links when new product management experience is enabled [#36382](https://github.com/woocommerce/woocommerce/pull/36382) +* Tweak - Updates and improves the docblocks for methods WC_Order::get_total() and WC_Order::get_subtotal(). [#34385](https://github.com/woocommerce/woocommerce/pull/34385) +* Tweak - Validation of Norweigan postcodes has been added. [#36277](https://github.com/woocommerce/woocommerce/pull/36277) +* Performance - Speed up HPOS search query by using group by instead of distinct. [#35897](https://github.com/woocommerce/woocommerce/pull/35897) +* Enhancement - Add context to countries shipping to prefix [#36254](https://github.com/woocommerce/woocommerce/pull/36254) +* Enhancement - Adds new order status filters for bacs and cheque email instructions. [#35849](https://github.com/woocommerce/woocommerce/pull/35849) +* Enhancement - Improves handling of the single product page quantity selector, in relation to variable products. [#36087](https://github.com/woocommerce/woocommerce/pull/36087) +* Enhancement - Remove default WooCommerce button styles if using a block theme which adds button styles in theme.json [#36225](https://github.com/woocommerce/woocommerce/pull/36225) + + = 7.3.0 2023-01-10 = **WooCommerce** diff --git a/package.json b/package.json index 1e35a6592065..54868064a869 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "sass": "^1.49.9", "sass-loader": "^10.2.1", "syncpack": "^8.3.9", - "turbo": "^1.4.5", + "turbo": "^1.7.0", "typescript": "^4.8.3", "url-loader": "^1.1.2", "webpack": "^5.70.0" diff --git a/packages/js/admin-e2e-tests/changelog/dev-consolidate-eslint-versions b/packages/js/admin-e2e-tests/changelog/dev-consolidate-eslint-versions new file mode 100644 index 000000000000..d3d95c39119b --- /dev/null +++ b/packages/js/admin-e2e-tests/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/admin-e2e-tests/changelog/update-bump-config b/packages/js/admin-e2e-tests/changelog/update-bump-config new file mode 100644 index 000000000000..c89120d38926 --- /dev/null +++ b/packages/js/admin-e2e-tests/changelog/update-bump-config @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Unify semver range for `config@3.3.7` (from `^3.3.7`). Fix `node_env_var_name is not defined` error. diff --git a/packages/js/admin-e2e-tests/package.json b/packages/js/admin-e2e-tests/package.json index 93926703248f..51bffa6e593c 100644 --- a/packages/js/admin-e2e-tests/package.json +++ b/packages/js/admin-e2e-tests/package.json @@ -29,7 +29,7 @@ "@jest/globals": "^27.5.1", "@types/jest": "^27.4.1", "@woocommerce/e2e-utils": "workspace:*", - "config": "^3.3.7" + "config": "3.3.7" }, "peerDependencies": { "@woocommerce/e2e-environment": "^0.2.3 || ^0.3.0", @@ -44,7 +44,7 @@ "@typescript-eslint/eslint-plugin": "^5.43.0", "@woocommerce/api": "^0.2.0", "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "jest-mock-extended": "^1.0.18", diff --git a/packages/js/api-core-tests/package.json b/packages/js/api-core-tests/package.json index d176de4d7b67..aac5f23762ee 100644 --- a/packages/js/api-core-tests/package.json +++ b/packages/js/api-core-tests/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "@woocommerce/eslint-plugin": "workspace:*", - "eslint": "^8.12.0" + "eslint": "^8.32.0" }, "publishConfig": { "access": "public" diff --git a/packages/js/api/changelog/dev-consolidate-eslint-versions b/packages/js/api/changelog/dev-consolidate-eslint-versions new file mode 100644 index 000000000000..d3d95c39119b --- /dev/null +++ b/packages/js/api/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/packages/js/api/package.json b/packages/js/api/package.json index b88f65e0018c..78562c197947 100644 --- a/packages/js/api/package.json +++ b/packages/js/api/package.json @@ -55,7 +55,7 @@ "@typescript-eslint/parser": "^5.43.0", "@woocommerce/eslint-plugin": "workspace:*", "axios-mock-adapter": "^1.20.0", - "eslint": "^8.2.0", + "eslint": "^8.32.0", "jest": "^27", "ts-jest": "^27", "typescript": "^4.8.3" diff --git a/packages/js/components/changelog/add-35851-tree-control-expander b/packages/js/components/changelog/add-35851-tree-control-expander new file mode 100644 index 000000000000..943ea1225d56 --- /dev/null +++ b/packages/js/components/changelog/add-35851-tree-control-expander @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add TreeControl expand/collapse functionality. diff --git a/packages/js/components/changelog/add-36248-added-flexibility-to-plugins b/packages/js/components/changelog/add-36248-added-flexibility-to-plugins new file mode 100644 index 000000000000..822bad3a2cb4 --- /dev/null +++ b/packages/js/components/changelog/add-36248-added-flexibility-to-plugins @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Added LearnMore option as well as made it possible to use this button multiple instances on the page diff --git a/packages/js/components/changelog/add-36355_new_product_editor_package b/packages/js/components/changelog/add-36355_new_product_editor_package new file mode 100644 index 000000000000..19797698691d --- /dev/null +++ b/packages/js/components/changelog/add-36355_new_product_editor_package @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Move experimental product section components to @woocommerce/product-editor package. diff --git a/packages/js/components/changelog/canceled b/packages/js/components/changelog/canceled deleted file mode 100644 index 2f8b5c512ca8..000000000000 --- a/packages/js/components/changelog/canceled +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: tweak - -Update spelling of Cancelled to Canceled for US English. diff --git a/packages/js/components/changelog/dev-consolidate-eslint-versions b/packages/js/components/changelog/dev-consolidate-eslint-versions new file mode 100644 index 000000000000..d3d95c39119b --- /dev/null +++ b/packages/js/components/changelog/dev-consolidate-eslint-versions @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update eslint to 8.32.0 across the monorepo. diff --git a/plugins/woocommerce/changelog/fix-36530_settings_tables_styles b/packages/js/components/changelog/fix-36614_refactor_create_ordered_children similarity index 50% rename from plugins/woocommerce/changelog/fix-36530_settings_tables_styles rename to packages/js/components/changelog/fix-36614_refactor_create_ordered_children index dfe35659e33d..a347e3adc16d 100644 --- a/plugins/woocommerce/changelog/fix-36530_settings_tables_styles +++ b/packages/js/components/changelog/fix-36614_refactor_create_ordered_children @@ -1,4 +1,4 @@ Significance: minor Type: fix -Fix settings tables styles +Refactor createOrderedChildren diff --git a/packages/js/components/changelog/fix-create_ordered_children b/packages/js/components/changelog/fix-create_ordered_children new file mode 100644 index 000000000000..18b502f176d9 --- /dev/null +++ b/packages/js/components/changelog/fix-create_ordered_children @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix issue were Options tab was not showing up anymore in new product management screen. diff --git a/packages/js/components/changelog/fix-experimental-tree-control-styles b/packages/js/components/changelog/fix-experimental-tree-control-styles new file mode 100644 index 000000000000..b1795694d0cb --- /dev/null +++ b/packages/js/components/changelog/fix-experimental-tree-control-styles @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Include CSS for experimental tree control so it renders properly in Storybook. diff --git a/packages/js/components/changelog/fix-select-control-style b/packages/js/components/changelog/fix-select-control-style new file mode 100644 index 000000000000..256abe194ed4 --- /dev/null +++ b/packages/js/components/changelog/fix-select-control-style @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix SelectControl and TreeControl styles. diff --git a/packages/js/components/changelog/fix-setstate-diff-component b/packages/js/components/changelog/fix-setstate-diff-component new file mode 100644 index 000000000000..85d405d9d992 --- /dev/null +++ b/packages/js/components/changelog/fix-setstate-diff-component @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Move registerFill call to inside an useEffect since it was updating a component while rendering another component + + diff --git a/packages/js/components/changelog/try-support-multiple-fills b/packages/js/components/changelog/try-support-multiple-fills new file mode 100644 index 000000000000..901059348bcc --- /dev/null +++ b/packages/js/components/changelog/try-support-multiple-fills @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Updating the product editor fill components to support multiple targets. diff --git a/packages/js/components/changelog/update-36610-support-variations-alternate b/packages/js/components/changelog/update-36610-support-variations-alternate new file mode 100644 index 000000000000..124a49aaa9f7 --- /dev/null +++ b/packages/js/components/changelog/update-36610-support-variations-alternate @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Updating WooProductFieldItem to uniquely generate IDs with different sections. diff --git a/packages/js/components/changelog/update-product_editor_packages b/packages/js/components/changelog/update-product_editor_packages new file mode 100644 index 000000000000..5e7d389add79 --- /dev/null +++ b/packages/js/components/changelog/update-product_editor_packages @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Add deprecated message to packages moved to product-editor package. diff --git a/packages/js/components/package.json b/packages/js/components/package.json index 93dd1d7cb21f..1f810a551cdc 100644 --- a/packages/js/components/package.json +++ b/packages/js/components/package.json @@ -44,6 +44,7 @@ "@woocommerce/navigation": "workspace:*", "@wordpress/a11y": "3.5.0", "@wordpress/api-fetch": "^6.0.1", + "@wordpress/base-styles": "^4.3.0", "@wordpress/block-editor": "^9.8.0", "@wordpress/block-library": "^7.16.0", "@wordpress/blocks": "^11.18.0", @@ -129,7 +130,7 @@ "@wordpress/scripts": "^12.6.1", "concurrently": "^7.0.0", "css-loader": "^3.6.0", - "eslint": "^8.12.0", + "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", "postcss-loader": "^3.0.0", @@ -164,5 +165,11 @@ "pnpm lint:fix", "pnpm test-staged" ] + }, + "pnpm": { + "overrides": { + "react": "^17.0.2", + "react-dom": "^17.0.2" + } } } diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-expander.ts b/packages/js/components/src/experimental-tree-control/hooks/use-expander.ts new file mode 100644 index 000000000000..49ac33379cfb --- /dev/null +++ b/packages/js/components/src/experimental-tree-control/hooks/use-expander.ts @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import { useEffect, useState } from 'react'; + +/** + * Internal dependencies + */ +import { TreeItemProps } from '../types'; + +export function useExpander( { + shouldItemBeExpanded, + item, +}: Pick< TreeItemProps, 'shouldItemBeExpanded' | 'item' > ) { + const [ isExpanded, setExpanded ] = useState( false ); + + useEffect( () => { + if ( + item.children?.length && + typeof shouldItemBeExpanded === 'function' + ) { + setExpanded( shouldItemBeExpanded( item ) ); + } + }, [ item, shouldItemBeExpanded ] ); + + function onToggleExpand() { + setExpanded( ( prev ) => ! prev ); + } + + return { isExpanded, onToggleExpand }; +} diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts b/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts index 9043fe00f691..23fe64d2c13c 100644 --- a/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts +++ b/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts @@ -1,30 +1,43 @@ /** * External dependencies */ +import React from 'react'; /** * Internal dependencies */ import { TreeItemProps } from '../types'; +import { useExpander } from './use-expander'; -export function useTreeItem( { item, level, ...props }: TreeItemProps ) { +export function useTreeItem( { + item, + level, + shouldItemBeExpanded, + ...props +}: TreeItemProps ) { const nextLevel = level + 1; - const nextHeadingPaddingLeft = ( level - 1 ) * 28 + 12; + + const expander = useExpander( { + item, + shouldItemBeExpanded, + } ); return { item, level: nextLevel, + expander, treeItemProps: { ...props, }, headingProps: { style: { - paddingLeft: nextHeadingPaddingLeft, - }, + '--level': level, + } as React.CSSProperties, }, treeProps: { items: item.children, level: nextLevel, + shouldItemBeExpanded, }, }; } diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts b/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts index 2ab6b889c589..028a338a3753 100644 --- a/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts +++ b/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts @@ -7,7 +7,13 @@ */ import { TreeProps } from '../types'; -export function useTree( { ref, items, level = 1, ...props }: TreeProps ) { +export function useTree( { + ref, + items, + level = 1, + shouldItemBeExpanded, + ...props +}: TreeProps ) { return { level, items, @@ -16,6 +22,7 @@ export function useTree( { ref, items, level = 1, ...props }: TreeProps ) { }, treeItemProps: { level, + shouldItemBeExpanded, }, }; } diff --git a/packages/js/components/src/experimental-tree-control/stories/index.tsx b/packages/js/components/src/experimental-tree-control/stories/index.tsx index d62a8ed1964a..fdfcf2aee367 100644 --- a/packages/js/components/src/experimental-tree-control/stories/index.tsx +++ b/packages/js/components/src/experimental-tree-control/stories/index.tsx @@ -1,14 +1,14 @@ /** * External dependencies */ -import { BaseControl } from '@wordpress/components'; -import React, { createElement } from 'react'; +import { BaseControl, TextControl } from '@wordpress/components'; +import React, { createElement, useCallback, useState } from 'react'; /** * Internal dependencies */ import { TreeControl } from '../tree-control'; -import { Item } from '../types'; +import { Item, LinkedTree } from '../types'; const listItems: Item[] = [ { value: '1', label: 'Technology' }, @@ -36,6 +36,35 @@ export const SimpleTree: React.FC = () => { ); }; +function shouldItemBeExpanded( item: LinkedTree, filter: string ) { + if ( ! filter || ! item.children?.length ) return false; + return item.children.some( ( child ) => { + if ( new RegExp( filter, 'ig' ).test( child.data.label ) ) { + return true; + } + return shouldItemBeExpanded( child, filter ); + } ); +} + +export const ExpandOnFilter: React.FC = () => { + const [ filter, setFilter ] = useState( '' ); + + return ( + <> + + + + shouldItemBeExpanded( item, filter ) + } + /> + + + ); +}; + export default { title: 'WooCommerce Admin/experimental/TreeControl', component: TreeControl, diff --git a/packages/js/components/src/experimental-tree-control/tree-item.scss b/packages/js/components/src/experimental-tree-control/tree-item.scss index a62dbde0122b..4c36ce5ab6f9 100644 --- a/packages/js/components/src/experimental-tree-control/tree-item.scss +++ b/packages/js/components/src/experimental-tree-control/tree-item.scss @@ -6,7 +6,7 @@ flex-grow: 1; gap: $gap-smaller; min-height: $gap-largest; - padding: 0 $gap-small; + padding: 0 $gap-small 0 calc( ( var( --level ) - 1 ) * ( $gap + $gap-small ) + $gap-small ); border-radius: 2px; &:hover, @@ -17,7 +17,7 @@ &:hover, &:focus-within { - background-color: $gray-0; + background-color: $gray-100; } } &__label { @@ -31,4 +31,12 @@ display: block; } } + &__expander { + display: flex; + align-items: center; + + .components-button { + padding: 0; + } + } } diff --git a/packages/js/components/src/experimental-tree-control/tree-item.tsx b/packages/js/components/src/experimental-tree-control/tree-item.tsx index e8e7c407932f..17c4d670c63d 100644 --- a/packages/js/components/src/experimental-tree-control/tree-item.tsx +++ b/packages/js/components/src/experimental-tree-control/tree-item.tsx @@ -1,7 +1,9 @@ /** * External dependencies */ +import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { chevronDown, chevronUp } from '@wordpress/icons'; import classNames from 'classnames'; import { createElement, forwardRef } from 'react'; @@ -16,7 +18,13 @@ export const TreeItem = forwardRef( function ForwardedTreeItem( props: TreeItemProps, ref: React.ForwardedRef< HTMLLIElement > ) { - const { item, treeItemProps, headingProps, treeProps } = useTreeItem( { + const { + item, + treeItemProps, + headingProps, + treeProps, + expander: { isExpanded, onToggleExpand }, + } = useTreeItem( { ...props, ref, } ); @@ -36,9 +44,26 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
{ item.data.label }
+ + { Boolean( item.children?.length ) && ( +
+
+ ) } - { Boolean( item.children.length ) && } + { Boolean( item.children.length ) && isExpanded && ( + + ) } ); } ); diff --git a/packages/js/components/src/experimental-tree-control/types.ts b/packages/js/components/src/experimental-tree-control/types.ts index c925bbb85746..e7487fcfe4dd 100644 --- a/packages/js/components/src/experimental-tree-control/types.ts +++ b/packages/js/components/src/experimental-tree-control/types.ts @@ -16,6 +16,21 @@ export type TreeProps = React.DetailedHTMLProps< > & { level?: number; items: LinkedTree[]; + /** + * Return if the tree item passed in should be expanded. + * + * @example + * checkExpanded( item, filter ) + * } + * /> + * + * @param item The tree item to determine if should be expanded. + * + * @see {@link LinkedTree} + */ + shouldItemBeExpanded?( item: LinkedTree ): boolean; }; export type TreeItemProps = React.DetailedHTMLProps< @@ -24,6 +39,7 @@ export type TreeItemProps = React.DetailedHTMLProps< > & { level: number; item: LinkedTree; + shouldItemBeExpanded?( item: LinkedTree ): boolean; }; export type TreeControlProps = Omit< TreeProps, 'items' | 'level' > & { diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts index f038899e38c2..08683120c625 100644 --- a/packages/js/components/src/index.ts +++ b/packages/js/components/src/index.ts @@ -88,10 +88,6 @@ export { createOrderedChildren, sortFillsByOrder } from './utils'; export { WooProductFieldItem as __experimentalWooProductFieldItem } from './woo-product-field-item'; export { WooProductSectionItem as __experimentalWooProductSectionItem } from './woo-product-section-item'; export { WooProductTabItem as __experimentalWooProductTabItem } from './woo-product-tab-item'; -export { - ProductSectionLayout as __experimentalProductSectionLayout, - ProductFieldSection as __experimentalProductFieldSection, -} from './product-section-layout'; export * from './product-fields'; export { SlotContextProvider, @@ -99,3 +95,9 @@ export { SlotContextType, SlotContextHelpersType, } from './slot-context'; + +// Exports below can be removed once the @woocommerce/product-editor package is released. +export { + ProductSectionLayout as __experimentalProductSectionLayout, + ProductFieldSection as __experimentalProductFieldSection, +} from './product-section-layout'; diff --git a/packages/js/components/src/plugins/index.tsx b/packages/js/components/src/plugins/index.tsx index f1d84f16f6cc..972874c664a3 100644 --- a/packages/js/components/src/plugins/index.tsx +++ b/packages/js/components/src/plugins/index.tsx @@ -9,7 +9,7 @@ import { useState, useEffect, } from '@wordpress/element'; -import { SyntheticEvent } from 'react'; +import { SyntheticEvent, useCallback } from 'react'; import { useDispatch, useSelect } from '@wordpress/data'; import { PLUGINS_STORE_NAME, InstallPluginsResponse } from '@woocommerce/data'; @@ -25,6 +25,11 @@ type PluginsProps = { pluginSlugs?: string[]; onAbort?: () => void; abortText?: string; + installText?: string; + installButtonVariant?: Button.BaseProps[ 'variant' ]; + learnMoreLink?: string; + learnMoreText?: string; + onLearnMore?: () => void; }; export const Plugins = ( { @@ -34,10 +39,17 @@ export const Plugins = ( { onError = () => null, pluginSlugs = [ 'jetpack', 'woocommerce-services' ], onSkip, + installText = __( 'Install & enable', 'woocommerce' ), skipText = __( 'No thanks', 'woocommerce' ), abortText = __( 'Abort', 'woocommerce' ), + installButtonVariant = 'primary', + learnMoreLink, + learnMoreText = __( 'Learn more', 'woocommerce' ), + onLearnMore, }: PluginsProps ) => { const [ hasErrors, setHasErrors ] = useState( false ); + // Tracks action so that multiple instances of this button don't all light up when one is clicked + const [ hasBeenClicked, setHasBeenClicked ] = useState( false ); const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME ); const { isRequesting } = useSelect( ( select ) => { const { getActivePlugins, getInstalledPlugins, isPluginsRequesting } = @@ -52,48 +64,56 @@ export const Plugins = ( { }; } ); - const handleErrors = ( - errors: unknown, - response: InstallPluginsResponse - ) => { - setHasErrors( true ); + const handleErrors = useCallback( + ( errors: unknown, response: InstallPluginsResponse ) => { + setHasErrors( true ); - onError( errors, response ); - }; + onError( errors, response ); + }, + [ onError ] + ); - const handleSuccess = ( - plugins: string[], - response: InstallPluginsResponse - ) => { - onComplete( plugins, response ); - }; + const handleSuccess = useCallback( + ( plugins: string[], response: InstallPluginsResponse ) => { + onComplete( plugins, response ); + }, + [ onComplete ] + ); - const installAndActivate = async ( - event?: SyntheticEvent< HTMLAnchorElement > - ) => { - if ( event ) { - event.preventDefault(); - } + const installAndActivate = useCallback( + async ( event?: SyntheticEvent< HTMLAnchorElement > ) => { + if ( event ) { + event.preventDefault(); + } - // Avoid double activating. - if ( isRequesting ) { - return false; - } + // Avoid double activating. + if ( isRequesting ) { + return false; + } - installAndActivatePlugins( pluginSlugs ) - .then( ( response ) => { - handleSuccess( response.data.activated, response ); - } ) - .catch( ( response ) => { - handleErrors( response.errors, response ); - } ); - }; + installAndActivatePlugins( pluginSlugs ) + .then( ( response ) => { + handleSuccess( response.data.activated, response ); + } ) + .catch( ( response ) => { + setHasBeenClicked( false ); + handleErrors( response.errors, response ); + } ); + }, + [ + handleErrors, + handleSuccess, + installAndActivatePlugins, + isRequesting, + pluginSlugs, + ] + ); useEffect( () => { if ( autoInstall ) { installAndActivate(); } - }, [] ); + }, [ autoInstall, installAndActivate ] ); if ( hasErrors ) { return ( @@ -131,17 +151,32 @@ export const Plugins = ( { return ( <> { onSkip && ( ) } + { learnMoreLink && ( + + + + ) } { onAbort && ( + ); + } + + if ( plugin.direct_install ) { + return ( + + ); + } + + return ( + + ); + }; + + return ( + } + name={ plugin.title } + pills={ plugin.tags.map( ( tag ) => ( + { tag.name } + ) ) } + description={ plugin.description } + button={ renderButton() } + /> + ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/index.ts b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/index.ts index e3497c710ba5..5e2c782db33c 100644 --- a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/index.ts +++ b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/index.ts @@ -1 +1,2 @@ export { PluginCardBody } from './PluginCardBody'; +export { SmartPluginCardBody } from './SmartPluginCardBody'; diff --git a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/useIsPluginInstalledNotActivated.ts b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/useIsPluginInstalledNotActivated.ts new file mode 100644 index 000000000000..1b5d79240d2b --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/useIsPluginInstalledNotActivated.ts @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { useCallback } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { PLUGINS_STORE_NAME } from '@woocommerce/data'; + +export const useIsPluginInstalledNotActivated = () => { + const { getPluginInstallState } = useSelect( ( select ) => { + return { + getPluginInstallState: + select( PLUGINS_STORE_NAME ).getPluginInstallState, + }; + } ); + + const isPluginInstalledNotActivated = useCallback( + ( slug: string ) => { + return getPluginInstallState( slug ) === 'installed'; + }, + [ getPluginInstallState ] + ); + + return { isPluginInstalledNotActivated }; +}; diff --git a/plugins/woocommerce-admin/client/marketing/components/index.js b/plugins/woocommerce-admin/client/marketing/components/index.js index c514c3ca633a..dc971c751603 100644 --- a/plugins/woocommerce-admin/client/marketing/components/index.js +++ b/plugins/woocommerce-admin/client/marketing/components/index.js @@ -4,4 +4,7 @@ export { default as ProductIcon } from './product-icon'; export { default as Slider } from './slider'; export { default as ReadBlogMessage } from './ReadBlogMessage'; export { CollapsibleCard, CardBody, CardDivider } from './CollapsibleCard'; -export { PluginCardBody } from './PluginCardBody'; +export { PluginCardBody, SmartPluginCardBody } from './PluginCardBody'; +export { CardHeaderTitle } from './CardHeaderTitle'; +export { CardHeaderDescription } from './CardHeaderDescription'; +export { CenteredSpinner } from './CenteredSpinner'; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts new file mode 100644 index 000000000000..ab0e40f15ae8 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/action-types.ts @@ -0,0 +1,10 @@ +export const TYPES = { + RECEIVE_REGISTERED_CHANNELS_SUCCESS: + 'RECEIVE_REGISTERED_CHANNELS_SUCCESS' as const, + RECEIVE_REGISTERED_CHANNELS_ERROR: + 'RECEIVE_REGISTERED_CHANNELS_ERROR' as const, + RECEIVE_RECOMMENDED_CHANNELS_SUCCESS: + 'RECEIVE_RECOMMENDED_CHANNELS_SUCCESS' as const, + RECEIVE_RECOMMENDED_CHANNELS_ERROR: + 'RECEIVE_RECOMMENDED_CHANNELS_ERROR' as const, +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts new file mode 100644 index 000000000000..685aa867c025 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/actions.ts @@ -0,0 +1,46 @@ +/** + * Internal dependencies + */ +import { TYPES } from './action-types'; +import { ApiFetchError, RegisteredChannel, RecommendedChannel } from './types'; + +export const receiveRegisteredChannelsSuccess = ( + channels: Array< RegisteredChannel > +) => { + return { + type: TYPES.RECEIVE_REGISTERED_CHANNELS_SUCCESS, + payload: channels, + }; +}; + +export const receiveRegisteredChannelsError = ( error: ApiFetchError ) => { + return { + type: TYPES.RECEIVE_REGISTERED_CHANNELS_ERROR, + payload: error, + error: true, + }; +}; + +export const receiveRecommendedChannelsSuccess = ( + channels: Array< RecommendedChannel > +) => { + return { + type: TYPES.RECEIVE_RECOMMENDED_CHANNELS_SUCCESS, + payload: channels, + }; +}; + +export const receiveRecommendedChannelsError = ( error: ApiFetchError ) => { + return { + type: TYPES.RECEIVE_RECOMMENDED_CHANNELS_ERROR, + payload: error, + error: true, + }; +}; + +export type Action = ReturnType< + | typeof receiveRegisteredChannelsSuccess + | typeof receiveRegisteredChannelsError + | typeof receiveRecommendedChannelsSuccess + | typeof receiveRecommendedChannelsError +>; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/constants.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/constants.ts new file mode 100644 index 000000000000..e57d9c8406ea --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/constants.ts @@ -0,0 +1,2 @@ +export const STORE_KEY = 'wc/marketing-multichannel'; +export const API_NAMESPACE = '/wc-admin/marketing'; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/guards.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/guards.ts new file mode 100644 index 000000000000..b8906cf2ed9b --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/guards.ts @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import { ApiFetchError } from './types'; + +export const isObject = ( obj: unknown ): obj is Record< string, unknown > => { + return !! obj && typeof obj === 'object'; +}; + +export const isApiFetchError = ( obj: unknown ): obj is ApiFetchError => { + return ( + isObject( obj ) && 'code' in obj && 'data' in obj && 'message' in obj + ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/index.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/index.ts new file mode 100644 index 000000000000..1c213637e2fe --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/index.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { createReduxStore, register } from '@wordpress/data'; +import { Reducer, AnyAction } from 'redux'; +import { controls } from '@wordpress/data-controls'; + +/** + * Internal dependencies + */ +import { State } from './types'; +import { STORE_KEY } from './constants'; +import { reducer } from './reducer'; +import * as actions from './actions'; +import * as selectors from './selectors'; +import * as resolvers from './resolvers'; + +const store = createReduxStore( STORE_KEY, { + reducer: reducer as Reducer< State, AnyAction >, + actions, + selectors, + resolvers, + controls, +} ); + +register( store ); diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts new file mode 100644 index 000000000000..82749ee49f6a --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/reducer.ts @@ -0,0 +1,61 @@ +/** + * External dependencies + */ + +import type { Reducer } from 'redux'; + +/** + * Internal dependencies + */ +import { State } from './types'; +import { Action } from './actions'; +import { TYPES } from './action-types'; + +const initialState = { + registeredChannels: { + data: undefined, + error: undefined, + }, + recommendedChannels: { + data: undefined, + error: undefined, + }, +}; + +export const reducer: Reducer< State, Action > = ( + state = initialState, + action +) => { + switch ( action.type ) { + case TYPES.RECEIVE_REGISTERED_CHANNELS_SUCCESS: + return { + ...state, + registeredChannels: { + data: action.payload, + }, + }; + case TYPES.RECEIVE_REGISTERED_CHANNELS_ERROR: + return { + ...state, + registeredChannels: { + error: action.payload, + }, + }; + case TYPES.RECEIVE_RECOMMENDED_CHANNELS_SUCCESS: + return { + ...state, + recommendedChannels: { + data: action.payload, + }, + }; + case TYPES.RECEIVE_RECOMMENDED_CHANNELS_ERROR: + return { + ...state, + recommendedChannels: { + error: action.payload, + }, + }; + default: + return state; + } +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts new file mode 100644 index 000000000000..771a3b084c30 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/resolvers.ts @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import { apiFetch } from '@wordpress/data-controls'; + +/** + * Internal dependencies + */ +import { + receiveRegisteredChannelsSuccess, + receiveRegisteredChannelsError, + receiveRecommendedChannelsSuccess, + receiveRecommendedChannelsError, +} from './actions'; +import { RegisteredChannel, RecommendedChannel } from './types'; +import { API_NAMESPACE } from './constants'; +import { isApiFetchError } from './guards'; + +export function* getRegisteredChannels() { + try { + const data: RegisteredChannel[] = yield apiFetch( { + path: `${ API_NAMESPACE }/channels`, + } ); + + yield receiveRegisteredChannelsSuccess( data ); + } catch ( error ) { + if ( isApiFetchError( error ) ) { + yield receiveRegisteredChannelsError( error ); + } + + throw error; + } +} + +export function* getRecommendedChannels() { + try { + const data: RecommendedChannel[] = yield apiFetch( { + path: `${ API_NAMESPACE }/recommendations?category=channels`, + } ); + + yield receiveRecommendedChannelsSuccess( data ); + } catch ( error ) { + if ( isApiFetchError( error ) ) { + yield receiveRecommendedChannelsError( error ); + } + + throw error; + } +} diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts new file mode 100644 index 000000000000..48df4625c015 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/selectors.ts @@ -0,0 +1,12 @@ +/** + * Internal dependencies + */ +import { State } from './types'; + +export const getRegisteredChannels = ( state: State ) => { + return state.registeredChannels; +}; + +export const getRecommendedChannels = ( state: State ) => { + return state.recommendedChannels; +}; diff --git a/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts new file mode 100644 index 000000000000..31cee8017c07 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/data-multichannel/types.ts @@ -0,0 +1,56 @@ +export type ApiFetchError = { + code: string; + data: { + status: number; + }; + message: string; +}; + +export type RegisteredChannel = { + slug: string; + is_setup_completed: boolean; + settings_url: string; + name: string; + description: string; + product_listings_status: string; + errors_count: number; + icon: string; +}; + +export type RegisteredChannelsState = { + data?: Array< RegisteredChannel >; + error?: ApiFetchError; +}; + +type Subcategory = { + slug: string; + name: string; +}; + +type Tag = { + slug: string; + name: string; +}; + +export type RecommendedChannel = { + title: string; + description: string; + url: string; + direct_install: boolean; + icon: string; + product: string; + plugin: string; + categories: Array< string >; + subcategories: Array< Subcategory >; + tags: Array< Tag >; +}; + +export type RecommendedChannelsState = { + data?: Array< RecommendedChannel >; + error?: ApiFetchError; +}; + +export type State = { + registeredChannels: RegisteredChannelsState; + recommendedChannels: RecommendedChannelsState; +}; diff --git a/plugins/woocommerce-admin/client/marketing/hooks/index.ts b/plugins/woocommerce-admin/client/marketing/hooks/index.ts index a504ae73f178..0a80d7f7f223 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/index.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/index.ts @@ -1 +1,3 @@ export { useInstalledPlugins } from './useInstalledPlugins'; +export { useRegisteredChannels } from './useRegisteredChannels'; +export { useRecommendedChannels } from './useRecommendedChannels'; diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useInstalledPlugins.ts b/plugins/woocommerce-admin/client/marketing/hooks/useInstalledPlugins.ts index 94dd9905ddab..88c39ac3e687 100644 --- a/plugins/woocommerce-admin/client/marketing/hooks/useInstalledPlugins.ts +++ b/plugins/woocommerce-admin/client/marketing/hooks/useInstalledPlugins.ts @@ -7,10 +7,10 @@ import { useSelect, useDispatch } from '@wordpress/data'; * Internal dependencies */ import { STORE_KEY } from '~/marketing/data/constants'; -import { Plugin } from '~/marketing/types'; +import { InstalledPlugin } from '~/marketing/types'; export type UseInstalledPlugins = { - installedPlugins: Plugin[]; + installedPlugins: InstalledPlugin[]; activatingPlugins: string[]; activateInstalledPlugin: ( slug: string ) => void; loadInstalledPluginsAfterActivation: ( slug: string ) => void; diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts new file mode 100644 index 000000000000..0e69e63e2eb6 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRecommendedChannels.ts @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; +import { PLUGINS_STORE_NAME } from '@woocommerce/data'; +import { differenceWith } from 'lodash'; + +/** + * Internal dependencies + */ +import { STORE_KEY } from '~/marketing/data-multichannel/constants'; +import { + RecommendedChannelsState, + RecommendedChannel, +} from '~/marketing/data-multichannel/types'; + +type UseRecommendedChannels = { + loading: boolean; + data?: Array< RecommendedChannel >; +}; + +export const useRecommendedChannels = (): UseRecommendedChannels => { + return useSelect( ( select ) => { + const { hasFinishedResolution, getRecommendedChannels } = + select( STORE_KEY ); + const { data, error } = + getRecommendedChannels< RecommendedChannelsState >(); + + const { getActivePlugins } = select( PLUGINS_STORE_NAME ); + const activePlugins = getActivePlugins(); + + /** + * Recommended channels that are not in "active" state, + * i.e. channels that are not installed or not activated yet. + */ + const nonActiveRecommendedChannels = + data && + differenceWith( data, activePlugins, ( a, b ) => { + return a.product === b; + } ); + + return { + loading: ! hasFinishedResolution( 'getRecommendedChannels' ), + data: nonActiveRecommendedChannels, + error, + }; + } ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts new file mode 100644 index 000000000000..bafbd5b6f2e4 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/hooks/useRegisteredChannels.ts @@ -0,0 +1,81 @@ +/** + * External dependencies + */ +import { useDispatch, useSelect } from '@wordpress/data'; +import { __, sprintf } from '@wordpress/i18n'; +import { useCallback } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { RegisteredChannel, SyncStatusType } from '~/marketing/types'; +import { STORE_KEY } from '~/marketing/data-multichannel/constants'; +import { + ApiFetchError, + RegisteredChannel as APIRegisteredChannel, + RegisteredChannelsState, +} from '~/marketing/data-multichannel/types'; + +type UseRegisteredChannels = { + loading: boolean; + data?: Array< RegisteredChannel >; + error?: ApiFetchError; + refetch: () => void; +}; + +/** + * A object that maps the product listings status in + * plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php backend + * to SyncStatusType frontend. + */ +const statusMap: Record< string, SyncStatusType > = { + 'sync-in-progress': 'syncing', + 'sync-failed': 'failed', + synced: 'synced', +}; + +const convert = ( data: APIRegisteredChannel ): RegisteredChannel => { + const issueType = data.errors_count >= 1 ? 'error' : 'none'; + const issueText = + data.errors_count >= 1 + ? sprintf( + // translators: %d: The number of issues to resolve. + __( '%d issues to resolve', 'woocommerce' ), + data.errors_count + ) + : __( 'No issues to resolve', 'woocommerce' ); + + return { + slug: data.slug, + title: data.name, + description: data.description, + icon: data.icon, + isSetupCompleted: data.is_setup_completed, + setupUrl: data.settings_url, + manageUrl: data.settings_url, + syncStatus: statusMap[ data.product_listings_status ], + issueType, + issueText, + }; +}; + +export const useRegisteredChannels = (): UseRegisteredChannels => { + const { invalidateResolution } = useDispatch( STORE_KEY ); + + const refetch = useCallback( () => { + invalidateResolution( 'getRegisteredChannels' ); + }, [ invalidateResolution ] ); + + return useSelect( ( select ) => { + const { hasFinishedResolution, getRegisteredChannels } = + select( STORE_KEY ); + const state = getRegisteredChannels< RegisteredChannelsState >(); + + return { + loading: ! hasFinishedResolution( 'getRegisteredChannels' ), + data: state.data?.map( convert ), + error: state.error, + refetch, + }; + } ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss new file mode 100644 index 000000000000..276caba39698 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.scss @@ -0,0 +1,13 @@ +.woocommerce-marketing-channels-card { + .components-card-header { + flex-direction: column; + align-items: flex-start; + gap: $gap-smallest; + } + + .components-button.is-link { + @include font-size( 14 ); + font-weight: 600; + text-decoration: none; + } +} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx new file mode 100644 index 000000000000..8e3b469634b0 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/Channels.tsx @@ -0,0 +1,119 @@ +/** + * External dependencies + */ +import { Fragment, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { + Card, + CardHeader, + CardBody, + CardDivider, + Button, + Icon, +} from '@wordpress/components'; +import { chevronUp, chevronDown } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { RecommendedChannel } from '~/marketing/data-multichannel/types'; +import { + CardHeaderTitle, + CardHeaderDescription, + SmartPluginCardBody, +} from '~/marketing/components'; +import { RegisteredChannel } from '~/marketing/types'; +import { RegisteredChannelCardBody } from './RegisteredChannelCardBody'; +import './Channels.scss'; + +type ChannelsProps = { + registeredChannels: Array< RegisteredChannel >; + recommendedChannels: Array< RecommendedChannel >; + onInstalledAndActivated?: () => void; +}; + +export const Channels: React.FC< ChannelsProps > = ( { + registeredChannels, + recommendedChannels, + onInstalledAndActivated, +} ) => { + const hasRegisteredChannels = registeredChannels.length >= 1; + + /** + * State to collapse / expand the recommended channels. + * Initial state is expanded if there are no registered channels in first page load. + */ + const [ expanded, setExpanded ] = useState( ! hasRegisteredChannels ); + + return ( + + + + { __( 'Channels', 'woocommerce' ) } + + { ! hasRegisteredChannels && ( + + { __( + 'Start by adding a channel to your store', + 'woocommerce' + ) } + + ) } + + + { /* Registered channels section. */ } + { registeredChannels.map( ( el, idx ) => { + return ( + + + { idx !== registeredChannels.length - 1 && ( + + ) } + + ); + } ) } + + { /* Recommended channels section. */ } + { recommendedChannels.length >= 1 && ( +
+ { hasRegisteredChannels && ( + <> + + + + + + ) } + { expanded && + recommendedChannels.map( ( el, idx ) => { + return ( + + + { idx !== + recommendedChannels.length - 1 && ( + + ) } + + ); + } ) } +
+ ) } +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.scss new file mode 100644 index 000000000000..df5c9b02811c --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.scss @@ -0,0 +1,15 @@ +.woocommerce-marketing-issue-status { + display: flex; + align-items: center; + gap: $gap-smallest; + + &__error { + color: $alert-red; + fill: $alert-red; + } + + &__warning { + color: $alert-yellow; + fill: $alert-yellow; + } +} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.tsx new file mode 100644 index 000000000000..d0aee1202e3c --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/IssueStatus.tsx @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import GridiconNotice from 'gridicons/dist/notice'; +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import { RegisteredChannel } from '~/marketing/types'; +import { iconSize } from './iconSize'; +import './IssueStatus.scss'; + +type IssueStatusPropsType = { + registeredChannel: RegisteredChannel; +}; + +const issueStatusClassName = 'woocommerce-marketing-issue-status'; + +export const IssueStatus: React.FC< IssueStatusPropsType > = ( { + registeredChannel, +} ) => { + if ( registeredChannel.issueType === 'error' ) { + return ( +
+ + { registeredChannel.issueText } +
+ ); + } + + if ( registeredChannel.issueType === 'warning' ) { + return ( +
+ + { registeredChannel.issueText } +
+ ); + } + + return ( +
+ { registeredChannel.issueText } +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss new file mode 100644 index 000000000000..50c4615fb250 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.scss @@ -0,0 +1,10 @@ +.woocommerce-marketing-registered-channel-card-body { + .woocommerce-marketing-registered-channel-description { + display: flex; + gap: $gap-smaller; + + &__separator::before { + content: '•'; + } + } +} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx new file mode 100644 index 000000000000..2d4d9c007fae --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx @@ -0,0 +1,75 @@ +/** + * External dependencies + */ +import { Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { PluginCardBody } from '~/marketing/components'; +import { RegisteredChannel } from '~/marketing/types'; +import { SyncStatus } from './SyncStatus'; +import { IssueStatus } from './IssueStatus'; +import './RegisteredChannelCardBody.scss'; + +type RegisteredChannelCardBodyProps = { + registeredChannel: RegisteredChannel; +}; + +export const RegisteredChannelCardBody: React.FC< + RegisteredChannelCardBodyProps +> = ( { registeredChannel } ) => { + /** + * The description section in the channel card. + * + * If setup is not completed, this would be the channel description. + * + * If setup is completed, this would be an element with sync status and issue status. + */ + const description = ! registeredChannel.isSetupCompleted ? ( + registeredChannel.description + ) : ( +
+ { registeredChannel.syncStatus && ( + <> + +
+ + ) } + +
+ ); + + /** + * The action button in the channel card. + * + * If setup is not completed, this would be a "Finish setup" primary button. + * + * If setup is completed, this would be a "Manage" secondary button. + */ + const button = ! registeredChannel.isSetupCompleted ? ( + + ) : ( + + ); + + return ( + + } + name={ registeredChannel.title } + description={ description } + button={ button } + /> + ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.scss new file mode 100644 index 000000000000..402b0163f80c --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.scss @@ -0,0 +1,16 @@ +.woocommerce-marketing-sync-status { + display: flex; + align-items: center; + gap: $gap-smallest; + + &__failed { + color: $alert-red; + fill: $alert-red; + } + + &__syncing, + &__synced { + color: $studio-green-50; + fill: $studio-green-50; + } +} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.tsx new file mode 100644 index 000000000000..772dc8fcf759 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/SyncStatus.tsx @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import GridiconCheckmarkCircle from 'gridicons/dist/checkmark-circle'; +import GridiconSync from 'gridicons/dist/sync'; +import GridiconNotice from 'gridicons/dist/notice'; +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import { SyncStatusType } from '~/marketing/types'; +import { iconSize } from './iconSize'; +import './SyncStatus.scss'; + +type SyncStatusPropsType = { + status: SyncStatusType; +}; + +const className = 'woocommerce-marketing-sync-status'; + +export const SyncStatus: React.FC< SyncStatusPropsType > = ( { status } ) => { + if ( status === 'failed' ) { + return ( +
+ + { __( 'Sync failed', 'woocommerce' ) } +
+ ); + } + + if ( status === 'syncing' ) { + return ( +
+ + { __( 'Syncing', 'woocommerce' ) } +
+ ); + } + + return ( +
+ + { __( 'Synced', 'woocommerce' ) } +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/iconSize.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/iconSize.ts new file mode 100644 index 000000000000..ef79ab572acf --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/iconSize.ts @@ -0,0 +1 @@ +export const iconSize = 18; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts new file mode 100644 index 000000000000..da0d9c56072d --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts @@ -0,0 +1 @@ +export { Channels } from './Channels'; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.scss index c11516aec588..ab21090dc41d 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.scss +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.scss @@ -1,11 +1,3 @@ -.woocommerce-marketing-discover-tools-card { - // place the spinner in the center of the card. - .woocommerce-spinner { - display: block; - margin: auto; - } -} - .woocommerce-marketing-discover-tools-card-body-empty-content { width: 50%; margin: auto; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx index ca79f53b0852..a116640fbe09 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx @@ -5,12 +5,15 @@ import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; import { Icon, trendingUp } from '@wordpress/icons'; import { recordEvent } from '@woocommerce/tracks'; -import { Spinner } from '@woocommerce/components'; /** * Internal dependencies */ -import { CollapsibleCard, CardBody } from '~/marketing/components'; +import { + CollapsibleCard, + CardBody, + CenteredSpinner, +} from '~/marketing/components'; import { useRecommendedPlugins } from './useRecommendedPlugins'; import { PluginsTabPanel } from './PluginsTabPanel'; import './DiscoverTools.scss'; @@ -30,7 +33,7 @@ export const DiscoverTools = () => { if ( isInitializing ) { return ( - + ); } @@ -72,7 +75,6 @@ export const DiscoverTools = () => { return ( { renderCardContent() } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/PluginsTabPanel.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/PluginsTabPanel.tsx index 8528e1996726..8a689f4f2c74 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/PluginsTabPanel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/PluginsTabPanel.tsx @@ -15,9 +15,9 @@ import { flatMapDeep, uniqBy } from 'lodash'; */ import { CardDivider, PluginCardBody } from '~/marketing/components'; import { useInstalledPlugins } from '~/marketing/hooks'; +import { RecommendedPlugin } from '~/marketing/types'; import { getInAppPurchaseUrl } from '~/lib/in-app-purchase'; import { createNoticesFromResponse } from '~/lib/notices'; -import { Plugin } from './types'; import './DiscoverTools.scss'; /** @@ -30,7 +30,7 @@ import './DiscoverTools.scss'; * 1. Get an array of unique subcategories from the list of plugins. * 2. Map the subcategories schema into tabs schema. */ -const getTabs = ( plugins: Plugin[] ) => { +const getTabs = ( plugins: RecommendedPlugin[] ) => { const pluginSubcategories = uniqBy( flatMapDeep( plugins, ( p ) => p.subcategories ), ( subcategory ) => subcategory.slug @@ -44,7 +44,7 @@ const getTabs = ( plugins: Plugin[] ) => { type PluginsTabPanelType = { isLoading: boolean; - plugins: Plugin[]; + plugins: RecommendedPlugin[]; onInstallAndActivate: ( pluginSlug: string ) => void; }; @@ -72,7 +72,7 @@ export const PluginsTabPanel = ( { * * @param plugin Plugin to be installed and activated. */ - const installAndActivate = async ( plugin: Plugin ) => { + const installAndActivate = async ( plugin: RecommendedPlugin ) => { setCurrentPlugin( plugin.product ); try { @@ -103,7 +103,7 @@ export const PluginsTabPanel = ( { ) ); - const renderButton = ( plugin: Plugin ) => { + const renderButton = ( plugin: RecommendedPlugin ) => { const buttonDisabled = !! currentPlugin || isLoading; if ( plugin.direct_install ) { diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/useRecommendedPlugins.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/useRecommendedPlugins.ts index 6cb2556e9ebc..e8865fd48881 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/useRecommendedPlugins.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/useRecommendedPlugins.ts @@ -7,7 +7,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; * Internal dependencies */ import { STORE_KEY } from '~/marketing/data/constants'; -import { Plugin } from './types'; +import { RecommendedPlugin } from '~/marketing/types'; const selector = 'getRecommendedPlugins'; const category = 'marketing'; @@ -24,7 +24,8 @@ export const useRecommendedPlugins = () => { return useSelect( ( select ) => { const { getRecommendedPlugins, hasFinishedResolution } = select( STORE_KEY ); - const plugins = getRecommendedPlugins< Plugin[] >( category ); + const plugins = + getRecommendedPlugins< RecommendedPlugin[] >( category ); const isLoading = ! hasFinishedResolution( selector, [ category ] ); return { diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/InstalledExtensions/InstalledExtensions.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/InstalledExtensions/InstalledExtensions.tsx index 8fb7100b83da..f445fa7a69bc 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/InstalledExtensions/InstalledExtensions.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/InstalledExtensions/InstalledExtensions.tsx @@ -15,7 +15,7 @@ import { ProductIcon, PluginCardBody, } from '~/marketing/components'; -import { Plugin } from '~/marketing/types'; +import { InstalledPlugin } from '~/marketing/types'; import { useInstalledPlugins } from '~/marketing/hooks'; export const InstalledExtensions = () => { @@ -26,7 +26,7 @@ export const InstalledExtensions = () => { return null; } - const getButton = ( plugin: Plugin ) => { + const getButton = ( plugin: InstalledPlugin ) => { if ( plugin.status === 'installed' ) { return (