diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 3e3b424196e..03c09011544 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -7,11 +7,11 @@ There are many ways to contribute to the project! - [Translating strings into your language](https://github.com/woocommerce/woocommerce/wiki/Translating-WooCommerce). - Answering questions on the various WooCommerce communities like the [WP.org support forums](https://wordpress.org/support/plugin/woocommerce/). - Testing open [issues](https://github.com/woocommerce/woocommerce/issues) or [pull requests](https://github.com/woocommerce/woocommerce/pulls) and sharing your findings in a comment. -- Testing WooCommerce beta versions and release candidates. Those are announced in the [WooCommerce development blog](https://woocommerce.wordpress.com/). +- Testing WooCommerce beta versions and release candidates. Those are announced in the [WooCommerce development blog](https://developer.woocommerce.com/blog/). - Submitting fixes, improvements, and enhancements. - To disclose a security issue to our team, [please submit a report via HackerOne](https://hackerone.com/automattic/). -If you wish to contribute code, please read the information in the sections below. Then [fork](https://help.github.com/articles/fork-a-repo/) WooCommerce, commit your changes, and [submit a pull request](https://help.github.com/articles/using-pull-requests/) 🎉 +If you wish to contribute code, please read the information in the sections below. Then [fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) WooCommerce, commit your changes, and [submit a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) 🎉 We use the `good first issue` label to mark issues that are suitable for new contributors. You can find all the issues with this label [here](https://github.com/woocommerce/woocommerce/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+good+first+issue%22). @@ -31,7 +31,7 @@ If you have questions about the process to contribute code or want to discuss de ## Coding Guidelines and Development 🛠 -- Ensure you stick to the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/) +- Ensure you stick to the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/). - Run our build process described in the document on [how to set up WooCommerce development environment](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment), it will install our pre-commit hook, code sniffs, dependencies, and more. - Whenever possible please fix pre-existing code standards errors in the files that you change. It is ok to skip that for larger files or complex fixes. - Ensure you use LF line endings in your code editor. Use [EditorConfig](http://editorconfig.org/) if your editor supports it so that indentation, line endings and other settings are auto configured. @@ -39,16 +39,13 @@ If you have questions about the process to contribute code or want to discuss de - Ensure that your code supports the minimum supported versions of PHP and WordPress; this is shown at the top of the `readme.txt` file. - Push the changes to your fork and submit a pull request on the trunk branch of the WooCommerce repository. - Make sure to write good and detailed commit messages (see [this post](https://chris.beams.io/posts/git-commit/) for more on this) and follow all the applicable sections of the pull request template. -- Please avoid modifying the changelog directly or updating the .pot files. These will be updated by the WooCommerce team. +- Please create a change file for your changes by running `pnpm --filter= changelog add`. For example, a change file for the WooCommerce Core project would be added by running `pnpm --filter=woocommerce changelog add`. +- Please avoid modifying the changelog directly or updating the .pot files. These will be updated by the WooCommerce team. -If you are contributing code to the (Javascript-driven) Gutenberg blocks, note that it's developed in an external package. - -- [Blocks](https://github.com/woocommerce/woocommerce-gutenberg-products-block) +If you are contributing code to our (Javascript-driven) Gutenberg blocks, please note that they are developed in their [own repository](https://github.com/woocommerce/woocommerce-gutenberg-products-block) and have their [own issue tracker](https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues). ## Feature Requests 🚀 -Feature requests can be [submitted to our issue tracker](https://github.com/woocommerce/woocommerce/issues/new?assignees=&labels=type%3A+enhancement%2Cstatus%3A+awaiting+triage&template=2-enhancement.yml&title=%5BEnhancement%5D%3A+). Be sure to include a description of the expected behavior and use case, and before submitting a request, please search for similar ones in the closed issues. +The best place to submit feature requests is over on our [dedicated feature request page](https://woocommerce.com/feature-requests/woocommerce/). You can easily search and vote for existing requests, or create new requests if necessary. -Feature request issues will remain closed until we see sufficient interest via comments and [👍 reactions](https://help.github.com/articles/about-discussions-in-issues-and-pull-requests/) from the community. - -You can see a [list of current feature requests which require votes here](https://github.com/woocommerce/woocommerce/issues?q=is%3Aissue+sort%3Areactions-%2B1-desc+label%3A%22needs%3A+votes%22+). +Alternatively, if you wish to propose a straightforward technical enhancement that is unlikely to require much discussion, you can [open a new issue](https://github.com/woocommerce/woocommerce/issues/new?assignees=&labels=type%3A+enhancement%2Cstatus%3A+awaiting+triage&template=2-enhancement.yml&title=%5BEnhancement%5D%3A+) right here on GitHub and, for any that may require more discussion, consider syncing with us during office hours or publishing a thread on [GitHub Discussions](https://github.com/woocommerce/woocommerce/discussions). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3b1ff6841c6..0ff2173615b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,10 +1,9 @@ -### All Submissions: +### Submission Review Guidelines: -- [ ] Have you followed the [WooCommerce Contributing guideline](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md)? -- [ ] Does your code follow the [WordPress' coding standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/)? -- [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/woocommerce/woocommerce/pulls) for the same update/change? - - +- I have followed the [WooCommerce Contributing Guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md) and the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/). +- I have checked to ensure there aren't other open [Pull Requests](https://github.com/woocommerce/woocommerce/pulls) for the same update/change. +- I have reviewed my code for [security best practices](https://developer.wordpress.org/apis/security/). +- Following the above guidelines will result in quick merges and clear and detailed feedback when appropriate. @@ -18,25 +17,12 @@ 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)? +Using the [WooCommerce Testing Instructions Guide](https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions), include your detailed testing instructions: 1. 2. 3. - - -### Other information: - -- [ ] Have you added an explanation of what your changes do and why you'd like us to include them? -- [ ] Have you written new tests for your changes, as applicable? -- [ ] Have you created a changelog file for each project being changed, ie `pnpm --filter= changelog add`? -- [ ] Have you included testing instructions? - - - -### FOR PR REVIEWER ONLY: - -- [ ] I have reviewed that everything is sanitized/escaped appropriately for any SQL or XSS injection possibilities. I made sure Linting is not ignored or disabled. + \ No newline at end of file diff --git a/.github/actions/setup-woocommerce-monorepo/action.yml b/.github/actions/setup-woocommerce-monorepo/action.yml index ec3868f9db8..bd691199a13 100644 --- a/.github/actions/setup-woocommerce-monorepo/action.yml +++ b/.github/actions/setup-woocommerce-monorepo/action.yml @@ -29,7 +29,7 @@ runs: - name: Setup PNPM uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd with: - version: '^7.22.0' + version: '7.29.1' - name: Setup Node uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c diff --git a/.github/actions/tests/run-api-tests/action.yml b/.github/actions/tests/run-api-tests/action.yml new file mode 100644 index 00000000000..598a67a0d5f --- /dev/null +++ b/.github/actions/tests/run-api-tests/action.yml @@ -0,0 +1,38 @@ +name: Run API tests +description: Runs the WooCommerce Core API tests and generates Allure report. +permissions: {} + +inputs: + report-name: + description: Name of Allure report to be generated. + required: true + tests: + description: Specific tests to run, separated by single whitespace. See https://playwright.dev/docs/test-cli + +runs: + using: composite + steps: + - name: Run API tests. + id: run-api-tests + working-directory: plugins/woocommerce + shell: bash + run: | + pnpm exec playwright test \ + --config=tests/api-core-tests/playwright.config.js \ + ${{ inputs.tests }} + + - name: Generate Test report. + if: success() || ( failure() && steps.run-api-tests.conclusion == 'failure' ) + working-directory: plugins/woocommerce + shell: bash + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} + + - name: Archive test report + if: success() || ( failure() && steps.run-api-tests.conclusion == 'failure' ) + uses: actions/upload-artifact@v3 + with: + name: ${{ inputs.report-name }} + path: | + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} + retention-days: 20 diff --git a/.github/actions/tests/run-e2e-tests/action.yml b/.github/actions/tests/run-e2e-tests/action.yml new file mode 100644 index 00000000000..ab04b6764c4 --- /dev/null +++ b/.github/actions/tests/run-e2e-tests/action.yml @@ -0,0 +1,49 @@ +name: Run E2E tests +description: Runs the WooCommerce Core E2E tests and generates Allure report. +permissions: {} + +inputs: + report-name: + description: Name of Allure report to be generated. + required: true + tests: + description: Specific tests to run, separated by single whitespace. See https://playwright.dev/docs/test-cli + playwright-config: + description: The Playwright configuration file to use. + default: playwright.config.js + +runs: + using: composite + steps: + - name: Download and install Chromium browser. + working-directory: plugins/woocommerce + shell: bash + run: pnpm exec playwright install chromium + + - name: Run E2E tests. + id: run-e2e-tests + env: + FORCE_COLOR: 1 + USE_WP_ENV: 1 + working-directory: plugins/woocommerce + shell: bash + run: | + pnpm exec playwright test \ + --config=tests/e2e-pw/${{ inputs.playwright-config }} \ + ${{ inputs.tests }} + + - name: Generate Test report. + if: success() || ( failure() && steps.run-e2e-tests.conclusion == 'failure' ) + working-directory: plugins/woocommerce + shell: bash + run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} + + - name: Archive test report + if: success() || ( failure() && steps.run-e2e-tests.conclusion == 'failure' ) + uses: actions/upload-artifact@v3 + with: + name: ${{ inputs.report-name }} + path: | + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} + retention-days: 20 diff --git a/.github/actions/tests/run-k6-tests/action.yml b/.github/actions/tests/run-k6-tests/action.yml new file mode 100644 index 00000000000..fbf67c7f9f6 --- /dev/null +++ b/.github/actions/tests/run-k6-tests/action.yml @@ -0,0 +1,17 @@ +name: Run k6 performance tests +description: Runs the WooCommerce Core k6 performance tests. +permissions: {} + +runs: + using: composite + steps: + - name: Install k6 + shell: bash + run: | + curl https://github.com/grafana/k6/releases/download/v0.33.0/k6-v0.33.0-linux-amd64.tar.gz -L | tar xvz --strip-components 1 + + - name: Run k6 performance tests + id: run-k6-tests + shell: bash + run: | + ./k6 run plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js diff --git a/.github/actions/tests/setup-local-test-environment/action.yml b/.github/actions/tests/setup-local-test-environment/action.yml new file mode 100644 index 00000000000..c5386a32c77 --- /dev/null +++ b/.github/actions/tests/setup-local-test-environment/action.yml @@ -0,0 +1,29 @@ +name: Setup local test environment +description: Set up a wp-env testing environment +permissions: {} + +inputs: + test-type: + required: true + type: choice + options: + - e2e + - api + - k6 + +runs: + using: composite + steps: + - name: Load docker images and start containers for E2E or API tests + if: ( inputs.test-type == 'e2e' ) || ( inputs.test-type == 'api' ) + working-directory: plugins/woocommerce + shell: bash + run: pnpm run env:test + + - name: Load docker images and start containers for k6 performance tests + if: inputs.test-type == 'k6' + working-directory: plugins/woocommerce + shell: bash + run: | + pnpm env:dev --filter=woocommerce + pnpm env:performance-init --filter=woocommerce diff --git a/.github/actions/tests/slack-alert-on-pr-merge/action.yml b/.github/actions/tests/slack-alert-on-pr-merge/action.yml new file mode 100644 index 00000000000..6fbc11ab62a --- /dev/null +++ b/.github/actions/tests/slack-alert-on-pr-merge/action.yml @@ -0,0 +1,41 @@ +name: Send Slack alert on PR merge test failure +description: Send a Slack alert when automated tests failed on trunk after PR merge. +permissions: {} + +inputs: + slack-bot-token: + required: true + channel-id: + required: true + test-type: + required: true + type: choice + options: + - E2E + - API + - k6 + +runs: + using: composite + steps: + - name: Compose Slack message + id: compose-slack-message + uses: actions/github-script@v6 + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + SHA: ${{ github.event.pull_request.merge_commit_sha }} + TEST_TYPE: ${{ inputs.test-type }} + with: + script: | + const script = require('./.github/actions/tests/slack-alert-on-pr-merge/scripts/compose-slack-message.js') + const slackMessage = script() + core.setOutput('slack-message', slackMessage) + + - name: Send Slack alert + uses: slackapi/slack-github-action@v1.23.0 + env: + SLACK_BOT_TOKEN: ${{ inputs.slack-bot-token }} + with: + channel-id: ${{ inputs.channel-id }} + payload: ${{ steps.compose-slack-message.outputs.slack-message }} diff --git a/.github/actions/tests/slack-alert-on-pr-merge/scripts/compose-slack-message.js b/.github/actions/tests/slack-alert-on-pr-merge/scripts/compose-slack-message.js new file mode 100644 index 00000000000..ca194507547 --- /dev/null +++ b/.github/actions/tests/slack-alert-on-pr-merge/scripts/compose-slack-message.js @@ -0,0 +1,114 @@ +module.exports = () => { + const { + GITHUB_BASE_REF, + GITHUB_RUN_ID, + PR_NUMBER, + PR_TITLE, + SHA, + TEST_TYPE, + } = process.env; + + // Slack message blocks + const blocks = []; + const dividerBlock = { + type: 'divider', + }; + const introBlock = { + type: 'section', + text: { + type: 'mrkdwn', + text: `${ TEST_TYPE } tests failed on \`${ GITHUB_BASE_REF }\` after merging PR `, + }, + }; + const prTitleBlock = { + type: 'header', + text: { + type: 'plain_text', + text: PR_TITLE, + emoji: true, + }, + }; + const prButtonBlock = { + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: 'View pull request :pr-merged:', + emoji: true, + }, + value: 'view_pr', + url: `https://github.com/woocommerce/woocommerce/pull/${ PR_NUMBER }`, + action_id: 'view-pr', + }, + ], + }; + const mergeCommitBlock = { + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: `View merge commit ${ SHA.substring( + 0, + 7 + ) } :alphabet-yellow-hash:`, + emoji: true, + }, + value: 'view_commit', + url: `https://github.com/woocommerce/woocommerce/commit/${ SHA }`, + action_id: 'view-commit', + }, + ], + }; + const githubBlock = { + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: 'View GitHub run log :github:', + emoji: true, + }, + value: 'view_github', + url: `https://github.com/woocommerce/woocommerce/actions/runs/${ GITHUB_RUN_ID }`, + action_id: 'view-github', + }, + ], + }; + const reportBlock = { + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: 'View test report :colorful-bar-chart:', + emoji: true, + }, + value: 'view_report', + url: `https://woocommerce.github.io/woocommerce-test-reports/pr-merge/${ PR_NUMBER }/${ TEST_TYPE.toLowerCase() }`, + action_id: 'view-report', + }, + ], + }; + + // Assemble blocks + blocks.push( dividerBlock ); + blocks.push( introBlock ); + blocks.push( prTitleBlock ); + blocks.push( prButtonBlock ); + blocks.push( mergeCommitBlock ); + blocks.push( githubBlock ); + + if ( [ 'e2e', 'api' ].includes( TEST_TYPE.toLowerCase() ) ) { + blocks.push( reportBlock ); + } + + blocks.push( dividerBlock ); + + return { blocks }; +}; diff --git a/.github/actions/tests/upload-allure-files-to-bucket/action.yml b/.github/actions/tests/upload-allure-files-to-bucket/action.yml new file mode 100644 index 00000000000..c99f5fd5c42 --- /dev/null +++ b/.github/actions/tests/upload-allure-files-to-bucket/action.yml @@ -0,0 +1,37 @@ +name: Upload Allure files to bucket +description: Upload Allure files to bucket. +permissions: {} + +inputs: + artifact-name: + description: Name of the artifact that contains the allure-report and/or allure-results folders. + required: true + aws-region: + required: true + aws-access-key-id: + required: true + aws-secret-access-key: + required: true + s3-bucket: + required: true + include-allure-results: + dafault: false + +runs: + using: composite + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1-node16 + with: + aws-region: ${{ inputs.aws-region }} + aws-access-key-id: ${{ inputs.aws-access-key-id }} + aws-secret-access-key: ${{ inputs.aws-secret-access-key }} + + - name: Upload Allure artifact + env: + ARTIFACT_NAME: ${{ inputs.artifact-name }} + S3_BUCKET: ${{ inputs.s3-bucket }} + INCLUDE_ALLURE_RESULTS: ${{ inputs.include-allure-results }} + shell: bash + working-directory: .github/actions/tests/upload-allure-files-to-bucket/scripts + run: bash upload-allure-artifact.sh diff --git a/.github/actions/tests/upload-allure-files-to-bucket/scripts/upload-allure-artifact.sh b/.github/actions/tests/upload-allure-files-to-bucket/scripts/upload-allure-artifact.sh new file mode 100644 index 00000000000..bc3a2cd810b --- /dev/null +++ b/.github/actions/tests/upload-allure-files-to-bucket/scripts/upload-allure-artifact.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +s3_upload () { + aws s3 cp "$1" "$2" \ + --recursive +} + +upload_allure_results () { + if [[ $INCLUDE_ALLURE_RESULTS != "true" ]]; then + return + fi + + SOURCE="$ALLURE_RESULTS_DIR" + DESTINATION="$S3_BUCKET/artifacts/$GITHUB_RUN_ID/$ARTIFACT_NAME/allure-results" + + s3_upload "$SOURCE" "$DESTINATION" +} + +upload_allure_report () { + SOURCE="$ALLURE_REPORT_DIR" + DESTINATION="$S3_BUCKET/artifacts/$GITHUB_RUN_ID/$ARTIFACT_NAME/allure-report" + + s3_upload "$SOURCE" "$DESTINATION" +} + +upload_allure_results +upload_allure_report + +EXIT_CODE=$(echo $?) +exit $EXIT_CODE \ No newline at end of file diff --git a/.github/project-pr-labeler.yml b/.github/project-pr-labeler.yml index 3055e4667cc..c80da44a922 100644 --- a/.github/project-pr-labeler.yml +++ b/.github/project-pr-labeler.yml @@ -65,3 +65,12 @@ - plugins/woocommerce/src/Admin/**/* - plugins/woocommerce/src/Internal/Admin/**/* - plugins/woocommerce-admin/**/* + +'focus: performance tests [team:Solaris]': +- plugins/woocommerce/tests/performance/**/* + +'focus: api tests [team:Solaris]': +- plugins/woocommerce/tests/api-core-tests/**/* + +'focus: e2e tests [team:Solaris]': +- plugins/woocommerce/tests/e2e-pw/**/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 423398bb8be..0f9942b0506 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,9 +31,9 @@ jobs: include: - wp: nightly php: '7.4' - - wp: '5.9' + - wp: '6.0' php: 7.4 - - wp: '5.8' + - wp: '5.9' php: 7.4 services: database: diff --git a/.github/workflows/cot-build-and-e2e-tests-daily.yml b/.github/workflows/cot-build-and-e2e-tests-daily.yml index a38d042cc1e..7ef685e22fe 100644 --- a/.github/workflows/cot-build-and-e2e-tests-daily.yml +++ b/.github/workflows/cot-build-and-e2e-tests-daily.yml @@ -173,7 +173,7 @@ jobs: return await script( { core } ) - name: Find PR comment by github-actions[bot] - uses: peter-evans/find-comment@v2 + uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb id: find-comment with: issue-number: ${{ github.event.pull_request.number }} @@ -181,7 +181,7 @@ jobs: body-includes: Test Results Summary - name: Create or update PR comment - uses: peter-evans/create-or-update-comment@v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/pr-build-and-e2e-tests.yml b/.github/workflows/pr-build-and-e2e-tests.yml index ed1eec649de..e267c3502b2 100644 --- a/.github/workflows/pr-build-and-e2e-tests.yml +++ b/.github/workflows/pr-build-and-e2e-tests.yml @@ -219,7 +219,7 @@ jobs: return await script( { core } ) - name: Find PR comment by github-actions[bot] - uses: peter-evans/find-comment@v2 + uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb id: find-comment with: issue-number: ${{ github.event.pull_request.number }} @@ -227,7 +227,7 @@ jobs: body-includes: Test Results Summary - name: Create or update PR comment - uses: peter-evans/create-or-update-comment@v2 + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/pr-highlight-changes.yml b/.github/workflows/pr-highlight-changes.yml index 069f1bbbe8c..b1ecc977f14 100644 --- a/.github/workflows/pr-highlight-changes.yml +++ b/.github/workflows/pr-highlight-changes.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v3 - name: Install prerequisites run: | - npm install -g pnpm + npm install -g pnpm@7 npm -g i @wordpress/env@5.1.0 pnpm install --filter code-analyzer --filter cli-core - name: Run analyzer diff --git a/.github/workflows/pr-unit-tests.yml b/.github/workflows/pr-unit-tests.yml index e328e50edcf..a5fcc87bcf0 100644 --- a/.github/workflows/pr-unit-tests.yml +++ b/.github/workflows/pr-unit-tests.yml @@ -15,12 +15,14 @@ permissions: {} jobs: test: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }} - name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} + name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} ${{ matrix.hpos && 'HPOS' || '' }} timeout-minutes: 30 runs-on: ubuntu-20.04 permissions: contents: read continue-on-error: ${{ matrix.wp == 'nightly' }} + env: + HPOS: ${{ matrix.hpos }} strategy: fail-fast: false matrix: @@ -33,6 +35,9 @@ jobs: php: 7.4 - wp: '5.9' php: 7.4 + - wp: 'latest' + php: '7.4' + hpos: true services: database: image: mysql:5.6 diff --git a/.github/workflows/review-testing-instructions.yml b/.github/workflows/review-testing-instructions.yml new file mode 100644 index 00000000000..066c502a1fb --- /dev/null +++ b/.github/workflows/review-testing-instructions.yml @@ -0,0 +1,48 @@ +name: Remind reviewers to also review the testing instructions. +on: + pull_request: + types: [review_requested] + +permissions: {} + +jobs: + add-testing-instructions-review-comment: + runs-on: ubuntu-20.04 + permissions: + pull-requests: write + steps: + - name: Get the username of requested reviewers + id: get_reviewer_username + run: | + # Retrieves the username of all reviewers and stores them in a comma-separated list + reviewers=$(echo '${{ toJson(github.event.pull_request.requested_reviewers[*].login) }}' | jq -r 'map("@\(.)") | join(", ")') + echo "REVIEWERS=$reviewers" >> $GITHUB_ENV + + - name: Get the name of requested teams + id: get_team_name + run: | + # Retrieves the name of all teams asked for review and stores them in a comma-separated list + teams=$(echo '${{ toJson(github.event.pull_request.requested_teams[*].slug) }}' | jq -r 'map("@woocommerce/\(.)") | join(", ")') + echo "TEAMS=$teams" >> $GITHUB_ENV + + - name: Find the comment by github-actions[bot] asking for reviewing the testing instructions + uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb + id: find-comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: please make sure to review the testing instructions + + - name: Create or update PR comment asking for reviewers to review the testing instructions + uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body: | + Hi ${{ env.REVIEWERS }}, ${{ env.TEAMS }} + + Apart from reviewing the code changes, please make sure to review the testing instructions as well. + + You can follow this guide to find out what good testing instructions should look like: + https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions + edit-mode: replace diff --git a/.github/workflows/smoke-test-pr-merge.yml b/.github/workflows/smoke-test-pr-merge.yml new file mode 100644 index 00000000000..60720ea8454 --- /dev/null +++ b/.github/workflows/smoke-test-pr-merge.yml @@ -0,0 +1,168 @@ +name: Run tests against trunk after PR merge +on: + pull_request: + types: + - closed +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true +permissions: {} + +jobs: + api: + name: Run API tests + runs-on: ubuntu-20.04 + if: (github.event.pull_request.merged == true) && (github.event.pull_request.base.ref == 'trunk') + permissions: + contents: read + 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 + ARTIFACT_NAME: api-pr-merge-${{ github.event.pull_request.number }}-run-${{ github.run_number }} + steps: + - name: Checkout merge commit on trunk + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.merge_commit_sha }} + + - name: Setup WooCommerce Monorepo + uses: ./.github/actions/setup-woocommerce-monorepo + with: + build-filters: woocommerce + + - name: Setup local test environment + uses: ./.github/actions/tests/setup-local-test-environment + with: + test-type: api + + - name: Run API tests + id: run-api-composite-action + uses: ./.github/actions/tests/run-api-tests + with: + report-name: ${{ env.ARTIFACT_NAME }} + + - name: Upload Allure files to bucket + if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' ) + uses: ./.github/actions/tests/upload-allure-files-to-bucket + with: + aws-access-key-id: ${{ secrets.REPORTS_AWS_ACCESS_KEY_ID }} + aws-region: ${{ secrets.REPORTS_AWS_REGION }} + aws-secret-access-key: ${{ secrets.REPORTS_AWS_SECRET_ACCESS_KEY }} + artifact-name: ${{ env.ARTIFACT_NAME }} + s3-bucket: ${{ secrets.REPORTS_BUCKET }} + + - name: Publish Allure report + if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' ) + env: + GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} + run: | + gh workflow run publish-test-reports-trunk-merge.yml \ + -f run_id=${{ github.run_id }} \ + -f artifact=${{ env.ARTIFACT_NAME }} \ + -f pr_number=${{ github.event.pull_request.number }} \ + -f test_type="api" \ + --repo woocommerce/woocommerce-test-reports + + - name: Send Slack alert on test failure + if: failure() && steps.run-api-composite-action.conclusion == 'failure' + uses: ./.github/actions/tests/slack-alert-on-pr-merge + with: + slack-bot-token: ${{ secrets.E2E_SLACK_TOKEN }} + channel-id: ${{ secrets.E2E_TRUNK_SLACK_CHANNEL }} + test-type: API + + e2e: + name: Run E2E tests + needs: [api] + runs-on: ubuntu-20.04 + permissions: + contents: read + env: + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-report + ARTIFACT_NAME: e2e-pr-merge-${{ github.event.pull_request.number }}-run-${{ github.run_number }} + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.merge_commit_sha }} + + - name: Setup WooCommerce Monorepo + uses: ./.github/actions/setup-woocommerce-monorepo + with: + build-filters: woocommerce + + - name: Setup local test environment + uses: ./.github/actions/tests/setup-local-test-environment + with: + test-type: e2e + + - name: Run E2E tests + id: run-e2e-composite-action + timeout-minutes: 60 + uses: ./.github/actions/tests/run-e2e-tests + env: + E2E_MAX_FAILURES: 15 + with: + report-name: ${{ env.ARTIFACT_NAME }} + + - name: Upload Allure files to bucket + if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' ) + uses: ./.github/actions/tests/upload-allure-files-to-bucket + with: + aws-access-key-id: ${{ secrets.REPORTS_AWS_ACCESS_KEY_ID }} + aws-region: ${{ secrets.REPORTS_AWS_REGION }} + aws-secret-access-key: ${{ secrets.REPORTS_AWS_SECRET_ACCESS_KEY }} + artifact-name: ${{ env.ARTIFACT_NAME }} + s3-bucket: ${{ secrets.REPORTS_BUCKET }} + include-allure-results: false + + - name: Publish Allure report + if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' ) + env: + GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} + run: | + gh workflow run publish-test-reports-trunk-merge.yml \ + -f run_id=${{ github.run_id }} \ + -f artifact=${{ env.ARTIFACT_NAME }} \ + -f pr_number=${{ github.event.pull_request.number }} \ + -f test_type="e2e" \ + --repo woocommerce/woocommerce-test-reports + + - name: Send Slack alert on test failure + if: failure() && steps.run-e2e-composite-action.conclusion == 'failure' + uses: ./.github/actions/tests/slack-alert-on-pr-merge + with: + slack-bot-token: ${{ secrets.E2E_SLACK_TOKEN }} + channel-id: ${{ secrets.E2E_TRUNK_SLACK_CHANNEL }} + test-type: E2E + + k6: + name: Run k6 Performance tests + needs: [api] + runs-on: ubuntu-20.04 + permissions: + contents: read + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.merge_commit_sha }} + + - name: Setup WooCommerce Monorepo + uses: ./.github/actions/setup-woocommerce-monorepo + + - name: Setup local test environment + uses: ./.github/actions/tests/setup-local-test-environment + with: + test-type: k6 + + - name: Run k6 performance tests + id: run-k6-composite-action + uses: './.github/actions/tests/run-k6-tests' + + - name: Send Slack alert on test failure + if: failure() && steps.run-k6-composite-action.conclusion == 'failure' + uses: ./.github/actions/tests/slack-alert-on-pr-merge + with: + slack-bot-token: ${{ secrets.E2E_SLACK_TOKEN }} + channel-id: ${{ secrets.E2E_TRUNK_SLACK_CHANNEL }} + test-type: k6 diff --git a/.github/workflows/smoke-test-release.yml b/.github/workflows/smoke-test-release.yml index 2bda71ec90e..bc07de36ba7 100644 --- a/.github/workflows/smoke-test-release.yml +++ b/.github/workflows/smoke-test-release.yml @@ -12,9 +12,8 @@ concurrency: cancel-in-progress: true permissions: {} env: - E2E_WP_LATEST_ARTIFACT: e2e-wp-latest--run-${{ github.run_number }} - E2E_UPDATE_WC_ARTIFACT: e2e-update-wc--run-${{ github.run_number }} - FORCE_COLOR: 1 + E2E_WP_LATEST_ARTIFACT: E2E test on release smoke test site with WP Latest (run ${{ github.run_number }}) + E2E_UPDATE_WC_ARTIFACT: WooCommerce version update test on release smoke test site (run ${{ github.run_number }}) jobs: get-tag: @@ -80,12 +79,13 @@ 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 + - name: Run E2E tests + id: run-e2e-composite-action + timeout-minutes: 60 + uses: ./.github/actions/tests/run-e2e-tests + with: + report-name: ${{ env.E2E_UPDATE_WC_ARTIFACT }} + tests: update-woocommerce.spec.js env: ADMIN_PASSWORD: ${{ secrets.RELEASE_TEST_ADMIN_PASSWORD }} ADMIN_USER: ${{ secrets.RELEASE_TEST_ADMIN_USER }} @@ -95,36 +95,19 @@ jobs: DEFAULT_TIMEOUT_OVERRIDE: 120000 GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }} UPDATE_WC: ${{ needs.get-tag.outputs.tag }} - run: | - pnpm exec playwright test \ - --config=tests/e2e-pw/playwright.config.js \ - update-woocommerce.spec.js - - name: Generate 'Update WooCommerce' 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: Configure AWS credentials - if: success() || failure() - uses: aws-actions/configure-aws-credentials@v1-node16 + - name: Upload Allure artifacts to bucket + if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' ) + uses: ./.github/actions/tests/upload-allure-files-to-bucket with: - aws-region: ${{ secrets.REPORTS_AWS_REGION }} aws-access-key-id: ${{ secrets.REPORTS_AWS_ACCESS_KEY_ID }} + aws-region: ${{ secrets.REPORTS_AWS_REGION }} aws-secret-access-key: ${{ secrets.REPORTS_AWS_SECRET_ACCESS_KEY }} - - - name: Upload Allure files to bucket - if: success() || failure() - run: | - aws s3 sync ${{ env.ALLURE_RESULTS_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.E2E_WP_LATEST_ARTIFACT }}/allure-results \ - --quiet - aws s3 sync ${{ env.ALLURE_REPORT_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.E2E_WP_LATEST_ARTIFACT }}/allure-report \ - --quiet + artifact-name: ${{ env.E2E_WP_LATEST_ARTIFACT }} + s3-bucket: ${{ secrets.REPORTS_BUCKET }} - name: Publish E2E Allure report - if: success() || failure() + if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' ) env: GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} ENV_DESCRIPTION: wp-latest @@ -139,17 +122,6 @@ jobs: -f test_type="e2e" \ --repo woocommerce/woocommerce-test-reports - - name: Archive 'Update WooCommerce' test report - if: success() || failure() - uses: actions/upload-artifact@v3 - with: - name: ${{ env.E2E_UPDATE_WC_ARTIFACT }} - path: | - ${{ env.ALLURE_RESULTS_DIR }} - ${{ env.ALLURE_REPORT_DIR }} - if-no-files-found: ignore - retention-days: 5 - api-wp-latest: name: API on WP Latest runs-on: ubuntu-20.04 @@ -159,7 +131,7 @@ jobs: env: ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results - API_WP_LATEST_ARTIFACT: api-wp-latest--run-${{ github.run_number }} + API_WP_LATEST_ARTIFACT: API test on release smoke test site with WP Latest (run ${{ github.run_number }}) steps: - uses: actions/checkout@v3 @@ -169,41 +141,29 @@ jobs: install-filters: woocommerce build: false - - name: Run API tests. - working-directory: plugins/woocommerce + - 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 env: BASE_URL: ${{ secrets.RELEASE_TEST_URL }} USER_KEY: ${{ secrets.RELEASE_TEST_ADMIN_USER }} USER_SECRET: ${{ secrets.RELEASE_TEST_ADMIN_PASSWORD }} - run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js hello - - 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: Configure AWS credentials - if: success() || failure() - uses: aws-actions/configure-aws-credentials@v1-node16 + - name: Upload Allure artifacts to bucket + if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' ) + uses: ./.github/actions/tests/upload-allure-files-to-bucket with: - aws-region: ${{ secrets.REPORTS_AWS_REGION }} aws-access-key-id: ${{ secrets.REPORTS_AWS_ACCESS_KEY_ID }} + aws-region: ${{ secrets.REPORTS_AWS_REGION }} aws-secret-access-key: ${{ secrets.REPORTS_AWS_SECRET_ACCESS_KEY }} - - - name: Upload Allure files to bucket - if: success() || failure() - run: | - aws s3 cp ${{ env.ALLURE_RESULTS_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.API_WP_LATEST_ARTIFACT }}/allure-results \ - --recursive \ - --quiet - aws s3 cp ${{ env.ALLURE_REPORT_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.API_WP_LATEST_ARTIFACT }}/allure-report \ - --recursive \ - --quiet + artifact-name: ${{ env.API_WP_LATEST_ARTIFACT }} + s3-bucket: ${{ secrets.REPORTS_BUCKET }} - name: Publish API Allure report - if: success() || failure() + if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' ) env: GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} ENV_DESCRIPTION: wp-latest @@ -218,17 +178,6 @@ jobs: -f test_type="api" \ --repo woocommerce/woocommerce-test-reports - - name: Archive API test report - if: success() || failure() - uses: actions/upload-artifact@v3 - with: - name: ${{ env.API_WP_LATEST_ARTIFACT }} - path: | - ${{ env.ALLURE_RESULTS_DIR }} - ${{ env.ALLURE_REPORT_DIR }} - if-no-files-found: ignore - retention-days: 5 - e2e-wp-latest: name: E2E on WP Latest runs-on: ubuntu-20.04 @@ -247,11 +196,13 @@ jobs: install-filters: woocommerce build: false - - name: Download and install Chromium browser. - working-directory: plugins/woocommerce - run: pnpm exec playwright install chromium - - name: Run E2E tests + id: run-e2e-composite-action + timeout-minutes: 60 + uses: ./.github/actions/tests/run-e2e-tests + with: + report-name: e2e-wp-latest--partial--run-${{ github.run_number }} + playwright-config: ignore-plugin-tests.playwright.config.js env: ADMIN_PASSWORD: ${{ secrets.RELEASE_TEST_ADMIN_PASSWORD }} ADMIN_USER: ${{ secrets.RELEASE_TEST_ADMIN_USER }} @@ -262,9 +213,6 @@ jobs: DEFAULT_TIMEOUT_OVERRIDE: 120000 E2E_MAX_FAILURES: 25 RESET_SITE: true - timeout-minutes: 60 - working-directory: plugins/woocommerce - run: pnpm exec playwright test --config=tests/e2e-pw/ignore-plugin-tests.playwright.config.js - name: Download 'e2e-update-wc' artifact if: success() || failure() @@ -283,23 +231,26 @@ jobs: working-directory: plugins/woocommerce run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }} - - name: Configure AWS credentials + - name: Archive E2E test report if: success() || failure() - uses: aws-actions/configure-aws-credentials@v1-node16 + uses: actions/upload-artifact@v3 with: - aws-region: ${{ secrets.REPORTS_AWS_REGION }} - aws-access-key-id: ${{ secrets.REPORTS_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.REPORTS_AWS_SECRET_ACCESS_KEY }} + name: ${{ env.E2E_WP_LATEST_ARTIFACT }} + path: | + ${{ env.ALLURE_RESULTS_DIR }} + ${{ env.ALLURE_REPORT_DIR }} + if-no-files-found: ignore + retention-days: 5 - - name: Upload report to bucket - if: success() || failure() - run: | - aws s3 sync ${{ env.ALLURE_RESULTS_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.E2E_WP_LATEST_ARTIFACT }}/allure-results \ - --quiet - aws s3 sync ${{ env.ALLURE_REPORT_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.E2E_WP_LATEST_ARTIFACT }}/allure-report \ - --quiet + - name: Upload Allure artifacts to bucket + if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' ) + uses: ./.github/actions/tests/upload-allure-files-to-bucket + with: + aws-access-key-id: ${{ secrets.REPORTS_AWS_ACCESS_KEY_ID }} + aws-region: ${{ secrets.REPORTS_AWS_REGION }} + aws-secret-access-key: ${{ secrets.REPORTS_AWS_SECRET_ACCESS_KEY }} + artifact-name: ${{ env.E2E_WP_LATEST_ARTIFACT }} + s3-bucket: ${{ secrets.REPORTS_BUCKET }} - name: Publish E2E Allure report if: success() || failure() @@ -317,17 +268,6 @@ jobs: -f test_type="e2e" \ --repo woocommerce/woocommerce-test-reports - - name: Archive E2E test report - if: success() || failure() - uses: actions/upload-artifact@v3 - with: - name: ${{ env.E2E_WP_LATEST_ARTIFACT }} - path: | - ${{ env.ALLURE_RESULTS_DIR }} - ${{ env.ALLURE_REPORT_DIR }} - if-no-files-found: ignore - retention-days: 5 - get-wp-versions: name: Get WP L-1 & L-2 version numbers needs: [get-tag] @@ -371,14 +311,15 @@ jobs: runs-on: ubuntu-20.04 needs: [get-wp-versions] strategy: + fail-fast: false matrix: ${{ fromJSON(needs.get-wp-versions.outputs.matrix) }} env: API_ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/api/allure-report API_ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/api/allure-results - API_WP_LATEST_X_ARTIFACT: api-${{ matrix.version.env_description }}--run-${{ github.run_number }} + API_WP_LATEST_X_ARTIFACT: API test on wp-env with WordPress ${{ matrix.version.number }} (run ${{ github.run_number }}) E2E_ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/e2e/allure-report E2E_ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/e2e/allure-results - E2E_WP_LATEST_X_ARTIFACT: e2e-${{ matrix.version.env_description }}--run-${{ github.run_number }} + E2E_WP_LATEST_X_ARTIFACT: E2E test on wp-env with WordPress ${{ matrix.version.number }} (run ${{ github.run_number }}) permissions: contents: read steps: @@ -414,38 +355,31 @@ jobs: pnpm exec wp-env run tests-cli "wp theme list" pnpm exec wp-env run tests-cli "wp user list" - - name: Run API tests. - id: api - working-directory: plugins/woocommerce + - name: Run API tests + id: run-api-composite-action + uses: ./.github/actions/tests/run-api-tests + with: + report-name: ${{ env.API_WP_LATEST_X_ARTIFACT }} + tests: hello env: ALLURE_RESULTS_DIR: ${{ env.API_ALLURE_RESULTS_DIR }} - run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js hello + ALLURE_REPORT_DIR: ${{ env.API_ALLURE_REPORT_DIR }} - - name: Generate API Allure report. - if: success() || failure() - working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean ${{ env.API_ALLURE_RESULTS_DIR }} --output ${{ env.API_ALLURE_REPORT_DIR }} - - - name: Configure AWS credentials - if: success() || failure() - uses: aws-actions/configure-aws-credentials@v1-node16 + - name: Upload Allure artifacts to bucket + if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' ) + uses: ./.github/actions/tests/upload-allure-files-to-bucket + env: + ALLURE_RESULTS_DIR: ${{ env.API_ALLURE_RESULTS_DIR }} + ALLURE_REPORT_DIR: ${{ env.API_ALLURE_REPORT_DIR }} with: - aws-region: ${{ secrets.REPORTS_AWS_REGION }} aws-access-key-id: ${{ secrets.REPORTS_AWS_ACCESS_KEY_ID }} + aws-region: ${{ secrets.REPORTS_AWS_REGION }} aws-secret-access-key: ${{ secrets.REPORTS_AWS_SECRET_ACCESS_KEY }} - - - name: Upload API Allure artifacts to bucket - if: success() || failure() - run: | - aws s3 sync ${{ env.API_ALLURE_RESULTS_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.API_WP_LATEST_X_ARTIFACT }}/allure-results \ - --quiet - aws s3 sync ${{ env.API_ALLURE_REPORT_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.API_WP_LATEST_X_ARTIFACT }}/allure-report \ - --quiet + artifact-name: ${{ env.API_WP_LATEST_X_ARTIFACT }} + s3-bucket: ${{ secrets.REPORTS_BUCKET }} - name: Publish API Allure report - if: success() || failure() + if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' ) env: GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} ENV_DESCRIPTION: ${{ matrix.version.env_description }} @@ -460,65 +394,33 @@ jobs: -f test_type="api" \ --repo woocommerce/woocommerce-test-reports - - name: Archive API Allure reports - if: success() || failure() - uses: actions/upload-artifact@v3 - with: - name: ${{ env.API_WP_LATEST_X_ARTIFACT }} - path: | - ${{ env.API_ALLURE_RESULTS_DIR }} - ${{ env.API_ALLURE_REPORT_DIR }} - if-no-files-found: ignore - retention-days: 5 - - - name: Download and install Chromium browser. - if: success() || failure() - working-directory: plugins/woocommerce - run: pnpm exec playwright install chromium - - - name: Run E2E tests. - if: | - success() || - ( failure() && steps.api.conclusion == 'success' ) + - name: Run E2E tests + id: run-e2e-composite-action timeout-minutes: 60 - id: e2e + uses: ./.github/actions/tests/run-e2e-tests env: - USE_WP_ENV: 1 E2E_MAX_FAILURES: 15 - FORCE_COLOR: 1 ALLURE_RESULTS_DIR: ${{ env.E2E_ALLURE_RESULTS_DIR }} + ALLURE_REPORT_DIR: ${{ env.E2E_ALLURE_REPORT_DIR }} DEFAULT_TIMEOUT_OVERRIDE: 120000 - working-directory: plugins/woocommerce - run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js - - - name: Generate E2E Allure report. - if: success() || failure() - working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean ${{ env.E2E_ALLURE_RESULTS_DIR }} --output ${{ env.E2E_ALLURE_REPORT_DIR }} - - - name: Upload E2E Allure artifacts to bucket - if: success() || failure() - run: | - aws s3 sync ${{ env.E2E_ALLURE_RESULTS_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.E2E_WP_LATEST_X_ARTIFACT }}/allure-results \ - --quiet - aws s3 sync ${{ env.E2E_ALLURE_REPORT_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.E2E_WP_LATEST_X_ARTIFACT }}/allure-report \ - --quiet - - - name: Archive E2E Allure reports - if: success() || failure() - uses: actions/upload-artifact@v3 with: - name: ${{ env.E2E_WP_LATEST_X_ARTIFACT }} - path: | - ${{ env.E2E_ALLURE_RESULTS_DIR }} - ${{ env.E2E_ALLURE_REPORT_DIR }} - if-no-files-found: ignore - retention-days: 5 + report-name: ${{ env.E2E_WP_LATEST_X_ARTIFACT }} + + - name: Upload Allure artifacts to bucket + if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' ) + uses: ./.github/actions/tests/upload-allure-files-to-bucket + env: + ALLURE_RESULTS_DIR: ${{ env.E2E_ALLURE_RESULTS_DIR }} + ALLURE_REPORT_DIR: ${{ env.E2E_ALLURE_REPORT_DIR }} + with: + aws-access-key-id: ${{ secrets.REPORTS_AWS_ACCESS_KEY_ID }} + aws-region: ${{ secrets.REPORTS_AWS_REGION }} + aws-secret-access-key: ${{ secrets.REPORTS_AWS_SECRET_ACCESS_KEY }} + artifact-name: ${{ env.E2E_WP_LATEST_X_ARTIFACT }} + s3-bucket: ${{ secrets.REPORTS_BUCKET }} - name: Publish E2E Allure report - if: success() || failure() + if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' ) env: GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} ENV_DESCRIPTION: ${{ matrix.version.env_description }} @@ -538,15 +440,16 @@ jobs: runs-on: ubuntu-20.04 needs: [get-tag] strategy: + fail-fast: false matrix: php_version: ['7.4', '8.1'] env: API_ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-report API_ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-results - API_ARTIFACT: api-php-${{ matrix.php_version }}--run-${{ github.run_number }} + API_ARTIFACT: API test on wp-env with PHP ${{ matrix.php_version }} (run ${{ github.run_number }}) E2E_ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-report E2E_ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-results - E2E_ARTIFACT: e2e-php-${{ matrix.php_version }}--run-${{ github.run_number }} + E2E_ARTIFACT: E2E test on wp-env with PHP ${{ matrix.php_version }} (run ${{ github.run_number }}) steps: - name: Checkout uses: actions/checkout@v3 @@ -574,38 +477,31 @@ jobs: - name: Replace `plugins/woocommerce` with unzipped woocommerce release build run: unzip -d plugins -o tmp/woocommerce.zip - - name: Run API tests. - id: api - working-directory: plugins/woocommerce + - name: Run API tests + id: run-api-composite-action + uses: ./.github/actions/tests/run-api-tests + with: + report-name: ${{ env.API_ARTIFACT }} + tests: hello env: ALLURE_RESULTS_DIR: ${{ env.API_ALLURE_RESULTS_DIR }} - run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js hello + ALLURE_REPORT_DIR: ${{ env.API_ALLURE_REPORT_DIR }} - - name: Generate API Allure report. - if: success() || failure() - working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean ${{ env.API_ALLURE_RESULTS_DIR }} --output ${{ env.API_ALLURE_REPORT_DIR }} - - - name: Configure AWS credentials - if: success() || failure() - uses: aws-actions/configure-aws-credentials@v1-node16 + - name: Upload Allure artifacts to bucket + if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' ) + uses: ./.github/actions/tests/upload-allure-files-to-bucket + env: + ALLURE_RESULTS_DIR: ${{ env.API_ALLURE_RESULTS_DIR }} + ALLURE_REPORT_DIR: ${{ env.API_ALLURE_REPORT_DIR }} with: - aws-region: ${{ secrets.REPORTS_AWS_REGION }} aws-access-key-id: ${{ secrets.REPORTS_AWS_ACCESS_KEY_ID }} + aws-region: ${{ secrets.REPORTS_AWS_REGION }} aws-secret-access-key: ${{ secrets.REPORTS_AWS_SECRET_ACCESS_KEY }} - - - name: Upload API Allure artifacts to bucket - if: success() || failure() - run: | - aws s3 sync ${{ env.API_ALLURE_RESULTS_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.API_ARTIFACT }}/allure-results \ - --quiet - aws s3 sync ${{ env.API_ALLURE_REPORT_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.API_ARTIFACT }}/allure-report \ - --quiet + artifact-name: ${{ env.API_ARTIFACT }} + s3-bucket: ${{ secrets.REPORTS_BUCKET }} - name: Publish API Allure report - if: success() || failure() + if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' ) env: GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} ENV_DESCRIPTION: php-${{ matrix.php_version }} @@ -620,63 +516,33 @@ jobs: -f test_type="api" \ --repo woocommerce/woocommerce-test-reports - - name: Archive API Allure reports - if: success() || failure() - uses: actions/upload-artifact@v3 - with: - name: ${{ env.API_ARTIFACT }} - path: | - ${{ env.API_ALLURE_RESULTS_DIR }} - ${{ env.API_ALLURE_REPORT_DIR }} - if-no-files-found: ignore - retention-days: 5 - - - name: Download and install Chromium browser. - working-directory: plugins/woocommerce - run: pnpm exec playwright install chromium - - - name: Run E2E tests. - if: | - success() || - ( failure() && steps.api.conclusion == 'success' ) + - name: Run E2E tests + id: run-e2e-composite-action timeout-minutes: 60 + uses: ./.github/actions/tests/run-e2e-tests env: - USE_WP_ENV: 1 - E2E_MAX_FAILURES: 15 - FORCE_COLOR: 1 ALLURE_RESULTS_DIR: ${{ env.E2E_ALLURE_RESULTS_DIR }} + ALLURE_REPORT_DIR: ${{ env.E2E_ALLURE_REPORT_DIR }} DEFAULT_TIMEOUT_OVERRIDE: 120000 - working-directory: plugins/woocommerce - run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js - - - name: Generate E2E Allure report. - if: success() || failure() - working-directory: plugins/woocommerce - run: pnpm exec allure generate --clean ${{ env.E2E_ALLURE_RESULTS_DIR }} --output ${{ env.E2E_ALLURE_REPORT_DIR }} - - - name: Upload E2E Allure artifacts to bucket - if: success() || failure() - run: | - aws s3 sync ${{ env.E2E_ALLURE_RESULTS_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.E2E_ARTIFACT }}/allure-results \ - --quiet - aws s3 sync ${{ env.E2E_ALLURE_REPORT_DIR }} \ - ${{ secrets.REPORTS_BUCKET }}/artifacts/${{ github.run_id }}/${{ env.E2E_ARTIFACT }}/allure-report \ - --quiet - - - name: Archive E2E Allure reports - if: success() || failure() - uses: actions/upload-artifact@v3 + E2E_MAX_FAILURES: 15 with: - name: ${{ env.E2E_ARTIFACT }} - path: | - ${{ env.E2E_ALLURE_RESULTS_DIR }} - ${{ env.E2E_ALLURE_REPORT_DIR }} - if-no-files-found: ignore - retention-days: 5 + report-name: ${{ env.E2E_ARTIFACT }} + + - name: Upload Allure artifacts to bucket + if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' ) + uses: ./.github/actions/tests/upload-allure-files-to-bucket + env: + ALLURE_RESULTS_DIR: ${{ env.E2E_ALLURE_RESULTS_DIR }} + ALLURE_REPORT_DIR: ${{ env.E2E_ALLURE_REPORT_DIR }} + with: + aws-access-key-id: ${{ secrets.REPORTS_AWS_ACCESS_KEY_ID }} + aws-region: ${{ secrets.REPORTS_AWS_REGION }} + aws-secret-access-key: ${{ secrets.REPORTS_AWS_SECRET_ACCESS_KEY }} + artifact-name: ${{ env.E2E_ARTIFACT }} + s3-bucket: ${{ secrets.REPORTS_BUCKET }} - name: Publish E2E Allure report - if: success() || failure() + if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' ) env: GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} ENV_DESCRIPTION: php-${{ matrix.php_version }} @@ -690,3 +556,103 @@ jobs: -f env_description="${{ env.ENV_DESCRIPTION }}" \ -f test_type="e2e" \ --repo woocommerce/woocommerce-test-reports + + test-plugins: + name: With ${{ matrix.plugin }} + runs-on: ubuntu-20.04 + needs: [get-tag] + env: + ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results + ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report + ARTIFACT_NAME: E2E test on wp-env with ${{ matrix.plugin }} installed (run ${{ github.run_number }}) + strategy: + fail-fast: false + matrix: + include: + - plugin: 'WooCommerce Payments' + repo: 'automattic/woocommerce-payments' + env_description: 'woocommerce-payments' + - plugin: 'WooCommerce PayPal Payments' + repo: 'woocommerce/woocommerce-paypal-payments' + env_description: 'woocommerce-paypal-payments' + - plugin: 'WooCommerce Shipping & Tax' + repo: 'automattic/woocommerce-services' + env_description: 'woocommerce-shipping-&-tax' + - plugin: 'WooCommerce Subscriptions' + repo: WC_SUBSCRIPTIONS_REPO + private: true + env_description: 'woocommerce-subscriptions' + - plugin: 'WordPress SEO' # Yoast SEO in the UI, but the slug is wordpress-seo + repo: 'Yoast/wordpress-seo' + env_description: 'wordpress-seo' + - plugin: 'Contact Form 7' + repo: 'takayukister/contact-form-7' + env_description: 'contact-form-7' + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup WooCommerce Monorepo + uses: ./.github/actions/setup-woocommerce-monorepo + + - name: Launch WP Env + working-directory: plugins/woocommerce + run: pnpm run env:test + + - name: Download release zip + env: + GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }} + run: gh release download ${{ needs.get-tag.outputs.tag }} --dir tmp + + - name: Replace `plugins/woocommerce` with unzipped woocommerce release build + run: unzip -d plugins -o tmp/woocommerce.zip + + - name: Run 'Upload plugin' test + id: run-upload-test + timeout-minutes: 60 + uses: ./.github/actions/tests/run-e2e-tests + with: + report-name: ${{ env.ARTIFACT_NAME }} + tests: upload-plugin.spec.js + env: + GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }} + PLUGIN_NAME: ${{ matrix.plugin }} + PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }} + + - name: Run the rest of E2E tests + id: run-e2e-composite-action + timeout-minutes: 60 + uses: ./.github/actions/tests/run-e2e-tests + with: + playwright-config: ignore-plugin-tests.playwright.config.js + report-name: ${{ env.ARTIFACT_NAME }} + env: + E2E_MAX_FAILURES: 15 + + - name: Upload Allure artifacts to bucket + if: | + success() || + ( failure() && + ( steps.run-upload-test.conclusion == 'failure' || steps.run-e2e-composite-action.conclusion == 'failure' ) ) + uses: ./.github/actions/tests/upload-allure-files-to-bucket + with: + aws-access-key-id: ${{ secrets.REPORTS_AWS_ACCESS_KEY_ID }} + aws-region: ${{ secrets.REPORTS_AWS_REGION }} + aws-secret-access-key: ${{ secrets.REPORTS_AWS_SECRET_ACCESS_KEY }} + artifact-name: ${{ env.ARTIFACT_NAME }} + s3-bucket: ${{ secrets.REPORTS_BUCKET }} + + - name: Publish E2E Allure report + if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' ) + env: + GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }} + run: | + gh workflow run publish-test-reports-release.yml \ + -f created_at="${{ needs.get-tag.outputs.created }}" \ + -f run_id=${{ github.run_id }} \ + -f run_number=${{ github.run_number }} \ + -f release_tag=${{ needs.get-tag.outputs.tag }} \ + -f artifact="${{ env.ARTIFACT_NAME }}" \ + -f env_description="${{ matrix.env_description }}" \ + -f test_type="e2e" \ + --repo woocommerce/woocommerce-test-reports diff --git a/.github/workflows/status-check-bypass-changelog-only.yml b/.github/workflows/status-check-bypass-changelog-only.yml deleted file mode 100644 index a717ab5e1af..00000000000 --- a/.github/workflows/status-check-bypass-changelog-only.yml +++ /dev/null @@ -1,56 +0,0 @@ -# Duplicate workflow that returns success for this check when there is no relevant file change. See https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks - -name: Status Check Bypass for Changelog Only Changes -on: - pull_request: - paths: - - '!**' - - '**/changelog/**' - -jobs: - bypass-lint: - runs-on: ubuntu-latest - name: "Lint and Test JS" - steps: - - run: 'echo "No build required"' - bypass-7-4-latest: - runs-on: ubuntu-latest - name: "PHP 7.4 WP latest" - steps: - - run: 'echo "No build required"' - bypass-8-0-latest: - runs-on: ubuntu-latest - name: "PHP 8.0 WP latest" - steps: - - run: 'echo "No build required"' - bypass-api-tests: - runs-on: ubuntu-latest - name: "Runs API tests." - steps: - - run: 'echo "No build required"' - bypass-k6: - runs-on: ubuntu-latest - name: "Runs k6 Performance tests" - steps: - - run: 'echo "No build required"' - bypass-sniff: - runs-on: ubuntu-latest - name: "Code sniff (PHP 7.4, WP Latest)" - steps: - - run: 'echo "No build required"' - bypass-changelogger-use: - runs-on: ubuntu-latest - name: "Changelogger use" - steps: - - run: 'echo "No build required"' - bypass-e2e: - runs-on: ubuntu-latest - name: "Runs E2E tests." - steps: - - run: 'echo "No build required"' - bypass-pr-highlight: - runs-on: ubuntu-latest - name: "Check pull request changes to highlight" - steps: - - run: 'echo "No build required"' - diff --git a/.syncpackrc b/.syncpackrc index 57c61d0a79a..832c6f62e33 100644 --- a/.syncpackrc +++ b/.syncpackrc @@ -1,6 +1,6 @@ { "dev": true, - "filter": "^(?:config|react|react-dom|eslint|typescript|@typescript-eslint|@types/react|@wordpress|@types/wordpress__components|postcss).*$", + "filter": "^(?:config|react|react-dom|eslint|typescript|@typescript-eslint|@types/react|@wordpress|@types/wordpress__components|postcss|@types/node).*$", "indent": "\t", "overrides": true, "peer": true, @@ -59,7 +59,7 @@ "packages": [ "**" ], - "pinVersion": "^4.8.3" + "pinVersion": "^4.9.5" }, { "dependencies": [ @@ -154,6 +154,15 @@ "**" ], "pinVersion": "^8.4.7" + }, + { + "dependencies": [ + "@types/node" + ], + "packages": [ + "**" + ], + "pinVersion": "^16.18.18" } ] } diff --git a/bin/packages/js/extend-cart-checkout-block/.prettierrc.js.mustache b/bin/packages/js/extend-cart-checkout-block/.prettierrc.js.mustache new file mode 100644 index 00000000000..51b8aeb4150 --- /dev/null +++ b/bin/packages/js/extend-cart-checkout-block/.prettierrc.js.mustache @@ -0,0 +1,3 @@ +// Import the default config file and expose it in the project root. +// Useful for editor integrations. +module.exports = require( '@wordpress/prettier-config' ); diff --git a/changelog.txt b/changelog.txt index ab4640efbf2..30d4a66ac9e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,78 @@ == Changelog == += 7.5.1 2023-03-21 = + +**WooCommerce** + +* Fix - Fix no enforcing of min/max limits in quantity selector of variable products. [#36871](https://github.com/woocommerce/woocommerce/pull/36871) +* Dev - Update column definitions with synonymous types to prevent dbDelta from trying to ALTER them on each install. [#37277](https://github.com/woocommerce/woocommerce/pull/37277) +* Update - Update WooCommerce Blocks to 9.6.6. [#37298](https://github.com/woocommerce/woocommerce/pull/37298) + += 7.5.0 2023-03-14 = + +**WooCommerce** + +* Fix - Add HPOS support to the reserved stock query [#36535](https://github.com/woocommerce/woocommerce/pull/36535) +* Fix - Comment: Fix inconsistencies on Analytics > Orders table when using date_paid or date_completed [#36876](https://github.com/woocommerce/woocommerce/pull/36876) +* Fix - Define a public `api` property in the WooCommerce class to prevent a PHP deprecation warning [#36545](https://github.com/woocommerce/woocommerce/pull/36545) +* Fix - Don't delete order from posts table when deleted from orders table if the later is authoritative and sync is off [#36617](https://github.com/woocommerce/woocommerce/pull/36617) +* Fix - Eliminate data store internal meta keys duplicates [#36611](https://github.com/woocommerce/woocommerce/pull/36611) +* Fix - Ensure changes made via the `woocommerce_order_list_table_prepare_items_query_args` are observed. [#36649](https://github.com/woocommerce/woocommerce/pull/36649) +* Fix - Ensuring that we know if allowTracking is true before adding exit page. [#36656](https://github.com/woocommerce/woocommerce/pull/36656) +* Fix - Fix Ampersand changed to & on product attribute export [#36525](https://github.com/woocommerce/woocommerce/pull/36525) +* Fix - Fix decimal points for NOK currency [#36780](https://github.com/woocommerce/woocommerce/pull/36780) +* Fix - Fix inconsitent product task icon colors [#36889](https://github.com/woocommerce/woocommerce/pull/36889) +* Fix - Fix WordPress unit tests libraries being installed in a symlinked folder structure [#36641](https://github.com/woocommerce/woocommerce/pull/36641) +* Fix - Make states optional for Hungary and Bulgaria. [#36701](https://github.com/woocommerce/woocommerce/pull/36701) +* Fix - Screen ID matching switched to untranslated 'woocommerce' strings. [#36854](https://github.com/woocommerce/woocommerce/pull/36854) +* Fix - Translate the labels for units of measure. [#36708](https://github.com/woocommerce/woocommerce/pull/36708) +* Fix - Update `config@3.3.7` (from `3.3.3`). Fix `node_env_var_name is not defined` error. [#33828](https://github.com/woocommerce/woocommerce/pull/33828) +* Add - Add 'add_tab' method in FormFactory to allow plugins to extend the WooCommerce admin product form [#36583](https://github.com/woocommerce/woocommerce/pull/36583) +* Add - Add @woocommerce/product-editor dependency and change dependency of ProductSectionLayout component. [#36600](https://github.com/woocommerce/woocommerce/pull/36600) +* Add - Add additional global attributes and local attributes information when saving product attributes [#36858](https://github.com/woocommerce/woocommerce/pull/36858) +* Add - Add a new Channels card in multichannel marketing page. [#36541](https://github.com/woocommerce/woocommerce/pull/36541) +* Add - Add an experimental slot for marketing overview extensibility [#36828](https://github.com/woocommerce/woocommerce/pull/36828) +* Add - Add slot fill support for tabs for the new product management MVP. [#36551](https://github.com/woocommerce/woocommerce/pull/36551) +* Add - Add survey after disabling new experience [#36544](https://github.com/woocommerce/woocommerce/pull/36544) +* Add - Add unique sku option to error data when setting product sku [#36612](https://github.com/woocommerce/woocommerce/pull/36612) +* Add - Add WC-specific criteria to the Site Health test for persistent object caches [#35202](https://github.com/woocommerce/woocommerce/pull/35202) +* Add - Enable new experience when new user selects "Physical product". [#36406](https://github.com/woocommerce/woocommerce/pull/36406) +* Update - Update WooCommerce Blocks to 9.6.5 [#37051](https://github.com/woocommerce/woocommerce/pull/37051) +* Update - Update WooCommerce Blocks to 9.6.3 [#36992](https://github.com/woocommerce/woocommerce/pull/36992) +* Update - Update WooCommerce Blocks to 9.6.2 [#36919](https://github.com/woocommerce/woocommerce/pull/36919) +* Update - Add date_paid and date_completed date sorting options for Revenue and Order reports [#36492](https://github.com/woocommerce/woocommerce/pull/36492) +* Update - Add default value for backorders [#36607](https://github.com/woocommerce/woocommerce/pull/36607) +* Update - Add Skydropx, Envia, Sendcloud, Packlink to shipping task [#36873](https://github.com/woocommerce/woocommerce/pull/36873) +* Update - Always show comments for product feedback form [#36484](https://github.com/woocommerce/woocommerce/pull/36484) +* Update - Delete FlexSlider code for legacy browsers. [#36690](https://github.com/woocommerce/woocommerce/pull/36690) +* Update - Disable the new product editor, pending design updates. [#36894](https://github.com/woocommerce/woocommerce/pull/36894) +* Update - Have "Grow your store" appear first in marketing task by default [#36826](https://github.com/woocommerce/woocommerce/pull/36826) +* Update - Migrating product editor pricing section to slot fills. [#36500](https://github.com/woocommerce/woocommerce/pull/36500) +* Update - Refactor slot fills to ensure variant fills have distinct slots. [#36646](https://github.com/woocommerce/woocommerce/pull/36646) +* Update - Removed I.D column from product import samples [#36857](https://github.com/woocommerce/woocommerce/pull/36857) +* Update - Remove Meta from grow your store list [#36886](https://github.com/woocommerce/woocommerce/pull/36886) +* Update - Remove opinionated styles from buttons in block themes so they inherit theme styles more accurately [#36651](https://github.com/woocommerce/woocommerce/pull/36651) +* Update - Replace $.ajax() calls with browser-native window.fetch() calls. [#36275](https://github.com/woocommerce/woocommerce/pull/36275) +* Update - Update payment gateway list ordering priority and remove Klarna from North America [#36550](https://github.com/woocommerce/woocommerce/pull/36550) +* Update - Update Playwright version from 1.28.0 -> 1.30.0 [#36789](https://github.com/woocommerce/woocommerce/pull/36789) +* Update - Updates to product editor fill to support new prop API. [#36592](https://github.com/woocommerce/woocommerce/pull/36592) +* Update - Update WooCommerce Blocks 9.6.0 & 9.6.1 [#36852](https://github.com/woocommerce/woocommerce/pull/36852) +* Dev - Add attribute creation form when there are no attributes [#36606](https://github.com/woocommerce/woocommerce/pull/36606) +* Dev - Add a unit test for woocommerce_admin_experimental_onboarding_tasklists filter [#36827](https://github.com/woocommerce/woocommerce/pull/36827) +* Dev - Code refactor on marketing components. [#36540](https://github.com/woocommerce/woocommerce/pull/36540) +* Dev - Made e2e selectors more robust [#36499](https://github.com/woocommerce/woocommerce/pull/36499) +* Dev - Remove attribute type logic from attribute component [#36563](https://github.com/woocommerce/woocommerce/pull/36563) +* Dev - Update eslint to 8.32.0 across the monorepo. [#36700](https://github.com/woocommerce/woocommerce/pull/36700) +* Dev - Update pnpm command to run e2e tests for consistency. Also update docs with new command. [#35287](https://github.com/woocommerce/woocommerce/pull/35287) +* Tweak - Add IR and fields priorities to list of get_country_locale() method to follow conventional way of addressing in Iran. [#36491](https://github.com/woocommerce/woocommerce/pull/36491) +* Tweak - Add missing deprecation notice for filter hook woocommerce_my_account_my_orders_columns. [#36356](https://github.com/woocommerce/woocommerce/pull/36356) +* Tweak - Adjust default sizes for the quantity and coupon input fields within the cart page. [#29122](https://github.com/woocommerce/woocommerce/pull/29122) +* Tweak - Do not display low/out-of-stock information in the dashboard status widget when stock management is disabled. [#36703](https://github.com/woocommerce/woocommerce/pull/36703) +* Tweak - Remove free trial terms from Avalara tax task [#36888](https://github.com/woocommerce/woocommerce/pull/36888) +* Tweak - Tweak product link description and display in the new product management experience [#36591](https://github.com/woocommerce/woocommerce/pull/36591) +* Enhancement - Change the sass variable names to more predictable ones. [#28908](https://github.com/woocommerce/woocommerce/pull/28908) + + = 7.4.1 2023-03-01 = **WooCommerce** diff --git a/changelog/e2e-fix-allure-upload-input b/changelog/e2e-fix-allure-upload-input new file mode 100644 index 00000000000..3ff1edec50d --- /dev/null +++ b/changelog/e2e-fix-allure-upload-input @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix the incorrect workflow input for uploading Allure reports. diff --git a/package.json b/package.json index 84bb3b8b7ec..adb2745dd07 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,10 @@ "title": "WooCommerce Monorepo", "description": "Monorepo for the WooCommerce ecosystem", "homepage": "https://woocommerce.com/", + "engines": { + "node": "^16.13.1", + "pnpm": "^7.13.3" + }, "private": true, "repository": { "type": "git", @@ -25,41 +29,41 @@ "sync-dependencies": "pnpm exec syncpack -- fix-mismatches" }, "devDependencies": { - "@babel/preset-env": "^7.16.11", - "@babel/runtime": "^7.17.2", - "@types/node": "14.14.33", + "@babel/preset-env": "^7.20.2", + "@babel/runtime": "^7.21.0", + "@types/node": "^16.18.18", "@woocommerce/eslint-plugin": "workspace:*", "@wordpress/data": "wp-6.0", "@wordpress/eslint-plugin": "^11.1.0", - "@wordpress/prettier-config": "^1.1.1", - "babel-loader": "^8.2.3", + "@wordpress/prettier-config": "^1.4.0", + "babel-loader": "^8.3.0", "chalk": "^4.1.2", "copy-webpack-plugin": "^10.2.4", - "core-js": "^3.21.1", - "css-loader": "^6.7.0", - "glob": "^7.2.0", + "core-js": "^3.29.1", + "css-loader": "^6.7.3", + "glob": "^7.2.3", "husky": "^7.0.4", - "jest": "^27.3.1", - "lint-staged": "^12.3.7", + "jest": "^27.5.1", + "lint-staged": "^12.5.0", "mkdirp": "^1.0.4", - "moment": "^2.29.1", + "moment": "^2.29.4", "node-stream-zip": "^1.15.0", "postcss-loader": "^4.3.0", - "prettier": "npm:wp-prettier@^2.2.1-beta-1", - "regenerator-runtime": "^0.13.9", + "prettier": "npm:wp-prettier@^2.6.2", + "regenerator-runtime": "^0.13.11", "request": "^2.88.2", - "sass": "^1.49.9", - "sass-loader": "^10.2.1", + "sass": "^1.59.3", + "sass-loader": "^10.4.1", "syncpack": "^9.8.4", - "turbo": "^1.7.0", - "typescript": "^4.8.3", + "turbo": "^1.8.5", + "typescript": "^4.9.5", "url-loader": "^1.1.2", - "webpack": "^5.70.0" + "webpack": "^5.76.2" }, "dependencies": { "@babel/core": "7.12.9", - "@wordpress/babel-plugin-import-jsx-pragma": "^3.1.0", - "@wordpress/babel-preset-default": "^6.4.1", + "@wordpress/babel-plugin-import-jsx-pragma": "^3.2.0", + "@wordpress/babel-preset-default": "^6.17.0", "lodash": "^4.17.21", "wp-textdomain": "1.0.1" }, diff --git a/packages/js/admin-e2e-tests/changelog/fix-typescript-incremental-builds b/packages/js/admin-e2e-tests/changelog/fix-typescript-incremental-builds new file mode 100644 index 00000000000..f2bdc9a96ae --- /dev/null +++ b/packages/js/admin-e2e-tests/changelog/fix-typescript-incremental-builds @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: TypeScript build change + + diff --git a/packages/js/admin-e2e-tests/changelog/fix-typescript-package-isolation b/packages/js/admin-e2e-tests/changelog/fix-typescript-package-isolation new file mode 100644 index 00000000000..2d087939231 --- /dev/null +++ b/packages/js/admin-e2e-tests/changelog/fix-typescript-package-isolation @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Configuration change only + + diff --git a/packages/js/admin-e2e-tests/changelog/remove-wcpay-accordion b/packages/js/admin-e2e-tests/changelog/remove-wcpay-accordion new file mode 100644 index 00000000000..a84afb86acd --- /dev/null +++ b/packages/js/admin-e2e-tests/changelog/remove-wcpay-accordion @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update showOtherPaymentMethods() to test latest payment task properly diff --git a/packages/js/admin-e2e-tests/package.json b/packages/js/admin-e2e-tests/package.json index 514fe44b7af..39837afded8 100644 --- a/packages/js/admin-e2e-tests/package.json +++ b/packages/js/admin-e2e-tests/package.json @@ -50,18 +50,18 @@ "jest-mock-extended": "^1.0.18", "rimraf": "^3.0.2", "ts-jest": "^27.1.3", - "typescript": "^4.8.3" + "typescript": "^4.9.5" }, "publishConfig": { "access": "public" }, "scripts": { - "turbo:build": "tsc --build", + "turbo:build": "tsc --project tsconfig.json", "prepare": "composer install", "changelog": "composer exec -- changelogger", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", "lint": "eslint src", - "start": "tsc --build --watch", + "start": "tsc --project tsconfig.json --watch", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "lint:fix": "eslint src --fix", "prepack": "pnpm run clean && pnpm run build" diff --git a/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts b/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts index 5454a276cc4..2404ff42000 100644 --- a/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts +++ b/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts @@ -31,12 +31,6 @@ export class PaymentsSetup extends BasePage { } async showOtherPaymentMethods(): Promise< void > { - const selector = '.woocommerce-task-payments button.toggle-button'; - await this.page.waitForSelector( selector ); - const toggleButton = await this.page.$( - `${ selector }[aria-expanded=false]` - ); - await toggleButton?.click(); await waitForElementByText( 'h2', 'Offline payment methods' ); } diff --git a/packages/js/admin-e2e-tests/tsconfig.json b/packages/js/admin-e2e-tests/tsconfig.json index b71c7906e5a..ec5de0e407d 100644 --- a/packages/js/admin-e2e-tests/tsconfig.json +++ b/packages/js/admin-e2e-tests/tsconfig.json @@ -1,8 +1,11 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "composite": true, "rootDir": "src", - "outDir": "build" + "outDir": "build", + "typeRoots": [ + "./typings", + "./node_modules/@types" + ] } } diff --git a/packages/js/admin-layout/changelog/add-migrate-more-menu-37097 b/packages/js/admin-layout/changelog/add-migrate-more-menu-37097 new file mode 100644 index 00000000000..9b69d3ac60b --- /dev/null +++ b/packages/js/admin-layout/changelog/add-migrate-more-menu-37097 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adding support for modifying fill name to WooHeaderItem. diff --git a/packages/js/admin-layout/changelog/fix-typescript-incremental-builds b/packages/js/admin-layout/changelog/fix-typescript-incremental-builds new file mode 100644 index 00000000000..f2bdc9a96ae --- /dev/null +++ b/packages/js/admin-layout/changelog/fix-typescript-incremental-builds @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: TypeScript build change + + diff --git a/packages/js/admin-layout/changelog/fix-typescript-package-isolation b/packages/js/admin-layout/changelog/fix-typescript-package-isolation new file mode 100644 index 00000000000..2d087939231 --- /dev/null +++ b/packages/js/admin-layout/changelog/fix-typescript-package-isolation @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Configuration change only + + diff --git a/packages/js/admin-layout/changelog/update-webpack-invalid-export-error b/packages/js/admin-layout/changelog/update-webpack-invalid-export-error new file mode 100644 index 00000000000..d844dbd186f --- /dev/null +++ b/packages/js/admin-layout/changelog/update-webpack-invalid-export-error @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update webpack config to use @woocommerce/internal-style-build's parser config diff --git a/packages/js/admin-layout/package.json b/packages/js/admin-layout/package.json index bd594825b36..946ee191af5 100644 --- a/packages/js/admin-layout/package.json +++ b/packages/js/admin-layout/package.json @@ -34,9 +34,9 @@ "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", "lint": "eslint src", - "build:js": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json", "build:css": "webpack", - "start": "concurrently \"tsc --build --watch\" \"webpack --watch\"", + "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" }, @@ -50,12 +50,13 @@ "eslint": "^8.32.0", "jest": "^27.5.1", "jest-cli": "^27.5.1", + "concurrently": "^7.0.0", "postcss-loader": "^4.3.0", "react": "^17.0.2", "react-dom": "^17.0.2", "sass-loader": "^10.2.1", "ts-jest": "^27.1.3", - "typescript": "^4.8.3", + "typescript": "^4.9.5", "webpack": "^5.70.0", "webpack-cli": "^3.3.12" }, diff --git a/packages/js/admin-layout/src/plugins/woo-header-item/index.tsx b/packages/js/admin-layout/src/plugins/woo-header-item/index.tsx index 770c5d63c78..d39cd47d21b 100644 --- a/packages/js/admin-layout/src/plugins/woo-header-item/index.tsx +++ b/packages/js/admin-layout/src/plugins/woo-header-item/index.tsx @@ -11,6 +11,20 @@ import { export const WC_HEADER_SLOT_NAME = 'woocommerce_header_item'; +/** + * Get the slot fill name for the generic header slot or a specific header if provided. + * + * @param name Name of the specific header. + * @return string + */ +const getSlotFillName = ( name?: string ) => { + if ( ! name || ! name.length ) { + return WC_HEADER_SLOT_NAME; + } + + return `${ WC_HEADER_SLOT_NAME }/${ name }`; +}; + /** * Create a Fill for extensions to add items to the WooCommerce Admin header. * @@ -26,17 +40,19 @@ export const WC_HEADER_SLOT_NAME = 'woocommerce_header_item'; * scope: 'woocommerce-admin', * } ); * @param {Object} param0 + * @param {Array} param0.name - Header name. * @param {Array} param0.children - Node children. * @param {Array} param0.order - Node order. */ export const WooHeaderItem: React.FC< { + name?: string; children?: React.ReactNode; order?: number; } > & { - Slot: React.FC< Slot.Props >; -} = ( { children, order = 1 } ) => { + Slot: React.FC< Slot.Props & { name?: string } >; +} = ( { children, order = 1, name = '' } ) => { return ( - + { ( fillProps: Fill.Props ) => { return createOrderedChildren( children, order, fillProps ); } } @@ -44,8 +60,8 @@ export const WooHeaderItem: React.FC< { ); }; -WooHeaderItem.Slot = ( { fillProps } ) => ( - +WooHeaderItem.Slot = ( { fillProps, name = '' } ) => ( + { sortFillsByOrder } ); diff --git a/packages/js/admin-layout/tsconfig-cjs.json b/packages/js/admin-layout/tsconfig-cjs.json index 035d2318baf..c8b5b6a6b96 100644 --- a/packages/js/admin-layout/tsconfig-cjs.json +++ b/packages/js/admin-layout/tsconfig-cjs.json @@ -1,6 +1,10 @@ { "extends": "../tsconfig-cjs", "compilerOptions": { - "outDir": "build" + "outDir": "build", + "typeRoots": [ + "./typings", + "./node_modules/@types" + ] } } diff --git a/packages/js/admin-layout/tsconfig.json b/packages/js/admin-layout/tsconfig.json index ea9f201d401..c5f351a60cc 100644 --- a/packages/js/admin-layout/tsconfig.json +++ b/packages/js/admin-layout/tsconfig.json @@ -5,6 +5,10 @@ "outDir": "build-module", "declaration": true, "declarationMap": true, - "declarationDir": "./build-types" + "declarationDir": "./build-types", + "typeRoots": [ + "./typings", + "./node_modules/@types" + ] } } diff --git a/packages/js/admin-layout/webpack.config.js b/packages/js/admin-layout/webpack.config.js index 46a04612713..9b42746f1dd 100644 --- a/packages/js/admin-layout/webpack.config.js +++ b/packages/js/admin-layout/webpack.config.js @@ -12,6 +12,7 @@ module.exports = { path: __dirname, }, module: { + parser: webpackConfig.parser, rules: webpackConfig.rules, }, plugins: webpackConfig.plugins, diff --git a/packages/js/api/changelog/fix-typescript-incremental-builds b/packages/js/api/changelog/fix-typescript-incremental-builds new file mode 100644 index 00000000000..f2bdc9a96ae --- /dev/null +++ b/packages/js/api/changelog/fix-typescript-incremental-builds @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: TypeScript build change + + diff --git a/packages/js/api/changelog/fix-typescript-package-isolation b/packages/js/api/changelog/fix-typescript-package-isolation new file mode 100644 index 00000000000..2d087939231 --- /dev/null +++ b/packages/js/api/changelog/fix-typescript-package-isolation @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Configuration change only + + diff --git a/packages/js/api/package.json b/packages/js/api/package.json index bf6177b4b56..86a297df751 100644 --- a/packages/js/api/package.json +++ b/packages/js/api/package.json @@ -36,7 +36,7 @@ "prepare": "composer install", "changelog": "composer exec -- changelogger", "clean": "rm -rf ./dist ./tsconfig.tsbuildinfo", - "compile": "tsc -b", + "compile": "tsc --project tsconfig.json", "prepack": "pnpm run build", "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", "lint": "eslint src", @@ -50,7 +50,7 @@ "devDependencies": { "@types/create-hmac": "1.1.0", "@types/jest": "^27.4.1", - "@types/node": "13.13.5", + "@types/node": "^16.18.18", "@typescript-eslint/eslint-plugin": "^5.54.0", "@typescript-eslint/parser": "^5.54.0", "@woocommerce/eslint-plugin": "workspace:*", @@ -58,7 +58,7 @@ "eslint": "^8.32.0", "jest": "^27", "ts-jest": "^27", - "typescript": "^4.8.3" + "typescript": "^4.9.5" }, "publishConfig": { "access": "public" diff --git a/packages/js/api/tsconfig.json b/packages/js/api/tsconfig.json index 8ef7d238961..0379fdd24a9 100644 --- a/packages/js/api/tsconfig.json +++ b/packages/js/api/tsconfig.json @@ -4,7 +4,11 @@ "types": [ "node", "jest", "axios", "create-hmac" ], "rootDir": "src", "outDir": "dist", - "target": "es5" + "target": "es5", + "typeRoots": [ + "./typings", + "./node_modules/@types" + ] }, "include": [ "src/" ] } diff --git a/packages/js/components/changelog/add-toggle-content-block-37253 b/packages/js/components/changelog/add-toggle-content-block-37253 new file mode 100644 index 00000000000..38395751212 --- /dev/null +++ b/packages/js/components/changelog/add-toggle-content-block-37253 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adding simple DisplayState wrapper and modifying Collapsible component to allow rendering hidden content. diff --git a/packages/js/components/changelog/add-tree-select b/packages/js/components/changelog/add-tree-select new file mode 100644 index 00000000000..6bf7d04c3de --- /dev/null +++ b/packages/js/components/changelog/add-tree-select @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Create SelectTree component that uses TreeControl diff --git a/packages/js/components/changelog/fix-37348 b/packages/js/components/changelog/fix-37348 new file mode 100644 index 00000000000..ae0096b28fc --- /dev/null +++ b/packages/js/components/changelog/fix-37348 @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Prevent duplicate registration of core blocks in client diff --git a/packages/js/components/changelog/fix-tour-kit-documentation b/packages/js/components/changelog/fix-tour-kit-documentation new file mode 100644 index 00000000000..d2df9ede44b --- /dev/null +++ b/packages/js/components/changelog/fix-tour-kit-documentation @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update TourKit README to correct primaryButton example and formatting. diff --git a/packages/js/components/changelog/fix-typescript-incremental-builds b/packages/js/components/changelog/fix-typescript-incremental-builds new file mode 100644 index 00000000000..f2bdc9a96ae --- /dev/null +++ b/packages/js/components/changelog/fix-typescript-incremental-builds @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: TypeScript build change + + diff --git a/packages/js/components/changelog/fix-typescript-incremental-regression b/packages/js/components/changelog/fix-typescript-incremental-regression new file mode 100644 index 00000000000..2d087939231 --- /dev/null +++ b/packages/js/components/changelog/fix-typescript-incremental-regression @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Configuration change only + + diff --git a/packages/js/components/changelog/fix-typescript-package-isolation b/packages/js/components/changelog/fix-typescript-package-isolation new file mode 100644 index 00000000000..2d087939231 --- /dev/null +++ b/packages/js/components/changelog/fix-typescript-package-isolation @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Configuration change only + + diff --git a/packages/js/components/changelog/update-experimental-select-control b/packages/js/components/changelog/update-experimental-select-control new file mode 100644 index 00000000000..8b315f59b12 --- /dev/null +++ b/packages/js/components/changelog/update-experimental-select-control @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Apply wccom experimental select control changes diff --git a/packages/js/components/changelog/update-migrate-category-field b/packages/js/components/changelog/update-migrate-category-field new file mode 100644 index 00000000000..6c8e15c88a6 --- /dev/null +++ b/packages/js/components/changelog/update-migrate-category-field @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Add unit tests + + diff --git a/packages/js/components/changelog/update-webpack-invalid-export-error b/packages/js/components/changelog/update-webpack-invalid-export-error new file mode 100644 index 00000000000..d844dbd186f --- /dev/null +++ b/packages/js/components/changelog/update-webpack-invalid-export-error @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update webpack config to use @woocommerce/internal-style-build's parser config diff --git a/packages/js/components/jest.config.json b/packages/js/components/jest.config.json index 91c0faef97f..3d8108048f6 100644 --- a/packages/js/components/jest.config.json +++ b/packages/js/components/jest.config.json @@ -1,4 +1,4 @@ { "rootDir": "./src", - "preset": "../../internal-js-tests/jest.config.js" + "preset": "../node_modules/@woocommerce/internal-js-tests/jest-preset.js" } diff --git a/packages/js/components/package.json b/packages/js/components/package.json index 56fe047b1e1..136755fe61c 100644 --- a/packages/js/components/package.json +++ b/packages/js/components/package.json @@ -128,6 +128,7 @@ "@types/wordpress__viewport": "^2.5.4", "@woocommerce/eslint-plugin": "workspace:*", "@woocommerce/internal-style-build": "workspace:*", + "@woocommerce/internal-js-tests": "workspace:*", "@wordpress/browserslist-config": "wp-6.0", "@wordpress/scripts": "^12.6.1", "concurrently": "^7.0.0", @@ -141,7 +142,7 @@ "rimraf": "^3.0.2", "sass-loader": "^10.2.1", "ts-jest": "^27.1.3", - "typescript": "^4.8.3", + "typescript": "^4.9.5", "uuid": "^8.3.0", "webpack": "^5.70.0", "webpack-cli": "^3.3.12" @@ -154,12 +155,12 @@ "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 src --ext=js,ts,tsx", - "build:js": "tsc --build ./tsconfig.json ./tsconfig-cjs.json", + "build:js": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json", "build:css": "webpack", "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "lint:fix": "eslint src --ext=js,ts,tsx --fix", "prepack": "pnpm run clean && pnpm run build", - "start": "concurrently \"tsc --build ./tsconfig.json --watch\" \"webpack --watch\"", + "start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\" \"webpack --watch\"", "test:update-snapshots": "pnpm run test -- --updateSnapshot", "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" }, diff --git a/packages/js/components/src/collapsible-content/collapsible-content.tsx b/packages/js/components/src/collapsible-content/collapsible-content.tsx index 0bacc92dbe5..e839de2b450 100644 --- a/packages/js/components/src/collapsible-content/collapsible-content.tsx +++ b/packages/js/components/src/collapsible-content/collapsible-content.tsx @@ -7,10 +7,12 @@ import { Icon, chevronDown, chevronUp } from '@wordpress/icons'; /** * Internal dependencies */ +import { DisplayState } from '../display-state'; export type CollapsedProps = { initialCollapsed?: boolean; toggleText: string; + persistRender?: boolean; children: React.ReactNode; } & React.HTMLAttributes< HTMLDivElement >; @@ -18,9 +20,19 @@ export const CollapsibleContent: React.FC< CollapsedProps > = ( { initialCollapsed = true, toggleText, children, + persistRender = false, ...props }: CollapsedProps ) => { const [ collapsed, setCollapsed ] = useState( initialCollapsed ); + + const getState = () => { + if ( ! collapsed ) { + return 'visible'; + } + + return persistRender ? 'visually-hidden' : 'hidden'; + }; + return (
= ( { />
- { ! collapsed && ( +
{ children }
- ) } +
); }; diff --git a/packages/js/components/src/display-state/display-state.tsx b/packages/js/components/src/display-state/display-state.tsx new file mode 100644 index 00000000000..a6f88084c3e --- /dev/null +++ b/packages/js/components/src/display-state/display-state.tsx @@ -0,0 +1,28 @@ +/** + * External dependencies + */ +import { createElement, Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ + +export type DisplayStateProps = { + state?: 'visible' | 'visually-hidden' | 'hidden'; + children: React.ReactNode; +} & React.HTMLAttributes< HTMLDivElement >; + +export const DisplayState: React.FC< DisplayStateProps > = ( { + state = 'visible', + children, +} ) => { + if ( state === 'visible' ) { + return <>{ children }; + } + + if ( state === 'visually-hidden' ) { + return
{ children }
; + } + + return null; +}; diff --git a/packages/js/components/src/display-state/index.ts b/packages/js/components/src/display-state/index.ts new file mode 100644 index 00000000000..16d59fea698 --- /dev/null +++ b/packages/js/components/src/display-state/index.ts @@ -0,0 +1 @@ +export * from './display-state'; diff --git a/packages/js/components/src/experimental-select-control/combo-box.scss b/packages/js/components/src/experimental-select-control/combo-box.scss index 481797ab2f0..7faddd52a83 100644 --- a/packages/js/components/src/experimental-select-control/combo-box.scss +++ b/packages/js/components/src/experimental-select-control/combo-box.scss @@ -44,3 +44,18 @@ .woocommerce-experimental-select-control__suffix { align-self: stretch; } + +.woocommerce-experimental-select-control__combox-box-toggle-button { + all: unset; + position: absolute; + right: 6px; + top: 50%; + transform: translateY( -50% ); + > svg { + margin-top: 4px; + } +} + +.woocommerce-experimental-select-control:not( .is-focused ) .woocommerce-experimental-select-control__combox-box-toggle-button { + pointer-events: none; // Prevents the icon from being clickable when the combobox is not focused, because otherwise we get a race condition when clicking on the icon, because focussing the combobox opens the menu, then sequentially the icon toggles it back closed +} diff --git a/packages/js/components/src/experimental-select-control/combo-box.tsx b/packages/js/components/src/experimental-select-control/combo-box.tsx index 1589c09f13a..d4a53d32c94 100644 --- a/packages/js/components/src/experimental-select-control/combo-box.tsx +++ b/packages/js/components/src/experimental-select-control/combo-box.tsx @@ -1,26 +1,42 @@ /** * External dependencies */ -import { createElement, MouseEvent, useRef } from 'react'; +import { createElement, MouseEvent, useRef, forwardRef } from 'react'; import classNames from 'classnames'; - -/** - * Internal dependencies - */ -import { Props } from './types'; +import { Icon, chevronDown } from '@wordpress/icons'; type ComboBoxProps = { children?: JSX.Element | JSX.Element[] | null; - comboBoxProps: Props; - inputProps: Props; + comboBoxProps: JSX.IntrinsicElements[ 'div' ]; + inputProps: JSX.IntrinsicElements[ 'input' ]; + getToggleButtonProps?: () => Omit< + JSX.IntrinsicElements[ 'button' ], + 'ref' + >; suffix?: JSX.Element | null; + showToggleButton?: boolean; }; +const ToggleButton = forwardRef< HTMLButtonElement >( ( props, ref ) => { + // using forwardRef here because getToggleButtonProps injects a ref prop + return ( + + ); +} ); + export const ComboBox = ( { children, comboBoxProps, + getToggleButtonProps = () => ( {} ), inputProps, suffix, + showToggleButton, }: ComboBoxProps ) => { const inputRef = useRef< HTMLInputElement | null >( null ); @@ -60,12 +76,14 @@ export const ComboBox = ( { { - inputRef.current = node; - ( - inputProps.ref as unknown as ( - node: HTMLInputElement | null - ) => void - )( node ); + if ( typeof inputProps.ref === 'function' ) { + inputRef.current = node; + ( + inputProps.ref as unknown as ( + node: HTMLInputElement | null + ) => void + )( node ); + } } } /> @@ -75,6 +93,9 @@ export const ComboBox = ( { { suffix } ) } + { showToggleButton && ( + + ) } ); }; diff --git a/packages/js/components/src/experimental-select-control/menu-item.tsx b/packages/js/components/src/experimental-select-control/menu-item.tsx index 2200ceba2ef..ef3dbbd465b 100644 --- a/packages/js/components/src/experimental-select-control/menu-item.tsx +++ b/packages/js/components/src/experimental-select-control/menu-item.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { createElement, ReactElement } from 'react'; +import { createElement, CSSProperties, ReactElement } from 'react'; /** * Internal dependencies @@ -14,6 +14,7 @@ export type MenuItemProps< ItemType > = { item: ItemType; children: ReactElement | string; getItemProps: getItemPropsType< ItemType >; + activeStyle?: CSSProperties; }; export const MenuItem = < ItemType, >( { @@ -21,11 +22,12 @@ export const MenuItem = < ItemType, >( { getItemProps, index, isActive, + activeStyle = { backgroundColor: '#bde4ff' }, item, }: MenuItemProps< ItemType > ) => { return (
  • diff --git a/packages/js/components/src/experimental-select-control/menu.tsx b/packages/js/components/src/experimental-select-control/menu.tsx index 13680d9349d..691b038af68 100644 --- a/packages/js/components/src/experimental-select-control/menu.tsx +++ b/packages/js/components/src/experimental-select-control/menu.tsx @@ -22,6 +22,8 @@ type MenuProps = { getMenuProps: getMenuPropsType; isOpen: boolean; className?: string; + position?: Popover.Position; + scrollIntoViewOnOpen?: boolean; }; export const Menu = ( { @@ -29,6 +31,8 @@ export const Menu = ( { getMenuProps, isOpen, className, + position = 'bottom right', + scrollIntoViewOnOpen = false, }: MenuProps ) => { const [ boundingRect, setBoundingRect ] = useState< DOMRect >(); const selectControlMenuRef = useRef< HTMLDivElement >( null ); @@ -41,6 +45,13 @@ export const Menu = ( { } }, [ selectControlMenuRef.current ] ); + // Scroll the selected item into view when the menu opens. + useEffect( () => { + if ( isOpen && scrollIntoViewOnOpen ) { + selectControlMenuRef.current?.scrollIntoView(); + } + }, [ isOpen, scrollIntoViewOnOpen ] ); + /* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ /* Disabled because of the onmouseup on the ul element below. */ return ( @@ -60,7 +71,7 @@ export const Menu = ( { 'has-results': Children.count( children ) > 0, } ) } - position="bottom right" + position={ position } animate={ false } >
      = { disabled?: boolean; inputProps?: GetInputPropsOptions; suffix?: JSX.Element | null; + showToggleButton?: boolean; /** * This is a feature already implemented in downshift@7.0.0 through the * reducer. In order for us to use it this prop is added temporarily until @@ -123,6 +124,7 @@ function SelectControl< ItemType = DefaultItemType >( { disabled, inputProps = {}, suffix = , + showToggleButton = false, __experimentalOpenMenuOnFocus = false, }: SelectControlProps< ItemType > ) { const [ isFocused, setIsFocused ] = useState( false ); @@ -154,12 +156,13 @@ function SelectControl< ItemType = DefaultItemType >( { } setInputValue( getItemLabel( singleSelectedItem ) ); - }, [ singleSelectedItem ] ); + }, [ getItemLabel, multiple, singleSelectedItem ] ); const { isOpen, getLabelProps, getMenuProps, + getToggleButtonProps, getInputProps, getComboboxProps, highlightedIndex, @@ -256,6 +259,7 @@ function SelectControl< ItemType = DefaultItemType >( { { /* eslint-enable jsx-a11y/label-has-for */ } ( { ...inputProps, } ) } suffix={ suffix } + showToggleButton={ showToggleButton } > <> { children( { diff --git a/packages/js/components/src/experimental-select-control/stories/index.tsx b/packages/js/components/src/experimental-select-control/stories/index.tsx index 80902ed691c..cb1706d98d3 100644 --- a/packages/js/components/src/experimental-select-control/stories/index.tsx +++ b/packages/js/components/src/experimental-select-control/stories/index.tsx @@ -573,6 +573,24 @@ export const CustomSuffix: React.FC = () => { ); }; +export const ToggleButton: React.FC = () => { + const [ selected, setSelected ] = + useState< SelectedType< DefaultItemType > >(); + + return ( + item && setSelected( item ) } + onRemove={ () => setSelected( null ) } + suffix={ null } + showToggleButton={ true } + __experimentalOpenMenuOnFocus={ true } + /> + ); +}; + export default { title: 'WooCommerce Admin/experimental/SelectControl', component: SelectControl, diff --git a/packages/js/components/src/experimental-select-control/types.ts b/packages/js/components/src/experimental-select-control/types.ts index 7d1a85f8cee..331b2d148b8 100644 --- a/packages/js/components/src/experimental-select-control/types.ts +++ b/packages/js/components/src/experimental-select-control/types.ts @@ -16,7 +16,8 @@ export type DefaultItemType = { export type SelectedType< ItemType > = ItemType | null; export type Props = { - [ key: string ]: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [ key: string ]: any; }; export type getItemPropsType< ItemType > = ( diff --git a/packages/js/components/src/experimental-select-tree-control/index.ts b/packages/js/components/src/experimental-select-tree-control/index.ts new file mode 100644 index 00000000000..9ea352bc18b --- /dev/null +++ b/packages/js/components/src/experimental-select-tree-control/index.ts @@ -0,0 +1 @@ +export * from './select-tree'; diff --git a/packages/js/components/src/experimental-select-tree-control/select-tree.scss b/packages/js/components/src/experimental-select-tree-control/select-tree.scss new file mode 100644 index 00000000000..509dadf5d6c --- /dev/null +++ b/packages/js/components/src/experimental-select-tree-control/select-tree.scss @@ -0,0 +1,15 @@ +.woocommerce-experimental-select-control__combo-box-wrapper { + &:focus { + box-shadow: 0 0 0 1px var( --wp-admin-theme-color ); + border-color: var( --wp-admin-theme-color ); + } +} + +.woocommerce-experimental-select-tree-control__dropdown { + display: block; +} + +// That's the only way I could remove the padding from the @wordpress/components Dropdown. +.woocommerce-experimental-select-tree-control__dropdown-content .components-popover__content { + padding: 0; +} diff --git a/packages/js/components/src/experimental-select-tree-control/select-tree.tsx b/packages/js/components/src/experimental-select-tree-control/select-tree.tsx new file mode 100644 index 00000000000..7406ea9d0f7 --- /dev/null +++ b/packages/js/components/src/experimental-select-tree-control/select-tree.tsx @@ -0,0 +1,199 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * External dependencies + */ +import { createElement, useRef, useState } from 'react'; +import classNames from 'classnames'; +import { search } from '@wordpress/icons'; +import { Dropdown, Spinner } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { useLinkedTree } from '../experimental-tree-control/hooks/use-linked-tree'; +import { Tree } from '../experimental-tree-control/tree'; +import { + Item, + LinkedTree, + TreeControlProps, +} from '../experimental-tree-control/types'; +import { SelectedItems } from '../experimental-select-control/selected-items'; +import { ComboBox } from '../experimental-select-control/combo-box'; +import { SuffixIcon } from '../experimental-select-control/suffix-icon'; + +interface SelectTreeProps extends TreeControlProps { + id: string; + selected?: Item[]; + getSelectedItemProps?: any; + treeRef?: React.ForwardedRef< HTMLOListElement >; + suffix?: JSX.Element | null; + isLoading?: boolean; + label: string | JSX.Element; + onInputChange?: ( value: string | undefined ) => void; +} + +export const SelectTree = function SelectTree( { + items, + getSelectedItemProps, + treeRef: ref, + suffix = , + placeholder, + isLoading, + onInputChange, + shouldShowCreateButton, + ...props +}: SelectTreeProps ) { + const linkedTree = useLinkedTree( items ); + + const [ isFocused, setIsFocused ] = useState( false ); + + const comboBoxRef = useRef< HTMLDivElement >( null ); + + // getting the parent's parent div width to set the width of the dropdown + const comboBoxWidth = + comboBoxRef.current?.parentElement?.parentElement?.getBoundingClientRect() + .width; + + const shouldItemBeExpanded = ( item: LinkedTree ): boolean => { + if ( ! props.createValue || ! item.children?.length ) return false; + return item.children.some( ( child ) => { + if ( + new RegExp( props.createValue || '', 'ig' ).test( + child.data.label + ) + ) { + return true; + } + return shouldItemBeExpanded( child ); + } ); + }; + + return ( + + isLoading ? ( +
      + +
      + ) : ( + + ) + } + renderToggle={ ( { isOpen, onToggle, onClose } ) => ( +
      + + { + if ( ! isOpen ) { + onToggle(); + } + setIsFocused( true ); + }, + onBlur: ( event ) => { + // if blurring to an element inside the dropdown, don't close it + if ( + isOpen && + ! document + .querySelector( + '.woocommerce-experimental-select-control ~ .components-popover' + ) + ?.contains( event.relatedTarget ) + ) { + onClose(); + } + setIsFocused( false ); + }, + onKeyDown: ( event ) => { + const baseQuery = + '.woocommerce-experimental-select-tree-control__dropdown > .components-popover'; + if ( event.key === 'ArrowDown' ) { + event.preventDefault(); + // focus on the first element from the Popover + ( + document.querySelector( + `${ baseQuery } input, ${ baseQuery } button` + ) as + | HTMLInputElement + | HTMLButtonElement + )?.focus(); + } + if ( event.key === 'Tab' ) { + onClose(); + } + }, + onChange: ( event ) => + onInputChange && + onInputChange( event.target.value ), + placeholder, + } } + suffix={ suffix } + > + item?.label || '' } + getItemValue={ ( item ) => item?.value || '' } + onRemove={ ( item ) => { + if ( + ! Array.isArray( item ) && + props.onRemove + ) { + props.onRemove( item ); + onClose(); + } + } } + getSelectedItemProps={ () => ( {} ) } + /> + +
      + ) } + /> + ); +}; diff --git a/packages/js/components/src/experimental-select-tree-control/stories/index.tsx b/packages/js/components/src/experimental-select-tree-control/stories/index.tsx new file mode 100644 index 00000000000..4034c571905 --- /dev/null +++ b/packages/js/components/src/experimental-select-tree-control/stories/index.tsx @@ -0,0 +1,101 @@ +/** + * External dependencies + */ + +import React, { createElement } from 'react'; + +/** + * Internal dependencies + */ +import { SelectTree } from '../select-tree'; +import { Item } from '../../experimental-tree-control/types'; + +const listItems: Item[] = [ + { value: '1', label: 'Technology' }, + { value: '1.1', label: 'Notebooks', parent: '1' }, + { value: '1.2', label: 'Phones', parent: '1' }, + { value: '1.2.1', label: 'iPhone', parent: '1.2' }, + { value: '1.2.1.1', label: 'iPhone 14 Pro', parent: '1.2.1' }, + { value: '1.2.1.2', label: 'iPhone 14 Pro Max', parent: '1.2.1' }, + { value: '1.2.2', label: 'Samsung', parent: '1.2' }, + { value: '1.2.2.1', label: 'Samsung Galaxy 22 Plus', parent: '1.2.2' }, + { value: '1.2.2.2', label: 'Samsung Galaxy 22 Ultra', parent: '1.2.2' }, + { value: '1.3', label: 'Wearables', parent: '1' }, + { value: '2', label: 'Hardware' }, + { value: '2.1', label: 'CPU', parent: '2' }, + { value: '2.2', label: 'GPU', parent: '2' }, + { value: '2.3', label: 'Memory RAM', parent: '2' }, + { value: '3', label: 'Other' }, +]; + +const filterItems = ( items: Item[], searchValue ) => { + const filteredItems = items.filter( ( e ) => + e.label.includes( searchValue ) + ); + const itemsToIterate = [ ...filteredItems ]; + while ( itemsToIterate.length > 0 ) { + // The filter should include the parents of the filtered items + const element = itemsToIterate.pop(); + if ( element ) { + const parent = listItems.find( + ( item ) => item.value === element.parent + ); + if ( parent && ! filteredItems.includes( parent ) ) { + filteredItems.push( parent ); + itemsToIterate.push( parent ); + } + } + } + return filteredItems; +}; + +export const MultipleSelectTree: React.FC = () => { + const [ value, setValue ] = React.useState( '' ); + const [ selected, setSelected ] = React.useState< Item[] >( [] ); + + const items = filterItems( listItems, value ); + + return ( + + ! value || + listItems.findIndex( ( item ) => item.label === typedValue ) === + -1 + } + createValue={ value } + // eslint-disable-next-line no-alert + onCreateNew={ () => alert( 'create new called' ) } + onInputChange={ ( a ) => setValue( a || '' ) } + onSelect={ ( selectedItems ) => { + if ( Array.isArray( selectedItems ) ) { + setSelected( [ ...selected, ...selectedItems ] ); + } + } } + onRemove={ ( removedItems ) => { + const newValues = Array.isArray( removedItems ) + ? selected.filter( + ( item ) => + ! removedItems.some( + ( { value: removedValue } ) => + item.value === removedValue + ) + ) + : selected.filter( + ( item ) => item.value !== removedItems.value + ); + setSelected( newValues ); + } } + /> + ); +}; + +export default { + title: 'WooCommerce Admin/experimental/SelectTreeControl', + component: SelectTree, +}; diff --git a/packages/js/components/src/experimental-select-tree-control/test/select-tree.test.tsx b/packages/js/components/src/experimental-select-tree-control/test/select-tree.test.tsx new file mode 100644 index 00000000000..efb87059c6c --- /dev/null +++ b/packages/js/components/src/experimental-select-tree-control/test/select-tree.test.tsx @@ -0,0 +1,107 @@ +import { render } from '@testing-library/react'; +import React, { createElement } from '@wordpress/element'; +import { SelectTree } from '../select-tree'; +import { Item } from '../../experimental-tree-control'; + +const mockItems: Item[] = [ + { + label: 'Item 1', + value: 'item-1', + }, + { + label: 'Item 2', + value: 'item-2', + parent: 'item-1', + }, + { + label: 'Item 3', + value: 'item-3', + }, +]; + +const DEFAULT_PROPS = { + id: 'select-tree', + items: mockItems, + label: 'Select Tree', + placeholder: 'Type here', +}; + +describe( 'SelectTree', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should show the popover only when focused', () => { + const { queryByPlaceholderText, queryByText } = render( + + ); + expect( queryByText( 'Item 1' ) ).not.toBeInTheDocument(); + queryByPlaceholderText( 'Type here' )?.focus(); + expect( queryByText( 'Item 1' ) ).toBeInTheDocument(); + } ); + + it( 'should show create button when callback is true ', () => { + const { queryByText, queryByPlaceholderText } = render( + true } + /> + ); + queryByPlaceholderText( 'Type here' )?.focus(); + expect( queryByText( 'Create new' ) ).toBeInTheDocument(); + } ); + it( 'should not show create button when callback is false or no callback', () => { + const { queryByText, queryByPlaceholderText } = render( + + ); + queryByPlaceholderText( 'Type here' )?.focus(); + expect( queryByText( 'Create new' ) ).not.toBeInTheDocument(); + } ); + it( 'should show a root item when focused and child when expand button is clicked', () => { + const { queryByText, queryByLabelText, queryByPlaceholderText } = + render( ); + queryByPlaceholderText( 'Type here' )?.focus(); + expect( queryByText( 'Item 1' ) ).toBeInTheDocument(); + + expect( queryByText( 'Item 2' ) ).not.toBeInTheDocument(); + queryByLabelText( 'Expand' )?.click(); + expect( queryByText( 'Item 2' ) ).toBeInTheDocument(); + } ); + + it( 'should show selected items', () => { + const { queryAllByRole, queryByPlaceholderText } = render( + + ); + queryByPlaceholderText( 'Type here' )?.focus(); + expect( queryAllByRole( 'treeitem' )[ 0 ] ).toHaveAttribute( + 'aria-selected', + 'true' + ); + } ); + + it( 'should show Create "" button', () => { + const { queryByPlaceholderText, queryByText } = render( + true } + /> + ); + queryByPlaceholderText( 'Type here' )?.focus(); + expect( queryByText( 'Create "new item"' ) ).toBeInTheDocument(); + } ); + it( 'should call onCreateNew when Create "" button is clicked', () => { + const mockFn = jest.fn(); + const { queryByPlaceholderText, queryByText } = render( + true } + onCreateNew={ mockFn } + /> + ); + queryByPlaceholderText( 'Type here' )?.focus(); + queryByText( 'Create "new item"' )?.click(); + expect( mockFn ).toBeCalledTimes( 1 ); + } ); +} ); 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 index e7cf63433eb..02a9e5e54d3 100644 --- a/packages/js/components/src/experimental-tree-control/hooks/use-expander.ts +++ b/packages/js/components/src/experimental-tree-control/hooks/use-expander.ts @@ -17,7 +17,8 @@ export function useExpander( { useEffect( () => { if ( item.children?.length && - typeof shouldItemBeExpanded === 'function' + typeof shouldItemBeExpanded === 'function' && + ! isExpanded ) { setExpanded( shouldItemBeExpanded( item ) ); } diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-keyboard.ts b/packages/js/components/src/experimental-tree-control/hooks/use-keyboard.ts index e769975d85b..54db7555200 100644 --- a/packages/js/components/src/experimental-tree-control/hooks/use-keyboard.ts +++ b/packages/js/components/src/experimental-tree-control/hooks/use-keyboard.ts @@ -110,12 +110,14 @@ export function useKeyboard( { onExpand, onCollapse, onToggleExpand, + onLastItemLoop, }: { item: LinkedTree; isExpanded: boolean; onExpand(): void; onCollapse(): void; onToggleExpand(): void; + onLastItemLoop?( event: React.KeyboardEvent< HTMLDivElement > ): void; } ) { function onKeyDown( event: React.KeyboardEvent< HTMLDivElement > ) { if ( event.code === 'ArrowRight' ) { @@ -154,6 +156,9 @@ export function useKeyboard( { event.code ); element?.focus(); + if ( event.code === 'ArrowDown' && ! element && onLastItemLoop ) { + onLastItemLoop( event ); + } } if ( event.code === 'Home' ) { @@ -169,5 +174,5 @@ export function useKeyboard( { } } - return { onKeyDown }; + return { onKeyDown, onLastItemLoop }; } diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-selection.ts b/packages/js/components/src/experimental-tree-control/hooks/use-selection.ts index 255799940cc..79e47bfc9ec 100644 --- a/packages/js/components/src/experimental-tree-control/hooks/use-selection.ts +++ b/packages/js/components/src/experimental-tree-control/hooks/use-selection.ts @@ -75,6 +75,7 @@ function hasSelectedSibblingChildren( export function useSelection( { item, multiple, + shouldNotRecursivelySelect, selected, level, index, @@ -89,6 +90,7 @@ export function useSelection( { | 'index' | 'onSelect' | 'onRemove' + | 'shouldNotRecursivelySelect' > ) { const selectedItems = useMemo( () => { if ( level === 1 && index === 0 ) { @@ -100,7 +102,11 @@ export function useSelection( { const checkedStatus: CheckedStatus = useMemo( () => { if ( item.data.value in selectedItems ) { - if ( multiple && isIndeterminate( selectedItems, item.children ) ) { + if ( + multiple && + ! shouldNotRecursivelySelect && + isIndeterminate( selectedItems, item.children ) + ) { return 'indeterminate'; } return 'checked'; @@ -113,7 +119,7 @@ export function useSelection( { if ( multiple ) { value = [ item.data ]; - if ( item.children.length ) { + if ( item.children.length && ! shouldNotRecursivelySelect ) { value.push( ...getDeepChildren( item ) ); } } else if ( item.children?.length ) { @@ -132,7 +138,7 @@ export function useSelection( { function onSelectChildren( value: Item | Item[] ) { if ( typeof onSelect !== 'function' ) return; - if ( multiple ) { + if ( multiple && ! shouldNotRecursivelySelect ) { value = [ item.data, ...( value as Item[] ) ]; } @@ -142,7 +148,11 @@ export function useSelection( { function onRemoveChildren( value: Item | Item[] ) { if ( typeof onRemove !== 'function' ) return; - if ( multiple && item.children?.length ) { + if ( + multiple && + item.children?.length && + ! shouldNotRecursivelySelect + ) { const hasSelectedSibbling = hasSelectedSibblingChildren( item.children, value as Item[], 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 70f2a6d55db..35a08d19042 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 @@ -17,6 +17,7 @@ export function useTreeItem( { item, level, multiple, + shouldNotRecursivelySelect, selected, index, getLabel, @@ -24,6 +25,11 @@ export function useTreeItem( { shouldItemBeHighlighted, onSelect, onRemove, + isExpanded, + onCreateNew, + shouldShowCreateButton, + onLastItemLoop, + onTreeBlur, ...props }: TreeItemProps ) { const nextLevel = level + 1; @@ -41,6 +47,7 @@ export function useTreeItem( { index, onSelect, onRemove, + shouldNotRecursivelySelect, } ); const highlighter = useHighlighter( { @@ -56,6 +63,7 @@ export function useTreeItem( { const { onKeyDown } = useKeyboard( { ...expander, + onLastItemLoop, item, } ); @@ -96,6 +104,7 @@ export function useTreeItem( { getItemLabel: getLabel, shouldItemBeExpanded, shouldItemBeHighlighted, + shouldNotRecursivelySelect, onSelect: selection.onSelectChildren, onRemove: selection.onRemoveChildren, }, 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 abba8a76ccd..656473e9456 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 @@ -8,7 +8,6 @@ import { TreeProps } from '../types'; export function useTree( { - ref, items, level = 1, role = 'tree', @@ -19,6 +18,11 @@ export function useTree( { shouldItemBeHighlighted, onSelect, onRemove, + shouldNotRecursivelySelect, + createValue, + onTreeBlur, + onCreateNew, + shouldShowCreateButton, ...props }: TreeProps ) { return { @@ -35,6 +39,7 @@ export function useTree( { getLabel: getItemLabel, shouldItemBeExpanded, shouldItemBeHighlighted, + shouldNotRecursivelySelect, onSelect, onRemove, }, diff --git a/packages/js/components/src/experimental-tree-control/tree.scss b/packages/js/components/src/experimental-tree-control/tree.scss index 221208658d5..f33dfe422a2 100644 --- a/packages/js/components/src/experimental-tree-control/tree.scss +++ b/packages/js/components/src/experimental-tree-control/tree.scss @@ -12,4 +12,13 @@ border: 1px solid $gray-400; border-radius: 2px; } + &__button { + width: 100%; + &:hover, + &:focus-within { + outline: 1.5px solid var( --wp-admin-theme-color ); + outline-offset: -1.5px; + background-color: $gray-100; + } + } } diff --git a/packages/js/components/src/experimental-tree-control/tree.tsx b/packages/js/components/src/experimental-tree-control/tree.tsx index 64a975481ca..491c6c161bd 100644 --- a/packages/js/components/src/experimental-tree-control/tree.tsx +++ b/packages/js/components/src/experimental-tree-control/tree.tsx @@ -1,8 +1,12 @@ /** * External dependencies */ +import { Button, Icon } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; import classNames from 'classnames'; -import { createElement, forwardRef } from 'react'; +import { createElement, forwardRef, Fragment, useRef } from 'react'; +import { plus } from '@wordpress/icons'; +import { useMergeRefs } from '@wordpress/compose'; /** * Internal dependencies @@ -13,31 +17,93 @@ import { TreeProps } from './types'; export const Tree = forwardRef( function ForwardedTree( props: TreeProps, - ref: React.ForwardedRef< HTMLOListElement > + forwardedRef: React.ForwardedRef< HTMLOListElement > ) { + const rootListRef = useRef< HTMLOListElement >( null ); + const ref = useMergeRefs( [ rootListRef, forwardedRef ] ); + const { level, items, treeProps, treeItemProps } = useTree( { ...props, ref, } ); - if ( ! items.length ) return null; + const isCreateButtonVisible = + props.shouldShowCreateButton && + props.shouldShowCreateButton( props.createValue ); + return ( -
        +
          + { items.map( ( child, index ) => ( + { + ( + rootListRef.current + ?.closest( 'ol[role="tree"]' ) + ?.parentElement?.querySelector( + '.experimental-woocommerce-tree__button' + ) as HTMLButtonElement + )?.focus(); + } } + /> + ) ) } +
        + { isCreateButtonVisible && ( + ) } - > - { items.map( ( child, index ) => ( - - ) ) } -
      + ); } ); diff --git a/packages/js/components/src/experimental-tree-control/types.ts b/packages/js/components/src/experimental-tree-control/types.ts index f97cb4ed58b..8c60fd36194 100644 --- a/packages/js/components/src/experimental-tree-control/types.ts +++ b/packages/js/components/src/experimental-tree-control/types.ts @@ -14,7 +14,7 @@ export type CheckedStatus = 'checked' | 'unchecked' | 'indeterminate'; type BaseTreeProps = { /** - * It contians one item if `multiple` value is false or + * It contains one item if `multiple` value is false or * a list of items if it is true. */ selected?: Item | Item[]; @@ -22,12 +22,30 @@ type BaseTreeProps = { * Whether the tree items are single or multiple selected. */ multiple?: boolean; + /** + * In `multiple` mode, when this flag is also set, selecting children does + * not select their parents and selecting parents does not select their children. + */ + shouldNotRecursivelySelect?: boolean; + /** + * The value to be used for comparison to determine if 'create new' button should be shown. + */ + createValue?: string; + /** + * Called when the 'create new' button is clicked. + */ + onCreateNew?: () => void; + /** + * If passed, shows create button if return from callback is true + */ + shouldShowCreateButton?( value?: string ): boolean; + isExpanded?: boolean; /** * When `multiple` is true and a child item is selected, all its * ancestors and its descendants are also selected. If it's false * only the clicked item is selected. * - * @param value The selection + * @param value The selection */ onSelect?( value: Item | Item[] ): void; /** @@ -36,7 +54,7 @@ type BaseTreeProps = { * are also unselected. If it's false only the clicked item is * unselected. * - * @param value The unselection + * @param value The unselection */ onRemove?( value: Item | Item[] ): void; /** @@ -48,12 +66,16 @@ type BaseTreeProps = { * shouldItemBeHighlighted={ isFirstChild } * /> * - * @param item The current linked tree item, useful to - * traverse the entire linked tree from this item. + * @param item The current linked tree item, useful to + * traverse the entire linked tree from this item. * * @see {@link LinkedTree} */ shouldItemBeHighlighted?( item: LinkedTree ): boolean; + /** + * Called when the create button is clicked to help closing any related popover. + */ + onTreeBlur?(): void; }; export type TreeProps = BaseTreeProps & @@ -66,7 +88,8 @@ export type TreeProps = BaseTreeProps & > & { level?: number; items: LinkedTree[]; - /** It gives a way to render a different Element as the + /** + * It gives a way to render a different Element as the * tree item label. * * @example @@ -74,7 +97,7 @@ export type TreeProps = BaseTreeProps & * getItemLabel={ ( item ) => ${ item.data.label } } * /> * - * @param item The current rendering tree item + * @param item The current rendering tree item * * @see {@link LinkedTree} */ @@ -107,8 +130,10 @@ export type TreeItemProps = BaseTreeProps & level: number; item: LinkedTree; index: number; + isFocused?: boolean; getLabel?( item: LinkedTree ): JSX.Element; shouldItemBeExpanded?( item: LinkedTree ): boolean; + onLastItemLoop?( event: React.KeyboardEvent< HTMLDivElement > ): void; }; export type TreeControlProps = Omit< TreeProps, 'items' | 'level' > & { diff --git a/packages/js/components/src/form/form-context.ts b/packages/js/components/src/form/form-context.ts index 21c46f36e79..39c41eb8647 100644 --- a/packages/js/components/src/form/form-context.ts +++ b/packages/js/components/src/form/form-context.ts @@ -18,7 +18,7 @@ export type FormErrors< Values > = { }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type FormContext< Values extends Record< string, any > > = { +export type FormContextType< Values extends Record< string, any > > = { values: Values; errors: FormErrors< Values >; isDirty: boolean; @@ -51,14 +51,16 @@ export type FormContext< Values extends Record< string, any > > = { }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const FormContext = createContext< FormContext< any > >( +export const FormContext: React.Context< FormContextType< any > > = // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as FormContext< any > -); + createContext< FormContextType< any > >( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {} as FormContextType< any > + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any export function useFormContext< Values extends Record< string, any > >() { - const formContext = useContext< FormContext< Values > >( FormContext ); + const formContext = useContext< FormContextType< Values > >( FormContext ); return formContext; } diff --git a/packages/js/components/src/form/test/index.tsx b/packages/js/components/src/form/test/index.tsx index daaae785b59..06f7bddd4ad 100644 --- a/packages/js/components/src/form/test/index.tsx +++ b/packages/js/components/src/form/test/index.tsx @@ -10,7 +10,7 @@ import { TextControl } from '@wordpress/components'; * Internal dependencies */ import { Form, useFormContext } from '../'; -import type { FormContext } from '../'; +import type { FormContextType } from '../'; import { DateTimePickerControl } from '../../date-time-picker-control'; const TestInputWithContext = () => { @@ -44,7 +44,7 @@ describe( 'Form', () => { > { ( { handleSubmit, - }: FormContext< Record< string, string > > ) => { + }: FormContextType< Record< string, string > > ) => { return ; } } @@ -71,7 +71,7 @@ describe( 'Form', () => { onChange={ mockOnChange } validate={ () => ( {} ) } > - { ( { setValue }: FormContext< Record< string, string > > ) => { + { ( { setValue }: FormContextType< Record< string, string > > ) => { return ( ; } } @@ -124,7 +124,7 @@ describe( 'Form', () => { const { queryByText } = render(
      ( {} ) }> - { ( { setValue }: FormContext< Record< string, string > > ) => { + { ( { setValue }: FormContextType< Record< string, string > > ) => { return (
      + ); + } } + + ); +} diff --git a/packages/js/product-editor/src/components/tabs/constants.ts b/packages/js/product-editor/src/components/tabs/constants.ts new file mode 100644 index 00000000000..0b2fc863fb7 --- /dev/null +++ b/packages/js/product-editor/src/components/tabs/constants.ts @@ -0,0 +1 @@ +export const TABS_SLOT_NAME = 'woocommerce_product_tabs'; diff --git a/packages/js/product-editor/src/components/tabs/index.ts b/packages/js/product-editor/src/components/tabs/index.ts new file mode 100644 index 00000000000..c2d1b4e91b2 --- /dev/null +++ b/packages/js/product-editor/src/components/tabs/index.ts @@ -0,0 +1 @@ +export * from './tabs'; diff --git a/packages/js/product-editor/src/components/tabs/style.scss b/packages/js/product-editor/src/components/tabs/style.scss new file mode 100644 index 00000000000..c12769d0562 --- /dev/null +++ b/packages/js/product-editor/src/components/tabs/style.scss @@ -0,0 +1,17 @@ +.woocommerce-product-tabs { + display: flex; + justify-content: center; + + .components-button { + padding: $gap-smaller 0 20px 0; + margin-left: $gap; + margin-right: $gap; + border-bottom: 3.5px solid transparent; + border-radius: 0; + height: auto; + + &.is-selected { + border-color: var(--wp-admin-theme-color); + } + } +} \ No newline at end of file diff --git a/packages/js/product-editor/src/components/tabs/tabs.tsx b/packages/js/product-editor/src/components/tabs/tabs.tsx new file mode 100644 index 00000000000..3f2643f0b21 --- /dev/null +++ b/packages/js/product-editor/src/components/tabs/tabs.tsx @@ -0,0 +1,96 @@ +/** + * External dependencies + */ +import { + createElement, + Fragment, + useEffect, + useState, +} from '@wordpress/element'; +import { ReactElement } from 'react'; +import { NavigableMenu, Slot } from '@wordpress/components'; +// 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 { navigateTo, getNewPath, getQuery } from '@woocommerce/navigation'; + +/** + * Internal dependencies + */ +import { TABS_SLOT_NAME } from './constants'; + +type TabsProps = { + onChange?: ( tabId: string | null ) => void; +}; + +export type TabsFillProps = { + onClick: ( tabId: string ) => void; +}; + +export function Tabs( { onChange = () => {} }: TabsProps ) { + const [ selected, setSelected ] = useState< string | null >( null ); + const query = getQuery() as Record< string, string >; + + function onClick( tabId: string ) { + window.document.documentElement.scrollTop = 0; + navigateTo( { + url: getNewPath( { tab: tabId } ), + } ); + } + + useEffect( () => { + onChange( selected ); + }, [ selected ] ); + + useEffect( () => { + if ( query.tab ) { + setSelected( query.tab ); + } + }, [ query.tab ] ); + + function maybeSetSelected( fills: readonly ( readonly ReactElement[] )[] ) { + if ( selected ) { + return; + } + + for ( let i = 0; i < fills.length; i++ ) { + if ( fills[ i ][ 0 ].props.disabled ) { + continue; + } + // Remove the `.$` prefix on keys. E.g., .$key => key + const tabId = fills[ i ][ 0 ].key?.toString().slice( 2 ) || null; + setSelected( tabId ); + return; + } + } + + function selectTabOnNavigate( + _childIndex: number, + child: HTMLButtonElement + ) { + child.click(); + } + + return ( + + + { ( fills ) => { + maybeSetSelected( fills ); + return <>{ fills }; + } } + + + ); +} diff --git a/packages/js/product-editor/src/components/tabs/test/tabs.spec.tsx b/packages/js/product-editor/src/components/tabs/test/tabs.spec.tsx new file mode 100644 index 00000000000..615f50cc46a --- /dev/null +++ b/packages/js/product-editor/src/components/tabs/test/tabs.spec.tsx @@ -0,0 +1,160 @@ +/** + * External dependencies + */ +import { render, fireEvent } from '@testing-library/react'; +import { getQuery, navigateTo } from '@woocommerce/navigation'; +import React, { createElement } from 'react'; +import { SlotFillProvider } from '@wordpress/components'; +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { Tabs } from '../'; +import { Edit as Tab } from '../../tab/edit'; + +jest.mock( '@wordpress/block-editor', () => ( { + ...jest.requireActual( '@wordpress/block-editor' ), + useBlockProps: jest.fn(), +} ) ); + +jest.mock( '@woocommerce/navigation', () => ( { + ...jest.requireActual( '@woocommerce/navigation' ), + navigateTo: jest.fn(), + getQuery: jest.fn().mockReturnValue( {} ), +} ) ); + +function MockTabs( { onChange = jest.fn() } ) { + const [ selected, setSelected ] = useState< string | null >( null ); + const mockContext = { + selectedTab: selected, + }; + + return ( + + { + setSelected( tabId ); + onChange( tabId ); + } } + /> + + + + + ); +} + +describe( 'Tabs', () => { + beforeEach( () => { + ( getQuery as jest.Mock ).mockReturnValue( { + tab: null, + } ); + } ); + + it( 'should render tab buttons added to the slot', () => { + const { queryByText } = render( ); + expect( queryByText( 'Test button 1' ) ).toBeInTheDocument(); + expect( queryByText( 'Test button 2' ) ).toBeInTheDocument(); + } ); + + it( 'should set the first tab as active initially', () => { + const { queryByText } = render( ); + expect( queryByText( 'Test button 1' ) ).toHaveAttribute( + 'aria-selected', + 'true' + ); + expect( queryByText( 'Test button 2' ) ).toHaveAttribute( + 'aria-selected', + 'false' + ); + } ); + + it( 'should navigate to a new URL when a tab is clicked', () => { + const { getByText } = render( ); + const button = getByText( 'Test button 2' ); + fireEvent.click( button ); + + expect( navigateTo ).toHaveBeenLastCalledWith( { + url: 'admin.php?page=wc-admin&tab=test2', + } ); + } ); + + it( 'should select the tab provided in the URL initially', () => { + ( getQuery as jest.Mock ).mockReturnValue( { + tab: 'test2', + } ); + + const { getByText } = render( ); + + expect( getByText( 'Test button 2' ) ).toHaveAttribute( + 'aria-selected', + 'true' + ); + } ); + + it( 'should select the tab provided on URL change', () => { + const { getByText, rerender } = render( ); + + ( getQuery as jest.Mock ).mockReturnValue( { + tab: 'test3', + } ); + + rerender( ); + + expect( getByText( 'Test button 3' ) ).toHaveAttribute( + 'aria-selected', + 'true' + ); + } ); + + it( 'should call the onChange props when changing', async () => { + const mockOnChange = jest.fn(); + const { rerender } = render( ); + + expect( mockOnChange ).toHaveBeenCalledWith( 'test1' ); + + ( getQuery as jest.Mock ).mockReturnValue( { + tab: 'test2', + } ); + + rerender( ); + + expect( mockOnChange ).toHaveBeenCalledWith( 'test2' ); + } ); + + it( 'should add a class to the initially selected tab panel', async () => { + const { getByRole } = render( ); + const panel1 = getByRole( 'tabpanel', { name: 'Test button 1' } ); + const panel2 = getByRole( 'tabpanel', { name: 'Test button 2' } ); + + expect( panel1.classList ).toContain( 'is-selected' ); + expect( panel2.classList ).not.toContain( 'is-selected' ); + } ); + + it( 'should add a class to the newly selected tab panel', async () => { + const { getByText, getByRole, rerender } = render( ); + const button = getByText( 'Test button 2' ); + fireEvent.click( button ); + const panel1 = getByRole( 'tabpanel', { name: 'Test button 1' } ); + const panel2 = getByRole( 'tabpanel', { name: 'Test button 2' } ); + + ( getQuery as jest.Mock ).mockReturnValue( { + tab: 'test2', + } ); + + rerender( ); + + expect( panel1.classList ).not.toContain( 'is-selected' ); + expect( panel2.classList ).toContain( 'is-selected' ); + } ); +} ); diff --git a/packages/js/product-editor/src/constants.ts b/packages/js/product-editor/src/constants.ts index 15d3b3a636c..3afadee6a1a 100644 --- a/packages/js/product-editor/src/constants.ts +++ b/packages/js/product-editor/src/constants.ts @@ -1,3 +1,8 @@ +export const PRODUCT_MVP_CES_ACTION_OPTION_NAME = + 'woocommerce_ces_product_mvp_ces_action'; +export const NEW_PRODUCT_MANAGEMENT_ENABLED_OPTION_NAME = + 'woocommerce_new_product_management_enabled'; + export const NUMBERS_AND_ALLOWED_CHARS = '[^-0-9%s1%s2]'; export const NUMBERS_AND_DECIMAL_SEPARATOR = '[^-\\d\\%s]+'; export const ONLY_ONE_DECIMAL_SEPARATOR = '[%s](?=%s*[%s])'; @@ -42,3 +47,5 @@ export const VARIANT_SHIPPING_SECTION_BASIC_ID = `variant/${ SHIPPING_SECTION_BA export const VARIANT_SHIPPING_SECTION_DIMENSIONS_ID = `variant/${ SHIPPING_SECTION_DIMENSIONS_ID }`; export const PRODUCT_DETAILS_SLUG = 'product-details'; + +export const PRODUCT_SCHEDULED_SALE_SLUG = 'product-scheduled-sale'; diff --git a/packages/js/product-editor/src/hooks/index.ts b/packages/js/product-editor/src/hooks/index.ts index 700bcf73864..e1ac9e9b7fc 100644 --- a/packages/js/product-editor/src/hooks/index.ts +++ b/packages/js/product-editor/src/hooks/index.ts @@ -1,2 +1,4 @@ export { useProductHelper as __experimentalUseProductHelper } from './use-product-helper'; +export { useProductMVPCESFooter as __experimentalUseProductMVPCESFooter } from './use-product-mvp-ces-footer'; export { useVariationsOrder as __experimentalUseVariationsOrder } from './use-variations-order'; +export { useCurrencyInputProps as __experimentalUseCurrencyInputProps } from './use-currency-input-props'; diff --git a/packages/js/product-editor/src/hooks/use-currency-input-props.ts b/packages/js/product-editor/src/hooks/use-currency-input-props.ts new file mode 100644 index 00000000000..da6b53d573a --- /dev/null +++ b/packages/js/product-editor/src/hooks/use-currency-input-props.ts @@ -0,0 +1,77 @@ +/** + * External dependencies + */ +import { CurrencyContext } from '@woocommerce/currency'; +import { useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { useProductHelper } from './use-product-helper'; + +export type CurrencyInputProps = { + prefix: string; + className: string; + sanitize: ( value: string | number ) => string; + onFocus: ( event: React.FocusEvent< HTMLInputElement > ) => void; + onKeyUp: ( event: React.KeyboardEvent< HTMLInputElement > ) => void; +}; + +type Props = { + value: string; + setValue: ( value: string ) => void; + onFocus?: ( event: React.FocusEvent< HTMLInputElement > ) => void; + onKeyUp?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void; +}; + +export const useCurrencyInputProps = ( { + value, + setValue, + onFocus, + onKeyUp, +}: Props ) => { + const { sanitizePrice } = useProductHelper(); + + const context = useContext( CurrencyContext ); + const { getCurrencyConfig } = context; + const currencyConfig = getCurrencyConfig(); + + const currencyInputProps: CurrencyInputProps = { + prefix: currencyConfig.symbol, + className: 'half-width-field components-currency-control', + sanitize: ( val: string | number ) => { + return sanitizePrice( String( val ) ); + }, + onFocus( event: React.FocusEvent< HTMLInputElement > ) { + // In some browsers like safari .select() function inside + // the onFocus event doesn't work as expected because it + // conflicts with onClick the first time user click the + // input. Using setTimeout defers the text selection and + // avoid the unexpected behaviour. + setTimeout( + function deferSelection( element: HTMLInputElement ) { + element.select(); + }, + 0, + event.currentTarget + ); + if ( onFocus ) { + onFocus( event ); + } + }, + onKeyUp( event: React.KeyboardEvent< HTMLInputElement > ) { + const amount = Number.parseFloat( sanitizePrice( value || '0' ) ); + const step = Number( event.currentTarget.step || '1' ); + if ( event.code === 'ArrowUp' ) { + setValue( String( amount + step ) ); + } + if ( event.code === 'ArrowDown' ) { + setValue( String( amount - step ) ); + } + if ( onKeyUp ) { + onKeyUp( event ); + } + }, + }; + return currencyInputProps; +}; diff --git a/packages/js/product-editor/src/hooks/use-product-mvp-ces-footer/index.ts b/packages/js/product-editor/src/hooks/use-product-mvp-ces-footer/index.ts new file mode 100644 index 00000000000..0fceddb4edf --- /dev/null +++ b/packages/js/product-editor/src/hooks/use-product-mvp-ces-footer/index.ts @@ -0,0 +1 @@ +export * from './use-product-mvp-ces-footer'; diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/use-product-mvp-ces-footer.ts b/packages/js/product-editor/src/hooks/use-product-mvp-ces-footer/use-product-mvp-ces-footer.ts similarity index 92% rename from plugins/woocommerce-admin/client/customer-effort-score-tracks/use-product-mvp-ces-footer.ts rename to packages/js/product-editor/src/hooks/use-product-mvp-ces-footer/use-product-mvp-ces-footer.ts index d8fa132b826..c7960499763 100644 --- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/use-product-mvp-ces-footer.ts +++ b/packages/js/product-editor/src/hooks/use-product-mvp-ces-footer/use-product-mvp-ces-footer.ts @@ -7,7 +7,7 @@ import { OPTIONS_STORE_NAME } from '@woocommerce/data'; /** * Internal dependencies */ -import { PRODUCT_MVP_CES_ACTION_OPTION_NAME } from './product-mvp-ces-footer'; +import { PRODUCT_MVP_CES_ACTION_OPTION_NAME } from '../../constants'; async function isProductMVPCESHidden(): Promise< boolean > { const productCESAction: string = await resolveSelect( diff --git a/packages/js/product-editor/src/hooks/use-validation/README.md b/packages/js/product-editor/src/hooks/use-validation/README.md new file mode 100644 index 00000000000..2c8b83977db --- /dev/null +++ b/packages/js/product-editor/src/hooks/use-validation/README.md @@ -0,0 +1,41 @@ +# useValidation + +This custom hook uses the helper functions `const { lockPostSaving, unlockPostSaving } = useDispatch( 'core/editor' );` to lock/unlock the current editing product before saving it. + +## Usage + +Syncronous validation + +```typescript +import { useCallback } from '@wordpress/element'; +import { useValidation } from '@woocommerce/product-editor'; + +const product = ...; + +const validateTitle = useCallback( (): boolean => { + if ( product.title.length < 2 ) { + return false; + } + return true; +}, [ product.title ] ); + +const isTitleValid = useValidation( 'product/title', validateTitle ); +``` + +Asyncronous validation + +```typescript +import { useCallback } from '@wordpress/element'; +import { useValidation } from '@woocommerce/product-editor'; + +const product = ...; + +const validateSlug = useCallback( async (): Promise< boolean > => { + return fetch( `.../validate-slug?slug=${ product.slug }` ) + .then( ( response ) => response.json() ) + .then( ( { isValid } ) => isValid ) + .catch( () => false ); +}, [ product.slug ] ); + +const isSlugValid = useValidation( 'product/slug', validateSlug ); +``` diff --git a/packages/js/product-editor/src/hooks/use-validation/index.ts b/packages/js/product-editor/src/hooks/use-validation/index.ts new file mode 100644 index 00000000000..a6a64cadb31 --- /dev/null +++ b/packages/js/product-editor/src/hooks/use-validation/index.ts @@ -0,0 +1 @@ +export * from './use-validation'; diff --git a/packages/js/product-editor/src/hooks/use-validation/test/use-validation.test.ts b/packages/js/product-editor/src/hooks/use-validation/test/use-validation.test.ts new file mode 100644 index 00000000000..1ea6903fea7 --- /dev/null +++ b/packages/js/product-editor/src/hooks/use-validation/test/use-validation.test.ts @@ -0,0 +1,95 @@ +/** + * External dependencies + */ +import { renderHook } from '@testing-library/react-hooks'; +import { useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { useValidation } from '../use-validation'; + +jest.mock( '@wordpress/data', () => ( { + useDispatch: jest.fn(), +} ) ); + +describe( 'useValidation', () => { + const useDispatchMock = useDispatch as jest.Mock; + const lockPostSaving = jest.fn(); + const unlockPostSaving = jest.fn(); + + beforeEach( () => { + useDispatchMock.mockReturnValue( { + lockPostSaving, + unlockPostSaving, + } ); + } ); + + afterEach( () => { + jest.clearAllMocks(); + } ); + + describe( 'sync', () => { + it( 'should lock the editor if validate returns false', async () => { + const { result, waitForNextUpdate } = renderHook( () => + useValidation( 'product/name', () => false ) + ); + + await waitForNextUpdate(); + + expect( result.current ).toBeFalsy(); + expect( lockPostSaving ).toHaveBeenCalled(); + expect( unlockPostSaving ).not.toHaveBeenCalled(); + } ); + + it( 'should unlock the editor if validate returns true', async () => { + const { result, waitForNextUpdate } = renderHook( () => + useValidation( 'product/name', () => true ) + ); + + await waitForNextUpdate(); + + expect( result.current ).toBeTruthy(); + expect( lockPostSaving ).not.toHaveBeenCalled(); + expect( unlockPostSaving ).toHaveBeenCalled(); + } ); + } ); + + describe( 'async', () => { + it( 'should lock the editor if validate resolves false', async () => { + const { result, waitForNextUpdate } = renderHook( () => + useValidation( 'product/name', () => Promise.resolve( false ) ) + ); + + await waitForNextUpdate(); + + expect( result.current ).toBeFalsy(); + expect( lockPostSaving ).toHaveBeenCalled(); + expect( unlockPostSaving ).not.toHaveBeenCalled(); + } ); + + it( 'should lock the editor if validate rejects', async () => { + const { result, waitForNextUpdate } = renderHook( () => + useValidation( 'product/name', () => Promise.reject() ) + ); + + await waitForNextUpdate(); + + expect( result.current ).toBeFalsy(); + expect( lockPostSaving ).toHaveBeenCalled(); + expect( unlockPostSaving ).not.toHaveBeenCalled(); + } ); + + it( 'should unlock the editor if validate resolves true', async () => { + const { result, waitForNextUpdate } = renderHook( () => + useValidation( 'product/name', () => Promise.resolve( true ) ) + ); + + await waitForNextUpdate(); + + expect( result.current ).toBeTruthy(); + expect( lockPostSaving ).not.toHaveBeenCalled(); + expect( unlockPostSaving ).toHaveBeenCalled(); + } ); + } ); +} ); diff --git a/packages/js/product-editor/src/hooks/use-validation/use-validation.ts b/packages/js/product-editor/src/hooks/use-validation/use-validation.ts new file mode 100644 index 00000000000..d0956431848 --- /dev/null +++ b/packages/js/product-editor/src/hooks/use-validation/use-validation.ts @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { useEffect, useState } from '@wordpress/element'; + +/** + * Signals that product saving is locked. + * + * @param lockName The namespace used to lock the product saving if validation fails. + * @param validate The validator function. + */ +export function useValidation( + lockName: string, + validate: () => boolean | Promise< boolean > +): boolean | undefined { + const [ isValid, setIsValid ] = useState< boolean | undefined >(); + const { lockPostSaving, unlockPostSaving } = useDispatch( 'core/editor' ); + + useEffect( () => { + let validationResponse = validate(); + + if ( typeof validationResponse === 'boolean' ) { + validationResponse = Promise.resolve( validationResponse ); + } + + validationResponse + .then( ( isValidationValid ) => { + if ( isValidationValid ) { + unlockPostSaving( lockName ); + } else { + lockPostSaving( lockName ); + } + setIsValid( isValidationValid ); + } ) + .catch( () => { + lockPostSaving( lockName ); + setIsValid( false ); + } ); + }, [ lockName, validate, lockPostSaving, unlockPostSaving ] ); + + return isValid; +} diff --git a/packages/js/product-editor/src/index.ts b/packages/js/product-editor/src/index.ts index bbccc12b921..c62b84726c4 100644 --- a/packages/js/product-editor/src/index.ts +++ b/packages/js/product-editor/src/index.ts @@ -1,5 +1,9 @@ export * from './components'; -export { DETAILS_SECTION_ID, TAB_GENERAL_ID } from './constants'; +export { + DETAILS_SECTION_ID, + NEW_PRODUCT_MANAGEMENT_ENABLED_OPTION_NAME, + TAB_GENERAL_ID, +} from './constants'; /** * Utils diff --git a/packages/js/product-editor/src/style.scss b/packages/js/product-editor/src/style.scss index 9d74dd658c9..7e2107e3fdb 100644 --- a/packages/js/product-editor/src/style.scss +++ b/packages/js/product-editor/src/style.scss @@ -1,7 +1,16 @@ +@import 'blocks/schedule-sale/editor.scss'; @import 'components/editor/style.scss'; @import 'components/product-section-layout/style.scss'; @import 'components/edit-product-link-modal/style.scss'; @import 'components/details-categories-field/style.scss'; @import 'components/details-categories-field/create-category-modal.scss'; @import 'components/header/style.scss'; +@import 'components/images/editor.scss'; @import 'components/block-editor/style.scss'; +@import 'components/radio/editor.scss'; +@import 'components/section/editor.scss'; +@import 'components/tab/editor.scss'; +@import 'components/tabs/style.scss'; +@import 'components/details-summary-block/style.scss'; +@import 'components/product-mvp-ces-footer/style.scss'; +@import 'components/product-mvp-feedback-modal/style.scss'; diff --git a/packages/js/product-editor/src/utils/constants.ts b/packages/js/product-editor/src/utils/constants.ts index 0aa72fd516a..6c0b603d436 100644 --- a/packages/js/product-editor/src/utils/constants.ts +++ b/packages/js/product-editor/src/utils/constants.ts @@ -7,3 +7,4 @@ export const ADD_NEW_SHIPPING_CLASS_OPTION_VALUE = export const UNCATEGORIZED_CATEGORY_SLUG = 'uncategorized'; export const PRODUCT_VARIATION_TITLE_LIMIT = 32; export const STANDARD_RATE_TAX_CLASS_SLUG = 'standard'; +export const AUTO_DRAFT_NAME = 'AUTO-DRAFT'; diff --git a/packages/js/product-editor/src/utils/create-ordered-children.tsx b/packages/js/product-editor/src/utils/create-ordered-children.tsx index a3341f31b3f..1eb0b49ecf7 100644 --- a/packages/js/product-editor/src/utils/create-ordered-children.tsx +++ b/packages/js/product-editor/src/utils/create-ordered-children.tsx @@ -64,7 +64,7 @@ function createOrderedChildren< T = Fill.Props, S = Record< string, unknown > >( order: number, props: T, injectProps?: S -) { +): React.ReactElement { const { children: childrenToRender, props: propsToRender } = getChildrenAndProps( children, order, props, injectProps ); return cloneElement( childrenToRender, propsToRender ); diff --git a/packages/js/product-editor/src/utils/get-header-title.ts b/packages/js/product-editor/src/utils/get-header-title.ts new file mode 100644 index 00000000000..9ebfd701855 --- /dev/null +++ b/packages/js/product-editor/src/utils/get-header-title.ts @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { AUTO_DRAFT_NAME } from './constants'; + +/** + * Get the header title using the product name. + * + * @param editedProductName Name value entered for the product. + * @param initialProductName Name already persisted to the database. + * @return The new title + */ +export const getHeaderTitle = ( + editedProductName: string, + initialProductName: string +): string => { + const isProductNameNotEmpty = Boolean( editedProductName ); + const isProductNameDirty = editedProductName !== initialProductName; + const isCreating = initialProductName === AUTO_DRAFT_NAME; + + if ( isProductNameNotEmpty && isProductNameDirty ) { + return editedProductName; + } + + if ( isCreating ) { + return __( 'Add new product', 'woocommerce' ); + } + + return initialProductName; +}; diff --git a/packages/js/product-editor/src/utils/get-product-stock-status.ts b/packages/js/product-editor/src/utils/get-product-stock-status.ts index 88ba69a46eb..9cc8f5419f3 100644 --- a/packages/js/product-editor/src/utils/get-product-stock-status.ts +++ b/packages/js/product-editor/src/utils/get-product-stock-status.ts @@ -51,7 +51,9 @@ export const getProductStockStatus = ( } if ( product.stock_status ) { - return PRODUCT_STOCK_STATUS_LABELS[ product.stock_status ]; + return PRODUCT_STOCK_STATUS_LABELS[ + product.stock_status as PRODUCT_STOCK_STATUS_KEYS + ]; } return PRODUCT_STOCK_STATUS_LABELS.instock; @@ -77,6 +79,8 @@ export const getProductStockStatusClass = ( return PRODUCT_STOCK_STATUS_CLASSES.outofstock; } return product.stock_status - ? PRODUCT_STOCK_STATUS_CLASSES[ product.stock_status ] + ? PRODUCT_STOCK_STATUS_CLASSES[ + product.stock_status as PRODUCT_STOCK_STATUS_KEYS + ] : ''; }; diff --git a/packages/js/product-editor/src/utils/get-product-title.ts b/packages/js/product-editor/src/utils/get-product-title.ts index 2f92e9d81b9..dc090527ca0 100644 --- a/packages/js/product-editor/src/utils/get-product-title.ts +++ b/packages/js/product-editor/src/utils/get-product-title.ts @@ -3,7 +3,10 @@ */ import { __ } from '@wordpress/i18n'; -export const AUTO_DRAFT_NAME = 'AUTO-DRAFT'; +/** + * Internal dependencies + */ +import { AUTO_DRAFT_NAME } from './constants'; /** * Get the product title for use in the header. diff --git a/packages/js/product-editor/src/utils/index.ts b/packages/js/product-editor/src/utils/index.ts index 1e15f6859f9..67a0014effc 100644 --- a/packages/js/product-editor/src/utils/index.ts +++ b/packages/js/product-editor/src/utils/index.ts @@ -1,16 +1,18 @@ /** * Internal dependencies */ +import { AUTO_DRAFT_NAME } from './constants'; import { formatCurrencyDisplayValue } from './format-currency-display-value'; import { getCheckboxTracks } from './get-checkbox-tracks'; import { getCurrencySymbolProps } from './get-currency-symbol-props'; import { getDerivedProductType } from './get-derived-product-type'; +import { getHeaderTitle } from './get-header-title'; import { getProductStatus, PRODUCT_STATUS_LABELS } from './get-product-status'; import { getProductStockStatus, getProductStockStatusClass, } from './get-product-stock-status'; -import { getProductTitle, AUTO_DRAFT_NAME } from './get-product-title'; +import { getProductTitle } from './get-product-title'; import { getProductVariationTitle, getTruncatedProductVariationTitle, @@ -19,6 +21,7 @@ import { preventLeavingProductForm } from './prevent-leaving-product-form'; export * from './create-ordered-children'; export * from './sort-fills-by-order'; +export * from './init-blocks'; export { AUTO_DRAFT_NAME, @@ -26,6 +29,7 @@ export { getCheckboxTracks, getCurrencySymbolProps, getDerivedProductType, + getHeaderTitle, getProductStatus, getProductStockStatus, getProductStockStatusClass, diff --git a/packages/js/product-editor/src/utils/init-block.ts b/packages/js/product-editor/src/utils/init-block.ts new file mode 100644 index 00000000000..3e290a911eb --- /dev/null +++ b/packages/js/product-editor/src/utils/init-block.ts @@ -0,0 +1,39 @@ +/** + * External dependencies + */ +import { + BlockConfiguration, + BlockEditProps, + registerBlockType, +} from '@wordpress/blocks'; +import { ComponentType } from 'react'; + +type BlockRepresentation = { + name: string; + metadata: BlockConfiguration; + settings: Partial< Omit< BlockConfiguration, 'edit' > > & { + readonly edit?: + | ComponentType< + BlockEditProps< object > & { + context?: Record< string, unknown >; + } + > + | undefined; + }; +}; + +/** + * Function to register an individual block. + * + * @param {Object} block The block to be registered. + * + * @return {?WPBlockType} The block, if it has been successfully registered; + * otherwise `undefined`. + */ +export default function initBlock( block: BlockRepresentation ) { + if ( ! block ) { + return; + } + const { metadata, settings, name } = block; + return registerBlockType( { name, ...metadata }, settings ); +} diff --git a/packages/js/product-editor/src/utils/init-blocks.ts b/packages/js/product-editor/src/utils/init-blocks.ts new file mode 100644 index 00000000000..06404008120 --- /dev/null +++ b/packages/js/product-editor/src/utils/init-blocks.ts @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import { + Block, + BlockConfiguration, + registerBlockType, +} from '@wordpress/blocks'; + +interface BlockRepresentation< T extends Record< string, object > > { + name?: string; + metadata: BlockConfiguration< T >; + settings: Partial< BlockConfiguration< T > >; +} + +/** + * Function to register an individual block. + * + * @param block The block to be registered. + * @return The block, if it has been successfully registered; otherwise `undefined`. + */ +export function initBlock< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends Record< string, any > = Record< string, any > +>( block: BlockRepresentation< T > ): Block< T > | undefined { + if ( ! block ) { + return; + } + const { metadata, settings, name } = block; + return registerBlockType< T >( { name, ...metadata }, settings ); +} diff --git a/packages/js/product-editor/src/utils/sanitize-html.ts b/packages/js/product-editor/src/utils/sanitize-html.ts new file mode 100644 index 00000000000..fb47b6530a3 --- /dev/null +++ b/packages/js/product-editor/src/utils/sanitize-html.ts @@ -0,0 +1,13 @@ +/** + * External dependencies + */ +import { sanitize } from 'dompurify'; + +const ALLOWED_TAGS = [ 'a', 'b', 'em', 'i', 'strong', 'p', 'br' ]; +const ALLOWED_ATTR = [ 'target', 'href', 'rel', 'name', 'download' ]; + +export function sanitizeHTML( html: string ) { + return { + __html: sanitize( html, { ALLOWED_TAGS, ALLOWED_ATTR } ), + }; +} diff --git a/packages/js/product-editor/tsconfig-cjs.json b/packages/js/product-editor/tsconfig-cjs.json index 035d2318baf..92bfe004f21 100644 --- a/packages/js/product-editor/tsconfig-cjs.json +++ b/packages/js/product-editor/tsconfig-cjs.json @@ -1,6 +1,16 @@ { "extends": "../tsconfig-cjs", + "include": [ + "**/*.d.ts", + "src/**/*", + "src/**/*.json" + ], "compilerOptions": { - "outDir": "build" + "outDir": "build", + "resolveJsonModule": true, + "typeRoots": [ + "./typings", + "./node_modules/@types" + ] } } diff --git a/packages/js/product-editor/tsconfig.json b/packages/js/product-editor/tsconfig.json index ea9f201d401..86bd897a9f3 100644 --- a/packages/js/product-editor/tsconfig.json +++ b/packages/js/product-editor/tsconfig.json @@ -5,6 +5,16 @@ "outDir": "build-module", "declaration": true, "declarationMap": true, - "declarationDir": "./build-types" - } + "declarationDir": "./build-types", + "resolveJsonModule": true, + "typeRoots": [ + "./typings", + "./node_modules/@types" + ] + }, + "include": [ + "**/*.d.ts", + "src/**/*", + "src/**/*.json" + ] } diff --git a/packages/js/product-editor/typings/global.d.ts b/packages/js/product-editor/typings/global.d.ts index 69abbdeff5e..3a27ae17ad8 100644 --- a/packages/js/product-editor/typings/global.d.ts +++ b/packages/js/product-editor/typings/global.d.ts @@ -6,4 +6,3 @@ declare global { /*~ If your module exports nothing, you'll need this line. Otherwise, delete it */ export {}; - diff --git a/packages/js/product-editor/typings/index.d.ts b/packages/js/product-editor/typings/index.d.ts new file mode 100644 index 00000000000..fa38181628c --- /dev/null +++ b/packages/js/product-editor/typings/index.d.ts @@ -0,0 +1,18 @@ +declare module '@woocommerce/settings' { + export declare function getAdminLink( path: string ): string; + export declare function getSetting< T >( + name: string, + fallback?: unknown, + filter = ( val: unknown, fb: unknown ) => + typeof val !== 'undefined' ? val : fb + ): T; +} + +declare module '@wordpress/core-data' { + function useEntityProp< T = unknown >( + kind: string, + name: string, + prop: string, + id?: string + ): [ T, ( value: T ) => void, T ]; +} diff --git a/packages/js/product-editor/webpack.config.js b/packages/js/product-editor/webpack.config.js index 46a04612713..ae2cba9f015 100644 --- a/packages/js/product-editor/webpack.config.js +++ b/packages/js/product-editor/webpack.config.js @@ -1,18 +1,73 @@ +/** + * External dependencies + */ +const CopyWebpackPlugin = require( 'copy-webpack-plugin' ); +const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); +const path = require( 'path' ); +const RemoveEmptyScriptsPlugin = require( 'webpack-remove-empty-scripts' ); +const WebpackRTLPlugin = require( 'webpack-rtl-plugin' ); + /** * Internal dependencies */ const { webpackConfig } = require( '@woocommerce/internal-style-build' ); +const { + blockEntryPoints, + getBlockMetaData, + getEntryPointName, +} = require( './config/block-entry-points' ); + +const NODE_ENV = process.env.NODE_ENV || 'development'; module.exports = { mode: process.env.NODE_ENV || 'development', entry: { 'build-style': __dirname + '/src/style.scss', + ...blockEntryPoints, }, output: { path: __dirname, }, module: { + parser: webpackConfig.parser, rules: webpackConfig.rules, }, - plugins: webpackConfig.plugins, + plugins: [ + new RemoveEmptyScriptsPlugin(), + new MiniCssExtractPlugin( { + filename: ( data ) => { + return data.chunk.name.startsWith( '/build/blocks' ) + ? `[name].css` + : `[name]/style.css`; + }, + chunkFilename: 'chunks/[id].style.css', + } ), + new WebpackRTLPlugin( { + test: /(? - + diff --git a/plugins/woocommerce-admin/client/activity-panel/activity-panel.js b/plugins/woocommerce-admin/client/activity-panel/activity-panel.js index 308cf600904..d37363e0c7d 100644 --- a/plugins/woocommerce-admin/client/activity-panel/activity-panel.js +++ b/plugins/woocommerce-admin/client/activity-panel/activity-panel.js @@ -12,6 +12,7 @@ import { import { useSelect, useDispatch } from '@wordpress/data'; import { uniqueId, find } from 'lodash'; import { Icon, help as helpIcon, external } from '@wordpress/icons'; +import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score'; import { H, Section } from '@woocommerce/components'; import { ONBOARDING_STORE_NAME, @@ -48,7 +49,6 @@ import { useActiveSetupTasklist } from '~/tasks'; import { LayoutContext } from '~/layout'; import { getSegmentsFromPath } from '~/utils/url-helpers'; import { FeedbackIcon } from '~/products/images/feedback-icon'; -import { STORE_KEY as CES_STORE_KEY } from '~/customer-effort-score-tracks/data/constants'; import { ProductFeedbackTour } from '~/guided-tours/add-product-feedback-tour'; const HelpPanel = lazy( () => diff --git a/plugins/woocommerce-admin/client/analytics/components/report-filters/index.js b/plugins/woocommerce-admin/client/analytics/components/report-filters/index.js index 3c4a5b691ef..c2313f1b4a6 100644 --- a/plugins/woocommerce-admin/client/analytics/components/report-filters/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/report-filters/index.js @@ -6,6 +6,7 @@ import { compose } from '@wordpress/compose'; import PropTypes from 'prop-types'; import { omitBy, isUndefined, snakeCase } from 'lodash'; import { withSelect, withDispatch } from '@wordpress/data'; +import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score'; import { ReportFilters as Filters } from '@woocommerce/components'; import { SETTINGS_STORE_NAME } from '@woocommerce/data'; import { @@ -19,7 +20,6 @@ import { CurrencyContext } from '@woocommerce/currency'; /** * Internal dependencies */ -import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants'; import { LOCALE } from '~/utils/admin-settings'; class ReportFilters extends Component { diff --git a/plugins/woocommerce-admin/client/analytics/components/report-table/index.js b/plugins/woocommerce-admin/client/analytics/components/report-table/index.js index 9a40653b9ad..22b5dbe4803 100644 --- a/plugins/woocommerce-admin/client/analytics/components/report-table/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/report-table/index.js @@ -10,6 +10,7 @@ import { withDispatch, withSelect } from '@wordpress/data'; import { get, noop, partial, uniq } from 'lodash'; import { __, sprintf } from '@wordpress/i18n'; import PropTypes from 'prop-types'; +import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score'; import { CompareButton, Search, TableCard } from '@woocommerce/components'; import { getIdsFromQuery, @@ -39,7 +40,6 @@ import { recordEvent } from '@woocommerce/tracks'; import DownloadIcon from './download-icon'; import ReportError from '../report-error'; import { extendTableData } from './utils'; -import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants'; import './style.scss'; const TABLE_FILTER = 'woocommerce_admin_report_table'; diff --git a/plugins/woocommerce-admin/client/analytics/report/categories/config.js b/plugins/woocommerce-admin/client/analytics/report/categories/config.js index 7db61624192..500a29907a8 100644 --- a/plugins/woocommerce-admin/client/analytics/report/categories/config.js +++ b/plugins/woocommerce-admin/client/analytics/report/categories/config.js @@ -4,12 +4,12 @@ import { __, _x } from '@wordpress/i18n'; import { applyFilters } from '@wordpress/hooks'; import { dispatch } from '@wordpress/data'; +import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score'; /** * Internal dependencies */ import { getCategoryLabels } from '../../../lib/async-requests'; -import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants'; const CATEGORY_REPORT_CHARTS_FILTER = 'woocommerce_admin_categories_report_charts'; diff --git a/plugins/woocommerce-admin/client/analytics/report/coupons/config.js b/plugins/woocommerce-admin/client/analytics/report/coupons/config.js index 4220e13f019..5a579020ae8 100644 --- a/plugins/woocommerce-admin/client/analytics/report/coupons/config.js +++ b/plugins/woocommerce-admin/client/analytics/report/coupons/config.js @@ -4,12 +4,12 @@ import { __, _x } from '@wordpress/i18n'; import { applyFilters } from '@wordpress/hooks'; import { dispatch } from '@wordpress/data'; +import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score'; /** * Internal dependencies */ import { getCouponLabels } from '../../../lib/async-requests'; -import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants'; const COUPON_REPORT_CHARTS_FILTER = 'woocommerce_admin_coupons_report_charts'; const COUPON_REPORT_FILTERS_FILTER = 'woocommerce_admin_coupons_report_filters'; diff --git a/plugins/woocommerce-admin/client/analytics/report/products/config.js b/plugins/woocommerce-admin/client/analytics/report/products/config.js index 1097ff28b96..320789d47a1 100644 --- a/plugins/woocommerce-admin/client/analytics/report/products/config.js +++ b/plugins/woocommerce-admin/client/analytics/report/products/config.js @@ -4,6 +4,7 @@ import { __, _x } from '@wordpress/i18n'; import { applyFilters } from '@wordpress/hooks'; import { dispatch } from '@wordpress/data'; +import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score'; /** * Internal dependencies @@ -12,7 +13,6 @@ import { getProductLabels, getVariationLabels, } from '../../../lib/async-requests'; -import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants'; const PRODUCTS_REPORT_CHARTS_FILTER = 'woocommerce_admin_products_report_charts'; diff --git a/plugins/woocommerce-admin/client/analytics/report/taxes/config.js b/plugins/woocommerce-admin/client/analytics/report/taxes/config.js index 49278680021..df83105cda1 100644 --- a/plugins/woocommerce-admin/client/analytics/report/taxes/config.js +++ b/plugins/woocommerce-admin/client/analytics/report/taxes/config.js @@ -3,6 +3,7 @@ */ import { __, _x } from '@wordpress/i18n'; import { applyFilters } from '@wordpress/hooks'; +import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score'; import { NAMESPACE } from '@woocommerce/data'; import { dispatch } from '@wordpress/data'; @@ -11,7 +12,6 @@ import { dispatch } from '@wordpress/data'; */ import { getRequestByIdString } from '../../../lib/async-requests'; import { getTaxCode } from './utils'; -import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants'; const TAXES_REPORT_CHARTS_FILTER = 'woocommerce_admin_taxes_report_charts'; const TAXES_REPORT_FILTERS_FILTER = 'woocommerce_admin_taxes_report_filters'; diff --git a/plugins/woocommerce-admin/client/analytics/report/variations/config.js b/plugins/woocommerce-admin/client/analytics/report/variations/config.js index f53dee1e713..2aaa20dbc80 100644 --- a/plugins/woocommerce-admin/client/analytics/report/variations/config.js +++ b/plugins/woocommerce-admin/client/analytics/report/variations/config.js @@ -4,6 +4,7 @@ import { __, _x } from '@wordpress/i18n'; import { applyFilters } from '@wordpress/hooks'; import { dispatch } from '@wordpress/data'; +import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score'; /** * Internal dependencies @@ -13,7 +14,6 @@ import { getProductLabels, getVariationLabels, } from '../../../lib/async-requests'; -import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants'; const VARIATIONS_REPORT_CHARTS_FILTER = 'woocommerce_admin_variations_report_charts'; diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/index.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/index.js deleted file mode 100644 index bd9b84cbbe4..00000000000 --- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { default as CustomerEffortScoreTracks } from './customer-effort-score-tracks'; -export { default as CustomerEffortScoreTracksContainer } from './customer-effort-score-tracks-container'; -export * from './customer-effort-score-modal-container.tsx'; diff --git a/plugins/woocommerce-admin/client/embedded-body-layout/embedded-body-layout.tsx b/plugins/woocommerce-admin/client/embedded-body-layout/embedded-body-layout.tsx index 98e7aa7c676..996490e3927 100644 --- a/plugins/woocommerce-admin/client/embedded-body-layout/embedded-body-layout.tsx +++ b/plugins/woocommerce-admin/client/embedded-body-layout/embedded-body-layout.tsx @@ -3,6 +3,7 @@ */ import { applyFilters } from '@wordpress/hooks'; import { useEffect } from '@wordpress/element'; +import { triggerExitPageCesSurvey } from '@woocommerce/customer-effort-score'; import QueryString, { parse } from 'qs'; /** @@ -13,7 +14,6 @@ import { ShippingRecommendations } from '../shipping'; import { EmbeddedBodyProps } from './embedded-body-props'; import { StoreAddressTour } from '../guided-tours/store-address-tour'; import './style.scss'; -import { triggerExitPageCesSurvey } from '~/customer-effort-score-tracks/customer-effort-score-exit-page'; type QueryParams = EmbeddedBodyProps; diff --git a/plugins/woocommerce-admin/client/embedded-body-layout/test/embedded-body-layout.test.tsx b/plugins/woocommerce-admin/client/embedded-body-layout/test/embedded-body-layout.test.tsx index 954d90492e1..f5b0a2bbc2d 100644 --- a/plugins/woocommerce-admin/client/embedded-body-layout/test/embedded-body-layout.test.tsx +++ b/plugins/woocommerce-admin/client/embedded-body-layout/test/embedded-body-layout.test.tsx @@ -9,12 +9,9 @@ import { addFilter } from '@wordpress/hooks'; */ import { EmbeddedBodyLayout } from '../embedded-body-layout'; -jest.mock( - '~/customer-effort-score-tracks/customer-effort-score-exit-page', - () => ( { - triggerExitPageCesSurvey: jest.fn(), - } ) -); +jest.mock( '@woocommerce/customer-effort-score', () => ( { + triggerExitPageCesSurvey: jest.fn(), +} ) ); jest.mock( '@wordpress/data', () => ( { ...jest.requireActual( '@wordpress/data' ), resolveSelect: jest.fn().mockReturnValue( { diff --git a/plugins/woocommerce-admin/client/guided-tours/variable-product-tour/index.tsx b/plugins/woocommerce-admin/client/guided-tours/variable-product-tour/index.tsx new file mode 100644 index 00000000000..6ed69be7aff --- /dev/null +++ b/plugins/woocommerce-admin/client/guided-tours/variable-product-tour/index.tsx @@ -0,0 +1,121 @@ +/** + * External dependencies + */ +import { useEffect, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { TourKit, TourKitTypes } from '@woocommerce/components'; +import { useUserPreferences } from '@woocommerce/data'; +import { recordEvent } from '@woocommerce/tracks'; + +function getStepName( + steps: TourKitTypes.WooStep[], + currentStepIndex: number +) { + return steps[ currentStepIndex ]?.meta?.name; +} + +export const VariableProductTour = () => { + const [ isTourOpen, setIsTourOpen ] = useState( false ); + + const { updateUserPreferences, variable_product_tour_shown: hasShownTour } = + useUserPreferences(); + + const config: TourKitTypes.WooConfig = { + steps: [ + { + referenceElements: { + desktop: '.attribute_tab', + }, + focusElement: { + desktop: '.attribute_tab', + }, + meta: { + name: 'attributes', + heading: __( 'Start by adding attributes', 'woocommerce' ), + descriptions: { + desktop: __( + 'Add attributes like size and color for customers to choose from on the product page. We will use them to generate product variations.', + 'woocommerce' + ), + }, + primaryButton: { + text: __( 'Got it', 'woocommerce' ), + }, + }, + }, + ], + options: { + // WooTourKit does not handle merging of default options properly, + // so we need to duplicate the effects options here. + effects: { + spotlight: { + interactivity: { + enabled: true, + rootElementSelector: '#wpwrap', + }, + }, + arrowIndicator: true, + liveResize: { + mutation: true, + resize: true, + rootElementSelector: '#wpwrap', + }, + }, + }, + closeHandler: ( steps, currentStepIndex ) => { + updateUserPreferences( { variable_product_tour_shown: 'yes' } ); + setIsTourOpen( false ); + + if ( currentStepIndex === steps.length - 1 ) { + recordEvent( 'variable_product_tour_completed', { + step: getStepName( + steps as TourKitTypes.WooStep[], + currentStepIndex + ), + } ); + } else { + recordEvent( 'variable_product_tour_dismissed', { + step: getStepName( + steps as TourKitTypes.WooStep[], + currentStepIndex + ), + } ); + } + }, + }; + + // show the tour when the product type is changed to variable + useEffect( () => { + const productTypeSelect = document.querySelector( + '#product-type' + ) as HTMLSelectElement; + + if ( hasShownTour === 'yes' || ! productTypeSelect ) { + return; + } + + function handleProductTypeChange() { + if ( productTypeSelect.value === 'variable' ) { + setIsTourOpen( true ); + recordEvent( 'variable_product_tour_started', { + step: getStepName( config.steps, 0 ), + } ); + } + } + + productTypeSelect.addEventListener( 'change', handleProductTypeChange ); + + return () => { + productTypeSelect.removeEventListener( + 'change', + handleProductTypeChange + ); + }; + } ); + + if ( ! isTourOpen ) { + return null; + } + + return ; +}; diff --git a/plugins/woocommerce-admin/client/homescreen/constants.js b/plugins/woocommerce-admin/client/homescreen/constants.js index b0b7575df8f..335e1bea6fe 100644 --- a/plugins/woocommerce-admin/client/homescreen/constants.js +++ b/plugins/woocommerce-admin/client/homescreen/constants.js @@ -9,9 +9,3 @@ export const WELCOME_MODAL_DISMISSED_OPTION_NAME = */ export const WELCOME_FROM_CALYPSO_MODAL_DISMISSED_OPTION_NAME = 'woocommerce_welcome_from_calypso_modal_dismissed'; - -/** - * WooCommerce Admin installation timestamp option name. - */ -export const WOOCOMMERCE_ADMIN_INSTALL_TIMESTAMP_OPTION_NAME = - 'woocommerce_admin_install_timestamp'; diff --git a/plugins/woocommerce-admin/client/homescreen/index.tsx b/plugins/woocommerce-admin/client/homescreen/index.tsx index e0199f9cddf..a9d80c505d4 100644 --- a/plugins/woocommerce-admin/client/homescreen/index.tsx +++ b/plugins/woocommerce-admin/client/homescreen/index.tsx @@ -3,7 +3,6 @@ */ import { compose } from '@wordpress/compose'; import { withSelect } from '@wordpress/data'; -import { identity } from 'lodash'; import { ONBOARDING_STORE_NAME, withOnboardingHydration, @@ -50,10 +49,8 @@ const withSelectHandler = ( select: WCDataSelector ) => { }; export default compose( - onboardingData.profile - ? withOnboardingHydration( { - profileItems: onboardingData.profile, - } ) - : identity, + withOnboardingHydration( { + profileItems: onboardingData.profile, + } ), withSelect( withSelectHandler ) )( Homescreen ); diff --git a/plugins/woocommerce-admin/client/homescreen/layout.js b/plugins/woocommerce-admin/client/homescreen/layout.js index 6fc310ebdcd..025c79e9fab 100644 --- a/plugins/woocommerce-admin/client/homescreen/layout.js +++ b/plugins/woocommerce-admin/client/homescreen/layout.js @@ -7,7 +7,6 @@ import { useCallback, useLayoutEffect, useRef, - useState, } from '@wordpress/element'; import { compose } from '@wordpress/compose'; import { withDispatch, withSelect } from '@wordpress/data'; @@ -53,7 +52,6 @@ const Tasks = lazy( () => export const Layout = ( { defaultHomescreenLayout, - isBatchUpdating, query, taskListComplete, hasTaskList, @@ -68,7 +66,6 @@ export const Layout = ( { const shouldShowStoreLinks = taskListComplete || isTaskListHidden; const hasTwoColumnContent = shouldShowStoreLinks || window.wcAdminFeatures.analytics; - const [ showInbox, setShowInbox ] = useState( true ); const isDashboardShown = ! query.task; // ?&task= query param is used to show tasks instead of the homescreen const activeSetupTaskList = useActiveSetupTasklist(); @@ -76,10 +73,6 @@ export const Layout = ( { ( userPrefs.homepage_layout || defaultHomescreenLayout ) === 'two_columns' && hasTwoColumnContent; - if ( isBatchUpdating && ! showInbox ) { - setShowInbox( true ); - } - const isWideViewport = useRef( true ); const maybeToggleColumns = useCallback( () => { isWideViewport.current = window.innerWidth >= 782; diff --git a/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/components/useJetpackPluginState.tsx b/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/components/useJetpackPluginState.tsx index f8d1308f2ca..68d89e5b383 100644 --- a/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/components/useJetpackPluginState.tsx +++ b/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/components/useJetpackPluginState.tsx @@ -89,8 +89,7 @@ export const useJetpackPluginState = () => { ); } else if ( jetpackConnectionData && - jetpackConnectionData?.currentUser?.username !== - jetpackConnectionData?.connectionOwner + ! jetpackConnectionData?.currentUser?.isMaster ) { setPluginState( JetpackPluginStates.NOT_OWNER_OF_CONNECTION diff --git a/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/illustrations/intro-devices-desktop.png b/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/illustrations/intro-devices-desktop.png index 07932b5539c..7911cb637a0 100644 Binary files a/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/illustrations/intro-devices-desktop.png and b/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/illustrations/intro-devices-desktop.png differ diff --git a/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/style.scss b/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/style.scss index 46c2847f6d7..814e78bd24c 100644 --- a/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/style.scss +++ b/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/style.scss @@ -102,6 +102,7 @@ img { margin-right: -24px; + width: 100%; } } } diff --git a/plugins/woocommerce-admin/client/index.js b/plugins/woocommerce-admin/client/index.js index 41a0866a6d8..67173157a26 100644 --- a/plugins/woocommerce-admin/client/index.js +++ b/plugins/woocommerce-admin/client/index.js @@ -3,6 +3,7 @@ */ import '@wordpress/notices'; import { render } from '@wordpress/element'; +import { CustomerEffortScoreTracksContainer } from '@woocommerce/customer-effort-score'; import { withCurrentUserHydration, withSettingsHydration, @@ -14,7 +15,6 @@ import { import './stylesheets/_index.scss'; import { getAdminSetting } from '~/utils/admin-settings'; import { PageLayout, EmbedLayout, PrimaryLayout as NoticeArea } from './layout'; -import { CustomerEffortScoreTracksContainer } from './customer-effort-score-tracks'; import { EmbeddedBodyLayout } from './embedded-body-layout'; import { WcAdminPaymentsGatewaysBannerSlot } from './payments/payments-settings-banner-slotfill'; import { WcAdminConflictErrorSlot } from './settings/conflict-error-slotfill.js'; diff --git a/plugins/woocommerce-admin/client/jest.config.js b/plugins/woocommerce-admin/client/jest.config.js index b914507a01f..2532b7004c2 100644 --- a/plugins/woocommerce-admin/client/jest.config.js +++ b/plugins/woocommerce-admin/client/jest.config.js @@ -1,6 +1,6 @@ module.exports = { rootDir: './', - preset: '../../../packages/js/internal-js-tests/jest.config.js', + preset: '../node_modules/@woocommerce/internal-js-tests/jest-preset.js', globals: { 'ts-jest': { diagnostics: { diff --git a/plugins/woocommerce-admin/client/layout/controller.js b/plugins/woocommerce-admin/client/layout/controller.js index 023b9af24c5..b89dd7596d5 100644 --- a/plugins/woocommerce-admin/client/layout/controller.js +++ b/plugins/woocommerce-admin/client/layout/controller.js @@ -51,11 +51,6 @@ const Dashboard = lazy( () => const Homescreen = lazy( () => import( /* webpackChunkName: "homescreen" */ '../homescreen' ) ); -const MarketingOverview = lazy( () => - import( - /* webpackChunkName: "marketing-overview" */ '../marketing/overview' - ) -); const MarketingOverviewMultichannel = lazy( () => import( /* webpackChunkName: "multichannel-marketing" */ '../marketing/overview-multichannel' @@ -157,9 +152,7 @@ export const getPages = () => { if ( window.wcAdminFeatures.marketing ) { pages.push( { - container: window.wcAdminFeatures[ 'multichannel-marketing' ] - ? MarketingOverviewMultichannel - : MarketingOverview, + container: MarketingOverviewMultichannel, path: '/marketing', breadcrumbs: [ ...initialBreadcrumbs, @@ -174,7 +167,7 @@ export const getPages = () => { } ); } - if ( window.wcAdminFeatures[ 'block-editor-feature-enabled' ] ) { + if ( window.wcAdminFeatures[ 'product-block-editor' ] ) { pages.push( { container: ProductPage, path: '/add-product', diff --git a/plugins/woocommerce-admin/client/layout/index.js b/plugins/woocommerce-admin/client/layout/index.js index 51c928048b2..9a6344e09b0 100644 --- a/plugins/woocommerce-admin/client/layout/index.js +++ b/plugins/woocommerce-admin/client/layout/index.js @@ -17,6 +17,10 @@ import { Children, cloneElement } from 'react'; import PropTypes from 'prop-types'; import { get, isFunction, identity, memoize } from 'lodash'; import { parse } from 'qs'; +import { + CustomerEffortScoreModalContainer, + triggerExitPageCesSurvey, +} from '@woocommerce/customer-effort-score'; import { getHistory, getQuery } from '@woocommerce/navigation'; import { PLUGINS_STORE_NAME, @@ -37,9 +41,7 @@ import { Header } from '../header'; import { Footer } from './footer'; import Notices from './notices'; import TransientNotices from './transient-notices'; -import { CustomerEffortScoreModalContainer } from '../customer-effort-score-tracks'; import { getAdminSetting } from '~/utils/admin-settings'; -import { triggerExitPageCesSurvey } from '~/customer-effort-score-tracks/customer-effort-score-exit-page'; import '~/activity-panel'; import '~/mobile-banner'; import './navigation'; diff --git a/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx b/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx index c7044810222..d6f811b5796 100644 --- a/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx +++ b/plugins/woocommerce-admin/client/marketing/components/CollapsibleCard/CollapsibleCard.tsx @@ -64,7 +64,7 @@ const CollapsibleCard: React.FC< CollapsibleCardProps > = ( { { ! collapsed && ( <> { children } - { footer && { footer } } + { !! footer && { footer } } ) } diff --git a/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.scss b/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.scss new file mode 100644 index 00000000000..377fedce2b5 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.scss @@ -0,0 +1,54 @@ +.woocommerce-marketing-create-campaign-modal { + max-width: 600px; + + .components-modal__content { + padding-bottom: $gap; + } + + .woocommerce-marketing-new-campaigns { + padding-top: $gap-large; + padding-bottom: $gap; + + &__question-label { + font-size: 14px; + font-weight: 600; + line-height: 17px; + margin-bottom: 16px; + } + } + + .components-button.is-link { + text-decoration: none; + font-size: 14px; + font-weight: 600; + line-height: 17px; + } + + .woocommerce_marketing_plugin_card_body { + padding: $gap 0; + } + + .woocommerce-marketing-new-campaign-type { + padding: $gap 0; + + &__name { + font-weight: 600; + font-size: 14px; + line-height: 20px; + } + + &__description { + color: #555d66; + } + + img, + svg { + display: block; + } + } + + .woocommerce-marketing-add-channels { + padding-top: $gap; + border-top: solid 1px $gray-300; + } +} diff --git a/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.test.tsx b/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.test.tsx new file mode 100644 index 00000000000..54a389a93ed --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.test.tsx @@ -0,0 +1,127 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +/** + * Internal dependencies + */ +import { useCampaignTypes, useRecommendedChannels } from '~/marketing/hooks'; +import { CreateNewCampaignModal } from './CreateNewCampaignModal'; + +jest.mock( '@woocommerce/components', () => { + const originalModule = jest.requireActual( '@woocommerce/components' ); + + return { + __esModule: true, + ...originalModule, + Spinner: () =>
      Spinner
      , + }; +} ); + +jest.mock( '~/marketing/hooks', () => ( { + useCampaignTypes: jest.fn(), + useRecommendedChannels: jest.fn(), + useRegisteredChannels: jest.fn( () => ( {} ) ), + useInstalledPluginsWithoutChannels: jest.fn( () => ( {} ) ), +} ) ); + +const google = { + id: 'google-ads', + icon: 'https://woocommerce.com/wp-content/uploads/2021/06/woo-GoogleListingsAds-jworee.png', + name: 'Google Ads', + description: + 'Boost your product listings with a campaign that is automatically optimized to meet your goals.', + createUrl: + 'https://wc1.test/wp-admin/admin.php?page=wc-admin&path=/google/dashboard&subpath=/campaigns/create', + channelName: 'Google Listings and Ads', + channelSlug: 'google-listings-and-ads', +}; + +const pinterest = { + title: 'Pinterest for WooCommerce', + description: + 'Grow your business on Pinterest! Use this official plugin to allow shoppers to Pin products while browsing your store, track conversions, and advertise on Pinterest.', + url: 'https://woocommerce.com/products/pinterest-for-woocommerce/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', + direct_install: true, + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/pinterest.svg', + product: 'pinterest-for-woocommerce', + plugin: 'pinterest-for-woocommerce/pinterest-for-woocommerce.php', + categories: [ 'marketing' ], + subcategories: [ { slug: 'sales-channels', name: 'Sales channels' } ], + tags: [ + { + slug: 'built-by-woocommerce', + name: 'Built by WooCommerce', + }, + ], + show_extension_promotions: true, +}; + +const amazon = { + title: 'Amazon, eBay & Walmart Integration for WooCommerce', + description: + 'Convert Woocommerce into a fully-featured omnichannel commerce platform, leveraging powerful automation and real-time sync to connect your brand with millions of new customers on the world\u2019s largest online marketplaces.', + url: 'https://woocommerce.com/products/amazon-ebay-integration/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons', + direct_install: false, + icon: 'https://woocommerce.com/wp-content/plugins/wccom-plugins/marketing-tab-rest-api/icons/amazon-ebay.svg', + product: 'amazon-ebay-integration', + plugin: 'woocommerce-amazon-ebay-integration/woocommerce-amazon-ebay-integration.php', + categories: [ 'marketing' ], + subcategories: [ { slug: 'sales-channels', name: 'Sales channels' } ], + tags: [], +}; + +describe( 'CreateNewCampaignModal component', () => { + it( 'renders new campaign types with recommended channels', async () => { + ( useCampaignTypes as jest.Mock ).mockReturnValue( { + data: [ google ], + } ); + ( useRecommendedChannels as jest.Mock ).mockReturnValue( { + data: [ pinterest, amazon ], + } ); + render( {} } /> ); + + expect( screen.getByText( 'Google Ads' ) ).toBeInTheDocument(); + expect( + screen.getByText( + 'Boost your product listings with a campaign that is automatically optimized to meet your goals.' + ) + ).toBeInTheDocument(); + + // Click button to expand recommended channels section. + await userEvent.click( + screen.getByRole( 'button', { + name: 'Add channels for other campaign types', + } ) + ); + + expect( + screen.getByText( 'Pinterest for WooCommerce' ) + ).toBeInTheDocument(); + + expect( + screen.getByText( + 'Amazon, eBay & Walmart Integration for WooCommerce' + ) + ).toBeInTheDocument(); + } ); + + it( 'does not render recommended channels section when there are no recommended channels', async () => { + ( useCampaignTypes as jest.Mock ).mockReturnValue( { + data: [ google ], + } ); + ( useRecommendedChannels as jest.Mock ).mockReturnValue( { + data: [], + } ); + render( {} } /> ); + + // The expand button should not be there. + expect( + screen.queryByRole( 'button', { + name: 'Add channels for other campaign types', + } ) + ).not.toBeInTheDocument(); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.tsx b/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.tsx new file mode 100644 index 00000000000..2df5a402104 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.tsx @@ -0,0 +1,165 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { + Button, + Modal, + Icon, + Flex, + FlexBlock, + FlexItem, +} from '@wordpress/components'; +import { chevronUp, chevronDown, external } from '@wordpress/icons'; +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import { + useRecommendedChannels, + useCampaignTypes, + useRegisteredChannels, + useInstalledPluginsWithoutChannels, +} from '~/marketing/hooks'; +import { SmartPluginCardBody } from '~/marketing/components'; +import './CreateNewCampaignModal.scss'; + +const isExternalURL = ( url: string ) => + new URL( url ).origin !== location.origin; + +/** + * Props for CreateNewCampaignModal, which is based on Modal.Props. + * + * Modal's title and children props are omitted because they are specified within the component + * and not needed to be specified by the consumer. + */ +type CreateCampaignModalProps = Omit< Modal.Props, 'title' | 'children' >; + +export const CreateNewCampaignModal = ( props: CreateCampaignModalProps ) => { + const { className, ...restProps } = props; + const [ collapsed, setCollapsed ] = useState( true ); + const { data: campaignTypes, refetch: refetchCampaignTypes } = + useCampaignTypes(); + const { refetch: refetchRegisteredChannels } = useRegisteredChannels(); + const { data: recommendedChannels } = useRecommendedChannels(); + const { loadInstalledPluginsAfterActivation } = + useInstalledPluginsWithoutChannels(); + + const hasCampaignTypes = !! campaignTypes?.length; + const hasRecommendedChannels = !! recommendedChannels?.length; + + const onInstalledAndActivated = ( pluginSlug: string ) => { + refetchCampaignTypes(); + refetchRegisteredChannels(); + loadInstalledPluginsAfterActivation( pluginSlug ); + }; + + return ( + +
      +
      + { hasCampaignTypes + ? __( + 'Where would you like to promote your products?', + 'woocommerce' + ) + : __( 'No campaign types found.', 'woocommerce' ) } +
      + { campaignTypes?.map( ( el ) => ( + + + { + + + + + { el.name } + + + { el.description } + + + + + + + + ) ) } +
      + { hasRecommendedChannels && ( +
      + + + + + { ! collapsed && ( + + { recommendedChannels.map( ( el ) => ( + + ) ) } + + ) } + +
      + ) } +
      + ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/index.ts b/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/index.ts new file mode 100644 index 00000000000..257f5c0d079 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/index.ts @@ -0,0 +1 @@ +export { CreateNewCampaignModal } from './CreateNewCampaignModal'; diff --git a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx index 32262e7139c..76cf16f01aa 100644 --- a/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/components/PluginCardBody/SmartPluginCardBody.tsx @@ -21,7 +21,7 @@ import './PluginCardBody.scss'; type SmartPluginCardBodyProps = { plugin: RecommendedPlugin; - onInstalledAndActivated?: () => void; + onInstalledAndActivated?: ( pluginSlug: string ) => void; }; /** @@ -62,7 +62,7 @@ export const SmartPluginCardBody = ( { plugin.product, ] ); - onInstalledAndActivated(); + onInstalledAndActivated( plugin.product ); createNoticesFromResponse( response ); } catch ( error ) { createNoticesFromResponse( error ); diff --git a/plugins/woocommerce-admin/client/marketing/components/button/README.md b/plugins/woocommerce-admin/client/marketing/components/button/README.md deleted file mode 100644 index 5c0298a9428..00000000000 --- a/plugins/woocommerce-admin/client/marketing/components/button/README.md +++ /dev/null @@ -1,22 +0,0 @@ -Button -=== - -This component creates simple reusable html `` element. - -## Usage - -```jsx - -``` - -### Props - -Name | Type | Default | Description ---- | --- | --- | --- -`className` | String | `null` | Additional class name to style the component diff --git a/plugins/woocommerce-admin/client/marketing/components/button/index.js b/plugins/woocommerce-admin/client/marketing/components/button/index.js deleted file mode 100644 index 0542f4fc998..00000000000 --- a/plugins/woocommerce-admin/client/marketing/components/button/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * External dependencies - */ -import { Button } from '@wordpress/components'; -import classnames from 'classnames'; - -/** - * Internal dependencies - */ -import './style.scss'; - -export default ( props ) => { - return ( - + { isModalOpen && ( + setModalOpen( false ) } + /> + ) } { getContent() } - { total && total > perPage && ( + { showFooter && ( ; recommendedChannels: Array< RecommendedChannel >; - onInstalledAndActivated?: () => void; + onInstalledAndActivated?: ( pluginSlug: string ) => void; }; -export const Channels: React.FC< ChannelsProps > = ( { - registeredChannels, - recommendedChannels, - onInstalledAndActivated, -} ) => { - const hasRegisteredChannels = registeredChannels.length >= 1; - +export type ChannelsRef = { /** - * State to collapse / expand the recommended channels. - * Initial state is expanded if there are no registered channels in first page load. + * Scroll into the "Add channels" section in the card. + * The section will be expanded, and the "Add channels" button will be in focus. */ - const [ expanded, setExpanded ] = useState( ! hasRegisteredChannels ); + scrollIntoAddChannels: () => void; +}; - return ( - - - - { __( 'Channels', 'woocommerce' ) } - - { ! hasRegisteredChannels && ( - - { __( - 'Start by adding a channel to your store', - 'woocommerce' - ) } - - ) } - +export const Channels = forwardRef< ChannelsRef, ChannelsProps >( + ( + { registeredChannels, recommendedChannels, onInstalledAndActivated }, + ref + ) => { + const hasRegisteredChannels = registeredChannels.length >= 1; - { /* Registered channels section. */ } - { registeredChannels.map( ( el, idx ) => { - return ( + /** + * 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 ); + const addChannelsButtonRef = useRef< HTMLButtonElement >( null ); + + useImperativeHandle( + ref, + () => ( { + scrollIntoAddChannels: () => { + setExpanded( true ); + addChannelsButtonRef.current?.focus(); + addChannelsButtonRef.current?.scrollIntoView( { + block: 'center', + } ); + }, + } ), + [] + ); + + return ( + + + + { __( 'Channels', 'woocommerce' ) } + + { ! hasRegisteredChannels && ( + + { __( + 'Start by adding a channel to your store', + 'woocommerce' + ) } + + ) } + + + { /* Registered channels section. */ } + { registeredChannels.map( ( el, idx ) => ( { idx !== registeredChannels.length - 1 && ( ) } - ); - } ) } + ) ) } - { /* Recommended channels section. */ } - { recommendedChannels.length >= 1 && ( -
      - { hasRegisteredChannels && ( - <> - - - - - - ) } - { expanded && - recommendedChannels.map( ( el, idx ) => { - return ( + > + { __( 'Add channels', 'woocommerce' ) } + + + + + ) } + { expanded && + recommendedChannels.map( ( el, idx ) => ( = ( { ) } - ); - } ) } -
      - ) } -
      - ); -}; + ) ) } + + ) } +
      + ); + } +); diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx index 2d4d9c007fa..ff80123cfc4 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/RegisteredChannelCardBody.tsx @@ -31,7 +31,7 @@ export const RegisteredChannelCardBody: React.FC< registeredChannel.description ) : (
      - { registeredChannel.syncStatus && ( + { !! registeredChannel.syncStatus && ( <>
      diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts index da0d9c56072..f0e4fcc3762 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Channels/index.ts @@ -1 +1,2 @@ export { Channels } from './Channels'; +export type { ChannelsRef } from './Channels'; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.test.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.test.tsx index 568b353147f..18ada3a3c24 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.test.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.test.tsx @@ -6,8 +6,7 @@ import { render, screen } from '@testing-library/react'; /** * Internal dependencies */ -import { useInstalledPlugins } from '../../hooks'; -import { useRecommendedPlugins } from './useRecommendedPlugins'; +import { useRecommendedPluginsWithoutChannels } from './useRecommendedPluginsWithoutChannels'; import { DiscoverTools } from './DiscoverTools'; jest.mock( '@woocommerce/components', () => { @@ -20,23 +19,20 @@ jest.mock( '@woocommerce/components', () => { }; } ); -jest.mock( './useRecommendedPlugins', () => ( { - useRecommendedPlugins: jest.fn(), +jest.mock( './useRecommendedPluginsWithoutChannels', () => ( { + useRecommendedPluginsWithoutChannels: jest.fn(), } ) ); -jest.mock( '../../hooks', () => ( { - useInstalledPlugins: jest.fn(), +jest.mock( '~/marketing/hooks', () => ( { + useInstalledPluginsWithoutChannels: jest.fn( () => ( {} ) ), } ) ); describe( 'DiscoverTools component', () => { it( 'should render a Spinner when loading is in progress', () => { - ( useRecommendedPlugins as jest.Mock ).mockReturnValue( { + ( useRecommendedPluginsWithoutChannels as jest.Mock ).mockReturnValue( { isInitializing: true, isLoading: true, - plugins: [], - } ); - ( useInstalledPlugins as jest.Mock ).mockReturnValue( { - loadInstalledPluginsAfterActivation: jest.fn(), + data: [], } ); render( ); @@ -44,13 +40,10 @@ describe( 'DiscoverTools component', () => { } ); it( 'should render message and link when loading is finish and there are no plugins', () => { - ( useRecommendedPlugins as jest.Mock ).mockReturnValue( { + ( useRecommendedPluginsWithoutChannels as jest.Mock ).mockReturnValue( { isInitializing: false, isLoading: false, - plugins: [], - } ); - ( useInstalledPlugins as jest.Mock ).mockReturnValue( { - loadInstalledPluginsAfterActivation: jest.fn(), + data: [], } ); render( ); @@ -66,10 +59,12 @@ describe( 'DiscoverTools component', () => { describe( 'With plugins loaded', () => { it( 'should render `direct_install: true` plugins with "Install plugin" button', () => { - ( useRecommendedPlugins as jest.Mock ).mockReturnValue( { + ( + useRecommendedPluginsWithoutChannels as jest.Mock + ).mockReturnValue( { isInitializing: false, isLoading: false, - plugins: [ + data: [ { title: 'Google Listings and Ads', description: @@ -95,9 +90,6 @@ describe( 'DiscoverTools component', () => { }, ], } ); - ( useInstalledPlugins as jest.Mock ).mockReturnValue( { - loadInstalledPluginsAfterActivation: jest.fn(), - } ); render( ); // Assert that we have the "Sales channels" tab, the plugin name, the "Built by WooCommerce" pill, and the "Install plugin" button. @@ -112,10 +104,12 @@ describe( 'DiscoverTools component', () => { } ); it( 'should render `direct_install: false` plugins with "View details" button', () => { - ( useRecommendedPlugins as jest.Mock ).mockReturnValue( { + ( + useRecommendedPluginsWithoutChannels as jest.Mock + ).mockReturnValue( { isInitializing: false, isLoading: false, - plugins: [ + data: [ { title: 'WooCommerce Zapier', description: @@ -136,9 +130,6 @@ describe( 'DiscoverTools component', () => { }, ], } ); - ( useInstalledPlugins as jest.Mock ).mockReturnValue( { - loadInstalledPluginsAfterActivation: jest.fn(), - } ); render( ); // Assert that we have the CRM tab, plugin name, and "View details" button. 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 a116640fbe0..bfade3d9f0d 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.tsx @@ -14,13 +14,13 @@ import { CardBody, CenteredSpinner, } from '~/marketing/components'; -import { useRecommendedPlugins } from './useRecommendedPlugins'; +import { useRecommendedPluginsWithoutChannels } from './useRecommendedPluginsWithoutChannels'; import { PluginsTabPanel } from './PluginsTabPanel'; import './DiscoverTools.scss'; export const DiscoverTools = () => { - const { isInitializing, isLoading, plugins, installAndActivate } = - useRecommendedPlugins(); + const { isInitializing, isLoading, data, installAndActivate } = + useRecommendedPluginsWithoutChannels(); /** * Renders card body. @@ -38,7 +38,7 @@ export const DiscoverTools = () => { ); } - if ( plugins.length === 0 ) { + if ( data.length === 0 ) { return ( @@ -66,7 +66,7 @@ export const DiscoverTools = () => { return ( 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 8a689f4f2c7..902bbc8a02e 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/PluginsTabPanel.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/PluginsTabPanel.tsx @@ -14,7 +14,7 @@ import { flatMapDeep, uniqBy } from 'lodash'; * Internal dependencies */ import { CardDivider, PluginCardBody } from '~/marketing/components'; -import { useInstalledPlugins } from '~/marketing/hooks'; +import { useInstalledPluginsWithoutChannels } from '~/marketing/hooks'; import { RecommendedPlugin } from '~/marketing/types'; import { getInAppPurchaseUrl } from '~/lib/in-app-purchase'; import { createNoticesFromResponse } from '~/lib/notices'; @@ -60,7 +60,8 @@ export const PluginsTabPanel = ( { null ); const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME ); - const { loadInstalledPluginsAfterActivation } = useInstalledPlugins(); + const { loadInstalledPluginsAfterActivation } = + useInstalledPluginsWithoutChannels(); /** * Install and activate a plugin. diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/useRecommendedPlugins.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/useRecommendedPlugins.ts deleted file mode 100644 index e8865fd4888..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/useRecommendedPlugins.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * External dependencies - */ -import { useSelect, useDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { STORE_KEY } from '~/marketing/data/constants'; -import { RecommendedPlugin } from '~/marketing/types'; - -const selector = 'getRecommendedPlugins'; -const category = 'marketing'; - -export const useRecommendedPlugins = () => { - const { invalidateResolution, installAndActivateRecommendedPlugin } = - useDispatch( STORE_KEY ); - - const installAndActivate = ( plugin: string ) => { - installAndActivateRecommendedPlugin( plugin, category ); - invalidateResolution( selector, [ category ] ); - }; - - return useSelect( ( select ) => { - const { getRecommendedPlugins, hasFinishedResolution } = - select( STORE_KEY ); - const plugins = - getRecommendedPlugins< RecommendedPlugin[] >( category ); - const isLoading = ! hasFinishedResolution( selector, [ category ] ); - - return { - isInitializing: ! plugins.length && isLoading, - isLoading, - plugins, - installAndActivate, - }; - }, [] ); -}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/useRecommendedPluginsWithoutChannels.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/useRecommendedPluginsWithoutChannels.ts new file mode 100644 index 00000000000..ef10efb9e8d --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/useRecommendedPluginsWithoutChannels.ts @@ -0,0 +1,92 @@ +/** + * External dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { differenceWith } from 'lodash'; + +/** + * Internal dependencies + */ +import { STORE_KEY } from '~/marketing/data/constants'; +import { useRecommendedChannels } from '~/marketing/hooks'; +import { RecommendedPlugin } from '~/marketing/types'; + +type UseRecommendedPluginsWithoutChannels = { + /** + * Boolean indicating whether it is initializing. + */ + isInitializing: boolean; + + /** + * Boolean indicating whether it is loading. + * + * This will be true when data is being refetched + * after `invalidateResolution` is called in the `installAndActivate` method. + */ + isLoading: boolean; + + /** + * An array of recommended marketing plugins without marketing channels. + */ + data: RecommendedPlugin[]; + + /** + * Install and activate a plugin. + */ + installAndActivate: ( slug: string ) => void; +}; + +const selector = 'getRecommendedPlugins'; +const category = 'marketing'; + +/** + * A hook to return a list of recommended plugins without marketing channels, + * and related methods, to be used with the `DiscoverTools` component. + */ +export const useRecommendedPluginsWithoutChannels = + (): UseRecommendedPluginsWithoutChannels => { + const { + loading: loadingRecommendedPlugins, + data: dataRecommendedPlugins, + } = useSelect( ( select ) => { + const { getRecommendedPlugins, hasFinishedResolution } = + select( STORE_KEY ); + + return { + loading: ! hasFinishedResolution( selector, [ category ] ), + data: getRecommendedPlugins< RecommendedPlugin[] >( category ), + }; + }, [] ); + + const { + loading: loadingRecommendedChannels, + data: dataRecommendedChannels, + } = useRecommendedChannels(); + + const { invalidateResolution, installAndActivateRecommendedPlugin } = + useDispatch( STORE_KEY ); + + const isInitializing = + ( loadingRecommendedPlugins && ! dataRecommendedPlugins.length ) || + ( loadingRecommendedChannels && ! dataRecommendedChannels ); + + const loading = loadingRecommendedPlugins || loadingRecommendedChannels; + + const recommendedPluginsWithoutChannels = differenceWith( + dataRecommendedPlugins, + dataRecommendedChannels || [], + ( a, b ) => a.product === b.product + ); + + const installAndActivate = ( slug: string ) => { + installAndActivateRecommendedPlugin( slug, category ); + invalidateResolution( selector, [ category ] ); + }; + + return { + isInitializing, + isLoading: loading, + data: isInitializing ? [] : recommendedPluginsWithoutChannels, + installAndActivate, + }; + }; 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 f445fa7a69b..cee200843c2 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/InstalledExtensions/InstalledExtensions.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/InstalledExtensions/InstalledExtensions.tsx @@ -16,13 +16,13 @@ import { PluginCardBody, } from '~/marketing/components'; import { InstalledPlugin } from '~/marketing/types'; -import { useInstalledPlugins } from '~/marketing/hooks'; +import { useInstalledPluginsWithoutChannels } from '~/marketing/hooks'; export const InstalledExtensions = () => { - const { installedPlugins, activatingPlugins, activateInstalledPlugin } = - useInstalledPlugins(); + const { data, activatingPlugins, activateInstalledPlugin } = + useInstalledPluginsWithoutChannels(); - if ( installedPlugins.length === 0 ) { + if ( data.length === 0 ) { return null; } @@ -81,7 +81,7 @@ export const InstalledExtensions = () => { return ( - { installedPlugins.map( ( el, idx ) => { + { data.map( ( el, idx ) => { return ( { description={ el.description } button={ getButton( el ) } /> - { idx !== installedPlugins.length - 1 && ( - - ) } + { idx !== data.length - 1 && } ); } ) } diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/IntroductionBanner.scss b/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/IntroductionBanner.scss new file mode 100644 index 00000000000..99ac6dc8ad4 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/IntroductionBanner.scss @@ -0,0 +1,54 @@ +.woocommerce-marketing-introduction-banner { + & > div { + display: flex; + flex-wrap: wrap; + } + + .woocommerce-marketing-introduction-banner-content { + flex: 1 0; + margin: 32px 20px 32px 40px; + + .woocommerce-marketing-introduction-banner-title { + font-size: 20px; + line-height: 28px; + margin-bottom: $gap-smaller; + } + + .woocommerce-marketing-introduction-banner-features { + color: $gray-700; + + svg { + fill: $studio-woocommerce-purple-50; + } + } + + .woocommerce-marketing-introduction-banner-buttons { + margin-top: $gap; + } + } + + .woocommerce-marketing-introduction-banner-illustration { + flex: 0 0 270px; + background: linear-gradient(90deg, rgba(247, 237, 247, 0) 5.31%, rgba(196, 152, 217, 0.12) 77.75%), + linear-gradient(90deg, rgba(247, 237, 247, 0) 22%, rgba(196, 152, 217, 0.12) 84.6%); + + .woocommerce-marketing-introduction-banner-image-placeholder { + width: 100%; + height: 100%; + background: center / contain no-repeat; + } + + .woocommerce-marketing-introduction-banner-close-button { + position: absolute; + top: $gap-small; + right: $gap; + padding: 0; + } + + img { + display: block; + width: 100%; + height: 100%; + } + } +} diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/IntroductionBanner.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/IntroductionBanner.tsx new file mode 100644 index 00000000000..0761d8da095 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/IntroductionBanner.tsx @@ -0,0 +1,153 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { Card, Flex, FlexItem, FlexBlock, Button } from '@wordpress/components'; +import { Icon, trendingUp, megaphone, closeSmall } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { CreateNewCampaignModal } from '~/marketing/components'; +import { + useRegisteredChannels, + useRecommendedChannels, +} from '~/marketing/hooks'; +import './IntroductionBanner.scss'; +import wooIconUrl from './woo.svg'; +import illustrationUrl from './illustration.svg'; + +type IntroductionBannerProps = { + onDismissClick: () => void; + onAddChannelsClick: () => void; +}; + +export const IntroductionBanner = ( { + onDismissClick, + onAddChannelsClick, +}: IntroductionBannerProps ) => { + const [ isModalOpen, setModalOpen ] = useState( false ); + const { data: dataRegistered } = useRegisteredChannels(); + const { data: dataRecommended } = useRecommendedChannels(); + + const showCreateCampaignButton = !! dataRegistered?.length; + + /** + * Boolean to display the "Add channels" button in the introduction banner. + * + * This depends on the number of registered channels, + * because if there are no registered channels, + * the Channels card will not have the "Add channels" toggle button, + * and it does not make sense to display the "Add channels" button in this introduction banner + * that will do nothing upon click. + * + * If there are registered channels and recommended channels, + * the Channels card will display the "Add channels" toggle button, + * and clicking on the "Add channels" button in this introduction banner + * will scroll to the button in Channels card. + */ + const showAddChannelsButton = !! ( + dataRegistered?.length && dataRecommended?.length + ); + + return ( + +
      +
      + { __( + 'Reach new customers and increase sales without leaving WooCommerce', + 'woocommerce' + ) } +
      + + + + + + { __( + 'Reach customers on other sales channels', + 'woocommerce' + ) } + + + + + + + + { __( + 'Advertise with marketing campaigns', + 'woocommerce' + ) } + + + + + + { + + { __( 'Built by WooCommerce', 'woocommerce' ) } + + + + + { ( showCreateCampaignButton || showAddChannelsButton ) && ( + + { showCreateCampaignButton && ( + + ) } + { showAddChannelsButton && ( + + ) } + + ) } + { isModalOpen && ( + setModalOpen( false ) } + /> + ) } +
      +
      + +
      +
      + + ); +}; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/illustration.svg b/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/illustration.svg new file mode 100644 index 00000000000..3a00d63416c --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/illustration.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/index.ts b/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/index.ts new file mode 100644 index 00000000000..8ae35a6ca33 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/index.ts @@ -0,0 +1 @@ +export { IntroductionBanner } from './IntroductionBanner'; diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/woo.svg b/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/woo.svg new file mode 100644 index 00000000000..fb4a4205043 --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/IntroductionBanner/woo.svg @@ -0,0 +1,15 @@ + + +WooCommerce Logo + + + +image/svg+xml + + + + + + + + diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/LearnMarketing/PostTile.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/LearnMarketing/PostTile.tsx index c5404557db5..7dc38070cd3 100644 --- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/LearnMarketing/PostTile.tsx +++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/LearnMarketing/PostTile.tsx @@ -27,7 +27,7 @@ export const PostTile: React.FC< PostTileProps > = ( { post } ) => { } } >
      - { post.image && } + { !! post.image && }
      { post.title } @@ -37,7 +37,7 @@ export const PostTile: React.FC< PostTileProps > = ( { post } ) => { // translators: %s: author's name. sprintf( __( 'By %s', 'woocommerce' ), post.author_name ) } - { post.author_avatar && ( + { !! post.author_avatar && ( { + const { + loading: loadingIntroductionBanner, + isIntroductionBannerDismissed, + dismissIntroductionBanner, + } = useIntroductionBanner(); + const { loading: loadingCampaigns, meta: metaCampaigns } = useCampaigns(); + const { + loading: loadingCampaignTypes, + data: dataCampaignTypes, + refetch: refetchCampaignTypes, + } = useCampaignTypes(); const { loading: loadingRegistered, data: dataRegistered, - refetch, + refetch: refetchRegisteredChannels, } = useRegisteredChannels(); const { loading: loadingRecommended, data: dataRecommended } = useRecommendedChannels(); + const { loadInstalledPluginsAfterActivation } = + useInstalledPluginsWithoutChannels(); const { currentUserCan } = useUser(); - - const shouldShowExtensions = - getAdminSetting( 'allowMarketplaceSuggestions', false ) && - currentUserCan( 'install_plugins' ); + const channelsRef = useRef< ChannelsRef >( null ); if ( + loadingIntroductionBanner || + ( loadingCampaigns && metaCampaigns?.total === undefined ) || + ( loadingCampaignTypes && ! dataCampaignTypes ) || ( loadingRegistered && ! dataRegistered ) || ( loadingRecommended && ! dataRecommended ) ) { return ; } + const showCampaigns = !! ( + dataRegistered?.length && + ( isIntroductionBannerDismissed || metaCampaigns?.total ) + ); + + const showChannels = !! ( + dataRegistered && + dataRecommended && + ( dataRegistered.length || dataRecommended.length ) + ); + + const showExtensions = !! ( + getAdminSetting( 'allowMarketplaceSuggestions', false ) && + currentUserCan( 'install_plugins' ) + ); + + const onInstalledAndActivated = ( pluginSlug: string ) => { + refetchCampaignTypes(); + refetchRegisteredChannels(); + loadInstalledPluginsAfterActivation( pluginSlug ); + }; + return (
      - { !! dataRegistered?.length && } - { dataRegistered && - dataRecommended && - !! ( dataRegistered.length || dataRecommended.length ) && ( - - ) } + { ! isIntroductionBannerDismissed && ( + { + channelsRef.current?.scrollIntoAddChannels(); + } } + /> + ) } + { showCampaigns && } + { showChannels && ( + + ) } - { shouldShowExtensions && } + { showExtensions && }
      ); diff --git a/plugins/woocommerce-admin/client/marketing/overview/index.js b/plugins/woocommerce-admin/client/marketing/overview/index.js deleted file mode 100644 index 966254141ed..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview/index.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * External dependencies - */ -import { useUser, withOptionsHydration } from '@woocommerce/data'; - -/** - * Internal dependencies - */ -import './style.scss'; -import InstalledExtensions from './installed-extensions'; -import RecommendedExtensions from '../components/recommended-extensions'; -import KnowledgeBase from '../components/knowledge-base'; -import WelcomeCard from './welcome-card'; -import { getAdminSetting } from '~/utils/admin-settings'; -import { MarketingOverviewSectionSlot } from './section-slot'; -import '../data'; - -const MarketingOverview = () => { - const { currentUserCan } = useUser(); - - const shouldShowExtensions = - getAdminSetting( 'allowMarketplaceSuggestions', false ) && - currentUserCan( 'install_plugins' ); - - return ( -
      - - - - { shouldShowExtensions && ( - - ) } - -
      - ); -}; - -export default withOptionsHydration( { - ...getAdminSetting( 'preloadOptions', {} ), -} )( MarketingOverview ); diff --git a/plugins/woocommerce-admin/client/marketing/overview/installed-extensions/index.js b/plugins/woocommerce-admin/client/marketing/overview/installed-extensions/index.js deleted file mode 100644 index 2ee875f8ba5..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview/installed-extensions/index.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * External dependencies - */ -import { Component } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { compose } from '@wordpress/compose'; -import { withSelect, withDispatch } from '@wordpress/data'; -import PropTypes from 'prop-types'; -import { Card, CardHeader } from '@wordpress/components'; -import { Text } from '@woocommerce/experimental'; - -/** - * Internal dependencies - */ -import './style.scss'; -import InstalledExtensionRow from './row'; -import { STORE_KEY } from '../../data/constants'; - -class InstalledExtensions extends Component { - activatePlugin( pluginSlug ) { - const { activateInstalledPlugin } = this.props; - activateInstalledPlugin( pluginSlug ); - } - - isActivatingPlugin( pluginSlug ) { - const { activatingPlugins } = this.props; - return activatingPlugins.includes( pluginSlug ); - } - - render() { - const { plugins } = this.props; - - if ( plugins.length === 0 ) { - return null; - } - - const title = __( 'Installed marketing extensions', 'woocommerce' ); - - return ( - - - - { title } - - - { plugins.map( ( plugin ) => { - return ( - - this.activatePlugin( plugin.slug ) - } - isLoading={ this.isActivatingPlugin( plugin.slug ) } - /> - ); - } ) } - - ); - } -} - -InstalledExtensions.propTypes = { - /** - * Array of installed plugin objects. - */ - plugins: PropTypes.arrayOf( PropTypes.object ).isRequired, - /** - * Array of plugins that are currently activating. - */ - activatingPlugins: PropTypes.arrayOf( PropTypes.string ).isRequired, -}; - -export default compose( - withSelect( ( select ) => { - const { getInstalledPlugins, getActivatingPlugins } = - select( STORE_KEY ); - - return { - plugins: getInstalledPlugins(), - activatingPlugins: getActivatingPlugins(), - }; - } ), - withDispatch( ( dispatch ) => { - const { activateInstalledPlugin } = dispatch( STORE_KEY ); - - return { - activateInstalledPlugin, - }; - } ) -)( InstalledExtensions ); diff --git a/plugins/woocommerce-admin/client/marketing/overview/installed-extensions/row.js b/plugins/woocommerce-admin/client/marketing/overview/installed-extensions/row.js deleted file mode 100644 index a6bd452ee9c..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview/installed-extensions/row.js +++ /dev/null @@ -1,170 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Component } from '@wordpress/element'; -import PropTypes from 'prop-types'; -import { Link } from '@woocommerce/components'; -import { recordEvent } from '@woocommerce/tracks'; - -/** - * Internal dependencies - */ -import { Button, ProductIcon } from '../../components'; - -class InstalledExtensionRow extends Component { - constructor( props ) { - super( props ); - - this.onActivateClick = this.onActivateClick.bind( this ); - this.onFinishSetupClick = this.onFinishSetupClick.bind( this ); - } - - getLinks() { - const { docsUrl, settingsUrl, supportUrl, dashboardUrl } = this.props; - const links = []; - - if ( docsUrl ) { - links.push( { - key: 'docs', - href: docsUrl, - text: __( 'Docs', 'woocommerce' ), - } ); - } - if ( supportUrl ) { - links.push( { - key: 'support', - href: supportUrl, - text: __( 'Get support', 'woocommerce' ), - } ); - } - if ( settingsUrl ) { - links.push( { - key: 'settings', - href: settingsUrl, - text: __( 'Settings', 'woocommerce' ), - } ); - } - if ( dashboardUrl ) { - links.push( { - key: 'dashboard', - href: dashboardUrl, - text: __( 'Dashboard', 'woocommerce' ), - } ); - } - - return ( -
        - { links.map( ( link ) => { - return ( -
      • - - { link.text } - -
      • - ); - } ) } -
      - ); - } - - onLinkClick( link ) { - const { name } = this.props; - recordEvent( 'marketing_installed_options', { name, link: link.key } ); - } - - onActivateClick() { - const { activatePlugin, name } = this.props; - recordEvent( 'marketing_installed_activate', { name } ); - activatePlugin(); - } - - onFinishSetupClick() { - const { name } = this.props; - recordEvent( 'marketing_installed_finish_setup', { name } ); - } - - getActivateButton() { - const { isLoading } = this.props; - - return ( - - ); - } - - getFinishSetupButton() { - return ( - - ); - } - - render() { - const { name, description, status, slug } = this.props; - let actions = null; - - switch ( status ) { - case 'installed': - actions = this.getActivateButton(); - break; - case 'activated': - actions = this.getFinishSetupButton(); - break; - case 'configured': - actions = this.getLinks(); - break; - } - - return ( -
      - -
      -
      -

      { name }

      - { status === 'configured' || ( -

      - { description } -

      - ) } -
      -
      - { actions } -
      -
      -
      - ); - } -} - -InstalledExtensionRow.defaultProps = { - isLoading: false, -}; - -InstalledExtensionRow.propTypes = { - name: PropTypes.string.isRequired, - slug: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - status: PropTypes.string.isRequired, - settingsUrl: PropTypes.string, - docsUrl: PropTypes.string, - supportUrl: PropTypes.string, - dashboardUrl: PropTypes.string, - activatePlugin: PropTypes.func.isRequired, -}; - -export default InstalledExtensionRow; diff --git a/plugins/woocommerce-admin/client/marketing/overview/installed-extensions/style.scss b/plugins/woocommerce-admin/client/marketing/overview/installed-extensions/style.scss deleted file mode 100644 index 8968f5315fe..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview/installed-extensions/style.scss +++ /dev/null @@ -1,91 +0,0 @@ -.woocommerce-marketing-installed-extensions-card { - &__item { - display: flex; - align-items: center; - padding: 18px 24px; - - h4 { - font-weight: 400; - font-size: 16px; - margin: 0 0 5px; - color: $gray-900; - } - - p { - color: $gray-700; - margin: 0; - } - } - - &__item:not(:last-child) { - border-bottom: 1px solid $gray-200; - } - - &__item-text-and-actions { - display: flex; - flex-wrap: wrap; - align-items: center; - flex-grow: 2; - min-width: 0; // Flexbox truncated text fix - - @include breakpoint( '>600px' ) { - flex-wrap: nowrap; - } - } - - &__item-actions { - @include breakpoint( '>600px' ) { - text-align: right; - white-space: nowrap; - padding-left: 25px; - } - } - - &__item-description { - @include breakpoint( '>960px' ) { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 550px; - } - } - - &__item-links { - margin: 0; - padding: 0; - - li { - display: inline-block; - margin: 0 25px 0 0; - - @include breakpoint( '>600px' ) { - margin: 0 0 0 30px; - } - } - - a { - font-weight: 600; - color: var(--wp-admin-theme-color) !important; - text-decoration: none; - font-size: 14px; - } - } - - .woocommerce-admin-marketing-product-icon { - align-self: flex-start; - margin-right: 14px; - margin-top: 2px; // Align top of image with text - } - - &__item-text { - min-width: 0; // Flexbox truncated text fix - flex-grow: 2; - margin: 0 0 10px; - width: 100%; - - @include breakpoint( '>600px' ) { - margin: 0; - width: auto; - } - } -} diff --git a/plugins/woocommerce-admin/client/marketing/overview/section-slot/index.ts b/plugins/woocommerce-admin/client/marketing/overview/section-slot/index.ts deleted file mode 100644 index 38d712f793e..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview/section-slot/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './section-slot'; -export * from './utils'; diff --git a/plugins/woocommerce-admin/client/marketing/overview/section-slot/section-slot.tsx b/plugins/woocommerce-admin/client/marketing/overview/section-slot/section-slot.tsx deleted file mode 100644 index f06fc18292e..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview/section-slot/section-slot.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/** - * External dependencies - */ -import { useSlot } from '@woocommerce/experimental'; -import classnames from 'classnames'; - -/** - * Internal dependencies - */ -import { - EXPERIMENTAL_WC_MARKETING_OVERVIEW_SECTION_SLOT_NAME, - WooMarketingOverviewSection, -} from './utils'; - -export const MarketingOverviewSectionSlot = ( { - className, -}: { - className: string; -} ) => { - const slot = useSlot( - EXPERIMENTAL_WC_MARKETING_OVERVIEW_SECTION_SLOT_NAME - ); - const hasFills = Boolean( slot?.fills?.length ); - - if ( ! hasFills ) { - return null; - } - return ( -
      - -
      - ); -}; diff --git a/plugins/woocommerce-admin/client/marketing/overview/section-slot/utils.tsx b/plugins/woocommerce-admin/client/marketing/overview/section-slot/utils.tsx deleted file mode 100644 index 5c9a9de9ea0..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview/section-slot/utils.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/** - * External dependencies - */ -import { Slot, Fill } from '@wordpress/components'; -import { - createOrderedChildren, - sortFillsByOrder, -} from '@woocommerce/components'; - -export const EXPERIMENTAL_WC_MARKETING_OVERVIEW_SECTION_SLOT_NAME = - 'experimental_woocommerce_marketing_overview_section'; -/** - * Create a Fill for extensions to add a section to the Marketing Overview page. - * - * @slotFill WooMarketingOverviewSection - * @scope woocommerce-admin - * @example - * const MySection = () => ( - * - *
      - *
      - * Slotfill goes in here! - *
      - *
      - *
      - * ); - * - * registerPlugin( 'my-extension', { - * render: MySection, - * scope: 'woocommerce-admin', - * } ); - * @param {Object} param0 - * @param {Array} param0.children - Node children. - * @param {Array} param0.order - Node order. - */ -export const WooMarketingOverviewSection = ( { - children, - order = 1, -}: { - children: React.ReactNode; - order?: number; -} ) => { - return ( - - { ( fillProps: Fill.Props ) => { - return createOrderedChildren( children, order, fillProps ); - } } - - ); -}; - -WooMarketingOverviewSection.Slot = ( { - fillProps, -}: { - fillProps?: Slot.Props; -} ) => ( - - { sortFillsByOrder } - -); diff --git a/plugins/woocommerce-admin/client/marketing/overview/style.scss b/plugins/woocommerce-admin/client/marketing/overview/style.scss deleted file mode 100644 index eaf6fdbbf4f..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview/style.scss +++ /dev/null @@ -1,8 +0,0 @@ -.woocommerce-marketing-overview { - max-width: 1032px; - margin: 0 auto; - - .components-card { - margin-bottom: 24px; - } -} diff --git a/plugins/woocommerce-admin/client/marketing/overview/welcome-card/images/welcome.svg b/plugins/woocommerce-admin/client/marketing/overview/welcome-card/images/welcome.svg deleted file mode 100644 index ae8d8a7a75f..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview/welcome-card/images/welcome.svg +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/plugins/woocommerce-admin/client/marketing/overview/welcome-card/index.js b/plugins/woocommerce-admin/client/marketing/overview/welcome-card/index.js deleted file mode 100644 index 99ed62c0b46..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview/welcome-card/index.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Button, Card, CardBody } from '@wordpress/components'; -import CrossIcon from 'gridicons/dist/cross'; -import { compose } from '@wordpress/compose'; -import { withDispatch, withSelect } from '@wordpress/data'; -import PropTypes from 'prop-types'; -import { OPTIONS_STORE_NAME } from '@woocommerce/data'; -import { recordEvent } from '@woocommerce/tracks'; - -/** - * Internal dependencies - */ -import './style.scss'; -import WelcomeImage from './images/welcome.svg'; - -const WelcomeCard = ( { isHidden, updateOptions } ) => { - const hide = () => { - updateOptions( { - woocommerce_marketing_overview_welcome_hidden: 'yes', - } ); - recordEvent( 'marketing_intro_close', {} ); - }; - - if ( isHidden ) { - return null; - } - - return ( - - - - -

      - { __( - 'Grow your customer base and increase your sales with marketing tools built for WooCommerce', - 'woocommerce' - ) } -

      -
      -
      - ); -}; - -WelcomeCard.propTypes = { - /** - * Whether the card is hidden. - */ - isHidden: PropTypes.bool.isRequired, - /** - * updateOptions function. - */ - updateOptions: PropTypes.func.isRequired, -}; - -// named export -export { WelcomeCard }; - -// default export -export default compose( - withSelect( ( select ) => { - const { getOption, isOptionsUpdating } = select( OPTIONS_STORE_NAME ); - const isUpdateRequesting = isOptionsUpdating(); - - return { - isHidden: - getOption( 'woocommerce_marketing_overview_welcome_hidden' ) === - 'yes' || isUpdateRequesting, - }; - } ), - withDispatch( ( dispatch ) => { - const { updateOptions } = dispatch( OPTIONS_STORE_NAME ); - return { - updateOptions, - }; - } ) -)( WelcomeCard ); diff --git a/plugins/woocommerce-admin/client/marketing/overview/welcome-card/style.scss b/plugins/woocommerce-admin/client/marketing/overview/welcome-card/style.scss deleted file mode 100644 index 3a6549a89ee..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview/welcome-card/style.scss +++ /dev/null @@ -1,76 +0,0 @@ -.woocommerce-marketing-overview-welcome-card { - position: relative; - - .components-card__body { - display: flex; - justify-content: center; - align-items: center; - flex-wrap: wrap; - padding: 22px; - - @include breakpoint( '>600px' ) { - flex-wrap: nowrap; - } - - @include breakpoint( '>960px' ) { - padding: 32px 108px; - } - } - - &__hide-button { - display: flex; - align-items: center; - padding: 8px; - margin: 0; - border: none; - background: none; - color: $gray-700; - overflow: hidden; - border-radius: 4px; - position: absolute; - top: 10px; - right: 10px; - - // Ensure that even SVG icons that don't include the .dashicon class are colored. - svg { - fill: currentColor; - outline: none; - } - - &:not(:disabled):not([aria-disabled='true']):not(.is-default):hover { - @include button-style__hover; - } - - &:not(:disabled):not([aria-disabled='true']):not(.is-default):active { - @include button-style__active; - } - - &[aria-disabled='true']:focus, - &:disabled:focus { - box-shadow: none; - } - } - - h3 { - font-size: 20px; - line-height: 26px; - font-weight: normal; - text-align: center; - margin: 1em 0 0; - - @include breakpoint( '>600px' ) { - text-align: left; - margin: 0 0 0 20px; - } - - @include breakpoint( '>960px' ) { - font-size: 24px; - line-height: 32px; - } - } - - img { - width: 231px; - flex: none; - } -} diff --git a/plugins/woocommerce-admin/client/marketing/overview/welcome-card/test/index.js b/plugins/woocommerce-admin/client/marketing/overview/welcome-card/test/index.js deleted file mode 100644 index b737ff6f204..00000000000 --- a/plugins/woocommerce-admin/client/marketing/overview/welcome-card/test/index.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * External dependencies - */ -import { recordEvent } from '@woocommerce/tracks'; -import { render, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { createElement } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { WelcomeCard } from '../index.js'; - -jest.mock( '@woocommerce/tracks' ); -jest.mock( '@woocommerce/settings' ); - -describe( 'WelcomeCard hide button', () => { - it( 'should record an event when clicked', () => { - const { getByRole } = render( - - ); - - userEvent.click( getByRole( 'button', { name: 'Hide' } ) ); - - expect( recordEvent ).toHaveBeenCalledTimes( 1 ); - expect( recordEvent ).toHaveBeenCalledWith( - 'marketing_intro_close', - {} - ); - } ); - - it( 'should update option when clicked', async () => { - const mockUpdateOptions = jest.fn(); - const { getByRole } = render( - - ); - - userEvent.click( getByRole( 'button' ) ); - - await waitFor( () => - expect( mockUpdateOptions ).toHaveBeenCalledTimes( 1 ) - ); - expect( mockUpdateOptions ).toHaveBeenCalledWith( { - woocommerce_marketing_overview_welcome_hidden: 'yes', - } ); - } ); -} ); - -describe( 'Component visibility can be toggled', () => { - it( 'WelcomeCard should be visible if isHidden is false', () => { - const { getByRole } = render( - - ); - - expect( getByRole( 'button' ) ).toBeInTheDocument(); - } ); - - it( 'WelcomeCard should be hidden if isHidden is true', () => { - const { queryByRole } = render( - - ); - - expect( queryByRole( 'button' ) ).toBeNull(); - } ); -} ); diff --git a/plugins/woocommerce-admin/client/marketing/types/CampaignType.ts b/plugins/woocommerce-admin/client/marketing/types/CampaignType.ts new file mode 100644 index 00000000000..49e0418038a --- /dev/null +++ b/plugins/woocommerce-admin/client/marketing/types/CampaignType.ts @@ -0,0 +1,9 @@ +export type CampaignType = { + id: string; + icon: string; + name: string; + description: string; + createUrl: string; + channelName: string; + channelSlug: string; +}; diff --git a/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx b/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx index babef4e8679..1425d26fab0 100644 --- a/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx +++ b/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx @@ -193,7 +193,14 @@ const PaymentRecommendations: React.FC = () => { ), before: ( - + ), }; } ); diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.tsx index 107b6ee6755..113e8283443 100644 --- a/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.tsx +++ b/plugins/woocommerce-admin/client/products/fields/attribute-term-input-field/create-attribute-term-modal.tsx @@ -11,7 +11,7 @@ import { import { useState } from '@wordpress/element'; import { useDispatch } from '@wordpress/data'; import { cleanForSlug } from '@wordpress/url'; -import { Form, FormContext, FormErrors } from '@woocommerce/components'; +import { Form, FormContextType, FormErrors } from '@woocommerce/components'; import { recordEvent } from '@woocommerce/tracks'; import { EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME, @@ -118,7 +118,7 @@ export const CreateAttributeTermModal: React.FC< isValidForm, setValue, values, - }: FormContext< QueryProductAttribute > ) => { + }: FormContextType< QueryProductAttribute > ) => { const nameInputProps = getInputProps< string >( 'name' ); return ( <> diff --git a/plugins/woocommerce-admin/client/products/fills/more-menu-items/classic-editor-menu-item.tsx b/plugins/woocommerce-admin/client/products/fills/more-menu-items/classic-editor-menu-item.tsx new file mode 100644 index 00000000000..bc7da76ecd8 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fills/more-menu-items/classic-editor-menu-item.tsx @@ -0,0 +1,75 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { getAdminLink } from '@woocommerce/settings'; +import { OPTIONS_STORE_NAME } from '@woocommerce/data'; +import { MenuItem } from '@wordpress/components'; +import { + ALLOW_TRACKING_OPTION_NAME, + STORE_KEY as CES_STORE_KEY, +} from '@woocommerce/customer-effort-score'; +import { NEW_PRODUCT_MANAGEMENT_ENABLED_OPTION_NAME } from '@woocommerce/product-editor'; + +/** + * Internal dependencies + */ +import { ClassicEditorIcon } from '../../images/classic-editor-icon'; + +export const ClassicEditorMenuItem = ( { + onClose, + productId, +}: { + productId: number; + onClose: () => void; +} ) => { + const { showProductMVPFeedbackModal } = useDispatch( CES_STORE_KEY ); + const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); + + const { allowTracking, resolving: isLoading } = useSelect( ( select ) => { + const { getOption, hasFinishedResolution } = + select( OPTIONS_STORE_NAME ); + + const allowTrackingOption = + getOption( ALLOW_TRACKING_OPTION_NAME ) || 'no'; + + const resolving = ! hasFinishedResolution( 'getOption', [ + ALLOW_TRACKING_OPTION_NAME, + ] ); + + return { + allowTracking: allowTrackingOption === 'yes', + resolving, + }; + } ); + + const classicEditorUrl = productId + ? getAdminLink( `post.php?post=${ productId }&action=edit` ) + : getAdminLink( 'post-new.php?post_type=product' ); + + if ( isLoading ) { + return null; + } + + return ( + { + if ( allowTracking ) { + updateOptions( { + [ NEW_PRODUCT_MANAGEMENT_ENABLED_OPTION_NAME ]: 'no', + } ); + showProductMVPFeedbackModal(); + onClose(); + } else { + window.location.href = classicEditorUrl; + onClose(); + } + } } + icon={ } + iconPosition="right" + > + { __( 'Use the classic editor', 'woocommerce' ) } + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/fills/more-menu-items/feedback-menu-item.tsx b/plugins/woocommerce-admin/client/products/fills/more-menu-items/feedback-menu-item.tsx new file mode 100644 index 00000000000..2cf6730a914 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fills/more-menu-items/feedback-menu-item.tsx @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { MenuItem } from '@wordpress/components'; +import { useDispatch } from '@wordpress/data'; +import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score'; + +/** + * Internal dependencies + */ +import { FeedbackIcon } from '../../images/feedback-icon'; + +export const FeedbackMenuItem = ( { onClose }: { onClose: () => void } ) => { + const { showCesModal } = useDispatch( CES_STORE_KEY ); + + return ( + { + showCesModal( + { + action: 'new_product', + title: __( + "How's your experience with the product editor?", + 'woocommerce' + ), + firstQuestion: __( + 'The product editing screen is easy to use', + 'woocommerce' + ), + secondQuestion: __( + "The product editing screen's functionality meets my needs", + 'woocommerce' + ), + }, + { shouldShowComments: () => true }, + { + type: 'snackbar', + icon: 🌟, + } + ); + onClose(); + } } + icon={ } + iconPosition="right" + > + { __( 'Share feedback', 'woocommerce' ) } + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/fills/more-menu-items/index.ts b/plugins/woocommerce-admin/client/products/fills/more-menu-items/index.ts new file mode 100644 index 00000000000..c180f511b00 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fills/more-menu-items/index.ts @@ -0,0 +1,2 @@ +export * from './feedback-menu-item'; +export * from './classic-editor-menu-item'; diff --git a/plugins/woocommerce-admin/client/products/fills/product-block-editor-fills.tsx b/plugins/woocommerce-admin/client/products/fills/product-block-editor-fills.tsx new file mode 100644 index 00000000000..4f56f880ba1 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fills/product-block-editor-fills.tsx @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import { + __experimentalProductMVPFeedbackModalContainer as ProductMVPFeedbackModalContainer, + __experimentalWooProductMoreMenuItem as WooProductMoreMenuItem, +} from '@woocommerce/product-editor'; +import { registerPlugin } from '@wordpress/plugins'; +import { WooHeaderItem } from '@woocommerce/admin-layout'; + +// 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 { + FeedbackMenuItem, + ClassicEditorMenuItem, +} from '../fills/more-menu-items'; + +const MoreMenuFill = ( { onClose }: { onClose: () => void } ) => { + const [ id ] = useEntityProp( 'postType', 'product', 'id' ); + + return ( + <> + + + + ); +}; + +const ProductHeaderFill = () => { + const [ id ] = useEntityProp( 'postType', 'product', 'id' ); + + return ; +}; + +registerPlugin( 'wc-admin-more-menu', { + // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. + scope: 'woocommerce-product-block-editor', + render: () => ( + <> + + { ( { onClose }: { onClose: () => void } ) => ( + + ) } + + + + + + ), +} ); diff --git a/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts b/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts new file mode 100644 index 00000000000..b0f03086810 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts @@ -0,0 +1,42 @@ +/** + * External dependencies + */ +import { AUTO_DRAFT_NAME } from '@woocommerce/product-editor'; +import { Product } from '@woocommerce/data'; +import { useDispatch, resolveSelect } from '@wordpress/data'; + +import { useEffect, useState } from '@wordpress/element'; + +export function useProductEntityRecord( + productId: string | undefined +): Product | undefined { + const { saveEntityRecord } = useDispatch( 'core' ); + const [ product, setProduct ] = useState< Product | undefined >( + undefined + ); + + useEffect( () => { + const getRecordPromise: Promise< Product > = productId + ? resolveSelect( 'core' ).getEntityRecord< Product >( + 'postType', + 'product', + Number.parseInt( productId, 10 ) + ) + : // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Incorrect types. + ( saveEntityRecord( 'postType', 'product', { + title: AUTO_DRAFT_NAME, + status: 'auto-draft', + } ) as Promise< Product > ); + getRecordPromise + .then( ( autoDraftProduct: Product ) => { + setProduct( autoDraftProduct ); + } ) + .catch( ( e ) => { + setProduct( undefined ); + throw e; + } ); + }, [ productId ] ); + + return product; +} diff --git a/plugins/woocommerce-admin/client/products/layout/product-form-footer.tsx b/plugins/woocommerce-admin/client/products/layout/product-form-footer.tsx index f04a269bbec..cdf8ba18e1d 100644 --- a/plugins/woocommerce-admin/client/products/layout/product-form-footer.tsx +++ b/plugins/woocommerce-admin/client/products/layout/product-form-footer.tsx @@ -1,8 +1,10 @@ /** - * Internal dependencies + * External dependencies */ -import { ProductMVPCESFooter } from '~/customer-effort-score-tracks/product-mvp-ces-footer'; -import { ProductMVPFeedbackModalContainer } from '~/customer-effort-score-tracks/product-mvp-feedback-modal-container'; +import { + __experimentalProductMVPCESFooter as ProductMVPCESFooter, + __experimentalProductMVPFeedbackModalContainer as ProductMVPFeedbackModalContainer, +} from '@woocommerce/product-editor'; export const ProductFormFooter: React.FC = () => { return ( diff --git a/plugins/woocommerce-admin/client/products/product-block-page.scss b/plugins/woocommerce-admin/client/products/product-block-page.scss new file mode 100644 index 00000000000..f24be5be5e1 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/product-block-page.scss @@ -0,0 +1,66 @@ +.woocommerce-product-block-editor { + .components-input-control { + &__prefix { + margin-left: $gap-smaller; + } + + &__suffix { + margin-right: $gap-smaller; + } + } + + .components-currency-control { + .components-input-control__prefix { + color: $gray-700; + } + + .components-input-control__input { + text-align: right; + } + } + + .components-summary-control { + width: 100%; + min-height: calc($gap-larger * 3); + background-color: $white; + box-sizing: border-box; + border: 1px solid #757575; + border-radius: 2px; + padding: $gap-smaller; + margin: 0; + appearance: textarea; + resize: vertical; + overflow: hidden; + + &:focus { + box-shadow: inset 0 0 0 1px var(--wp-admin-theme-color-darker-10, --wp-admin-theme-color); + border-color: var(--wp-admin-theme-color-darker-10, --wp-admin-theme-color); + } + } + + .woocommerce-product-form { + &__custom-label-input { + display: flex; + flex-direction: column; + + label { + display: block; + margin-bottom: $gap-smaller; + } + } + + &__optional-input { + color: $gray-700; + } + } + + .wp-block-columns { + gap: $gap-large; + } + + .wp-block-woocommerce-product-section { + > .block-editor-inner-blocks > .block-editor-block-list__layout > .wp-block:not(:first-child) { + margin-top: $gap-large; + } + } +} diff --git a/plugins/woocommerce-admin/client/products/product-form-actions.tsx b/plugins/woocommerce-admin/client/products/product-form-actions.tsx index c6f613421e2..e7f44375646 100644 --- a/plugins/woocommerce-admin/client/products/product-form-actions.tsx +++ b/plugins/woocommerce-admin/client/products/product-form-actions.tsx @@ -13,13 +13,15 @@ import { chevronDown, check, Icon } from '@wordpress/icons'; import { registerPlugin } from '@wordpress/plugins'; import { WooHeaderItem } from '@woocommerce/admin-layout'; import { useFormContext } from '@woocommerce/components'; +import { useCustomerEffortScoreExitPageTracker } from '@woocommerce/customer-effort-score'; import { preventLeavingProductForm, __experimentalUseProductHelper as useProductHelper, + __experimentalUseProductMVPCESFooter as useProductMVPCESFooter, } from '@woocommerce/product-editor'; import { Product } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; -import { navigateTo } from '@woocommerce/navigation'; +import { navigateTo, useConfirmUnsavedChanges } from '@woocommerce/navigation'; import { useSelect } from '@wordpress/data'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore No types for this exist yet. @@ -29,10 +31,7 @@ import { store } from '@wordpress/viewport'; /** * Internal dependencies */ -import usePreventLeavingPage from '~/hooks/usePreventLeavingPage'; import './product-form-actions.scss'; -import { useProductMVPCESFooter } from '~/customer-effort-score-tracks/use-product-mvp-ces-footer'; -import { useCustomerEffortScoreExitPageTracker } from '~/customer-effort-score-tracks/use-customer-effort-score-exit-page-tracker'; export const ProductFormActions: React.FC = () => { const { @@ -50,7 +49,7 @@ export const ProductFormActions: React.FC = () => { const { isDirty, isValidForm, values, resetForm } = useFormContext< Product >(); - usePreventLeavingPage( isDirty, preventLeavingProductForm ); + useConfirmUnsavedChanges( isDirty, preventLeavingProductForm ); useCustomerEffortScoreExitPageTracker( ! values.id ? 'new_product' : 'editing_new_product', diff --git a/plugins/woocommerce-admin/client/products/product-more-menu.tsx b/plugins/woocommerce-admin/client/products/product-more-menu.tsx index ac83cc3d9d1..887a77138da 100644 --- a/plugins/woocommerce-admin/client/products/product-more-menu.tsx +++ b/plugins/woocommerce-admin/client/products/product-more-menu.tsx @@ -2,51 +2,39 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { DropdownMenu, MenuItem } from '@wordpress/components'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { DropdownMenu } from '@wordpress/components'; +import { useFormContext } from '@woocommerce/components'; +import { useSelect } from '@wordpress/data'; import { WooHeaderItem } from '@woocommerce/admin-layout'; -import { getAdminLink } from '@woocommerce/settings'; import { moreVertical } from '@wordpress/icons'; import { OPTIONS_STORE_NAME, Product } from '@woocommerce/data'; -import { useFormContext } from '@woocommerce/components'; +import { ALLOW_TRACKING_OPTION_NAME } from '@woocommerce/customer-effort-score'; /** * Internal dependencies */ -import { ClassicEditorIcon } from './images/classic-editor-icon'; -import { FeedbackIcon } from './images/feedback-icon'; -import { STORE_KEY as CES_STORE_KEY } from '~/customer-effort-score-tracks/data/constants'; -import { NEW_PRODUCT_MANAGEMENT } from '~/customer-effort-score-tracks/product-mvp-ces-footer'; -import { ALLOW_TRACKING_OPTION_NAME } from '~/customer-effort-score-tracks/constants'; + +import { + FeedbackMenuItem, + ClassicEditorMenuItem, +} from './fills/more-menu-items'; + import './product-more-menu.scss'; export const ProductMoreMenu = () => { const { values } = useFormContext< Product >(); - const { showCesModal, showProductMVPFeedbackModal } = - useDispatch( CES_STORE_KEY ); - const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); - - const { allowTracking, resolving: isLoading } = useSelect( ( select ) => { - const { getOption, hasFinishedResolution } = - select( OPTIONS_STORE_NAME ); - - const allowTrackingOption = - getOption( ALLOW_TRACKING_OPTION_NAME ) || 'no'; + const { resolving: isLoading } = useSelect( ( select ) => { + const { hasFinishedResolution } = select( OPTIONS_STORE_NAME ); const resolving = ! hasFinishedResolution( 'getOption', [ ALLOW_TRACKING_OPTION_NAME, ] ); return { - allowTracking: allowTrackingOption === 'yes', resolving, }; } ); - const classEditorUrl = values.id - ? getAdminLink( `post.php?post=${ values.id }&action=edit` ) - : getAdminLink( 'post-new.php?post_type=product' ); - if ( isLoading ) { return null; } @@ -61,55 +49,11 @@ export const ProductMoreMenu = () => { > { ( { onClose } ) => ( <> - { - showCesModal( - { - action: 'new_product', - title: __( - "How's your experience with the product editor?", - 'woocommerce' - ), - firstQuestion: __( - 'The product editing screen is easy to use', - 'woocommerce' - ), - secondQuestion: __( - "The product editing screen's functionality meets my needs", - 'woocommerce' - ), - }, - { shouldShowComments: () => true }, - { - type: 'snackbar', - icon: 🌟, - } - ); - onClose(); - } } - icon={ } - iconPosition="right" - > - { __( 'Share feedback', 'woocommerce' ) } - - { - if ( allowTracking ) { - updateOptions( { - [ NEW_PRODUCT_MANAGEMENT ]: 'no', - } ); - showProductMVPFeedbackModal(); - onClose(); - } else { - window.location.href = classEditorUrl; - onClose(); - } - } } - icon={ } - iconPosition="right" - > - { __( 'Use the classic editor', 'woocommerce' ) } - + + ) } diff --git a/plugins/woocommerce-admin/client/products/product-page.tsx b/plugins/woocommerce-admin/client/products/product-page.tsx index ce20414283e..6b414978f5a 100644 --- a/plugins/woocommerce-admin/client/products/product-page.tsx +++ b/plugins/woocommerce-admin/client/products/product-page.tsx @@ -3,71 +3,36 @@ */ import { __experimentalEditor as Editor, - AUTO_DRAFT_NAME, + ProductEditorSettings, } from '@woocommerce/product-editor'; -import { Product } from '@woocommerce/data'; -import { useDispatch, useSelect, select as WPSelect } from '@wordpress/data'; -import { useEffect, useState } from '@wordpress/element'; + import { Spinner } from '@wordpress/components'; import { useParams } from 'react-router-dom'; /** * Internal dependencies */ +import { useProductEntityRecord } from './hooks/use-product-entity-record'; + import './product-page.scss'; +import './product-block-page.scss'; +import './fills/product-block-editor-fills'; -const ProductEditor: React.FC< { product: Product | undefined } > = ( { - product, -} ) => { - if ( ! product ) { - return ; - } - - return ; -}; - -const EditProductEditor: React.FC< { productId: string } > = ( { - productId, -} ) => { - const { product } = useSelect( ( select: typeof WPSelect ) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Missing types. - const { getEditedEntityRecord } = select( 'core' ); - - return { - product: getEditedEntityRecord( 'postType', 'product', productId ), - }; - } ); - - return ; -}; - -const AddProductEditor = () => { - const { saveEntityRecord } = useDispatch( 'core' ); - const [ product, setProduct ] = useState< Product | undefined >( - undefined - ); - - useEffect( () => { - saveEntityRecord( 'postType', 'product', { - title: AUTO_DRAFT_NAME, - status: 'auto-draft', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Incorrect types. - } ).then( ( autoDraftProduct: Product ) => { - setProduct( autoDraftProduct ); - } ); - }, [] ); - - return ; -}; +declare const productBlockEditorSettings: ProductEditorSettings; export default function ProductPage() { const { productId } = useParams(); - if ( productId ) { - return ; + const product = useProductEntityRecord( productId ); + + if ( ! product?.id ) { + return ; } - return ; + return ( + + ); } diff --git a/plugins/woocommerce-admin/client/products/product-variation-form-actions.tsx b/plugins/woocommerce-admin/client/products/product-variation-form-actions.tsx index 280aeaaa369..7a4b258ba6a 100644 --- a/plugins/woocommerce-admin/client/products/product-variation-form-actions.tsx +++ b/plugins/woocommerce-admin/client/products/product-variation-form-actions.tsx @@ -11,6 +11,7 @@ import { import { preventLeavingProductForm } from '@woocommerce/product-editor'; import { registerPlugin } from '@wordpress/plugins'; import { useDispatch } from '@wordpress/data'; +import { useConfirmUnsavedChanges } from '@woocommerce/navigation'; import { useFormContext } from '@woocommerce/components'; import { useParams } from 'react-router-dom'; import { useState } from '@wordpress/element'; @@ -18,7 +19,6 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import usePreventLeavingPage from '~/hooks/usePreventLeavingPage'; import './product-form-actions.scss'; export const ProductVariationFormActions: React.FC = () => { @@ -31,7 +31,7 @@ export const ProductVariationFormActions: React.FC = () => { const { createNotice } = useDispatch( 'core/notices' ); const [ isSaving, setIsSaving ] = useState( false ); - usePreventLeavingPage( isDirty, preventLeavingProductForm ); + useConfirmUnsavedChanges( isDirty, preventLeavingProductForm ); const onSave = async () => { setIsSaving( true ); diff --git a/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx b/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx index 6c10650170d..611738e032d 100644 --- a/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx +++ b/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx @@ -3,7 +3,7 @@ */ import { render, waitFor, screen, within } from '@testing-library/react'; import { Fragment } from '@wordpress/element'; -import { Form, FormContext } from '@woocommerce/components'; +import { Form, FormContextType } from '@woocommerce/components'; import { Product } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; import userEvent from '@testing-library/user-event'; @@ -23,21 +23,12 @@ const onDraftCES = jest.fn().mockResolvedValue( {} ); jest.mock( '@wordpress/plugins', () => ( { registerPlugin: jest.fn() } ) ); -jest.mock( '@woocommerce/data', () => ( { - ...jest.requireActual( '@woocommerce/data' ), +jest.mock( '@wordpress/data', () => ( { + ...jest.requireActual( '@wordpress/data' ), useDispatch: jest.fn().mockReturnValue( { updateOptions: jest.fn() } ), useSelect: jest.fn().mockReturnValue( { productCESAction: 'hide' } ), } ) ); jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) ); -jest.mock( - '~/customer-effort-score-tracks/use-product-mvp-ces-footer', - () => ( { - useProductMVPCESFooter: () => ( { - onPublish: onPublishCES, - onSaveDraft: onDraftCES, - } ), - } ) -); jest.mock( '@woocommerce/admin-layout', () => ( { WooHeaderItem: ( props: { children: () => React.ReactElement } ) => ( { props.children() } @@ -51,15 +42,19 @@ jest.mock( '@woocommerce/product-editor', () => { copyProductWithStatus, deleteProductAndRedirect, } ), + __experimentalUseProductMVPCESFooter: () => ( { + onPublish: onPublishCES, + onSaveDraft: onDraftCES, + } ), }; } ); -jest.mock( '~/hooks/usePreventLeavingPage' ); -jest.mock( - '~/customer-effort-score-tracks/use-customer-effort-score-exit-page-tracker', - () => ( { - useCustomerEffortScoreExitPageTracker: jest.fn(), - } ) -); +jest.mock( '@woocommerce/navigation', () => ( { + ...jest.requireActual( '@woocommerce/navigation' ), + useConfirmUnsavedChanges: jest.fn(), +} ) ); +jest.mock( '@woocommerce/customer-effort-score', () => ( { + useCustomerEffortScoreExitPageTracker: jest.fn(), +} ) ); describe( 'ProductFormActions', () => { beforeEach( () => { @@ -203,7 +198,7 @@ describe( 'ProductFormActions', () => { }; const { queryByText, getByLabelText } = render( > initialValues={ product }> - { ( { getInputProps }: FormContext< Product > ) => { + { ( { getInputProps }: FormContextType< Product > ) => { return ( <> diff --git a/plugins/woocommerce-admin/client/products/tour/product-tour-container.tsx b/plugins/woocommerce-admin/client/products/tour/product-tour-container.tsx index d93f19de3d3..d36d64519b7 100644 --- a/plugins/woocommerce-admin/client/products/tour/product-tour-container.tsx +++ b/plugins/woocommerce-admin/client/products/tour/product-tour-container.tsx @@ -1,9 +1,13 @@ +/** + * External dependencies + */ +import { __experimentalUseProductMVPCESFooter as useProductMVPCESFooter } from '@woocommerce/product-editor'; + /** * Internal dependencies */ import { ProductTour } from './product-tour'; import { ProductTourModal } from './product-tour-modal'; -import { useProductMVPCESFooter } from '~/customer-effort-score-tracks/use-product-mvp-ces-footer'; import { useProductTour } from './use-product-tour'; export const ProductTourContainer: React.FC = () => { diff --git a/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss b/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss index 887d6dc4656..999b3398aa0 100644 --- a/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss +++ b/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss @@ -61,7 +61,7 @@ body.woocommerce-page { .components-button.is-primary { - &:not(:disabled) { + &:not(:disabled):not([aria-disabled='true']):hover { color: $studio-white; } } diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/Item.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/Item.js index 80bdede39d2..25ceb1c4557 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/Item.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/Item.js @@ -31,6 +31,7 @@ export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => { settingsUrl: manageUrl, is_local_partner: isLocalPartner, external_link: externalLink, + transaction_processors: transactionProcessors, } = paymentGateway; const connectSlot = useSlot( @@ -88,6 +89,21 @@ export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => {
      { content }
      + { transactionProcessors && ( +
      + { Object.keys( transactionProcessors ).map( + ( key ) => { + return ( + { + ); + } + ) } +
      + ) }
      { recordEvent( 'tasklist_payment_see_more', {} ); }; - const trackToggle = ( isShow ) => { - recordEvent( 'tasklist_payment_show_toggle', { - toggle: isShow ? 'hide' : 'show', - payment_method_count: - offlineGateways.length + additionalGateways.length, - } ); - }; - if ( query.id && ! currentGateway ) { return ; } @@ -270,16 +261,8 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { { wcPayGateway.length ? ( <> - - { additionalSection } - { offlineSection } - + { additionalSection } + { offlineSection } ) : ( <> diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js index b56e28545ae..08de19aa480 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js @@ -90,7 +90,7 @@ const paymentGatewaySuggestionsWithoutWCPay = paymentGatewaySuggestions.filter( ); describe( 'PaymentGatewaySuggestions', () => { - test( 'should render only WCPay if its suggested', () => { + test( 'should render all payment gateways, including WCPay', () => { const onComplete = jest.fn(); const query = {}; useSelect.mockImplementation( () => ( { @@ -115,7 +115,10 @@ describe( 'PaymentGatewaySuggestions', () => { ( e ) => e.textContent ); - expect( paymentTitles ).toEqual( [] ); + expect( paymentTitles ).toEqual( [ + 'Cash on delivery', + 'Direct bank transfer', + ] ); expect( container @@ -126,7 +129,7 @@ describe( 'PaymentGatewaySuggestions', () => { ).toBe( true ); } ); - test( 'should render all payment gateways if no WCPay', () => { + test( 'should render all payment gateways except WCPay', () => { const onComplete = jest.fn(); const query = {}; useSelect.mockImplementation( () => ( { @@ -299,39 +302,6 @@ describe( 'PaymentGatewaySuggestions', () => { } ); } ); - test( 'should record event correctly when Other payment providers is clicked', () => { - const onComplete = jest.fn(); - const query = {}; - useSelect.mockImplementation( () => ( { - isResolving: false, - getPaymentGateway: jest.fn(), - paymentGatewaySuggestions, - installedPaymentGateways: [], - countryCode: 'US', - } ) ); - - render( - - ); - - fireEvent.click( screen.getByText( 'Other payment providers' ) ); - - // By default it's hidden, so when toggle it shows. - // Second call after "tasklist_payments_options". - expect( - recordEvent.mock.calls[ recordEvent.mock.calls.length - 1 ] - ).toEqual( [ - 'tasklist_payment_show_toggle', - { - toggle: 'show', - payment_method_count: paymentGatewaySuggestions.length - 1, // Minus one for WCPay since it's not counted in "Other payment providers". - }, - ] ); - } ); - test( 'should record event correctly when see more is clicked', () => { const onComplete = jest.fn(); const query = {}; diff --git a/plugins/woocommerce-admin/client/tasks/fills/connect.js b/plugins/woocommerce-admin/client/tasks/fills/connect.js deleted file mode 100644 index 538e76d06fc..00000000000 --- a/plugins/woocommerce-admin/client/tasks/fills/connect.js +++ /dev/null @@ -1,150 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Component } from '@wordpress/element'; -import { compose } from '@wordpress/compose'; -import apiFetch from '@wordpress/api-fetch'; -import { withDispatch, withSelect } from '@wordpress/data'; -import { omit } from 'lodash'; -import { getHistory, getNewPath } from '@woocommerce/navigation'; -import { ONBOARDING_STORE_NAME, WC_ADMIN_NAMESPACE } from '@woocommerce/data'; -import { registerPlugin } from '@wordpress/plugins'; -import { WooOnboardingTask } from '@woocommerce/onboarding'; - -class Connect extends Component { - componentDidMount() { - document.body.classList.add( 'woocommerce-admin-is-loading' ); - const { query } = this.props; - - if ( query.deny === '1' ) { - this.errorMessage( - __( - 'You must click approve to install your extensions and connect to WooCommerce.com', - 'woocommerce' - ) - ); - return; - } - - if ( ! query[ 'wccom-connected' ] || ! query.request_token ) { - this.request(); - return; - } - this.finish(); - } - - baseQuery() { - const { query } = this.props; - const baseQuery = omit( { ...query, page: 'wc-admin' }, [ - 'task', - 'wccom-connected', - 'request_token', - 'deny', - ] ); - return getNewPath( {}, '/', baseQuery ); - } - - errorMessage( - message = __( - 'There was an error connecting to WooCommerce.com. Please try again', - 'woocommerce' - ) - ) { - document.body.classList.remove( 'woocommerce-admin-is-loading' ); - getHistory().push( this.baseQuery() ); - this.props.createNotice( 'error', message ); - } - - async request() { - try { - const connectResponse = await apiFetch( { - path: `${ WC_ADMIN_NAMESPACE }/plugins/request-wccom-connect`, - method: 'POST', - } ); - if ( connectResponse && connectResponse.connectAction ) { - window.location = connectResponse.connectAction; - return; - } - throw new Error(); - } catch ( err ) { - this.errorMessage(); - } - } - - async finish() { - const { onComplete, query } = this.props; - try { - const connectResponse = await apiFetch( { - path: `${ WC_ADMIN_NAMESPACE }/plugins/finish-wccom-connect`, - method: 'POST', - data: { - request_token: query.request_token, - }, - } ); - if ( connectResponse && connectResponse.success ) { - await this.props.updateProfileItems( { - wccom_connected: true, - } ); - if ( ! this.props.isProfileItemsError ) { - this.props.createNotice( - 'success', - __( - 'Store connected to WooCommerce.com and extensions are being installed', - 'woocommerce' - ) - ); - - // @todo Show a notice for when extensions are correctly installed. - - document.body.classList.remove( - 'woocommerce-admin-is-loading' - ); - onComplete(); - } else { - this.errorMessage(); - } - - return; - } - throw new Error(); - } catch ( err ) { - this.errorMessage(); - } - } - - render() { - return null; - } -} - -const ConnectWrapper = compose( - withSelect( ( select ) => { - const { getOnboardingError } = select( ONBOARDING_STORE_NAME ); - - const isProfileItemsError = Boolean( - getOnboardingError( 'updateProfileItems' ) - ); - - return { isProfileItemsError }; - } ), - withDispatch( ( dispatch ) => { - const { createNotice } = dispatch( 'core/notices' ); - const { updateProfileItems } = dispatch( ONBOARDING_STORE_NAME ); - return { - createNotice, - updateProfileItems, - }; - } ) -)( Connect ); - -registerPlugin( 'wc-admin-onboarding-task-connect', { - scope: 'woocommerce-tasks', - render: () => ( - - { ( { onComplete, query } ) => ( - - ) } - - ), -} ); diff --git a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/plugin-banner.scss b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/plugin-banner.scss index aeb41e3fafe..64a78605fab 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/plugin-banner.scss +++ b/plugins/woocommerce-admin/client/tasks/fills/experimental-shipping-recommendation/components/plugin-banner.scss @@ -1,5 +1,4 @@ .woocommerce-task-shipping-recommendation__plugins-install { - display: flex; padding: $gap-large calc($gap + $gap-smallest); border: 1px solid #ddd; border-radius: 3px; @@ -7,19 +6,20 @@ margin-bottom: $gap-large; &.dual { + display: flex; flex-direction: column; - justify-content: space-between; + justify-content: flex-start; width: 285px; - height: 322px; p { margin-top: 0; - margin-bottom: -$gap-smaller; + margin-bottom: 15px; color: $gray-700; } .plugins-install__plugin-banner-image { display: flex; + margin-bottom: $gap-large; img { width: 120px; height: 28px; @@ -28,8 +28,9 @@ } &.single { + display: flex; .plugins-install__list { - max-width: 380px; + max-width: 360px; } } @@ -51,6 +52,10 @@ .woocommerce-task-shipping-recommendations_plugins-buttons { display: flex; justify-content: space-between; + margin-top: $gap-large; + flex-grow: 1; + align-items: flex-end; + button { min-width: 40%; padding-inline: $gap-smaller; diff --git a/plugins/woocommerce-admin/client/tasks/fills/import-products/cards.scss b/plugins/woocommerce-admin/client/tasks/fills/import-products/cards.scss index 715d2bd6eaf..29e15f6cdb8 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/import-products/cards.scss +++ b/plugins/woocommerce-admin/client/tasks/fills/import-products/cards.scss @@ -8,7 +8,9 @@ grid-template-columns: auto auto; grid-column-gap: 16px; grid-row-gap: 24px; - justify-content: start; + // Set it to center here since we only have one import option. + // We can change it to start when we have more import options. + justify-content: center; } .woocommerce-list__item { diff --git a/plugins/woocommerce-admin/client/tasks/fills/import-products/importTypes.tsx b/plugins/woocommerce-admin/client/tasks/fills/import-products/importTypes.tsx index abb15efd783..dd8a3cd77b0 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/import-products/importTypes.tsx +++ b/plugins/woocommerce-admin/client/tasks/fills/import-products/importTypes.tsx @@ -3,10 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import PageIcon from 'gridicons/dist/pages'; -import ReblogIcon from 'gridicons/dist/reblog'; import { getAdminLink } from '@woocommerce/settings'; -import interpolateComponents from '@automattic/interpolate-components'; -import { ExternalLink } from '@wordpress/components'; import { recordEvent } from '@woocommerce/tracks'; export const importTypes = [ @@ -25,32 +22,4 @@ export const importTypes = [ ); }, }, - { - key: 'from-cart2cart' as const, - title: __( 'FROM CART2CART', 'woocommerce' ), - content: interpolateComponents( { - mixedString: __( - 'Migrate all store data like products, customers, and orders in no time with this 3rd party plugin. {{link}}Learn more{{/link}}', - 'woocommerce' - ), - components: { - link: ( - e.preventDefault() } - > - ), - }, - } ), - before: , - onClick: () => { - recordEvent( 'tasklist_add_product', { method: 'migrate' } ); - window - .open( - 'https://woocommerce.com/products/cart2cart/?utm_medium=product', - '_blank' - ) - ?.focus(); - }, - }, ]; diff --git a/plugins/woocommerce-admin/client/tasks/fills/import-products/test/index.tsx b/plugins/woocommerce-admin/client/tasks/fills/import-products/test/index.tsx index 981543421f5..6bf835fea08 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/import-products/test/index.tsx +++ b/plugins/woocommerce-admin/client/tasks/fills/import-products/test/index.tsx @@ -57,24 +57,6 @@ describe( 'Products', () => { ); } ); - test( 'should fire "tasklist_add_product" event when the cart2cart option clicked', async () => { - const { getByRole } = render( ); - - userEvent.click( - getByRole( 'menuitem', { - name: 'FROM CART2CART Migrate all store data like products, customers, and orders in no time with this 3rd party plugin. Learn more (opens in a new tab)', - } ) - ); - await waitFor( () => - expect( recordEvent ).toHaveBeenCalledWith( - 'tasklist_add_product', - { - method: 'migrate', - } - ) - ); - } ); - test( 'should fire "task_completion_time" event when an option clicked', async () => { Object.defineProperty( window, 'performance', { value: { diff --git a/plugins/woocommerce-admin/client/tasks/fills/index.js b/plugins/woocommerce-admin/client/tasks/fills/index.js index e191ba1b103..767469bd3d7 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/index.js @@ -6,7 +6,6 @@ import './PaymentGatewaySuggestions'; import './shipping'; import './Marketing'; import './appearance'; -import './connect'; import './tax'; import './woocommerce-payments'; import './purchase'; diff --git a/plugins/woocommerce-admin/client/tasks/fills/products/footer.tsx b/plugins/woocommerce-admin/client/tasks/fills/products/footer.tsx index 2538012b50b..86dba20973c 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/products/footer.tsx +++ b/plugins/woocommerce-admin/client/tasks/fills/products/footer.tsx @@ -4,7 +4,6 @@ import { __ } from '@wordpress/i18n'; import interpolateComponents from '@automattic/interpolate-components'; import { Text } from '@woocommerce/experimental'; -import { ExternalLink } from '@wordpress/components'; import { Link } from '@woocommerce/components'; import { getAdminLink } from '@woocommerce/settings'; import { recordEvent } from '@woocommerce/tracks'; @@ -25,7 +24,7 @@ const Footer: React.FC = () => { { interpolateComponents( { mixedString: __( - '{{importCSVLink}}Import your products from a CSV file{{/importCSVLink}} or {{_3rdLink}}use a 3rd party migration plugin{{/_3rdLink}}.', + '{{importCSVLink}}Import your products from a CSV file{{/importCSVLink}}.', 'woocommerce' ), components: { @@ -47,20 +46,6 @@ const Footer: React.FC = () => { <> ), - _3rdLink: ( - { - recordEvent( 'tasklist_add_product', { - method: 'migrate', - } ); - recordCompletionTime(); - } } - href="https://woocommerce.com/products/cart2cart/?utm_medium=product" - type="external" - > - <> - - ), }, } ) } diff --git a/plugins/woocommerce-admin/client/tasks/fills/products/test/footer.tsx b/plugins/woocommerce-admin/client/tasks/fills/products/test/footer.tsx index f042aa61be2..b75e20d3107 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/products/test/footer.tsx +++ b/plugins/woocommerce-admin/client/tasks/fills/products/test/footer.tsx @@ -16,9 +16,9 @@ describe( 'Footer', () => { beforeEach( () => { ( recordEvent as jest.Mock ).mockClear(); } ); - it( 'should render footer with two links', () => { + it( 'should render footer with one links', () => { const { queryAllByRole } = render(
      ); - expect( queryAllByRole( 'link' ) ).toHaveLength( 2 ); + expect( queryAllByRole( 'link' ) ).toHaveLength( 1 ); } ); it( 'clicking on import CSV should fire event tasklist_add_product with method:import and task_completion_time', () => { @@ -35,19 +35,4 @@ describe( 'Footer', () => { { task_name: 'products', time: '0-2s' } ); } ); - - it( 'clicking on start blank should fire event tasklist_add_product with method:migrate and task_completion_time', () => { - const { getByText } = render(
      ); - userEvent.click( getByText( 'use a 3rd party migration plugin' ) ); - expect( recordEvent ).toHaveBeenNthCalledWith( - 1, - 'tasklist_add_product', - { method: 'migrate' } - ); - expect( recordEvent ).toHaveBeenNthCalledWith( - 2, - 'task_completion_time', - { task_name: 'products', time: '0-2s' } - ); - } ); } ); diff --git a/plugins/woocommerce-admin/client/tasks/fills/shipping/shipping-providers/shipping-providers.ts b/plugins/woocommerce-admin/client/tasks/fills/shipping/shipping-providers/shipping-providers.ts index 58df653cdbf..10f703ac863 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/shipping/shipping-providers/shipping-providers.ts +++ b/plugins/woocommerce-admin/client/tasks/fills/shipping/shipping-providers/shipping-providers.ts @@ -83,6 +83,7 @@ const providers = { }, Skydropx: { name: 'Skydropx' as const, + slug: 'skydropx-cotizador-y-envios', // https://wordpress.org/plugins/skydropx-cotizador-y-envios/ url: 'https://wordpress.org/plugins/skydropx-cotizador-y-envios/', 'single-partner-layout': SkydropxSinglePartner, }, diff --git a/plugins/woocommerce-admin/client/tasks/fills/steps/location.js b/plugins/woocommerce-admin/client/tasks/fills/steps/location.tsx similarity index 57% rename from plugins/woocommerce-admin/client/tasks/fills/steps/location.js rename to plugins/woocommerce-admin/client/tasks/fills/steps/location.tsx index a4cce788a99..670f5a5016d 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/steps/location.js +++ b/plugins/woocommerce-admin/client/tasks/fills/steps/location.tsx @@ -5,9 +5,9 @@ import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; import { COUNTRIES_STORE_NAME } from '@woocommerce/data'; import { Fragment } from '@wordpress/element'; -import { Form, Spinner } from '@woocommerce/components'; +import { Form, FormContextType, Spinner } from '@woocommerce/components'; import { useSelect } from '@wordpress/data'; - +import { Status, Options } from 'wordpress__notices'; /** * Internal dependencies */ @@ -16,6 +16,42 @@ import { getStoreAddressValidator, } from '../../../dashboard/components/settings/general/store-address'; +type FormValues = { + addressLine1: string; + addressLine2: string; + countryState: string; + city: string; + postCode: string; +}; + +type StoreLocationProps = { + onComplete: ( values: FormValues ) => void; + createNotice: ( + status: Status | undefined, + content: string, + options?: Partial< Options > + ) => void; + isSettingsError: boolean; + isSettingsRequesting: boolean; + buttonText?: string; + updateAndPersistSettingsForGroup: ( + group: string, + data: { + [ key: string ]: unknown; + } & { + general?: { + [ key: string ]: string; + }; + tax?: { + [ key: string ]: string; + }; + } + ) => void; + settings?: { + [ key: string ]: string; + }; +}; + const StoreLocation = ( { onComplete, createNotice, @@ -24,8 +60,8 @@ const StoreLocation = ( { updateAndPersistSettingsForGroup, settings, buttonText = __( 'Continue', 'woocommerce' ), -} ) => { - const { getLocale, hasFinishedResolution } = useSelect( ( select ) => { +}: StoreLocationProps ) => { + const { hasFinishedResolution } = useSelect( ( select ) => { const countryStore = select( COUNTRIES_STORE_NAME ); countryStore.getCountries(); return { @@ -36,7 +72,7 @@ const StoreLocation = ( { countryStore.hasFinishedResolution( 'getCountries' ), }; } ); - const onSubmit = async ( values ) => { + const onSubmit = async ( values: FormValues ) => { await updateAndPersistSettingsForGroup( 'general', { general: { ...settings, @@ -62,26 +98,17 @@ const StoreLocation = ( { }; const getInitialValues = () => { - const { - woocommerce_store_address: storeAddress, - woocommerce_store_address_2: storeAddress2, - woocommerce_store_city: storeCity, - woocommerce_default_country: defaultCountry, - woocommerce_store_postcode: storePostcode, - } = settings; - return { - addressLine1: storeAddress || '', - addressLine2: storeAddress2 || '', - city: storeCity || '', - countryState: defaultCountry || '', - postCode: storePostcode || '', + addressLine1: settings?.woocommerce_store_address || '', + addressLine2: settings?.woocommerce_store_address_2 || '', + city: settings?.woocommerce_store_city || '', + countryState: settings?.woocommerce_default_country || '', + postCode: settings?.woocommerce_store_postcode || '', }; }; - const validate = ( values ) => { - const locale = getLocale( values.countryState ); - const validator = getStoreAddressValidator( locale ); + const validate = ( values: FormValues ) => { + const validator = getStoreAddressValidator(); return validator( values ); }; @@ -95,9 +122,14 @@ const StoreLocation = ( { onSubmit={ onSubmit } validate={ validate } > - { ( { getInputProps, handleSubmit, setValue } ) => ( + { ( { + getInputProps, + handleSubmit, + setValue, + }: FormContextType< FormValues > ) => ( diff --git a/plugins/woocommerce-admin/client/tasks/fills/tax/avalara/card.tsx b/plugins/woocommerce-admin/client/tasks/fills/tax/avalara/card.tsx index 264b29e29f3..d88b1c114aa 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/tax/avalara/card.tsx +++ b/plugins/woocommerce-admin/client/tasks/fills/tax/avalara/card.tsx @@ -46,7 +46,7 @@ export const Card: React.FC< TaxChildProps > = ( { task } ) => { actionText={ avalaraActivated ? __( 'Continue setup', 'woocommerce' ) - : __( 'Enable & set up', 'woocommerce' ) + : __( 'Download', 'woocommerce' ) } onClick={ () => { recordEvent( 'tasklist_tax_select_option', { diff --git a/plugins/woocommerce-admin/client/tasks/fills/woocommerce-payments.js b/plugins/woocommerce-admin/client/tasks/fills/woocommerce-payments.tsx similarity index 87% rename from plugins/woocommerce-admin/client/tasks/fills/woocommerce-payments.js rename to plugins/woocommerce-admin/client/tasks/fills/woocommerce-payments.tsx index b41112cab10..c5bf53e71cc 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/woocommerce-payments.js +++ b/plugins/woocommerce-admin/client/tasks/fills/woocommerce-payments.tsx @@ -22,7 +22,13 @@ const WoocommercePaymentsTaskItem = () => { return ( - { ( { defaultTaskItem: DefaultTaskItem } ) => ( + { ( { + defaultTaskItem: DefaultTaskItem, + }: { + defaultTaskItem: ( props: { + onClick: () => void; + } ) => JSX.Element; + } ) => ( { @@ -41,6 +47,7 @@ const WoocommercePaymentsTaskItem = () => { }; registerPlugin( 'woocommerce-admin-task-wcpay', { + // @ts-expect-error scope is not defined in the type definition but it is a valid property scope: 'woocommerce-tasks', render: WoocommercePaymentsTaskItem, } ); @@ -85,6 +92,7 @@ const WoocommercePaymentsTaskPage = () => ( ); registerPlugin( 'woocommerce-admin-task-wcpay-page', { + // @ts-expect-error scope is not defined in the type definition but it is a valid property scope: 'woocommerce-tasks', render: WoocommercePaymentsTaskPage, } ); diff --git a/plugins/woocommerce-admin/client/two-column-tasks/completed-header.tsx b/plugins/woocommerce-admin/client/two-column-tasks/completed-header.tsx index 91e7623b723..a3e94b33d2c 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/completed-header.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/completed-header.tsx @@ -10,8 +10,11 @@ import { OPTIONS_STORE_NAME, WCDataSelector, WEEK } from '@woocommerce/data'; import { Button, Card, CardHeader } from '@wordpress/components'; import { Text } from '@woocommerce/experimental'; import { + ADMIN_INSTALL_TIMESTAMP_OPTION_NAME, + ALLOW_TRACKING_OPTION_NAME, CustomerFeedbackModal, CustomerFeedbackSimple, + SHOWN_FOR_ACTIONS_OPTION_NAME, } from '@woocommerce/customer-effort-score'; import { __ } from '@wordpress/i18n'; @@ -27,11 +30,7 @@ type TaskListCompletedHeaderProps = { customerEffortScore: boolean; }; -const ADMIN_INSTALL_TIMESTAMP_OPTION_NAME = - 'woocommerce_admin_install_timestamp'; -const SHOWN_FOR_ACTIONS_OPTION_NAME = 'woocommerce_ces_shown_for_actions'; const CUSTOMER_EFFORT_SCORE_ACTION = 'store_setup'; -const ALLOW_TRACKING_OPTION_NAME = 'woocommerce_allow_tracking'; function getStoreAgeInWeeks( adminInstallTimestamp: number ) { if ( adminInstallTimestamp === 0 ) { diff --git a/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx b/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx index edc274633fb..86df930ced4 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/test/task-list.test.tsx @@ -32,6 +32,12 @@ jest.mock( '../task-headers', () => ( { required: () =>
      required_header
      , completed: () =>
      completed_header
      , } ) ); +jest.mock( '@woocommerce/data', () => ( { + ...jest.requireActual( '@woocommerce/data' ), + useUserPreferences: jest.fn().mockReturnValue( { + updateUserPreferences: jest.fn(), + } ), +} ) ); const tasks: { [ key: string ]: TaskType[] } = { setup: [ diff --git a/plugins/woocommerce-admin/client/typings/global.d.ts b/plugins/woocommerce-admin/client/typings/global.d.ts index 18d3d3c6d02..88b1a9564e8 100644 --- a/plugins/woocommerce-admin/client/typings/global.d.ts +++ b/plugins/woocommerce-admin/client/typings/global.d.ts @@ -9,7 +9,7 @@ declare global { wcAdminFeatures: { 'activity-panels': boolean; analytics: boolean; - 'block-editor-feature-enabled': boolean; + 'product-block-editor': boolean; coupons: boolean; 'customer-effort-score-tracks': boolean; homescreen: boolean; diff --git a/plugins/woocommerce-admin/client/utils/url-helpers.ts b/plugins/woocommerce-admin/client/utils/url-helpers.ts index 2a4de62a627..6be812527bf 100644 --- a/plugins/woocommerce-admin/client/utils/url-helpers.ts +++ b/plugins/woocommerce-admin/client/utils/url-helpers.ts @@ -1,7 +1,7 @@ /** * Extracts all segments from the path query param as a string array. * - * @param path The query path param + * @param path The query path param * @return The list of segments from the path */ export function getSegmentsFromPath( path?: string ): string[] { diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/order-tracking/exit-order-page.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/order-tracking/exit-order-page.ts index 5a4768de0e2..78f5af5febc 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/order-tracking/exit-order-page.ts +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/order-tracking/exit-order-page.ts @@ -1,7 +1,11 @@ +/** + * External dependencies + */ +import { addCustomerEffortScoreExitPageListener } from '@woocommerce/customer-effort-score'; + /** * Internal dependencies */ -import { addCustomerEffortScoreExitPageListener } from '~/customer-effort-score-tracks/customer-effort-score-exit-page'; import { staticFormDataToObject } from '~/utils/static-form-helper'; type FormElements = { diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/product-import-tracking/product-import-tracking.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/product-import-tracking/product-import-tracking.ts index 2ad37d9a56e..9db01727cd7 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/product-import-tracking/product-import-tracking.ts +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/product-import-tracking/product-import-tracking.ts @@ -1,15 +1,11 @@ /** * External dependencies */ -import { getQuery } from '@woocommerce/navigation'; - -/** - * Internal dependencies - */ import { addExitPage, removeExitPage, -} from '~/customer-effort-score-tracks/customer-effort-score-exit-page'; +} from '@woocommerce/customer-effort-score'; +import { getQuery } from '@woocommerce/navigation'; const ACTION_NAME = 'import_products'; diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts index b78d2405d34..38712440139 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts @@ -1,13 +1,13 @@ /** * External dependencies */ +import { addCustomerEffortScoreExitPageListener } from '@woocommerce/customer-effort-score'; import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies */ import { waitUntilElementIsPresent } from './utils'; -import { addCustomerEffortScoreExitPageListener } from '~/customer-effort-score-tracks/customer-effort-score-exit-page'; /** * Get the product data. @@ -18,6 +18,11 @@ import { addCustomerEffortScoreExitPageListener } from '~/customer-effort-score- const isElementVisible = ( element: HTMLElement ) => ! ( window.getComputedStyle( element ).display === 'none' ); +const getProductType = () => { + return ( document.querySelector( '#product-type' ) as HTMLInputElement ) + ?.value; +}; + const getProductData = () => { const isBlockEditor = document.querySelectorAll( '.block-editor' ).length > 0; @@ -50,9 +55,7 @@ const getProductData = () => { const productData = { product_id: ( document.querySelector( '#post_ID' ) as HTMLInputElement ) ?.value, - product_type: ( - document.querySelector( '#product-type' ) as HTMLInputElement - )?.value, + product_type: getProductType(), is_downloadable: ( document.querySelector( '#_downloadable' ) as HTMLInputElement )?.checked @@ -135,7 +138,7 @@ const getProductData = () => { /** * Get the publish date as a string. * - * @param prefix Prefix for date element selectors. + * @param prefix Prefix for date element selectors. * @return string */ const getPublishDate = ( prefix = '' ) => { @@ -186,8 +189,8 @@ const getPublishingWidgetData = () => { /** * Prefix all object keys with a string. * - * @param obj Object to create keys from. - * @param prefix Prefix used before all keys. + * @param obj Object to create keys from. + * @param prefix Prefix used before all keys. * @return object */ const prefixObjectKeys = ( @@ -199,6 +202,94 @@ const prefixObjectKeys = ( ); }; +/** + * Gets the tab name for a tab element. + * + * @param tab Tab element to get slug for. + * @return string + */ +const getTabName = ( tab: Element ) => { + const optionsSuffix = '_options'; + + const optionsClassNames = Array.from( tab.classList ).filter( + ( className ) => className.endsWith( optionsSuffix ) + ); + + if ( optionsClassNames.length > 0 ) { + const className = optionsClassNames[ 0 ]; + + return className.slice( 0, -optionsSuffix.length ); + } + + return ''; +}; + +/** + * Gets additional data associated with a product tab click. + * + * @param tabName The name of the tab to get data for. + * @return object + */ +const getDataForProductTabClickEvent = ( tabName: string ) => { + const data: Record< string, boolean | string > = {}; + + data.product_type = getProductType(); + + if ( tabName === 'inventory' ) { + data.is_store_stock_management_enabled = + document.querySelector( '#_manage_stock' ) !== null; + } + + return data; +}; + +/** + * Initializes the product tabs Tracks events. + */ +const initProductTabsTracks = () => { + const tabs = document.querySelectorAll( '.product_data_tabs > li' ); + + tabs.forEach( ( tab ) => { + const tabName = getTabName( tab ); + + tab.querySelector( 'a' )?.addEventListener( 'click', () => { + recordEvent( 'product_tab_click', { + product_tab: tabName, + ...getDataForProductTabClickEvent( tabName ), + } ); + } ); + } ); +}; + +/** + * Initializes the inventory tab Tracks events. + */ +const initInventoryTabTracks = () => { + document + .querySelector( '#_manage_stock' ) + ?.addEventListener( 'click', ( event ) => { + recordEvent( 'product_manage_stock_click', { + is_enabled: ( event.target as HTMLInputElement )?.checked, + } ); + } ); + + document + .querySelector( '#_manage_stock_disabled' ) + ?.addEventListener( 'click', () => { + recordEvent( + 'product_manage_stock_disabled_store_settings_link_click' + ); + } ); + + document + .querySelector( '#inventory_product_data .notice a' ) + ?.addEventListener( 'click', () => { + recordEvent( + 'product_inventory_variations_notice_learn_more_click' + ); + } ); +}; + /** * Initialize all product screen tracks. */ @@ -413,7 +504,14 @@ export const initProductScreenTracks = () => { document .querySelector( '.save_attributes' ) - ?.addEventListener( 'click', () => { + ?.addEventListener( 'click', ( event ) => { + if ( + event.target instanceof Element && + event.target.classList.contains( 'disabled' ) + ) { + // skip in case the button is disabled + return; + } const newAttributesCount = document.querySelectorAll( '.woocommerce_attribute' ).length; @@ -465,6 +563,9 @@ export const initProductScreenTracks = () => { recordEvent( 'product_view_product_dismiss', getProductData() ); } ); } ); + + initProductTabsTracks(); + initInventoryTabTracks(); }; export function addExitPageListener( pageId: string ) { diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/settings-tracking/exit-setting-page.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/settings-tracking/exit-setting-page.ts index 61cfdee8a0f..b6152f2351f 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/settings-tracking/exit-setting-page.ts +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/settings-tracking/exit-setting-page.ts @@ -1,7 +1,11 @@ +/** + * External dependencies + */ +import { addCustomerEffortScoreExitPageListener } from '@woocommerce/customer-effort-score'; + /** * Internal dependencies */ -import { addCustomerEffortScoreExitPageListener } from '~/customer-effort-score-tracks/customer-effort-score-exit-page'; import { staticFormDataToObject } from '~/utils/static-form-helper'; type FormElements = { diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/variable-product-tour/index.tsx b/plugins/woocommerce-admin/client/wp-admin-scripts/variable-product-tour/index.tsx new file mode 100644 index 00000000000..da8894416eb --- /dev/null +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/variable-product-tour/index.tsx @@ -0,0 +1,13 @@ +/** + * External dependencies + */ +import { render } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { VariableProductTour } from '../../guided-tours/variable-product-tour'; + +const root = document.createElement( 'div' ); +root.setAttribute( 'id', 'variable-product-tour-root' ); +render( , document.body.appendChild( root ) ); diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 01aa5cbe395..f07b86d7a2c 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -33,16 +33,19 @@ "lint:js-pre-commit": "eslint --ext=js,ts,tsx", "prepack": "pnpm install && pnpm run lint && pnpm run test && cross-env WC_ADMIN_PHASE=core pnpm run build", "packages:fix:textdomain": "node ./bin/package-update-textdomain.js", - "packages:watch": "cross-env WC_ADMIN_PHASE=development pnpm run:packages -- start --parallel", - "run:packages": "pnpm run --filter ../../packages/js/", - "start": "cross-env WC_ADMIN_PHASE=development pnpm -w run build --filter='./packages/js/*' && cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && concurrently \"cross-env WC_ADMIN_PHASE=development webpack --watch\" \"cross-env WC_ADMIN_PHASE=development pnpm packages:watch\"", + "packages:build": "pnpm run:packages -- build", + "packages:watch": "pnpm run:packages -- start", + "run:packages": "pnpm run --parallel --filter='../../packages/js/**'", + "prestart": "pnpm packages:build && cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config", + "start": "concurrently \"cross-env WC_ADMIN_PHASE=development webpack --watch\" \"cross-env WC_ADMIN_PHASE=development pnpm packages:watch\"", + "start:hot": "pnpm prestart && concurrently \"cross-env HOT=true WC_ADMIN_PHASE=development webpack serve\" \"cross-env WC_ADMIN_PHASE=development pnpm packages:watch\"", "test-staged": "pnpm run test:client --bail --findRelatedTests", "test:client": "jest --config client/jest.config.js", "test:debug": "node --inspect-brk ./node_modules/.bin/jest --config client/jest.config.js --watch --runInBand --no-cache", "test:help": "wp-scripts test-unit-js --help", "test:update-snapshots": "pnpm run test:client --updateSnapshot && pnpm run --filter @woocommerce/components test:update-snapshots", "test:watch": "pnpm run test:client --watch", - "ts:check": "tsc --build ./tsconfig.json --pretty", + "ts:check": "tsc --project tsconfig.json --pretty", "ts:check:watch": "npm run ts:check -- --watch" }, "dependencies": { @@ -109,6 +112,7 @@ "@babel/preset-typescript": "^7.16.7", "@babel/runtime": "^7.17.2", "@octokit/core": "^3.5.1", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "@testing-library/dom": "^8.11.3", "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^12.1.3", @@ -203,6 +207,7 @@ "prop-types": "^15.8.1", "puppeteer": "^2.0.0", "raw-loader": "^4.0.2", + "react-refresh": "^0.14.0", "readline-sync": "^1.4.10", "replace": "^1.2.1", "rimraf": "^3.0.2", @@ -212,11 +217,12 @@ "style-loader": "^0.23.1", "stylelint": "^14.5.3", "ts-jest": "^27.1.3", - "typescript": "^4.8.3", + "typescript": "^4.9.5", "url-loader": "^1.1.2", "webpack": "^5.70.0", "webpack-bundle-analyzer": "^3.9.0", "webpack-cli": "^4.9.2", + "webpack-dev-server": "^4.11.1", "webpack-fix-style-only-entries": "^0.6.1", "webpack-merge": "^5.8.0", "webpack-rtl-plugin": "^2.0.0" diff --git a/plugins/woocommerce-admin/tsconfig.json b/plugins/woocommerce-admin/tsconfig.json index ea312614914..7cb4a609425 100644 --- a/plugins/woocommerce-admin/tsconfig.json +++ b/plugins/woocommerce-admin/tsconfig.json @@ -20,12 +20,7 @@ "~/*": [ "*" ] }, "rootDir": "client", - "typeRoots": [ - "./client/typings", - "./node_modules/@types" - ] + "typeRoots": [ "./client/typings", "./node_modules/@types" ] }, - "include": [ - "./client/**/*" - ] + "include": [ "./client/**/*" ] } diff --git a/plugins/woocommerce-admin/webpack.config.js b/plugins/woocommerce-admin/webpack.config.js index 47642993727..5b535c7e8c8 100644 --- a/plugins/woocommerce-admin/webpack.config.js +++ b/plugins/woocommerce-admin/webpack.config.js @@ -2,6 +2,7 @@ * External dependencies */ const { get } = require( 'lodash' ); +const fs = require( 'fs' ); const path = require( 'path' ); const CopyWebpackPlugin = require( 'copy-webpack-plugin' ); const CustomTemplatedPathPlugin = require( '@wordpress/custom-templated-path-webpack-plugin' ); @@ -9,6 +10,7 @@ const BundleAnalyzerPlugin = require( 'webpack-bundle-analyzer' ).BundleAnalyzerPlugin; const MomentTimezoneDataPlugin = require( 'moment-timezone-data-webpack-plugin' ); const ForkTsCheckerWebpackPlugin = require( 'fork-ts-checker-webpack-plugin' ); +const ReactRefreshWebpackPlugin = require( '@pmmmwh/react-refresh-webpack-plugin' ); /** * Internal dependencies @@ -21,6 +23,8 @@ const WooCommerceDependencyExtractionWebpackPlugin = require( '../../packages/js const NODE_ENV = process.env.NODE_ENV || 'development'; const WC_ADMIN_PHASE = process.env.WC_ADMIN_PHASE || 'development'; +const isHot = Boolean( process.env.HOT ); +const isProduction = NODE_ENV === 'production'; const wcAdminPackages = [ 'admin-layout', @@ -62,6 +66,7 @@ const wpAdminScripts = [ 'settings-tracking', 'order-tracking', 'product-import-tracking', + 'variable-product-tour', ]; const getEntryPoints = () => { const entryPoints = { @@ -101,6 +106,7 @@ const webpackConfig = { uniqueName: '__wcAdmin_webpackJsonp', }, module: { + parser: styleConfig.parser, rules: [ { test: /\.(t|j)sx?$/, @@ -129,7 +135,12 @@ const webpackConfig = { ], [ '@babel/preset-typescript' ], ], - plugins: [ '@babel/plugin-proposal-class-properties' ], + plugins: [ + '@babel/plugin-proposal-class-properties', + ! isProduction && + isHot && + require.resolve( 'react-refresh/babel' ), + ].filter( Boolean ), }, }, }, @@ -184,6 +195,19 @@ const webpackConfig = { } ) ), } ), + // Get all product editor blocks so they can be loaded via JSON. + new CopyWebpackPlugin( { + patterns: [ + { + from: '../../packages/js/product-editor/build/blocks', + to: './product-editor/blocks', + }, + ], + } ), + + // React Fast Refresh. + ! isProduction && isHot && new ReactRefreshWebpackPlugin(), + // We reuse this Webpack setup for Storybook, where we need to disable dependency extraction. ! process.env.STORYBOOK && new WooCommerceDependencyExtractionWebpackPlugin( { @@ -217,13 +241,29 @@ const webpackConfig = { }, }, }; +if ( ! isProduction || WC_ADMIN_PHASE === 'development' ) { + // Set default sourcemap mode if it wasn't set by WP_DEVTOOL. + webpackConfig.devtool = webpackConfig.devtool || 'source-map'; -// Use the source map if we're in development mode, . -if ( - webpackConfig.mode === 'development' || - WC_ADMIN_PHASE === 'development' -) { - webpackConfig.devtool = process.env.SOURCEMAP || 'source-map'; + if ( isHot ) { + // Add dev server config + // Copied from https://github.com/WordPress/gutenberg/blob/05bea6dd5c6198b0287c41a401d36a06b48831eb/packages/scripts/config/webpack.config.js#L312-L326 + webpackConfig.devServer = { + devMiddleware: { + writeToDisk: true, + }, + allowedHosts: 'auto', + host: 'localhost', + port: 8887, + proxy: { + '/build': { + pathRewrite: { + '^/build': '', + }, + }, + }, + }; + } } module.exports = webpackConfig; diff --git a/plugins/woocommerce-beta-tester/api/rest-api-filters/rest-api-filters.php b/plugins/woocommerce-beta-tester/api/rest-api-filters/rest-api-filters.php index 19b06c9a722..c2cefb6158e 100644 --- a/plugins/woocommerce-beta-tester/api/rest-api-filters/rest-api-filters.php +++ b/plugins/woocommerce-beta-tester/api/rest-api-filters/rest-api-filters.php @@ -1,109 +1,139 @@ 'POST', - 'args' => array( - 'endpoint' => array( - 'description' => 'Rest API endpoint.', - 'type' => 'string', - 'required' => true, - 'sanitize_callback' => 'sanitize_text_field', - ), - 'dot_notation' => array( - 'description' => 'Dot notation of the target field.', - 'type' => 'string', - 'required' => true, - 'sanitize_callback' => 'sanitize_text_field', - ), - 'replacement' => array( - 'description' => 'Replacement value for the target field.', - 'type' => 'string', - 'required' => true, - 'sanitize_callback' => 'sanitize_text_field', - ), - ), - ) + '/rest-api-filters', + array( WCA_Test_Helper_Rest_Api_Filters::class, 'create' ), + array( + 'methods' => 'POST', + 'args' => array( + 'endpoint' => array( + 'description' => 'Rest API endpoint.', + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => 'sanitize_text_field', + ), + 'dot_notation' => array( + 'description' => 'Dot notation of the target field.', + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => 'sanitize_text_field', + ), + 'replacement' => array( + 'description' => 'Replacement value for the target field.', + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ) ); register_woocommerce_admin_test_helper_rest_route( - '/rest-api-filters', - [ WCA_Test_Helper_Rest_Api_Filters::class, 'delete' ], - array( - 'methods' => 'DELETE', - 'args' => array( - 'index' => array( - 'description' => 'Rest API endpoint.', - 'type' => 'integer', - 'required' => true, - ), - ), - ) + '/rest-api-filters', + array( WCA_Test_Helper_Rest_Api_Filters::class, 'delete' ), + array( + 'methods' => 'DELETE', + 'args' => array( + 'index' => array( + 'description' => 'Rest API endpoint.', + 'type' => 'integer', + 'required' => true, + ), + ), + ) ); register_woocommerce_admin_test_helper_rest_route( - '/rest-api-filters/(?P\d+)/toggle', - [ WCA_Test_Helper_Rest_Api_Filters::class, 'toggle' ], - array( - 'methods' => 'POST', - ) + '/rest-api-filters/(?P\d+)/toggle', + array( WCA_Test_Helper_Rest_Api_Filters::class, 'toggle' ), + array( + 'methods' => 'POST', + ) ); class WCA_Test_Helper_Rest_Api_Filters { - const WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION = 'wc-admin-test-helper-rest-api-filters'; + const WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION = 'wc-admin-test-helper-rest-api-filters'; - public static function create( $request ) { - $endpoint = $request->get_param('endpoint'); - $dot_notation = $request->get_param('dot_notation'); - $replacement = $request->get_param('replacement'); + /** + * Create a filter. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public static function create( $request ) { + $endpoint = $request->get_param( 'endpoint' ); + $dot_notation = $request->get_param( 'dot_notation' ); + $replacement = $request->get_param( 'replacement' ); - self::update( - function ( $filters ) use ( - $endpoint, - $dot_notation, - $replacement - ) { - $filters[] = array( - 'endpoint' => $endpoint, - 'dot_notation' => $dot_notation, - 'replacement' => filter_var( $replacement, FILTER_VALIDATE_BOOLEAN ), - 'enabled' => true, - ); - return $filters; - } - ); - return new WP_REST_RESPONSE(null, 204); - } + if ( 'false' === $replacement ) { + $replacement = false; + } elseif ( 'true' === $replacement ) { + $replacement = true; + } - public static function update( callable $callback ) { - $filters = get_option(self::WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION, array()); - $filters = $callback( $filters ); - return update_option(self::WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION, $filters); - } + self::update( + function ( $filters ) use ( + $endpoint, + $dot_notation, + $replacement + ) { + $filters[] = array( + 'endpoint' => $endpoint, + 'dot_notation' => $dot_notation, + 'replacement' => $replacement, + 'enabled' => true, + ); + return $filters; + } + ); + return new WP_REST_RESPONSE( null, 204 ); + } - public static function delete( $request ) { - self::update( - function ( $filters ) use ( $request ) { - array_splice($filters, $request->get_param('index'), 1); - return $filters; - } - ); + /** + * Update the filters. + * + * @param callable $callback Callback to update the filters. + * @return bool + */ + public static function update( callable $callback ) { + $filters = get_option( self::WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION, array() ); + $filters = $callback( $filters ); + return update_option( self::WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION, $filters ); + } - return new WP_REST_RESPONSE(null, 204); - } + /** + * Delete a filter. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public static function delete( $request ) { + self::update( + function ( $filters ) use ( $request ) { + array_splice( $filters, $request->get_param( 'index' ), 1 ); + return $filters; + } + ); - public static function toggle( $request ) { - self::update( - function ( $filters ) use ( $request ) { - $index = $request->get_param('index'); - $filters[$index]['enabled'] = !$filters[$index]['enabled']; - return $filters; - } - ); - return new WP_REST_RESPONSE(null, 204); - } -} \ No newline at end of file + return new WP_REST_RESPONSE( null, 204 ); + } + + /** + * Toggle a filter on or off. + * + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public static function toggle( $request ) { + self::update( + function ( $filters ) use ( $request ) { + $index = $request->get_param( 'index' ); + $filters[ $index ]['enabled'] = ! $filters[ $index ]['enabled']; + return $filters; + } + ); + return new WP_REST_RESPONSE( null, 204 ); + } +} diff --git a/plugins/woocommerce-beta-tester/changelog/fix-rest-api-filter b/plugins/woocommerce-beta-tester/changelog/fix-rest-api-filter new file mode 100644 index 00000000000..91c2f4fd978 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/fix-rest-api-filter @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix rest api filter to allow any strings in replacement diff --git a/plugins/woocommerce-beta-tester/changelog/fix-test-helper-option-modal b/plugins/woocommerce-beta-tester/changelog/fix-test-helper-option-modal new file mode 100644 index 00000000000..ae1bfdb42bc --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/fix-test-helper-option-modal @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Adjust option modal so that longer option names are fully shown. diff --git a/plugins/woocommerce-beta-tester/changelog/fix-typescript-incremental-builds b/plugins/woocommerce-beta-tester/changelog/fix-typescript-incremental-builds new file mode 100644 index 00000000000..f2bdc9a96ae --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/fix-typescript-incremental-builds @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: TypeScript build change + + diff --git a/plugins/woocommerce-beta-tester/changelog/fix-typescript-package-isolation b/plugins/woocommerce-beta-tester/changelog/fix-typescript-package-isolation new file mode 100644 index 00000000000..2d087939231 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/fix-typescript-package-isolation @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Configuration change only + + diff --git a/plugins/woocommerce-beta-tester/changelog/update-test-helper-features-list b/plugins/woocommerce-beta-tester/changelog/update-test-helper-features-list new file mode 100644 index 00000000000..eacb1ddb3c4 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/update-test-helper-features-list @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Alphabetize the features list, and use toggle controls instead of buttons. diff --git a/plugins/woocommerce-beta-tester/package.json b/plugins/woocommerce-beta-tester/package.json index 88518e4d3e9..e1f33cf7fc4 100644 --- a/plugins/woocommerce-beta-tester/package.json +++ b/plugins/woocommerce-beta-tester/package.json @@ -21,7 +21,7 @@ "eslint": "^8.32.0", "prettier": "npm:wp-prettier@^2.6.2", "ts-loader": "^9.4.1", - "typescript": "^4.8.3", + "typescript": "^4.9.5", "uglify-js": "^3.5.3" }, "dependencies": { diff --git a/plugins/woocommerce-beta-tester/src/features/index.js b/plugins/woocommerce-beta-tester/src/features/index.js index f89defecc1c..667f817de85 100644 --- a/plugins/woocommerce-beta-tester/src/features/index.js +++ b/plugins/woocommerce-beta-tester/src/features/index.js @@ -2,7 +2,7 @@ * External dependencies */ import { useDispatch, useSelect } from '@wordpress/data'; -import { Button } from '@wordpress/components'; +import { Button, ToggleControl } from '@wordpress/components'; /** * Internal dependencies @@ -21,6 +21,8 @@ function Features() { const { toggleFeature, resetModifiedFeatures } = useDispatch( STORE_KEY ); + const sortedFeatureNames = Object.keys( features ).sort(); + return (

      @@ -34,37 +36,21 @@ function Features() { Reset to defaults

      - - - - - - - - - - { Object.keys( features ).map( ( feature_name ) => { - return ( - - - - - - ); - } ) } - -
      Feature NameEnabled?Toggle
      - { feature_name } - { features[ feature_name ].toString() } - -
      +
        + { sortedFeatureNames.map( ( feature_name ) => { + return ( +
      • + { + toggleFeature( feature_name ); + } } + /> +
      • + ); + } ) } +
      ); } diff --git a/plugins/woocommerce-beta-tester/src/index.scss b/plugins/woocommerce-beta-tester/src/index.scss index b051363c9f4..d213c21ec37 100644 --- a/plugins/woocommerce-beta-tester/src/index.scss +++ b/plugins/woocommerce-beta-tester/src/index.scss @@ -50,13 +50,33 @@ } } -.wca-test-helper-option-editor { - width: 100%; - height: 300px; -} +.wca-test-helper-option-modal-content { + display: flex; + flex-direction: column; -.wca-test-helper-edit-btn-save { - float: right; + label { + margin-bottom: 5px; + } + + output, textarea { + margin-bottom: 10px; + } + + #wca-test-helper-option-modal-name { + font-size: 1.2em; + font-weight: bold; + } + + #wca-test-helper-option-editor { + height: 300px; + } + + .wca-test-helper-option-modal-buttons { + display: flex; + justify-content: flex-end; + margin-top: 10px; + gap: 10px; + } } #wc-admin-test-helper-tools, diff --git a/plugins/woocommerce-beta-tester/src/options/OptionEditor.js b/plugins/woocommerce-beta-tester/src/options/OptionEditor.js deleted file mode 100644 index 5784b179fd3..00000000000 --- a/plugins/woocommerce-beta-tester/src/options/OptionEditor.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * External dependencies - */ -import { useEffect, useState } from '@wordpress/element'; -import PropTypes from 'prop-types'; -import { Button } from '@wordpress/components'; - -const OptionEditor = ( props ) => { - const [ value, setValue ] = useState( props.option.content ); - - useEffect( () => { - setValue( props.option.content ); - }, [ props.option ] ); - - const handleChange = ( event ) => { - setValue( event.target.value ); - }; - - const handleSave = () => { - props.onSave( props.option.name, value ); - }; - - return ( - <> - - -
      - - ); -}; - -OptionEditor.propTypes = { - option: PropTypes.object.isRequired, - onSave: PropTypes.func.isRequired, -}; - -export default OptionEditor; diff --git a/plugins/woocommerce-beta-tester/src/options/index.js b/plugins/woocommerce-beta-tester/src/options/index.js index a85a37afcea..c4eae7e908d 100644 --- a/plugins/woocommerce-beta-tester/src/options/index.js +++ b/plugins/woocommerce-beta-tester/src/options/index.js @@ -3,14 +3,14 @@ */ import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; -import { Modal, Notice } from '@wordpress/components'; +import { Notice } from '@wordpress/components'; import { useState } from '@wordpress/element'; /** * Internal dependencies */ import { STORE_KEY } from './data/constants'; -import { default as OptionEditor } from './OptionEditor'; +import { OptionModal } from './option-modal'; import './data'; function shorten( input ) { @@ -131,17 +131,13 @@ function Options( { return ( <> { isEditModalOpen && ( - { setEditModalOpen( false ); } } - > - - + option={ editingOption } + onSave={ handleSaveOption } + /> ) }
      { notice.message.length > 0 && ( @@ -224,12 +220,8 @@ function Options( { export default compose( withSelect( ( select ) => { - const { - getOptions, - getOptionForEditing, - getNotice, - isLoading, - } = select( STORE_KEY ); + const { getOptions, getOptionForEditing, getNotice, isLoading } = + select( STORE_KEY ); const options = getOptions(); const editingOption = getOptionForEditing(); const notice = getNotice(); diff --git a/plugins/woocommerce-beta-tester/src/options/option-modal.js b/plugins/woocommerce-beta-tester/src/options/option-modal.js new file mode 100644 index 00000000000..6bbca0c3567 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/options/option-modal.js @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import { useEffect, useState } from '@wordpress/element'; +import PropTypes from 'prop-types'; +import { Button, Modal } from '@wordpress/components'; + +export const OptionModal = ( props ) => { + const [ value, setValue ] = useState( props.option.content ); + + useEffect( () => { + setValue( props.option.content ); + }, [ props.option ] ); + + const handleChange = ( event ) => { + setValue( event.target.value ); + }; + + const handleSave = () => { + props.onSave( props.option.name, value ); + }; + + return ( + +
      + + + { props.option.name } + + + +
      + + +
      +
      +
      + ); +}; + +OptionModal.propTypes = { + option: PropTypes.object.isRequired, + onRequestClose: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired, +}; diff --git a/plugins/woocommerce/assets/images/icons/external-link.svg b/plugins/woocommerce/assets/images/icons/external-link.svg new file mode 100644 index 00000000000..e021f4dbef9 --- /dev/null +++ b/plugins/woocommerce/assets/images/icons/external-link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/woocommerce/assets/images/icons/global-attributes-icon.svg b/plugins/woocommerce/assets/images/icons/global-attributes-icon.svg index bbb5d8122d7..7e567300f87 100644 --- a/plugins/woocommerce/assets/images/icons/global-attributes-icon.svg +++ b/plugins/woocommerce/assets/images/icons/global-attributes-icon.svg @@ -1,8 +1,8 @@ - - + + - + diff --git a/plugins/woocommerce/assets/images/icons/info.svg b/plugins/woocommerce/assets/images/icons/info.svg new file mode 100644 index 00000000000..f85694e0c03 --- /dev/null +++ b/plugins/woocommerce/assets/images/icons/info.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/plugins/woocommerce/assets/images/onboarding/payoneer.png b/plugins/woocommerce/assets/images/onboarding/payoneer.png new file mode 100644 index 00000000000..4f109263dd3 Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/payoneer.png differ diff --git a/plugins/woocommerce/assets/images/onboarding/zipco.png b/plugins/woocommerce/assets/images/onboarding/zipco.png new file mode 100644 index 00000000000..70e18cd4b69 Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/zipco.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/payoneer.png b/plugins/woocommerce/assets/images/payment_methods/72x72/payoneer.png new file mode 100644 index 00000000000..49df118b1c8 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/payoneer.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/zipco.png b/plugins/woocommerce/assets/images/payment_methods/72x72/zipco.png new file mode 100644 index 00000000000..00867047ef9 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/zipco.png differ diff --git a/plugins/woocommerce/assets/images/product_data/no-variation-arrow.svg b/plugins/woocommerce/assets/images/product_data/no-variation-arrow.svg new file mode 100644 index 00000000000..551d585d6d4 --- /dev/null +++ b/plugins/woocommerce/assets/images/product_data/no-variation-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/woocommerce/assets/images/product_data/no-variation-background-image.svg b/plugins/woocommerce/assets/images/product_data/no-variation-background-image.svg new file mode 100644 index 00000000000..bbb5d8122d7 --- /dev/null +++ b/plugins/woocommerce/assets/images/product_data/no-variation-background-image.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/plugins/woocommerce/assets/images/shipping_partners/check.svg b/plugins/woocommerce/assets/images/shipping_partners/check.svg new file mode 100644 index 00000000000..2ddc7203902 --- /dev/null +++ b/plugins/woocommerce/assets/images/shipping_partners/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/woocommerce/assets/images/shipping_partners/discount.svg b/plugins/woocommerce/assets/images/shipping_partners/discount.svg new file mode 100644 index 00000000000..2855be644bd --- /dev/null +++ b/plugins/woocommerce/assets/images/shipping_partners/discount.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/woocommerce/assets/images/shipping_partners/envia-column.svg b/plugins/woocommerce/assets/images/shipping_partners/envia-column.svg new file mode 100644 index 00000000000..ab9b864ed21 --- /dev/null +++ b/plugins/woocommerce/assets/images/shipping_partners/envia-column.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/shipping_partners/packlink-column.svg b/plugins/woocommerce/assets/images/shipping_partners/packlink-column.svg new file mode 100644 index 00000000000..26e912a6c77 --- /dev/null +++ b/plugins/woocommerce/assets/images/shipping_partners/packlink-column.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/shipping_partners/packlink-row.svg b/plugins/woocommerce/assets/images/shipping_partners/packlink-row.svg new file mode 100644 index 00000000000..31be20af962 --- /dev/null +++ b/plugins/woocommerce/assets/images/shipping_partners/packlink-row.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/shipping_partners/sendcloud-column.svg b/plugins/woocommerce/assets/images/shipping_partners/sendcloud-column.svg new file mode 100644 index 00000000000..5eb02636c50 --- /dev/null +++ b/plugins/woocommerce/assets/images/shipping_partners/sendcloud-column.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/shipping_partners/sendcloud-row.svg b/plugins/woocommerce/assets/images/shipping_partners/sendcloud-row.svg new file mode 100644 index 00000000000..7af5c26fe42 --- /dev/null +++ b/plugins/woocommerce/assets/images/shipping_partners/sendcloud-row.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/shipping_partners/shipstation-column.svg b/plugins/woocommerce/assets/images/shipping_partners/shipstation-column.svg new file mode 100644 index 00000000000..7e4ce249077 --- /dev/null +++ b/plugins/woocommerce/assets/images/shipping_partners/shipstation-column.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/shipping_partners/shipstation-row.svg b/plugins/woocommerce/assets/images/shipping_partners/shipstation-row.svg new file mode 100644 index 00000000000..965fcdb3e8f --- /dev/null +++ b/plugins/woocommerce/assets/images/shipping_partners/shipstation-row.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/shipping_partners/skydropx-column.svg b/plugins/woocommerce/assets/images/shipping_partners/skydropx-column.svg new file mode 100644 index 00000000000..bb7e0f47fdd --- /dev/null +++ b/plugins/woocommerce/assets/images/shipping_partners/skydropx-column.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/shipping_partners/star.svg b/plugins/woocommerce/assets/images/shipping_partners/star.svg new file mode 100644 index 00000000000..2283a6f2cb4 --- /dev/null +++ b/plugins/woocommerce/assets/images/shipping_partners/star.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/shipping_partners/timer.svg b/plugins/woocommerce/assets/images/shipping_partners/timer.svg new file mode 100644 index 00000000000..afb0cdc184b --- /dev/null +++ b/plugins/woocommerce/assets/images/shipping_partners/timer.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/shipping_partners/wcs-column.svg b/plugins/woocommerce/assets/images/shipping_partners/wcs-column.svg new file mode 100644 index 00000000000..57d2056c995 --- /dev/null +++ b/plugins/woocommerce/assets/images/shipping_partners/wcs-column.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/bin/package-update.sh b/plugins/woocommerce/bin/package-update.sh index dd574adfa97..40422b2586d 100755 --- a/plugins/woocommerce/bin/package-update.sh +++ b/plugins/woocommerce/bin/package-update.sh @@ -34,7 +34,7 @@ if [ -z "$SKIP_UPDATE_TEXTDOMAINS" ]; then output 2 "Done!" output 3 "Updating package JS textdomains..." - find ./packages/woocommerce-blocks -iname '*.js' -exec sed -i.bak -e "s/'woo-gutenberg-products-block'/'woocommerce'/g" -e "s/\"woo-gutenberg-products-block\"/\"woocommerce\"/g" {} \; + find ./packages/woocommerce-blocks \( -iname '*.js' -o -iname '*.json' \) -exec sed -i.bak -e "s/'woo-gutenberg-products-block'/'woocommerce'/g" -e "s/\"woo-gutenberg-products-block\"/\"woocommerce\"/g" {} \; find ./packages/woocommerce-blocks -iname '*.js' -exec sed -i.bak -e "s/'woocommerce-admin'/'woocommerce'/g" -e "s/\"woocommerce-admin\"/\"woocommerce\"/g" {} \; fi diff --git a/plugins/woocommerce/changelog/36257-redux b/plugins/woocommerce/changelog/36257-redux deleted file mode 100644 index 84a77791f36..00000000000 --- a/plugins/woocommerce/changelog/36257-redux +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Allow sorting by menu_order in products widget. diff --git a/plugins/woocommerce/changelog/add-35851-tree-control-selection b/plugins/woocommerce/changelog/add-35851-tree-control-selection deleted file mode 100644 index 8ec6ebea260..00000000000 --- a/plugins/woocommerce/changelog/add-35851-tree-control-selection +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix unit test snapshots due to a dependency version change diff --git a/plugins/woocommerce/changelog/add-36413-support-for-cart-checkout-in-declare-compatibility b/plugins/woocommerce/changelog/add-36413-support-for-cart-checkout-in-declare-compatibility deleted file mode 100644 index 7724faa14f5..00000000000 --- a/plugins/woocommerce/changelog/add-36413-support-for-cart-checkout-in-declare-compatibility +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: enhancement - -Add the support for the C&C Blocks in declaring compatibility feature diff --git a/plugins/woocommerce/changelog/add-36661_existing_attribute_layout b/plugins/woocommerce/changelog/add-36661_existing_attribute_layout deleted file mode 100644 index 7ea6d84a2e2..00000000000 --- a/plugins/woocommerce/changelog/add-36661_existing_attribute_layout +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Add existing global attribute layout #36944 diff --git a/plugins/woocommerce/changelog/add-36991 b/plugins/woocommerce/changelog/add-36991 deleted file mode 100644 index 710c3b98efc..00000000000 --- a/plugins/woocommerce/changelog/add-36991 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Create editor skeleton on add/edit product pages diff --git a/plugins/woocommerce/changelog/add-37103 b/plugins/woocommerce/changelog/add-37103 new file mode 100644 index 00000000000..b555a67c8ae --- /dev/null +++ b/plugins/woocommerce/changelog/add-37103 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add summary block diff --git a/plugins/woocommerce/changelog/add-37128 b/plugins/woocommerce/changelog/add-37128 deleted file mode 100644 index ad668d22778..00000000000 --- a/plugins/woocommerce/changelog/add-37128 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add new feature flag for the product edit blocks experience diff --git a/plugins/woocommerce/changelog/add-37148_change_variations_dropdown_visibility b/plugins/woocommerce/changelog/add-37148_change_variations_dropdown_visibility new file mode 100644 index 00000000000..5209f44c4a5 --- /dev/null +++ b/plugins/woocommerce/changelog/add-37148_change_variations_dropdown_visibility @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Change variations dropdown menu visibility diff --git a/plugins/woocommerce/changelog/add-37241-blocks-build b/plugins/woocommerce/changelog/add-37241-blocks-build new file mode 100644 index 00000000000..be52ba4b353 --- /dev/null +++ b/plugins/woocommerce/changelog/add-37241-blocks-build @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add product editor blocks to assets build folder diff --git a/plugins/woocommerce/changelog/add-37249 b/plugins/woocommerce/changelog/add-37249 new file mode 100644 index 00000000000..547df677b1c --- /dev/null +++ b/plugins/woocommerce/changelog/add-37249 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix global button aria-disabled style diff --git a/plugins/woocommerce/changelog/add-37264 b/plugins/woocommerce/changelog/add-37264 new file mode 100644 index 00000000000..e340926e2e7 --- /dev/null +++ b/plugins/woocommerce/changelog/add-37264 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add image configuration to the product block template diff --git a/plugins/woocommerce/changelog/add-37272 b/plugins/woocommerce/changelog/add-37272 new file mode 100644 index 00000000000..b87251a28a6 --- /dev/null +++ b/plugins/woocommerce/changelog/add-37272 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add images block to product editor template diff --git a/plugins/woocommerce/changelog/add-37390 b/plugins/woocommerce/changelog/add-37390 new file mode 100644 index 00000000000..ec8d62081e0 --- /dev/null +++ b/plugins/woocommerce/changelog/add-37390 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add pricing section to the pricing tab diff --git a/plugins/woocommerce/changelog/add-37394 b/plugins/woocommerce/changelog/add-37394 new file mode 100644 index 00000000000..cd427860cb2 --- /dev/null +++ b/plugins/woocommerce/changelog/add-37394 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add product schedule sale pricing block to blocks template definition diff --git a/plugins/woocommerce/changelog/add-37395 b/plugins/woocommerce/changelog/add-37395 new file mode 100644 index 00000000000..0865cfe9c7d --- /dev/null +++ b/plugins/woocommerce/changelog/add-37395 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add tax class to product editor template diff --git a/plugins/woocommerce/changelog/add-admin-layout-package b/plugins/woocommerce/changelog/add-admin-layout-package deleted file mode 100644 index ec1bd147634..00000000000 --- a/plugins/woocommerce/changelog/add-admin-layout-package +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Add @woocommerce/admin-layout package. diff --git a/plugins/woocommerce/changelog/add-charge-sales-tax-37396 b/plugins/woocommerce/changelog/add-charge-sales-tax-37396 new file mode 100644 index 00000000000..55779d97a07 --- /dev/null +++ b/plugins/woocommerce/changelog/add-charge-sales-tax-37396 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adding charge sales tax field to product block editor template. diff --git a/plugins/woocommerce/changelog/add-country-code-remote-payment-suggestion b/plugins/woocommerce/changelog/add-country-code-remote-payment-suggestion new file mode 100644 index 00000000000..5cf797795b3 --- /dev/null +++ b/plugins/woocommerce/changelog/add-country-code-remote-payment-suggestion @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add country query param to payment gateway data sources diff --git a/plugins/woocommerce/changelog/add-customer-object-taxable-address-filter b/plugins/woocommerce/changelog/add-customer-object-taxable-address-filter new file mode 100644 index 00000000000..24dddd173b1 --- /dev/null +++ b/plugins/woocommerce/changelog/add-customer-object-taxable-address-filter @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Dev - Add customer object parameter to taxable address filter diff --git a/plugins/woocommerce/changelog/add-encoding-selector-to-product-importer b/plugins/woocommerce/changelog/add-encoding-selector-to-product-importer deleted file mode 100644 index 7d2631b3da9..00000000000 --- a/plugins/woocommerce/changelog/add-encoding-selector-to-product-importer +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add an encoding selector to the product importer diff --git a/plugins/woocommerce/changelog/add-initial-product-draft-37003 b/plugins/woocommerce/changelog/add-initial-product-draft-37003 deleted file mode 100644 index 5ac217f5914..00000000000 --- a/plugins/woocommerce/changelog/add-initial-product-draft-37003 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Creating product entity in auto-draft status, and adding support for retrieving preexisting products. diff --git a/plugins/woocommerce/changelog/add-legacy-start b/plugins/woocommerce/changelog/add-legacy-start new file mode 100644 index 00000000000..206be55753c --- /dev/null +++ b/plugins/woocommerce/changelog/add-legacy-start @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Add start command to package.json + + diff --git a/plugins/woocommerce/changelog/add-marketing-trackers b/plugins/woocommerce/changelog/add-marketing-trackers deleted file mode 100644 index 44debd914aa..00000000000 --- a/plugins/woocommerce/changelog/add-marketing-trackers +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: add - -Add marketplace suggestions and multichannel marketing information to WC Tracker. diff --git a/plugins/woocommerce/changelog/add-migrate-more-menu-37097 b/plugins/woocommerce/changelog/add-migrate-more-menu-37097 new file mode 100644 index 00000000000..66bbc13845c --- /dev/null +++ b/plugins/woocommerce/changelog/add-migrate-more-menu-37097 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Refactoring product editor more menu items, and using in block editor slot fills. diff --git a/plugins/woocommerce/changelog/add-order_cache b/plugins/woocommerce/changelog/add-order_cache deleted file mode 100644 index 1b800aff214..00000000000 --- a/plugins/woocommerce/changelog/add-order_cache +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add a cache for orders, to use when custom order tables are enabled diff --git a/plugins/woocommerce/changelog/add-tracking-for-loca-pickup b/plugins/woocommerce/changelog/add-tracking-for-loca-pickup deleted file mode 100644 index 20ccbdbeb1f..00000000000 --- a/plugins/woocommerce/changelog/add-tracking-for-loca-pickup +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: tweak - -Add tracking for local pickup method in Checkout diff --git a/plugins/woocommerce/changelog/add-turn-get-tax-location-public b/plugins/woocommerce/changelog/add-turn-get-tax-location-public deleted file mode 100644 index c15b487c25d..00000000000 --- a/plugins/woocommerce/changelog/add-turn-get-tax-location-public +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Make WC_Order::get_tax_location accessible publicly through a wrapper function. diff --git a/plugins/woocommerce/changelog/add-variable-product-type-spotlight b/plugins/woocommerce/changelog/add-variable-product-type-spotlight new file mode 100644 index 00000000000..39318829748 --- /dev/null +++ b/plugins/woocommerce/changelog/add-variable-product-type-spotlight @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Show tour when product type is changed to variable. diff --git a/plugins/woocommerce/changelog/bump-required-php-to-7.3 b/plugins/woocommerce/changelog/bump-required-php-to-7.3 new file mode 100644 index 00000000000..8da66073db3 --- /dev/null +++ b/plugins/woocommerce/changelog/bump-required-php-to-7.3 @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Bump required PHP version to 7.3 and PHPUnit version to 9 diff --git a/plugins/woocommerce/changelog/dev-37117_set_default_quantity_value b/plugins/woocommerce/changelog/dev-37117_set_default_quantity_value new file mode 100644 index 00000000000..131462c4f1d --- /dev/null +++ b/plugins/woocommerce/changelog/dev-37117_set_default_quantity_value @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Set quantity value when stock tracking is enabled diff --git a/plugins/woocommerce/changelog/dev-37147_empty_state_no_variation_created b/plugins/woocommerce/changelog/dev-37147_empty_state_no_variation_created new file mode 100644 index 00000000000..48fb332d92f --- /dev/null +++ b/plugins/woocommerce/changelog/dev-37147_empty_state_no_variation_created @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +New empty state for variations - no variations have been created yet diff --git a/plugins/woocommerce/changelog/dev-clean-analytics-classes b/plugins/woocommerce/changelog/dev-clean-analytics-classes deleted file mode 100644 index fdd8860a366..00000000000 --- a/plugins/woocommerce/changelog/dev-clean-analytics-classes +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: dev -Comment: This is just a dev maintenance, removing duplicated code. - - diff --git a/plugins/woocommerce/changelog/dev-pin-wp-deps-6 b/plugins/woocommerce/changelog/dev-pin-wp-deps-6 deleted file mode 100644 index 551e0919dac..00000000000 --- a/plugins/woocommerce/changelog/dev-pin-wp-deps-6 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Sync @wordpress package versions via syncpack. diff --git a/plugins/woocommerce/changelog/dev-update-eslint-plugin b/plugins/woocommerce/changelog/dev-update-eslint-plugin deleted file mode 100644 index 47894ec9c6c..00000000000 --- a/plugins/woocommerce/changelog/dev-update-eslint-plugin +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Fix lint issues diff --git a/plugins/woocommerce/changelog/e2e-fix-daily-k6-workflow-env-var b/plugins/woocommerce/changelog/e2e-fix-daily-k6-workflow-env-var deleted file mode 100644 index 65522dd597d..00000000000 --- a/plugins/woocommerce/changelog/e2e-fix-daily-k6-workflow-env-var +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Fix the value of `UPDATE_WC` environment variable in the daily k6 performance tests. diff --git a/plugins/woocommerce/changelog/e2e-fix-failing-daily-product-variations b/plugins/woocommerce/changelog/e2e-fix-failing-daily-product-variations new file mode 100644 index 00000000000..c2a615baf27 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-fix-failing-daily-product-variations @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixes a failing e2e test for product variations diff --git a/plugins/woocommerce/changelog/e2e-release-include-drafts b/plugins/woocommerce/changelog/e2e-release-include-drafts deleted file mode 100644 index a2a4d56118d..00000000000 --- a/plugins/woocommerce/changelog/e2e-release-include-drafts +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Support E2E testing of draft releases. diff --git a/plugins/woocommerce/changelog/e2e-release-plugins b/plugins/woocommerce/changelog/e2e-release-plugins new file mode 100644 index 00000000000..3aab6e26e88 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-release-plugins @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Smoke test WooCommerce with plugins installed on releases. diff --git a/plugins/woocommerce/changelog/e2e-setup-flakiness b/plugins/woocommerce/changelog/e2e-setup-flakiness new file mode 100644 index 00000000000..d4cb783d5b0 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-setup-flakiness @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Reduce flakiness on E2E test setup. diff --git a/plugins/woocommerce/changelog/e2e-trunk-merge b/plugins/woocommerce/changelog/e2e-trunk-merge new file mode 100644 index 00000000000..0ada563b534 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-trunk-merge @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Run E2E tests on PR merge to trunk. diff --git a/plugins/woocommerce/changelog/e2e-update-expectedIndustries-variable-name b/plugins/woocommerce/changelog/e2e-update-expectedIndustries-variable-name new file mode 100644 index 00000000000..ee366f755db --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-update-expectedIndustries-variable-name @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Updates an e2e variable name to be more descriptive diff --git a/plugins/woocommerce/changelog/e2e-update-release-e2e-testing b/plugins/woocommerce/changelog/e2e-update-release-e2e-testing deleted file mode 100644 index 6f907635c94..00000000000 --- a/plugins/woocommerce/changelog/e2e-update-release-e2e-testing +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Updates automated release testing workflow to use Playwright diff --git a/plugins/woocommerce/changelog/enhance-textdomain-replacement b/plugins/woocommerce/changelog/enhance-textdomain-replacement new file mode 100644 index 00000000000..4aca7564230 --- /dev/null +++ b/plugins/woocommerce/changelog/enhance-textdomain-replacement @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update textdomain in woocommerce-blocks *.json files to `woocommerce` diff --git a/plugins/woocommerce/changelog/feature-34905-marketing-campaigns-card b/plugins/woocommerce/changelog/feature-34905-marketing-campaigns-card deleted file mode 100644 index d578a5a71e9..00000000000 --- a/plugins/woocommerce/changelog/feature-34905-marketing-campaigns-card +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add Campaigns card into Multichannel Marketing page. diff --git a/plugins/woocommerce/changelog/feature-34907-marketing-exclude-channels b/plugins/woocommerce/changelog/feature-34907-marketing-exclude-channels new file mode 100644 index 00000000000..941984e2475 --- /dev/null +++ b/plugins/woocommerce/changelog/feature-34907-marketing-exclude-channels @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Filter out marketing channels in "Installed extensions" and "Discover more marketing tools" cards. diff --git a/plugins/woocommerce/changelog/feature-37127-marketing-reload-installed-extensions b/plugins/woocommerce/changelog/feature-37127-marketing-reload-installed-extensions new file mode 100644 index 00000000000..932bb4a3710 --- /dev/null +++ b/plugins/woocommerce/changelog/feature-37127-marketing-reload-installed-extensions @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Refetch data for "Installed extensions" card after installing a recommended marketing channel. diff --git a/plugins/woocommerce/changelog/feature-37367-marketing-multichannel-remove-toggle b/plugins/woocommerce/changelog/feature-37367-marketing-multichannel-remove-toggle new file mode 100644 index 00000000000..a36d836c3dc --- /dev/null +++ b/plugins/woocommerce/changelog/feature-37367-marketing-multichannel-remove-toggle @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Make Multichannel Marketing the default new UI for Marketing page; remove classic Marketing page and unused code. diff --git a/plugins/woocommerce/changelog/feature-marketing-boolean-expression b/plugins/woocommerce/changelog/feature-marketing-boolean-expression new file mode 100644 index 00000000000..88fadf329b4 --- /dev/null +++ b/plugins/woocommerce/changelog/feature-marketing-boolean-expression @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Simplify boolean expression before && in Marketing page. diff --git a/plugins/woocommerce/changelog/feature-marketing-refactor-components b/plugins/woocommerce/changelog/feature-marketing-refactor-components new file mode 100644 index 00000000000..17d9307e65d --- /dev/null +++ b/plugins/woocommerce/changelog/feature-marketing-refactor-components @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Code refactor on marketing components. diff --git a/plugins/woocommerce/changelog/feature-remove-mcm-flag-option b/plugins/woocommerce/changelog/feature-remove-mcm-flag-option new file mode 100644 index 00000000000..caa01e7e98b --- /dev/null +++ b/plugins/woocommerce/changelog/feature-remove-mcm-flag-option @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Remove the multichannel marketing feature flag from database since it's the default option now. diff --git a/plugins/woocommerce/changelog/fix-#34200-need-to-add-space-between-author-image-and-meta b/plugins/woocommerce/changelog/fix-#34200-need-to-add-space-between-author-image-and-meta deleted file mode 100644 index 962ff56149f..00000000000 --- a/plugins/woocommerce/changelog/fix-#34200-need-to-add-space-between-author-image-and-meta +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: Make Reviews table CSS match other list tables. - - diff --git a/plugins/woocommerce/changelog/fix-27518-payment-gateway-title b/plugins/woocommerce/changelog/fix-27518-payment-gateway-title new file mode 100644 index 00000000000..654ab0ba5dc --- /dev/null +++ b/plugins/woocommerce/changelog/fix-27518-payment-gateway-title @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Avoid over-aggressive escaping of the payment gateway title in the context of the checkout thank you page. diff --git a/plugins/woocommerce/changelog/fix-28969-modify-order-on-refund b/plugins/woocommerce/changelog/fix-28969-modify-order-on-refund deleted file mode 100644 index 57a4c3c9806..00000000000 --- a/plugins/woocommerce/changelog/fix-28969-modify-order-on-refund +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Update the date modified field for an order when a refund for it is successfully processed. diff --git a/plugins/woocommerce/changelog/fix-32316-download-adjuster b/plugins/woocommerce/changelog/fix-32316-download-adjuster new file mode 100644 index 00000000000..89f94aad1d5 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-32316-download-adjuster @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +fixed bug where adjust_download_permissions was being scheduled on variable products without downloadable variations diff --git a/plugins/woocommerce/changelog/fix-34109 b/plugins/woocommerce/changelog/fix-34109 new file mode 100644 index 00000000000..36fe6df1707 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-34109 @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Accessibility update for product_categories shortcode. diff --git a/plugins/woocommerce/changelog/fix-34391 b/plugins/woocommerce/changelog/fix-34391 deleted file mode 100644 index afbb07ece83..00000000000 --- a/plugins/woocommerce/changelog/fix-34391 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: tweak - -Make sure 'safe_text' settings are rendered as 'text' inputs for compatibility. diff --git a/plugins/woocommerce/changelog/fix-34594-product-editor-reviews b/plugins/woocommerce/changelog/fix-34594-product-editor-reviews new file mode 100644 index 00000000000..f3ffd7d5e56 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-34594-product-editor-reviews @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Restores comments (reviews) to the product editor. diff --git a/plugins/woocommerce/changelog/fix-35543 b/plugins/woocommerce/changelog/fix-35543 deleted file mode 100644 index a427c9577b6..00000000000 --- a/plugins/woocommerce/changelog/fix-35543 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Prevent possible warning arising from use of woocommerce_wp_* family of functions. diff --git a/plugins/woocommerce/changelog/fix-36142-mobile-app-image-resolution b/plugins/woocommerce/changelog/fix-36142-mobile-app-image-resolution new file mode 100644 index 00000000000..181f8449549 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-36142-mobile-app-image-resolution @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Update mobile app image resolution diff --git a/plugins/woocommerce/changelog/fix-36498 b/plugins/woocommerce/changelog/fix-36498 new file mode 100644 index 00000000000..574e324e7bb --- /dev/null +++ b/plugins/woocommerce/changelog/fix-36498 @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Default to sorting orders by date (desc) when HPOS is active. diff --git a/plugins/woocommerce/changelog/fix-36618-product-import b/plugins/woocommerce/changelog/fix-36618-product-import deleted file mode 100644 index 67560ceed4c..00000000000 --- a/plugins/woocommerce/changelog/fix-36618-product-import +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Ensure product importer imports all lines in a CSV file. diff --git a/plugins/woocommerce/changelog/fix-36711-obw-blank-screen-wp5_9 b/plugins/woocommerce/changelog/fix-36711-obw-blank-screen-wp5_9 deleted file mode 100644 index 4f39fe3029a..00000000000 --- a/plugins/woocommerce/changelog/fix-36711-obw-blank-screen-wp5_9 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix blank screen is displayed during OBW when using WP5.9 diff --git a/plugins/woocommerce/changelog/fix-36855-qty-selector-in-variables b/plugins/woocommerce/changelog/fix-36855-qty-selector-in-variables deleted file mode 100644 index 1f55fad5c17..00000000000 --- a/plugins/woocommerce/changelog/fix-36855-qty-selector-in-variables +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix no enforcing of min/max limits in quantity selector of variable products. diff --git a/plugins/woocommerce/changelog/fix-36890_create_wc_extension_script b/plugins/woocommerce/changelog/fix-36890_create_wc_extension_script deleted file mode 100644 index 253dcf4a315..00000000000 --- a/plugins/woocommerce/changelog/fix-36890_create_wc_extension_script +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Update create-wc-extension script within woocommerce-admin. diff --git a/plugins/woocommerce/changelog/fix-37020_duplicated_global_attribute b/plugins/woocommerce/changelog/fix-37020_duplicated_global_attribute deleted file mode 100644 index a698ac0c2d5..00000000000 --- a/plugins/woocommerce/changelog/fix-37020_duplicated_global_attribute +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Fix duplicated global attribute diff --git a/plugins/woocommerce/changelog/fix-37021_add_validation_when_saving_attributes b/plugins/woocommerce/changelog/fix-37021_add_validation_when_saving_attributes deleted file mode 100644 index 81560990d51..00000000000 --- a/plugins/woocommerce/changelog/fix-37021_add_validation_when_saving_attributes +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Add validation when saving attributes and variations diff --git a/plugins/woocommerce/changelog/fix-37083-page_displays_0_for_empty_recommended_channels b/plugins/woocommerce/changelog/fix-37083-page_displays_0_for_empty_recommended_channels deleted file mode 100644 index 4e690c01a3a..00000000000 --- a/plugins/woocommerce/changelog/fix-37083-page_displays_0_for_empty_recommended_channels +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix 0 rendered on short-circuit evaluation. diff --git a/plugins/woocommerce/changelog/fix-37090-menu-select-all b/plugins/woocommerce/changelog/fix-37090-menu-select-all new file mode 100644 index 00000000000..e9e9ea8612b --- /dev/null +++ b/plugins/woocommerce/changelog/fix-37090-menu-select-all @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +update select all to checkbox in menu editor diff --git a/plugins/woocommerce/changelog/fix-37173 b/plugins/woocommerce/changelog/fix-37173 new file mode 100644 index 00000000000..a8e167ce10b --- /dev/null +++ b/plugins/woocommerce/changelog/fix-37173 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix table alias issue in order field queries. diff --git a/plugins/woocommerce/changelog/fix-37276-remove-information-schema-queries b/plugins/woocommerce/changelog/fix-37276-remove-information-schema-queries new file mode 100644 index 00000000000..eb03b735fd3 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-37276-remove-information-schema-queries @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Replace information_schema queries in favor of create table parsing to remove foreign key constraints during updates diff --git a/plugins/woocommerce/changelog/fix-arrayutil_get_value_or_default b/plugins/woocommerce/changelog/fix-arrayutil_get_value_or_default deleted file mode 100644 index 0c8dd965fab..00000000000 --- a/plugins/woocommerce/changelog/fix-arrayutil_get_value_or_default +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix ArrayUtil::get_value_or_default method not behaving as documented for null array values diff --git a/plugins/woocommerce/changelog/fix-cant-apply-coupon-0 b/plugins/woocommerce/changelog/fix-cant-apply-coupon-0 deleted file mode 100644 index d9066b3f2a1..00000000000 --- a/plugins/woocommerce/changelog/fix-cant-apply-coupon-0 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix the inability to apply a coupon whose code is "0" diff --git a/plugins/woocommerce/changelog/fix-disable-save-btn b/plugins/woocommerce/changelog/fix-disable-save-btn new file mode 100644 index 00000000000..37f582bd74e --- /dev/null +++ b/plugins/woocommerce/changelog/fix-disable-save-btn @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Attach event to parent node to avoid it being overwritten when DOM changes + + diff --git a/plugins/woocommerce/changelog/fix-gutengerg15-5-update-issues b/plugins/woocommerce/changelog/fix-gutengerg15-5-update-issues new file mode 100644 index 00000000000..5e9a1834400 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-gutengerg15-5-update-issues @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix incorrect usage of dispatch, useSelect, and setState calls in homescreen along with settings and onboarding package diff --git a/plugins/woocommerce/changelog/fix-import-of-draft-variations b/plugins/woocommerce/changelog/fix-import-of-draft-variations deleted file mode 100644 index 00f56b8dba5..00000000000 --- a/plugins/woocommerce/changelog/fix-import-of-draft-variations +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix variations exported as draft being imported as draft (and thus remaining invisible) diff --git a/plugins/woocommerce/changelog/fix-incorrect-variable-in-api-core-tests-37387 b/plugins/woocommerce/changelog/fix-incorrect-variable-in-api-core-tests-37387 new file mode 100644 index 00000000000..bd976e51b5c --- /dev/null +++ b/plugins/woocommerce/changelog/fix-incorrect-variable-in-api-core-tests-37387 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix incorrect variable name in api-core-tests diff --git a/plugins/woocommerce/changelog/fix-install-plugin-error-track b/plugins/woocommerce/changelog/fix-install-plugin-error-track new file mode 100644 index 00000000000..9be0defc8d8 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-install-plugin-error-track @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix missing result prop in wcadmin_install_plugin_error track diff --git a/plugins/woocommerce/changelog/fix-k6-baseline-imports b/plugins/woocommerce/changelog/fix-k6-baseline-imports deleted file mode 100644 index c6a3c6e2630..00000000000 --- a/plugins/woocommerce/changelog/fix-k6-baseline-imports +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -add wpLogin import to wc-baseline-load.js diff --git a/plugins/woocommerce/changelog/fix-location-type-error b/plugins/woocommerce/changelog/fix-location-type-error new file mode 100644 index 00000000000..cbff3a88913 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-location-type-error @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Fix FormContext type error caused by #37257 + + diff --git a/plugins/woocommerce/changelog/fix-react-dependency-versions b/plugins/woocommerce/changelog/fix-react-dependency-versions deleted file mode 100644 index 026a7cd5b70..00000000000 --- a/plugins/woocommerce/changelog/fix-react-dependency-versions +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Override react version to 17.0.2 diff --git a/plugins/woocommerce/changelog/fix-refund-rounding-difference b/plugins/woocommerce/changelog/fix-refund-rounding-difference new file mode 100644 index 00000000000..906ec2b0d5e --- /dev/null +++ b/plugins/woocommerce/changelog/fix-refund-rounding-difference @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Fix rounding difference on refunds with per-line taxes diff --git a/plugins/woocommerce/changelog/fix-stricter_migration b/plugins/woocommerce/changelog/fix-stricter_migration new file mode 100644 index 00000000000..6913259462b --- /dev/null +++ b/plugins/woocommerce/changelog/fix-stricter_migration @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Make migration more strict by removing IGNORE diff --git a/plugins/woocommerce/changelog/fix-sync-ssr-REST-and-UI b/plugins/woocommerce/changelog/fix-sync-ssr-REST-and-UI new file mode 100644 index 00000000000..6891ea83cbe --- /dev/null +++ b/plugins/woocommerce/changelog/fix-sync-ssr-REST-and-UI @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Synchronized SSR from template to REST API. diff --git a/plugins/woocommerce/changelog/fix-tax_lookup_and_order_stat_deletion b/plugins/woocommerce/changelog/fix-tax_lookup_and_order_stat_deletion new file mode 100644 index 00000000000..6fedd274ecf --- /dev/null +++ b/plugins/woocommerce/changelog/fix-tax_lookup_and_order_stat_deletion @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Delete tax lookup and order stats database records when an order is deleted while orders table is authoritative diff --git a/plugins/woocommerce/changelog/fix-tt2-wp60-missing-padding-in-some-buttons b/plugins/woocommerce/changelog/fix-tt2-wp60-missing-padding-in-some-buttons deleted file mode 100644 index e4e172c8cf5..00000000000 --- a/plugins/woocommerce/changelog/fix-tt2-wp60-missing-padding-in-some-buttons +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Add default button padding to TT2 stylesheet to fix some visual issues in WP 5.9 and 6.0 diff --git a/plugins/woocommerce/changelog/fix-typescript-incremental-builds b/plugins/woocommerce/changelog/fix-typescript-incremental-builds new file mode 100644 index 00000000000..f2bdc9a96ae --- /dev/null +++ b/plugins/woocommerce/changelog/fix-typescript-incremental-builds @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: TypeScript build change + + diff --git a/plugins/woocommerce/changelog/fix-typescript-package-isolation b/plugins/woocommerce/changelog/fix-typescript-package-isolation new file mode 100644 index 00000000000..2d087939231 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-typescript-package-isolation @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Configuration change only + + diff --git a/plugins/woocommerce/changelog/fix-typo-in-stats-controller b/plugins/woocommerce/changelog/fix-typo-in-stats-controller new file mode 100644 index 00000000000..b2d5cc53fad --- /dev/null +++ b/plugins/woocommerce/changelog/fix-typo-in-stats-controller @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Fix typo in Stats controller diff --git a/plugins/woocommerce/changelog/fix-variations-dom-events b/plugins/woocommerce/changelog/fix-variations-dom-events deleted file mode 100644 index 1545c761283..00000000000 --- a/plugins/woocommerce/changelog/fix-variations-dom-events +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: fix -Comment: This fixes a bug which was introduced in a recent PR - - diff --git a/plugins/woocommerce/changelog/fix-wc_add_number_precision-not-supporting-null b/plugins/woocommerce/changelog/fix-wc_add_number_precision-not-supporting-null deleted file mode 100644 index e7b9ce5c13c..00000000000 --- a/plugins/woocommerce/changelog/fix-wc_add_number_precision-not-supporting-null +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Add support for null inputs to pnpm wc_add_number_precision diff --git a/plugins/woocommerce/changelog/hpos-add_sort b/plugins/woocommerce/changelog/hpos-add_sort new file mode 100644 index 00000000000..dad318f81a1 --- /dev/null +++ b/plugins/woocommerce/changelog/hpos-add_sort @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add sort order to migration script for consistency. diff --git a/plugins/woocommerce/changelog/hpos-end_at_support b/plugins/woocommerce/changelog/hpos-end_at_support new file mode 100644 index 00000000000..e73f5dd38f3 --- /dev/null +++ b/plugins/woocommerce/changelog/hpos-end_at_support @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add support for end_at ID to allow partial verification. diff --git a/plugins/woocommerce/changelog/issues-35004-attributes-saved-trigger b/plugins/woocommerce/changelog/issues-35004-attributes-saved-trigger deleted file mode 100644 index 7e4e46cc8a5..00000000000 --- a/plugins/woocommerce/changelog/issues-35004-attributes-saved-trigger +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: tweak - -Trigger event `woocommerce_attributes_saved` following successful product meta box ajax update. diff --git a/plugins/woocommerce/changelog/perf-count_orders b/plugins/woocommerce/changelog/perf-count_orders new file mode 100644 index 00000000000..87af41b515f --- /dev/null +++ b/plugins/woocommerce/changelog/perf-count_orders @@ -0,0 +1,4 @@ +Significance: minor +Type: performance + +Improve search count query performance by avoiding LEFT JOIN in favor of subquery. diff --git a/plugins/woocommerce/changelog/pr-35940 b/plugins/woocommerce/changelog/pr-35940 new file mode 100644 index 00000000000..16aa6505b31 --- /dev/null +++ b/plugins/woocommerce/changelog/pr-35940 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix the type for order items in the schema definition of REST API v1 and V2 \ No newline at end of file diff --git a/plugins/woocommerce/changelog/pr-36705 b/plugins/woocommerce/changelog/pr-36705 deleted file mode 100644 index c8308e99213..00000000000 --- a/plugins/woocommerce/changelog/pr-36705 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Added `woocommerce_widget_layered_nav_filters_start/end` hooks around layered nav filters widget \ No newline at end of file diff --git a/plugins/woocommerce/changelog/pr-36759 b/plugins/woocommerce/changelog/pr-36759 deleted file mode 100644 index 5e9cda49931..00000000000 --- a/plugins/woocommerce/changelog/pr-36759 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -fix typo in variable name \ No newline at end of file diff --git a/plugins/woocommerce/changelog/pr-36885 b/plugins/woocommerce/changelog/pr-36885 new file mode 100644 index 00000000000..ed20c22ae98 --- /dev/null +++ b/plugins/woocommerce/changelog/pr-36885 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Remove double checking for woocommerce_tax_display_cart filter. diff --git a/plugins/woocommerce/changelog/pr-37052 b/plugins/woocommerce/changelog/pr-37052 deleted file mode 100644 index c137f14ef99..00000000000 --- a/plugins/woocommerce/changelog/pr-37052 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Corrects a class reference in the ProductDownloadsServiceProvider. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/pr-37055 b/plugins/woocommerce/changelog/pr-37055 new file mode 100644 index 00000000000..ce0f7867340 --- /dev/null +++ b/plugins/woocommerce/changelog/pr-37055 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Corrects imported ContainerInterface. It was not replaced because of the leading backslash. diff --git a/plugins/woocommerce/changelog/pr-37056 b/plugins/woocommerce/changelog/pr-37056 deleted file mode 100644 index 569c062225f..00000000000 --- a/plugins/woocommerce/changelog/pr-37056 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Corrects class namespaces in Onboarding. It was missed during last restructuring. diff --git a/plugins/woocommerce/changelog/pr-37057 b/plugins/woocommerce/changelog/pr-37057 deleted file mode 100644 index 29c071af96e..00000000000 --- a/plugins/woocommerce/changelog/pr-37057 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Corrects a variable name in Reports\Stock\Stats. It was missed during the last name change. diff --git a/plugins/woocommerce/changelog/pr-37058 b/plugins/woocommerce/changelog/pr-37058 deleted file mode 100644 index 526d0042221..00000000000 --- a/plugins/woocommerce/changelog/pr-37058 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Corrects imported classes. Class names should not begin with a backslash. diff --git a/plugins/woocommerce/changelog/pr-37307 b/plugins/woocommerce/changelog/pr-37307 new file mode 100644 index 00000000000..130f827acab --- /dev/null +++ b/plugins/woocommerce/changelog/pr-37307 @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +add import webp support \ No newline at end of file diff --git a/plugins/woocommerce/changelog/pr-37405 b/plugins/woocommerce/changelog/pr-37405 new file mode 100644 index 00000000000..3c131a2db1e --- /dev/null +++ b/plugins/woocommerce/changelog/pr-37405 @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Fix typos in comments in REST API customers controller \ No newline at end of file diff --git a/plugins/woocommerce/changelog/revert-36275-trunk b/plugins/woocommerce/changelog/revert-36275-trunk new file mode 100644 index 00000000000..b4237659dab --- /dev/null +++ b/plugins/woocommerce/changelog/revert-36275-trunk @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Revert changes to use window.fetch in legacy cart JS diff --git a/plugins/woocommerce/changelog/try-remove-e2e-waits b/plugins/woocommerce/changelog/try-remove-e2e-waits new file mode 100644 index 00000000000..8e07a999db3 --- /dev/null +++ b/plugins/woocommerce/changelog/try-remove-e2e-waits @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Remove timeouts in e2e tests for variable products and analytics. diff --git a/plugins/woocommerce/changelog/tweak-variations-label b/plugins/woocommerce/changelog/tweak-variations-label new file mode 100644 index 00000000000..bcb141646af --- /dev/null +++ b/plugins/woocommerce/changelog/tweak-variations-label @@ -0,0 +1,5 @@ +Significance: patch +Type: tweak +Comment: Small label change + + diff --git a/plugins/woocommerce/changelog/update-35887_spotlight_on_analytics_revenue b/plugins/woocommerce/changelog/update-35887_spotlight_on_analytics_revenue deleted file mode 100644 index 49596b65370..00000000000 --- a/plugins/woocommerce/changelog/update-35887_spotlight_on_analytics_revenue +++ /dev/null @@ -1,4 +0,0 @@ -Significance: major -Type: update - -Change the default date used on Revenue and Orders report to 'date_paid' and create spotlight on both reports diff --git a/plugins/woocommerce/changelog/update-36355_product_editor_package b/plugins/woocommerce/changelog/update-36355_product_editor_package deleted file mode 100644 index f78ff8529ce..00000000000 --- a/plugins/woocommerce/changelog/update-36355_product_editor_package +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Update imports of product slot fills to new @woocommerce/product-editor library diff --git a/plugins/woocommerce/changelog/update-36395-move-product-fields b/plugins/woocommerce/changelog/update-36395-move-product-fields deleted file mode 100644 index 82ab9e78887..00000000000 --- a/plugins/woocommerce/changelog/update-36395-move-product-fields +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Moving some components out of core and into product-editor package. diff --git a/plugins/woocommerce/changelog/update-36719 b/plugins/woocommerce/changelog/update-36719 deleted file mode 100644 index 896dc558a69..00000000000 --- a/plugins/woocommerce/changelog/update-36719 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Move product utils into product editor package diff --git a/plugins/woocommerce/changelog/update-37188-migrate-location-step-to-ts b/plugins/woocommerce/changelog/update-37188-migrate-location-step-to-ts new file mode 100644 index 00000000000..c0223de6799 --- /dev/null +++ b/plugins/woocommerce/changelog/update-37188-migrate-location-step-to-ts @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Migrate steps location task to TS diff --git a/plugins/woocommerce/changelog/update-37241-block-registration b/plugins/woocommerce/changelog/update-37241-block-registration new file mode 100644 index 00000000000..bc69f48ac0f --- /dev/null +++ b/plugins/woocommerce/changelog/update-37241-block-registration @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Register product editor blocks server-side diff --git a/plugins/woocommerce/changelog/update-confirm-remove-attr b/plugins/woocommerce/changelog/update-confirm-remove-attr new file mode 100644 index 00000000000..6b39ccb9d9a --- /dev/null +++ b/plugins/woocommerce/changelog/update-confirm-remove-attr @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Show different error message when deleting an attribute used in variations diff --git a/plugins/woocommerce/changelog/update-default-payment-gateway-priority b/plugins/woocommerce/changelog/update-default-payment-gateway-priority new file mode 100644 index 00000000000..1313a944650 --- /dev/null +++ b/plugins/woocommerce/changelog/update-default-payment-gateway-priority @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Add default priority for countries that are not in the payment recommendation map diff --git a/plugins/woocommerce/changelog/update-entity-store-endpoint-36990 b/plugins/woocommerce/changelog/update-entity-store-endpoint-36990 deleted file mode 100644 index d6f53eea345..00000000000 --- a/plugins/woocommerce/changelog/update-entity-store-endpoint-36990 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Updating rest namespace for product posttype to version 3. diff --git a/plugins/woocommerce/changelog/update-manage-stock-disabled b/plugins/woocommerce/changelog/update-manage-stock-disabled deleted file mode 100644 index 54bbac5922e..00000000000 --- a/plugins/woocommerce/changelog/update-manage-stock-disabled +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Show link to store settings when stock management is disabled. diff --git a/plugins/woocommerce/changelog/update-manage-stock-label b/plugins/woocommerce/changelog/update-manage-stock-label deleted file mode 100644 index 046a681b500..00000000000 --- a/plugins/woocommerce/changelog/update-manage-stock-label +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: tweak - -Rename "Manage stock?" label to "Stock management". diff --git a/plugins/woocommerce/changelog/update-migrate-task-fills-to-ts b/plugins/woocommerce/changelog/update-migrate-task-fills-to-ts new file mode 100644 index 00000000000..c5d100b1641 --- /dev/null +++ b/plugins/woocommerce/changelog/update-migrate-task-fills-to-ts @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Migrate woocommerce-payments task to TS and remove connect.js from task fills diff --git a/plugins/woocommerce/changelog/update-move-ces-to-product-editor b/plugins/woocommerce/changelog/update-move-ces-to-product-editor new file mode 100644 index 00000000000..bcb410bdf9a --- /dev/null +++ b/plugins/woocommerce/changelog/update-move-ces-to-product-editor @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Move components to @woocommerce/product-editor diff --git a/plugins/woocommerce/changelog/update-move-product-hooks b/plugins/woocommerce/changelog/update-move-product-hooks deleted file mode 100644 index 5dbc28e2162..00000000000 --- a/plugins/woocommerce/changelog/update-move-product-hooks +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Moving use-product-helper and related product hooks to product editor package. diff --git a/plugins/woocommerce/changelog/update-move-remaining-ces-to-package b/plugins/woocommerce/changelog/update-move-remaining-ces-to-package new file mode 100644 index 00000000000..7bd3509065d --- /dev/null +++ b/plugins/woocommerce/changelog/update-move-remaining-ces-to-package @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Move additional CES-related components to @woocommerce/customer-effort-score. diff --git a/plugins/woocommerce/changelog/update-payment-order b/plugins/woocommerce/changelog/update-payment-order new file mode 100644 index 00000000000..dedd63eacb9 --- /dev/null +++ b/plugins/woocommerce/changelog/update-payment-order @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update payment gateway recommendation priority diff --git a/plugins/woocommerce/changelog/update-payment-partners b/plugins/woocommerce/changelog/update-payment-partners new file mode 100644 index 00000000000..e492b335a5a --- /dev/null +++ b/plugins/woocommerce/changelog/update-payment-partners @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Add Payoneer, zipco payment gateways and update Klarna available countries diff --git a/plugins/woocommerce/changelog/update-product-attribute-add-new-global-value-label b/plugins/woocommerce/changelog/update-product-attribute-add-new-global-value-label new file mode 100644 index 00000000000..92a90de1ee8 --- /dev/null +++ b/plugins/woocommerce/changelog/update-product-attribute-add-new-global-value-label @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Changed label for button to add a new global attribute value from the product screen. diff --git a/plugins/woocommerce/changelog/update-product-attributes-empty-state b/plugins/woocommerce/changelog/update-product-attributes-empty-state new file mode 100644 index 00000000000..204f428b324 --- /dev/null +++ b/plugins/woocommerce/changelog/update-product-attributes-empty-state @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Update style of product attributes tab empty state. diff --git a/plugins/woocommerce/changelog/update-product-page-add-hook-37281 b/plugins/woocommerce/changelog/update-product-page-add-hook-37281 new file mode 100644 index 00000000000..22f43052aba --- /dev/null +++ b/plugins/woocommerce/changelog/update-product-page-add-hook-37281 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Replacing multiple components on the block product page with a single hook. diff --git a/plugins/woocommerce/changelog/update-refactor-currency-context b/plugins/woocommerce/changelog/update-refactor-currency-context deleted file mode 100644 index 542b256874c..00000000000 --- a/plugins/woocommerce/changelog/update-refactor-currency-context +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Moving currencyContext to relevant package, and updating all references. diff --git a/plugins/woocommerce/changelog/update-remove-multichannel-tracker b/plugins/woocommerce/changelog/update-remove-multichannel-tracker new file mode 100644 index 00000000000..d429a94b9d5 --- /dev/null +++ b/plugins/woocommerce/changelog/update-remove-multichannel-tracker @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Remove multichannel marketing info from WC Tracker diff --git a/plugins/woocommerce/changelog/update-save-attributes-tooltip b/plugins/woocommerce/changelog/update-save-attributes-tooltip new file mode 100644 index 00000000000..7518ccd9ba3 --- /dev/null +++ b/plugins/woocommerce/changelog/update-save-attributes-tooltip @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Show tooltip in Save attributes button instead of using title attribute diff --git a/plugins/woocommerce/changelog/update-stable-tag b/plugins/woocommerce/changelog/update-stable-tag deleted file mode 100644 index e5ee8f7e111..00000000000 --- a/plugins/woocommerce/changelog/update-stable-tag +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev -Comment: This PR updates the stable tag. No changelog entry is required. - diff --git a/plugins/woocommerce/changelog/update-stable-tag-7-5-1 b/plugins/woocommerce/changelog/update-stable-tag-7-5-1 new file mode 100644 index 00000000000..993c5ee05ab --- /dev/null +++ b/plugins/woocommerce/changelog/update-stable-tag-7-5-1 @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: This PR updates stable tag, no changelog entry is required. + diff --git a/plugins/woocommerce/changelog/update-use-theme-color-for-completed-task-strikethrough b/plugins/woocommerce/changelog/update-use-theme-color-for-completed-task-strikethrough deleted file mode 100644 index 26d3972b2a4..00000000000 --- a/plugins/woocommerce/changelog/update-use-theme-color-for-completed-task-strikethrough +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Use the currently activated theme color for completed tasks strikethough diff --git a/plugins/woocommerce/changelog/update-variations-empty-state-no-attr b/plugins/woocommerce/changelog/update-variations-empty-state-no-attr new file mode 100644 index 00000000000..b1024baad3d --- /dev/null +++ b/plugins/woocommerce/changelog/update-variations-empty-state-no-attr @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Show info message when on variations tab and no attributes have been assigned to product. diff --git a/plugins/woocommerce/changelog/update-variations-form b/plugins/woocommerce/changelog/update-variations-form deleted file mode 100644 index 15de2b48117..00000000000 --- a/plugins/woocommerce/changelog/update-variations-form +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Change Variations form shown in Variations tab when there are no variations created diff --git a/plugins/woocommerce/changelog/update-webpack-invalid-export-error b/plugins/woocommerce/changelog/update-webpack-invalid-export-error new file mode 100644 index 00000000000..d844dbd186f --- /dev/null +++ b/plugins/woocommerce/changelog/update-webpack-invalid-export-error @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Update webpack config to use @woocommerce/internal-style-build's parser config diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-9.6.2 b/plugins/woocommerce/changelog/update-woocommerce-blocks-9.6.2 deleted file mode 100644 index 70c4f39490e..00000000000 --- a/plugins/woocommerce/changelog/update-woocommerce-blocks-9.6.2 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Update WooCommerce Blocks to 9.6.2 diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-9.6.3 b/plugins/woocommerce/changelog/update-woocommerce-blocks-9.6.3 deleted file mode 100644 index 2d8f61daef0..00000000000 --- a/plugins/woocommerce/changelog/update-woocommerce-blocks-9.6.3 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Update WooCommerce Blocks to 9.6.3 diff --git a/plugins/woocommerce/changelog/woocommerce-reduce-order-item-stock b/plugins/woocommerce/changelog/woocommerce-reduce-order-item-stock deleted file mode 100644 index 5c46be6f864..00000000000 --- a/plugins/woocommerce/changelog/woocommerce-reduce-order-item-stock +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: enhancement - -Added woocommerce_reduce_order_item_stock action hook diff --git a/plugins/woocommerce/client/admin/config/core.json b/plugins/woocommerce/client/admin/config/core.json index 48e6fe51ff5..ddc2eb8f623 100644 --- a/plugins/woocommerce/client/admin/config/core.json +++ b/plugins/woocommerce/client/admin/config/core.json @@ -2,7 +2,7 @@ "features": { "activity-panels": true, "analytics": true, - "block-editor-feature-enabled": false, + "product-block-editor": false, "coupons": true, "customer-effort-score-tracks": true, "import-products-task": true, @@ -11,7 +11,6 @@ "shipping-setting-tour": true, "homescreen": true, "marketing": true, - "multichannel-marketing": true, "minified-js": false, "mobile-app-banner": true, "navigation": true, diff --git a/plugins/woocommerce/client/admin/config/development.json b/plugins/woocommerce/client/admin/config/development.json index 39286384fb8..c6a95550b95 100644 --- a/plugins/woocommerce/client/admin/config/development.json +++ b/plugins/woocommerce/client/admin/config/development.json @@ -2,7 +2,7 @@ "features": { "activity-panels": true, "analytics": true, - "block-editor-feature-enabled": true, + "product-block-editor": true, "coupons": true, "customer-effort-score-tracks": true, "import-products-task": true, @@ -11,7 +11,6 @@ "shipping-setting-tour": true, "homescreen": true, "marketing": true, - "multichannel-marketing": true, "minified-js": true, "mobile-app-banner": true, "navigation": true, diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss index 8e07798ded9..ad9a3e3d1cf 100644 --- a/plugins/woocommerce/client/legacy/css/admin.scss +++ b/plugins/woocommerce/client/legacy/css/admin.scss @@ -6,10 +6,10 @@ /** * Imports */ -@import 'mixins'; -@import 'variables'; -@import 'animation'; -@import 'fonts'; +@import "mixins"; +@import "variables"; +@import "animation"; +@import "fonts"; /** * Styling begins @@ -20,7 +20,7 @@ .wc-addons-wrap { .marketplace-header { - background-image: url( ../images/marketplace-header-bg@2x.png ); + background-image: url(../images/marketplace-header-bg@2x.png); background-position: right; background-size: cover; box-sizing: border-box; @@ -85,7 +85,7 @@ height: 60px; margin: 0 0 16px; - @media only screen and ( min-width: 768px ) { + @media only screen and (min-width: 768px) { margin-bottom: 24px; } } @@ -97,7 +97,7 @@ position: relative; width: 100%; - @media only screen and ( min-width: 600px ) { + @media only screen and (min-width: 600px) { width: 288px; } @@ -115,13 +115,13 @@ width: 100%; z-index: 10; - @media only screen and ( min-width: 600px ) { + @media only screen and (min-width: 600px) { border: 1px solid #1e1e1e; left: -1px; top: 48px; } - @media only screen and ( min-width: 1100px ) { + @media only screen and (min-width: 1100px) { justify-content: center; } @@ -131,8 +131,8 @@ margin: 0; &.current a::after { - background-image: url( ../images/icons/gridicons-checkmark.svg ); - content: ''; + background-image: url(../images/icons/gridicons-checkmark.svg); + content: ""; display: block; height: 20px; position: absolute; @@ -157,7 +157,7 @@ position: relative; width: 100%; - @media only screen and ( min-width: 600px ) { + @media only screen and (min-width: 600px) { padding: 10px 18px; } } @@ -177,9 +177,9 @@ } .current-section-name::after { - background-image: url( ../images/icons/gridicons-chevron-down.svg ); + background-image: url(../images/icons/gridicons-chevron-down.svg); background-size: contain; - content: ''; + content: ""; display: block; height: 20px; position: absolute; @@ -194,7 +194,7 @@ } .current-section-name::after { - transform: rotate( 0.5turn ); + transform: rotate(0.5turn); } } @@ -214,8 +214,8 @@ /** * Marketplace related variables */ - $font-sf-pro-text: helveticaneue-light, 'Helvetica Neue Light', - 'Helvetica Neue', sans-serif; + $font-sf-pro-text: helveticaneue-light, "Helvetica Neue Light", + "Helvetica Neue", sans-serif; $font-sf-pro-display: sans-serif; @@ -237,11 +237,11 @@ } .subsubsub li::after { - content: '|'; + content: "|"; } .subsubsub li:last-child::after { - content: ''; + content: ""; } .addons-button { @@ -316,7 +316,7 @@ flex-direction: row; justify-content: center; - @media screen and ( min-width: 500px ) { + @media screen and (min-width: 500px) { justify-content: left; } @@ -340,7 +340,7 @@ padding: 0 0.5em; } - .addons-column:nth-child( 2 ) { + .addons-column:nth-child(2) { margin-right: 0; } @@ -429,7 +429,7 @@ opacity: 0.8; } - @media only screen and ( max-width: 400px ) { + @media only screen and (max-width: 400px) { .addons-button { width: 100%; } @@ -497,13 +497,13 @@ .product.addons-product-banner, .product.addons-buttons-banner { - max-width: calc( 100% - 2px ); + max-width: calc(100% - 2px); } - @media screen and ( min-width: 960px ) { + @media screen and (min-width: 960px) { // Adjust heading titles font for three-column product groups &.addons-products-three-column li.product { - max-width: calc( 33.33% - 12px ); + max-width: calc(33.33% - 12px); h2, h3 { @@ -522,7 +522,7 @@ flex-direction: column; justify-content: space-between; margin: 12px 0; - max-width: calc( 50% - 12px ); + max-width: calc(50% - 12px); min-width: 280px; min-height: 220px; overflow: hidden; @@ -533,7 +533,7 @@ max-width: 100%; } - @media only screen and ( max-width: 768px ) { + @media only screen and (max-width: 768px) { max-width: none; width: 100%; } @@ -589,7 +589,7 @@ line-height: 28px; margin: 0 !important; // Don't cover a product icon - max-width: calc( 100% - 48px ); + max-width: calc(100% - 48px); } .addons-buttons-banner-details h2 { @@ -694,15 +694,15 @@ width: 17px; &__fill { - background-image: url( ../images/icons/star-golden.svg ); + background-image: url(../images/icons/star-golden.svg); } &__half-fill { - background-image: url( ../images/icons/star-half-filled.svg ); + background-image: url(../images/icons/star-half-filled.svg); } &__no-fill { - background-image: url( ../images/icons/star-gray.svg ); + background-image: url(../images/icons/star-gray.svg); } } @@ -755,7 +755,7 @@ .addons-buttons-banner-details-container { padding-left: 0; - width: calc( 100% - 198px - 24px - 24px ); + width: calc(100% - 198px - 24px - 24px); } .addons-buttons-banner-details-container { @@ -782,7 +782,7 @@ .storefront { max-width: 990px; - background: url( ../images/storefront-bg.jpg ) bottom right #f6f6f6; + background: url(../images/storefront-bg.jpg) bottom right #f6f6f6; border: 1px solid #ddd; margin: 1em auto; padding: 24px; @@ -795,7 +795,7 @@ max-width: 400px; height: auto; margin: 0 auto 16px; - box-shadow: 0 1px 6px rgba( 0, 0, 0, 0.1 ); + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1); } p:last-of-type { @@ -847,7 +847,7 @@ } .current-section-name::after { - transform: rotate( 0.5turn ); + transform: rotate(0.5turn); } } } @@ -904,7 +904,7 @@ button.button-primary { background: #bb77ae; border-color: #a36597; - box-shadow: inset 0 1px 0 rgba( 255, 255, 255, 0.25 ), 0 1px 0 #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; color: #fff; text-shadow: 0 -1px 1px #a36597, 1px 0 1px #a36597, 0 1px 1px #a36597, -1px 0 1px #a36597; @@ -915,8 +915,7 @@ &:active { background: #a36597; border-color: #a36597; - box-shadow: inset 0 1px 0 rgba( 255, 255, 255, 0.25 ), - 0 1px 0 #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; } } } @@ -959,11 +958,18 @@ } #variable_product_options #message, +#inventory_product_data .notice, #variable_product_options .notice { display: flex; margin: 10px; background-color: #ffffff; > p { + a { + text-decoration: none; + white-space: nowrap; + } + } + > p:not(:last-child) { width: 85%; } .woocommerce-add-variation-price-container { @@ -999,43 +1005,128 @@ } } +/** + * Variations related variables + */ +$container-height: 360px; +$max-content-width: 544px; +$default-font-size: 14px; +$default-line-height: 18px; + #variable_product_options { .form-row select { max-width: 100%; } + .variation_actions { + max-width: 131px; + } + .toolbar-top { - .button { + .button, + .select { margin: 1px; } } + + %container-defaults { + box-sizing: border-box; + display: flex; + padding: 32px 0; + height: $container-height; + } + + %centered-text { + width: 90%; + max-width: $max-content-width; + font-size: $default-font-size; + line-height: $default-line-height; + text-align: center; + } + + .add-attributes-container { + @extend %container-defaults; + align-items: center; + + a { + text-decoration: none; + + &[target="_blank"]::after { + content: url("../images/icons/external-link.svg"); + margin-left: 2px; + vertical-align: sub; + } + } + + .add-attributes-message { + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; + width: 100%; + + p { + @extend %centered-text; + } + } + } + + .add-variation-container { + @extend %container-defaults; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 24px; + position: relative; + + .arrow-image-wrapper { + position: absolute; + top: 10px; + left: 87px; + } + + p { + @extend %centered-text; + } + + &.hidden { + display: none; + } + } } #product_attributes { .add-global-attribute-container { - box-sizing: border-box; display: flex; flex-direction: column; - justify-content: center; - align-items: center; - padding: 32px 0px; - gap: 24px; height: 360px; - @media screen and ( max-width: 782px ) { - button { - vertical-align: top; - } - } - p { - width: 90%; - max-width: 544px; - font-size: 14px; - line-height: 18px; - text-align: center; - } + &.hidden { display: none; } + + .message { + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 32px 0px; + gap: 24px; + flex: 1; + @media screen and (max-width: 782px) { + button { + vertical-align: top; + } + } + p { + width: 90%; + max-width: 544px; + font-size: 14px; + line-height: 18px; + text-align: center; + } + } } .toolbar-top { .button, @@ -1085,7 +1176,7 @@ mark.amount { width: 16px; &::after { - @include icon_dashicons( '\f223' ); + @include icon_dashicons("\f223"); cursor: help; } } @@ -1112,7 +1203,7 @@ table.wc_status_table { margin: 0; } - tr:nth-child( 2n ) { + tr:nth-child(2n) { th, td { background: #fcfcfc; @@ -1263,7 +1354,7 @@ table.wc_status_table--tools { } // Adjust log table columns only when table is not collapsed - @media screen and ( min-width: 783px ) { + @media screen and (min-width: 783px) { .column-timestamp { width: 18%; } @@ -1290,7 +1381,7 @@ table.wc_status_table--tools { #log-viewer { background: #fff; border: 1px solid #e5e5e5; - box-shadow: 0 1px 1px rgba( 0, 0, 0, 0.04 ); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); padding: 5px 20px; pre { @@ -1449,7 +1540,7 @@ ul.wc_coupon_list { */ &::before { - @include icon_dashicons( '\f158' ); + @include icon_dashicons("\f158"); } &:hover::before { @@ -1491,7 +1582,7 @@ ul.wc_coupon_list_block { display: inline-block; &::after { - @include icon_dashicons( '\f345' ); + @include icon_dashicons("\f345"); line-height: 28px; } } @@ -1513,8 +1604,8 @@ ul.wc_coupon_list_block { h2 { margin: 0; - font-family: 'HelveticaNeue-Light', 'Helvetica Neue Light', - 'Helvetica Neue', sans-serif; + font-family: "HelveticaNeue-Light", "Helvetica Neue Light", + "Helvetica Neue", sans-serif; font-size: 21px; font-weight: normal; line-height: 1.2; @@ -1538,8 +1629,8 @@ ul.wc_coupon_list_block { p.order_number { margin: 0; - font-family: 'HelveticaNeue-Light', 'Helvetica Neue Light', - 'Helvetica Neue', sans-serif; + font-family: "HelveticaNeue-Light", "Helvetica Neue Light", + "Helvetica Neue", sans-serif; font-weight: normal; line-height: 1.6em; font-size: 16px; @@ -1692,7 +1783,7 @@ ul.wc_coupon_list_block { } &::after { - font-family: 'WooCommerce'; + font-family: "WooCommerce"; position: absolute; top: 0; left: 0; @@ -1705,8 +1796,8 @@ ul.wc_coupon_list_block { } a.edit_address::after { - font-family: 'Dashicons'; - content: '\f464'; + font-family: "Dashicons"; + content: "\f464"; } .billing-same-as-shipping, @@ -1838,7 +1929,7 @@ ul.wc_coupon_list_block { margin: 0 0 0 0.5em; box-sizing: border-box; - input[type='text'] { + input[type="text"] { width: 96%; float: right; } @@ -2010,7 +2101,7 @@ ul.wc_coupon_list_block { text-align: center; &:empty::before { - @include icon_dashicons( '\f128' ); + @include icon_dashicons("\f128"); width: 38px; line-height: 38px; display: block; @@ -2081,7 +2172,7 @@ ul.wc_coupon_list_block { display: inline-block; background: #fff; border: 1px solid #ddd; - box-shadow: inset 0 1px 2px rgba( 0, 0, 0, 0.07 ); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); margin: 1px 0; min-width: 80px; overflow: hidden; @@ -2254,7 +2345,7 @@ ul.wc_coupon_list_block { margin: 0 auto; &::before { - @include icon( '\e007' ); + @include icon("\e007"); color: #ccc; } } @@ -2267,7 +2358,7 @@ ul.wc_coupon_list_block { margin: 0 auto; &::before { - @include icon( '\e014' ); + @include icon("\e014"); color: #ccc; } } @@ -2281,7 +2372,7 @@ ul.wc_coupon_list_block { margin: 0 auto; &::before { - @include icon( '\e01a' ); + @include icon("\e01a"); color: #ccc; } } @@ -2307,7 +2398,7 @@ ul.wc_coupon_list_block { margin: 3px -18px 0 0; &::before { - @include icon_dashicons( '\f153' ); + @include icon_dashicons("\f153"); color: #999; } @@ -2328,7 +2419,7 @@ ul.wc_coupon_list_block { margin-top: 0.5em; &::before { - @include icon_dashicons( '\f171' ); + @include icon_dashicons("\f171"); position: relative; top: auto; left: auto; @@ -2382,14 +2473,14 @@ ul.wc_coupon_list_block { } .edit-order-item::before { - @include icon_dashicons( '\f464' ); + @include icon_dashicons("\f464"); position: relative; } .delete-order-item, .delete_refund { &::before { - @include icon_dashicons( '\f158' ); + @include icon_dashicons("\f158"); position: relative; } @@ -2534,7 +2625,7 @@ ul.wc_coupon_list_block { vertical-align: middle; &::after { - font-family: 'Dashicons'; + font-family: "Dashicons"; speak: never; font-weight: normal; font-variant: normal; @@ -2558,30 +2649,30 @@ ul.wc_coupon_list_block { } a.edit::after { - content: '\f464'; + content: "\f464"; } a.link::after { - font-family: 'WooCommerce'; - content: '\e00d'; + font-family: "WooCommerce"; + content: "\e00d"; } a.view::after { - content: '\f177'; + content: "\f177"; } a.refresh::after { - font-family: 'WooCommerce'; - content: '\e031'; + font-family: "WooCommerce"; + content: "\e031"; } a.processing::after { - font-family: 'WooCommerce'; - content: '\e00f'; + font-family: "WooCommerce"; + content: "\e00f"; } a.complete::after { - content: '\f147'; + content: "\f147"; } } @@ -2672,7 +2763,7 @@ ul.wc_coupon_list_block { border-top: 1px solid #f5f5f5; } - tbody tr:hover:not( .status-trash ):not( .no-link ) td { + tbody tr:hover:not(.status-trash):not(.no-link) td { cursor: pointer; } @@ -2756,7 +2847,7 @@ ul.wc_coupon_list_block { border-radius: 4px; &::before { - @include icon( '\e010' ); + @include icon("\e010"); line-height: 16px; font-size: 14px; vertical-align: middle; @@ -2770,9 +2861,8 @@ ul.wc_coupon_list_block { .order-preview.disabled { &::before { - content: ''; - background: url( '../images/wpspin-2x.gif' ) no-repeat center - top; + content: ""; + background: url("../images/wpspin-2x.gif") no-repeat center top; background-size: 71%; } } @@ -2785,7 +2875,7 @@ ul.wc_coupon_list_block { color: #777; background: #e5e5e5; border-radius: 4px; - border-bottom: 1px solid rgba( 0, 0, 0, 0.05 ); + border-bottom: 1px solid rgba(0, 0, 0, 0.05); margin: -0.25em 0; cursor: inherit !important; white-space: nowrap; @@ -2990,7 +3080,7 @@ ul.wc_coupon_list_block { } } -@media screen and ( max-width: 782px ) { +@media screen and (max-width: 782px) { .wc-order-preview footer { .wc-action-button-group .wc-action-button-group__items { display: flex; @@ -3047,7 +3137,7 @@ ul.wc_coupon_list_block { color: #999; &::after { - @include icon( '\e026' ); + @include icon("\e026"); line-height: 16px; } } @@ -3058,7 +3148,7 @@ ul.wc_coupon_list_block { color: #999; &::after { - @include icon( '\e027' ); + @include icon("\e027"); line-height: 16px; } } @@ -3084,8 +3174,8 @@ ul.wc_coupon_list_block { width: 2em; &::after { - @include icon( '\f111' ); - font-family: 'Dashicons'; + @include icon("\f111"); + font-family: "Dashicons"; line-height: 1.85; } } @@ -3125,7 +3215,7 @@ ul.order_notes { } .note_content::after { - content: ''; + content: ""; display: block; position: absolute; bottom: -10px; @@ -3193,8 +3283,8 @@ table.wp-list-table { vertical-align: text-top; &::before { - content: '\f333'; - font-family: 'Dashicons'; + content: "\f333"; + font-family: "Dashicons"; text-align: center; line-height: 1; color: #999; @@ -3281,17 +3371,17 @@ table.wp-list-table { margin: 0 auto; &::before { - @include icon_dashicons( '\f128' ); + @include icon_dashicons("\f128"); } } span.wc-featured { &::before { - content: '\f155'; + content: "\f155"; } &.not-featured::before { - content: '\f154'; + content: "\f154"; } } @@ -3334,15 +3424,15 @@ table.wp-list-table { } .order-notes_head::after { - content: '\e028'; + content: "\e028"; } .notes_head::after { - content: '\e026'; + content: "\e026"; } .status_head::after { - content: '\e011'; + content: "\e011"; } .column-order_items { @@ -3432,8 +3522,8 @@ table.wc_input_table { background: #fff; cursor: default; - input[type='text'], - input[type='number'] { + input[type="text"], + input[type="number"] { width: 100% !important; min-width: 100px; padding: 8px 10px; @@ -3485,7 +3575,7 @@ table.wc_input_table { padding: 0 4px; } - .ui-sortable:not( .ui-sortable-disabled ) td.sort { + .ui-sortable:not(.ui-sortable-disabled) td.sort { cursor: move; font-size: 15px; background: #f9f9f9; @@ -3493,8 +3583,8 @@ table.wc_input_table { vertical-align: middle; &::before { - content: '\f333'; - font-family: 'Dashicons'; + content: "\f333"; + font-family: "Dashicons"; text-align: center; line-height: 1; color: #999; @@ -3567,7 +3657,7 @@ table.wc_shipping { vertical-align: middle; } - tr:nth-child( odd ) td { + tr:nth-child(odd) td { background: #f9f9f9; } @@ -3602,8 +3692,8 @@ table.wc_shipping { width: 72px; &::before { - content: '\f333'; - font-family: 'Dashicons'; + content: "\f333"; + font-family: "Dashicons"; text-align: center; line-height: 1; color: #999; @@ -3652,11 +3742,11 @@ table.wc_shipping { } .wc-move-down::before { - content: '\f347'; + content: "\f347"; } .wc-move-up::before { - content: '\f343'; + content: "\f343"; } .wc-move-disabled { @@ -3851,8 +3941,8 @@ table.wc-shipping-classes { } &::before { - content: '\e01b'; - font-family: 'WooCommerce'; + content: "\e01b"; + font-family: "WooCommerce"; text-align: center; line-height: 1; color: #eee2ec; @@ -3868,8 +3958,8 @@ table.wc-shipping-classes { .button-primary { background-color: #804877; border-color: #804877; - box-shadow: inset 0 1px 0 rgba( 255, 255, 255, 0.2 ), - 0 1px 0 rgba( 0, 0, 0, 0.15 ); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), + 0 1px 0 rgba(0, 0, 0, 0.15); margin: 0; opacity: 1; text-shadow: 0 -1px 1px #8a4f7f, 1px 0 1px #8a4f7f, @@ -3883,13 +3973,13 @@ table.wc-shipping-classes { } .wc-shipping-zone-method-rows { - tr:nth-child( even ) td { + tr:nth-child(even) td { background: #f9f9f9; } } tr.odd, - .wc-shipping-class-rows tr:nth-child( odd ) { + .wc-shipping-class-rows tr:nth-child(odd) { td { background: #f9f9f9; } @@ -3926,8 +4016,8 @@ table.wc-shipping-classes { text-align: center; &::before { - content: '\f333'; - font-family: 'Dashicons'; + content: "\f333"; + font-family: "Dashicons"; text-align: center; line-height: 1; color: #999; @@ -3947,8 +4037,8 @@ table.wc-shipping-classes { text-align: center; &::before { - content: '\f319'; - font-family: 'dashicons'; + content: "\f319"; + font-family: "dashicons"; text-align: center; line-height: 1; color: #999; @@ -4009,11 +4099,11 @@ table.wc-shipping-classes { } li::before { - content: ', '; + content: ", "; } li:first-child::before { - content: ''; + content: ""; } } @@ -4027,8 +4117,8 @@ table.wc-shipping-classes { &::before { @include icon; - font-family: 'Dashicons'; - content: '\f502'; + font-family: "Dashicons"; + content: "\f502"; color: #999; vertical-align: middle; line-height: 24px; @@ -4102,7 +4192,7 @@ table.wc-shipping-classes { vertical-align: text-top; &::before { - content: ''; + content: ""; display: block; width: 16px; height: 16px; @@ -4160,7 +4250,7 @@ table.wc-shipping-classes { min-width: 250px; } - input[type='checkbox'] { + input[type="checkbox"] { width: auto; min-width: 16px; } @@ -4222,17 +4312,17 @@ img.help_tip { } .status-manual::before { - @include icon( '\e008' ); + @include icon("\e008"); color: #999; } .status-enabled::before { - @include icon( '\e015' ); + @include icon("\e015"); color: $woocommerce; } .status-disabled::before { - @include icon( '\e013' ); + @include icon("\e013"); color: #ccc; } @@ -4245,7 +4335,7 @@ img.help_tip { margin: 1.5em 0 1em; } - .subsubsub:not( .list-table-filters ) { + .subsubsub:not(.list-table-filters) { margin: -8px 0 0; } @@ -4271,7 +4361,7 @@ img.help_tip { } } - textarea[disabled='disabled'] { + textarea[disabled="disabled"] { background: #dfdfdf !important; } @@ -4288,9 +4378,9 @@ img.help_tip { } } - input[type='text'], - input[type='number'], - input[type='email'] { + input[type="text"], + input[type="number"], + input[type="email"] { height: auto; } @@ -4302,17 +4392,17 @@ img.help_tip { // Give regular settings inputs a standard width and padding. textarea, - input[type='text'], - input[type='email'], - input[type='number'], - input[type='password'], - input[type='datetime'], - input[type='datetime-local'], - input[type='date'], - input[type='time'], - input[type='week'], - input[type='url'], - input[type='tel'], + input[type="text"], + input[type="email"], + input[type="number"], + input[type="password"], + input[type="datetime"], + input[type="datetime-local"], + input[type="date"], + input[type="time"], + input[type="week"], + input[type="url"], + input[type="tel"], input.regular-input { width: 400px; margin: 0; @@ -4321,11 +4411,11 @@ img.help_tip { vertical-align: top; } - input[type='datetime-local'], - input[type='date'], - input[type='time'], - input[type='week'], - input[type='tel'] { + input[type="datetime-local"], + input[type="date"], + input[type="time"], + input[type="week"], + input[type="tel"] { width: 200px; } @@ -4345,9 +4435,9 @@ img.help_tip { table { select, textarea, - input[type='text'], - input[type='email'], - input[type='number'], + input[type="text"], + input[type="email"], + input[type="number"], input.regular-input { width: auto; } @@ -4448,7 +4538,7 @@ img.help_tip { position: absolute; border: 1px solid #ccc; border-radius: 3px; - box-shadow: 0 1px 3px rgba( 0, 0, 0, 0.2 ); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); .ui-slider { border: 0 !important; @@ -4522,27 +4612,27 @@ img.help_tip { table.form-table { // Give regular settings inputs a standard width and padding. textarea, - input[type='text'], - input[type='email'], - input[type='number'], - input[type='password'], - input[type='datetime'], - input[type='datetime-local'], - input[type='date'], - input[type='time'], - input[type='week'], - input[type='url'], - input[type='tel'], + input[type="text"], + input[type="email"], + input[type="number"], + input[type="password"], + input[type="datetime"], + input[type="datetime-local"], + input[type="date"], + input[type="time"], + input[type="week"], + input[type="url"], + input[type="tel"], input.regular-input { padding: 0 8px; - @media only screen and ( max-width: 782px ) { + @media only screen and (max-width: 782px) { width: 100%; } } select { - @media only screen and ( max-width: 782px ) { + @media only screen and (max-width: 782px) { width: 100%; } } @@ -4552,7 +4642,7 @@ img.help_tip { .woocommerce-help-tip { margin: -7px -24px 0 0; - @media only screen and ( max-width: 782px ) { + @media only screen and (max-width: 782px) { right: auto; margin-left: 5px; } @@ -4567,12 +4657,12 @@ img.help_tip { padding: 0; width: 30px; height: 30px; - box-shadow: inset 0 0 0 1px rgba( 0, 0, 0, 0.2 ); + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2); font-size: 16px; border-radius: 4px; margin-right: 3px; - @media only screen and ( max-width: 782px ) { + @media only screen and (max-width: 782px) { float: left; width: 40px; height: 40px; @@ -4666,7 +4756,7 @@ img.help_tip { margin: 9px 9px 0 0; background: #f7f7f7; - @include border-radius( 2px ); + @include border-radius(2px); position: relative; box-sizing: border-box; @@ -4682,7 +4772,7 @@ img.help_tip { position: relative; &::after { - @include icon_dashicons( '\f161' ); + @include icon_dashicons("\f161"); font-size: 2.618em; line-height: 72px; color: #ddd; @@ -4696,7 +4786,7 @@ img.help_tip { padding: 2px; display: none; - @media ( max-width: 768px ) { + @media (max-width: 768px) { display: block; } @@ -4722,7 +4812,7 @@ img.help_tip { font-size: 1.4em; &::before { - @include icon_dashicons( '\f153' ); + @include icon_dashicons("\f153"); color: #999; background: #fff; border-radius: 50%; @@ -4884,7 +4974,7 @@ img.help_tip { box-sizing: border-box; &::after { - content: ''; + content: ""; display: block; width: 100%; height: 9999em; @@ -4916,34 +5006,34 @@ img.help_tip { } &::before { - @include iconbeforedashicons( '\f107' ); + @include iconbeforedashicons("\f107"); } } &.general_options a::before { - content: '\f107'; + content: "\f107"; } &.inventory_options a::before { - content: '\f481'; + content: "\f481"; } &.shipping_options a::before { - font-family: 'WooCommerce'; - content: '\e01a'; + font-family: "WooCommerce"; + content: "\e01a"; } &.linked_product_options a::before { - content: '\f103'; + content: "\f103"; } &.attribute_options a::before { - content: '\f175'; + content: "\f175"; } &.advanced_options a::before { - font-family: 'Dashicons'; - content: '\f111'; + font-family: "Dashicons"; + content: "\f111"; } &.marketplace-suggestions_options a::before { @@ -4951,22 +5041,22 @@ img.help_tip { } &.variations_options a::before { - content: '\f509'; + content: "\f509"; } &.usage_restriction_options a::before { - font-family: 'WooCommerce'; - content: '\e602'; + font-family: "WooCommerce"; + content: "\e602"; } &.usage_limit_options a::before { - font-family: 'WooCommerce'; - content: '\e601'; + font-family: "WooCommerce"; + content: "\e601"; } &.general_coupon_data a::before { - font-family: 'WooCommerce'; - content: '\e600'; + font-family: "WooCommerce"; + content: "\e600"; } &.active a { @@ -4982,8 +5072,8 @@ img.help_tip { * Shipping */ .woocommerce_page_wc-settings { - input[type='url'], - input[type='email'] { + input[type="url"], + input[type="email"] { direction: ltr; } @@ -4997,7 +5087,7 @@ img.help_tip { } .add.button::before { - @include iconbefore( '\e007' ); + @include iconbefore("\e007"); } } @@ -5047,7 +5137,7 @@ img.help_tip { line-height: 24px; &::after { - content: '.'; + content: "."; display: block; height: 0; clear: both; @@ -5117,7 +5207,7 @@ img.help_tip { font-size: 1.2em; &::before { - @include icon_dashicons( '\f153' ); + @include icon_dashicons("\f153"); color: #999; } @@ -5138,8 +5228,8 @@ img.help_tip { padding-right: 7px !important; &::before { - content: '\f333'; - font-family: 'Dashicons'; + content: "\f333"; + font-family: "Dashicons"; text-align: center; line-height: 1; color: #999; @@ -5156,7 +5246,7 @@ img.help_tip { /* Warning asterisk (indicates if there is a problem with a downloadable file). */ span.disabled { - color: var( --wc-red ); + color: var(--wc-red); } } } @@ -5175,8 +5265,8 @@ img.help_tip { vertical-align: middle; &::before { - content: '\f333'; - font-family: 'Dashicons'; + content: "\f333"; + font-family: "Dashicons"; text-align: center; line-height: 28px; color: #999; @@ -5238,7 +5328,7 @@ img.help_tip { margin-bottom: 1em; } - .short:nth-of-type( 2 ) { + .short:nth-of-type(2) { clear: left; } } @@ -5282,10 +5372,10 @@ img.help_tip { vertical-align: top; } - input[type='text'], - input[type='email'], - input[type='number'], - input[type='password'] { + input[type="text"], + input[type="email"], + input[type="number"], + input[type="password"] { width: 50%; float: left; } @@ -5299,10 +5389,10 @@ img.help_tip { float: left; } - input[type='text'].short, - input[type='email'].short, - input[type='number'].short, - input[type='password'].short, + input[type="text"].short, + input[type="email"].short, + input[type="number"].short, + input[type="password"].short, .short { width: 50%; } @@ -5410,7 +5500,7 @@ img.help_tip { margin: 0 !important; border-top: 1px solid white; padding: 9px 12px !important; - &:not( .expand-close-hidden ) { + &:not(.expand-close-hidden) { border-bottom: 1px solid #eee; } @@ -5494,10 +5584,10 @@ img.help_tip { float: right; &::before { - content: '\f142' !important; + content: "\f142" !important; cursor: pointer; display: inline-block; - font: 400 20px/1 'Dashicons'; + font: 400 20px/1 "Dashicons"; line-height: 0.5 !important; padding: 8px 10px; position: relative; @@ -5507,10 +5597,10 @@ img.help_tip { } &.closed { - @include border-radius( 3px ); + @include border-radius(3px); .handlediv::before { - content: '\f140' !important; + content: "\f140" !important; } h3 { @@ -5689,7 +5779,7 @@ img.help_tip { a { padding: 0 10px 3px; - background: rgba( 0, 0, 0, 0.05 ); + background: rgba(0, 0, 0, 0.05); font-size: 16px; font-weight: 400; text-decoration: none; @@ -5700,7 +5790,7 @@ img.help_tip { a.disabled:focus, a.disabled:hover { color: #a0a5aa; - background: rgba( 0, 0, 0, 0.05 ); + background: rgba(0, 0, 0, 0.05); } } @@ -5737,8 +5827,8 @@ img.help_tip { } &::before { - content: '\f128'; - font-family: 'Dashicons'; + content: "\f128"; + font-family: "Dashicons"; position: absolute; top: 0; left: 0; @@ -5757,7 +5847,7 @@ img.help_tip { } &::before { - content: '\f335'; + content: "\f335"; display: none; } @@ -5777,7 +5867,7 @@ img.help_tip { padding: 4px 1em 2px 0; } - input[type='checkbox'] { + input[type="checkbox"] { margin: 0 5px 0 0.5em !important; vertical-align: middle; } @@ -5793,20 +5883,20 @@ img.help_tip { float: right; } - input[type='text'], - input[type='number'], - input[type='password'], - input[type='color'], - input[type='date'], - input[type='datetime'], - input[type='datetime-local'], - input[type='email'], - input[type='month'], - input[type='search'], - input[type='tel'], - input[type='time'], - input[type='url'], - input[type='week'], + input[type="text"], + input[type="number"], + input[type="password"], + input[type="color"], + input[type="date"], + input[type="datetime"], + input[type="datetime-local"], + input[type="email"], + input[type="month"], + input[type="search"], + input[type="tel"], + input[type="time"], + input[type="url"], + input[type="week"], select, textarea { width: 100%; @@ -5926,7 +6016,7 @@ img.tips { text-align: center; border-radius: 3px; padding: 0.618em 1em; - box-shadow: 0 1px 3px rgba( 0, 0, 0, 0.2 ); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); code { padding: 1px; @@ -5961,7 +6051,7 @@ img.tips { z-index: 9999999; &::after { - content: ''; + content: ""; display: block; border: 8px solid #d82223; border-right-color: transparent; @@ -6031,7 +6121,7 @@ img.ui-datepicker-trigger { .postbox { &::after { - content: '.'; + content: "."; display: block; height: 0; clear: both; @@ -6062,7 +6152,7 @@ img.ui-datepicker-trigger { text-decoration: none; &::before { - @include iconbeforedashicons( '\f346' ); + @include iconbeforedashicons("\f346"); margin-right: 4px; } } @@ -6077,7 +6167,7 @@ img.ui-datepicker-trigger { &::before, &::after { - content: ' '; + content: " "; display: table; } @@ -6158,7 +6248,7 @@ img.ui-datepicker-trigger { border: 1px solid #dfdfdf; &::after { - content: '.'; + content: "."; display: block; height: 0; clear: both; @@ -6174,11 +6264,7 @@ img.ui-datepicker-trigger { margin: 0; color: $blue; border-top-width: 0; - background-image: linear-gradient( - to top, - #ececec, - #f9f9f9 - ); + background-image: linear-gradient(to top, #ececec, #f9f9f9); &.section_title:hover { color: $red; @@ -6192,7 +6278,7 @@ img.ui-datepicker-trigger { display: block; &::after { - @include iconafter( '\e035' ); + @include iconafter("\e035"); float: right; font-size: 0.9em; line-height: 1.618; @@ -6316,8 +6402,8 @@ img.ui-datepicker-trigger { color: #464646; font-weight: normal; display: block; - font-family: 'HelveticaNeue-Light', 'Helvetica Neue Light', - 'Helvetica Neue', sans-serif; + font-family: "HelveticaNeue-Light", "Helvetica Neue Light", + "Helvetica Neue", sans-serif; del { color: #e74c3c; @@ -6327,7 +6413,7 @@ img.ui-datepicker-trigger { &:hover { box-shadow: inset 0 -1px 0 0 #dfdfdf, - inset 300px 0 0 rgba( 156, 93, 144, 0.1 ); + inset 300px 0 0 rgba(156, 93, 144, 0.1); border-right: 5px solid #9c5d90 !important; padding-left: 1.5em; color: #9c5d90; @@ -6525,24 +6611,24 @@ table.bar_chart { .woocommerce_page_wc-orders, .post-type-shop_order { .woocommerce-BlankState-message::before { - @include icon( '\e01d' ); + @include icon("\e01d"); } } .post-type-shop_coupon .woocommerce-BlankState-message::before { - @include icon( '\e600' ); + @include icon("\e600"); } .post-type-product .woocommerce-BlankState-message::before { - @include icon( '\e006' ); + @include icon("\e006"); } .woocommerce-BlankState--api .woocommerce-BlankState-message::before { - @include icon( '\e01c' ); + @include icon("\e01c"); } .woocommerce-BlankState--webhooks .woocommerce-BlankState-message::before { - @include icon( '\e01b' ); + @include icon("\e01b"); } .woocommerce-BlankState { @@ -6558,8 +6644,8 @@ table.bar_chart { &::before { color: #ddd; - text-shadow: 0 -1px 1px rgba( 0, 0, 0, 0.2 ), - 0 1px 0 rgba( 255, 255, 255, 0.8 ); + text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.2), + 0 1px 0 rgba(255, 255, 255, 0.8); font-size: 8em; display: block; position: relative !important; @@ -6615,7 +6701,7 @@ table.bar_chart { /** * Small screen optimisation */ -@media only screen and ( max-width: 1280px ) { +@media only screen and (max-width: 1280px) { #order_data { .order_data_column { width: 48%; @@ -6634,10 +6720,10 @@ table.bar_chart { } .short, - input[type='text'].short, - input[type='email'].short, - input[type='number'].short, - input[type='password'].short, + input[type="text"].short, + input[type="email"].short, + input[type="number"].short, + input[type="password"].short, .dimensions_field .wrap { width: 80%; } @@ -6673,7 +6759,7 @@ table.bar_chart { /** * Optimisation for screens 900px and smaller */ -@media only screen and ( max-width: 900px ) { +@media only screen and (max-width: 900px) { #woocommerce-coupon-data ul.coupon_data_tabs, #woocommerce-product-data ul.product_data_tabs, #woocommerce-product-data .wc-tabs-back { @@ -6711,7 +6797,7 @@ table.bar_chart { /** * Optimisation for screens 782px and smaller */ -@media only screen and ( max-width: 782px ) { +@media only screen and (max-width: 782px) { #wp-excerpt-media-buttons a { font-size: 16px; line-height: 37px; @@ -6771,7 +6857,7 @@ table.bar_chart { } } - .is-expanded td:not( .hidden ) { + .is-expanded td:not(.hidden) { overflow: visible; } @@ -6793,7 +6879,7 @@ table.bar_chart { margin: 0; } - .is-expanded td:not( .hidden ) { + .is-expanded td:not(.hidden) { overflow: visible; } @@ -6804,7 +6890,7 @@ table.bar_chart { } } -@media only screen and ( max-width: 500px ) { +@media only screen and (max-width: 500px) { .woocommerce_options_panel label, .woocommerce_options_panel legend { float: none; @@ -6855,7 +6941,7 @@ table.bar_chart { z-index: 100000; left: 50%; top: 50%; - transform: translate( -50%, -50% ); + transform: translate(-50%, -50%); max-width: 100%; min-width: 500px; @@ -6874,7 +6960,7 @@ table.bar_chart { } } -@media screen and ( max-width: 782px ) { +@media screen and (max-width: 782px) { .wc-backbone-modal .wc-backbone-modal-content { width: 100%; height: 100%; @@ -6932,10 +7018,10 @@ table.bar_chart { transition: color 0.1s ease-in-out, background 0.1s ease-in-out; &::before { - font: normal 22px/50px 'dashicons' !important; + font: normal 22px/50px "dashicons" !important; color: #666; display: block; - content: '\f335'; + content: "\f335"; font-weight: 300; } @@ -7024,7 +7110,7 @@ table.bar_chart { padding: 1em 1.5em; background: #fcfcfc; border-top: 1px solid #dfdfdf; - box-shadow: 0 -4px 4px -4px rgba( 0, 0, 0, 0.1 ); + box-shadow: 0 -4px 4px -4px rgba(0, 0, 0, 0.1); .inner { text-align: right; @@ -7066,11 +7152,11 @@ table.bar_chart { } .select2-dropdown--below { - box-shadow: 0 1px 1px rgba( 0, 0, 0, 0.1 ); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); } .select2-dropdown--above { - box-shadow: 0 -1px 1px rgba( 0, 0, 0, 0.1 ); + box-shadow: 0 -1px 1px rgba(0, 0, 0, 0.1); } .select2-container { @@ -7155,12 +7241,12 @@ table.bar_chart { right: 0; height: 1px; background: #fff; - content: ''; + content: ""; } } .select2-dropdown--below { - box-shadow: 0 0 0 1px #007cba, 0 2px 1px rgba( 0, 0, 0, 0.1 ); + box-shadow: 0 0 0 1px #007cba, 0 2px 1px rgba(0, 0, 0, 0.1); &::after { top: -1px; @@ -7168,7 +7254,7 @@ table.bar_chart { } .select2-dropdown--above { - box-shadow: 0 0 0 1px #007cba, 0 -2px 1px rgba( 0, 0, 0, 0.1 ); + box-shadow: 0 0 0 1px #007cba, 0 -2px 1px rgba(0, 0, 0, 0.1); &::after { bottom: -1px; @@ -7176,7 +7262,7 @@ table.bar_chart { } .select2-container { - @media only screen and ( max-width: 782px ) { + @media only screen and (max-width: 782px) { font-size: 16px; } @@ -7188,7 +7274,7 @@ table.bar_chart { height: 30px; border-color: #7e8993; - @media only screen and ( max-width: 782px ) { + @media only screen and (max-width: 782px) { height: 40px; } @@ -7199,7 +7285,7 @@ table.bar_chart { .select2-selection__rendered { line-height: 28px; - @media only screen and ( max-width: 782px ) { + @media only screen and (max-width: 782px) { line-height: 38px; } @@ -7212,11 +7298,11 @@ table.bar_chart { right: 1px; height: 28px; width: 23px; - background: url( 'data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E' ) + background: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E") no-repeat right 5px top 55%; background-size: 16px 16px; - @media only screen and ( max-width: 782px ) { + @media only screen and (max-width: 782px) { height: 38px; } @@ -7246,7 +7332,7 @@ table.bar_chart { } .woocommerce table.form-table .select2-container { - @media only screen and ( max-width: 782px ) { + @media only screen and (max-width: 782px) { min-width: 100% !important; } } @@ -7288,11 +7374,11 @@ table.bar_chart { } .select2-dropdown--below { - box-shadow: 0 0 0 1px $color, 0 2px 1px rgba( 0, 0, 0, 0.1 ); + box-shadow: 0 0 0 1px $color, 0 2px 1px rgba(0, 0, 0, 0.1); } .select2-dropdown--above { - box-shadow: 0 0 0 1px $color, 0 -2px 1px rgba( 0, 0, 0, 0.1 ); + box-shadow: 0 0 0 1px $color, 0 -2px 1px rgba(0, 0, 0, 0.1); } .select2-selection--single .select2-selection__rendered:hover { @@ -7374,7 +7460,7 @@ table.bar_chart { } li::before { - content: ''; + content: ""; border: 4px solid #ccc; border-radius: 100%; width: 4px; @@ -7417,9 +7503,9 @@ table.bar_chart { border-radius: 4px; background-color: #bb77ae; border-color: #a36597; - -webkit-box-shadow: inset 0 1px 0 rgba( 255, 255, 255, 0.25 ), + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; - box-shadow: inset 0 1px 0 rgba( 255, 255, 255, 0.25 ), 0 1px 0 #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; text-shadow: 0 -1px 1px #a36597, 1px 0 1px #a36597, 0 1px 1px #a36597, -1px 0 1px #a36597; margin: 0; @@ -7430,10 +7516,9 @@ table.bar_chart { &:active { background: #a36597; border-color: #a36597; - -webkit-box-shadow: inset 0 1px 0 rgba( 255, 255, 255, 0.25 ), - 0 1px 0 #a36597; - box-shadow: inset 0 1px 0 rgba( 255, 255, 255, 0.25 ), + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; } } @@ -7464,7 +7549,7 @@ table.bar_chart { overflow: hidden; padding: 0; margin: 0 0 16px; - box-shadow: 0 1px 3px rgba( 0, 0, 0, 0.13 ); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.13); color: #555; text-align: left; @@ -7515,13 +7600,13 @@ table.bar_chart { font-weight: normal; } - input[type='checkbox'] { + input[type="checkbox"] { margin: 0 4px 0 0; padding: 7px; } - input[type='text'], - input[type='number'] { + input[type="text"], + input[type="number"] { padding: 7px; height: auto; margin: 0; @@ -7529,8 +7614,8 @@ table.bar_chart { .woocommerce-importer-file-url-field-wrapper { border: 1px solid #ddd; - -webkit-box-shadow: inset 0 1px 2px rgba( 0, 0, 0, 0.07 ); - box-shadow: inset 0 1px 2px rgba( 0, 0, 0, 0.07 ); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); background-color: #fff; color: #32373c; outline: 0; @@ -7577,7 +7662,7 @@ table.bar_chart { border: 2px solid #eee; border-radius: 4px; padding: 0; - box-shadow: 0 1px 0 0 rgba( 255, 255, 255, 0.2 ); + box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.2); } progress::-webkit-progress-bar { @@ -7590,25 +7675,25 @@ table.bar_chart { progress::-webkit-progress-value { border-radius: 3px; - box-shadow: inset 0 1px 1px 0 rgba( 255, 255, 255, 0.4 ); + box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4); background: #a46497; - background: linear-gradient( to bottom, #a46497, #66405f ), #a46497; + background: linear-gradient(to bottom, #a46497, #66405f), #a46497; transition: width 1s ease; } progress::-moz-progress-bar { border-radius: 3px; - box-shadow: inset 0 1px 1px 0 rgba( 255, 255, 255, 0.4 ); + box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4); background: #a46497; - background: linear-gradient( to bottom, #a46497, #66405f ), #a46497; + background: linear-gradient(to bottom, #a46497, #66405f), #a46497; transition: width 1s ease; } progress::-ms-fill { border-radius: 3px; - box-shadow: inset 0 1px 1px 0 rgba( 255, 255, 255, 0.4 ); + box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4); background: #a46497; - background: linear-gradient( to bottom, #a46497, #66405f ), #a46497; + background: linear-gradient(to bottom, #a46497, #66405f), #a46497; transition: width 1s ease; } @@ -7653,8 +7738,8 @@ table.bar_chart { } } - tbody tr:nth-child( odd ) td, - tbody tr:nth-child( odd ) th { + tbody tr:nth-child(odd) td, + tbody tr:nth-child(odd) th { background: #fbfbfb; } @@ -7700,7 +7785,7 @@ table.bar_chart { line-height: 1.75em; &::before { - @include icon( '\e015' ); + @include icon("\e015"); color: #a16696; position: static; font-size: 100px; @@ -7742,7 +7827,7 @@ table.bar_chart { } } -@media screen and ( min-width: 600px ) { +@media screen and (min-width: 600px) { .wc-addons-wrap { .marketplace-header { padding-left: 84px; @@ -7766,13 +7851,13 @@ table.bar_chart { } } -@media screen and ( min-width: 961px ) { +@media screen and (min-width: 961px) { .marketplace-header__tabs { margin-left: 84px; } } -@media screen and ( min-width: 1024px ) { +@media screen and (min-width: 1024px) { .current-section-name { display: none; } @@ -7847,7 +7932,6 @@ table.bar_chart { * Product Reviews */ .wp-list-table.product-reviews { - .column-author { width: 20%; } @@ -7856,7 +7940,7 @@ table.bar_chart { width: 10%; } - @media screen and ( max-width: 782px ) { + @media screen and (max-width: 782px) { th.column-type, td.column-type, th.column-author, @@ -7907,6 +7991,6 @@ table.bar_chart { } #post-status-info { margin: 0px 12px 12px; - width: calc( 100% - 24px ); + width: calc(100% - 24px); } } diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss index 0a3d6bad802..3613c74465f 100644 --- a/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss +++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss @@ -365,63 +365,6 @@ } - // Description/Additional info/Reviews tabs. - .woocommerce-tabs { - padding-top: var(--wp--style--block-gap); - - ul.wc-tabs { - padding: 0; - border-bottom-style: solid; - border-bottom-width: 1px; - border-bottom-color: #eae9eb; - - li { - opacity: 0.5; - color: var(--wp--preset--color--contrast); - margin: 0; - padding: 0.5em 1em 0.5em 1em; - border-color: #eae9eb; - border-top-left-radius: 5px; - border-top-right-radius: 5px; - float: left; - border-style: solid; - border-width: 1px; - font-weight: 600; - font-size: var(--wp--preset--font-size--medium); - - &:first-child { - border-left-color: #eae9eb; - margin-left: 1em; - } - - &.active { - background: var(--wp--preset--color--tertiary); - color: var(--wp--preset--color--contrast); - opacity: 1; - } - - a { - text-decoration: none; - color: var(--wp--preset--color--contrast); - } - } - } - - .woocommerce-Tabs-panel { - padding-top: var(--wp--style--block-gap); - font-size: var(--wp--preset--font-size--small); - margin-left: 1em; - - h2 { - display: none; - } - - table.woocommerce-product-attributes { - text-align: left; - } - } - } - // Reviews tab. .woocommerce-Reviews { ol.commentlist { @@ -593,6 +536,63 @@ } +// Description/Additional info/Reviews tabs. +.woocommerce-tabs { + padding-top: var(--wp--style--block-gap); +} + +ul.wc-tabs { + padding: 0; + border-bottom-style: solid; + border-bottom-width: 1px; + border-bottom-color: #eae9eb; + + li { + opacity: 0.5; + color: var(--wp--preset--color--contrast); + margin: 0; + padding: 0.5em 1em 0.5em 1em; + border-color: #eae9eb; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + float: left; + border-style: solid; + border-width: 1px; + font-weight: 600; + font-size: var(--wp--preset--font-size--medium); + + &:first-child { + border-left-color: #eae9eb; + margin-left: 1em; + } + + &.active { + background: var(--wp--preset--color--tertiary); + color: var(--wp--preset--color--contrast); + opacity: 1; + } + + a { + text-decoration: none; + color: var(--wp--preset--color--contrast); + } + } +} + +.woocommerce-Tabs-panel { + padding-top: var(--wp--style--block-gap); + font-size: var(--wp--preset--font-size--small); + margin-left: 1em; + + h2 { + display: none; + } + + table.woocommerce-product-attributes { + text-align: left; + } +} + .woocommerce-page { .woocommerce-cart-form { diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss index 1ae290e0ed2..be8519c50e1 100644 --- a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss +++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss @@ -369,63 +369,6 @@ $tt2-gray: #f7f7f7; } } - .woocommerce-tabs { - padding-top: var(--wp--style--block-gap); - - ul.wc-tabs { - padding: 0; - border-bottom-style: solid; - border-bottom-width: 1px; - border-bottom-color: #eae9eb; - - li { - background: #eae9eb; - margin: 0; - padding: 0.5em 1em 0.5em 1em; - border-color: #eae9eb; - border-top-left-radius: 5px; - border-top-right-radius: 5px; - float: left; - border-style: solid; - border-width: 1px; - border-left-color: var(--wp--preset--color--background); - font-weight: 600; - font-size: var(--wp--preset--font-size--medium); - - &:first-child { - border-left-color: #eae9eb; - margin-left: 1em; - } - - &.active { - box-shadow: 0 1px var(--wp--preset--color--background); - } - - a { - text-decoration: none; - } - } - } - - // Moved from blocktheme.scss to retain full styling. - ul.tabs li.active { - // Style active tab in theme colors. - background: var(--wp--preset--color--background, $contentbg); - border-bottom-color: var(--wp--preset--color--background, $contentbg); - } - - - .woocommerce-Tabs-panel { - padding-top: var(--wp--style--block-gap); - font-size: var(--wp--preset--font-size--small); - margin-left: 1em; - - h2 { - display: none; - } - } - } - .woocommerce-Reviews { ol.commentlist { list-style: none; @@ -603,6 +546,59 @@ $tt2-gray: #f7f7f7; } } +// Description/Additional info/Reviews tabs. +.woocommerce-tabs { + padding-top: var(--wp--style--block-gap); +} + +ul.wc-tabs { + padding: 0; + border-bottom-style: solid; + border-bottom-width: 1px; + border-bottom-color: #eae9eb; + + li { + background: #eae9eb; + margin: 0; + padding: 0.5em 1em 0.5em 1em; + border-color: #eae9eb; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + float: left; + border-style: solid; + border-width: 1px; + border-left-color: var(--wp--preset--color--background); + font-weight: 600; + font-size: var(--wp--preset--font-size--medium); + + &:first-child { + border-left-color: #eae9eb; + margin-left: 1em; + } + + &.active { + // Style active tab in theme colors. + background: var(--wp--preset--color--background, $contentbg); + border-bottom-color: var(--wp--preset--color--background, $contentbg); + box-shadow: 0 1px var(--wp--preset--color--background); + } + + a { + text-decoration: none; + } + } +} + +.woocommerce-Tabs-panel { + padding-top: var(--wp--style--block-gap); + font-size: var(--wp--preset--font-size--small); + margin-left: 1em; + + h2 { + display: none; + } +} + /** * Form fields */ diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js index 44e68b622e4..60f45e6f5e5 100644 --- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js +++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js @@ -488,6 +488,13 @@ jQuery( function ( $ ) { if ( response.success ) { $( '#woocommerce-order-items' ).find( '.inside' ).empty(); $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); + + // Update notes. + if ( response.data.notes_html ) { + $( 'ul.order_notes' ).empty(); + $( 'ul.order_notes' ).append( $( response.data.notes_html ).find( 'li' ) ); + } + wc_meta_boxes_order_items.reloaded_items(); wc_meta_boxes_order_items.unblock(); } else { @@ -524,6 +531,13 @@ jQuery( function ( $ ) { if ( response.success ) { $( '#woocommerce-order-items' ).find( '.inside' ).empty(); $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); + + // Update notes. + if ( response.data.notes_html ) { + $( 'ul.order_notes' ).empty(); + $( 'ul.order_notes' ).append( $( response.data.notes_html ).find( 'li' ) ); + } + wc_meta_boxes_order_items.reloaded_items(); wc_meta_boxes_order_items.unblock(); } else { @@ -1029,15 +1043,19 @@ jQuery( function ( $ ) { }, input_changed: function() { - var refund_amount = 0; - var $items = $( '.woocommerce_order_items' ).find( 'tr.item, tr.fee, tr.shipping' ); + var refund_amount = 0; + var $items = $( '.woocommerce_order_items' ).find( 'tr.item, tr.fee, tr.shipping' ); + var round_at_subtotal = 'yes' === woocommerce_admin_meta_boxes.round_at_subtotal; $items.each(function() { var $row = $( this ); var refund_cost_fields = $row.find( '.refund input:not(.refund_order_item_qty)' ); refund_cost_fields.each(function( index, el ) { - refund_amount += parseFloat( accounting.unformat( $( el ).val() || 0, woocommerce_admin.mon_decimal_point ) ); + var field_amount = accounting.unformat( $( el ).val() || 0, woocommerce_admin.mon_decimal_point ); + refund_amount += parseFloat( round_at_subtotal ? + field_amount : + accounting.formatNumber( field_amount, woocommerce_admin_meta_boxes.currency_format_num_decimals, '' ) ); }); }); diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js index b4ddb08d186..7f865a90066 100644 --- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js +++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js @@ -56,7 +56,11 @@ jQuery( function ( $ ) { ); }, - create_variations: function () { + create_variations: function ( event ) { + if ( $( this ).hasClass( 'disabled' ) ) { + event.preventDefault(); + return; + } var new_attribute_data = $( '.woocommerce_variation_new_attribute_data' ); @@ -126,7 +130,11 @@ jQuery( function ( $ ) { * @param {Int} qty */ reload: function () { - wc_meta_boxes_product_variations_ajax.load_variations( 1 ); + wc_meta_boxes_product_variations_ajax + .load_variations( 1 ) + .then( + wc_meta_boxes_product_variations_ajax.show_hide_variation_empty_state + ); wc_meta_boxes_product_variations_pagenav.set_paginav( 0 ); }, @@ -651,10 +659,22 @@ jQuery( function ( $ ) { } ); $( '.wc-metaboxes-wrapper' ).on( - 'click', - 'a.do_variation_action', + 'change', + '#field_to_edit', this.do_variation_action ); + + $( '.wc-metaboxes-wrapper' ).on( + 'click', + 'button.generate_variations', + this.generate_variations + ); + + $( '.wc-metaboxes-wrapper' ).on( + 'click', + 'button.add_variation_manually', + this.add_variation_manually + ); }, /** @@ -726,41 +746,50 @@ jQuery( function ( $ ) { * @param {Int} per_page (default: 10) */ load_variations: function ( page, per_page ) { - page = page || 1; - per_page = - per_page || - woocommerce_admin_meta_boxes_variations.variations_per_page; + return new Promise( ( resolve, reject ) => { + page = page || 1; + per_page = + per_page || + woocommerce_admin_meta_boxes_variations.variations_per_page; - var wrapper = $( '#variable_product_options' ).find( - '.woocommerce_variations' - ); + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ); - wc_meta_boxes_product_variations_ajax.block(); + wc_meta_boxes_product_variations_ajax.block(); - $.ajax( { - url: woocommerce_admin_meta_boxes_variations.ajax_url, - data: { - action: 'woocommerce_load_variations', - security: - woocommerce_admin_meta_boxes_variations.load_variations_nonce, - product_id: woocommerce_admin_meta_boxes_variations.post_id, - attributes: wrapper.data( 'attributes' ), - page: page, - per_page: per_page, - }, - type: 'POST', - success: function ( response ) { - wrapper - .empty() - .append( response ) - .attr( 'data-page', page ); + $.ajax( { + url: woocommerce_admin_meta_boxes_variations.ajax_url, + data: { + action: 'woocommerce_load_variations', + security: + woocommerce_admin_meta_boxes_variations.load_variations_nonce, + product_id: + woocommerce_admin_meta_boxes_variations.post_id, + attributes: wrapper.data( 'attributes' ), + page: page, + per_page: per_page, + }, + type: 'POST', + success: function ( response ) { + wrapper + .empty() + .append( response ) + .attr( 'data-page', page ); - $( '#woocommerce-product-data' ).trigger( - 'woocommerce_variations_loaded' - ); + $( '#woocommerce-product-data' ).trigger( + 'woocommerce_variations_loaded' + ); - wc_meta_boxes_product_variations_ajax.unblock(); - }, + wc_meta_boxes_product_variations_ajax.unblock(); + + resolve( response ); + }, + error: function ( jqXHR, textStatus, errorThrown ) { + wc_meta_boxes_product_variations_ajax.unblock(); + reject( { jqXHR, textStatus, errorThrown } ); + }, + } ); } ); }, @@ -972,6 +1001,7 @@ jQuery( function ( $ ) { 'woocommerce_variations_added', 1 ); + wc_meta_boxes_product_variations_ajax.show_hide_variation_empty_state(); wc_meta_boxes_product_variations_ajax.unblock(); } ); @@ -1104,6 +1134,8 @@ jQuery( function ( $ ) { ); } + wc_meta_boxes_product_variations_ajax.show_hide_variation_empty_state(); + if ( count > 0 ) { wc_meta_boxes_product_variations_pagenav.go_to_page( 1, @@ -1167,18 +1199,12 @@ jQuery( function ( $ ) { * Actions */ do_variation_action: function () { - var do_variation_action = $( 'select.variation_actions' ).val(), + var do_variation_action = $( this ).val(), data = {}, changes = 0, value; switch ( do_variation_action ) { - case 'add_variation': - wc_meta_boxes_product_variations_ajax.add_variation(); - return; - case 'link_all_variations': - wc_meta_boxes_product_variations_ajax.link_all_variations(); - return; case 'delete_all': if ( window.confirm( @@ -1311,6 +1337,36 @@ jQuery( function ( $ ) { }, } ); }, + + /** + * Show/hide variation empty state + */ + show_hide_variation_empty_state: function () { + var wrapper = $( '#variable_product_options' ).find( + '.woocommerce_variations' + ); + if ( parseInt( wrapper.attr( 'data-total' ) ) > 0 ) { + $( '.add-variation-container' ).addClass( 'hidden' ); + $( '#field_to_edit' ).removeClass( 'hidden' ); + } else { + $( '.add-variation-container' ).removeClass( 'hidden' ); + $( '#field_to_edit' ).addClass( 'hidden' ); + } + }, + + /** + * Generate variations + */ + generate_variations: function () { + wc_meta_boxes_product_variations_ajax.link_all_variations(); + }, + + /** + * Add variation + */ + add_variation_manually: function () { + wc_meta_boxes_product_variations_ajax.add_variation(); + }, }; /** @@ -1397,7 +1453,7 @@ jQuery( function ( $ ) { if ( page_nav.is( ':hidden' ) ) { $( 'option, optgroup', '.variation_actions' ).show(); - $( '.variation_actions' ).val( 'add_variation' ); + $( '.variation_actions' ).val( 'bulk_actions' ); $( '#variable_product_options' ).find( '.toolbar' ).show(); page_nav.show(); $( '.pagination-links', page_nav ).hide(); @@ -1444,13 +1500,13 @@ jQuery( function ( $ ) { toolbar.not( '.toolbar-top, .toolbar-buttons' ).hide(); page_nav.hide(); $( 'option, optgroup', variation_action ).hide(); - $( '.variation_actions' ).val( 'add_variation' ); + $( '.variation_actions' ).val( 'bulk_actions' ); $( 'option[data-global="true"]', variation_action ).show(); } else { toolbar.show(); page_nav.show(); $( 'option, optgroup', variation_action ).show(); - $( '.variation_actions' ).val( 'add_variation' ); + $( '.variation_actions' ).val( 'bulk_actions' ); // Show/hide links if ( 1 === total_pages ) { @@ -1536,7 +1592,11 @@ jQuery( function ( $ ) { selected, parseInt( wrapper.attr( 'data-total_pages' ), 10 ) ); - wc_meta_boxes_product_variations_ajax.load_variations( selected ); + wc_meta_boxes_product_variations_ajax + .load_variations( selected ) + .then( + wc_meta_boxes_product_variations_ajax.show_hide_variation_empty_state() + ); }, /** diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js index 3227cd4b7ed..efb439fb50e 100644 --- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js +++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js @@ -628,47 +628,56 @@ jQuery( function ( $ ) { } ); - $( '.product_attributes' ).on( 'click', '.remove_row', function () { - if ( window.confirm( woocommerce_admin_meta_boxes.remove_attribute ) ) { + $( '#product_attributes' ).on( + 'click', + '.product_attributes .remove_row', + function () { var $parent = $( this ).parent().parent(); + var confirmMessage = $parent + .find( 'input[name^="attribute_variation"]' ) + .is( ':checked' ) + ? woocommerce_admin_meta_boxes.i18n_remove_used_attribute_confirmation_message + : woocommerce_admin_meta_boxes.remove_attribute; - if ( $parent.is( '.taxonomy' ) ) { - $parent.find( 'select, input[type=text]' ).val( '' ); - $parent.hide(); - $( 'select.attribute_taxonomy' ) - .find( - 'option[value="' + $parent.data( 'taxonomy' ) + '"]' - ) - .prop( 'disabled', false ); - selectedAttributes = selectedAttributes.filter( - ( attr ) => attr !== $parent.data( 'taxonomy' ) - ); - $( 'select.wc-attribute-search' ).data( - 'disabled-items', - selectedAttributes - ); - } else { - $parent.find( 'select, input[type=text]' ).val( '' ); - $parent.hide(); - attribute_row_indexes(); + if ( window.confirm( confirmMessage ) ) { + if ( $parent.is( '.taxonomy' ) ) { + $parent.find( 'select, input[type=text]' ).val( '' ); + $parent.hide(); + $( 'select.attribute_taxonomy' ) + .find( + 'option[value="' + $parent.data( 'taxonomy' ) + '"]' + ) + .prop( 'disabled', false ); + selectedAttributes = selectedAttributes.filter( + ( attr ) => attr !== $parent.data( 'taxonomy' ) + ); + $( 'select.wc-attribute-search' ).data( + 'disabled-items', + selectedAttributes + ); + } else { + $parent.find( 'select, input[type=text]' ).val( '' ); + $parent.hide(); + attribute_row_indexes(); + } + + $parent.remove(); + + if ( + ! $( '.woocommerce_attribute_data' ).is( ':visible' ) && + ! $( 'div.add-global-attribute-container' ).hasClass( + 'hidden' + ) && + $( '.product_attributes' ).find( 'input, select, textarea' ) + .length === 0 + ) { + toggle_add_global_attribute_layout(); + } + jQuery.maybe_disable_save_button(); } - - $parent.remove(); - - if ( - ! $( '.woocommerce_attribute_data' ).is( ':visible' ) && - ! $( 'div.add-global-attribute-container' ).hasClass( - 'hidden' - ) && - $( '.product_attributes' ).find( 'input, select, textarea' ) - .length === 0 - ) { - toggle_add_global_attribute_layout(); - } - jQuery.maybe_disable_save_button(); + return false; } - return false; - } ); + ); // Attribute ordering. $( '.product_attributes' ).sortable( { @@ -750,7 +759,11 @@ jQuery( function ( $ ) { ); // Save attributes and update variations. - $( '.save_attributes' ).on( 'click', function () { + $( '.save_attributes' ).on( 'click', function ( event ) { + if ( $( this ).hasClass( 'disabled' ) ) { + event.preventDefault(); + return; + } $( '.product_attributes' ).block( { message: null, overlayCSS: { @@ -836,6 +849,18 @@ jQuery( function ( $ ) { } ); } ); + // Go to attributes tab when clicking on link in variations message + $( document.body ).on( + 'click', + '#variable_product_options .add-attributes-message a[href="#product_attributes"]', + function () { + $( + '#woocommerce-product-data .attribute_tab a[href="#product_attributes"]' + ).trigger( 'click' ); + return false; + } + ); + // Uploading files. var downloadable_file_frame; var file_path_field; diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes.js index 5e58c4a902a..f773e673ffa 100644 --- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes.js +++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes.js @@ -30,13 +30,16 @@ jQuery( function ( $ ) { * Function to maybe disable the save button. */ jQuery.maybe_disable_save_button = function () { - var $tab = $( '.product_attributes' ); - var $save_button = $( 'button.save_attributes' ); + var $tab; + var $save_button; if ( $( '.woocommerce_variation_new_attribute_data' ).is( ':visible' ) ) { $tab = $( '.woocommerce_variation_new_attribute_data' ); $save_button = $( 'button.create-variations' ); + } else { + var $tab = $( '.product_attributes' ); + var $save_button = $( 'button.save_attributes' ); } var attributes_and_variations_data = $tab.find( @@ -47,17 +50,14 @@ jQuery( function ( $ ) { attributes_and_variations_data ) ) { - if ( ! $save_button.is( ':disabled' ) ) { - $save_button.attr( 'disabled', 'disabled' ); - $save_button.attr( - 'title', - woocommerce_admin_meta_boxes.i18n_save_attribute_variation_tip - ); + if ( ! $save_button.hasClass( 'disabled' ) ) { + $save_button.addClass( 'disabled' ); + $save_button.attr( 'aria-disabled', true ); } - return; + } else { + $save_button.removeClass( 'disabled' ); + $save_button.removeAttr( 'aria-disabled' ); } - $save_button.removeAttr( 'disabled' ); - $save_button.removeAttr( 'title' ); }; // Run tipTip @@ -76,6 +76,30 @@ jQuery( function ( $ ) { runTipTip(); + $( '.save_attributes' ).tipTip( { + content: function () { + return $( '.save_attributes' ).hasClass( 'disabled' ) + ? woocommerce_admin_meta_boxes.i18n_save_attribute_variation_tip + : ''; + }, + fadeIn: 50, + fadeOut: 50, + delay: 200, + keepAlive: true, + } ); + + $( '.create-variations' ).tipTip( { + content: function () { + return $( '.create-variations' ).hasClass( 'disabled' ) + ? woocommerce_admin_meta_boxes.i18n_save_attribute_variation_tip + : ''; + }, + fadeIn: 50, + fadeOut: 50, + delay: 200, + keepAlive: true, + } ); + $( '.wc-metaboxes-wrapper' ).on( 'click', '.wc-metabox > h3', function () { var metabox = $( this ).parent( '.wc-metabox' ); @@ -153,15 +177,14 @@ jQuery( function ( $ ) { $( this ).find( '.wc-metabox-content' ).hide(); } ); - $( '.product_attributes, .woocommerce_variation_new_attribute_data' ).on( - 'keyup', - 'input, textarea', - jQuery.maybe_disable_save_button - ); - $( '#product_attributes' ).on( 'change', 'select.attribute_values', jQuery.maybe_disable_save_button ); + $( '#product_attributes, #variable_product_options' ).on( + 'keyup', + 'input, textarea', + jQuery.maybe_disable_save_button + ); } ); diff --git a/plugins/woocommerce/client/legacy/js/frontend/add-to-cart-variation.js b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart-variation.js index d5e6bfa26a6..ba2ba97eb25 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/add-to-cart-variation.js +++ b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart-variation.js @@ -167,16 +167,16 @@ if ( attributes.count && attributes.count === attributes.chosenCount ) { if ( form.useAjax ) { - if ( form.controller ) { - form.controller.abort(); + if ( form.xhr ) { + form.xhr.abort(); } form.$form.block( { message: null, overlayCSS: { background: '#fff', opacity: 0.6 } } ); currentAttributes.product_id = parseInt( form.$form.data( 'product_id' ), 10 ); currentAttributes.custom_data = form.$form.data( 'custom_data' ); - const options = { + form.xhr = $.ajax( { url: wc_add_to_cart_variation_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'get_variation' ), type: 'POST', - data: $.param( currentAttributes ), + data: currentAttributes, success: function( variation ) { if ( variation ) { form.$form.trigger( 'found_variation', [ variation ] ); @@ -199,25 +199,7 @@ complete: function() { form.$form.unblock(); } - }; - - const controller = new AbortController(); - form.controller = controller; - - window.fetch( options.url, { - method: options.type, - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, - body: options.data, - signal: controller.signal - } ) - .then( response => { - if ( !response.ok ) { - throw new Error( response.statusText ); - } - return response.json(); - }) - .then( options.success ) - .finally( () => options.complete() ); + } ); } else { form.$form.trigger( 'update_variation_values' ); diff --git a/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js index dac6676e560..e27baf072a4 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js +++ b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js @@ -51,21 +51,7 @@ jQuery( function( $ ) { } }; - const options = this.requests[0]; - window.fetch( options.url, { - method: options.type, - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, - body: options.data - } ) - .then( response => { - if ( !response.ok ) { - throw new Error( response.statusText ); - } - return response.json(); - } ) - .then( options.success ) - .catch( error => options.error && options.error() ) - .finally( () => options.complete && options.complete() ); + $.ajax( this.requests[0] ); }; /** @@ -109,7 +95,7 @@ jQuery( function( $ ) { e.data.addToCartHandler.addRequest({ type: 'POST', url: wc_add_to_cart_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'add_to_cart' ), - data: $.param( data ), + data: data, success: function( response ) { if ( ! response ) { return; @@ -153,9 +139,9 @@ jQuery( function( $ ) { e.data.addToCartHandler.addRequest({ type: 'POST', url: wc_add_to_cart_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'remove_from_cart' ), - data: new URLSearchParams( { + data: { cart_item_key : $thisbutton.data( 'cart_item_key' ) - } ).toString(), + }, success: function( response ) { if ( ! response || ! response.fragments ) { window.location = $thisbutton.attr( 'href' ); diff --git a/plugins/woocommerce/client/legacy/js/frontend/cart-fragments.js b/plugins/woocommerce/client/legacy/js/frontend/cart-fragments.js index b57b2618d84..c5eef7b4c72 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/cart-fragments.js +++ b/plugins/woocommerce/client/legacy/js/frontend/cart-fragments.js @@ -38,9 +38,9 @@ jQuery( function( $ ) { var $fragment_refresh = { url: wc_cart_fragments_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'get_refreshed_fragments' ), type: 'POST', - data: new URLSearchParams( { + data: { time: new Date().getTime() - } ).toString(), + }, timeout: wc_cart_fragments_params.request_timeout, success: function( data ) { if ( data && data.fragments ) { @@ -68,25 +68,7 @@ jQuery( function( $ ) { /* Named callback for refreshing cart fragment */ function refresh_cart_fragment() { - const controller = new AbortController(); - const timeoutId = setTimeout( () => controller.abort(), $fragment_refresh.timeout ); - - window.fetch( $fragment_refresh.url, { - method: $fragment_refresh.type, - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, - body: $fragment_refresh.data, - signal: controller.signal - } ) - .then( response => { - clearTimeout( timeoutId ); - - if ( !response.ok ) { - throw new Error( response.statusText ); - } - return response.json(); - } ) - .then( $fragment_refresh.success ) - .catch( error => $fragment_refresh.error() ); + $.ajax( $fragment_refresh ); } /* Cart Handling */ diff --git a/plugins/woocommerce/client/legacy/js/frontend/cart.js b/plugins/woocommerce/client/legacy/js/frontend/cart.js index 62524a6bd1e..cdee33f5d79 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/cart.js +++ b/plugins/woocommerce/client/legacy/js/frontend/cart.js @@ -8,27 +8,6 @@ jQuery( function( $ ) { // Utility functions for the file. - /** - * Perform an AJAX request that expects an HTML response. - * - * @param {Object} options - */ - const ajax = options => { - window.fetch( options.url, { - method: options.type || 'GET', - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, - body: options.data - } ) - .then( response => { - if ( !response.ok ) { - throw new Error( response.statusText ); - } - return response.text(); - } ) - .then( options.success ) - .finally( () => options.complete() ); - }; - /** * Gets a url for a given AJAX endpoint. * @@ -243,17 +222,13 @@ jQuery( function( $ ) { var data = { security: wc_cart_params.update_shipping_method_nonce, + shipping_method: shipping_methods }; - // Flatten shipping_methods for use in URLSearchParams() - for ( var k in shipping_methods ) { - data[ 'shipping_method[' + k + ']' ] = shipping_methods[ k ]; - } - - ajax( { + $.ajax( { type: 'post', url: get_url( 'update_shipping_method' ), - data: new URLSearchParams( data ).toString(), + data: data, dataType: 'html', success: function( response ) { update_cart_totals_div( response ); @@ -285,10 +260,10 @@ jQuery( function( $ ) { .appendTo( $form ); // Make call to actual form post URL. - ajax( { + $.ajax( { type: $form.attr( 'method' ), url: $form.attr( 'action' ), - data: new URLSearchParams( new FormData( $form[0] ) ).toString(), + data: $form.serialize(), dataType: 'html', success: function( response ) { update_wc_div( response ); @@ -372,10 +347,10 @@ jQuery( function( $ ) { block( $( 'div.cart_totals' ) ); // Make call to actual form post URL. - ajax( { + $.ajax( { type: $form.attr( 'method' ), url: $form.attr( 'action' ), - data: new URLSearchParams( new FormData( $form[0] ) ).toString(), + data: $form.serialize(), dataType: 'html', success: function( response ) { update_wc_div( response, preserve_notices ); @@ -394,7 +369,7 @@ jQuery( function( $ ) { update_cart_totals: function() { block( $( 'div.cart_totals' ) ); - ajax( { + $.ajax( { url: get_url( 'get_cart_totals' ), dataType: 'html', success: function( response ) { @@ -496,10 +471,10 @@ jQuery( function( $ ) { coupon_code: coupon_code }; - ajax( { + $.ajax( { type: 'POST', url: get_url( 'apply_coupon' ), - data: new URLSearchParams( data ).toString(), + data: data, dataType: 'html', success: function( response ) { $( '.woocommerce-error, .woocommerce-message, .woocommerce-info' ).remove(); @@ -533,10 +508,10 @@ jQuery( function( $ ) { coupon: coupon }; - ajax( { + $.ajax( { type: 'POST', url: get_url( 'remove_coupon' ), - data: new URLSearchParams( data ).toString(), + data: data, dataType: 'html', success: function( response ) { $( '.woocommerce-error, .woocommerce-message, .woocommerce-info' ).remove(); @@ -566,10 +541,10 @@ jQuery( function( $ ) { .appendTo( $form ); // Make call to actual form post URL. - ajax( { + $.ajax( { type: $form.attr( 'method' ), url: $form.attr( 'action' ), - data: new URLSearchParams( new FormData( $form[0] ) ).toString(), + data: $form.serialize(), dataType: 'html', success: function( response ) { update_wc_div( response ); @@ -596,7 +571,7 @@ jQuery( function( $ ) { block( $form ); block( $( 'div.cart_totals' ) ); - ajax( { + $.ajax( { type: 'GET', url: $a.attr( 'href' ), dataType: 'html', @@ -625,7 +600,7 @@ jQuery( function( $ ) { block( $form ); block( $( 'div.cart_totals' ) ); - ajax( { + $.ajax( { type: 'GET', url: $a.attr( 'href' ), dataType: 'html', diff --git a/plugins/woocommerce/client/legacy/js/frontend/checkout.js b/plugins/woocommerce/client/legacy/js/frontend/checkout.js index ff9044164c2..91a33b69329 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/checkout.js +++ b/plugins/woocommerce/client/legacy/js/frontend/checkout.js @@ -8,38 +8,6 @@ jQuery( function( $ ) { $.blockUI.defaults.overlayCSS.cursor = 'default'; - const ajax = options => { - const controller = new AbortController(); - - window.fetch( options.url, { - method: options.type, - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, - body: options.data, - signal: controller.signal - } ) - .then( response => { - response.text().then( text => { - if ( !response.ok ) { - const error = new Error( response.statusText ); - error.responseText = text; // Needed for when wc_checkout_params.debug_mode is enabled - throw error; - } - - if ( options.dataType === 'html' ) { - options.success( text ); - return; - } - - const json = JSON.parse( ajax.dataFilter( text, 'json' ) ); - options.success( json ); - } ); - } ) - .catch( error => options.error && options.error( error.responseText, 'error', error.message ) ); - - return controller; - }; - ajax.dataFilter = ( raw_response, dataType ) => raw_response; - var wc_checkout_form = { updateTimer: false, dirtyInput: false, @@ -288,12 +256,11 @@ jQuery( function( $ ) { wc_checkout_form.updateTimer = setTimeout( wc_checkout_form.update_checkout_action, '5', args ); }, update_checkout_action: function( args ) { - if ( wc_checkout_form.controller ) { - wc_checkout_form.controller.abort(); + if ( wc_checkout_form.xhr ) { + wc_checkout_form.xhr.abort(); } - var $form = $( 'form.checkout' ); - if ( $form.length === 0 ) { + if ( $( 'form.checkout' ).length === 0 ) { return; } @@ -349,7 +316,7 @@ jQuery( function( $ ) { s_address : s_address, s_address_2 : s_address_2, has_full_address: has_full_address, - post_data : new URLSearchParams( new FormData( $form[0] ) ).toString() + post_data : $( 'form.checkout' ).serialize() }; if ( false !== args.update_shipping_method ) { @@ -360,10 +327,7 @@ jQuery( function( $ ) { shipping_methods[ $( this ).data( 'index' ) ] = $( this ).val(); } ); - // Flatten shipping_methods for use in URLSearchParams() - for ( var k in shipping_methods ) { - data[ 'shipping_method[' + k + ']' ] = shipping_methods[ k ]; - } + data.shipping_method = shipping_methods; } $( '.woocommerce-checkout-payment, .woocommerce-checkout-review-order-table' ).block({ @@ -374,10 +338,10 @@ jQuery( function( $ ) { } }); - wc_checkout_form.controller = ajax({ + wc_checkout_form.xhr = $.ajax({ type: 'POST', url: wc_checkout_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'update_order_review' ), - data: new URLSearchParams( data ).toString(), + data: data, success: function( data ) { // Reload the page if requested @@ -440,6 +404,8 @@ jQuery( function( $ ) { // Check for error if ( data && 'failure' === data.result ) { + var $form = $( 'form.checkout' ); + // Remove notices from all sources $( '.woocommerce-error, .woocommerce-message' ).remove(); @@ -520,37 +486,39 @@ jQuery( function( $ ) { // Attach event to block reloading the page when the form has been submitted wc_checkout_form.attachUnloadEventsOnSubmit(); - // ajax.dataFilter affects all ajax() calls, but we use it to ensure JSON is valid once returned. - ajax.dataFilter = function( raw_response, dataType ) { - // We only want to work with JSON - if ( 'json' !== dataType ) { - return raw_response; - } - - if ( wc_checkout_form.is_valid_json( raw_response ) ) { - return raw_response; - } else { - // Attempt to fix the malformed JSON - var maybe_valid_json = raw_response.match( /{"result.*}/ ); - - if ( null === maybe_valid_json ) { - console.log( 'Unable to fix malformed JSON' ); - } else if ( wc_checkout_form.is_valid_json( maybe_valid_json[0] ) ) { - console.log( 'Fixed malformed JSON. Original:' ); - console.log( raw_response ); - raw_response = maybe_valid_json[0]; - } else { - console.log( 'Unable to fix malformed JSON' ); + // ajaxSetup is global, but we use it to ensure JSON is valid once returned. + $.ajaxSetup( { + dataFilter: function( raw_response, dataType ) { + // We only want to work with JSON + if ( 'json' !== dataType ) { + return raw_response; } + + if ( wc_checkout_form.is_valid_json( raw_response ) ) { + return raw_response; + } else { + // Attempt to fix the malformed JSON + var maybe_valid_json = raw_response.match( /{"result.*}/ ); + + if ( null === maybe_valid_json ) { + console.log( 'Unable to fix malformed JSON' ); + } else if ( wc_checkout_form.is_valid_json( maybe_valid_json[0] ) ) { + console.log( 'Fixed malformed JSON. Original:' ); + console.log( raw_response ); + raw_response = maybe_valid_json[0]; + } else { + console.log( 'Unable to fix malformed JSON' ); + } + } + + return raw_response; } + } ); - return raw_response; - }; - - ajax({ + $.ajax({ type: 'POST', url: wc_checkout_params.checkout_url, - data: new URLSearchParams( new FormData( $form[0] ) ).toString(), + data: $form.serialize(), dataType: 'json', success: function( result ) { // Detach the unload handler that prevents a reload / redirect @@ -588,7 +556,7 @@ jQuery( function( $ ) { } } }, - error: function( responseText, textStatus, errorThrown ) { + error: function( jqXHR, textStatus, errorThrown ) { // Detach the unload handler that prevents a reload / redirect wc_checkout_form.detachUnloadEventsOnSubmit(); @@ -653,10 +621,10 @@ jQuery( function( $ ) { coupon_code: $form.find( 'input[name="coupon_code"]' ).val() }; - ajax({ + $.ajax({ type: 'POST', url: wc_checkout_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'apply_coupon' ), - data: new URLSearchParams( data ).toString(), + data: data, success: function( code ) { $( '.woocommerce-error, .woocommerce-message' ).remove(); $form.removeClass( 'processing' ).unblock(); @@ -693,10 +661,10 @@ jQuery( function( $ ) { coupon: coupon }; - ajax({ + $.ajax({ type: 'POST', url: wc_checkout_params.wc_ajax_url.toString().replace( '%%endpoint%%', 'remove_coupon' ), - data: new URLSearchParams( data ).toString(), + data: data, success: function( code ) { $( '.woocommerce-error, .woocommerce-message' ).remove(); container.removeClass( 'processing' ).unblock(); @@ -711,10 +679,10 @@ jQuery( function( $ ) { $( 'form.checkout_coupon' ).find( 'input[name="coupon_code"]' ).val( '' ); } }, - error: function ( responseText ) { + error: function ( jqXHR ) { if ( wc_checkout_params.debug_mode ) { /* jshint devel: true */ - console.log( responseText ); + console.log( jqXHR.responseText ); } }, dataType: 'html' diff --git a/plugins/woocommerce/client/legacy/js/frontend/geolocation.js b/plugins/woocommerce/client/legacy/js/frontend/geolocation.js index 29f8435f9df..e7029a3539d 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/geolocation.js +++ b/plugins/woocommerce/client/legacy/js/frontend/geolocation.js @@ -127,17 +127,7 @@ jQuery( function( $ ) { // Get the current geo hash. If it doesn't exist, or if it doesn't match the current // page URL, perform a geolocation request. if ( ! get_geo_hash() || needs_refresh() ) { - window.fetch( $geolocate_customer.url, { - method: $geolocate_customer.type, - headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } - } ) - .then( response => { - if ( !response.ok ) { - throw new Error( response.statusText ); - } - return response.json(); - } ) - .then( $geolocate_customer.success ); + $.ajax( $geolocate_customer ); } // Page updates. diff --git a/plugins/woocommerce/client/legacy/js/jquery-tiptip/jquery.tipTip.js b/plugins/woocommerce/client/legacy/js/jquery-tiptip/jquery.tipTip.js index 6672f09fc07..8a2d1acc582 100644 --- a/plugins/woocommerce/client/legacy/js/jquery-tiptip/jquery.tipTip.js +++ b/plugins/woocommerce/client/legacy/js/jquery-tiptip/jquery.tipTip.js @@ -4,7 +4,10 @@ * www.drewwilson.com * code.drewwilson.com/entry/tiptip-jquery-plugin * - * Version 1.3 - Updated: Mar. 23, 2010 + * Version 1.3.1 - Updated: Mar. 30, 2023 + * + * This is a custom version of TipTip. This file has been locally modified for specific requirements. + * Since the original version is no longer maintained, the changes were not submitted back to the original author. * * This Plug-In will create a custom tooltip to replace the default * browser tooltip. It is extremely lightweight and very smart in @@ -31,7 +34,7 @@ fadeIn: 200, fadeOut: 200, attribute: "title", - content: false, // HTML or String to fill TipTIp with + content: false, // HTML or String or callback to fill TipTIp with enter: function(){}, exit: function(){} }; @@ -98,8 +101,12 @@ } function active_tiptip(){ + var content = typeof opts.content === 'function' ? opts.content() : org_title; + if (!content) { + return; + } opts.enter.call(this); - tiptip_content.html(org_title); + tiptip_content.html(content); tiptip_holder.hide().css("margin","0"); tiptip_holder.removeAttr('class'); tiptip_arrow.removeAttr("style"); diff --git a/plugins/woocommerce/client/legacy/package.json b/plugins/woocommerce/client/legacy/package.json index 58ddd7fc8c9..b38dcb19d08 100644 --- a/plugins/woocommerce/client/legacy/package.json +++ b/plugins/woocommerce/client/legacy/package.json @@ -7,9 +7,11 @@ "main": "Gruntfile.js", "scripts": { "turbo:build": "grunt assets", - "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name" + "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", + "start": "nodemon --watch js --watch css -e js,scss --exec pnpm build" }, "devDependencies": { + "@types/node": "^16.18.18", "@wordpress/stylelint-config": "19.1.0", "autoprefixer": "9.8.6", "browserslist": "4.19.3", @@ -29,6 +31,7 @@ "grunt-sass": "3.1.0", "grunt-stylelint": "0.16.0", "gruntify-eslint": "5.0.0", + "nodemon": "^2.0.21", "sass": "^1.45.0", "stylelint": "13.8.0" } diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index 5fce28d1d98..cdac34076f3 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -2,7 +2,7 @@ "name": "woocommerce/woocommerce", "description": "An eCommerce toolkit that helps you sell anything. Beautifully.", "homepage": "https://woocommerce.com/", - "version": "7.6.0", + "version": "7.7.0", "type": "wordpress-plugin", "license": "GPL-3.0-or-later", "prefer-stable": true, @@ -14,21 +14,21 @@ } ], "require": { - "php": ">=7.2", + "php": ">=7.3", "automattic/jetpack-autoloader": "2.10.1", "automattic/jetpack-constants": "1.5.1", "composer/installers": "^1.9", "maxmind-db/reader": "^1.11", "pelago/emogrifier": "^6.0", "woocommerce/action-scheduler": "3.5.4", - "woocommerce/woocommerce-blocks": "9.6.5" + "woocommerce/woocommerce-blocks": "9.8.4" }, "require-dev": { "automattic/jetpack-changelogger": "^3.3.0", "bamarni/composer-bin-plugin": "^1.4", "dms/phpunit-arraysubset-asserts": "^0.4.0", - "phpunit/phpunit": "^8.0", - "sebastian/comparator": "3.0.3", + "phpunit/phpunit": "^9.0", + "sebastian/comparator": "^4.0", "yoast/phpunit-polyfills": "^1.0" }, "config": { @@ -40,7 +40,7 @@ }, "sort-packages": true, "platform": { - "php": "7.2" + "php": "7.3" }, "allow-plugins": { "automattic/jetpack-autoloader": true, diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock index c2af5fe76f9..570a718d4b3 100644 --- a/plugins/woocommerce/composer.lock +++ b/plugins/woocommerce/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f08d44536a1162957e0edb212f042956", + "content-hash": "6267272b0deee3fb00d6f72b07b199c4", "packages": [ { "name": "automattic/jetpack-autoloader", @@ -439,20 +439,20 @@ }, { "name": "symfony/css-selector", - "version": "v4.4.44", + "version": "v5.4.21", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "bd0a6737e48de45b4b0b7b6fc98c78404ddceaed" + "reference": "95f3c7468db1da8cc360b24fa2a26e7cefcb355d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/bd0a6737e48de45b4b0b7b6fc98c78404ddceaed", - "reference": "bd0a6737e48de45b4b0b7b6fc98c78404ddceaed", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/95f3c7468db1da8cc360b24fa2a26e7cefcb355d", + "reference": "95f3c7468db1da8cc360b24fa2a26e7cefcb355d", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", "symfony/polyfill-php80": "^1.16" }, "type": "library", @@ -485,7 +485,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v4.4.44" + "source": "https://github.com/symfony/css-selector/tree/v5.4.21" }, "funding": [ { @@ -501,7 +501,7 @@ "type": "tidelift" } ], - "time": "2022-06-27T13:16:42+00:00" + "time": "2023-02-14T08:03:56+00:00" }, { "name": "symfony/polyfill-php80", @@ -628,16 +628,16 @@ }, { "name": "woocommerce/woocommerce-blocks", - "version": "v9.6.5", + "version": "9.8.4", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-blocks.git", - "reference": "0d316bd6a7edd18a4af6fa55406b1279b18b28a4" + "reference": "bdb2e6ab2288f980a7fa75fa46dc846fd0c6b31f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/0d316bd6a7edd18a4af6fa55406b1279b18b28a4", - "reference": "0d316bd6a7edd18a4af6fa55406b1279b18b28a4", + "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/bdb2e6ab2288f980a7fa75fa46dc846fd0c6b31f", + "reference": "bdb2e6ab2288f980a7fa75fa46dc846fd0c6b31f", "shasum": "" }, "require": { @@ -648,7 +648,7 @@ }, "require-dev": { "mockery/mockery": "^1.4", - "woocommerce/woocommerce-sniffs": "0.1.0", + "woocommerce/woocommerce-sniffs": "0.1.3", "wp-hooks/generator": "^0.9.0", "wp-phpunit/wp-phpunit": "^6.0", "yoast/phpunit-polyfills": "^1.0" @@ -683,9 +683,9 @@ ], "support": { "issues": "https://github.com/woocommerce/woocommerce-blocks/issues", - "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v9.6.5" + "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v9.8.4" }, - "time": "2023-03-06T15:42:05+00:00" + "time": "2023-03-29T12:01:19+00:00" } ], "packages-dev": [ @@ -748,29 +748,36 @@ }, { "name": "bamarni/composer-bin-plugin", - "version": "v1.5.0", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/bamarni/composer-bin-plugin.git", - "reference": "49934ffea764864788334c1485fbb08a4b852031" + "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/49934ffea764864788334c1485fbb08a4b852031", - "reference": "49934ffea764864788334c1485fbb08a4b852031", + "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880", + "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": "^5.5.9 || ^7.0 || ^8.0" + "composer-plugin-api": "^2.0", + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "composer/composer": "^1.0 || ^2.0", - "symfony/console": "^2.5 || ^3.0 || ^4.0" + "composer/composer": "^2.0", + "ext-json": "*", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0" }, "type": "composer-plugin", "extra": { - "class": "Bamarni\\Composer\\Bin\\Plugin" + "class": "Bamarni\\Composer\\Bin\\BamarniBinPlugin" }, "autoload": { "psr-4": { @@ -792,9 +799,9 @@ ], "support": { "issues": "https://github.com/bamarni/composer-bin-plugin/issues", - "source": "https://github.com/bamarni/composer-bin-plugin/tree/v1.5.0" + "source": "https://github.com/bamarni/composer-bin-plugin/tree/1.8.2" }, - "time": "2022-02-22T21:01:25+00:00" + "time": "2022-10-31T08:38:03+00:00" }, { "name": "dms/phpunit-arraysubset-asserts", @@ -913,16 +920,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -960,7 +967,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -968,7 +975,63 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.15.4", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" + }, + "time": "2023-03-05T19:49:14+00:00" }, { "name": "phar-io/manifest", @@ -1083,40 +1146,44 @@ }, { "name": "phpunit/php-code-coverage", - "version": "7.0.15", + "version": "9.2.26", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "819f92bba8b001d4363065928088de22f25a3a48" + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/819f92bba8b001d4363065928088de22f25a3a48", - "reference": "819f92bba8b001d4363065928088de22f25a3a48", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-xmlwriter": "*", - "php": ">=7.2", - "phpunit/php-file-iterator": "^2.0.2", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.1.3 || ^4.0", - "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^4.2.2", - "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1.3" + "nikic/php-parser": "^4.15", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" }, "require-dev": { - "phpunit/phpunit": "^8.2.2" + "phpunit/phpunit": "^9.3" }, "suggest": { - "ext-xdebug": "^2.7.2" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "7.0-dev" + "dev-master": "9.2-dev" } }, "autoload": { @@ -1144,7 +1211,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.15" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" }, "funding": [ { @@ -1152,32 +1219,32 @@ "type": "github" } ], - "time": "2021-07-26T12:20:09+00:00" + "time": "2023-03-06T12:58:08+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "2.0.5", + "version": "3.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5" + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5", - "reference": "42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^8.5" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1204,7 +1271,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.5" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" }, "funding": [ { @@ -1212,26 +1279,97 @@ "type": "github" } ], - "time": "2021-12-02T12:42:26+00:00" + "time": "2021-12-02T12:48:52+00:00" }, { - "name": "phpunit/php-text-template", - "version": "1.2.1", + "name": "phpunit/php-invoker", + "version": "3.1.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, "autoload": { "classmap": [ "src/" @@ -1255,34 +1393,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" }, - "time": "2015-06-21T13:50:34+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" }, { "name": "phpunit/php-timer", - "version": "2.1.3", + "version": "5.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662" + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662", - "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^8.5" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -1308,7 +1452,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3" + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" }, "funding": [ { @@ -1316,112 +1460,54 @@ "type": "github" } ], - "time": "2020-11-30T08:20:02+00:00" - }, - { - "name": "phpunit/php-token-stream", - "version": "3.1.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "9c1da83261628cb24b6a6df371b6e312b3954768" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9c1da83261628cb24b6a6df371b6e312b3954768", - "reference": "9c1da83261628cb24b6a6df371b6e312b3954768", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", - "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "abandoned": true, - "time": "2021-07-26T12:15:06+00:00" + "time": "2020-10-26T13:16:10+00:00" }, { "name": "phpunit/phpunit", - "version": "8.5.29", + "version": "9.6.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e8c563c47a9a303662955518ca532b022b337f4d" + "reference": "86e761949019ae83f49240b2f2123fb5ab3b2fc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e8c563c47a9a303662955518ca532b022b337f4d", - "reference": "e8c563c47a9a303662955518ca532b022b337f4d", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/86e761949019ae83f49240b2f2123fb5ab3b2fc5", + "reference": "86e761949019ae83f49240b2f2123fb5ab3b2fc5", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", + "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.0", + "myclabs/deep-copy": "^1.10.1", "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", - "php": ">=7.2", - "phpunit/php-code-coverage": "^7.0.12", - "phpunit/php-file-iterator": "^2.0.4", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^2.1.2", - "sebastian/comparator": "^3.0.2", - "sebastian/diff": "^3.0.2", - "sebastian/environment": "^4.2.3", - "sebastian/exporter": "^3.1.2", - "sebastian/global-state": "^3.0.0", - "sebastian/object-enumerator": "^3.0.3", - "sebastian/resource-operations": "^2.0.1", - "sebastian/type": "^1.1.3", - "sebastian/version": "^2.0.1" + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*", - "phpunit/php-invoker": "^2.0.0" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -1429,10 +1515,13 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "8.5-dev" + "dev-master": "9.6-dev" } }, "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], "classmap": [ "src/" ] @@ -1457,7 +1546,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.29" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.5" }, "funding": [ { @@ -1467,36 +1556,35 @@ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2022-08-22T13:59:39+00:00" + "time": "2023-03-09T06:34:10+00:00" }, { - "name": "psr/log", - "version": "1.1.4", + "name": "psr/container", + "version": "1.1.1", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + "url": "https://github.com/php-fig/container.git", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.2.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Container\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1509,42 +1597,157 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "log", - "psr", - "psr-3" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.1" }, - "time": "2021-05-03T11:20:27+00:00" + "time": "2021-03-05T17:36:06+00:00" }, { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.2", + "name": "sebastian/cli-parser", + "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", - "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^8.5" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" } }, "autoload": { @@ -1566,7 +1769,7 @@ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" }, "funding": [ { @@ -1574,34 +1777,34 @@ "type": "github" } ], - "time": "2020-11-30T08:15:22+00:00" + "time": "2020-09-28T05:30:19+00:00" }, { "name": "sebastian/comparator", - "version": "3.0.3", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "1071dfcef776a57013124ff35e1fc41ccd294758" + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758", - "reference": "1071dfcef776a57013124ff35e1fc41ccd294758", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", "shasum": "" }, "require": { - "php": ">=7.1", - "sebastian/diff": "^3.0", - "sebastian/exporter": "^3.1" + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^8.5" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1640,7 +1843,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" }, "funding": [ { @@ -1648,33 +1851,90 @@ "type": "github" } ], - "time": "2020-11-30T08:04:30+00:00" + "time": "2022-09-14T12:41:17+00:00" }, { - "name": "sebastian/diff", - "version": "3.0.3", + "name": "sebastian/complexity", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211" + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211", - "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", "shasum": "" }, "require": { - "php": ">=7.1" + "nikic/php-parser": "^4.7", + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.0", - "symfony/process": "^2 || ^3.3 || ^4" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" } }, "autoload": { @@ -1706,7 +1966,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" }, "funding": [ { @@ -1714,27 +1974,27 @@ "type": "github" } ], - "time": "2020-11-30T07:59:04+00:00" + "time": "2020-10-26T13:10:38+00:00" }, { "name": "sebastian/environment", - "version": "4.2.4", + "version": "5.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", - "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^7.5" + "phpunit/phpunit": "^9.3" }, "suggest": { "ext-posix": "*" @@ -1742,7 +2002,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.2-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -1769,7 +2029,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { @@ -1777,34 +2037,34 @@ "type": "github" } ], - "time": "2020-11-30T07:53:42+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", - "version": "3.1.5", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6" + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/73a9676f2833b9a7c36968f9d882589cd75511e6", - "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { - "php": ">=7.0", - "sebastian/recursion-context": "^3.0" + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "^8.5" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1839,14 +2099,14 @@ } ], "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.5" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" }, "funding": [ { @@ -1854,30 +2114,30 @@ "type": "github" } ], - "time": "2022-09-14T06:00:17+00:00" + "time": "2022-09-14T06:03:37+00:00" }, { "name": "sebastian/global-state", - "version": "3.0.2", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "de036ec91d55d2a9e0db2ba975b512cdb1c23921" + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/de036ec91d55d2a9e0db2ba975b512cdb1c23921", - "reference": "de036ec91d55d2a9e0db2ba975b512cdb1c23921", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", "shasum": "" }, "require": { - "php": ">=7.2", - "sebastian/object-reflector": "^1.1.1", - "sebastian/recursion-context": "^3.0" + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^8.0" + "phpunit/phpunit": "^9.3" }, "suggest": { "ext-uopz": "*" @@ -1885,7 +2145,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -1910,7 +2170,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" }, "funding": [ { @@ -1918,34 +2178,91 @@ "type": "github" } ], - "time": "2022-02-10T06:55:38+00:00" + "time": "2022-02-14T08:28:10+00:00" }, { - "name": "sebastian/object-enumerator", - "version": "3.0.4", + "name": "sebastian/lines-of-code", + "version": "1.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2" + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", - "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", "shasum": "" }, "require": { - "php": ">=7.0", - "sebastian/object-reflector": "^1.1.1", - "sebastian/recursion-context": "^3.0" + "nikic/php-parser": "^4.6", + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" } }, "autoload": { @@ -1967,7 +2284,7 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" }, "funding": [ { @@ -1975,32 +2292,32 @@ "type": "github" } ], - "time": "2020-11-30T07:40:27+00:00" + "time": "2020-10-26T13:12:34+00:00" }, { "name": "sebastian/object-reflector", - "version": "1.1.2", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d" + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", - "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -2022,7 +2339,7 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" }, "funding": [ { @@ -2030,32 +2347,32 @@ "type": "github" } ], - "time": "2020-11-30T07:37:18+00:00" + "time": "2020-10-26T13:14:26+00:00" }, { "name": "sebastian/recursion-context", - "version": "3.0.1", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb" + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb", - "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { - "php": ">=7.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -2082,10 +2399,10 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { @@ -2093,29 +2410,32 @@ "type": "github" } ], - "time": "2020-11-30T07:34:24+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { "name": "sebastian/resource-operations", - "version": "2.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3" + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3", - "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -2137,7 +2457,7 @@ "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" }, "funding": [ { @@ -2145,32 +2465,32 @@ "type": "github" } ], - "time": "2020-11-30T07:30:19+00:00" + "time": "2020-09-28T06:45:17+00:00" }, { "name": "sebastian/type", - "version": "1.1.4", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4" + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0150cfbc4495ed2df3872fb31b26781e4e077eb4", - "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^8.2" + "phpunit/phpunit": "^9.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -2193,7 +2513,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/1.1.4" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { @@ -2201,29 +2521,29 @@ "type": "github" } ], - "time": "2020-11-30T07:25:11+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { "name": "sebastian/version", - "version": "2.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + "reference": "c6c1022351a901512170118436c764e473f6de8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=7.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -2246,43 +2566,58 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/master" + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" }, - "time": "2016-10-03T07:35:21+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" }, { "name": "symfony/console", - "version": "v3.4.47", + "version": "v5.4.21", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81" + "reference": "c77433ddc6cdc689caf48065d9ea22ca0853fbd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a10b1da6fc93080c180bba7219b5ff5b7518fe81", - "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81", + "url": "https://api.github.com/repos/symfony/console/zipball/c77433ddc6cdc689caf48065d9ea22ca0853fbd9", + "reference": "c77433ddc6cdc689caf48065d9ea22ca0853fbd9", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/debug": "~2.8|~3.0|~4.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" }, "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/process": "<3.3" + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" }, "provide": { - "psr/log-implementation": "1.0" + "psr/log-implementation": "1.0|2.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" + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" }, "suggest": { "psr/log": "For using the console logger", @@ -2313,10 +2648,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Console Component", + "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], "support": { - "source": "https://github.com/symfony/console/tree/v3.4.47" + "source": "https://github.com/symfony/console/tree/v5.4.21" }, "funding": [ { @@ -2332,39 +2673,38 @@ "type": "tidelift" } ], - "time": "2020-10-24T10:57:07+00:00" + "time": "2023-02-25T16:59:41+00:00" }, { - "name": "symfony/debug", - "version": "v4.4.44", + "name": "symfony/deprecation-contracts", + "version": "v2.5.2", "source": { "type": "git", - "url": "https://github.com/symfony/debug.git", - "reference": "1a692492190773c5310bc7877cb590c04c2f05be" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/1a692492190773c5310bc7877cb590c04c2f05be", - "reference": "1a692492190773c5310bc7877cb590c04c2f05be", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", "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" + "php": ">=7.1" }, "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Debug\\": "" + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" }, - "exclude-from-classmap": [ - "/Tests/" + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2373,18 +2713,18 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides tools to ease debugging PHP code", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/debug/tree/v4.4.44" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" }, "funding": [ { @@ -2400,8 +2740,254 @@ "type": "tidelift" } ], - "abandoned": "symfony/error-handler", - "time": "2022-07-28T16:29:46+00:00" + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.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": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", + "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "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 intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.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": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "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 intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.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": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -2487,21 +3073,101 @@ "time": "2022-11-03T14:55:06+00:00" }, { - "name": "symfony/process", - "version": "v3.4.47", + "name": "symfony/polyfill-php73", + "version": "v1.27.0", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca" + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/b8648cf1d5af12a44a51d07ef9bf980921f15fca", - "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9", + "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8" + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "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 backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.27.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": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/process", + "version": "v5.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "d4ce417ebcb0b7d090b4c178ed6d3accc518e8bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/d4ce417ebcb0b7d090b4c178ed6d3accc518e8bd", + "reference": "d4ce417ebcb0b7d090b4c178ed6d3accc518e8bd", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" }, "type": "library", "autoload": { @@ -2526,10 +3192,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Process Component", + "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v3.4.47" + "source": "https://github.com/symfony/process/tree/v5.4.21" }, "funding": [ { @@ -2545,7 +3211,176 @@ "type": "tidelift" } ], - "time": "2020-10-24T10:57:07+00:00" + "time": "2023-02-21T19:46:44+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "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": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" + }, + "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": "2022-05-30T19:17:29+00:00" + }, + { + "name": "symfony/string", + "version": "v5.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "edac10d167b78b1d90f46a80320d632de0bd9f2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/edac10d167b78b1d90f46a80320d632de0bd9f2f", + "reference": "edac10d167b78b1d90f46a80320d632de0bd9f2f", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "conflict": { + "symfony/translation-contracts": ">=3.0" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.4.21" + }, + "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-02-22T08:00:55+00:00" }, { "name": "theseer/tokenizer", @@ -2599,28 +3434,28 @@ }, { "name": "wikimedia/at-ease", - "version": "v2.0.0", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/wikimedia/at-ease.git", - "reference": "013ac61929797839c80a111a3f1a4710d8248e7a" + "reference": "e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wikimedia/at-ease/zipball/013ac61929797839c80a111a3f1a4710d8248e7a", - "reference": "013ac61929797839c80a111a3f1a4710d8248e7a", + "url": "https://api.github.com/repos/wikimedia/at-ease/zipball/e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33", + "reference": "e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33", "shasum": "" }, "require": { - "php": ">=5.6.99" + "php": ">=7.2.9" }, "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" + "mediawiki/mediawiki-codesniffer": "35.0.0", + "mediawiki/minus-x": "1.1.1", + "ockcyp/covers-validator": "1.3.3", + "php-parallel-lint/php-console-highlighter": "0.5.0", + "php-parallel-lint/php-parallel-lint": "1.2.0", + "phpunit/phpunit": "^8.5" }, "type": "library", "autoload": { @@ -2648,9 +3483,9 @@ "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" + "source": "https://github.com/wikimedia/at-ease/tree/v2.1.0" }, - "time": "2018-10-10T15:39:06+00:00" + "time": "2021-02-27T15:53:37+00:00" }, { "name": "yoast/phpunit-polyfills", @@ -2720,11 +3555,11 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=7.2" + "php": ">=7.3" }, "platform-dev": [], "platform-overrides": { - "php": "7.2" + "php": "7.3" }, "plugin-api-version": "2.3.0" } diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php index ae3bbb8a5f1..717800b4c4c 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php @@ -131,6 +131,17 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { } } + /** + * This method overwrites the base class's clone method to make it a no-op. In base class WC_Data, we are unsetting the meta_id to clone. + * It seems like this was done to avoid conflicting the metadata when duplicating products. However, doing that does not seems necessary for orders. + * In-fact, when we do that for orders, we lose the capability to clone orders with custom meta data by caching plugins. This is because, when we clone an order object for caching, it will clone the metadata without the ID. Unfortunately, when this cached object with nulled meta ID is retreived, WC_Data will consider it as a new meta and will insert it as a new meta-data causing duplicates. + * + * Eventually, we should move away from overwriting the __clone method in base class itself, since it's easily possible to still duplicate the product without having to hook into the __clone method. + * + * @since 7.6.0 + */ + public function __clone() {} + /** * Get internal type. * @@ -207,7 +218,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { if ( OrderUtil::orders_cache_usage_is_enabled() ) { $order_cache = wc_get_container()->get( OrderCache::class ); - $order_cache->update_if_cached( $this ); + $order_cache->remove( $this->get_id() ); } /** @@ -1285,9 +1296,11 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { * Manual discounts are not affected; those are separate and do not affect * stored line totals. * - * @since 3.2.0 + * @since 3.2.0 + * @since 7.6.0 Returns a boolean indicating success. + * * @param string $code Coupon code. - * @return void + * @return bool TRUE if coupon was removed, FALSE otherwise. */ public function remove_coupon( $code ) { $coupons = $this->get_items( 'coupon' ); @@ -1299,9 +1312,12 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { $coupon_object = new WC_Coupon( $code ); $coupon_object->decrease_usage_count( $this->get_user_id() ); $this->recalculate_coupons(); - break; + + return true; } } + + return false; } /** @@ -2169,7 +2185,13 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { */ public function get_discount_to_display( $tax_display = '' ) { $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); - return apply_filters( 'woocommerce_order_discount_to_display', wc_price( $this->get_total_discount( 'excl' === $tax_display && 'excl' === get_option( 'woocommerce_tax_display_cart' ) ), array( 'currency' => $this->get_currency() ) ), $this ); + + /** + * Filter the discount amount to display. + * + * @since 2.7.0. + */ + return apply_filters( 'woocommerce_order_discount_to_display', wc_price( $this->get_total_discount( 'excl' === $tax_display ), array( 'currency' => $this->get_currency() ) ), $this ); } /** diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php index 02755717b0b..1407dac5a2e 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php @@ -56,12 +56,22 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : wp_style_add_data( 'woocommerce_admin_privacy_styles', 'rtl', 'replace' ); if ( $screen && $screen->is_block_editor() ) { - wp_register_style( 'woocommerce-general', WC()->plugin_url() . '/assets/css/woocommerce.css', array(), $version ); - wp_style_add_data( 'woocommerce-general', 'rtl', 'replace' ); - if ( wc_current_theme_is_fse_theme() ) { - wp_register_style( 'woocommerce-blocktheme', WC()->plugin_url() . '/assets/css/woocommerce-blocktheme.css', array(), $version ); - wp_style_add_data( 'woocommerce-blocktheme', 'rtl', 'replace' ); - wp_enqueue_style( 'woocommerce-blocktheme' ); + $styles = WC_Frontend_Scripts::get_styles(); + + if ( $styles ) { + foreach ( $styles as $handle => $args ) { + wp_register_style( + $handle, + $args['src'], + $args['deps'], + $args['version'], + $args['media'] + ); + + if ( ! isset( $args['has_rtl'] ) ) { + wp_style_add_data( $handle, 'rtl', 'replace' ); + } + } } } @@ -215,7 +225,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : 'gateway_toggle' => wp_create_nonce( 'woocommerce-toggle-payment-gateway-enabled' ), ), 'urls' => array( - 'add_product' => Features::is_enabled( 'new-product-management-experience' ) || Features::is_enabled( 'block-editor-feature-enabled' ) ? esc_url_raw( admin_url( 'admin.php?page=wc-admin&path=/add-product' ) ) : null, + 'add_product' => Features::is_enabled( 'new-product-management-experience' ) || Features::is_enabled( 'product-block-editor' ) ? esc_url_raw( admin_url( 'admin.php?page=wc-admin&path=/add-product' ) ) : null, 'import_products' => current_user_can( 'import' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_importer' ) ) : null, 'export_products' => current_user_can( 'export' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_exporter' ) ) : null, ), @@ -277,7 +287,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : 'save_variations_nonce' => wp_create_nonce( 'save-variations' ), 'bulk_edit_variations_nonce' => wp_create_nonce( 'bulk-edit-variations' ), /* translators: %d: Number of variations */ - 'i18n_link_all_variations' => esc_js( sprintf( __( 'Are you sure you want to link all variations? This will create a new variation for each and every possible combination of variation attributes (max %d per run).', 'woocommerce' ), Constants::is_defined( 'WC_MAX_LINKED_VARIATIONS' ) ? Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ) : 50 ) ), + 'i18n_link_all_variations' => esc_js( sprintf( __( 'Do you want to generate all variations? This will create a new variation for each and every possible combination of variation attributes (max %d per run).', 'woocommerce' ), Constants::is_defined( 'WC_MAX_LINKED_VARIATIONS' ) ? Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ) : 50 ) ), 'i18n_enter_a_value' => esc_js( __( 'Enter a value', 'woocommerce' ) ), 'i18n_enter_menu_order' => esc_js( __( 'Variation menu order (determines position in the list of variations)', 'woocommerce' ) ), 'i18n_enter_a_value_fixed_or_percent' => esc_js( __( 'Enter a value (fixed or %)', 'woocommerce' ) ), @@ -422,6 +432,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) : 'i18n_save_attribute_variation_tip' => __( 'Make sure you enter the name and values for each attribute.', 'woocommerce' ), /* translators: %1$s: maximum file size */ 'i18n_product_image_tip' => sprintf( __( 'For best results, upload JPEG or PNG files that are 1000 by 1000 pixels or larger. Maximum upload file size: %1$s.', 'woocommerce' ) , size_format( wp_max_upload_size() ) ), + 'i18n_remove_used_attribute_confirmation_message' => __( 'If you remove this attribute, customers will no longer be able to purchase some variations of this product.', 'woocommerce' ), ); wp_localize_script( 'wc-admin-meta-boxes', 'woocommerce_admin_meta_boxes', $params ); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php index ce36ee7a291..79d4eb44045 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php @@ -375,9 +375,12 @@ class WC_Admin_Menus { ?>
    -

    +

    - + @@ -424,7 +427,7 @@ class WC_Admin_Menus { * Maybe add new management product experience. */ public function maybe_add_new_product_management_experience() { - if ( Features::is_enabled( 'new-product-management-experience' ) || Features::is_enabled( 'block-editor-feature-enabled' ) ) { + if ( Features::is_enabled( 'new-product-management-experience' ) || Features::is_enabled( 'product-block-editor' ) ) { global $submenu; if ( isset( $submenu['edit.php?post_type=product'][10] ) ) { // Disable phpcs since we need to override submenu classes. diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-notices.php b/plugins/woocommerce/includes/admin/class-wc-admin-notices.php index 05f528fc04c..c913ba10c9f 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-notices.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-notices.php @@ -57,7 +57,6 @@ class WC_Admin_Notices { add_action( 'woocommerce_installed', array( __CLASS__, 'reset_admin_notices' ) ); add_action( 'wp_loaded', array( __CLASS__, 'add_redirect_download_method_notice' ) ); add_action( 'admin_init', array( __CLASS__, 'hide_notices' ), 20 ); - self::add_action( 'admin_init', array( __CLASS__, 'maybe_remove_php73_required_notice' ) ); // @TODO: This prevents Action Scheduler async jobs from storing empty list of notices during WC installation. // That could lead to OBW not starting and 'Run setup wizard' notice not appearing in WP admin, which we want @@ -120,53 +119,8 @@ class WC_Admin_Notices { self::add_notice( 'template_files' ); self::add_min_version_notice(); self::add_maxmind_missing_license_key_notice(); - self::maybe_add_php73_required_notice(); } - // phpcs:disable Generic.Commenting.Todo.TaskFound - - /** - * Add an admin notice about the bump of the required PHP version in WooCommerce 7.7 - * if the current PHP version is too old. - * - * TODO: Remove this method in WooCommerce 7.7. - */ - private static function maybe_add_php73_required_notice() { - if ( version_compare( phpversion(), '7.3', '>=' ) ) { - return; - } - - self::add_custom_notice( - 'php73_required_in_woo_77', - sprintf( - '%s%s', - sprintf( - '

    %s

    ', - esc_html__( 'PHP version requirements will change soon', 'woocommerce' ) - ), - sprintf( - // translators: Placeholder is a URL. - wpautop( wp_kses_data( __( 'WooCommerce 7.7, scheduled for May 2023, will require PHP 7.3 or newer to work. Your server is currently running an older version of PHP, so this change will impact your store. Upgrading to at least PHP 8.0 is recommended. Learn more about this change.', 'woocommerce' ) ) ), - 'https://developer.woocommerce.com/2023/01/10/new-requirement-for-woocommerce-7-7-php-7-3/' - ) - ) - ); - } - - /** - * Remove the admin notice about the bump of the required PHP version in WooCommerce 7.7 - * if the current PHP version is good. - * - * TODO: Remove this method in WooCommerce 7.7. - */ - private static function maybe_remove_php73_required_notice() { - if ( version_compare( phpversion(), '7.3', '>=' ) && self::has_notice( 'php73_required_in_woo_77' ) ) { - self::remove_notice( 'php73_required_in_woo_77' ); - } - } - - // phpcs:enable Generic.Commenting.Todo.TaskFound - /** * Show a notice. * diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-pointers.php b/plugins/woocommerce/includes/admin/class-wc-admin-pointers.php index d719d1f83d7..6721135b17a 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-pointers.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-pointers.php @@ -38,6 +38,7 @@ class WC_Admin_Pointers { switch ( $screen->id ) { case 'product': $this->create_product_tutorial(); + $this->create_variable_product_tutorial(); break; case 'woocommerce_page_wc-addons': $this->create_wc_addons_tutorial(); @@ -64,6 +65,17 @@ class WC_Admin_Pointers { WCAdminAssets::register_script( 'wp-admin-scripts', 'product-tour', true ); } + /** + * Pointers for creating a variable product. + */ + public function create_variable_product_tutorial() { + if ( ! current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return; + } + + WCAdminAssets::register_script( 'wp-admin-scripts', 'variable-product-tour', true ); + } + /** * Pointers for accessing In-App Marketplace. */ diff --git a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php index 560468487b4..4a776a60a20 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php @@ -169,6 +169,16 @@ class WC_Meta_Box_Product_Data { return true === $attribute->get_variation(); } + /** + * Filter callback for finding non-variation attributes. + * + * @param WC_Product_Attribute $attribute Product attribute. + * @return bool + */ + private static function filter_non_variation_attributes( $attribute ) { + return false === $attribute->get_variation(); + } + /** * Show options for the variable product type. */ @@ -176,12 +186,14 @@ class WC_Meta_Box_Product_Data { global $post, $wpdb, $product_object; /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ - $variation_attributes = array_filter( $product_object->get_attributes(), array( __CLASS__, 'filter_variation_attributes' ) ); - $default_attributes = $product_object->get_default_attributes(); - $variations_count = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_count', $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_parent = %d AND post_type = 'product_variation' AND post_status IN ('publish', 'private')", $post->ID ) ), $post->ID ) ); - $variations_per_page = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_per_page', 15 ) ); - $variations_total_pages = ceil( $variations_count / $variations_per_page ); - $modal_title = get_bloginfo( 'name' ) . __( ' says', 'woocommerce' ); + $global_attributes_count = count( wc_get_attribute_taxonomies() ); + $variation_attributes = array_filter( $product_object->get_attributes(), array( __CLASS__, 'filter_variation_attributes' ) ); + $non_variation_attributes_count = count( array_filter( $product_object->get_attributes(), array( __CLASS__, 'filter_non_variation_attributes' ) ) ); + $default_attributes = $product_object->get_default_attributes(); + $variations_count = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_count', $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_parent = %d AND post_type = 'product_variation' AND post_status IN ('publish', 'private')", $post->ID ) ), $post->ID ) ); + $variations_per_page = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_per_page', 15 ) ); + $variations_total_pages = ceil( $variations_count / $variations_per_page ); + $modal_title = get_bloginfo( 'name' ) . __( ' says', 'woocommerce' ); /* phpcs: enable */ include __DIR__ . '/views/html-product-data-variations.php'; diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-item.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-item.php index ea66d76964a..13c301748e7 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-item.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-item.php @@ -3,7 +3,7 @@ * Shows an order item * * @package WooCommerce\Admin - * @var object $item The item being displayed + * @var WC_Order_Item $item The item being displayed * @var int $item_id The id of the item being displayed */ diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php index 205148941dd..f89ed0dfff5 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php @@ -64,7 +64,7 @@ if ( ! defined( 'ABSPATH' ) ) { - +
    - -

    - -

    -
    - -
    +
    + +

    + +

    +
    / - +
    diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-inventory.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-inventory.php index 7dc64ad29f1..7b4c9674778 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-inventory.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-inventory.php @@ -10,7 +10,11 @@ if ( ! defined( 'ABSPATH' ) ) { } ?>