diff --git a/.github/actions/report-flaky-tests/UPDATE.md b/.github/actions/report-flaky-tests/UPDATE.md new file mode 100644 index 00000000000..49aeee9fb55 --- /dev/null +++ b/.github/actions/report-flaky-tests/UPDATE.md @@ -0,0 +1,78 @@ +# How to update + +## Source code + +This action is extracted and bundled version of the following: + +Repository: https://github.com/WordPress/gutenberg/tree/trunk/packages/report-flaky-tests +Commit ID: ce803384250671d01fde6c7d6d2aa83075fcc726 + +## How to build + +After checking out the repository, navigate to packages/report-flaky-tests and do some modifications: + +### package.json file + +Add the following dependency: `"ts-loader": "^9.5.1",`. + +### tsconfig.json file + +The file context should be updated to following state: + +``` +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "target": "es6", + "module": "commonjs", + "esModuleInterop": true, + "moduleResolution": "node", + "declarationDir": "build-types", + "rootDir": "src", + "emitDeclarationOnly": false, + }, + "include": [ "src/**/*" ], + "exclude": [ "src/__tests__/**/*", "src/__fixtures__/**/*" ] +} +``` + +### webpack.config.js file + +The file should be added with the following content: + +``` +const path = require( 'path' ); +const buildMode = process.env.NODE_ENV || 'production'; + +module.exports = { + entry: './src/index.ts', + target: 'node', + mode: buildMode, + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: [ '.tsx', '.ts', '.js' ], + }, + plugins: [], + output: { + filename: 'index.js', + path: path.resolve( __dirname, 'dist' ), + clean: true, + }, +}; +``` + +### Build + +Run `webpack --config webpack.config.js` (don't forget about `npm install` before that). + +Use the generated files under `packages/report-flaky-tests/dist` to update the bundled distribution in this repository. diff --git a/.github/actions/setup-woocommerce-monorepo/action.yml b/.github/actions/setup-woocommerce-monorepo/action.yml index 165b6930322..c6a7031c7af 100644 --- a/.github/actions/setup-woocommerce-monorepo/action.yml +++ b/.github/actions/setup-woocommerce-monorepo/action.yml @@ -22,14 +22,14 @@ inputs: runs: using: 'composite' steps: - - name: 'Read PNPM Version' - id: 'read-pnpm-version' - shell: 'bash' - run: 'echo "version=$(./.github/actions/setup-woocommerce-monorepo/scripts/read-pnpm-version.sh package.json)" >> $GITHUB_OUTPUT' - name: 'Setup PNPM' uses: 'pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d' - with: - version: ${{ steps.read-pnpm-version.outputs.version }} + # Next step is rudimentary - fixes a know composite action bug during post-actions: + # Error: Index was out of range. Must be non-negative and less than the size of the collection. + - name: 'Read PNPM version' + id: 'read-pnpm-version' + shell: 'bash' + run: 'echo "version=$(pnpm --version)" >> $GITHUB_OUTPUT' - name: 'Setup Node' uses: 'actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65' with: diff --git a/.github/actions/setup-woocommerce-monorepo/scripts/read-pnpm-version.sh b/.github/actions/setup-woocommerce-monorepo/scripts/read-pnpm-version.sh deleted file mode 100755 index 2acc7598b81..00000000000 --- a/.github/actions/setup-woocommerce-monorepo/scripts/read-pnpm-version.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -PACKAGE_FILE=$1 -if [[ -z "$PACKAGE_FILE" ]]; then - echo "Usage: $0 " - exit 1 -fi - -awk -F'"' '/"pnpm": ".+"/{ print $4; exit; }' $PACKAGE_FILE diff --git a/.github/project-community-pr-assigner.yml b/.github/project-community-pr-assigner.yml index 2474d002fa1..a2e409ca4cf 100644 --- a/.github/project-community-pr-assigner.yml +++ b/.github/project-community-pr-assigner.yml @@ -31,24 +31,25 @@ - team: vortex "packages/js/components/**/*": - - team: mothra + - team: woo-fse - team: ghidorah "packages/js/csv-export/**/*": - - team: mothra + - team: woo-fse "packages/js/currency/**/*": - - team: mothra + - team: woo-fse "packages/js/customer-effort-score/**/*": - - team: mothra + - team: woo-fse + - team: ghidorah "packages/js/data/**/*": - - team: mothra + - team: woo-fse - team: ghidorah "packages/js/date/**/*": - - team: mothra + - team: woo-fse "packages/js/dependency-extraction-webpack-plugin/**/*": - team: vortex @@ -57,26 +58,27 @@ - team: vortex "packages/js/experimental/**/*": - - team: mothra + - team: woo-fse "packages/js/explat/**/*": - - team: mothra + - team: woo-fse - team: ghidorah "packages/js/navigation/**/*": - - team: mothra + - team: woo-fse "packages/js/number/**/*": - - team: mothra + - team: woo-fse "packages/js/onboarding/**/*": - team: ghidorah "packages/js/product-editor/**/*": - - team: mothra + - team: woo-fse "packages/js/tracks/**/*": - - team: mothra + - team: woo-fse + - team: ghidorah "plugins/woocommerce/**/*": - team: proton @@ -89,7 +91,7 @@ - team: proton "plugins/woocommerce/src/Admin/**/*": - - team: mothra + - team: woo-fse - team: ghidorah "plugins/woocommerce/src/Blocks/**/*": @@ -97,7 +99,7 @@ - team: woo-fse "plugins/woocommerce/src/Internal/Admin/**/*": - - team: mothra + - team: woo-fse - team: ghidorah "plugins/woocommerce/src/StoreApi/**/*": @@ -105,7 +107,7 @@ - team: woo-fse "plugins/woocommerce-admin/**/*": - - team: mothra + - team: woo-fse - team: ghidorah "plugins/woocommerce-blocks/**/*": diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9d43842306..4262ae81b1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,8 @@ on: type: string concurrency: - group: '${{ github.workflow }}-${{ github.ref }}-${{ inputs.trigger }}' + # Cancel concurrent jobs but not for push event. For push use the run_id to have a unique group. + group: ci-${{ github.event_name == 'push' && github.run_id || github.event_name }}-${{ github.ref }}-${{ inputs.trigger }} cancel-in-progress: true env: @@ -149,37 +150,92 @@ jobs: env: ${{ matrix.testEnv.envVars }} run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.testEnv.start }}' - - name: 'Get commit message' - id: 'get_commit_message' + - name: 'Determine BuildKite Analytics Message' env: HEAD_COMMIT_MESSAGE: ${{ github.event.head_commit.message }} PR_TITLE: ${{ github.event.pull_request.title }} run: | if [[ "${{ github.event_name }}" == "push" ]]; then - COMMIT_MESSAGE=`echo "$HEAD_COMMIT_MESSAGE" | head -1` + MESSAGE=`echo "$HEAD_COMMIT_MESSAGE" | head -1` elif [[ "${{ github.event_name }}" == "pull_request" ]]; then - COMMIT_MESSAGE="$PR_TITLE" + MESSAGE="$PR_TITLE" else - COMMIT_MESSAGE="${{ github.event_name }}" + MESSAGE="${{ github.event_name }}" fi - echo "COMMIT_MESSAGE=$COMMIT_MESSAGE" >> "$GITHUB_OUTPUT" + echo "BUILDKITE_ANALYTICS_MESSAGE=$MESSAGE" >> "$GITHUB_ENV" shell: bash - - name: 'Run tests (${{ matrix.testType }})' - env: - E2E_ENV_KEY: ${{ secrets.E2E_ENV_KEY }} - BUILDKITE_ANALYTICS_TOKEN: ${{ secrets.BUILDKITE_CORE_E2E_TOKEN }} - BUILDKITE_ANALYTICS_MESSAGE: ${{ steps.get_commit_message.outputs.COMMIT_MESSAGE }} - CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} # required by Metrics tests - run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }}' - - name: 'Resolve artifacts path' if: ${{ always() && matrix.report.resultsPath != '' }} # Blocks e2e use a relative path which is not supported by actions/upload-artifact@v4 # https://github.com/actions/upload-artifact/issues/176 env: ARTIFACTS_PATH: '${{ matrix.projectPath }}/${{ matrix.report.resultsPath }}' - run: echo "ARTIFACTS_PATH=$(realpath $ARTIFACTS_PATH)" >> $GITHUB_ENV + run: | + # first runs will probably not have the directory, so we need to create it so that realpath doesn't fail + mkdir -p $ARTIFACTS_PATH + echo "ARTIFACTS_PATH=$(realpath $ARTIFACTS_PATH)" >> $GITHUB_ENV + + - name: 'Download Playwright last run info' + id: 'download-last-run-info' + if: ${{ always() && matrix.report.resultsPath != '' && matrix.testType == 'e2e' }} + uses: actions/download-artifact@v4 + with: + pattern: 'last-run__${{ strategy.job-index }}' + + - name: 'Run tests (${{ matrix.testType }})' + env: + E2E_ENV_KEY: ${{ secrets.E2E_ENV_KEY }} + BUILDKITE_ANALYTICS_TOKEN: ${{ secrets.BUILDKITE_CORE_E2E_TOKEN }} + CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} # required by Metrics tests + LAST_FAILED_RUN: ${{ vars.LAST_FAILED_RUN }} + run: | + lastRunFile="${{ steps.download-last-run-info.outputs.download-path }}/last-run__${{ strategy.job-index }}/.last-run.json" + lastRunFileDest="$ARTIFACTS_PATH/.last-run.json" + + if [ -f "$lastRunFile" ]; then + echo "Found last run info file: \"$lastRunFile\"" + echo "Moving to destination: \"$lastRunFileDest\"" + mkdir -p "$ARTIFACTS_PATH" + mv "$lastRunFile" "$lastRunFileDest" + else + echo "No last run info file found. Searched for: \"$lastRunFile\"" + fi + + lastRunFlag="" + if [ -f "$lastRunFileDest" ]; then + # Playwright last run info is available, parse the file and check if there are failed tests + cat "$lastRunFileDest" + failedTests=$(jq '.failedTests | length' "$lastRunFileDest") + + # Only if there are failed tests, we want to use the --last-failed flag. + # The run will fail if we're using the flag and there are no failed tests. + if [ "$failedTests" -gt 0 ]; then + if [ "$LAST_FAILED_RUN" == "1" ]; then + echo "Found failed tests, running only failed tests" + # Add shard 1/1 to override the default shard value. No tests will run for shards > 1. + # The clean way would be to replace the shard flag from the command, but this also works. + lastRunFlag="--last-failed --shard=1/1" + else + echo "Found failed tests, but LAST_FAILED_RUN is switched off. Running all tests." + fi + else + echo "No failed tests found, running all tests" + fi + fi + + # Finally, run the tests + pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }} $lastRunFlag + + - name: 'Upload Playwright last run info' + # always upload the last run info, even if the test run passed + if: ${{ always() && matrix.report.resultsPath != '' }} + uses: actions/upload-artifact@v4 + with: + name: 'last-run__${{ strategy.job-index }}' + path: '${{ env.ARTIFACTS_PATH }}/.last-run.json' + if-no-files-found: ignore + overwrite: true - name: 'Upload artifacts' if: ${{ always() && matrix.report.resultsPath != '' }} @@ -194,15 +250,6 @@ jobs: name: flaky-tests-${{ strategy.job-index }} path: ${{ env.ARTIFACTS_PATH }}/flaky-tests if-no-files-found: ignore - - - name: 'Archive metrics results' - if: ${{ success() && startsWith(matrix.name, 'Metrics') }} # this seems too fragile, we should update the reporting path and use the generic upload step above - uses: actions/upload-artifact@v4 - env: - WP_ARTIFACTS_PATH: ${{ github.workspace }}/artifacts - with: - name: metrics-results - path: ${{ env.WP_ARTIFACTS_PATH }}/*.performance-results*.json evaluate-project-jobs: # In order to add a required status check we need a consistent job that we can grab onto. diff --git a/.github/workflows/pr-highlight-changes.yml b/.github/workflows/pr-highlight-changes.yml index 3fd276e222a..aaae308e045 100644 --- a/.github/workflows/pr-highlight-changes.yml +++ b/.github/workflows/pr-highlight-changes.yml @@ -3,6 +3,7 @@ on: pull_request: paths: - 'plugins/woocommerce/**' + - '!plugins/woocommerce/templates/templates/**' jobs: analyze: name: 'Analyze Branch Changes' @@ -26,7 +27,7 @@ jobs: GIT_CLONE_PROTECTION_ACTIVE: false run: | HEAD_REF=$(git rev-parse HEAD) - exclude="plugins/woocommerce/tests plugins/woocommerce-admin/tests plugins/woocommerce-blocks/tests" + exclude="plugins/woocommerce/tests plugins/woocommerce/templates/templates plugins/woocommerce-admin/tests plugins/woocommerce-blocks/tests" version=$(pnpm analyzer major-minor "$HEAD_REF" "plugins/woocommerce/woocommerce.php" | tail -n 1) pnpm analyzer "$HEAD_REF" $version -o "github" -e $exclude - uses: 'actions/github-script@v6' diff --git a/.github/workflows/pr-lint-markdown.yml b/.github/workflows/pr-lint-markdown.yml index 4a600aefd93..e996a23ee4e 100644 --- a/.github/workflows/pr-lint-markdown.yml +++ b/.github/workflows/pr-lint-markdown.yml @@ -38,7 +38,7 @@ jobs: docs/docs-manifest.json - name: Setup PNPM - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 + uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d - name: Setup Node uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c diff --git a/.github/workflows/release-code-freeze.yml b/.github/workflows/release-code-freeze.yml index 2605eb5cce4..633000a1801 100644 --- a/.github/workflows/release-code-freeze.yml +++ b/.github/workflows/release-code-freeze.yml @@ -31,7 +31,6 @@ jobs: issues: write pull-requests: write outputs: - pnpmVersion: ${{ steps.read-pnpm-version.outputs.version }} isTodayAcceleratedFreeze: ${{ steps.get-versions.outputs.isTodayAcceleratedFreeze }} isTodayMonthlyFreeze: ${{ steps.get-versions.outputs.isTodayMonthlyFreeze }} acceleratedVersion: ${{ steps.get-versions.outputs.acceleratedVersion }} @@ -47,18 +46,8 @@ jobs: with: fetch-depth: 0 - - name: Read PNPM Version - id: read-pnpm-version - shell: bash - run: | - version=$(./.github/actions/setup-woocommerce-monorepo/scripts/read-pnpm-version.sh package.json) - echo "version=$version" >> $GITHUB_OUTPUT - echo "PNPM Version: $version" - - name: Setup PNPM - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 - with: - version: ${{ steps.read-pnpm-version.outputs.version }} + uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d - name: Setup Node uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c @@ -149,9 +138,7 @@ jobs: fetch-depth: 0 - name: Setup PNPM - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 - with: - version: ${{ needs.code-freeze-prep.outputs.pnpmVersion }} + uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d - name: Setup Node uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c @@ -275,9 +262,7 @@ jobs: fetch-depth: 0 - name: Setup PNPM - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 - with: - version: ${{ needs.code-freeze-prep.outputs.pnpmVersion }} + uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d - name: Setup Node uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c @@ -346,9 +331,7 @@ jobs: fetch-depth: 0 - name: Setup PNPM - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 - with: - version: ${{ needs.code-freeze-prep.outputs.pnpmVersion }} + uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d - name: Setup Node uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c diff --git a/.github/workflows/release-wc-beta-tester.yml b/.github/workflows/release-wc-beta-tester.yml index f11c1c9d27b..5858f759338 100644 --- a/.github/workflows/release-wc-beta-tester.yml +++ b/.github/workflows/release-wc-beta-tester.yml @@ -20,14 +20,10 @@ jobs: - name: Setup WooCommerce Monorepo uses: ./.github/actions/setup-woocommerce-monorepo with: - install: '@woocommerce/plugin-woocommerce' - build: '@woocommerce/plugin-woocommerce' + install: '@woocommerce/plugin-woocommerce-beta-tester...' + build: '@woocommerce/plugin-woocommerce-beta-tester...' pull-package-deps: '@woocommerce/plugin-woocommerce-beta-tester' - - name: Lint - working-directory: plugins/woocommerce-beta-tester - run: composer run phpcs - - name: Build WooCommerce Beta Tester Zip working-directory: plugins/woocommerce-beta-tester run: pnpm build:zip diff --git a/.github/workflows/scripts/run-metrics.sh b/.github/workflows/scripts/run-metrics.sh index 4bbe85ee470..dfb6565b3e8 100755 --- a/.github/workflows/scripts/run-metrics.sh +++ b/.github/workflows/scripts/run-metrics.sh @@ -2,30 +2,49 @@ set -eo pipefail +function title() { + echo -e "\n\033[1m$1\033[0m" +} + if [[ -z "$GITHUB_EVENT_NAME" ]]; then echo "::error::GITHUB_EVENT_NAME must be set" exit 1 fi +title "Installing NVM" +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash > /dev/null +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" +echo "Installed version: $(nvm -v)" + +title "Installing dependencies" +pnpm install --frozen-lockfile --filter="compare-perf" > /dev/null + if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then - echo "Comparing performance with trunk" + title "Comparing performance with trunk" pnpm --filter="compare-perf" run compare perf $GITHUB_SHA trunk --tests-branch $GITHUB_SHA elif [[ "$GITHUB_EVENT_NAME" == "push" ]]; then - echo "Comparing performance with base branch" - # The base hash used here need to be a commit that is compatible with the current WP version - # The current one is 19f3d0884617d7ecdcf37664f648a51e2987cada - # it needs to be updated every time it becomes unsupported by the current wp-env (WP version). - # It is used as a base comparison point to avoid fluctuation in the performance metrics. + title "Comparing performance with base branch" WP_VERSION=$(awk -F ': ' '/^Tested up to/{print $2}' readme.txt) + # Updating the WP version used for performance jobs means there’s a high + # chance that the reference commit used for performance test stability + # becomes incompatible with the WP version. So, every time the "Tested up + # to" flag is updated in the readme.txt, we also have to update the + # reference commit below (BASE_SHA). The new reference needs to meet the + # following requirements: + # - Be compatible with the new WP version used in the “Tested up to” flag. + # - Be tracked on https://www.codevitals.run/project/woo for all existing + # metrics. + BASE_SHA=3d7d7f02017383937f1a4158d433d0e5d44b3dc9 echo "WP_VERSION: $WP_VERSION" IFS=. read -ra WP_VERSION_ARRAY <<< "$WP_VERSION" WP_MAJOR="${WP_VERSION_ARRAY[0]}.${WP_VERSION_ARRAY[1]}" - pnpm --filter="compare-perf" run compare perf $GITHUB_SHA 19f3d0884617d7ecdcf37664f648a51e2987cada --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" + pnpm --filter="compare-perf" run compare perf $GITHUB_SHA $BASE_SHA --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" - echo "Publish results to CodeVitals" + title "Publish results to CodeVitals" COMMITTED_AT=$(git show -s $GITHUB_SHA --format="%cI") - pnpm --filter="compare-perf" run log $CODEVITALS_PROJECT_TOKEN trunk $GITHUB_SHA 19f3d0884617d7ecdcf37664f648a51e2987cada $COMMITTED_AT + pnpm --filter="compare-perf" run log $CODEVITALS_PROJECT_TOKEN trunk $GITHUB_SHA $BASE_SHA $COMMITTED_AT else echo "Unsupported event: $GITHUB_EVENT_NAME" fi diff --git a/.github/workflows/stalebot.yml b/.github/workflows/stalebot.yml index 944392074ba..669c3f09dfc 100644 --- a/.github/workflows/stalebot.yml +++ b/.github/workflows/stalebot.yml @@ -1,45 +1,51 @@ name: 'Process stale needs-feedback issues' on: - schedule: - - cron: '21 0 * * *' + schedule: + - cron: '21 0 * * *' + workflow_dispatch: -permissions: {} +permissions: { } jobs: - stale: - runs-on: ubuntu-20.04 - permissions: - contents: read - issues: write - pull-requests: write - steps: - - name: Scan issues - uses: actions/stale@v9.0.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: "As a part of this repository's maintenance, this issue is being marked as stale due to inactivity. Please feel free to comment on it in case we missed something.\n\n###### After 7 days with no activity this issue will be automatically be closed." - close-issue-message: 'This issue was closed because it has been 14 days with no activity.' - operations-per-run: 140 - days-before-stale: -1 - days-before-close: -1 - days-before-issue-stale: 7 - days-before-issue-close: 7 - stale-issue-label: 'status: stale' - stale-pr-label: 'status: stale' - exempt-issue-labels: 'type: enhancement' - only-issue-labels: 'needs: author feedback' - close-issue-label: "status: can't reproduce" - ascending: true - - name: Close Stale Flaky Test Issues - uses: actions/stale@v9.0.0 - with: - only-labels: 'metric: flaky e2e test, team: Vortex' - days-before-stale: 5 - days-before-close: 2 - stale-issue-label: 'metric: stale flaky e2e test report' - stale-issue-message: 'This test may have been a one-time failure. It will be auto-closed if no further activity occurs within the next 2 days.' - close-issue-message: 'Auto-closed due to inactivity. Please re-open if you believe this issue is still valid.' - close-issue-reason: 'not_planned' - remove-stale-when-updated: true - exempt-all-assignees: true - enable-statistics: true + stale: + runs-on: ubuntu-20.04 + permissions: + contents: read + issues: write + pull-requests: write + steps: + - name: Scan issues + uses: actions/stale@v9.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: "As a part of this repository's maintenance, this issue is being marked as stale due to inactivity. Please feel free to comment on it in case we missed something.\n\n###### After 7 days with no activity this issue will be automatically be closed." + close-issue-message: 'This issue was closed because it has been 14 days with no activity.' + operations-per-run: 140 + days-before-stale: -1 + days-before-close: -1 + days-before-issue-stale: 7 + days-before-issue-close: 7 + stale-issue-label: 'status: stale' + stale-pr-label: 'status: stale' + exempt-issue-labels: 'type: enhancement' + only-issue-labels: 'needs: author feedback' + close-issue-label: "status: can't reproduce" + ascending: true + - name: Process Stale Flaky Test Issues + uses: actions/stale@v9.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + only-issue-labels: 'metric: flaky e2e test' + days-before-stale: -1 + days-before-close: -1 + days-before-issue-stale: 5 + days-before-issue-close: 2 + stale-issue-label: 'status: stale' + stale-issue-message: 'This issue is being marked as stale due to inactivity. It will be auto-closed if no further activity occurs within the next 2 days.' + close-issue-message: 'Auto-closed due to inactivity. Please re-open if you believe this issue is still valid.' + close-issue-reason: 'not_planned' + remove-stale-when-updated: true + exempt-all-assignees: false + enable-statistics: true + ascending: true + operations-per-run: 120 diff --git a/.syncpackrc b/.syncpackrc index d82f3b8375e..14b98ade137 100644 --- a/.syncpackrc +++ b/.syncpackrc @@ -12,7 +12,7 @@ "dependencies": [ "pnpm" ], - "pinVersion": "^9.1.0", + "pinVersion": "9.1.3", "packages": [ "**" ] @@ -172,7 +172,7 @@ "packages": [ "**" ], - "pinVersion": "^1.45.1" + "pinVersion": "^1.46.1" }, { "dependencies": [ @@ -244,7 +244,7 @@ "@woocommerce/block-library", "**" ], - "pinVersion": "^9.7.0" + "pinVersion": "^10.1.0" }, { "dependencies": [ @@ -253,7 +253,7 @@ "packages": [ "**" ], - "pinVersion": "wp-6.4" + "pinVersion": "wp-6.6" }, { "dependencies": [ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f6b77a94530..06f3593c515 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -4,7 +4,7 @@ This document aims to provide as much context as possible to aid in the developm ## Getting Started -Please refer to [the Getting Started section of the `README.md`](README.md#getting-started) for a general-purpose guide on getting started. The rest of this document will assume that you've installed all of the prequisites and setup described there. +Please refer to [the Getting Started section of the `README.md`](README.md#getting-started) for a general-purpose guide on getting started. The rest of this document will assume that you've installed all of the prerequisites and setup described there. ### Plugin, Package, and Tool Filtering diff --git a/changelog.txt b/changelog.txt index 01b8c0ed896..ed1be0eb305 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,344 @@ == Changelog == += 9.2.2 2024-08-22 = + +**WooCommerce** + +* Fix - Revert PR#48731 to address possible issues with plugins using WC's bundled select2 package. [#50854](https://github.com/woocommerce/woocommerce/pull/50854) +* Fix - Partially revert PR#48709 as it could cause issues for some users of the REST API system_status endpoint. [#50881](https://github.com/woocommerce/woocommerce/pull/50881) + + += 9.2.1 2024-08-21 = + +**WooCommerce** + +* Fix - Revert turning of AccessiblePrivateMethods::_accessible_private_methods into a static property because it caused fatal errors in some edge cases. [#50809](https://github.com/woocommerce/woocommerce/pull/50809) + += 9.2.0 2024-08-20 = + +**WooCommerce** + +* Update - Enhanced sanitization in `@woocommerce/currency`. [#50802](https://github.com/woocommerce/woocommerce/pull/50802) +* Fix - Hardening against XSS by leveraging HTML API for adding block attribute data. [#50801](https://github.com/woocommerce/woocommerce/pull/50801) +* Fix - Prevent downloads of digitial products after partial refund. [#50804](https://github.com/woocommerce/woocommerce/pull/50804) +* Add - Adds support for tracking downloads when a partial (ranged) request is made. [#50805](https://github.com/woocommerce/woocommerce/pull/50805) +* Update - Turn AccessiblePrivateMethods::_accessible_private_methods into a static property. [#50806](https://github.com/woocommerce/woocommerce/pull/50806) +* Fix - Correct label of shipping dimensions length field. [#50180](https://github.com/woocommerce/woocommerce/pull/50180) +* Fix - Allow new accounts to set new password even if logged in. [#50700](https://github.com/woocommerce/woocommerce/pull/50700) +* Fix - Remove global_unique_id from interface and add warning in case it is not implemented [#50685](https://github.com/woocommerce/woocommerce/pull/50685) +* Fix - CYS: avoid to enqueue Jetpack scripts [#50679](https://github.com/woocommerce/woocommerce/pull/50679) +* Fix - Clear product unique ID (`global_unique_id`) when duplicating products. [#50629](https://github.com/woocommerce/woocommerce/pull/50629) +* Fix - Resolved an issue that caused the page title and content text to display in the incorrect order on the Order Confirmation page. [#50592](https://github.com/woocommerce/woocommerce/pull/50592) +* Fix - Customer Account - Maintain the size of the icon in smaller screens. [#50410](https://github.com/woocommerce/woocommerce/pull/50410) +* Fix - Accessibility: Prevent shipping losing focus when making selections during checkout. [#48370](https://github.com/woocommerce/woocommerce/pull/48370) +* Fix - Add aria-label to mini-cart button on first render to improve accessibility [#48329](https://github.com/woocommerce/woocommerce/pull/48329) +* Fix - Add missing script dependencies to product variation generation script. [#49595](https://github.com/woocommerce/woocommerce/pull/49595) +* Fix - Address timing issue with React 18 when unregistering blocks and add missing dependency to editor bootstrapping code. [#49642](https://github.com/woocommerce/woocommerce/pull/49642) +* Fix - Add the "Customer account" block into the "Minimal Header" to fix the spacing. [#49893](https://github.com/woocommerce/woocommerce/pull/49893) +* Fix - Avoid Product Search Results block template to fall back to the Product Catalog template from the theme [#48887](https://github.com/woocommerce/woocommerce/pull/48887) +* Fix - Changed from using React.render to React.createRoot for product editor areas as it has been deprecated since React 18 [#48834](https://github.com/woocommerce/woocommerce/pull/48834) +* Fix - Check if parent product exists when creating attribute lookup data for variations [#49474](https://github.com/woocommerce/woocommerce/pull/49474) +* Fix - Check if there's an actual session available inside wc_clear_cart_after_payment [#45821](https://github.com/woocommerce/woocommerce/pull/45821) +* Fix - Comment: Update db_version variable and use it to prevent adding global_unique_id when the lookup table was not yet updated [#49472](https://github.com/woocommerce/woocommerce/pull/49472) +* Fix - Correctly escape the HTML when linking customer orders. [#49195](https://github.com/woocommerce/woocommerce/pull/49195) +* Fix - CSY - Fix intro banner copy for existing themes [#49787](https://github.com/woocommerce/woocommerce/pull/49787) +* Fix - CYS - Fix marking the CYS task as completed only by accessing the Intro page. [#49374](https://github.com/woocommerce/woocommerce/pull/49374) +* Fix - CYS - Remove the site title block length from the "Large Header" and the "Centered Menu Header". [#48671](https://github.com/woocommerce/woocommerce/pull/48671) +* Fix - CYS: Disable readonly mode only when full composability feature flag is enabled. [#48752](https://github.com/woocommerce/woocommerce/pull/48752) +* Fix - CYS: Ensure that the button in the Intro page redirects to the Customizer when a classic theme is enabled. [#49804](https://github.com/woocommerce/woocommerce/pull/49804) +* Fix - CYS: fix: Assembler follows admin color schema. [#49159](https://github.com/woocommerce/woocommerce/pull/49159) +* Fix - CYS: fix arrow welcome tour [#49607](https://github.com/woocommerce/woocommerce/pull/49607) +* Fix - CYS: Fix border width pattern preview. [#49753](https://github.com/woocommerce/woocommerce/pull/49753) +* Fix - CYS: Fix button background on Featured Category Cover image [#49659](https://github.com/woocommerce/woocommerce/pull/49659) +* Fix - CYS: fix drag to resize bar [#49657](https://github.com/woocommerce/woocommerce/pull/49657) +* Fix - CYS: fix flickering effect. [#48767](https://github.com/woocommerce/woocommerce/pull/48767) +* Fix - CYS: fix logic to disable click on the no block placeholder [#48722](https://github.com/woocommerce/woocommerce/pull/48722) +* Fix - CYS: Fix no block placeholder style. [#49673](https://github.com/woocommerce/woocommerce/pull/49673) +* Fix - CYS: Fix pattern rendering issues [#49041](https://github.com/woocommerce/woocommerce/pull/49041) +* Fix - CYS: fix pattern wrapped twice by group blocks [#48712](https://github.com/woocommerce/woocommerce/pull/48712) +* Fix - CYS: fix selected header/footer pattern [#49788](https://github.com/woocommerce/woocommerce/pull/49788) +* Fix - CYS: fix shuffle feature logic. [#49153](https://github.com/woocommerce/woocommerce/pull/49153) +* Fix - CYS: fix the default intro pattern. [#49082](https://github.com/woocommerce/woocommerce/pull/49082) +* Fix - CYS: Fix the tooltip overlap with the pattern. [#49700](https://github.com/woocommerce/woocommerce/pull/49700) +* Fix - CYS: fix toolbar position after the site preview resizes [#49028](https://github.com/woocommerce/woocommerce/pull/49028) +* Fix - CYS: hide button to resize the image [#48714](https://github.com/woocommerce/woocommerce/pull/48714) +* Fix - CYS: Hide shuffle button when only one pattern is available [#49790](https://github.com/woocommerce/woocommerce/pull/49790) +* Fix - CYS: not enable PTK features on WordPress 6.5. [#49591](https://github.com/woocommerce/woocommerce/pull/49591) +* Fix - CYS: open Intro panel when user clicks on Design your homepage [#49005](https://github.com/woocommerce/woocommerce/pull/49005) +* Fix - CYS: Remove not necessary placeholder on homepage sidebar. [#49705](https://github.com/woocommerce/woocommerce/pull/49705) +* Fix - CYS: show default TT4 fonts pair. [#49675](https://github.com/woocommerce/woocommerce/pull/49675) +* Fix - CYS: show logo when the header is updated. [#49681](https://github.com/woocommerce/woocommerce/pull/49681) +* Fix - Display "View store" button text by default in the toolbar. [#48690](https://github.com/woocommerce/woocommerce/pull/48690) +* Fix - Display admin notice of expired and expiring Woo subscription when the product + is connected / activated on the site. [#49717](https://github.com/woocommerce/woocommerce/pull/49717) +* Fix - Do not set the `tk_ai` tracking cookie if tracking is disabled. [#47863](https://github.com/woocommerce/woocommerce/pull/47863) +* Fix - Ensure the second address line input appears when using autofill [#49730](https://github.com/woocommerce/woocommerce/pull/49730) +* Fix - Ensure the Store API recalculates cart totals prior to running validation hooks. [#49455](https://github.com/woocommerce/woocommerce/pull/49455) +* Fix - Exclude simple products from variations reports by default. [#49244](https://github.com/woocommerce/woocommerce/pull/49244) +* Fix - Failure to fetch the country list no longer blocks users in the profiler. [#49519](https://github.com/woocommerce/woocommerce/pull/49519) +* Fix - Fetch site cache status correctly if directly navigating to LYS Success page, and some refactoring [#48710](https://github.com/woocommerce/woocommerce/pull/48710) +* Fix - Fix: Make woocommerce/product-price available in the Single Product template [#49906](https://github.com/woocommerce/woocommerce/pull/49906) +* Fix - Fix a bug where woocommerce removes the current-menu-item class. [#45095](https://github.com/woocommerce/woocommerce/pull/45095) +* Fix - Fix add product task to create template first and simplify logic [#49631](https://github.com/woocommerce/woocommerce/pull/49631) +* Fix - Fix add zone button flinching and vertical centering [#48869](https://github.com/woocommerce/woocommerce/pull/48869) +* Fix - Fix Analytics - Tax Report Pagination [#49562](https://github.com/woocommerce/woocommerce/pull/49562) +* Fix - Fix confusing messages prompting switch to classic templates [#48717](https://github.com/woocommerce/woocommerce/pull/48717) +* Fix - Fix default shipping selection when changing between pickup and shipping on block checkout. [#49718](https://github.com/woocommerce/woocommerce/pull/49718) +* Fix - Fixed "woocommerce_new_order" triggering on checkout blocks page visit. [#47422](https://github.com/woocommerce/woocommerce/pull/47422) +* Fix - Fixed a bug causing account email not to be taken in consideration for coupon validation when a customer has a different billing email set. [#48488](https://github.com/woocommerce/woocommerce/pull/48488) +* Fix - Fixed a bug that would cause incorrect pricing at checkout for very large amounts. [#49361](https://github.com/woocommerce/woocommerce/pull/49361) +* Fix - Fixed a bug where the close button is not visible on the mini cart when viewed on a mobile device [#48769](https://github.com/woocommerce/woocommerce/pull/48769) +* Fix - Fixed horizontal and vertical layout overflows on LYS success page [#49127](https://github.com/woocommerce/woocommerce/pull/49127) +* Fix - Fix fatal error in order reports when parent order doesn't exist [#49006](https://github.com/woocommerce/woocommerce/pull/49006) +* Fix - Fix get_options deprecated notice when viewing Analytics > Orders [#49092](https://github.com/woocommerce/woocommerce/pull/49092) +* Fix - Fix Order Count inconsistency between stats and reports [#49283](https://github.com/woocommerce/woocommerce/pull/49283) +* Fix - Fix sidebar attribute control in Products by Attribute block [#49351](https://github.com/woocommerce/woocommerce/pull/49351) +* Fix - Fix site coming soon page heading color [#49129](https://github.com/woocommerce/woocommerce/pull/49129) +* Fix - Fix Task List - Reminder bar close button styling [#49532](https://github.com/woocommerce/woocommerce/pull/49532) +* Fix - Fix the mini cart items not being visible when zoomed in [#48384](https://github.com/woocommerce/woocommerce/pull/48384) +* Fix - Fix the namespace of the RestApiControllerBase class [#49333](https://github.com/woocommerce/woocommerce/pull/49333) +* Fix - Fix the undefined array key "name" warning in ComingSoonRequestHandler.php when the font name is not set [#49795](https://github.com/woocommerce/woocommerce/pull/49795) +* Fix - Fix variation name in analytics variations report [#49440](https://github.com/woocommerce/woocommerce/pull/49440) +* Fix - Issue fixed where tags are overlapping divider line in "Filter by product category". [#48756](https://github.com/woocommerce/woocommerce/pull/48756) +* Fix - Made coupon fields during block checkout stack on smaller screen sizes [#48623](https://github.com/woocommerce/woocommerce/pull/48623) +* Fix - Make sure the correct block template file is used in the Site Editor for templates with fallback [#48621](https://github.com/woocommerce/woocommerce/pull/48621) +* Fix - Make the edit/delete actions of shipping/products in manual orders accessible [#48238](https://github.com/woocommerce/woocommerce/pull/48238) +* Fix - Make the Leaderboards on the Analytics > Dashboard page use consistent currency and number formatting across the page, and perceive the currency setting comes from the relevant filter. [#49097](https://github.com/woocommerce/woocommerce/pull/49097) +* Fix - Narrowed scope of block theme notice templates so other template overrides are unaffected [#48686](https://github.com/woocommerce/woocommerce/pull/48686) +* Fix - Prevent a warning showing when using the Shipping Address Calculator in combination with the additional checkout fields API [#49685](https://github.com/woocommerce/woocommerce/pull/49685) +* Fix - Prevent download permissions metabox from being toggled when toggling individual permission details. [#49022](https://github.com/woocommerce/woocommerce/pull/49022) +* Fix - Prevent possible type error in BatchProcessingController. [#49728](https://github.com/woocommerce/woocommerce/pull/49728) +* Fix - Prevent product editor styles loading on non wc-admin pages [#49170](https://github.com/woocommerce/woocommerce/pull/49170) +* Fix - Product Collection: allow custom collections to hide all of the filter controls, not only some of them [#49713](https://github.com/woocommerce/woocommerce/pull/49713) +* Fix - Product Collection: Fix alignment of the first item in Grid layout for WP 6.6 [#49096](https://github.com/woocommerce/woocommerce/pull/49096) +* Fix - Product Collection: Fix the Preview badge's corner radius [#48856](https://github.com/woocommerce/woocommerce/pull/48856) +* Fix - Product Collection: Show "Sync with current query" option only in archive templates where it makes sense [#48917](https://github.com/woocommerce/woocommerce/pull/48917) +* Fix - Product Fitlers: Bring back pattern [#49601](https://github.com/woocommerce/woocommerce/pull/49601) +* Fix - Product rating - Inherit the color of the star when no ratings [#49678](https://github.com/woocommerce/woocommerce/pull/49678) +* Fix - Provide more informative errors if a refund cannot be requested via the REST API, due to plugin conflicts. [#47918](https://github.com/woocommerce/woocommerce/pull/47918) +* Fix - Redirect the lost password page to edit account while logged in. [#49670](https://github.com/woocommerce/woocommerce/pull/49670) +* Fix - Removes several side effects in the code bases that caused translations to be loaded too early. [#47113](https://github.com/woocommerce/woocommerce/pull/47113) +* Fix - Remove strong tags from patterns [#49901](https://github.com/woocommerce/woocommerce/pull/49901) +* Fix - Replace the red CSS color with the $red SASS variable in WooCommerce legacy elements [#48742](https://github.com/woocommerce/woocommerce/pull/48742) +* Fix - Resolved issues with new order hook triggers during transitions to and from draft statuses. [#49098](https://github.com/woocommerce/woocommerce/pull/49098) +* Fix - Set coming soon preview body aspect ratio to 1/1 to fix broken template [#49749](https://github.com/woocommerce/woocommerce/pull/49749) +* Fix - Show variations only for the selected product's variations [#49470](https://github.com/woocommerce/woocommerce/pull/49470) +* Fix - Tooltips message is now selected based on the order status instead of the label of the order status, which would break if the current WordPress language was not English. Also passes the order object to the woocommerce_get_order_status_labels filter to allow for more personalized tooltips. [#49812](https://github.com/woocommerce/woocommerce/pull/49812) +* Fix - Update allowed statuses in legacy payment handler for checkout block. [#48788](https://github.com/woocommerce/woocommerce/pull/48788) +* Add - Use UTM parameters to link Tracks events from connect notice CTA and successful site connection [#50126](https://github.com/woocommerce/woocommerce/pull/50126) +* Add - Add a generic function to determine remote logging eligibility [#49371](https://github.com/woocommerce/woocommerce/pull/49371) +* Add - Add a rest api to manage the product custom fields [#48504](https://github.com/woocommerce/woocommerce/pull/48504) +* Add - Add column `global_unique_id` to `wc_product_meta_lookup` table + Add global_unique_id field to product and product variations [#49472](https://github.com/woocommerce/woocommerce/pull/49472) +* Add - Added notice to the order confirmation page for new accounts instructing them to change the default password. [#48673](https://github.com/woocommerce/woocommerce/pull/48673) +* Add - Add global unique ID field to the classic product editor [#49312](https://github.com/woocommerce/woocommerce/pull/49312) +* Add - Add placeholder options and validation to checkout country and state select inputs. [#49616](https://github.com/woocommerce/woocommerce/pull/49616) +* Add - Add Refresh Remote Inbox Notifications and Delete Remote Notification Tools [#48961](https://github.com/woocommerce/woocommerce/pull/48961) +* Add - Add some robots.txt rules about directories created by WooCommerce [#49432](https://github.com/woocommerce/woocommerce/pull/49432) +* Add - Adds product usage notice [#47697](https://github.com/woocommerce/woocommerce/pull/47697) +* Add - Add support to run e2e tests against WPCOM website. [#49403](https://github.com/woocommerce/woocommerce/pull/49403) +* Add - Add UTM parameters to missing payment method notice links. [#49621](https://github.com/woocommerce/woocommerce/pull/49621) +* Add - Add validation for `__experimentalRegisterProductCollection` arguments [#48141](https://github.com/woocommerce/woocommerce/pull/48141) +* Add - CYS: add badge that informs how many patterns have been inserted from each category. [#48668](https://github.com/woocommerce/woocommerce/pull/48668) +* Add - CYS: Add default patterns. [#48839](https://github.com/woocommerce/woocommerce/pull/48839) +* Add - CYS: Add Tracking for Fiverr Logo Maker CTA [#49783](https://github.com/woocommerce/woocommerce/pull/49783) +* Add - CYS: Enable the PTK feature. [#49565](https://github.com/woocommerce/woocommerce/pull/49565) +* Add - Expose __experimentalRegisterProductCollection in @woocommerce/blocks-registry Package [#48141](https://github.com/woocommerce/woocommerce/pull/48141) +* Add - Improvements in the handling of feature compatibility for plugins [#48169](https://github.com/woocommerce/woocommerce/pull/48169) +* Add - Product Collection: Rename "Sync with current query" option to "Use page context" and make it working in non-archive context as well [#49627](https://github.com/woocommerce/woocommerce/pull/49627) +* Update - Add pattern validation for global_unique_id [#50501](https://github.com/woocommerce/woocommerce/pull/50501) +* Update - Clear global_unique_id when restoring a product that doesn't have an unique id [#50496](https://github.com/woocommerce/woocommerce/pull/50496) +* Update - Use admin password reset on admin login screen [#50200](https://github.com/woocommerce/woocommerce/pull/50200) +* Update - Prevent creation of password-protected coupons. [#50236](https://github.com/woocommerce/woocommerce/pull/50236) +* Update - Add a link to the Theming docs from the blockified templates README.md file [#48538](https://github.com/woocommerce/woocommerce/pull/48538) +* Update - Add a new icon style to the "Customer Account" block. [#48979](https://github.com/woocommerce/woocommerce/pull/48979) +* Update - Adds setting to control the visibility of product count in Mini cart block [#48545](https://github.com/woocommerce/woocommerce/pull/48545) +* Update - Add tracking for enable auto renew links on notices [#49710](https://github.com/woocommerce/woocommerce/pull/49710) +* Update - Add UTM parameters to subscription renewal notice links. [#49645](https://github.com/woocommerce/woocommerce/pull/49645) +* Update - Adjust input field height and input label text size. [#49636](https://github.com/woocommerce/woocommerce/pull/49636) +* Update - Adjust top margin of the coupon code in the Cart and Checkout blocks. [#49603](https://github.com/woocommerce/woocommerce/pull/49603) +* Update - CYS - Add borders to footer and header patterns on the assembler. [#49299](https://github.com/woocommerce/woocommerce/pull/49299) +* Update - CYS - Add missing patterns to their categories for the assembler [#49154](https://github.com/woocommerce/woocommerce/pull/49154) +* Update - CYS - Add tracking events to pattern features. [#49556](https://github.com/woocommerce/woocommerce/pull/49556) +* Update - CYS - Disable the Full Composability for CYS AI flows [#49290](https://github.com/woocommerce/woocommerce/pull/49290) +* Update - CYS - Fetch patterns from the private dotcom patterns category instead of from the default source site. [#49007](https://github.com/woocommerce/woocommerce/pull/49007) +* Update - CYS - Fetch patterns from the WooCommerce PTK source site. [#48492](https://github.com/woocommerce/woocommerce/pull/48492) +* Update - CYS - Filter out patterns with external dependencies. [#48618](https://github.com/woocommerce/woocommerce/pull/48618) +* Update - CYS - Fix CSS spacing issues in the assembler. [#49232](https://github.com/woocommerce/woocommerce/pull/49232) +* Update - CYS - Fix dark patterns buttons color. [#49181](https://github.com/woocommerce/woocommerce/pull/49181) +* Update - CYS - Fix the column spacing for the "Four Image Grid Content Left" pattern [#49669](https://github.com/woocommerce/woocommerce/pull/49669) +* Update - CYS - Fix the font size of the "DON'T HAVE A LOGO YET?" title. [#49231](https://github.com/woocommerce/woocommerce/pull/49231) +* Update - CYS - Fix the intro cards size to match the designs. [#49297](https://github.com/woocommerce/woocommerce/pull/49297) +* Update - CYS - Fix the pattern preview border color on hover and for inserted patterns. [#49206](https://github.com/woocommerce/woocommerce/pull/49206) +* Update - CYS - Improve margins for CYS core patterns. [#49196](https://github.com/woocommerce/woocommerce/pull/49196) +* Update - CYS - Improve the designs of the Intro page bottom cards. [#48983](https://github.com/woocommerce/woocommerce/pull/48983) +* Update - CYS - Include the dotcom patterns from the "Reviews" category. [#49140](https://github.com/woocommerce/woocommerce/pull/49140) +* Update - CYS - Make some titles bold on CYS patterns. [#49151](https://github.com/woocommerce/woocommerce/pull/49151) +* Update - CYS - Make the patterns content translatable for patterns in the dictionary. [#49633](https://github.com/woocommerce/woocommerce/pull/49633) +* Update - CYS - Register PTK "Testimonials" patterns as "Reviews" [#48674](https://github.com/woocommerce/woocommerce/pull/48674) +* Update - CYS - Remove non-default patterns and register them from the PTK. Update margins. [#49101](https://github.com/woocommerce/woocommerce/pull/49101) +* Update - CYS - Schedule the `fetch_patterns` actions only once every hour. [#49754](https://github.com/woocommerce/woocommerce/pull/49754) +* Update - CYS - Update pattern categories and its descriptions. [#48665](https://github.com/woocommerce/woocommerce/pull/48665) +* Update - CYS - Update pattern toolbar delete button copy to `Delete`. [#49295](https://github.com/woocommerce/woocommerce/pull/49295) +* Update - CYS - Update the full composability layout styles [#49303](https://github.com/woocommerce/woocommerce/pull/49303) +* Update - CYS - Update the intro pages for different type of themes. [#49910](https://github.com/woocommerce/woocommerce/pull/49910) +* Update - CYS: Add the proper tracking string to the external Fiverr link in sidebar of the **Add your logo** screen. [#49745](https://github.com/woocommerce/woocommerce/pull/49745) +* Update - CYS: Add `rel="noreferrer"` to External Fiverr Link in sidebar of **Add your logo** screen. [#49314](https://github.com/woocommerce/woocommerce/pull/49314) +* Update - CYS: Improve Block Toolbar logic. [#48799](https://github.com/woocommerce/woocommerce/pull/48799) +* Update - CYS: improve copy no blocks placeholder. [#49030](https://github.com/woocommerce/woocommerce/pull/49030) +* Update - CYS: Improve patterns order [#49204](https://github.com/woocommerce/woocommerce/pull/49204) +* Update - CYS: no highlight the pattern when it is added. [#48802](https://github.com/woocommerce/woocommerce/pull/48802) +* Update - CYS: Remove margin last pattern preview [#49767](https://github.com/woocommerce/woocommerce/pull/49767) +* Update - CYS: Remove not necessary patterns. [#48750](https://github.com/woocommerce/woocommerce/pull/48750) +* Update - CYS: Revisit sidebar layout. [#48803](https://github.com/woocommerce/woocommerce/pull/48803) +* Update - CYS: Update Block Toolbar Position [#48662](https://github.com/woocommerce/woocommerce/pull/48662) +* Update - CYS: Update icon used by the "Customer account" block into header patterns [#49133](https://github.com/woocommerce/woocommerce/pull/49133) +* Update - CYS: Update sidebar homepage copy [#48882](https://github.com/woocommerce/woocommerce/pull/48882) +* Update - CYS: Update verbiage in the CTA to our Fiverr Logo Maker landing page. [#48987](https://github.com/woocommerce/woocommerce/pull/48987) +* Update - CYS: when the footer/header is clicked, the border color is blue. [#48765](https://github.com/woocommerce/woocommerce/pull/48765) +* Update - Deprecate and create new coming soon block version [#49786](https://github.com/woocommerce/woocommerce/pull/49786) +* Update - E2E: check the `Add` button when creating product variations in the new Product Editor [#48928](https://github.com/woocommerce/woocommerce/pull/48928) +* Update - E2E: in the new Product Editor app, update how to detect when global attributes are loaded. [#48915](https://github.com/woocommerce/woocommerce/pull/48915) +* Update - E2E: remove UI check when creating attribute global terms [#48934](https://github.com/woocommerce/woocommerce/pull/48934) +* Update - Ensure expiration-related modal is shown to the installed Woo subscriptions [#49747](https://github.com/woocommerce/woocommerce/pull/49747) +* Update - Ensures the product ID is valid when interacting with product variations via the REST API. [#48804](https://github.com/woocommerce/woocommerce/pull/48804) +* Update - Ensure that active plugins shown in the System Status api endpoint actually exist [#48709](https://github.com/woocommerce/woocommerce/pull/48709) +* Update - Ensuring product creation with unique sku for concurrent requests [#47476](https://github.com/woocommerce/woocommerce/pull/47476) +* Update - Fix: Show preview label only when Product Collection block is selected [#48795](https://github.com/woocommerce/woocommerce/pull/48795) +* Update - Fix Classic Template block registration on WP 6.6 [#48730](https://github.com/woocommerce/woocommerce/pull/48730) +* Update - Fix typo on Congratulations screen [#49233](https://github.com/woocommerce/woocommerce/pull/49233) +* Update - Hide account creation options not relevent to block checkout when using block checkout. [#49389](https://github.com/woocommerce/woocommerce/pull/49389) +* Update - Improve rendering of order list table on mobile. [#49592](https://github.com/woocommerce/woocommerce/pull/49592) +* Update - Improve the handling of the deprecated WC()->api property [#48884](https://github.com/woocommerce/woocommerce/pull/48884) +* Update - Make proceed to order button non sticky when zoom level is bigger than 100% [#48391](https://github.com/woocommerce/woocommerce/pull/48391) +* Update - Make Single Product gallery thumbnail images sharper by defining a srcset [#49112](https://github.com/woocommerce/woocommerce/pull/49112) +* Update - Migrate the cart and checkout block's state, country and custom field input to a native select [#48180](https://github.com/woocommerce/woocommerce/pull/48180) +* Update - Move remote logger to `./src` and improve `fetch_latest_woocommerce_version()` logic [#49639](https://github.com/woocommerce/woocommerce/pull/49639) +* Update - Optimize the method that gets the downloads count for a given download [#49008](https://github.com/woocommerce/woocommerce/pull/49008) +* Update - preparing checkout blocks docs for dev docs site [#49010](https://github.com/woocommerce/woocommerce/pull/49010) +* Update - Product Collection: revert renaming "Sync with current query" option [#49907](https://github.com/woocommerce/woocommerce/pull/49907) +* Update - Product Editor: improve E2E tests. Test the `+3 More` item label in the Organization tab [#48891](https://github.com/woocommerce/woocommerce/pull/48891) +* Update - Product Editor: restore and fix E2E test that creates product variations [#48725](https://github.com/woocommerce/woocommerce/pull/48725) +* Update - Product Editor: restore Product (local) Attributes E2E test [#48871](https://github.com/woocommerce/woocommerce/pull/48871) +* Update - Product Editor: update create product variations E2E test [#48627](https://github.com/woocommerce/woocommerce/pull/48627) +* Update - Redesigned the Product Collection block's insertion journey. [#48911](https://github.com/woocommerce/woocommerce/pull/48911) +* Update - Remove Jetpack copy experiment from core profiler [#49452](https://github.com/woocommerce/woocommerce/pull/49452) +* Update - Remove the hooked blocks feature gate and replace with wc_hooked_blocks_version option [#49302](https://github.com/woocommerce/woocommerce/pull/49302) +* Update - Replace the "Customer account" line logo. [#49666](https://github.com/woocommerce/woocommerce/pull/49666) +* Update - Return HTTP 404 for REST API requests involving non-existing tax class. [#48579](https://github.com/woocommerce/woocommerce/pull/48579) +* Update - Return HTTP 404 when accessing non-existent webhooks via REST API. [#48729](https://github.com/woocommerce/woocommerce/pull/48729) +* Update - Return HTTP 404 when trying to read or delete a non-existent product review. [#48726](https://github.com/woocommerce/woocommerce/pull/48726) +* Update - Run full e2e tests suite against Pressable and WPCOM websites. [#49597](https://github.com/woocommerce/woocommerce/pull/49597) +* Update - Update content for usage tracking modal for CYS experience [#49911](https://github.com/woocommerce/woocommerce/pull/49911) +* Update - Updated account settings descriptions for added clarity [#48556](https://github.com/woocommerce/woocommerce/pull/48556) +* Update - Update Jetpack's new SSO classes and methods to prevent deprecation notice. [#49752](https://github.com/woocommerce/woocommerce/pull/49752) +* Update - Update required and tested up to WP versions for the WordPress 6.6 release. [#49619](https://github.com/woocommerce/woocommerce/pull/49619) +* Update - Update Settings to disable Save button unless modifications are made. [#47444](https://github.com/woocommerce/woocommerce/pull/47444) +* Update - Update shipping method setup modal copy if the block-based local pickup is enabled [#48529](https://github.com/woocommerce/woocommerce/pull/48529) +* Update - Update Store Alert styles [#49174](https://github.com/woocommerce/woocommerce/pull/49174) +* Update - Update Store Alert widths to match main body [#48487](https://github.com/woocommerce/woocommerce/pull/48487) +* Update - Update the "Customer Account" block icon for the line style. [#49401](https://github.com/woocommerce/woocommerce/pull/49401) +* Update - Update the CYS opt-in messaging [#49894](https://github.com/woocommerce/woocommerce/pull/49894) +* Update - Update the footer section in "Add products" task [#49782](https://github.com/woocommerce/woocommerce/pull/49782) +* Update - Update WC Tasks in the WC Home. Rename to WooCommerce marketplace, add new browse marketplace, remove connect to woocomerce.com from inbox [#48128](https://github.com/woocommerce/woocommerce/pull/48128) +* Update - Utilize the new shared component to showcase WooPayments payment method logos. [#49300](https://github.com/woocommerce/woocommerce/pull/49300) +* Dev - Add Allure to Blocks e2e tests [#49228](https://github.com/woocommerce/woocommerce/pull/49228) +* Dev - Add daily checks for core e2e with PHP 8.1 and WP latest-1 [#48929](https://github.com/woocommerce/woocommerce/pull/48929) +* Dev - Add hook to customize the rendered receipt template [#48872](https://github.com/woocommerce/woocommerce/pull/48872) +* Dev - Add new CI workflow to trigger tests on demand. [#49674](https://github.com/woocommerce/woocommerce/pull/49674) +* Dev - Add support for e2e testing against external sites in CI [#49017](https://github.com/woocommerce/woocommerce/pull/49017) +* Dev - Another attempt to stabilize flaky Product Collection E2E tests. [#49638](https://github.com/woocommerce/woocommerce/pull/49638) +* Dev - Blocks E2E: Fix a flaky Product Collection test where accidental multiple edits occur and break the template saving step. [#49590](https://github.com/woocommerce/woocommerce/pull/49590) +* Dev - Blocks E2E: Fix DB snapshot removal step in setup script. [#49677](https://github.com/woocommerce/woocommerce/pull/49677) +* Dev - Build: speedup dependencies installation by disabling composer optimize autoloading by default. [#48980](https://github.com/woocommerce/woocommerce/pull/48980) +* Dev - CI: buffix for linting missing strict types directive. [#49015](https://github.com/woocommerce/woocommerce/pull/49015) +* Dev - CI: cleanup CI related command after fixing jobs matrix generation. [#49330](https://github.com/woocommerce/woocommerce/pull/49330) +* Dev - CI: code style fixes to pass linting in updated CI environment. [#49020](https://github.com/woocommerce/woocommerce/pull/49020) +* Dev - CI: improve flacky tests reporting job execution time. [#49665](https://github.com/woocommerce/woocommerce/pull/49665) +* Dev - CI: Re-group PHPUnit jobs. [#49443](https://github.com/woocommerce/woocommerce/pull/49443) +* Dev - CI: reduce running time for PHPUnit related jobs. [#49193](https://github.com/woocommerce/woocommerce/pull/49193) +* Dev - CI: tuning deps caching for playwright. [#49081](https://github.com/woocommerce/woocommerce/pull/49081) +* Dev - CI config: update changes lists to include wp-env config [#49626](https://github.com/woocommerce/woocommerce/pull/49626) +* Dev - Clean up unused files in plugins/woocommerce-blocks [#49319](https://github.com/woocommerce/woocommerce/pull/49319) +* Dev - Create a separate JS cart and checkout JavaScript bundle to improve performance. [#48010](https://github.com/woocommerce/woocommerce/pull/48010) +* Dev - CYS - Fix the "test_fetch_patterns_should_register_testimonials_category_as_reviews" tests. [#48719](https://github.com/woocommerce/woocommerce/pull/48719) +* Dev - Dropping select2 and point it to SelectWoo [#48731](https://github.com/woocommerce/woocommerce/pull/48731) +* Dev - E2E: enable slow tests reporting for blocks E2E tests. [#49367](https://github.com/woocommerce/woocommerce/pull/49367) +* Dev - E2E tests: fix basic spec for multiple environments [#49609](https://github.com/woocommerce/woocommerce/pull/49609) +* Dev - E2E tests: fix failing settings-tax e2e test [#48792](https://github.com/woocommerce/woocommerce/pull/48792) +* Dev - E2E tests: Fix flaky account email receiving test [#48957](https://github.com/woocommerce/woocommerce/pull/48957) +* Dev - E2E tests: Fix flaky connect to Woo.com test [#48926](https://github.com/woocommerce/woocommerce/pull/48926) +* Dev - E2E tests: Fix flaky filling regular price in the inventory tab [#49226](https://github.com/woocommerce/woocommerce/pull/49226) +* Dev - E2E tests: Fix flaky filling SKU field and CYS footer [#49191](https://github.com/woocommerce/woocommerce/pull/49191) +* Dev - E2E tests: Fix flaky Gutenberg, WC Services tests [#48916](https://github.com/woocommerce/woocommerce/pull/48916) +* Dev - E2E tests: Fix flaky Gutenberg tests [#48896](https://github.com/woocommerce/woocommerce/pull/48896) +* Dev - E2E tests: Fix flaky Gutenberg tests [#49548](https://github.com/woocommerce/woocommerce/pull/49548) +* Dev - E2E tests: Fix flaky logo picker waiting for response tests [#49451](https://github.com/woocommerce/woocommerce/pull/49451) +* Dev - E2E tests: Fix flaky merchant filling sku field in the new editor [#49134](https://github.com/woocommerce/woocommerce/pull/49134) +* Dev - E2E tests: Fix flaky merchant tests [#49381](https://github.com/woocommerce/woocommerce/pull/49381) +* Dev - E2E tests: Fix merchant settings general test [#48907](https://github.com/woocommerce/woocommerce/pull/48907) +* Dev - E2E tests: fix shopper checkout block test [#49596](https://github.com/woocommerce/woocommerce/pull/49596) +* Dev - E2E tests: fix some tests for WP 6.6 [#49634](https://github.com/woocommerce/woocommerce/pull/49634) +* Dev - E2E tests: tag different envs in e2e test suite to run in workflows [#48715](https://github.com/woocommerce/woocommerce/pull/48715) +* Dev - Final sanity check to make sure attributes are done saving [#48737](https://github.com/woocommerce/woocommerce/pull/48737) +* Dev - Fix broken syntax in e2e-guidelines.md. [#49018](https://github.com/woocommerce/woocommerce/pull/49018) +* Dev - Fix flaky orphaned refund test [#49741](https://github.com/woocommerce/woocommerce/pull/49741) +* Dev - Hide product filters overlay template part experimental feature from public release [#49564](https://github.com/woocommerce/woocommerce/pull/49564) +* Dev - In blocks codebase, export SITE_CURRENCY property with properties matching typescript definitions. [#48727](https://github.com/woocommerce/woocommerce/pull/48727) +* Dev - Lint new PHP files for strict types directive [#48943](https://github.com/woocommerce/woocommerce/pull/48943) +* Dev - Minor tooling tweaks (zip compression level, composer invocation) [#48857](https://github.com/woocommerce/woocommerce/pull/48857) +* Dev - Monorepo: enable Jest and babel-loader caching. [#49656](https://github.com/woocommerce/woocommerce/pull/49656) +* Dev - Monorepo: fix side-effects of setting strict mode for linting script. [#49436](https://github.com/woocommerce/woocommerce/pull/49436) +* Dev - Monorepo: minor tweaks in zip building script (use frozen lock file when installing dependecies). [#49640](https://github.com/woocommerce/woocommerce/pull/49640) +* Dev - Monorepo: refine approach to patching dependecies in favour of built-in pnpm functionality. [#49892](https://github.com/woocommerce/woocommerce/pull/49892) +* Dev - Monorepo: set strict mode for linting script. [#49366](https://github.com/woocommerce/woocommerce/pull/49366) +* Dev - Monorepo: tweak patching dependencies for better Webpack perfromance. [#49703](https://github.com/woocommerce/woocommerce/pull/49703) +* Dev - Move buildkite-test-collector to devDependencies [#49051](https://github.com/woocommerce/woocommerce/pull/49051) +* Dev - move docs out of main folder until subcategories are ready [#49354](https://github.com/woocommerce/woocommerce/pull/49354) +* Dev - Product Collection: add tracking for block usage [#46466](https://github.com/woocommerce/woocommerce/pull/46466) +* Dev - Remove performance tests from PR checks (leave on push to trunk) [#48927](https://github.com/woocommerce/woocommerce/pull/48927) +* Dev - Switch `render()` to `createRoot().render()` to use React 18 features. [#48858](https://github.com/woocommerce/woocommerce/pull/48858) +* Dev - Tests: pin wp-env core version to 6.5 (temporary until tests are fixed to pass with 6.6) [#49620](https://github.com/woocommerce/woocommerce/pull/49620) +* Dev - Tweaks related to caching Composer dependecies and Playwright downloads in CI. [#48865](https://github.com/woocommerce/woocommerce/pull/48865) +* Dev - Update Action Scheduler to 3.8.1 [#49483](https://github.com/woocommerce/woocommerce/pull/49483) +* Dev - Update a few e2e tests failing on the daily run [#49313](https://github.com/woocommerce/woocommerce/pull/49313) +* Dev - Update all values to be random so that retries don't fail on assertions [#48734](https://github.com/woocommerce/woocommerce/pull/48734) +* Dev - Updated CodeSniffer configuration to address conflicting rules. [#49183](https://github.com/woocommerce/woocommerce/pull/49183) +* Dev - Update Playwright from 1.44 to 1.45 [#49202](https://github.com/woocommerce/woocommerce/pull/49202) +* Tweak - Add admin_install_timestamp in WC_Tracker [#50076](https://github.com/woocommerce/woocommerce/pull/50076) +* Tweak - Add $wpdb->esc_like to the search criteria when searching for a product custom field name [#48949](https://github.com/woocommerce/woocommerce/pull/48949) +* Tweak - Add the initially installed WooCommerce version to the wp_options table [#49139](https://github.com/woocommerce/woocommerce/pull/49139) +* Tweak - Change CLIRunner namespace for better PSR compatibility. [#49390](https://github.com/woocommerce/woocommerce/pull/49390) +* Tweak - Equalize data in Order export with data in Client side CSV export [#49356](https://github.com/woocommerce/woocommerce/pull/49356) +* Tweak - Exclude coming soon patterns from block inserter [#48821](https://github.com/woocommerce/woocommerce/pull/48821) +* Tweak - Fix tip block syntax in register-product-collection.md [#49785](https://github.com/woocommerce/woocommerce/pull/49785) +* Tweak - Get tax line label instead of name in StoreAPI Order endpoint. [#48445](https://github.com/woocommerce/woocommerce/pull/48445) +* Tweak - Improve checks when offering to remove test orders [#49032](https://github.com/woocommerce/woocommerce/pull/49032) +* Tweak - Initialize BlockTemplatesController for block themes only. [#48905](https://github.com/woocommerce/woocommerce/pull/48905) +* Tweak - Product Collection: flatten telemetry query filters data to json string [#49680](https://github.com/woocommerce/woocommerce/pull/49680) +* Tweak - Remove enctype from verify email form [#48859](https://github.com/woocommerce/woocommerce/pull/48859) +* Tweak - Remove unneeded IE styling as IE is no longer supported [#49027](https://github.com/woocommerce/woocommerce/pull/49027) +* Tweak - Rename Google Listings and Ads with Google for WooCommerce [#47614](https://github.com/woocommerce/woocommerce/pull/47614) +* Tweak - Show Guest when there is no Customer name [#49594](https://github.com/woocommerce/woocommerce/pull/49594) +* Tweak - Trigger doing_it_wrong() when using HPOS query args for CPT order queries. [#47457](https://github.com/woocommerce/woocommerce/pull/47457) +* Tweak - Update text to Content right with image left pattern [#49792](https://github.com/woocommerce/woocommerce/pull/49792) +* Tweak - Update the "API enabled" entry in the System Status Report to clarify that it pertains to the Legacy REST API. [#48878](https://github.com/woocommerce/woocommerce/pull/48878) +* Tweak - Add placeholder options and validation to all checkout select inputs. [#49929](https://github.com/woocommerce/woocommerce/pull/49929) +* Performance - Load REST API namespaces only when needed. [#47704](https://github.com/woocommerce/woocommerce/pull/47704) +* Performance - Reduced number of recalculations on Store API cart routes [#48944](https://github.com/woocommerce/woocommerce/pull/48944) +* Enhancement - Add address title to edit/add buttons on My Account page [#49171](https://github.com/woocommerce/woocommerce/pull/49171) +* Enhancement - Added password field to block checkout (when enabled in settings) for new accounts to set a custom password. [#48985](https://github.com/woocommerce/woocommerce/pull/48985) +* Enhancement - Add required indication to login forms [#48743](https://github.com/woocommerce/woocommerce/pull/48743) +* Enhancement - Add scope attributes to the order table on My Account [#49201](https://github.com/woocommerce/woocommerce/pull/49201) +* Enhancement - Add strength meter to block checkout password field. [#49164](https://github.com/woocommerce/woocommerce/pull/49164) +* Enhancement - Allow blocks with parents in the "woocommerce" namespace to be added to the Checkout block without requiring them to be added to the "__experimental_woocommerce_blocks_add_data_attributes_to_block" hook. [#48543](https://github.com/woocommerce/woocommerce/pull/48543) +* Enhancement - Convert edit address link to a button on checkout [#49471](https://github.com/woocommerce/woocommerce/pull/49471) +* Enhancement - CYS: make the entire shuffle section clickable. [#48889](https://github.com/woocommerce/woocommerce/pull/48889) +* Enhancement - CYS: Remove iframe animation [#48941](https://github.com/woocommerce/woocommerce/pull/48941) +* Enhancement - Made the "return to cart" link (in the checkout block) hidden by default. [#48762](https://github.com/woocommerce/woocommerce/pull/48762) +* Enhancement - Provide the location context within the Product Collection block context [#44145](https://github.com/woocommerce/woocommerce/pull/44145) +* Enhancement - Updated block checkout and Store API stock handling so stock is only reserved when attempting payment for an order. [#49446](https://github.com/woocommerce/woocommerce/pull/49446) + = 9.1.4 2024-07-26 = **WooCommerce** @@ -8,7 +347,6 @@ * Fix - Hardening against XSS via the Product Button unescaped attribute. [#50010](https://github.com/woocommerce/woocommerce/pull/50010) * Fix - Enhance escaping for block attributes. [#50015](https://github.com/woocommerce/woocommerce/pull/50015) - = 9.1.2 2024-07-12 = **WooCommerce** @@ -5901,12 +6239,12 @@ * Dev - Revert work done in #4857 for automated shipping after OBW is completed #5971 * Add - Welcome modal when coming from Calypso #6004 * Enhancement - Add an a/b experiment for installing free business features #5786 -* Dev - Add `onChangeCallback` feature to the wc-admin `
` component #5786 +* Dev - Add `onChangeCallback` feature to the wc-admin `` component #5786 * Fix - Generate JSON translation chunks on plugin activation #6028 -* Dev - Add merchant email notifications #5922 +* Dev - Add merchant email notifications #5922 * Add - Email note to add first product. #6024 * Add - Note for users coming from Calypso. #6030 -* Enhancement - Add an "unread" indicator to inbox messages. #6047 +* Enhancement - Add an "unread" indicator to inbox messages. #6047 * Add - Manage activity from home screen inbox message. #6072 = 4.9.5 2022-03-10 = @@ -6709,7 +7047,7 @@ * Fix - The WCPay method not appearing as recommended sometimes #4345 * Fix - Removed URLSearchParams method #4501 * Fix - REST API collections schema. #4484 -* Fix - null issue in wpNavMenuClassChange #4513 🎉 gradosevic +* Fix - null issue in wpNavMenuClassChange #4513 🎉 gradosevic * Fix - RTL stylesheet loading for split code chunks. #4542 * Fix - Don't show store location step in tax and shipping tasks if the address has already been provided #4507 * Fix - Check for enabled methods before payment task completion #4530 @@ -6851,7 +7189,7 @@ * Dev - Fixed failing unit tests. #105 **WooCommerce Admin 1.2.3** -* Enhancement - Add onboarding payments note #4157 +* Enhancement - Add onboarding payments note #4157 * Enhancement - Marketing Inbox Note #4030 * Performance - Use Route based code splitting to reduce bundle size #4094 * Performance - trim down inbox note API request. #3977 @@ -7262,7 +7600,7 @@ * Dev - Add a filter `woocommerce_ajax_add_order_item_validation` to allow validations in `add_order_item` function. #24518 * Dev - Use `wc_get_cart_url` instead of `wc_get_page_permalink( 'cart' )` because former has a filter `woocommerce_get_cart_url` to allow customization. #24530 * Dev - New `woocommerce_product_after_tabs` action hook added. #24694 -* Dev - Enable append hashes on custom events (like ajax requests) #24665 +* Dev - Enable append hashes on custom events (like ajax requests) #24665 * Dev - Introduced `woocommerce_order_get_formatted_billing_address` and `woocommerce_order_get_formatted_shipping_address` filters. #24677 * Dev - WC_Abstract_Order::recalculate_coupons() is public now. #24740 * Dev - Added 'applied_coupon' trigger to checkout.js. #24406 @@ -7375,7 +7713,7 @@ * Tweak - Update the generate username setting description label to reflect how the username is actually generated. #23911 * Tweak - OBW: Adjust plugin highlight container sizes to avoid overlap. #23997 * Tweak - Round tax amounts late when the round at subtotal level setting is enabled to reduce rounding errors. #24024 -* Tweak - OBW: Now includes WooCommerce Admin as a recommended plugin. #24058 +* Tweak - OBW: Now includes WooCommerce Admin as a recommended plugin. #24058 * Template - Review and update all template files escaping. #23460 * Template - Remove mention of shipping section from the checkout/form-login.php template as shipping is not always a requirement for an order. #23941 * Template - Add new filter `woocommerce_before_thankyou` to the checkout/thankyou.php template. #23538 @@ -8047,7 +8385,7 @@ * Fix - Fix warning when using logger instance in woocommerce_logging_class filter. #21448 * Fix - Use uppercase "ID" when sorting product queries by ID. #21461 * Fix - Consistently escape the gateway ID in the checkout payment method template. #21439 -* Fix - Avoid treating HTTP 301 and 302 codes as failures for webhooks. #21491 +* Fix - Avoid treating HTTP 301 and 302 codes as failures for webhooks. #21491 * Fix - Add address_1 to shipping packages info in WC_Cart:: get_shipping_packages to make it work correctly in address formatting functions. #21493 * Fix - Don't fire two of the same action when saving shipping settings. #21494 * Fix - Remove double condition for address line 2 in `WC_Countries::get_default_address_fields`. #20629 @@ -8359,7 +8697,7 @@ * Fix - Delete orphaned variations after product import. #19378 * Fix - Ensure API credentials exist before defining PayPal refund support. #19380 * Fix - Force word-wrapping in the log viewer to prevent layout-breaking long lines. #19503 -* Fix - Removes permission checks that were preventing webhooks from displaying properly when no post object existed. #19508 +* Fix - Removes permission checks that were preventing webhooks from displaying properly when no post object existed. #19508 * Fix - Empty cart after completing PayPal payment. #19509 * Fix - Strip tags on aria-labels in Add to Cart template to prevent broken HTML. #19522 * Fix - Update post_modified date when saving products and variations but no other product data. #19595 @@ -8381,7 +8719,7 @@ * Fix - WC API should not try to create a product image when creating a product variation if an empty image is passed. #19971 * Fix - Force settings API settings to autoload by default. #19998 * Fix - Cart html5 validation events when using keyboard. #20001 -* Fix - Don't show stock status fields in external product quick-edit. #20005 +* Fix - Don't show stock status fields in external product quick-edit. #20005 * Fix - Prevent an infinite loop if 2 grouped products are linked. #20020 * Fix - Switch stock_status when manage stock gets changed to prevent being out of stock if stock quantity is > 0. #20021 * Fix - When duplicating variation, set the date to null. #20083 @@ -8643,7 +8981,7 @@ * Fix - is_visible should ensure product is is not trashed before returning true. * Fix - Return packages with no rates back to the cart so the shipping calculator is displayed even when the current country is not shippable. * Fix - Merge session and persistent carts when both exists after login. -* Fix - Remove "wc_error" query string after login. +* Fix - Remove "wc_error" query string after login. * Fix - Allow woocommerce_form_field() have 'custom_attributes' equal 0. * Fix - Bulk actions in status logs table. * Fix - Exclude add-to-cart from pagination links. @@ -8684,19 +9022,19 @@ * Fix - Removed class within class in admin meta boxes HTML. * Fix - Fixed wrong `flex-control-nav` selector scope in `add-to-cart-variation.js` * Fix - Allow variations to be added to cart from query string. -* Fix - Use `add_filter` for `comment_feed_where` hook. +* Fix - Use `add_filter` for `comment_feed_where` hook. * Fix - Change nocache_headers hook firing in the cache helper. * Fix - Coupon min/max spend based on displayed subtotal. * Fix - Fix event propagation on click in setup wizard and improve validation. * Fix - API - Change how line items are saved in API so calculations are correct. -* Tweak - Hide downloads from admin emails. +* Tweak - Hide downloads from admin emails. * Tweak - Set placeholder for variation lxwxh field to that of the parent. * Tweak - Improve the Add Payment Methods display so buttons are not shown when no payment methods support the feature. * Localization - Update NJ tax rate. * Localization - Add Belarusian ruble BYN. = 3.2.3 - 2017-11-02 = -* Fix - Fixed a conflict with some slider plugins due to sanitization of archive/term descriptions. +* Fix - Fixed a conflict with some slider plugins due to sanitization of archive/term descriptions. * Fix - Fixed a flexslider bug when there is only 1 image on the product page (no gallery). * Fix - Prevent potential notices when someone extends product tabs wrongly. * Fix - Fixed display of shipping calculator under some conditions. diff --git a/docs/.markdownlint.json b/docs/.markdownlint.json index 6728d0fd76a..3297523f014 100644 --- a/docs/.markdownlint.json +++ b/docs/.markdownlint.json @@ -3,7 +3,7 @@ "MD003": { "style": "atx" }, "MD007": { "indent": 4 }, "MD013": { "line_length": 9999 }, - "MD024": { "allow_different_nesting": true }, + "MD024": { "siblings_only": true }, "MD033": { "allowed_elements": ["video"] }, "MD035": false, "MD041": false, diff --git a/docs/block-theme-development/README.md b/docs/block-theme-development/README.md new file mode 100644 index 00000000000..7b2564c7433 --- /dev/null +++ b/docs/block-theme-development/README.md @@ -0,0 +1,5 @@ +--- +category_title: Block Theme Development +category_slug: block-theme-development +post_title: Block theme development +--- \ No newline at end of file diff --git a/plugins/woocommerce-blocks/docs/designers/theming/cart-and-checkout.md b/docs/block-theme-development/cart-and-checkout.md similarity index 81% rename from plugins/woocommerce-blocks/docs/designers/theming/cart-and-checkout.md rename to docs/block-theme-development/cart-and-checkout.md index 5af905846d8..f3151f8fa5c 100644 --- a/plugins/woocommerce-blocks/docs/designers/theming/cart-and-checkout.md +++ b/docs/block-theme-development/cart-and-checkout.md @@ -1,13 +1,12 @@ -# Cart and Checkout Blocks Theming +--- +post_title: Cart and checkout blocks theming +menu_title: Cart and Checkout Blocks Theming +tags: reference +--- > [!IMPORTANT] -> We strongly discourage writing CSS code based on existing block class names and prioritize using global styles when possible. We especially discourage writing CSS selectors that rely on a specific block being a descendant of another one, as users can move blocks around freely, so they are prone to breaking. Similar to WordPress itself, we consider the HTML structure within components, blocks, and block templates to be “private”, and subject to further change in the future, so using CSS to target the internals of a block or a block template is _not recommended or supported_. +> We strongly discourage writing CSS code based on existing block class names and prioritize using global styles when possible. We especially discourage writing CSS selectors that rely on a specific block being a descendant of another one, as users can move blocks around freely, so they are prone to breaking. Similar to WordPress itself, we consider the HTML structure within components, blocks, and block templates to be "private", and subject to further change in the future, so using CSS to target the internals of a block or a block template is _not recommended or supported_. -## Table of Contents - -- [Buttons](#buttons) -- [Mobile submit container](#mobile-submit-container) -- [Item quantity badge](#item-quantity-badge) ## Buttons @@ -81,13 +80,5 @@ By default, it uses a combination of black and white borders and shadows so it h ![Order summary screenshot with custom styles for the item quantity badge](https://user-images.githubusercontent.com/3616980/83863109-2e421c80-a723-11ea-9bf7-2033a96cf5b2.png) - ---- - -[We're hiring!](https://woocommerce.com/careers/) Come work with us! - -🐞 Found a mistake, or have a suggestion? [Leave feedback about this document here.](https://github.com/woocommerce/woocommerce-blocks/issues/new?assignees=&labels=type%3A+documentation&template=--doc-feedback.md&title=Feedback%20on%20./docs/designers/theming/cart-and-checkout.md) - - diff --git a/plugins/woocommerce-blocks/docs/designers/theming/css-styling.md b/docs/block-theme-development/css-styling.md similarity index 69% rename from plugins/woocommerce-blocks/docs/designers/theming/css-styling.md rename to docs/block-theme-development/css-styling.md index 978dfb7444d..2470d621ae0 100644 --- a/plugins/woocommerce-blocks/docs/designers/theming/css-styling.md +++ b/docs/block-theme-development/css-styling.md @@ -1,11 +1,15 @@ -# CSS Styling +--- +post_title: CSS styling for themes +menu_title: CSS Styling for Themes +tags: reference +--- ## Block and component class names > [!IMPORTANT] -> We strongly discourage writing CSS code based on existing block class names and prioritize using global styles when possible. We especially discourage writing CSS selectors that rely on a specific block being a descendant of another one, as users can move blocks around freely, so they are prone to breaking. Similar to WordPress itself, we consider the HTML structure within components, blocks, and block templates to be “private”, and subject to further change in the future, so using CSS to target the internals of a block or a block template is _not recommended or supported_. +> We strongly discourage writing CSS code based on existing block class names and prioritize using global styles when possible. We especially discourage writing CSS selectors that rely on a specific block being a descendant of another one, as users can move blocks around freely, so they are prone to breaking. Similar to WordPress itself, we consider the HTML structure within components, blocks, and block templates to be "private", and subject to further change in the future, so using CSS to target the internals of a block or a block template is _not recommended or supported_. -WooCommerce Blocks follows BEM for class names, as [stated in our coding guidelines](../../contributors/coding-guidelines.md). All classes start with one of these two prefixes: +WooCommerce Blocks follows BEM for class names, as [stated in our coding guidelines](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-blocks/docs/contributors/coding-guidelines.md). All classes start with one of these two prefixes: * `.wc-block-`: class names specific to a single block. * `.wc-block-components-`: class names specific to a component. The component might be reused by different blocks. @@ -38,10 +42,10 @@ Those classes are: Container width | Class name ----------------|------------ -\>700px | `is-large` +\>700px | `is-large` 521px-700px | `is-medium` 401px-520px | `is-small` -<=400px | `is-mobile` +<=400px | `is-mobile` As an example, if we wanted to do the Checkout font size 10% larger when the container has a width of 521px or wider, we could do so with this code: @@ -54,9 +58,9 @@ As an example, if we wanted to do the Checkout font size 10% larger when the con ## WC Blocks _vs._ theme style conflicts for semantic elements -WooCommerce Blocks uses HTML elements according to their semantic meaning, not their default layout. That means that some times blocks might use an anchor link (``) but display it as a button. Or the other way around, a ` + + + +`; diff --git a/packages/js/components/src/analytics/error/tests/index.test.js b/packages/js/components/src/analytics/error/tests/index.test.js new file mode 100644 index 00000000000..d01f55c8724 --- /dev/null +++ b/packages/js/components/src/analytics/error/tests/index.test.js @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +/** + * Internal dependencies + */ +import AnalyticsError from '..'; + +describe( 'AnalyticsError', () => { + // Mock window.location.reload by using a global variable + const originalLocation = window.location; + + beforeAll( () => { + delete window.location; + window.location = { reload: jest.fn() }; + } ); + + afterAll( () => { + window.location = originalLocation; + } ); + + it( 'displays an error message', () => { + render( ); + + expect( + screen.getByText( + 'There was an error getting your stats. Please try again.' + ) + ).toBeInTheDocument(); + } ); + + it( 'shows reload button', () => { + render( ); + + expect( + screen.getByRole( 'button', { name: 'Reload' } ) + ).toBeInTheDocument(); + } ); + + it( 'refreshes the page when Reload Page button is clicked', () => { + const reloadMock = jest.fn(); + Object.defineProperty( window.location, 'reload', { + configurable: true, + value: reloadMock, + } ); + + render( ); + + userEvent.click( screen.getByText( 'Reload' ) ); + + expect( reloadMock ).toHaveBeenCalled(); + } ); + + it( 'should match snapshot', () => { + const { container } = render( ); + expect( container ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/js/components/src/analytics/index.js b/packages/js/components/src/analytics/index.js new file mode 100644 index 00000000000..ebf10a5b7e5 --- /dev/null +++ b/packages/js/components/src/analytics/index.js @@ -0,0 +1 @@ +export { default as AnalyticsError } from './error'; diff --git a/packages/js/components/src/calendar/input.js b/packages/js/components/src/calendar/input.js index 1b3c433dd18..049b529954b 100644 --- a/packages/js/components/src/calendar/input.js +++ b/packages/js/components/src/calendar/input.js @@ -9,17 +9,17 @@ import { uniqueId, noop } from 'lodash'; import PropTypes from 'prop-types'; const DateInput = ( { - disabled, + disabled = false, value, onChange, dateFormat, label, describedBy, error, - onFocus, - onBlur, - onKeyDown, - errorPosition, + onFocus = () => {}, + onBlur = () => {}, + onKeyDown = noop, + errorPosition = 'bottom center', } ) => { const classes = classnames( 'woocommerce-calendar__input', { 'is-empty': value.length === 0, @@ -73,12 +73,4 @@ DateInput.propTypes = { onKeyDown: PropTypes.func, }; -DateInput.defaultProps = { - disabled: false, - onFocus: () => {}, - onBlur: () => {}, - errorPosition: 'bottom center', - onKeyDown: noop, -}; - export default DateInput; diff --git a/packages/js/components/src/date/index.js b/packages/js/components/src/date/index.js index e67a986039f..281d54ed538 100644 --- a/packages/js/components/src/date/index.js +++ b/packages/js/components/src/date/index.js @@ -15,7 +15,12 @@ import { createElement } from '@wordpress/element'; * @param {string} props.visibleFormat * @return {Object} - */ -const Date = ( { date, machineFormat, screenReaderFormat, visibleFormat } ) => { +const Date = ( { + date, + machineFormat = 'Y-m-d H:i:s', + screenReaderFormat = 'F j, Y', + visibleFormat = 'Y-m-d', +} ) => { return ( @@ -279,7 +279,7 @@ Get a DateValue object for a period prior to the current period. ### getCurrentPeriod(period, compare) ⇒ [DateValue](#DateValue) -Get a DateValue object for a curent period. The period begins on the first day of the period, +Get a DateValue object for a current period. The period begins on the first day of the period, and ends on the current day. **Kind**: global function diff --git a/packages/js/date/changelog/50047-fix-typos b/packages/js/date/changelog/50047-fix-typos new file mode 100644 index 00000000000..e5d2f2b3db2 --- /dev/null +++ b/packages/js/date/changelog/50047-fix-typos @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Comment: Fix comment typos across various files. diff --git a/packages/js/date/changelog/502820-fix-markdown-typos b/packages/js/date/changelog/502820-fix-markdown-typos new file mode 100644 index 00000000000..f3ee7e05d02 --- /dev/null +++ b/packages/js/date/changelog/502820-fix-markdown-typos @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Comment: Fix typos in documentation. \ No newline at end of file diff --git a/packages/js/date/changelog/50828-dev-constrain-pnpm-version b/packages/js/date/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/date/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/date/package.json b/packages/js/date/package.json index 17e3f6152c8..d7f2fef1241 100644 --- a/packages/js/date/package.json +++ b/packages/js/date/package.json @@ -11,7 +11,7 @@ ], "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/date/README.md", "repository": { diff --git a/packages/js/date/src/test/index.ts b/packages/js/date/src/test/index.ts index 7c47f6a2182..87f2a653107 100644 --- a/packages/js/date/src/test/index.ts +++ b/packages/js/date/src/test/index.ts @@ -1052,7 +1052,7 @@ describe( 'getStoreTimeZoneMoment', () => { expect( utcOffset ).not.toHaveBeenCalled(); } ); - it( 'should use the utc offest when it is set', () => { + it( 'should use the utc offset when it is set', () => { global.window.wcSettings = { timeZone: '+06:00', }; diff --git a/packages/js/dependency-extraction-webpack-plugin/changelog/50828-dev-constrain-pnpm-version b/packages/js/dependency-extraction-webpack-plugin/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/dependency-extraction-webpack-plugin/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/dependency-extraction-webpack-plugin/package.json b/packages/js/dependency-extraction-webpack-plugin/package.json index 18eb2cca63e..869625dd58b 100644 --- a/packages/js/dependency-extraction-webpack-plugin/package.json +++ b/packages/js/dependency-extraction-webpack-plugin/package.json @@ -10,7 +10,7 @@ ], "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/dependency-extraction-webpack-plugin/README.md", "repository": { diff --git a/packages/js/e2e-core-tests/package.json b/packages/js/e2e-core-tests/package.json index 7aa1cb71ad0..3f512664f5b 100644 --- a/packages/js/e2e-core-tests/package.json +++ b/packages/js/e2e-core-tests/package.json @@ -10,7 +10,7 @@ "license": "GPL-3.0+", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "main": "build/index.js", "module": "build-module/index.js", diff --git a/packages/js/e2e-environment/package.json b/packages/js/e2e-environment/package.json index f73aff3163c..f5f0afa6411 100644 --- a/packages/js/e2e-environment/package.json +++ b/packages/js/e2e-environment/package.json @@ -12,7 +12,7 @@ ], "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/e2e-environment/README.md", "bugs": { diff --git a/packages/js/e2e-utils/package.json b/packages/js/e2e-utils/package.json index d133e6c9a8f..1e47b320022 100644 --- a/packages/js/e2e-utils/package.json +++ b/packages/js/e2e-utils/package.json @@ -10,7 +10,7 @@ "license": "GPL-3.0+", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "main": "build/index.js", "module": "build-module/index.js", diff --git a/packages/js/eslint-plugin/changelog/50828-dev-constrain-pnpm-version b/packages/js/eslint-plugin/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/eslint-plugin/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/eslint-plugin/package.json b/packages/js/eslint-plugin/package.json index 7913b5a2b3e..4e5407484dc 100644 --- a/packages/js/eslint-plugin/package.json +++ b/packages/js/eslint-plugin/package.json @@ -6,7 +6,7 @@ "license": "GPL-2.0-or-later", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "keywords": [ "wordpress", diff --git a/packages/js/experimental/changelog/50828-dev-constrain-pnpm-version b/packages/js/experimental/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/experimental/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/experimental/package.json b/packages/js/experimental/package.json index de525dbc624..3a2016f62ce 100644 --- a/packages/js/experimental/package.json +++ b/packages/js/experimental/package.json @@ -6,7 +6,7 @@ "license": "GPL-3.0-or-later", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "keywords": [ "wordpress", diff --git a/packages/js/explat/changelog/50828-dev-constrain-pnpm-version b/packages/js/explat/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/explat/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/explat/package.json b/packages/js/explat/package.json index 376e4893ae4..377b1f25e98 100644 --- a/packages/js/explat/package.json +++ b/packages/js/explat/package.json @@ -6,7 +6,7 @@ "license": "GPL-2.0-or-later", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "keywords": [ "wordpress", diff --git a/packages/js/expression-evaluation/changelog/50047-fix-typos b/packages/js/expression-evaluation/changelog/50047-fix-typos new file mode 100644 index 00000000000..e5d2f2b3db2 --- /dev/null +++ b/packages/js/expression-evaluation/changelog/50047-fix-typos @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Comment: Fix comment typos across various files. diff --git a/packages/js/expression-evaluation/changelog/50828-dev-constrain-pnpm-version b/packages/js/expression-evaluation/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/expression-evaluation/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/expression-evaluation/package.json b/packages/js/expression-evaluation/package.json index dc8316bd48c..7ebe4e2fb67 100644 --- a/packages/js/expression-evaluation/package.json +++ b/packages/js/expression-evaluation/package.json @@ -8,11 +8,11 @@ "wordpress", "woocommerce", "expression", - "evalution" + "evaluation" ], "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/expression-evaluation/README.md", "repository": { diff --git a/packages/js/extend-cart-checkout-block/changelog/50828-dev-constrain-pnpm-version b/packages/js/extend-cart-checkout-block/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/extend-cart-checkout-block/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/extend-cart-checkout-block/changelog/add-upgrade-blocks-to-v3 b/packages/js/extend-cart-checkout-block/changelog/add-upgrade-blocks-to-v3 new file mode 100644 index 00000000000..def28ee9bcc --- /dev/null +++ b/packages/js/extend-cart-checkout-block/changelog/add-upgrade-blocks-to-v3 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update all blocks to use API Version 3. diff --git a/packages/js/extend-cart-checkout-block/package.json b/packages/js/extend-cart-checkout-block/package.json index 5695910fd8c..0e032c37149 100644 --- a/packages/js/extend-cart-checkout-block/package.json +++ b/packages/js/extend-cart-checkout-block/package.json @@ -5,7 +5,7 @@ "main": "index.js", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "repository": { "type": "git", diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.json.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.json.mustache index 98553e8cae7..9a157b4c339 100644 --- a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.json.mustache +++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/block.json.mustache @@ -1,5 +1,5 @@ { - "apiVersion": 2, + "apiVersion": 3, "name": "{{slug}}/checkout-newsletter-subscription", "version": "2.0.0", "title": "Newsletter Subscription!", diff --git a/packages/js/internal-e2e-builds/package.json b/packages/js/internal-e2e-builds/package.json index 49c4030cf54..ba7b5c89049 100644 --- a/packages/js/internal-e2e-builds/package.json +++ b/packages/js/internal-e2e-builds/package.json @@ -10,7 +10,7 @@ ], "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "bin": { "e2e-builds": "./build.js" diff --git a/packages/js/internal-js-tests/changelog/50828-dev-constrain-pnpm-version b/packages/js/internal-js-tests/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/internal-js-tests/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/internal-js-tests/package.json b/packages/js/internal-js-tests/package.json index 2a112db89d5..4f1049b8572 100644 --- a/packages/js/internal-js-tests/package.json +++ b/packages/js/internal-js-tests/package.json @@ -6,7 +6,7 @@ "license": "GPL-2.0-or-later", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/internal-js-tests/README.md", "repository": { diff --git a/packages/js/internal-style-build/package.json b/packages/js/internal-style-build/package.json index a777f36e231..9955a65abe9 100644 --- a/packages/js/internal-style-build/package.json +++ b/packages/js/internal-style-build/package.json @@ -6,7 +6,7 @@ "license": "GPL-2.0-or-later", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "keywords": [ "wordpress", diff --git a/packages/js/navigation/changelog/50190-add-navigation-deprecation b/packages/js/navigation/changelog/50190-add-navigation-deprecation new file mode 100644 index 00000000000..1f80f12d42d --- /dev/null +++ b/packages/js/navigation/changelog/50190-add-navigation-deprecation @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove WooCommerce Navigation client side feature and deprecate PHP classes. \ No newline at end of file diff --git a/packages/js/navigation/changelog/50828-dev-constrain-pnpm-version b/packages/js/navigation/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/navigation/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/navigation/changelog/add-navigation-deprecation b/packages/js/navigation/changelog/add-navigation-deprecation new file mode 100644 index 00000000000..478aeb0697e --- /dev/null +++ b/packages/js/navigation/changelog/add-navigation-deprecation @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Remove deprecated Navigation SlotFill diff --git a/packages/js/navigation/package.json b/packages/js/navigation/package.json index 903b2f3ef26..56c29eacab8 100644 --- a/packages/js/navigation/package.json +++ b/packages/js/navigation/package.json @@ -6,7 +6,7 @@ "license": "GPL-3.0-or-later", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "keywords": [ "wordpress", diff --git a/packages/js/navigation/src/index.js b/packages/js/navigation/src/index.js index 9197c337b26..b22b51ad883 100644 --- a/packages/js/navigation/src/index.js +++ b/packages/js/navigation/src/index.js @@ -1,17 +1,11 @@ /** * External dependencies */ -import { - createElement, - useState, - useEffect, - useLayoutEffect, -} from '@wordpress/element'; +import { useState, useEffect, useLayoutEffect } from '@wordpress/element'; import { addQueryArgs } from '@wordpress/url'; import { parse } from 'qs'; import { pick } from 'lodash'; import { applyFilters } from '@wordpress/hooks'; -import { Slot, Fill } from '@wordpress/components'; import { getAdminLink } from '@woocommerce/settings'; /** @@ -343,19 +337,3 @@ export const navigateTo = ( { url } ) => { window.location.href = String( parsedUrl ); }; - -/** - * A Fill for extensions to add client facing custom Navigation Items. - * - * @slotFill WooNavigationItem - * @scope woocommerce-navigation - * @param {Object} props React props. - * @param {Array} props.children Node children. - * @param {string} props.item Navigation item slug. - */ -export const WooNavigationItem = ( { children, item } ) => { - return { children }; -}; -WooNavigationItem.Slot = ( { name } ) => ( - -); diff --git a/packages/js/notices/changelog/50828-dev-constrain-pnpm-version b/packages/js/notices/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/notices/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/notices/package.json b/packages/js/notices/package.json index 0a649b7c7b3..b2c5ac704f8 100644 --- a/packages/js/notices/package.json +++ b/packages/js/notices/package.json @@ -6,7 +6,7 @@ "license": "GPL-2.0-or-later", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "keywords": [ "wordpress", diff --git a/packages/js/number/changelog/50047-fix-typos b/packages/js/number/changelog/50047-fix-typos new file mode 100644 index 00000000000..e5d2f2b3db2 --- /dev/null +++ b/packages/js/number/changelog/50047-fix-typos @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Comment: Fix comment typos across various files. diff --git a/packages/js/number/changelog/50828-dev-constrain-pnpm-version b/packages/js/number/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/number/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/number/changelog/fix-misspelling-in-parse-number-tests b/packages/js/number/changelog/fix-misspelling-in-parse-number-tests new file mode 100644 index 00000000000..25ded53e90c --- /dev/null +++ b/packages/js/number/changelog/fix-misspelling-in-parse-number-tests @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Fix typos in parse number tests diff --git a/packages/js/number/package.json b/packages/js/number/package.json index eebf9a2dede..44a2332d8df 100644 --- a/packages/js/number/package.json +++ b/packages/js/number/package.json @@ -6,7 +6,7 @@ "license": "GPL-3.0-or-later", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "keywords": [ "wordpress", diff --git a/packages/js/number/src/test/index.ts b/packages/js/number/src/test/index.ts index b5d2ac089e3..f2db08195dc 100644 --- a/packages/js/number/src/test/index.ts +++ b/packages/js/number/src/test/index.ts @@ -50,7 +50,7 @@ describe( 'numberFormat', () => { } ); describe( 'parseNumber', () => { - it( 'should remove thousand seperator before parsing number', () => { + it( 'should remove thousand separator before parsing number', () => { const config = { decimalSeparator: ',', thousandSeparator: '.', @@ -59,7 +59,7 @@ describe( 'parseNumber', () => { expect( parseNumber( config, '12.345,679' ) ).toBe( '12345.679' ); } ); - it( 'supports empty string as the thousandSeperator', () => { + it( 'supports empty string as the thousandSeparator', () => { const config = { decimalSeparator: ',', thousandSeparator: '', @@ -68,7 +68,7 @@ describe( 'parseNumber', () => { expect( parseNumber( config, '12345,679' ) ).toBe( '12345.679' ); } ); - it( 'supports empty string as the decimalSeperator', () => { + it( 'supports empty string as the decimalSeparator', () => { const config = { decimalSeparator: '', thousandSeparator: ',', diff --git a/packages/js/onboarding/changelog/50828-dev-constrain-pnpm-version b/packages/js/onboarding/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/onboarding/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/onboarding/package.json b/packages/js/onboarding/package.json index 55825d51629..0521b2f2163 100644 --- a/packages/js/onboarding/package.json +++ b/packages/js/onboarding/package.json @@ -6,7 +6,7 @@ "license": "GPL-3.0-or-later", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "keywords": [ "wordpress", diff --git a/packages/js/product-editor/changelog/add-upgrade-blocks-to-v3 b/packages/js/product-editor/changelog/add-upgrade-blocks-to-v3 new file mode 100644 index 00000000000..def28ee9bcc --- /dev/null +++ b/packages/js/product-editor/changelog/add-upgrade-blocks-to-v3 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update all blocks to use API Version 3. diff --git a/packages/js/product-editor/changelog/dev-50276_rename_and_move_error_handler b/packages/js/product-editor/changelog/dev-50276_rename_and_move_error_handler new file mode 100644 index 00000000000..0951cfcbedc --- /dev/null +++ b/packages/js/product-editor/changelog/dev-50276_rename_and_move_error_handler @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Rename and move errorHandler method #50277 diff --git a/packages/js/product-editor/changelog/fix-49697_settings_button_on_mobile b/packages/js/product-editor/changelog/fix-49697_settings_button_on_mobile new file mode 100644 index 00000000000..d8052fd8af0 --- /dev/null +++ b/packages/js/product-editor/changelog/fix-49697_settings_button_on_mobile @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Tweak header styling in description modal editor to address issues with smaller viewports. diff --git a/packages/js/product-editor/changelog/fix-50058_gb_modal_conflict b/packages/js/product-editor/changelog/fix-50058_gb_modal_conflict new file mode 100644 index 00000000000..7607a7c9737 --- /dev/null +++ b/packages/js/product-editor/changelog/fix-50058_gb_modal_conflict @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Increase specificity of modal styling to avoid Gutenberg styling conflicts. diff --git a/packages/js/product-editor/changelog/fix-misspelling-in-getProductStockStatusClass-test b/packages/js/product-editor/changelog/fix-misspelling-in-getProductStockStatusClass-test new file mode 100644 index 00000000000..c09c6462e96 --- /dev/null +++ b/packages/js/product-editor/changelog/fix-misspelling-in-getProductStockStatusClass-test @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Fix typo in getProductStockStatusClass test diff --git a/packages/js/product-editor/changelog/fix-misspelling-in-noticeDimissed-ref b/packages/js/product-editor/changelog/fix-misspelling-in-noticeDimissed-ref new file mode 100644 index 00000000000..f9fda3b1ea1 --- /dev/null +++ b/packages/js/product-editor/changelog/fix-misspelling-in-noticeDimissed-ref @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Fix typo in noticeDismissed ref diff --git a/packages/js/product-editor/changelog/fix-toggle-misspelling-in-class-names b/packages/js/product-editor/changelog/fix-toggle-misspelling-in-class-names new file mode 100644 index 00000000000..cdef9bd7270 --- /dev/null +++ b/packages/js/product-editor/changelog/fix-toggle-misspelling-in-class-names @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Fix toogle typo in class names diff --git a/packages/js/product-editor/changelog/update-e2e-product-editor-create-simple-product b/packages/js/product-editor/changelog/update-e2e-product-editor-create-simple-product new file mode 100644 index 00000000000..6b01942f83b --- /dev/null +++ b/packages/js/product-editor/changelog/update-e2e-product-editor-create-simple-product @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix association of label and input for SKU field. diff --git a/packages/js/product-editor/changelog/update-stricter-global-unique-id b/packages/js/product-editor/changelog/update-stricter-global-unique-id new file mode 100644 index 00000000000..02752f58d56 --- /dev/null +++ b/packages/js/product-editor/changelog/update-stricter-global-unique-id @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix an issue in the text block where the validation was not being run due to missing dependencies in the parameters diff --git a/packages/js/product-editor/src/blocks/generic/checkbox/block.json b/packages/js/product-editor/src/blocks/generic/checkbox/block.json index 49b4d26080f..8cd8ae0109a 100644 --- a/packages/js/product-editor/src/blocks/generic/checkbox/block.json +++ b/packages/js/product-editor/src/blocks/generic/checkbox/block.json @@ -1,11 +1,15 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-checkbox-field", "title": "Product checkbox control", "category": "woocommerce", "description": "A reusable checkbox for the product editor.", - "keywords": [ "products", "checkbox", "input" ], + "keywords": [ + "products", + "checkbox", + "input" + ], "textdomain": "default", "attributes": { "title": { @@ -38,5 +42,7 @@ "__experimentalToolbar": false }, "editorStyle": "file:./editor.css", - "usesContext": [ "postType" ] -} + "usesContext": [ + "postType" + ] +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/collapsible/block.json b/packages/js/product-editor/src/blocks/generic/collapsible/block.json index 8cf084538cf..08ef5803965 100644 --- a/packages/js/product-editor/src/blocks/generic/collapsible/block.json +++ b/packages/js/product-editor/src/blocks/generic/collapsible/block.json @@ -1,6 +1,6 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-collapsible", "title": "Collapsible", "category": "widgets", @@ -27,4 +27,4 @@ "lock": false, "__experimentalToolbar": false } -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/conditional/block.json b/packages/js/product-editor/src/blocks/generic/conditional/block.json index 5afff2f5865..20b825c2f25 100644 --- a/packages/js/product-editor/src/blocks/generic/conditional/block.json +++ b/packages/js/product-editor/src/blocks/generic/conditional/block.json @@ -1,6 +1,6 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/conditional", "title": "Conditional", "category": "widgets", @@ -25,4 +25,4 @@ "lock": false, "__experimentalToolbar": false } -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/linked-product-list/block.json b/packages/js/product-editor/src/blocks/generic/linked-product-list/block.json index 245876fc294..65aa7146e71 100644 --- a/packages/js/product-editor/src/blocks/generic/linked-product-list/block.json +++ b/packages/js/product-editor/src/blocks/generic/linked-product-list/block.json @@ -1,11 +1,15 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-linked-list-field", "title": "Linked product list", "category": "widgets", "description": "The linked product list.", - "keywords": [ "products", "linked", "list" ], + "keywords": [ + "products", + "linked", + "list" + ], "textdomain": "default", "attributes": { "property": { @@ -27,5 +31,8 @@ "__experimentalToolbar": false }, "editorStyle": "file:./editor.css", - "usesContext": [ "postType", "isInSelectedTab" ] -} + "usesContext": [ + "postType", + "isInSelectedTab" + ] +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/number/block.json b/packages/js/product-editor/src/blocks/generic/number/block.json index c8fdbc69878..dada23bf4fb 100644 --- a/packages/js/product-editor/src/blocks/generic/number/block.json +++ b/packages/js/product-editor/src/blocks/generic/number/block.json @@ -1,11 +1,15 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-number-field", "title": "Product number control", "category": "woocommerce", "description": "A reusable number field for the product editor.", - "keywords": [ "products", "number", "input" ], + "keywords": [ + "products", + "number", + "input" + ], "textdomain": "default", "attributes": { "label": { @@ -52,4 +56,4 @@ "__experimentalToolbar": false }, "editorStyle": "file:./editor.css" -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/pricing/block.json b/packages/js/product-editor/src/blocks/generic/pricing/block.json index 1402b64c62f..b2f1b6eaff8 100644 --- a/packages/js/product-editor/src/blocks/generic/pricing/block.json +++ b/packages/js/product-editor/src/blocks/generic/pricing/block.json @@ -1,11 +1,14 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-pricing-field", "description": "A product price block with currency display.", "title": "Product pricing", "category": "widgets", - "keywords": [ "products", "price" ], + "keywords": [ + "products", + "price" + ], "textdomain": "default", "attributes": { "property": { @@ -32,5 +35,7 @@ "__experimentalToolbar": false }, "editorStyle": "file:./editor.css", - "usesContext": [ "postType" ] -} + "usesContext": [ + "postType" + ] +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/radio/block.json b/packages/js/product-editor/src/blocks/generic/radio/block.json index d480795cf9e..45dc7f7d084 100644 --- a/packages/js/product-editor/src/blocks/generic/radio/block.json +++ b/packages/js/product-editor/src/blocks/generic/radio/block.json @@ -1,11 +1,15 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-radio-field", "title": "Product radio control", "category": "woocommerce", "description": "The product radio.", - "keywords": [ "products", "radio", "input" ], + "keywords": [ + "products", + "radio", + "input" + ], "textdomain": "default", "attributes": { "title": { @@ -39,5 +43,7 @@ "lock": false, "__experimentalToolbar": false }, - "usesContext": [ "postType" ] -} + "usesContext": [ + "postType" + ] +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/section-description/block.json b/packages/js/product-editor/src/blocks/generic/section-description/block.json index a31b7a0e3e6..dbccdf98913 100644 --- a/packages/js/product-editor/src/blocks/generic/section-description/block.json +++ b/packages/js/product-editor/src/blocks/generic/section-description/block.json @@ -1,11 +1,15 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-section-description", "title": "Product section description", "category": "woocommerce", "description": "The product section description.", - "keywords": [ "products", "section", "description" ], + "keywords": [ + "products", + "section", + "description" + ], "textdomain": "default", "attributes": { "content": { @@ -22,4 +26,4 @@ "lock": false, "__experimentalToolbar": false } -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/section/block.json b/packages/js/product-editor/src/blocks/generic/section/block.json index 17a503bbdfd..b2d77ecfb43 100644 --- a/packages/js/product-editor/src/blocks/generic/section/block.json +++ b/packages/js/product-editor/src/blocks/generic/section/block.json @@ -1,11 +1,15 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-section", "title": "Product section", "category": "woocommerce", "description": "The product section.", - "keywords": [ "products", "section", "group" ], + "keywords": [ + "products", + "section", + "group" + ], "textdomain": "default", "attributes": { "title": { @@ -17,7 +21,11 @@ }, "blockGap": { "type": "string", - "enum": [ "unit-20", "unit-30", "unit-40" ], + "enum": [ + "unit-20", + "unit-30", + "unit-40" + ], "default": "unit-20" } }, @@ -31,4 +39,4 @@ "__experimentalToolbar": false }, "editorStyle": "file:./editor.css" -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/select/block.json b/packages/js/product-editor/src/blocks/generic/select/block.json index 620f3300fa2..738ebdcbfbd 100644 --- a/packages/js/product-editor/src/blocks/generic/select/block.json +++ b/packages/js/product-editor/src/blocks/generic/select/block.json @@ -1,11 +1,14 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-select-field", "title": "Product select field", "category": "woocommerce", "description": "A select field for use in the product editor.", - "keywords": [ "products", "select" ], + "keywords": [ + "products", + "select" + ], "textdomain": "default", "attributes": { "label": { @@ -60,5 +63,7 @@ "lock": false, "__experimentalToolbar": false }, - "usesContext": [ "postType" ] -} + "usesContext": [ + "postType" + ] +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/subsection-description/block.json b/packages/js/product-editor/src/blocks/generic/subsection-description/block.json index 58bb4dbc4ab..12c0712c53c 100644 --- a/packages/js/product-editor/src/blocks/generic/subsection-description/block.json +++ b/packages/js/product-editor/src/blocks/generic/subsection-description/block.json @@ -1,11 +1,15 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-subsection-description", "title": "Product subsection description", "category": "woocommerce", "description": "The product subsection description.", - "keywords": [ "products", "subsection", "description" ], + "keywords": [ + "products", + "subsection", + "description" + ], "textdomain": "default", "attributes": { "content": { @@ -22,4 +26,4 @@ "lock": false, "__experimentalToolbar": false } -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/subsection/block.json b/packages/js/product-editor/src/blocks/generic/subsection/block.json index 6cbac142291..4dc20a21fd2 100644 --- a/packages/js/product-editor/src/blocks/generic/subsection/block.json +++ b/packages/js/product-editor/src/blocks/generic/subsection/block.json @@ -1,11 +1,15 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-subsection", "title": "Product subsection", "category": "woocommerce", "description": "The product subsection.", - "keywords": [ "products", "subsection", "group" ], + "keywords": [ + "products", + "subsection", + "group" + ], "textdomain": "default", "attributes": { "title": { @@ -17,7 +21,11 @@ }, "blockGap": { "type": "string", - "enum": [ "unit-20", "unit-30", "unit-40" ], + "enum": [ + "unit-20", + "unit-30", + "unit-40" + ], "default": "unit-20" } }, @@ -31,4 +39,4 @@ "__experimentalToolbar": false }, "editorStyle": "file:./editor.css" -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/tab/block.json b/packages/js/product-editor/src/blocks/generic/tab/block.json index 537e245ae57..20c1e649166 100644 --- a/packages/js/product-editor/src/blocks/generic/tab/block.json +++ b/packages/js/product-editor/src/blocks/generic/tab/block.json @@ -1,11 +1,15 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-tab", "title": "Product tab", "category": "woocommerce", "description": "The product tab.", - "keywords": [ "products", "tab", "group" ], + "keywords": [ + "products", + "tab", + "group" + ], "textdomain": "default", "attributes": { "id": { @@ -27,6 +31,8 @@ "providesContext": { "isInSelectedTab": "isSelected" }, - "usesContext": [ "selectedTab" ], + "usesContext": [ + "selectedTab" + ], "editorStyle": "file:./editor.css" -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/taxonomy/block.json b/packages/js/product-editor/src/blocks/generic/taxonomy/block.json index 69b5423852b..eeab0afc2c5 100644 --- a/packages/js/product-editor/src/blocks/generic/taxonomy/block.json +++ b/packages/js/product-editor/src/blocks/generic/taxonomy/block.json @@ -1,11 +1,13 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-taxonomy-field", "title": "Taxonomy", "category": "widgets", "description": "A block that displays a taxonomy field, allowing searching, selection, and creation of new items", - "keywords": [ "taxonomy" ], + "keywords": [ + "taxonomy" + ], "textdomain": "default", "attributes": { "slug": { @@ -51,5 +53,8 @@ "__experimentalToolbar": false }, "editorStyle": "file:./editor.css", - "usesContext": [ "postType", "isInSelectedTab" ] -} + "usesContext": [ + "postType", + "isInSelectedTab" + ] +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/text-area/block.json b/packages/js/product-editor/src/blocks/generic/text-area/block.json index dd750f115d3..a9b16ba55f7 100644 --- a/packages/js/product-editor/src/blocks/generic/text-area/block.json +++ b/packages/js/product-editor/src/blocks/generic/text-area/block.json @@ -1,11 +1,14 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-text-area-field", "title": "Product textarea block", "category": "woocommerce", "description": "A text-area field for use in the product editor.", - "keywords": [ "textarea", "rich-text" ], + "keywords": [ + "textarea", + "rich-text" + ], "textdomain": "default", "attributes": { "property": { @@ -32,11 +35,19 @@ }, "align": { "type": "string", - "enum": [ "left", "center", "right", "justify" ] + "enum": [ + "left", + "center", + "right", + "justify" + ] }, "mode": { "type": "string", - "enum": [ "plain-text", "rich-text" ], + "enum": [ + "plain-text", + "rich-text" + ], "default": "rich-text" }, "allowedFormats": { @@ -56,7 +67,10 @@ }, "direction": { "type": "string", - "enum": [ "ltr", "rtl" ] + "enum": [ + "ltr", + "rtl" + ] } }, "supports": { @@ -68,4 +82,4 @@ "lock": false, "__experimentalToolbar": true } -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/text/block.json b/packages/js/product-editor/src/blocks/generic/text/block.json index 4a2f037fe73..88314b4be5e 100644 --- a/packages/js/product-editor/src/blocks/generic/text/block.json +++ b/packages/js/product-editor/src/blocks/generic/text/block.json @@ -1,11 +1,14 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-text-field", "title": "Product text field", "category": "woocommerce", "description": "A text field for use in the product editor.", - "keywords": [ "products", "text" ], + "keywords": [ + "products", + "text" + ], "textdomain": "default", "attributes": { "label": { @@ -59,5 +62,7 @@ "__experimentalToolbar": false }, "editorStyle": "file:./editor.css", - "usesContext": [ "postType" ] -} + "usesContext": [ + "postType" + ] +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/generic/text/edit.tsx b/packages/js/product-editor/src/blocks/generic/text/edit.tsx index 3944eab1f93..3a8fb948376 100644 --- a/packages/js/product-editor/src/blocks/generic/text/edit.tsx +++ b/packages/js/product-editor/src/blocks/generic/text/edit.tsx @@ -137,7 +137,7 @@ export function Edit( { }; } }, - [ type, required, pattern, minLength, maxLength, min, max ] + [ type, required, pattern, minLength, maxLength, min, max, value ] ); function getSuffix() { diff --git a/packages/js/product-editor/src/blocks/generic/toggle/block.json b/packages/js/product-editor/src/blocks/generic/toggle/block.json index 5b41cb951c8..c26a3261401 100644 --- a/packages/js/product-editor/src/blocks/generic/toggle/block.json +++ b/packages/js/product-editor/src/blocks/generic/toggle/block.json @@ -1,11 +1,15 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-toggle-field", "title": "Product toggle control", "category": "woocommerce", "description": "The product toggle.", - "keywords": [ "products", "radio", "input" ], + "keywords": [ + "products", + "radio", + "input" + ], "textdomain": "default", "attributes": { "label": { @@ -48,5 +52,7 @@ "lock": false, "__experimentalToolbar": false }, - "usesContext": [ "postType" ] -} + "usesContext": [ + "postType" + ] +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/product-fields/attributes/block.json b/packages/js/product-editor/src/blocks/product-fields/attributes/block.json index aed5dd2c121..7d69925cfdc 100644 --- a/packages/js/product-editor/src/blocks/product-fields/attributes/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/attributes/block.json @@ -1,11 +1,14 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-attributes-field", "title": "Product attributes", "category": "widgets", "description": "The product attributes.", - "keywords": [ "products", "attributes" ], + "keywords": [ + "products", + "attributes" + ], "textdomain": "default", "attributes": { "name": { @@ -22,6 +25,8 @@ "lock": false, "__experimentalToolbar": false }, - "usesContext": [ "isInSelectedTab" ], + "usesContext": [ + "isInSelectedTab" + ], "editorStyle": "file:./editor.css" -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/product-fields/catalog-visibility/block.json b/packages/js/product-editor/src/blocks/product-fields/catalog-visibility/block.json index b70967f8fa0..18b37da13e3 100644 --- a/packages/js/product-editor/src/blocks/product-fields/catalog-visibility/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/catalog-visibility/block.json @@ -1,11 +1,14 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-catalog-visibility-field", "description": "A checkbox to manage the catalog visibility of the product.", "title": "Product catalog visibility", "category": "widgets", - "keywords": [ "products", "catalog" ], + "keywords": [ + "products", + "catalog" + ], "textdomain": "default", "attributes": { "label": { @@ -14,7 +17,12 @@ }, "visibility": { "type": "string", - "enum": [ "visible", "catalog", "search", "hidden" ], + "enum": [ + "visible", + "catalog", + "search", + "hidden" + ], "default": "visible" } }, @@ -27,4 +35,4 @@ "lock": false, "__experimentalToolbar": false } -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/product-fields/custom-fields-toggle/block.json b/packages/js/product-editor/src/blocks/product-fields/custom-fields-toggle/block.json index ae2da4df2df..08aca1dcf79 100644 --- a/packages/js/product-editor/src/blocks/product-fields/custom-fields-toggle/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/custom-fields-toggle/block.json @@ -1,11 +1,15 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-custom-fields-toggle-field", "title": "Product custom fields toggle control", "category": "woocommerce", "description": "The product custom fields toggle.", - "keywords": [ "products", "custom", "fields" ], + "keywords": [ + "products", + "custom", + "fields" + ], "textdomain": "default", "attributes": { "label": { @@ -23,4 +27,4 @@ "__experimentalToolbar": false }, "editorStyle": "file:./editor.css" -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/product-fields/custom-fields/block.json b/packages/js/product-editor/src/blocks/product-fields/custom-fields/block.json index 73824721fe2..250d1b3a2db 100644 --- a/packages/js/product-editor/src/blocks/product-fields/custom-fields/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/custom-fields/block.json @@ -1,11 +1,15 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-custom-fields", "title": "Product custom fields control", "category": "woocommerce", "description": "The product custom fields.", - "keywords": [ "products", "custom", "fields" ], + "keywords": [ + "products", + "custom", + "fields" + ], "textdomain": "default", "attributes": { "name": { @@ -22,4 +26,4 @@ "lock": false, "__experimentalToolbar": false } -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/product-fields/description/block.json b/packages/js/product-editor/src/blocks/product-fields/description/block.json index 3306eedfe1a..d8cbbbc98a7 100644 --- a/packages/js/product-editor/src/blocks/product-fields/description/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/description/block.json @@ -1,11 +1,14 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-description-field", "title": "Product description", "category": "woocommerce", "description": "The product description.", - "keywords": [ "products", "description" ], + "keywords": [ + "products", + "description" + ], "textdomain": "default", "attributes": { "__contentEditable": { @@ -22,4 +25,4 @@ "lock": false, "__experimentalToolbar": true } -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/block.json b/packages/js/product-editor/src/blocks/product-fields/downloads/block.json index 4f081aa6a28..a80b3ad6439 100644 --- a/packages/js/product-editor/src/blocks/product-fields/downloads/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/block.json @@ -1,11 +1,14 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-downloads-field", "title": "Product downloads", "category": "widgets", "description": "The product downloads.", - "keywords": [ "products", "downloads" ], + "keywords": [ + "products", + "downloads" + ], "textdomain": "default", "attributes": { "name": { @@ -23,5 +26,7 @@ "__experimentalToolbar": false }, "editorStyle": "file:./editor.css", - "usesContext": [ "postType" ] -} + "usesContext": [ + "postType" + ] +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/downloads-menu.tsx b/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/downloads-menu.tsx index 3bf0ff3f2d5..e90a3921cf7 100644 --- a/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/downloads-menu.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/downloads-menu.tsx @@ -33,7 +33,7 @@ export function DownloadsMenu( { icon={ isOpen ? chevronUp : chevronDown } variant="secondary" onClick={ onToggle } - className="woocommerce-downloads-menu__toogle" + className="woocommerce-downloads-menu__toggle" > { __( 'Add new', 'woocommerce' ) } diff --git a/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/style.scss b/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/style.scss index fe56bd2c248..0cec19b4642 100644 --- a/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/style.scss +++ b/packages/js/product-editor/src/blocks/product-fields/downloads/downloads-menu/style.scss @@ -1,5 +1,5 @@ .woocommerce-downloads-menu { - &__toogle { + &__toggle { flex-direction: row-reverse; > span { diff --git a/packages/js/product-editor/src/blocks/product-fields/images/block.json b/packages/js/product-editor/src/blocks/product-fields/images/block.json index 12fe9006930..40885aeb5fc 100644 --- a/packages/js/product-editor/src/blocks/product-fields/images/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/images/block.json @@ -1,11 +1,16 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-images-field", "title": "Product images", "category": "widgets", "description": "The product images.", - "keywords": [ "products", "image", "images", "gallery" ], + "keywords": [ + "products", + "image", + "images", + "gallery" + ], "textdomain": "default", "attributes": { "mediaId": { @@ -38,5 +43,7 @@ "__experimentalToolbar": false }, "editorStyle": "file:./editor.css", - "usesContext": [ "postType" ] -} + "usesContext": [ + "postType" + ] +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/product-fields/inventory-email/block.json b/packages/js/product-editor/src/blocks/product-fields/inventory-email/block.json index 5ac6cf80d4c..2b54ab1cb5f 100644 --- a/packages/js/product-editor/src/blocks/product-fields/inventory-email/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/inventory-email/block.json @@ -1,11 +1,16 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-inventory-email-field", "title": "Stock level threshold", "category": "widgets", "description": "Stock management minimum quantity.", - "keywords": [ "products", "inventory", "email", "minimum" ], + "keywords": [ + "products", + "inventory", + "email", + "minimum" + ], "textdomain": "default", "attributes": { "name": { @@ -23,4 +28,4 @@ "__experimentalToolbar": false }, "editorStyle": "file:./editor.css" -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/product-fields/inventory-quantity/block.json b/packages/js/product-editor/src/blocks/product-fields/inventory-quantity/block.json index fe649ca6a38..67b23f5d447 100644 --- a/packages/js/product-editor/src/blocks/product-fields/inventory-quantity/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/inventory-quantity/block.json @@ -1,11 +1,15 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-inventory-quantity-field", "title": "Product inventory quantity available", "category": "woocommerce", "description": "The product available quantity.", - "keywords": [ "products", "quantity", "inventory" ], + "keywords": [ + "products", + "quantity", + "inventory" + ], "textdomain": "default", "attributes": { "name": { @@ -22,5 +26,7 @@ "lock": false, "__experimentalToolbar": false }, - "usesContext": [ "postType" ] -} + "usesContext": [ + "postType" + ] +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/product-fields/inventory-sku/block.json b/packages/js/product-editor/src/blocks/product-fields/inventory-sku/block.json index 5608abb15a1..39940b25b6c 100644 --- a/packages/js/product-editor/src/blocks/product-fields/inventory-sku/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/inventory-sku/block.json @@ -1,11 +1,14 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-sku-field", "title": "Product text control", "category": "woocommerce", "description": "The product sku.", - "keywords": [ "products", "sku" ], + "keywords": [ + "products", + "sku" + ], "textdomain": "default", "attributes": { "name": { @@ -27,5 +30,7 @@ "__experimentalToolbar": false }, "editorStyle": "file:./editor.css", - "usesContext": [ "postType" ] -} + "usesContext": [ + "postType" + ] +} \ No newline at end of file diff --git a/packages/js/product-editor/src/blocks/product-fields/inventory-sku/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/inventory-sku/edit.tsx index a0c18d28675..845d1a7658e 100644 --- a/packages/js/product-editor/src/blocks/product-fields/inventory-sku/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/inventory-sku/edit.tsx @@ -3,6 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { BlockAttributes } from '@wordpress/blocks'; +import { useInstanceId } from '@wordpress/compose'; import { createElement, createInterpolateElement } from '@wordpress/element'; import { useWooBlockProps } from '@woocommerce/block-templates'; import { Product } from '@woocommerce/data'; @@ -46,10 +47,15 @@ export function Edit( { [ sku ] ); + const inputControlId = useInstanceId( + BaseControl, + 'product_sku' + ) as string; + return (
', 'woocommerce' ), @@ -64,6 +70,7 @@ export function Edit( { > ) { - const noticeDimissed = useRef( false ); + const noticeDismissed = useRef( false ); const { invalidateResolution } = useDispatch( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME ); @@ -107,7 +107,7 @@ export function Edit( { */ if ( totalCountWithoutPrice > 0 && - ! noticeDimissed.current && + ! noticeDismissed.current && productStatus !== 'publish' && // New status. newData?.status === 'publish' @@ -198,7 +198,7 @@ export function Edit( { ref={ variationTableRef as React.Ref< HTMLDivElement > } noticeText={ noticeText } onNoticeDismiss={ () => { - noticeDimissed.current = true; + noticeDismissed.current = true; updateUserPreferences( { variable_items_without_price_notice_dismissed: { ...( itemsWithoutPriceNoticeDismissed || {} ), diff --git a/packages/js/product-editor/src/blocks/product-fields/variation-options/block.json b/packages/js/product-editor/src/blocks/product-fields/variation-options/block.json index 4b2f7c70d33..e16d5ebb37f 100644 --- a/packages/js/product-editor/src/blocks/product-fields/variation-options/block.json +++ b/packages/js/product-editor/src/blocks/product-fields/variation-options/block.json @@ -1,11 +1,14 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, + "apiVersion": 3, "name": "woocommerce/product-variations-options-field", "title": "Product variations options", "category": "woocommerce", "description": "The product variations options.", - "keywords": [ "products", "variations" ], + "keywords": [ + "products", + "variations" + ], "textdomain": "default", "attributes": { "description": { @@ -22,6 +25,9 @@ "lock": false, "__experimentalToolbar": false }, - "usesContext": [ "postType", "isInSelectedTab" ], + "usesContext": [ + "postType", + "isInSelectedTab" + ], "editorStyle": "file:./editor.css" -} +} \ No newline at end of file diff --git a/packages/js/product-editor/src/components/add-products-modal/style.scss b/packages/js/product-editor/src/components/add-products-modal/style.scss index d4f484044a9..048ffbe09d7 100644 --- a/packages/js/product-editor/src/components/add-products-modal/style.scss +++ b/packages/js/product-editor/src/components/add-products-modal/style.scss @@ -1,5 +1,5 @@ -.woocommerce-add-products-modal, -.woocommerce-reorder-products-modal { +.components-modal__frame.woocommerce-add-products-modal, +.components-modal__frame.woocommerce-reorder-products-modal { @include breakpoint(">600px") { width: calc(100% - 32px); } @@ -11,7 +11,9 @@ width: 50%; } } - +} +.woocommerce-add-products-modal, +.woocommerce-reorder-products-modal { &__input-suffix { margin-right: $grid-unit-15; } diff --git a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.scss b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.scss index 94f6f0dfcc1..b4b1795aa27 100644 --- a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.scss +++ b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.scss @@ -1,5 +1,7 @@ -.woocommerce-new-attribute-modal { +.components-modal__frame.woocommerce-new-attribute-modal { min-width: 75%; +} +.woocommerce-new-attribute-modal { .components-notice.is-info { margin-left: 0; margin-right: 0; diff --git a/packages/js/product-editor/src/components/custom-fields/create-modal/style.scss b/packages/js/product-editor/src/components/custom-fields/create-modal/style.scss index 304f6495b43..b52bba2f43b 100644 --- a/packages/js/product-editor/src/components/custom-fields/create-modal/style.scss +++ b/packages/js/product-editor/src/components/custom-fields/create-modal/style.scss @@ -1,6 +1,8 @@ -.woocommerce-product-custom-fields__create-modal { +.components-modal__frame.woocommerce-product-custom-fields__create-modal { min-width: 75%; +} +.woocommerce-product-custom-fields__create-modal { [role="table"] { width: 100%; diff --git a/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx b/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx index e3302b6be98..33af470f52f 100644 --- a/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx +++ b/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx @@ -16,7 +16,7 @@ import { useValidations } from '../../../../contexts/validation-context'; import { WPError } from '../../../../hooks/use-error-handler'; import { useProductURL } from '../../../../hooks/use-product-url'; import { PreviewButtonProps } from '../../preview-button'; -import { errorHandler } from '../../../../hooks/use-product-manager'; +import { formatProductError } from '../../../../utils/format-product-error'; export function usePreview( { productStatus, @@ -129,7 +129,10 @@ export function usePreview( { } catch ( error ) { if ( onSaveError ) { onSaveError( - errorHandler( error as WPError, productStatus ) as WPError + formatProductError( + error as WPError, + productStatus + ) as WPError ); } } diff --git a/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx b/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx index d1e2a64bc0b..7379ced3471 100644 --- a/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx +++ b/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx @@ -18,7 +18,7 @@ import { useValidations } from '../../../../contexts/validation-context'; import { WPError } from '../../../../hooks/use-error-handler'; import { SaveDraftButtonProps } from '../../save-draft-button'; import { recordProductEvent } from '../../../../utils/record-product-event'; -import { errorHandler } from '../../../../hooks/use-product-manager'; +import { formatProductError } from '../../../../utils/format-product-error'; export function useSaveDraft( { productStatus, @@ -108,7 +108,10 @@ export function useSaveDraft( { } catch ( error ) { if ( onSaveError ) { onSaveError( - errorHandler( error as WPError, productStatus ) as WPError + formatProductError( + error as WPError, + productStatus + ) as WPError ); } } diff --git a/packages/js/product-editor/src/components/iframe-editor/header-toolbar/header-toolbar.scss b/packages/js/product-editor/src/components/iframe-editor/header-toolbar/header-toolbar.scss index deda3512663..35780aca303 100644 --- a/packages/js/product-editor/src/components/iframe-editor/header-toolbar/header-toolbar.scss +++ b/packages/js/product-editor/src/components/iframe-editor/header-toolbar/header-toolbar.scss @@ -31,8 +31,8 @@ align-items: center; display: flex; - .woocommerce-iframe-editor-document-tools { - &__left { + .components-accessible-toolbar { + .woocommerce-iframe-editor-document-tools__left { align-items: center; display: inline-flex; gap: 8px; diff --git a/packages/js/product-editor/src/components/iframe-editor/iframe-editor.scss b/packages/js/product-editor/src/components/iframe-editor/iframe-editor.scss index 2c92e08edb6..7208bc412d2 100644 --- a/packages/js/product-editor/src/components/iframe-editor/iframe-editor.scss +++ b/packages/js/product-editor/src/components/iframe-editor/iframe-editor.scss @@ -75,4 +75,10 @@ display: none; } } + + .woocommerce-iframe-editor__header-right + .interface-pinned-items + .components-button { + display: flex; + } } diff --git a/packages/js/product-editor/src/components/iframe-editor/iframe-editor.tsx b/packages/js/product-editor/src/components/iframe-editor/iframe-editor.tsx index 1edeb2cd7c0..a9d2649e1ef 100644 --- a/packages/js/product-editor/src/components/iframe-editor/iframe-editor.tsx +++ b/packages/js/product-editor/src/components/iframe-editor/iframe-editor.tsx @@ -30,7 +30,6 @@ import { // eslint-disable-next-line @woocommerce/dependency-group import { ComplementaryArea, - store as interfaceStore, // @ts-expect-error No types for this exist yet. } from '@wordpress/interface'; @@ -118,18 +117,12 @@ export function IframeEditor( { return select( blockEditorStore ).getSettings(); }, [] ); - const { hasFixedToolbar, isRightSidebarOpen } = useSelect( ( select ) => { + const { hasFixedToolbar } = useSelect( ( select ) => { // @ts-expect-error These selectors are available in the block data store. const { get: getPreference } = select( preferencesStore ); - // @ts-expect-error These selectors are available in the interface data store. - const { getActiveComplementaryArea } = select( interfaceStore ); - return { hasFixedToolbar: getPreference( 'core', 'fixedToolbar' ), - isRightSidebarOpen: getActiveComplementaryArea( - SIDEBAR_COMPLEMENTARY_AREA_SCOPE - ), }; }, [] ); @@ -264,11 +257,9 @@ export function IframeEditor( { bounds. */ }
- { isRightSidebarOpen && ( - - ) } +
{ /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ } diff --git a/packages/js/product-editor/src/components/modal-editor/style.scss b/packages/js/product-editor/src/components/modal-editor/style.scss index 74087df3f28..ab235d72f7a 100644 --- a/packages/js/product-editor/src/components/modal-editor/style.scss +++ b/packages/js/product-editor/src/components/modal-editor/style.scss @@ -1,6 +1,6 @@ $modal-editor-height: 60px; -.woocommerce-modal-editor { +.components-modal__frame.woocommerce-modal-editor { width: 100%; height: 100%; max-height: calc( 100% - 40px ); diff --git a/packages/js/product-editor/src/components/tags-field/create-tag-modal.scss b/packages/js/product-editor/src/components/tags-field/create-tag-modal.scss index ffc4b9e95d9..102dfd93da9 100644 --- a/packages/js/product-editor/src/components/tags-field/create-tag-modal.scss +++ b/packages/js/product-editor/src/components/tags-field/create-tag-modal.scss @@ -1,6 +1,8 @@ .woocommerce-create-new-tag-modal { - min-width: 650px; - overflow: visible; + &__wrapper { + min-width: 590px; + overflow: visible; + } &__buttons { margin-top: $gap-larger; diff --git a/packages/js/product-editor/src/components/variations-table/styles.scss b/packages/js/product-editor/src/components/variations-table/styles.scss index 9e78670f16a..a58f35e4c3f 100644 --- a/packages/js/product-editor/src/components/variations-table/styles.scss +++ b/packages/js/product-editor/src/components/variations-table/styles.scss @@ -162,7 +162,7 @@ gap: $gap-smaller; margin-right: $gap-smallest; - .variations-actions-menu__toogle:disabled { + .variations-actions-menu__toggle:disabled { cursor: not-allowed; } diff --git a/packages/js/product-editor/src/components/variations-table/variation-actions-menus/multiple-update-menu.tsx b/packages/js/product-editor/src/components/variations-table/variation-actions-menus/multiple-update-menu.tsx index 8c5f6e74fa7..12c3eb0b0a4 100644 --- a/packages/js/product-editor/src/components/variations-table/variation-actions-menus/multiple-update-menu.tsx +++ b/packages/js/product-editor/src/components/variations-table/variation-actions-menus/multiple-update-menu.tsx @@ -35,7 +35,7 @@ export function MultipleUpdateMenu( { icon={ isOpen ? chevronUp : chevronDown } variant="secondary" onClick={ onToggle } - className="variations-actions-menu__toogle" + className="variations-actions-menu__toggle" > { __( 'Quick update', 'woocommerce' ) } diff --git a/packages/js/product-editor/src/components/variations-table/variation-actions-menus/styles.scss b/packages/js/product-editor/src/components/variations-table/variation-actions-menus/styles.scss index 99143614edc..7956e681575 100644 --- a/packages/js/product-editor/src/components/variations-table/variation-actions-menus/styles.scss +++ b/packages/js/product-editor/src/components/variations-table/variation-actions-menus/styles.scss @@ -1,5 +1,5 @@ .variations-actions-menu { - &__toogle { + &__toggle { flex-direction: row-reverse; > span { diff --git a/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts b/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts index 584bdc3fae7..cb9e6afd57e 100644 --- a/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts +++ b/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts @@ -12,37 +12,7 @@ import { Product, ProductStatus, PRODUCTS_STORE_NAME } from '@woocommerce/data'; import { useValidations } from '../../contexts/validation-context'; import type { WPError } from '../../hooks/use-error-handler'; import { AUTO_DRAFT_NAME } from '../../utils/constants'; - -export function errorHandler( error: WPError, productStatus: ProductStatus ) { - if ( error.code ) { - return error; - } - - const errorObj = Object.values( error ).find( - ( value ) => value !== undefined - ) as WPError | undefined; - - if ( 'variations' in error && error.variations ) { - return { - ...errorObj, - code: 'variable_product_no_variation_prices', - }; - } - - if ( errorObj !== undefined ) { - return { - ...errorObj, - code: 'product_form_field_error', - }; - } - - return { - code: - productStatus === 'publish' || productStatus === 'future' - ? 'product_publish_error' - : 'product_create_error', - }; -} +import { formatProductError } from '../../utils/format-product-error'; export function useProductManager< T = Product >( postType: string ) { const [ id ] = useEntityProp< number >( 'postType', postType, 'id' ); @@ -107,7 +77,7 @@ export function useProductManager< T = Product >( postType: string ) { return savedProduct as T; } catch ( error ) { - throw errorHandler( error as WPError, status ); + throw formatProductError( error as WPError, status ); } finally { setIsSaving( false ); } @@ -128,7 +98,7 @@ export function useProductManager< T = Product >( postType: string ) { return duplicatedProduct as T; } catch ( error ) { - throw errorHandler( error as WPError, status ); + throw formatProductError( error as WPError, status ); } finally { setIsSaving( false ); } @@ -174,7 +144,7 @@ export function useProductManager< T = Product >( postType: string ) { return deletedProduct as T; } catch ( error ) { - throw errorHandler( error as WPError, status ); + throw formatProductError( error as WPError, status ); } finally { setTrashing( false ); } diff --git a/packages/js/product-editor/src/utils/format-product-error.ts b/packages/js/product-editor/src/utils/format-product-error.ts new file mode 100644 index 00000000000..6ac039c838b --- /dev/null +++ b/packages/js/product-editor/src/utils/format-product-error.ts @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import { ProductStatus } from '@woocommerce/data'; + +/** + * Internal dependencies + */ +import type { WPError } from '../hooks/use-error-handler'; + +export function formatProductError( + error: WPError, + productStatus: ProductStatus +) { + if ( error.code ) { + return error; + } + + const errorObj = Object.values( error ).find( + ( value ) => value !== undefined + ) as WPError | undefined; + + if ( 'variations' in error && error.variations ) { + return { + ...errorObj, + code: 'variable_product_no_variation_prices', + }; + } + + if ( errorObj !== undefined ) { + return { + ...errorObj, + code: 'product_form_field_error', + }; + } + + return { + code: + productStatus === 'publish' || productStatus === 'future' + ? 'product_publish_error' + : 'product_create_error', + }; +} diff --git a/packages/js/product-editor/src/utils/test/get-product-stock-status.test.ts b/packages/js/product-editor/src/utils/test/get-product-stock-status.test.ts index 78d581504a0..03e6373f21f 100644 --- a/packages/js/product-editor/src/utils/test/get-product-stock-status.test.ts +++ b/packages/js/product-editor/src/utils/test/get-product-stock-status.test.ts @@ -82,7 +82,7 @@ describe( 'getProductStockStatus', () => { } ); describe( 'getProductStockStatusClass', () => { - it( 'should return an emtpy string when the stock is not being managed and there is no stock status', () => { + it( 'should return an empty string when the stock is not being managed and there is no stock status', () => { const status = getProductStockStatusClass( products[ 0 ] ); expect( status ).toBe( '' ); } ); diff --git a/packages/js/remote-logging/README.md b/packages/js/remote-logging/README.md index 8ae44259b4c..46a3b9aa45f 100644 --- a/packages/js/remote-logging/README.md +++ b/packages/js/remote-logging/README.md @@ -75,7 +75,7 @@ addFilter( ### API Reference - `init(config: RemoteLoggerConfig): void`: Initializes the remote logger with the given configuration. -- `log(severity: LogSeverity, message: string, extraData?: object): Promise`: Logs a message with the specified severity and optional extra data. +- `log(severity: LogSeverity, message: string, extraData?: object): Promise`: Logs a message with the specified severity and optional extra data. - `captureException(error: Error, extraData?: object): void`: Captures an error and sends it to the remote API. For more detailed information about types and interfaces, refer to the source code and inline documentation. diff --git a/packages/js/remote-logging/changelog/50828-dev-constrain-pnpm-version b/packages/js/remote-logging/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/remote-logging/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/remote-logging/changelog/add-remote-logging-beta-tester-tool b/packages/js/remote-logging/changelog/add-remote-logging-beta-tester-tool new file mode 100644 index 00000000000..1e7f8794894 --- /dev/null +++ b/packages/js/remote-logging/changelog/add-remote-logging-beta-tester-tool @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Tweak logic for adding remote logging tool in beta tester diff --git a/packages/js/remote-logging/changelog/fix-remote-logging-asset-path b/packages/js/remote-logging/changelog/fix-remote-logging-asset-path new file mode 100644 index 00000000000..fc166a1888b --- /dev/null +++ b/packages/js/remote-logging/changelog/fix-remote-logging-asset-path @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix wc asset url check diff --git a/packages/js/remote-logging/changelog/update-remote-logging-data b/packages/js/remote-logging/changelog/update-remote-logging-data new file mode 100644 index 00000000000..69c8edf57c4 --- /dev/null +++ b/packages/js/remote-logging/changelog/update-remote-logging-data @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Add request_uri prop to remote logging data diff --git a/packages/js/remote-logging/package.json b/packages/js/remote-logging/package.json index 8bdc1cb7ace..c96e6ca3f18 100644 --- a/packages/js/remote-logging/package.json +++ b/packages/js/remote-logging/package.json @@ -6,7 +6,7 @@ "license": "GPL-2.0-or-later", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "keywords": [ "wordpress", diff --git a/packages/js/remote-logging/src/remote-logger.ts b/packages/js/remote-logging/src/remote-logger.ts index e88ccc6c76f..e4e71d72bac 100644 --- a/packages/js/remote-logging/src/remote-logger.ts +++ b/packages/js/remote-logging/src/remote-logger.ts @@ -69,10 +69,10 @@ export class RemoteLogger { severity: Exclude< LogData[ 'severity' ], undefined >, message: string, extraData?: Partial< Exclude< LogData, 'message' | 'severity' > > - ) { + ): Promise< boolean > { if ( ! message ) { debug( 'Empty message' ); - return; + return false; } const logData: LogData = mergeLogData( DEFAULT_LOG_DATA, { @@ -82,7 +82,7 @@ export class RemoteLogger { } ); debug( 'Logging:', logData ); - await this.sendLog( logData ); + return await this.sendLog( logData ); } /** @@ -103,6 +103,11 @@ export class RemoteLogger { message: error.message, severity: 'error', ...extraData, + properties: { + ...extraData?.properties, + request_uri: + window.location.pathname + window.location.search, + }, } ), trace: this.getFormattedStackFrame( TraceKit.computeStackTrace( error ) @@ -144,10 +149,10 @@ export class RemoteLogger { * * @param logData - The log data to be sent. */ - private async sendLog( logData: LogData ): Promise< void > { + private async sendLog( logData: LogData ): Promise< boolean > { if ( isDevelopmentEnvironment ) { debug( 'Skipping send log in development environment' ); - return; + return false; } const body = new window.FormData(); @@ -166,13 +171,19 @@ export class RemoteLogger { 'https://public-api.wordpress.com/rest/v1.1/logstash' ) as string; - await window.fetch( endpoint, { + const response = await window.fetch( endpoint, { method: 'POST', body, } ); + if ( ! response.ok ) { + throw new Error( `response body: ${ response.body }` ); + } + + return true; } catch ( error ) { // eslint-disable-next-line no-console console.error( 'Failed to send log to API:', error ); + return false; } } @@ -200,6 +211,10 @@ export class RemoteLogger { message: error.message, severity: 'critical', tags: [ 'js-unhandled-error' ], + properties: { + request_uri: + window.location.pathname + window.location.search, + }, } ), trace: this.getFormattedStackFrame( trace ), }; @@ -328,7 +343,7 @@ export class RemoteLogger { ) { const containsWooCommerceFrame = stackFrames.some( ( frame ) => - frame.url && frame.url.includes( '/woocommerce/assets/' ) + frame.url && frame.url.startsWith( getSetting( 'wcAssetUrl' ) ) ); /** @@ -368,13 +383,13 @@ let logger: RemoteLogger | null = null; * * @return {boolean} - Returns true if remote logging is enabled and the logger is initialized, otherwise false. */ -function canLog(): boolean { - if ( ! getSetting( 'isRemoteLoggingEnabled', false ) ) { +function canLog( _logger: RemoteLogger | null ): _logger is RemoteLogger { + if ( ! window.wcSettings?.isRemoteLoggingEnabled ) { debug( 'Remote logging is disabled.' ); return false; } - if ( ! logger ) { + if ( ! _logger ) { warnLog( 'RemoteLogger is not initialized. Call init() first.' ); return false; } @@ -390,7 +405,7 @@ function canLog(): boolean { * */ export function init( config: RemoteLoggerConfig ) { - if ( ! getSetting( 'isRemoteLoggingEnabled', false ) ) { + if ( ! window.wcSettings?.isRemoteLoggingEnabled ) { debug( 'Remote logging is disabled.' ); return; } @@ -424,15 +439,16 @@ export async function log( severity: Exclude< LogData[ 'severity' ], undefined >, message: string, extraData?: Partial< Exclude< LogData, 'message' | 'severity' > > -): Promise< void > { - if ( ! canLog() ) { - return; +): Promise< boolean > { + if ( ! canLog( logger ) ) { + return false; } try { - await logger?.log( severity, message, extraData ); + return await logger.log( severity, message, extraData ); } catch ( error ) { errorLog( 'Failed to send log:', error ); + return false; } } @@ -446,12 +462,12 @@ export async function captureException( error: Error, extraData?: Partial< LogData > ) { - if ( ! canLog() ) { - return; + if ( ! canLog( logger ) ) { + return false; } try { - await logger?.error( error, extraData ); + await logger.error( error, extraData ); } catch ( _error ) { errorLog( 'Failed to send log:', _error ); } diff --git a/packages/js/remote-logging/src/test/index.ts b/packages/js/remote-logging/src/test/index.ts index a2542330bb5..66a06a42f50 100644 --- a/packages/js/remote-logging/src/test/index.ts +++ b/packages/js/remote-logging/src/test/index.ts @@ -3,8 +3,6 @@ */ import '@wordpress/jest-console'; import { addFilter, removeFilter } from '@wordpress/hooks'; -import { getSetting } from '@woocommerce/settings'; - /** * Internal dependencies */ @@ -18,12 +16,6 @@ import { } from '../remote-logger'; import { fetchMock } from './__mocks__/fetch'; -jest.mock( '@woocommerce/settings', () => ( { - getSetting: jest.fn().mockReturnValue( { - isRemoteLoggingEnabled: true, - } ), -} ) ); - jest.mock( 'tracekit', () => ( { computeStackTrace: jest.fn().mockReturnValue( { name: 'Error', @@ -40,6 +32,17 @@ jest.mock( 'tracekit', () => ( { } ), } ) ); +jest.mock( '@woocommerce/settings', () => { + return { + getSetting: jest.fn().mockImplementation( ( key ) => { + if ( key === 'wcAssetUrl' ) { + return 'http://example.com/woocommerce/assets'; + } + return null; + } ), + }; +} ); + describe( 'RemoteLogger', () => { const originalConsoleWarn = console.warn; let logger: RemoteLogger; @@ -102,49 +105,57 @@ describe( 'RemoteLogger', () => { } ); describe( 'error', () => { - it( 'should send an error to the API with default data', async () => { - const error = new Error( 'Test error' ); - await logger.error( error ); + it( 'should send an error to the API with default data', async () => { + const error = new Error( 'Test error' ); + await logger.error( error ); - expect( fetchMock ).toHaveBeenCalledWith( - 'https://public-api.wordpress.com/rest/v1.1/js-error', - expect.objectContaining( { - method: 'POST', - body: expect.any( FormData ), - } ) - ); + expect( fetchMock ).toHaveBeenCalledWith( + 'https://public-api.wordpress.com/rest/v1.1/js-error', + expect.objectContaining( { + method: 'POST', + body: expect.any( FormData ), + } ) + ); - const formData = fetchMock.mock.calls[0][1].body; - const payload = JSON.parse(formData.get('error')); - expect( payload['message'] ).toBe( 'Test error' ); - expect( payload['severity'] ).toBe( 'error' ); - expect( payload['trace'] ).toContain( '#1 at testFunction (http://example.com/woocommerce/assets/js/admin/app.min.js:1:1)' ); - } ); + const formData = fetchMock.mock.calls[ 0 ][ 1 ].body; + const payload = JSON.parse( formData.get( 'error' ) ); + expect( payload[ 'message' ] ).toBe( 'Test error' ); + expect( payload[ 'severity' ] ).toBe( 'error' ); + expect( payload[ 'trace' ] ).toContain( + '#1 at testFunction (http://example.com/woocommerce/assets/js/admin/app.min.js:1:1)' + ); + } ); - it( 'should send an error to the API with extra data', async () => { - const error = new Error( 'Test error' ); - const extraData = { - severity: 'warning' as const, - tags: ['custom-tag'], - }; - await logger.error( error, extraData ); + it( 'should send an error to the API with extra data', async () => { + const error = new Error( 'Test error' ); + const extraData = { + severity: 'warning' as const, + tags: [ 'custom-tag' ], + }; + await logger.error( error, extraData ); - expect( fetchMock ).toHaveBeenCalledWith( - 'https://public-api.wordpress.com/rest/v1.1/js-error', - expect.objectContaining( { - method: 'POST', - body: expect.any( FormData ), - } ) - ); + expect( fetchMock ).toHaveBeenCalledWith( + 'https://public-api.wordpress.com/rest/v1.1/js-error', + expect.objectContaining( { + method: 'POST', + body: expect.any( FormData ), + } ) + ); - const formData = fetchMock.mock.calls[0][1].body; - const payload = JSON.parse(formData.get('error')); - expect( payload['message'] ).toBe( 'Test error' ); - expect( payload['severity'] ).toBe( 'warning' ); - expect( payload['tags'] ).toEqual( ["woocommerce", "js", "custom-tag"]); - expect( payload['trace'] ).toContain( '#1 at testFunction (http://example.com/woocommerce/assets/js/admin/app.min.js:1:1)' ); - } ); - } ); + const formData = fetchMock.mock.calls[ 0 ][ 1 ].body; + const payload = JSON.parse( formData.get( 'error' ) ); + expect( payload[ 'message' ] ).toBe( 'Test error' ); + expect( payload[ 'severity' ] ).toBe( 'warning' ); + expect( payload[ 'tags' ] ).toEqual( [ + 'woocommerce', + 'js', + 'custom-tag', + ] ); + expect( payload[ 'trace' ] ).toContain( + '#1 at testFunction (http://example.com/woocommerce/assets/js/admin/app.min.js:1:1)' + ); + } ); + } ); describe( 'handleError', () => { it( 'should send an error to the API', async () => { @@ -222,7 +233,7 @@ describe( 'RemoteLogger', () => { const error = new Error( 'Test error' ); const stackFrames = [ { - url: 'http://example.com/wp-content/plugins/woocommerce/assets/js/admin/app.min.js', + url: 'http://example.com/woocommerce/assets/js/admin/app.min.js', func: 'testFunction', args: [], line: 1, @@ -246,6 +257,13 @@ describe( 'RemoteLogger', () => { line: 1, column: 1, }, + { + url: 'http://example.com/other/plugin/woocommerce/assets/js/app.min.js', + func: 'testFunction', + args: [], + line: 1, + column: 1, + }, ]; const result = ( logger as any ).shouldHandleError( error, @@ -305,31 +323,23 @@ describe( 'RemoteLogger', () => { } ); } ); +global.window.wcSettings = { + isRemoteLoggingEnabled: true, +}; + describe( 'init', () => { beforeEach( () => { jest.clearAllMocks(); - ( getSetting as jest.Mock ).mockImplementation( - ( key, defaultValue ) => { - if ( key === 'isRemoteLoggingEnabled' ) { - return true; - } - return defaultValue; - } - ); + global.window.wcSettings = { + isRemoteLoggingEnabled: true, + }; } ); it( 'should not initialize or log when remote logging is disabled', () => { - // Mock the getSetting function to return false for isRemoteLoggingEnabled - ( getSetting as jest.Mock ).mockImplementation( - ( key, defaultValue ) => { - if ( key === 'isRemoteLoggingEnabled' ) { - return false; - } - return defaultValue; - } - ); - + global.window.wcSettings = { + isRemoteLoggingEnabled: false, + }; init( { errorRateLimitMs: 1000 } ); log( 'info', 'Test message' ); expect( fetchMock ).not.toHaveBeenCalled(); @@ -353,15 +363,9 @@ describe( 'init', () => { describe( 'log', () => { it( 'should not log if remote logging is disabled', () => { - ( getSetting as jest.Mock ).mockImplementation( - ( key, defaultValue ) => { - if ( key === 'isRemoteLoggingEnabled' ) { - return false; - } - return defaultValue; - } - ); - + global.window.wcSettings = { + isRemoteLoggingEnabled: false, + }; log( 'info', 'Test message' ); expect( fetchMock ).not.toHaveBeenCalled(); } ); @@ -369,15 +373,9 @@ describe( 'log', () => { describe( 'captureException', () => { it( 'should not log error if remote logging is disabled', () => { - ( getSetting as jest.Mock ).mockImplementation( - ( key, defaultValue ) => { - if ( key === 'isRemoteLoggingEnabled' ) { - return false; - } - return defaultValue; - } - ); - + global.window.wcSettings = { + isRemoteLoggingEnabled: false, + }; captureException( new Error( 'Test error' ) ); expect( fetchMock ).not.toHaveBeenCalled(); } ); diff --git a/packages/js/remote-logging/typings/global.d.ts b/packages/js/remote-logging/typings/global.d.ts index 2ba9695a964..e4db641ebd0 100644 --- a/packages/js/remote-logging/typings/global.d.ts +++ b/packages/js/remote-logging/typings/global.d.ts @@ -1,8 +1,8 @@ declare global { interface Window { - wcTracks: { - isEnabled: boolean; - }; + wcSettings?: { + isRemoteLoggingEnabled: boolean; + } } } diff --git a/packages/js/tracks/changelog/50828-dev-constrain-pnpm-version b/packages/js/tracks/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/packages/js/tracks/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/packages/js/tracks/package.json b/packages/js/tracks/package.json index 82aaf9e44ee..50fee1aac21 100644 --- a/packages/js/tracks/package.json +++ b/packages/js/tracks/package.json @@ -6,7 +6,7 @@ "license": "GPL-3.0-or-later", "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "keywords": [ "wordpress", diff --git a/packages/php/blueprint/.wp-env.json b/packages/php/blueprint/.wp-env.json new file mode 100644 index 00000000000..fc70ff6df15 --- /dev/null +++ b/packages/php/blueprint/.wp-env.json @@ -0,0 +1,26 @@ +{ + "phpVersion": "7.4", + "plugins": [ + "." + ], + "config": { + "JETPACK_AUTOLOAD_DEV": true, + "WP_DEBUG_LOG": true, + "WP_DEBUG_DISPLAY": true, + "ALTERNATE_WP_CRON": true + }, + "mappings": { + }, + "env": { + "development": {}, + "tests": { + "port": 8087, + "plugins": [ + "." + ], + "themes": [], + "config": { + } + } + } +} diff --git a/packages/php/blueprint/README.md b/packages/php/blueprint/README.md new file mode 100644 index 00000000000..4339d8d0531 --- /dev/null +++ b/packages/php/blueprint/README.md @@ -0,0 +1,127 @@ + +# Blueprint + +This PHP Composer package facilitates exporting and importing WordPress Blueprint +compatible JSON formats. It offers a solid framework for seamless integration with +WordPress sites and supports extensibility, enabling plugins to customize export +and import functionalities. Manage site configurations, options, and settings +effortlessly with JSON files. + +## Built-in Steps + +| Step | +|------------------| +| `installPlugin` | +| `activatePlugin` | +| `deactivatePlugin` | +| `deletePlugin` | +| `installTheme` | +| `activateTheme` | +| `setSiteOptions` | + +## Hooks + +| Hook | Description | +|--------------------------|---------------------------------| +| `wooblueprint_exporters` | A hook to add custom exporters. | +| `wooblueprint_importers` | A hook to add custom importers. | + +## Example: Adding a Custom Exporter + +1. Create a new class that extends `Automattic\WooCommerce\Blueprint\Exporters\StepExporter`. + +```php + get_option( 'option1', 'value1' ), + 'option2' => get_option( 'option2', 'value2' ), + ]; + return new SetSiteOptions( $data ); + } + + public function get_step_name() { + return SetSiteOptions::get_step_name(); + } +} + +``` + +5. Lastly, register the exporter with the Blueprint package via `wooblueprint_exporters` +filter. + +```php +use Automattic\WooCommerce\Blueprint\Exporters\StepExporter; +use Automattic\WooCommerce\Blueprint\Steps\Step; + +class MyCustomExporter extends StepExporter { + public function export(): Step { + $data = [ + 'option1' => get_option( 'option1', 'value1' ), + 'option2' => get_option( 'option2', 'value2' ), + ]; + return new SetSiteOptions( $data ); + } + + public function get_step_name() { + return SetSiteOptions::get_step_name(); + } +} + +add_filter( 'wooblueprint_exporters', function( array $exporters ) { + $exporters[] = new MyCustomExporter(); + return $exporters; +} ); + +``` + +When exporting a Blueprint, the `MyCustomExporter` class will be called and the `SetSiteOptions` +step will be added to the Blueprint JSON. + +Output: + + ```json + { + "steps": [ + { + "name": "setSiteOptions", + "options": { + "option1": "value1", + "option2": "value2" + } + } + ] + } + ``` + +## Example: Adding a Custom Importer + +## Example: Adding a Custom Step + +## Example: Aliasing a Custom Exporter diff --git a/packages/php/blueprint/blueprint.php b/packages/php/blueprint/blueprint.php new file mode 100644 index 00000000000..97fd898a36d --- /dev/null +++ b/packages/php/blueprint/blueprint.php @@ -0,0 +1,11 @@ +=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2024-06-12T14:39:25+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.1.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-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/v5.1.0" + }, + "time": "2024-07-01T20:03:41+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.31", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.18 || ^5.0", + "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": "^9.3" + }, + "suggest": { + "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": "9.2-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 that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:37:42+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.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": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "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/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.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": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.19", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.28", + "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": "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" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-04-05T04:35:58+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "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": "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.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+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": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "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/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "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.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "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": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "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/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "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": "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.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+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": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "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" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-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 types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.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 that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "yoast/phpunit-polyfills", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", + "reference": "4a088f125c970d6d6ea52c927f96fe39b330d0f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/4a088f125c970d6d6ea52c927f96fe39b330d0f1", + "reference": "4a088f125c970d6d6ea52c927f96fe39b330d0f1", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "phpunit/phpunit": "^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "yoast/yoastcs": "^3.1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "files": [ + "phpunitpolyfills-autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Team Yoast", + "email": "support@yoast.com", + "homepage": "https://yoast.com" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills/graphs/contributors" + } + ], + "description": "Set of polyfills for changed PHPUnit functionality to allow for creating PHPUnit cross-version compatible tests", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills", + "keywords": [ + "phpunit", + "polyfill", + "testing" + ], + "support": { + "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", + "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", + "source": "https://github.com/Yoast/PHPUnit-Polyfills" + }, + "time": "2024-04-05T16:36:44+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/packages/php/blueprint/development.md b/packages/php/blueprint/development.md new file mode 100644 index 00000000000..cd914e1b149 --- /dev/null +++ b/packages/php/blueprint/development.md @@ -0,0 +1,9 @@ +# Development + +## Running Unit Tests + +We use [wp-env](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/) to setup test environment in Docker. +Please install `wp-env` by running `npm install -g @wordpress/env` if you haven't already. + +1. Run `composer run test:setup` to run wp-env. +2. Run `composer run test:unit` to run unit tests. diff --git a/packages/php/blueprint/phpcs.xml b/packages/php/blueprint/phpcs.xml new file mode 100644 index 00000000000..5640982ec60 --- /dev/null +++ b/packages/php/blueprint/phpcs.xml @@ -0,0 +1,56 @@ + + + WooCommerce dev PHP_CodeSniffer ruleset. + + . + + + */node_modules/* + */vendor/* + + + + + + + + + + + + + + + + + + src/ + tests/ + + + src/ + tests/ + + + src/ + . + + + src/ + tests/ + + + src/ + tests/ + + + tests/ + + + tests/ + + + tests/ + + + diff --git a/packages/php/blueprint/phpunit.xml.dist b/packages/php/blueprint/phpunit.xml.dist new file mode 100644 index 00000000000..adc3bbb9554 --- /dev/null +++ b/packages/php/blueprint/phpunit.xml.dist @@ -0,0 +1,21 @@ + + + + + ./tests + + + + + + + diff --git a/packages/php/blueprint/src/BuiltInExporters.php b/packages/php/blueprint/src/BuiltInExporters.php new file mode 100644 index 00000000000..687931a2c6a --- /dev/null +++ b/packages/php/blueprint/src/BuiltInExporters.php @@ -0,0 +1,23 @@ +schema = $schema; + } + + /** + * Returns an array of all step processors. + * + * @return array The array of step processors. + */ + public function get_all() { + return array( + $this->create_install_plugins_processor(), + $this->create_install_themes_processor(), + new ImportSetSiteOptions(), + new ImportDeletePlugin(), + new ImportActivatePlugin(), + new ImportActivateTheme(), + new ImportDeactivatePlugin(), + ); + } + + /** + * Creates the processor for installing plugins. + * + * @return ImportInstallPlugin The processor for installing plugins. + */ + private function create_install_plugins_processor() { + $storages = new ResourceStorages(); + $storages->add_storage( new OrgPluginResourceStorage() ); + + if ( $this->schema instanceof ZipSchema ) { + $storages->add_storage( new LocalPluginResourceStorage( $this->schema->get_unzipped_path() ) ); + } + + return new ImportInstallPlugin( $storages ); + } + + /** + * Creates the processor for installing themes. + * + * @return ImportInstallTheme The processor for installing themes. + */ + private function create_install_themes_processor() { + $storage = new ResourceStorages(); + $storage->add_storage( new OrgThemeResourceStorage() ); + if ( $this->schema instanceof ZipSchema ) { + $storage->add_storage( new LocalThemeResourceStorage( $this->schema->get_unzipped_path() ) ); + } + + return new ImportInstallTheme( $storage ); + } +} diff --git a/packages/php/blueprint/src/Cli.php b/packages/php/blueprint/src/Cli.php new file mode 100644 index 00000000000..9c6e8e3ac7e --- /dev/null +++ b/packages/php/blueprint/src/Cli.php @@ -0,0 +1,95 @@ +run( $assoc_args ); + }, + array( + 'synopsis' => array( + array( + 'type' => 'positional', + 'name' => 'schema-path', + 'optional' => false, + ), + array( + 'type' => 'assoc', + 'name' => 'show-messages', + 'optional' => true, + 'options' => array( 'all', 'error', 'info', 'debug' ), + ), + ), + 'when' => 'after_wp_load', + ) + ); + + \WP_CLI::add_command( + 'wc blueprint export', + function ( $args, $assoc_args ) { + $export = new ExportCli( $args[0] ); + $steps = array(); + $format = $assoc_args['format'] ?? 'json'; + + if ( isset( $assoc_args['steps'] ) ) { + $steps = array_map( + function ( $step ) { + return trim( $step ); + }, + explode( ',', $assoc_args['steps'] ) + ); + } + $export->run( + array( + 'steps' => $steps, + 'format' => $format, + ) + ); + }, + array( + 'synopsis' => array( + array( + 'type' => 'positional', + 'name' => 'save-to', + 'optional' => false, + ), + array( + 'type' => 'assoc', + 'name' => 'steps', + 'optional' => true, + ), + array( + 'type' => 'assoc', + 'name' => 'format', + 'optional' => true, + 'default' => 'json', + 'options' => array( 'json', 'zip' ), + ), + ), + 'when' => 'after_wp_load', + ) + ); + } +} diff --git a/packages/php/blueprint/src/Cli/ExportCli.php b/packages/php/blueprint/src/Cli/ExportCli.php new file mode 100644 index 00000000000..d3b9b14d95a --- /dev/null +++ b/packages/php/blueprint/src/Cli/ExportCli.php @@ -0,0 +1,61 @@ +save_to = $save_to; + } + + /** + * Run the export process. + * + * @param array $args The arguments for the export process. + */ + public function run( $args = array() ) { + $export_as_zip = isset( $args['format'] ) && 'zip' === $args['format']; + if ( ! isset( $args['steps'] ) ) { + $args['steps'] = array(); + } + + $exporter = new ExportSchema(); + + $schema = $exporter->export( $args['steps'], $export_as_zip ); + + if ( $export_as_zip ) { + $zip_exported_schema = new ZipExportedSchema( $schema ); + $this->save_to = $zip_exported_schema->zip(); + \WP_CLI::success( "Exported zip to {$this->save_to}" ); + } else { + // phpcs:ignore + $save = file_put_contents( $this->save_to, json_encode( $schema, JSON_PRETTY_PRINT ) ); + if ( false === $save ) { + \WP_CLI::error( "Failed to save to {$this->save_to}" ); + } else { + \WP_CLI::success( "Exported JSON to {$this->save_to}" ); + } + } + } +} diff --git a/packages/php/blueprint/src/Cli/ImportCli.php b/packages/php/blueprint/src/Cli/ImportCli.php new file mode 100644 index 00000000000..cd3a082b344 --- /dev/null +++ b/packages/php/blueprint/src/Cli/ImportCli.php @@ -0,0 +1,52 @@ +schema_path = $schema_path; + } + + /** + * Run the import process. + * + * @param array $optional_args Optional arguments. + * + * @return void + */ + public function run( $optional_args ) { + $blueprint = ImportSchema::create_from_file( $this->schema_path ); + $results = $blueprint->import(); + + $result_formatter = new CliResultFormatter( $results ); + $is_success = $result_formatter->is_success(); + + if ( isset( $optional_args['show-messages'] ) ) { + $result_formatter->format( $optional_args['show-messages'] ); + } + + if ( $is_success ) { + \WP_CLI::success( "$this->schema_path imported successfully" ); + } else { + \WP_CLI::error( "Failed to import $this->schema_path. Run with --show-messages=all to debug" ); + } + } +} diff --git a/packages/php/blueprint/src/ExportSchema.php b/packages/php/blueprint/src/ExportSchema.php new file mode 100644 index 00000000000..3fde3de3e77 --- /dev/null +++ b/packages/php/blueprint/src/ExportSchema.php @@ -0,0 +1,104 @@ +exporters = $exporters; + } + + /** + * Export the schema steps. + * + * @param string[] $steps Array of step names to export, optional. + * @param bool $zip Whether to export as a ZIP file, optional. + * + * @return array The exported schema array. + */ + public function export( $steps = array(), $zip = false ) { + $schema = array( + 'landingPage' => $this->wp_apply_filters( 'wooblueprint_export_landingpage', '/' ), + 'steps' => array(), + ); + + $built_in_exporters = ( new BuiltInExporters() )->get_all(); + + /** + * Filters the step exporters. + * + * Allows adding/removing custom step exporters. + * + * @param StepExporter[] $exporters Array of step exporters. + * + * @since 0.0.1 + */ + $exporters = $this->wp_apply_filters( 'wooblueprint_exporters', array_merge( $this->exporters, $built_in_exporters ) ); + + // Filter out any exporters that are not in the list of steps to export. + if ( count( $steps ) ) { + foreach ( $exporters as $key => $exporter ) { + $name = $exporter->get_step_name(); + $alias = $exporter instanceof HasAlias ? $exporter->get_alias() : $name; + if ( ! in_array( $name, $steps, true ) && ! in_array( $alias, $steps, true ) ) { + unset( $exporters[ $key ] ); + } + } + } + + if ( $zip ) { + $exporters = array_map( + function ( $exporter ) { + if ( $exporter instanceof ExportInstallPluginSteps ) { + $exporter->include_private_plugins( true ); + } + return $exporter; + }, + $exporters + ); + } + + /** + * StepExporter. + * + * @var StepExporter $exporter + */ + foreach ( $exporters as $exporter ) { + $step = $exporter->export(); + if ( is_array( $step ) ) { + foreach ( $step as $_step ) { + $schema['steps'][] = $_step->get_json_array(); + } + } else { + $schema['steps'][] = $step->get_json_array(); + } + } + + return $schema; + } +} diff --git a/packages/php/blueprint/src/Exporters/ExportInstallPluginSteps.php b/packages/php/blueprint/src/Exporters/ExportInstallPluginSteps.php new file mode 100644 index 00000000000..d239347cdbb --- /dev/null +++ b/packages/php/blueprint/src/Exporters/ExportInstallPluginSteps.php @@ -0,0 +1,93 @@ +include_private_plugins = $boolean; + } + + /** + * Export the steps required to install plugins. + * + * @return array The array of InstallPlugin steps. + */ + public function export() { + $plugins = $this->wp_get_plugins(); + + // @todo temporary fix for JN site -- it includes WooCommerce as a custom plugin + // since JN sites are using a different slug. + $exclude = array( 'WooCommerce Beta Tester' ); + $steps = array(); + foreach ( $plugins as $path => $plugin ) { + if ( in_array( $plugin['Name'], $exclude, true ) ) { + continue; + } + // skip inactive plugins for now. + if ( ! $this->wp_is_plugin_active( $path ) ) { + continue; + } + $slug = dirname( $path ); + // single-file plugin. + if ( '.' === $slug ) { + $slug = pathinfo( $path )['filename']; + } + $info = $this->wp_plugins_api( + 'plugin_information', + array( + 'slug' => $slug, + 'fields' => array( + 'sections' => false, + ), + ) + ); + + $has_download_link = isset( $info->download_link ); + if ( false === $this->include_private_plugins && ! $has_download_link ) { + continue; + } + + $resource = $has_download_link ? 'wordpress.org/plugins' : 'self/plugins'; + $steps[] = new InstallPlugin( + $slug, + $resource, + array( + 'activate' => true, + ) + ); + } + + return $steps; + } + + /** + * Get the name of the step. + * + * @return string The step name. + */ + public function get_step_name() { + return InstallPlugin::get_step_name(); + } +} diff --git a/packages/php/blueprint/src/Exporters/ExportInstallThemeSteps.php b/packages/php/blueprint/src/Exporters/ExportInstallThemeSteps.php new file mode 100644 index 00000000000..02a25cb4906 --- /dev/null +++ b/packages/php/blueprint/src/Exporters/ExportInstallThemeSteps.php @@ -0,0 +1,63 @@ +wp_get_themes(); + $active_theme = $this->wp_get_theme(); + + foreach ( $themes as $slug => $theme ) { + // Check if the theme is active. + $is_active = $theme->get( 'Name' ) === $active_theme->get( 'Name' ); + + $info = $this->wp_themes_api( + 'theme_information', + array( + 'slug' => $slug, + 'fields' => array( + 'sections' => false, + ), + ) + ); + if ( isset( $info->download_link ) ) { + $steps[] = new InstallTheme( + $slug, + 'wordpress.org/themes', + array( + 'activate' => $is_active, + ) + ); + } + } + + return $steps; + } + + /** + * Get the step name. + * + * @return string + */ + public function get_step_name() { + return InstallTheme::get_step_name(); + } +} diff --git a/packages/php/blueprint/src/Exporters/HasAlias.php b/packages/php/blueprint/src/Exporters/HasAlias.php new file mode 100644 index 00000000000..6cb0b027b9a --- /dev/null +++ b/packages/php/blueprint/src/Exporters/HasAlias.php @@ -0,0 +1,38 @@ + Settings + * Step B: Exports options for the core profiler selection. + * Step C: Exports options for the task list. + * + * You also have a UI where a client can select which steps to export. In this case, we have three checkboxes. + * + * [ ] WooCommerce Settings + * [ ] WooCommerce Core Profiler + * [ ] WooCommerce Task List + * + * Without alias, the client would see three `setSiteOptions` steps and it's not possible + * to distinguish between them from the ExportSchema class. + * + * With alias, you can give each step a unique alias while keeping the step name the same. + * + * @todo Link to an example class that uses this interface. + * + * Interface HasAlias + */ +interface HasAlias { + /** + * Get the alias for the step. + * + * @return string The alias for the step. + */ + public function get_alias(); +} diff --git a/packages/php/blueprint/src/Exporters/StepExporter.php b/packages/php/blueprint/src/Exporters/StepExporter.php new file mode 100644 index 00000000000..ddbd5f309a7 --- /dev/null +++ b/packages/php/blueprint/src/Exporters/StepExporter.php @@ -0,0 +1,27 @@ +schema = $schema; + if ( null === $validator ) { + $validator = new Validator(); + } + + $this->validator = $validator; + + $this->builtin_step_processors = new BuiltInStepProcessors( $schema ); + } + + /** + * Get the schema. + * + * @return JsonSchema The schema. + */ + public function get_schema() { + return $this->schema; + } + + /** + * Create an ImportSchema instance from a file. + * + * @param string $file The file path. + * @return ImportSchema The created ImportSchema instance. + */ + public static function create_from_file( $file ) { + // @todo check for mime type + // @todo check for allowed types -- json or zip + $path_info = pathinfo( $file ); + $is_zip = 'zip' === $path_info['extension']; + + return $is_zip ? self::create_from_zip( $file ) : self::create_from_json( $file ); + } + + /** + * Create an ImportSchema instance from a JSON file. + * + * @param string $json_path The JSON file path. + * @return ImportSchema The created ImportSchema instance. + */ + public static function create_from_json( $json_path ) { + return new self( new JsonSchema( $json_path ) ); + } + + /** + * Create an ImportSchema instance from a ZIP file. + * + * @param string $zip_path The ZIP file path. + * + * @return ImportSchema The created ImportSchema instance. + */ + public static function create_from_zip( $zip_path ) { + return new self( new ZipSchema( $zip_path ) ); + } + + /** + * Import the schema steps. + * + * @return StepProcessorResult[] + */ + public function import() { + $results = array(); + $result = StepProcessorResult::success( 'ImportSchema' ); + $results[] = $result; + + $step_processors = $this->builtin_step_processors->get_all(); + + /** + * Filters the step processors. + * + * Allows adding/removing custom step processors. + * + * @param StepProcessor[] $step_processors The step processors. + * + * @since 0.0.1 + */ + $step_processors = $this->wp_apply_filters( 'wooblueprint_importers', $step_processors ); + + $indexed_step_processors = Util::index_array( + $step_processors, + function ( $key, $step_processor ) { + return $step_processor->get_step_class()::get_step_name(); + } + ); + + // validate steps before processing. + $this->validate_step_schemas( $indexed_step_processors, $result ); + + if ( count( $result->get_messages( 'error' ) ) !== 0 ) { + return $results; + } + + foreach ( $this->schema->get_steps() as $step_schema ) { + $step_processor = $indexed_step_processors[ $step_schema->step ] ?? null; + if ( ! $step_processor instanceof StepProcessor ) { + $result->add_error( "Unable to create a step processor for {$step_schema->step}" ); + continue; + } + + $results[] = $step_processor->process( $step_schema ); + } + + return $results; + } + + /** + * Validate the step schemas. + * + * @param array $indexed_step_processors Array of step processors indexed by step name. + * @param StepProcessorResult $result The result object to add messages to. + * + * @return void + */ + protected function validate_step_schemas( array $indexed_step_processors, StepProcessorResult $result ) { + $step_schemas = array_map( + function ( $step_processor ) { + return $step_processor->get_step_class()::get_schema(); + }, + $indexed_step_processors + ); + + foreach ( $this->schema->get_steps() as $step_json ) { + $step_schema = $step_schemas[ $step_json->step ] ?? null; + if ( ! $step_schema ) { + $result->add_info( "No schema found for step $step_json->step" ); + continue; + } + + // phpcs:ignore + $validate = $this->validator->validate( $step_json, json_encode( $step_schema ) ); + + if ( ! $validate->isValid() ) { + $result->add_error( "Schema validation failed for step {$step_json->step}" ); + $errors = ( new ErrorFormatter() )->format( $validate->error() ); + $formatted_errors = array(); + foreach ( $errors as $value ) { + $formatted_errors[] = implode( "\n", $value ); + } + + $result->add_error( implode( "\n", $formatted_errors ) ); + } + } + } +} diff --git a/packages/php/blueprint/src/Importers/ImportActivatePlugin.php b/packages/php/blueprint/src/Importers/ImportActivatePlugin.php new file mode 100644 index 00000000000..2535c35262b --- /dev/null +++ b/packages/php/blueprint/src/Importers/ImportActivatePlugin.php @@ -0,0 +1,47 @@ +pluginName; + + $activate = $this->activate_plugin_by_slug( $name ); + if ( $activate ) { + $result->add_info( "Activated {$name}." ); + } else { + $result->add_error( "Unable to activate {$name}." ); + } + + return $result; + } + + /** + * Get the step class. + * + * @return string + */ + public function get_step_class(): string { + return ActivatePlugin::class; + } +} diff --git a/packages/php/blueprint/src/Importers/ImportActivateTheme.php b/packages/php/blueprint/src/Importers/ImportActivateTheme.php new file mode 100644 index 00000000000..8475782d0dd --- /dev/null +++ b/packages/php/blueprint/src/Importers/ImportActivateTheme.php @@ -0,0 +1,47 @@ +themeName; + + $switch = $this->wp_switch_theme( $name ); + $switch && $result->add_debug( "Switched theme to '{$name}'." ); + + return $result; + } + + /** + * Returns the class name of the step this processor handles. + * + * @return string The class name of the step this processor handles. + */ + public function get_step_class(): string { + return ActivateTheme::class; + } +} diff --git a/packages/php/blueprint/src/Importers/ImportDeactivatePlugin.php b/packages/php/blueprint/src/Importers/ImportDeactivatePlugin.php new file mode 100644 index 00000000000..e66e201bda1 --- /dev/null +++ b/packages/php/blueprint/src/Importers/ImportDeactivatePlugin.php @@ -0,0 +1,42 @@ +pluginName; + + $this->deactivate_plugin_by_slug( $name ); + $result->add_info( "Deactivated {$name}." ); + + return $result; + } + + /** + * Get the step class. + * + * @return string + */ + public function get_step_class(): string { + return DeactivatePlugin::class; + } +} diff --git a/packages/php/blueprint/src/Importers/ImportDeletePlugin.php b/packages/php/blueprint/src/Importers/ImportDeletePlugin.php new file mode 100644 index 00000000000..a45d5a8bc11 --- /dev/null +++ b/packages/php/blueprint/src/Importers/ImportDeletePlugin.php @@ -0,0 +1,47 @@ +pluginName; + + $delete = $this->delete_plugin_by_slug( $name ); + if ( $delete ) { + $result->add_info( "Deleted {$name}." ); + } else { + $result->add_error( "Unable to delete {$name}." ); + } + + return $result; + } + + /** + * Get the step class. + * + * @return string + */ + public function get_step_class(): string { + return DeletePlugin::class; + } +} diff --git a/packages/php/blueprint/src/Importers/ImportInstallPlugin.php b/packages/php/blueprint/src/Importers/ImportInstallPlugin.php new file mode 100644 index 00000000000..6e99f2bf82a --- /dev/null +++ b/packages/php/blueprint/src/Importers/ImportInstallPlugin.php @@ -0,0 +1,156 @@ +storage = $storage; + } + + /** + * Processes the schema to install and optionally activate a plugin. + * + * @param object $schema Schema object containing plugin information. + * @return StepProcessorResult Result of the processing. + */ + public function process( $schema ): StepProcessorResult { + $result = StepProcessorResult::success( InstallPlugin::get_step_name() ); + + $installed_plugins = $this->get_installed_plugins_paths(); + + // phpcs:ignore + $plugin = $schema->pluginZipFile; + + if ( isset( $installed_plugins[ $plugin->slug ] ) ) { + $result->add_info( "Skipped installing {$plugin->slug}. It is already installed." ); + return $result; + } + if ( $this->storage->is_supported_resource( $plugin->resource ) === false ) { + $result->add_error( "Invalid resource type for {$plugin->slug}." ); + return $result; + } + + $downloaded_path = $this->storage->download( $plugin->slug, $plugin->resource ); + if ( ! $downloaded_path ) { + $result->add_error( "Unable to download {$plugin->slug} with {$plugin->resource} resource type." ); + return $result; + } + + $install = $this->install( $downloaded_path ); + $install && $result->add_info( "Installed {$plugin->slug}." ); + + if ( isset( $plugin->options, $plugin->options->activate ) && true === $plugin->options->activate ) { + $activate = $this->activate( $plugin->slug ); + + if ( $activate instanceof \WP_Error ) { + $result->add_error( "Failed to activate {$plugin->slug}." ); + } + + if ( null === $activate ) { + $result->add_info( "Activated {$plugin->slug}." ); + } + } + + return $result; + } + + /** + * Installs a plugin from the given local path. + * + * @param string $local_plugin_path Path to the local plugin file. + * @return bool True on success, false on failure. + */ + protected function install( $local_plugin_path ) { + if ( ! class_exists( 'Plugin_Upgrader' ) ) { + include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php'; + include_once ABSPATH . '/wp-admin/includes/class-plugin-upgrader.php'; + } + + $upgrader = new \Plugin_Upgrader( new \Automatic_Upgrader_Skin() ); + return $upgrader->install( $local_plugin_path ); + } + + /** + * Activates an installed plugin by its slug. + * + * @param string $slug Plugin slug. + * @return \WP_Error|null WP_Error on failure, null on success. + */ + protected function activate( $slug ) { + if ( empty( $this->installed_plugin_paths ) ) { + $this->installed_plugin_paths = $this->get_installed_plugins_paths(); + } + + $path = $this->installed_plugin_paths[ $slug ] ?? false; + + if ( ! $path ) { + return new \WP_Error( 'plugin_not_installed', "Plugin {$slug} is not installed." ); + } + + return $this->wp_activate_plugin( $path ); + } + + /** + * Retrieves an array of installed plugins and their paths. + * + * @return array Array of installed plugins and their paths. + */ + protected function get_installed_plugins_paths() { + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $plugins = get_plugins(); + $installed_plugins = array(); + + foreach ( $plugins as $path => $plugin ) { + $path_parts = explode( '/', $path ); + $slug = $path_parts[0]; + $installed_plugins[ $slug ] = $path; + } + + return $installed_plugins; + } + + /** + * Returns the class name of the step being processed. + * + * @return string Class name of the step. + */ + public function get_step_class(): string { + return InstallPlugin::class; + } +} diff --git a/packages/php/blueprint/src/Importers/ImportInstallTheme.php b/packages/php/blueprint/src/Importers/ImportInstallTheme.php new file mode 100644 index 00000000000..9b95a96f035 --- /dev/null +++ b/packages/php/blueprint/src/Importers/ImportInstallTheme.php @@ -0,0 +1,120 @@ +result = StepProcessorResult::success( InstallTheme::get_step_name() ); + $this->storage = $storage; + } + + /** + * Process the schema to install the theme. + * + * @param object $schema The schema containing theme installation details. + * + * @return StepProcessorResult The result of the step processing. + */ + public function process( $schema ): StepProcessorResult { + $installed_themes = $this->wp_get_themes(); + // phpcs:ignore + $theme = $schema->themeZipFile; + + if ( isset( $installed_themes[ $theme->slug ] ) ) { + $this->result->add_info( "Skipped installing {$theme->slug}. It is already installed." ); + return $this->result; + } + if ( $this->storage->is_supported_resource( $theme->resource ) === false ) { + $this->result->add_error( "Invalid resource type for {$theme->slug}" ); + return $this->result; + } + + $downloaded_path = $this->storage->download( $theme->slug, $theme->resource ); + + if ( ! $downloaded_path ) { + $this->result->add_error( "Unable to download {$theme->slug} with {$theme->resource} resource type." ); + return $this->result; + } + + $this->result->add_debug( "'$theme->slug' has been downloaded in $downloaded_path" ); + + $install = $this->install( $downloaded_path ); + + if ( $install ) { + $this->result->add_debug( "Theme '$theme->slug' installed successfully." ); + } else { + $this->result->add_error( "Failed to install theme '$theme->slug'." ); + } + + $theme_switch = true === $theme->activate && $this->wp_switch_theme( $theme->slug ); + + if ( $theme_switch ) { + $this->result->add_info( "Switched theme to '$theme->slug'." ); + } else { + $this->result->add_error( "Failed to switch theme to '$theme->slug'." ); + } + + return $this->result; + } + + /** + * Install the theme from the local plugin path. + * + * @param string $local_plugin_path The local path of the plugin to be installed. + * + * @return bool True if the installation was successful, false otherwise. + */ + protected function install( $local_plugin_path ) { + $unzip_result = $this->wp_unzip_file( $local_plugin_path, $this->wp_get_theme_root() ); + + if ( $this->is_wp_error( $unzip_result ) ) { + return false; + } + + return true; + } + + /** + * Get the class name of the step. + * + * @return string The class name of the step. + */ + public function get_step_class(): string { + return InstallTheme::class; + } +} diff --git a/packages/php/blueprint/src/Importers/ImportSetSiteOptions.php b/packages/php/blueprint/src/Importers/ImportSetSiteOptions.php new file mode 100644 index 00000000000..9a70447fdba --- /dev/null +++ b/packages/php/blueprint/src/Importers/ImportSetSiteOptions.php @@ -0,0 +1,57 @@ +options as $key => $value ) { + if ( is_object( $value ) ) { + $value = (array) $value; + } + + $updated = $this->wp_update_option( $key, $value ); + + if ( $updated ) { + $result->add_info( "{$key} has been updated" ); + } else { + $current_value = $this->wp_get_option( $key ); + if ( $current_value === $value ) { + $result->add_info( "{$key} has not been updated because the current value is already up to date." ); + } + } + } + + return $result; + } + + /** + * Get the step class. + * + * @return string + */ + public function get_step_class(): string { + return SetSiteOptions::class; + } +} diff --git a/packages/php/blueprint/src/ResourceStorages.php b/packages/php/blueprint/src/ResourceStorages.php new file mode 100644 index 00000000000..98876c32122 --- /dev/null +++ b/packages/php/blueprint/src/ResourceStorages.php @@ -0,0 +1,68 @@ +get_supported_resource(); + if ( ! isset( $this->storages[ $supported_resource ] ) ) { + $this->storages[ $supported_resource ] = array(); + } + $this->storages[ $supported_resource ][] = $downloader; + } + + /** + * Check if the resource is supported. + * + * @param string $resource The resource to check. + * + * @return bool + */ + // phpcs:ignore + public function is_supported_resource( $resource ) { + return isset( $this->storages[ $resource ] ); + } + + /** + * Download the resource. + * + * @param string $slug The slug of the resource to download. + * @param string $resource The resource to download. + * + * @return false|string + */ + // phpcs:ignore + public function download( $slug, $resource ) { + if ( ! isset( $this->storages[ $resource ] ) ) { + return false; + } + $storages = $this->storages[ $resource ]; + foreach ( $storages as $storage ) { + // phpcs:ignore + if ( $found = $storage->download( $slug ) ) { + return $found; + } + } + + return false; + } +} diff --git a/packages/php/blueprint/src/ResourceStorages/LocalPluginResourceStorage.php b/packages/php/blueprint/src/ResourceStorages/LocalPluginResourceStorage.php new file mode 100644 index 00000000000..ccdd456663b --- /dev/null +++ b/packages/php/blueprint/src/ResourceStorages/LocalPluginResourceStorage.php @@ -0,0 +1,58 @@ +paths[] = $path; + } + + /** + * Local plugins are already included (downloaded) in the zip file. + * Return the full path. + * + * @param string $slug The slug of the plugin to be downloaded. + * + * @return string|null + */ + public function download( $slug ): ?string { + foreach ( $this->paths as $path ) { + $full_path = $path . "/{$this->suffix}/" . $slug . '.zip'; + if ( is_file( $full_path ) ) { + return $full_path; + } + } + return null; + } + + /** + * Get the supported resource. + * + * @return string The supported resource. + */ + public function get_supported_resource(): string { + return 'self/plugins'; + } +} diff --git a/packages/php/blueprint/src/ResourceStorages/LocalThemeResourceStorage.php b/packages/php/blueprint/src/ResourceStorages/LocalThemeResourceStorage.php new file mode 100644 index 00000000000..6d910366337 --- /dev/null +++ b/packages/php/blueprint/src/ResourceStorages/LocalThemeResourceStorage.php @@ -0,0 +1,24 @@ +get_download_link( $slug ); + if ( ! $download_link ) { + return false; + } + return $this->download_url( $download_link ); + } + + /** + * Download the file from the given URL. + * + * @param string $url The URL to download the file from. + * + * @return string|null The path to the downloaded file, or null on failure. + */ + protected function download_url( $url ) { + return $this->wp_download_url( $url ); + } + + /** + * Get the download link for a plugin from wordpress.org. + * + * @param string $slug The slug of the plugin. + * + * @return string|null The download link, or null if not found. + */ + protected function get_download_link( $slug ): ?string { + $info = $this->wp_plugins_api( + 'plugin_information', + array( + 'slug' => $slug, + 'fields' => array( + 'sections' => false, + ), + ) + ); + + if ( is_object( $info ) && isset( $info->download_link ) ) { + return $info->download_link; + } + + return null; + } + + /** + * Get the supported resource type. + * + * @return string The supported resource type. + */ + public function get_supported_resource(): string { + return 'wordpress.org/plugins'; + } +} diff --git a/packages/php/blueprint/src/ResourceStorages/OrgThemeResourceStorage.php b/packages/php/blueprint/src/ResourceStorages/OrgThemeResourceStorage.php new file mode 100644 index 00000000000..6c116e9488f --- /dev/null +++ b/packages/php/blueprint/src/ResourceStorages/OrgThemeResourceStorage.php @@ -0,0 +1,42 @@ +wp_themes_api( + 'theme_information', + array( + 'slug' => $slug, + 'fields' => array( + 'sections' => false, + ), + ) + ); + + if ( isset( $info->download_link ) ) { + return $info->download_link; + } + + return null; + } + + /** + * Get the supported resource. + * + * @return string The supported resource. + */ + public function get_supported_resource(): string { + return 'wordpress.org/themes'; + } +} diff --git a/packages/php/blueprint/src/ResourceStorages/ResourceStorage.php b/packages/php/blueprint/src/ResourceStorages/ResourceStorage.php new file mode 100644 index 00000000000..ca34be9d413 --- /dev/null +++ b/packages/php/blueprint/src/ResourceStorages/ResourceStorage.php @@ -0,0 +1,30 @@ +results = $results; + } + + /** + * Format the results. + * + * @param string $message_type The message type to format. + * + * @return void + */ + public function format( $message_type = 'debug' ) { + $header = array( 'Step Processor', 'Type', 'Message' ); + $items = array(); + + foreach ( $this->results as $result ) { + $step_name = $result->get_step_name(); + foreach ( $result->get_messages( $message_type ) as $message ) { + $items[] = array( + 'Step Processor' => $step_name, + 'Type' => $message['type'], + 'Message' => $message['message'], + ); + } + } + + format_items( 'table', $items, $header ); + } + + /** + * Check if all results are successful. + * + * @return bool True if all results are successful, false otherwise. + */ + public function is_success() { + foreach ( $this->results as $result ) { + $is_success = $result->is_success(); + if ( ! $is_success ) { + return false; + } + } + return true; + } +} diff --git a/packages/php/blueprint/src/ResultFormatters/JsonResultFormatter.php b/packages/php/blueprint/src/ResultFormatters/JsonResultFormatter.php new file mode 100644 index 00000000000..4c0837eea3e --- /dev/null +++ b/packages/php/blueprint/src/ResultFormatters/JsonResultFormatter.php @@ -0,0 +1,69 @@ +results = $results; + } + + /** + * Format the results. + * + * @param string $message_type The message type to format. + * + * @return array + */ + public function format( $message_type = 'all' ) { + $data = array( + 'is_success' => $this->is_success(), + 'messages' => array(), + ); + + foreach ( $this->results as $result ) { + $step_name = $result->get_step_name(); + foreach ( $result->get_messages( $message_type ) as $message ) { + if ( ! isset( $data['messages'][ $message['type'] ] ) ) { + $data['messages'][ $message['type'] ] = array(); + } + $data['messages'][ $message['type'] ][] = array( + 'step' => $step_name, + 'type' => $message['type'], + 'message' => $message['message'], + ); + } + } + + return $data; + } + + /** + * Check if all results are successful. + * + * @return bool True if all results are successful, false otherwise. + */ + public function is_success() { + foreach ( $this->results as $result ) { + $is_success = $result->is_success(); + if ( ! $is_success ) { + return false; + } + } + return true; + } +} diff --git a/packages/php/blueprint/src/Schemas/JsonSchema.php b/packages/php/blueprint/src/Schemas/JsonSchema.php new file mode 100644 index 00000000000..c38b35c979a --- /dev/null +++ b/packages/php/blueprint/src/Schemas/JsonSchema.php @@ -0,0 +1,78 @@ +schema = $schema; + + if ( ! $this->validate() ) { + throw new \InvalidArgumentException( "Invalid JSON or missing 'steps' field." ); + } + } + + /** + * Returns the steps from the schema. + * + * @return array + */ + public function get_steps() { + return $this->schema->steps; + } + + /** + * Returns steps by name. + * + * @param string $name The name of the step. + * + * @return array + */ + public function get_step( $name ) { + $steps = array(); + foreach ( $this->schema->steps as $step ) { + if ( $step->step === $name ) { + $steps[] = $step; + } + } + + return $steps; + } + + /** + * Just makes sure that the JSON contains 'steps' field. + * + * We're going to validate 'steps' later because we can't know the exact schema + * ahead of time. 3rd party plugins can add their step processors. + * + * @return bool[ + */ + public function validate() { + if ( json_last_error() !== JSON_ERROR_NONE ) { + return false; + } + + if ( ! isset( $this->schema->steps ) ) { + return false; + } + + return true; + } +} diff --git a/packages/php/blueprint/src/Schemas/ZipSchema.php b/packages/php/blueprint/src/Schemas/ZipSchema.php new file mode 100644 index 00000000000..ef3d3d3fe54 --- /dev/null +++ b/packages/php/blueprint/src/Schemas/ZipSchema.php @@ -0,0 +1,74 @@ +unzip_path = $unzip_path ?? $this->wp_upload_dir()['path']; + + // Attempt to unzip the file. + $unzip_result = $this->wp_unzip_file( $zip_path, $this->unzip_path ); + if ( $unzip_result instanceof \WP_Error ) { + throw new \Exception( $unzip_result->get_error_message() ); + } + + // Determine the name of the unzipped directory. + $unzipped_dir_name = str_replace( '.zip', '', basename( $zip_path ) ); + + // Define the paths to the JSON file and the unzipped directory. + $json_path = "{$this->unzip_path}/{$unzipped_dir_name}/woo-blueprint.json"; + $this->unzipped_path = "{$this->unzip_path}/{$unzipped_dir_name}"; + + // Check if the JSON file exists in the expected location. + if ( ! file_exists( $json_path ) ) { + // Update paths if the JSON file is in the unzip root directory. + $this->unzipped_path = $this->unzip_path; + $json_path = "{$this->unzip_path}/woo-blueprint.json"; + } + + parent::__construct( $json_path ); + } + + /** + * Get the path to the unzipped file. + * + * @return mixed|string + */ + public function get_unzipped_path() { + return $this->unzipped_path; + } +} diff --git a/packages/php/blueprint/src/StepProcessor.php b/packages/php/blueprint/src/StepProcessor.php new file mode 100644 index 00000000000..a97f62e5f66 --- /dev/null +++ b/packages/php/blueprint/src/StepProcessor.php @@ -0,0 +1,24 @@ +success = $success; + $this->step_name = $step_name; + } + + /** + * Get messages. + * + * @param string $step_name The name of the step. + * + * @return void + */ + public function set_step_name( $step_name ) { + $this->step_name = $step_name; + } + + /** + * Create a new instance with $success = true. + * + * @param string $stp_name The name of the step. + * + * @return StepProcessorResult + */ + public static function success( string $stp_name ): self { + return ( new self( true, $stp_name ) ); + } + + + /** + * Add a new message. + * + * @param string $message message. + * @param string $type one of error, info. + * + * @throws InvalidArgumentException When incorrect type is given. + * @return void + */ + public function add_message( string $message, string $type = 'error' ) { + if ( ! in_array( $type, self::MESSAGE_TYPES, true ) ) { + // phpcs:ignore + throw new InvalidArgumentException( "{$type} is not allowed. Type must be one of " . implode( ',', self::MESSAGE_TYPES ) ); + } + + $this->messages[] = compact( 'message', 'type' ); + } + + /** + * Add a new error message. + * + * @param string $message message. + * + * @return void + */ + public function add_error( string $message ) { + $this->add_message( $message ); + } + + /** + * Add a new debug message. + * + * @param string $message message. + * + * @return void + */ + public function add_debug( string $message ) { + $this->add_message( $message, 'debug' ); + } + + + /** + * Add a new info message. + * + * @param string $message message. + * + * @return void + */ + public function add_info( string $message ) { + $this->add_message( $message, 'info' ); + } + + /** + * Filter messages. + * + * @param string $type one of all, error, and info. + * + * @return array + */ + public function get_messages( string $type = 'all' ): array { + if ( 'all' === $type ) { + return $this->messages; + } + + return array_filter( + $this->messages, + function ( $message ) use ( $type ) { + return $type === $message['type']; + } + ); + } + + /** + * Check to see if the result was success. + * + * @return bool + */ + public function is_success(): bool { + return true === $this->success && 0 === count( $this->get_messages( 'error' ) ); + } + + /** + * Get the name of the step. + * + * @return string The name of the step. + */ + public function get_step_name() { + return $this->step_name; + } +} diff --git a/packages/php/blueprint/src/Steps/ActivatePlugin.php b/packages/php/blueprint/src/Steps/ActivatePlugin.php new file mode 100644 index 00000000000..e081e0f02b9 --- /dev/null +++ b/packages/php/blueprint/src/Steps/ActivatePlugin.php @@ -0,0 +1,69 @@ +plugin_name = $plugin_name; + } + + /** + * Returns the name of this step. + * + * @return string The step name. + */ + public static function get_step_name(): string { + return 'activatePlugin'; + } + + /** + * Returns the schema for the JSON representation of this step. + * + * @param int $version The version of the schema to return. + * @return array The schema array. + */ + public static function get_schema( int $version = 1 ): array { + return array( + 'type' => 'object', + 'properties' => array( + 'step' => array( + 'type' => 'string', + 'enum' => array( static::get_step_name() ), + ), + 'pluginName' => array( + 'type' => 'string', + ), + ), + 'required' => array( 'step', 'pluginName' ), + ); + } + + /** + * Prepares an associative array for JSON encoding. + * + * @return array Array of data to be encoded as JSON. + */ + public function prepare_json_array(): array { + return array( + 'step' => static::get_step_name(), + 'pluginName' => $this->plugin_name, + ); + } +} diff --git a/packages/php/blueprint/src/Steps/ActivateTheme.php b/packages/php/blueprint/src/Steps/ActivateTheme.php new file mode 100644 index 00000000000..6a234bea7d8 --- /dev/null +++ b/packages/php/blueprint/src/Steps/ActivateTheme.php @@ -0,0 +1,69 @@ +theme_name = $theme_name; + } + + /** + * Returns the name of this step. + * + * @return string The step name. + */ + public static function get_step_name(): string { + return 'activateTheme'; + } + + /** + * Returns the schema for the JSON representation of this step. + * + * @param int $version The version of the schema to return. + * @return array The schema array. + */ + public static function get_schema( int $version = 1 ): array { + return array( + 'type' => 'object', + 'properties' => array( + 'step' => array( + 'type' => 'string', + 'enum' => array( static::get_step_name() ), + ), + 'themeName' => array( + 'type' => 'string', + ), + ), + 'required' => array( 'step', 'themeName' ), + ); + } + + /** + * Prepares an associative array for JSON encoding. + * + * @return array Array of data to be encoded as JSON. + */ + public function prepare_json_array(): array { + return array( + 'step' => static::get_step_name(), + 'themeName' => $this->theme_name, + ); + } +} diff --git a/packages/php/blueprint/src/Steps/DeactivatePlugin.php b/packages/php/blueprint/src/Steps/DeactivatePlugin.php new file mode 100644 index 00000000000..cb606e0d5bb --- /dev/null +++ b/packages/php/blueprint/src/Steps/DeactivatePlugin.php @@ -0,0 +1,68 @@ +plugin_name = $plugin_name; + } + + /** + * Get the step name. + * + * @return string + */ + public static function get_step_name(): string { + return 'deactivatePlugin'; + } + + /** + * Get the schema for this step. + * + * @param int $version The schema version. + * + * @return array + */ + public static function get_schema( int $version = 1 ): array { + return array( + 'type' => 'object', + 'properties' => array( + 'step' => array( + 'type' => 'string', + 'enum' => array( static::get_step_name() ), + ), + 'pluginName' => array( + 'type' => 'string', + ), + ), + 'required' => array( 'step', 'pluginName' ), + ); + } + + /** + * Prepare the JSON array for this step. + * + * @return array + */ + public function prepare_json_array(): array { + return array( + 'step' => static::get_step_name(), + 'pluginName' => $this->plugin_name, + ); + } +} diff --git a/packages/php/blueprint/src/Steps/DeletePlugin.php b/packages/php/blueprint/src/Steps/DeletePlugin.php new file mode 100644 index 00000000000..eb47b7a4575 --- /dev/null +++ b/packages/php/blueprint/src/Steps/DeletePlugin.php @@ -0,0 +1,69 @@ +plugin_name = $plugin_name; + } + + /** + * Returns the name of this step. + * + * @return string The step name. + */ + public static function get_step_name(): string { + return 'deletePlugin'; + } + + /** + * Returns the schema for the JSON representation of this step. + * + * @param int $version The version of the schema to return. + * @return array The schema array. + */ + public static function get_schema( int $version = 1 ): array { + return array( + 'type' => 'object', + 'properties' => array( + 'step' => array( + 'type' => 'string', + 'enum' => array( static::get_step_name() ), + ), + 'pluginName' => array( + 'type' => 'string', + ), + ), + 'required' => array( 'step', 'pluginName' ), + ); + } + + /** + * Prepares an associative array for JSON encoding. + * + * @return array Array representation of this step. + */ + public function prepare_json_array(): array { + return array( + 'step' => static::get_step_name(), + 'pluginName' => $this->plugin_name, + ); + } +} diff --git a/packages/php/blueprint/src/Steps/InstallPlugin.php b/packages/php/blueprint/src/Steps/InstallPlugin.php new file mode 100644 index 00000000000..5d483e18921 --- /dev/null +++ b/packages/php/blueprint/src/Steps/InstallPlugin.php @@ -0,0 +1,113 @@ +slug = $slug; + $this->resource = $resource; + $this->options = $options; + } + + /** + * Prepares an associative array for JSON encoding. + * + * @return array Array representing this installation step. + */ + public function prepare_json_array(): array { + return array( + 'step' => static::get_step_name(), + 'pluginZipFile' => array( + 'resource' => $this->resource, + 'slug' => $this->slug, + ), + 'options' => $this->options, + ); + } + + /** + * Returns the schema for the JSON representation of this step. + * + * @param int $version The version of the schema to return. + * @return array The schema array. + */ + public static function get_schema( int $version = 1 ): array { + return array( + 'type' => 'object', + 'properties' => array( + 'step' => array( + 'type' => 'string', + 'enum' => array( static::get_step_name() ), + ), + 'pluginZipFile' => array( + 'type' => 'object', + 'properties' => array( + 'resource' => array( + 'type' => 'string', + ), + 'slug' => array( + 'type' => 'string', + ), + ), + 'required' => array( 'resource', 'slug' ), + ), + 'options' => array( + 'type' => 'object', + 'properties' => array( + 'activate' => array( + 'type' => 'boolean', + ), + ), + ), + ), + 'required' => array( 'step', 'pluginZipFile' ), + ); + } + + /** + * Returns the name of this step. + * + * @return string The step name. + */ + public static function get_step_name(): string { + return 'installPlugin'; + } +} diff --git a/packages/php/blueprint/src/Steps/InstallTheme.php b/packages/php/blueprint/src/Steps/InstallTheme.php new file mode 100644 index 00000000000..bd6b18aa5f7 --- /dev/null +++ b/packages/php/blueprint/src/Steps/InstallTheme.php @@ -0,0 +1,113 @@ +slug = $slug; + $this->resource = $resource; + $this->options = $options; + } + + /** + * Prepares an associative array for JSON encoding. + * + * @return array The JSON-encoded array representing this installation step. + */ + public function prepare_json_array(): array { + return array( + 'step' => static::get_step_name(), + 'themeZipFile' => array( + 'resource' => $this->resource, + 'slug' => $this->slug, + ), + 'options' => $this->options, + ); + } + + /** + * Returns the schema for the JSON representation of this step. + * + * @param int $version The version of the schema to return. + * @return array The schema array. + */ + public static function get_schema( int $version = 1 ): array { + return array( + 'type' => 'object', + 'properties' => array( + 'step' => array( + 'type' => 'string', + 'enum' => array( static::get_step_name() ), + ), + 'themeZipFile' => array( + 'type' => 'object', + 'properties' => array( + 'resource' => array( + 'type' => 'string', + ), + 'slug' => array( + 'type' => 'string', + ), + ), + 'required' => array( 'resource', 'slug' ), + ), + 'options' => array( + 'type' => 'object', + 'properties' => array( + 'activate' => array( + 'type' => 'boolean', + ), + ), + ), + ), + 'required' => array( 'step', 'themeZipFile' ), + ); + } + + /** + * Returns the name of this step. + * + * @return string The step name. + */ + public static function get_step_name(): string { + return 'installTheme'; + } +} diff --git a/packages/php/blueprint/src/Steps/SetSiteOptions.php b/packages/php/blueprint/src/Steps/SetSiteOptions.php new file mode 100644 index 00000000000..20cdcb59068 --- /dev/null +++ b/packages/php/blueprint/src/Steps/SetSiteOptions.php @@ -0,0 +1,69 @@ +options = $options; + } + + /** + * Get the name of the step. + * + * @return string step name + */ + public static function get_step_name(): string { + return 'setSiteOptions'; + } + + /** + * Get the schema for the step. + * + * @param int $version schema version. + * + * @return array schema for the step + */ + public static function get_schema( int $version = 1 ): array { + return array( + 'type' => 'object', + 'properties' => array( + 'step' => array( + 'type' => 'string', + 'enum' => array( static::get_step_name() ), + ), + 'options' => array( + 'type' => 'object', + 'additionalProperties' => new \stdClass(), + ), + ), + 'required' => array( 'step', 'options' ), + ); + } + + /** + * Prepare the step for JSON serialization. + * + * @return array array representation of the step + */ + public function prepare_json_array(): array { + return array( + 'step' => static::get_step_name(), + 'options' => $this->options, + ); + } +} diff --git a/packages/php/blueprint/src/Steps/Step.php b/packages/php/blueprint/src/Steps/Step.php new file mode 100644 index 00000000000..552ccfb8229 --- /dev/null +++ b/packages/php/blueprint/src/Steps/Step.php @@ -0,0 +1,68 @@ +meta_values = $meta_values; + } + + /** + * Get the JSON array for the step. + * + * @return mixed + */ + public function get_json_array() { + $json_array = $this->prepare_json_array(); + if ( ! empty( $this->meta_values ) ) { + $json_array['meta'] = $this->meta_values; + } + return $json_array; + } +} diff --git a/packages/php/blueprint/src/UsePluginHelpers.php b/packages/php/blueprint/src/UsePluginHelpers.php new file mode 100644 index 00000000000..ba59932ac62 --- /dev/null +++ b/packages/php/blueprint/src/UsePluginHelpers.php @@ -0,0 +1,111 @@ +wp_get_plugins(); + + // Loop through all plugins to find the one with the specified slug. + foreach ( $all_plugins as $plugin_path => $plugin_info ) { + // Check if the plugin path contains the slug. + if ( strpos( $plugin_path, $slug . '/' ) === 0 ) { + // Deactivate the plugin. + return $this->wp_activate_plugin( $plugin_path ); + } + } + return false; + } + + /** + * Check if a plugin with the specified slug is installed. + * + * @param string $slug The slug of the plugin to check. + * + * @return bool + */ + public function is_plugin_dir( $slug ) { + $all_plugins = $this->wp_get_plugins(); + foreach ( $all_plugins as $plugin_file => $plugin_data ) { + // Extract the directory name from the plugin file path + $plugin_dir = explode( '/', $plugin_file )[0]; + + // Check for an exact match with the slug + if ( $plugin_dir === $slug ) { + return true; + } + } + return false; + } + + /** + * Deactivate and delete a plugin by its slug. + * + * Searches for the plugin with the specified slug in the installed plugins, + * deactivates it if active, and then deletes it. + * + * @param string $slug The slug of the plugin to delete. + * + * @return bool True if the plugin was deleted, false otherwise. + */ + public function delete_plugin_by_slug( $slug ) { + // Get all installed plugins. + $all_plugins = $this->wp_get_plugins(); + + // Loop through all plugins to find the one with the specified slug. + foreach ( $all_plugins as $plugin_path => $plugin_info ) { + // Check if the plugin path contains the slug. + if ( strpos( $plugin_path, $slug . '/' ) === 0 ) { + // Deactivate the plugin. + if ( $this->deactivate_plugin_by_slug( $slug ) ) { + // Delete the plugin. + return $this->wp_delete_plugins( array( $plugin_path ) ); + } + } + } + return false; + } + + /** + * Deactivate a plugin by its slug. + * + * Searches for the plugin with the specified slug in the installed plugins + * and deactivates it. + * + * @param string $slug The slug of the plugin to deactivate. + * + * @return bool True if the plugin was deactivated, false otherwise. + */ + public function deactivate_plugin_by_slug( $slug ) { + // Get all installed plugins. + $all_plugins = $this->wp_get_plugins(); + + // Loop through all plugins to find the one with the specified slug. + foreach ( $all_plugins as $plugin_path => $plugin_info ) { + // Check if the plugin path contains the slug. + if ( strpos( $plugin_path, $slug . '/' ) === 0 ) { + // Deactivate the plugin. + deactivate_plugins( $plugin_path ); + + // Check if the plugin has been deactivated. + if ( ! is_plugin_active( $plugin_path ) ) { + return true; + } + } + } + return false; + } +} diff --git a/packages/php/blueprint/src/UseWPFunctions.php b/packages/php/blueprint/src/UseWPFunctions.php new file mode 100644 index 00000000000..51c0155e6d0 --- /dev/null +++ b/packages/php/blueprint/src/UseWPFunctions.php @@ -0,0 +1,287 @@ +filesystem_initialized ) { + if ( ! class_exists( 'WP_Filesystem' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + + WP_Filesystem(); + $this->filesystem_initialized = true; + } + + return true; + } + + /** + * Unzips a file to a specified location. + * + * @param string $path Path to the ZIP file. + * @param string $to Destination directory. + * @return bool|WP_Error True on success, WP_Error on failure. + */ + public function wp_unzip_file( $path, $to ) { + $this->wp_init_filesystem(); + return unzip_file( $path, $to ); + } + + /** + * Retrieves the upload directory information. + * + * @return array Array of upload directory information. + */ + public function wp_upload_dir() { + return \wp_upload_dir(); + } + + /** + * Retrieves the root directory of the current theme. + * + * @return string The root directory of the current theme. + */ + public function wp_get_theme_root() { + return \get_theme_root(); + } + + /** + * Checks if a variable is a WP_Error. + * + * @param mixed $thing Variable to check. + * @return bool True if the variable is a WP_Error, false otherwise. + */ + public function is_wp_error( $thing ) { + return is_wp_error( $thing ); + } + + /** + * Downloads a file from a URL. + * + * @param string $url The URL of the file to download. + * @return string|WP_Error The local file path on success, WP_Error on failure. + */ + public function wp_download_url( $url ) { + if ( ! function_exists( 'download_url' ) ) { + include ABSPATH . '/wp-admin/includes/file.php'; + } + return download_url( $url ); + } + + /** + * Alias for WP_Filesystem::put_contents(). + * + * @param string $file_path The path to the file to write. + * @param mixed $content The data to write to the file. + * + * @return mixed + */ + public function wp_filesystem_put_contents( $file_path, $content ) { + global $wp_filesystem; + $this->wp_init_filesystem(); + + return $wp_filesystem->put_contents( $file_path, $content ); + } +} diff --git a/packages/php/blueprint/src/Util.php b/packages/php/blueprint/src/Util.php new file mode 100644 index 00000000000..5e38b45a8da --- /dev/null +++ b/packages/php/blueprint/src/Util.php @@ -0,0 +1,133 @@ + $value ) { + $new_key = $callback( $key, $value ); + $result[ $new_key ] = $value; + } + return $result; + } + + /** + * Check to see if given string is a valid WordPress plugin slug. + * + * @param string $slug The slug to be validated. + * + * @return bool + */ + public static function is_valid_wp_plugin_slug( $slug ) { + // Check if the slug only contains allowed characters. + if ( preg_match( '/^[a-z0-9-]+$/', $slug ) ) { + return true; + } + + return false; + } + + /** + * Recursively delete a directory. + * + * @param string $dir_path The path to the directory. + * + * @return void + * @throws \InvalidArgumentException If $dir_path is not a directory. + */ + public static function delete_dir( $dir_path ) { + if ( ! is_dir( $dir_path ) ) { + throw new \InvalidArgumentException( "$dir_path must be a directory" ); + } + if ( substr( $dir_path, strlen( $dir_path ) - 1, 1 ) !== '/' ) { + $dir_path .= '/'; + } + $files = glob( $dir_path . '*', GLOB_MARK ); + foreach ( $files as $file ) { + if ( is_dir( $file ) ) { + static::delete_dir( $file ); + } else { + // phpcs:ignore + unlink( $file ); + } + } + // phpcs:ignore + rmdir( $dir_path ); + } +} diff --git a/packages/php/blueprint/src/ZipExportedSchema.php b/packages/php/blueprint/src/ZipExportedSchema.php new file mode 100644 index 00000000000..106e28c497c --- /dev/null +++ b/packages/php/blueprint/src/ZipExportedSchema.php @@ -0,0 +1,225 @@ +schema = $schema; + + $this->dir = $this->get_default_destination_dir(); + $this->destination = null === $destination ? $this->dir . '/woo-blueprint.zip' : Util::ensure_wp_content_path( $destination ); + $this->working_dir = $this->dir . '/' . gmdate( 'Ymd' ) . '_' . time(); + + if ( ! class_exists( 'PclZip' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-pclzip.php'; + } + } + + /** + * Returns the full path for a file in the working directory. + * + * @param string $filename The name of the file. + * @return string Full path to the file. + */ + protected function get_working_dir_path( $filename ) { + return $this->working_dir . '/' . $filename; + } + + /** + * Creates a directory if it does not exist. + * + * @param string $dir Directory path. + */ + protected function maybe_create_dir( $dir ) { + if ( ! is_dir( $dir ) ) { + // phpcs:ignore + mkdir( $dir, 0777, true ); + } + } + + /** + * Creates a ZIP archive of the schema and resources. + * + * @return string Path to the created ZIP file. + * @throws \Exception If there is an error creating the ZIP archive. + */ + public function zip() { + $this->maybe_create_dir( $this->working_dir ); + + // Create .json file. + $this->files[] = $this->create_json_schema_file(); + $this->files = array_merge( $this->files, $this->add_resource( InstallPlugin::get_step_name(), 'plugins' ) ); + $this->files = array_merge( $this->files, $this->add_resource( InstallTheme::get_step_name(), 'themes' ) ); + + $archive = new \PclZip( $this->destination ); + if ( $archive->create( $this->files, PCLZIP_OPT_REMOVE_PATH, $this->working_dir ) === 0 ) { + throw new \Exception( 'Error : ' . $archive->errorInfo( true ) ); + } + + $this->clean(); + + return $this->destination; + } + + /** + * Returns the default destination directory for the ZIP file. + * + * @return string Default destination directory path. + */ + protected function get_default_destination_dir() { + return WP_CONTENT_DIR . '/uploads/blueprint'; + } + + /** + * Finds steps in the schema matching the given step name. + * + * @param string $step_name Name of the step to find. + * @return array|null Array of matching steps, or null if none found. + */ + protected function find_steps( $step_name ) { + $steps = array_filter( + $this->schema['steps'], + function ( $step ) use ( $step_name ) { + return $step['step'] === $step_name; + } + ); + if ( count( $steps ) ) { + return $steps; + } + return null; + } + + /** + * Adds resources to the list of files for the ZIP archive. + * + * @param string $step Step name to find resources for. + * @param string $type Type of resources ('plugins' or 'themes'). + * @return array Array of file paths to include in the ZIP archive. + * + * @throws \Exception If there is an error creating the ZIP archive. + * @throws \InvalidArgumentException If the given slug is not a valid plugin or theme. + */ + protected function add_resource( $step, $type ) { + $steps = $this->find_steps( $step ); + if ( null === $steps ) { + return array(); + } + + $steps = array_filter( + $steps, + // phpcs:ignore + function ( $resource ) use ( $type ) { + if ( 'plugins' === $type ) { + return 'self/plugins' === $resource['pluginZipFile']['resource']; + } elseif ( 'themes' === $type ) { + return 'self/themes' === $resource['themeZipFile']['resource']; + } + + return false; + } + ); + + if ( count( $steps ) === 0 ) { + return array(); + } + + // Create 'plugins' or 'themes' directory. + $this->maybe_create_dir( $this->working_dir . '/' . $type ); + + $files = array(); + + foreach ( $steps as $step ) { + $resource = $step[ 'plugins' === $type ? 'pluginZipFile' : 'themeZipFile' ]; + if ( ! $this->is_plugin_dir( $resource['slug'] ) ) { + throw new \InvalidArgumentException( 'Invalid plugin slug: ' . $resource['slug'] ); + } + + $destination = $this->working_dir . '/' . $type . '/' . $resource['slug'] . '.zip'; + $plugin_dir = WP_CONTENT_DIR . '/' . $type . '/' . $resource['slug']; + if ( ! is_dir( $plugin_dir ) ) { + $plugin_dir = $plugin_dir . '.php'; + if ( ! file_exists( $plugin_dir ) ) { + continue; + } + } + $archive = new \PclZip( $destination ); + $result = $archive->create( $plugin_dir, PCLZIP_OPT_REMOVE_PATH, WP_CONTENT_DIR . '/' . $type ); + if ( 0 === $result ) { + throw new \Exception( $archive->errorInfo( true ) ); + } + $files[] = $destination; + } + + return $files; + } + + /** + * Creates a JSON file from the schema. + * + * @return string Path to the created JSON schema file. + */ + private function create_json_schema_file() { + $schema_file = $this->get_working_dir_path( 'woo-blueprint.json' ); + $this->wp_filesystem_put_contents( $schema_file, json_encode( $this->schema, JSON_PRETTY_PRINT ) ); + return $schema_file; + } + + /** + * Cleans up the working directory by deleting it. + */ + private function clean() { + Util::delete_dir( $this->working_dir ); + } +} diff --git a/packages/php/blueprint/src/docs/json-examples/activatePlugin.json b/packages/php/blueprint/src/docs/json-examples/activatePlugin.json new file mode 100644 index 00000000000..fdd0b1d0ad0 --- /dev/null +++ b/packages/php/blueprint/src/docs/json-examples/activatePlugin.json @@ -0,0 +1,4 @@ +{ + "step": "activatePlugin", + "pluginName": "woocommerce" +} diff --git a/packages/php/blueprint/src/docs/json-examples/deactivatePlugin.json b/packages/php/blueprint/src/docs/json-examples/deactivatePlugin.json new file mode 100644 index 00000000000..72a179a6a9e --- /dev/null +++ b/packages/php/blueprint/src/docs/json-examples/deactivatePlugin.json @@ -0,0 +1,4 @@ +{ + "step": "deactivatePlugin", + "pluginName": "woocommerce" +} diff --git a/packages/php/blueprint/src/docs/json-examples/deletePlugin.json b/packages/php/blueprint/src/docs/json-examples/deletePlugin.json new file mode 100644 index 00000000000..e1744740a28 --- /dev/null +++ b/packages/php/blueprint/src/docs/json-examples/deletePlugin.json @@ -0,0 +1,4 @@ +{ + "step": "deletePlugin", + "pluginName": "woocommerce" +} diff --git a/packages/php/blueprint/src/docs/json-examples/installPlugin.json b/packages/php/blueprint/src/docs/json-examples/installPlugin.json new file mode 100644 index 00000000000..5ab17a477e0 --- /dev/null +++ b/packages/php/blueprint/src/docs/json-examples/installPlugin.json @@ -0,0 +1,10 @@ +{ + "step": "installPlugin", + "pluginZipFile": { + "resource": "wordpress.org/plugins", + "slug": "akismet" + }, + "options": { + "activate": true + } +} \ No newline at end of file diff --git a/packages/php/blueprint/src/docs/json-examples/installTheme.json b/packages/php/blueprint/src/docs/json-examples/installTheme.json new file mode 100644 index 00000000000..57b3986e30b --- /dev/null +++ b/packages/php/blueprint/src/docs/json-examples/installTheme.json @@ -0,0 +1,10 @@ +{ + "step": "installTheme", + "themeZipFile": { + "resource": "wordpress.org/themes", + "slug": "twentytwenty" + }, + "options": { + "activate": true + } +} \ No newline at end of file diff --git a/packages/php/blueprint/src/docs/json-examples/setSiteOptions.json b/packages/php/blueprint/src/docs/json-examples/setSiteOptions.json new file mode 100644 index 00000000000..2c762a96d7c --- /dev/null +++ b/packages/php/blueprint/src/docs/json-examples/setSiteOptions.json @@ -0,0 +1,6 @@ +{ + "step": "setSiteOptions", + "options": { + "woocommerce_allow_tracking": true + } +} diff --git a/packages/php/blueprint/tests/TestCase.php b/packages/php/blueprint/tests/TestCase.php new file mode 100644 index 00000000000..1bf89658f04 --- /dev/null +++ b/packages/php/blueprint/tests/TestCase.php @@ -0,0 +1,24 @@ +makePartial(); + } + + return $mock; + } + + /** + * Test that it uses exporters passed to the constructor + * with the built-in exporters. + */ + public function test_it_uses_exporters_passed_to_the_constructor() { + $empty_exporter = new EmptySetSiteOptionsExporter(); + $mock = Mock( ExportSchema::class, array( array( $empty_exporter ) ) ); + $built_in_exporters = ( new BuiltInExporters() )->get_all(); + $mock->makePartial(); + // Make sure wooblueprint_exporters filter passes the empty exporter + built-in exporters. + // and then return only the empty exporter to test that it is used. + // We're removing the built-in exporters as some of them make network calls. + $mock->shouldReceive( 'wp_apply_filters' ) + ->with( 'wooblueprint_exporters', array_merge( array( $empty_exporter ), $built_in_exporters ) ) + ->andReturn( array( $empty_exporter ) ); + + $result = $mock->export(); + $this->assertCount( 1, $result['steps'] ); + $this->assertEquals( 'setSiteOptions', $result['steps'][0]['step'] ); + $this->assertEquals( array(), $result['steps'][0]['options'] ); + } + + /** + * Test that it correctly sets landingPage value from the filter. + */ + public function test_wooblueprint_export_landingpage_filter() { + $exporter = $this->get_mock( true ); + $exporter->shouldReceive( 'wp_apply_filters' ) + ->with( 'wooblueprint_exporters', Mockery::any() ) + ->andReturn( array() ); + + $exporter->shouldReceive( 'wp_apply_filters' ) + ->with( 'wooblueprint_export_landingpage', Mockery::any() ) + ->andReturn( 'test' ); + + $result = $exporter->export(); + $this->assertEquals( 'test', $result['landingPage'] ); + } + + /** + * Test that it uses the exporters from the filter. + * + * @return void + */ + public function test_wooblueprint_exporters_filter() { + } + + /** + * Test that it filters out exporters that are not in the list of steps to export. + * + * @return void + */ + public function test_it_only_uses_exporters_specified_by_steps_argment() { + } + + /** + * Test that it calls include_private_plugins method on ExportInstallPluginSteps when + * exporting a zip schema. + * + * @return void + */ + public function test_it_calls_include_private_plugins_for_zip_export() { + } +} diff --git a/packages/php/blueprint/tests/Unit/Schemas/JsonSchemaTest.php b/packages/php/blueprint/tests/Unit/Schemas/JsonSchemaTest.php new file mode 100644 index 00000000000..7e8ca9a7fcf --- /dev/null +++ b/packages/php/blueprint/tests/Unit/Schemas/JsonSchemaTest.php @@ -0,0 +1,48 @@ +get_fixture_path( 'empty-steps.json' ) ); + $steps = $schema->get_steps(); + $this->assertIsArray( $steps ); + $this->assertCount( 0, $steps ); + } + + /** + * Test getting a step from a schema. + * + * @return void + */ + public function test_get_step() { + $name = 'installPlugin'; + $schema = new JsonSchema( $this->get_fixture_path( 'with-install-plugin-step.json' ) ); + $steps = $schema->get_step( $name ); + $this->assertIsArray( $steps ); + foreach ( $steps as $step ) { + $this->assertEquals( $name, $step->step ); + } + } + + /** + * Test getting a step from a schema that does not exist. + * + * @return void + */ + public function test_it_throws_invalid_argument_exception_with_invalid_json() { + $this->expectException( \InvalidArgumentException::class ); + new JsonSchema( $this->get_fixture_path( 'invalid-json.json' ) ); + } +} diff --git a/packages/php/blueprint/tests/Unit/Schemas/ZipSchemaTest.php b/packages/php/blueprint/tests/Unit/Schemas/ZipSchemaTest.php new file mode 100644 index 00000000000..9af56d88a2e --- /dev/null +++ b/packages/php/blueprint/tests/Unit/Schemas/ZipSchemaTest.php @@ -0,0 +1,34 @@ +expectException( \Exception::class ); + new ZipSchema( $this->get_fixture_path( 'invalid-zip.zip' ) ); + } + + /** + * Test unzipping a zip file. + * + * @return void + * @throws \Exception If the zip file is invalid. + */ + public function test_unzip() { + $schema = new ZipSchema( $this->get_fixture_path( 'zipped-schema.zip' ) ); + $unzipped_path = $schema->get_unzipped_path(); + $this->assertEquals( wp_upload_dir()['path'], $unzipped_path ); + } +} diff --git a/packages/php/blueprint/tests/Unit/ZipExportedSchemaTest.php b/packages/php/blueprint/tests/Unit/ZipExportedSchemaTest.php new file mode 100644 index 00000000000..cfbc6d6e7c6 --- /dev/null +++ b/packages/php/blueprint/tests/Unit/ZipExportedSchemaTest.php @@ -0,0 +1,29 @@ +expectException( \InvalidArgumentException::class ); + // phpcs:ignore + $json = json_decode( file_get_contents( $this->get_fixture_path( 'install-plugin-with-invalid-slug.json' ) ), true ); + $mock = Mock( ZipExportedSchema::class, array( $json ) ); + $mock->makePartial(); + $mock->shouldAllowMockingProtectedMethods(); + $mock->shouldReceive( 'maybe_create_dir' )->andReturn( null ); + $mock->shouldReceive( 'wp_filesystem_put_contents' )->andReturn( null ); + $mock->zip(); + } +} diff --git a/packages/php/blueprint/tests/bootstrap.php b/packages/php/blueprint/tests/bootstrap.php new file mode 100644 index 00000000000..97fdc8f1f90 --- /dev/null +++ b/packages/php/blueprint/tests/bootstrap.php @@ -0,0 +1,21 @@ + { const [ isPanelSwitching, setIsPanelSwitching ] = useState( false ); const { fills } = useSlot( ABBREVIATED_NOTIFICATION_SLOT_NAME ); const hasExtendedNotifications = Boolean( fills?.length ); - const { updateUserPreferences, ...userData } = useUserPreferences(); const activeSetupList = useActiveSetupTasklist(); const { comingSoon } = useLaunchYourStore( { enabled: isHomescreen, @@ -168,7 +165,6 @@ export const ActivityPanel = ( { isEmbedded, query } ) => { const { hasUnreadNotes, hasAbbreviatedNotifications, - isCompletedTask, thingsToDoNextCount, requestingTaskListOptions, setupTaskListComplete, @@ -419,34 +415,8 @@ export const ActivityPanel = ( { isEmbedded, query } ) => { } }; - const closedHelpPanelHighlight = () => { - recordEvent( 'help_tooltip_click' ); - if ( userData && updateUserPreferences ) { - updateUserPreferences( { - help_panel_highlight_shown: 'yes', - } ); - } - }; - - const shouldShowHelpTooltip = () => { - const { task } = query; - const startedTasks = - userData && userData.task_list_tracked_started_tasks; - const highlightShown = userData && userData.help_panel_highlight_shown; - if ( - task && - highlightShown !== 'yes' && - ( startedTasks || {} )[ task ] > 1 && - ! isCompletedTask - ) { - return true; - } - return false; - }; - const tabs = getTabs(); const headerId = uniqueId( 'activity-panel-header_' ); - const showHelpHighlightTooltip = shouldShowHelpTooltip(); return ( @@ -483,28 +453,9 @@ export const ActivityPanel = ( { isEmbedded, query } ) => { clearPanel={ () => clearPanel() } /> - { showHelpHighlightTooltip ? ( - closedHelpPanelHighlight() } - onShow={ () => recordEvent( 'help_tooltip_view' ) } - /> - ) : null }
); }; -ActivityPanel.defaultProps = { - getHistory, -}; - export default ActivityPanel; diff --git a/plugins/woocommerce-admin/client/activity-panel/panels/help.js b/plugins/woocommerce-admin/client/activity-panel/panels/help.js index 2d29a5babb8..cf4bddb7638 100644 --- a/plugins/woocommerce-admin/client/activity-panel/panels/help.js +++ b/plugins/woocommerce-admin/client/activity-panel/panels/help.js @@ -15,7 +15,7 @@ import { SETTINGS_STORE_NAME, } from '@woocommerce/data'; import { compose } from 'redux'; -import { recordEvent } from '@woocommerce/tracks'; +import { recordEvent as fallbackRecordEvent } from '@woocommerce/tracks'; /** * Internal dependencies @@ -359,15 +359,18 @@ function getListItems( props ) { } ) ); } -export const HelpPanel = ( props ) => { - const { taskName } = props; +export const HelpPanel = ( { + taskName, + recordEvent = fallbackRecordEvent, + ...props +} ) => { useEffect( () => { - props.recordEvent( 'help_panel_open', { + recordEvent( 'help_panel_open', { task_name: taskName || 'homescreen', } ); - }, [ taskName ] ); + }, [ taskName, recordEvent ] ); - const listItems = getListItems( props ); + const listItems = getListItems( { taskName, recordEvent, ...props } ); return ( @@ -382,10 +385,6 @@ export const HelpPanel = ( props ) => { ); }; -HelpPanel.defaultProps = { - recordEvent, -}; - export default compose( withSelect( ( select ) => { const { getSettings } = select( SETTINGS_STORE_NAME ); diff --git a/plugins/woocommerce-admin/client/activity-panel/test/index.js b/plugins/woocommerce-admin/client/activity-panel/test/index.js index d364494557f..06db14fb6ab 100644 --- a/plugins/woocommerce-admin/client/activity-panel/test/index.js +++ b/plugins/woocommerce-admin/client/activity-panel/test/index.js @@ -9,7 +9,7 @@ import { createEvent, } from '@testing-library/react'; import { useSelect } from '@wordpress/data'; -import { useUser, useUserPreferences } from '@woocommerce/data'; +import { useUser } from '@woocommerce/data'; import { useState } from '@wordpress/element'; /** @@ -247,82 +247,6 @@ describe( 'Activity Panel', () => { expect( queryByText( 'Finish setup' ) ).toBeDefined(); } ); - describe( 'help panel tooltip', () => { - it( 'should render highlight tooltip when task count is at-least 2, task is not completed, and tooltip not shown yet', () => { - useUserPreferences.mockReturnValue( { - updateUserPreferences: () => {}, - task_list_tracked_started_tasks: { payment: 2 }, - } ); - const { getByText } = render( - - ); - - expect( getByText( '[HighlightTooltip]' ) ).toBeInTheDocument(); - } ); - - it( 'should not render highlight tooltip when task is not visited more then once', () => { - useSelect.mockImplementation( () => ( { - requestingTaskListOptions: false, - setupTaskListComplete: false, - setupTaskListHidden: false, - trackedCompletedTasks: [], - } ) ); - useUserPreferences.mockReturnValue( { - updateUserPreferences: () => {}, - task_list_tracked_started_tasks: { payment: 1 }, - } ); - render( - - ); - - expect( screen.queryByText( '[HighlightTooltip]' ) ).toBeNull(); - - useUserPreferences.mockReturnValue( { - updateUserPreferences: () => {}, - task_list_tracked_started_tasks: {}, - } ); - - render( - - ); - - expect( screen.queryByText( '[HighlightTooltip]' ) ).toBeNull(); - } ); - - it( 'should not render highlight tooltip when task is visited twice, but completed already', () => { - useSelect.mockImplementation( () => ( { - requestingTaskListOptions: false, - setupTaskListComplete: false, - setupTaskListHidden: false, - isCompletedTask: true, - } ) ); - - useUserPreferences.mockReturnValue( { - updateUserPreferences: () => {}, - task_list_tracked_started_tasks: { payment: 2 }, - } ); - - const { queryByText } = render( - - ); - - expect( queryByText( '[HighlightTooltip]' ) ).toBeNull(); - } ); - - it( 'should not render highlight tooltip when task is visited twice, not completed, but already shown', () => { - useUserPreferences.mockReturnValue( { - task_list_tracked_started_tasks: { payment: 2 }, - help_panel_highlight_shown: 'yes', - } ); - - const { queryByText } = render( - - ); - - expect( queryByText( '[HighlightTooltip]' ) ).toBeNull(); - } ); - } ); - describe( 'panel', () => { it( 'should set focus when panel opened/closed without removing element when onTransitionEnd is triggered', () => { const content = 'test'; diff --git a/plugins/woocommerce-admin/client/analytics/components/index.js b/plugins/woocommerce-admin/client/analytics/components/index.js index cc02f168d36..4ee3476b0b2 100644 --- a/plugins/woocommerce-admin/client/analytics/components/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/index.js @@ -1,4 +1,3 @@ export { default as ReportChart } from './report-chart'; -export { default as ReportError } from './report-error'; export { default as ReportSummary } from './report-summary'; export { default as ReportTable } from './report-table'; diff --git a/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js b/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js index 98e9bdbd9b3..2e8d406247f 100644 --- a/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js @@ -5,7 +5,7 @@ import { __ } from '@wordpress/i18n'; import { Card, CardBody, CardHeader } from '@wordpress/components'; import { Component } from '@wordpress/element'; import { compose } from '@wordpress/compose'; -import { EmptyTable, TableCard } from '@woocommerce/components'; +import { EmptyTable, AnalyticsError, TableCard } from '@woocommerce/components'; import { withSelect } from '@wordpress/data'; import PropTypes from 'prop-types'; import { getPersistedQuery } from '@woocommerce/navigation'; @@ -21,7 +21,6 @@ import { Text } from '@woocommerce/experimental'; /** * Internal dependencies */ -import ReportError from '../report-error'; import sanitizeHTML from '../../../lib/sanitize-html'; import './style.scss'; @@ -88,7 +87,7 @@ export class Leaderboard extends Component { const classes = 'woocommerce-leaderboard'; if ( isError ) { - return ; + return ; } const rows = this.getFormattedRows(); diff --git a/plugins/woocommerce-admin/client/analytics/components/report-chart/index.js b/plugins/woocommerce-admin/client/analytics/components/report-chart/index.js index 31f714c3d91..a19830c0b44 100644 --- a/plugins/woocommerce-admin/client/analytics/components/report-chart/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/report-chart/index.js @@ -8,7 +8,7 @@ import { format as formatDate } from '@wordpress/date'; import { withSelect } from '@wordpress/data'; import { get, isEqual } from 'lodash'; import PropTypes from 'prop-types'; -import { Chart } from '@woocommerce/components'; +import { Chart, AnalyticsError } from '@woocommerce/components'; import { getReportChartData, getTooltipValueFormat, @@ -27,7 +27,6 @@ import { CurrencyContext } from '@woocommerce/currency'; /** * Internal dependencies */ -import ReportError from '../report-error'; import { getChartMode, getSelectedFilter, @@ -197,7 +196,7 @@ export class ReportChart extends Component { const { isRequesting, primaryData } = this.props; if ( primaryData.isError ) { - return ; + return ; } const isChartRequesting = isRequesting || primaryData.isRequesting; @@ -214,7 +213,7 @@ export class ReportChart extends Component { const { isRequesting, primaryData, secondaryData } = this.props; if ( ! primaryData || primaryData.isError || secondaryData.isError ) { - return ; + return ; } const isChartRequesting = diff --git a/plugins/woocommerce-admin/client/analytics/components/report-summary/index.js b/plugins/woocommerce-admin/client/analytics/components/report-summary/index.js index 57eefcd17f2..ac1b509619b 100644 --- a/plugins/woocommerce-admin/client/analytics/components/report-summary/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/report-summary/index.js @@ -8,6 +8,7 @@ import { withSelect } from '@wordpress/data'; import PropTypes from 'prop-types'; import { getNewPath } from '@woocommerce/navigation'; import { + AnalyticsError, SummaryList, SummaryListPlaceholder, SummaryNumber, @@ -21,7 +22,6 @@ import { CurrencyContext } from '@woocommerce/currency'; /** * Internal dependencies */ -import ReportError from '../report-error'; /** * Component to render summary numbers in reports. @@ -64,7 +64,7 @@ export class ReportSummary extends Component { const { isError, isRequesting } = summaryData; if ( isError ) { - return ; + return ; } if ( isRequesting ) { @@ -138,7 +138,7 @@ ReportSummary.propTypes = { * The endpoint to use in API calls to populate the Summary Numbers. * For example, if `taxes` is provided, data will be fetched from the report * `taxes` endpoint (ie: `/wc-analytics/reports/taxes/stats`). If the provided endpoint - * doesn't exist, an error will be shown to the user with `ReportError`. + * doesn't exist, an error will be shown to the user with `AnalyticsError`. */ endpoint: PropTypes.string.isRequired, /** diff --git a/plugins/woocommerce-admin/client/analytics/components/report-summary/test/index.js b/plugins/woocommerce-admin/client/analytics/components/report-summary/test/index.js index fa51e9abde8..697071d8b69 100644 --- a/plugins/woocommerce-admin/client/analytics/components/report-summary/test/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/report-summary/test/index.js @@ -124,7 +124,7 @@ describe( 'ReportSummary', () => { expect( delta ).toBeInTheDocument(); } ); - test( 'should display ReportError when isError is true', () => { + test( 'should display AnalyticsError when isError is true', () => { renderChart( 'number', null, null, true ); expect( @@ -134,7 +134,7 @@ describe( 'ReportSummary', () => { ).toBeInTheDocument(); } ); - test( 'should display SummaryListPlaceholder when isRequesting is true', () => { + test( 'should display SummaryListPlaceholder when summaryData.isRequesting is true', () => { const { container } = renderChart( 'number', null, null, false, true ); expect( 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 89f020645ce..b486e059fdc 100644 --- a/plugins/woocommerce-admin/client/analytics/components/report-table/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/report-table/index.js @@ -7,11 +7,16 @@ import { Fragment, useRef, useState } from '@wordpress/element'; import { compose } from '@wordpress/compose'; import { focus } from '@wordpress/dom'; import { withDispatch, withSelect } from '@wordpress/data'; -import { get, noop, partial, uniq } from 'lodash'; +import { get, 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 { + CompareButton, + AnalyticsError, + Search, + TableCard, +} from '@woocommerce/components'; import { getIdsFromQuery, getSearchWords, @@ -38,7 +43,6 @@ import { recordEvent } from '@woocommerce/tracks'; * Internal dependencies */ import DownloadIcon from './download-icon'; -import ReportError from '../report-error'; import { extendTableData } from './utils'; import './style.scss'; @@ -50,17 +54,23 @@ const ReportTable = ( props ) => { getRowsContent, getSummary, isRequesting, - primaryData, - tableData, + primaryData = {}, + tableData = { + items: { + data: [], + totalResults: 0, + }, + query: {}, + }, endpoint, // These props are not used in the render function, but are destructured // so they are not included in the `tableProps` variable. // eslint-disable-next-line no-unused-vars itemIdField, - // eslint-disable-next-line no-unused-vars - tableQuery, + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + tableQuery = {}, compareBy, - compareParam, + compareParam = 'filter', searchBy, labels = {}, ...tableProps @@ -83,7 +93,7 @@ const ReportTable = ( props ) => { const isError = tableData.isError || primaryData.isError; if ( isError ) { - return ; + return ; } let userPrefColumns = []; @@ -249,7 +259,7 @@ const ReportTable = ( props ) => { }; const onSearchChange = ( values ) => { - const { baseSearchQuery, addCesSurveyForCustomerSearch } = props; + const { baseSearchQuery = {}, addCesSurveyForCustomerSearch } = props; // A comma is used as a separator between search terms, so we want to escape // any comma they contain. const searchTerms = values.map( ( v ) => @@ -483,7 +493,7 @@ ReportTable.propTypes = { * For example, if `taxes` is provided, data will be fetched from the report * `taxes` endpoint (ie: `/wc-analytics/reports/taxes` and `/wc/v4/reports/taxes/stats`). * If the provided endpoint doesn't exist, an error will be shown to the user - * with `ReportError`. + * with `AnalyticsError`. */ endpoint: PropTypes.string, /** @@ -542,7 +552,7 @@ ReportTable.propTypes = { * Table data of that report. If it's not provided, it will be automatically * loaded via the provided `endpoint`. */ - tableData: PropTypes.object.isRequired, + tableData: PropTypes.object, /** * Properties to be added to the query sent to the report table endpoint. */ @@ -553,22 +563,6 @@ ReportTable.propTypes = { title: PropTypes.string.isRequired, }; -ReportTable.defaultProps = { - primaryData: {}, - tableData: { - items: { - data: [], - totalResults: 0, - }, - query: {}, - }, - tableQuery: {}, - compareParam: 'filter', - downloadable: false, - onSearch: noop, - baseSearchQuery: {}, -}; - const EMPTY_ARRAY = []; const EMPTY_OBJECT = {}; diff --git a/plugins/woocommerce-admin/client/analytics/report/categories/index.js b/plugins/woocommerce-admin/client/analytics/report/categories/index.js index 904f1e1bac1..02110c765da 100644 --- a/plugins/woocommerce-admin/client/analytics/report/categories/index.js +++ b/plugins/woocommerce-admin/client/analytics/report/categories/index.js @@ -69,7 +69,6 @@ class CategoriesReport extends Component { Order # ', 'woocommerce' @@ -210,7 +210,7 @@ export const advancedFilters = applyFilters( 'Select an IP address filter match', 'woocommerce' ), - /* translators: A sentence describing a order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */ + /* translators: A sentence describing an order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */ title: __( 'IP Address ', 'woocommerce' diff --git a/plugins/woocommerce-admin/client/analytics/report/index.js b/plugins/woocommerce-admin/client/analytics/report/index.js index 85118becdbf..3a7da1d481d 100644 --- a/plugins/woocommerce-admin/client/analytics/report/index.js +++ b/plugins/woocommerce-admin/client/analytics/report/index.js @@ -8,6 +8,7 @@ import PropTypes from 'prop-types'; import { find } from 'lodash'; import { getQuery, getSearchWords } from '@woocommerce/navigation'; import { searchItemsByString, ITEMS_STORE_NAME } from '@woocommerce/data'; +import { AnalyticsError } from '@woocommerce/components'; import { CurrencyContext, getFilteredCurrencyInstance, @@ -18,7 +19,6 @@ import { */ import './style.scss'; import { NoMatch } from '~/layout/NoMatch'; -import ReportError from '../components/report-error'; import getReports from './get-reports'; /** @@ -83,7 +83,7 @@ class Report extends Component { const { isError } = this.props; if ( isError ) { - return ; + return ; } const reportParam = getReportParam( this.props ); diff --git a/plugins/woocommerce-admin/client/analytics/report/products/index.js b/plugins/woocommerce-admin/client/analytics/report/products/index.js index b3d7d893998..bab9201f385 100644 --- a/plugins/woocommerce-admin/client/analytics/report/products/index.js +++ b/plugins/woocommerce-admin/client/analytics/report/products/index.js @@ -6,6 +6,7 @@ import { Component, Fragment } from '@wordpress/element'; import { compose } from '@wordpress/compose'; import PropTypes from 'prop-types'; import { ITEMS_STORE_NAME } from '@woocommerce/data'; +import { AnalyticsError } from '@woocommerce/components'; import { withSelect } from '@wordpress/data'; /** @@ -15,7 +16,6 @@ import { advancedFilters, charts, filters } from './config'; import getSelectedChart from '../../../lib/get-selected-chart'; import ProductsReportTable from './table'; import ReportChart from '../../components/report-chart'; -import ReportError from '../../components/report-error'; import ReportSummary from '../../components/report-summary'; import VariationsReportTable from '../variations/table'; import ReportFilters from '../../components/report-filters'; @@ -57,7 +57,7 @@ class ProductsReport extends Component { this.props; if ( isError ) { - return ; + return ; } const chartQuery = { @@ -82,7 +82,6 @@ class ProductsReport extends Component { mode={ mode } charts={ charts } endpoint="products" - isRequesting={ isRequesting } query={ chartQuery } selectedChart={ getSelectedChart( query.chart, charts ) } filters={ filters } diff --git a/plugins/woocommerce-admin/client/analytics/report/taxes/index.js b/plugins/woocommerce-admin/client/analytics/report/taxes/index.js index 1b089e790c0..493ca5e30b8 100644 --- a/plugins/woocommerce-admin/client/analytics/report/taxes/index.js +++ b/plugins/woocommerce-admin/client/analytics/report/taxes/index.js @@ -52,7 +52,6 @@ class TaxesReport extends Component { { const { path, query, isError, isRequesting } = props; if ( isError ) { - return ; + return ; } const chartQuery = { @@ -59,7 +59,6 @@ const VariationsReport = ( props ) => { mode={ mode } charts={ charts } endpoint="variations" - isRequesting={ isRequesting } query={ chartQuery } selectedChart={ getSelectedChart( query.chart, charts ) } filters={ filters } diff --git a/plugins/woocommerce-admin/client/blueprint/index.js b/plugins/woocommerce-admin/client/blueprint/index.js new file mode 100644 index 00000000000..b00a8c8b588 --- /dev/null +++ b/plugins/woocommerce-admin/client/blueprint/index.js @@ -0,0 +1,4 @@ +/** + * Internal dependencies + */ +export { registerBlueprintSlotfill } from './settings/slotfill'; diff --git a/plugins/woocommerce-admin/client/blueprint/settings/slotfill.js b/plugins/woocommerce-admin/client/blueprint/settings/slotfill.js new file mode 100644 index 00000000000..2807f9fa801 --- /dev/null +++ b/plugins/woocommerce-admin/client/blueprint/settings/slotfill.js @@ -0,0 +1,262 @@ +/** + * External dependencies + */ +import { createSlotFill, Button, Notice } from '@wordpress/components'; +import { getAdminLink } from '@woocommerce/settings'; + +import apiFetch from '@wordpress/api-fetch'; + +import { useState, createElement, useEffect } from '@wordpress/element'; +import { registerPlugin } from '@wordpress/plugins'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { SETTINGS_SLOT_FILL_CONSTANT } from '../../settings/settings-slots'; +import './style.scss'; + +const { Fill } = createSlotFill( SETTINGS_SLOT_FILL_CONSTANT ); + +const Blueprint = () => { + const [ exportEnabled, setExportEnabled ] = useState( true ); + const [ exportAsZip, setExportAsZip ] = useState( false ); + const [ error, setError ] = useState( null ); + + const steps = { + Settings: 'setWCSettings', + 'Core Profiler Options': 'setWCCoreProfilerOptions', + 'Payment Gateways': 'setWCPaymentGateways', + Shipping: 'setWCShipping', + 'Tax rates': 'setWCTaxRates', + Plugins: 'installPlugin', + Themes: 'installTheme', + 'Task Options': 'setWCTaskOptions', + }; + + const instructions = { + Settings: + 'It includes all the settings in General, Accounts & Privacy, Emails, Integration, Site Visibility, Advanced.', + 'Core Profiler Options': 'It in includes the setup wizard options.', + 'Payment Gateways': + 'It includes all the settings in WooCommerce | Settings | Payments.', + Shipping: + 'It includes all the settings in WooCommerce | Settings | Shipping.', + 'Tax rates': + 'It includes all the settings in WooCommerce | Settings | Tax.', + Plugins: 'It includes the active plugins on the site.', + Themes: 'It includes the active theme on the site.', + 'Task Options': 'It includes the state of the setup task list.', + }; + // Initialize state to keep track of checkbox values + const [ checkedState, setCheckedState ] = useState( + Object.keys( steps ).reduce( ( acc, key ) => { + acc[ key ] = true; + return acc; + }, {} ) + ); + + const exportBlueprint = async ( _steps ) => { + setExportEnabled( false ); + + const linkContainer = document.getElementById( + 'download-link-container' + ); + linkContainer.innerHTML = ''; + + try { + const response = await apiFetch( { + path: '/blueprint/export', + method: 'POST', + data: { + steps: _steps, + export_as_zip: exportAsZip, + }, + } ); + const link = document.createElement( 'a' ); + link.innerHTML = + 'Click here in case download does not start automatically'; + + let url = null; + + if ( response.type === 'zip' ) { + link.href = response.data; + link.target = '_blank'; + } else { + // Create a link element and trigger the download + url = window.URL.createObjectURL( + new Blob( [ JSON.stringify( response.data, null, 2 ) ] ) + ); + link.href = url; + link.setAttribute( 'download', 'woo-blueprint.json' ); + } + + linkContainer.appendChild( link ); + + link.click(); + if ( url ) { + window.URL.revokeObjectURL( url ); + } + } catch ( e ) { + setError( e.message ); + } + + setExportEnabled( true ); + }; + + // Handle checkbox change + const handleOnChange = ( key ) => { + setCheckedState( ( prevState ) => ( { + ...prevState, + [ key ]: ! prevState[ key ], + } ) ); + }; + + useEffect( () => { + const saveButton = document.getElementsByClassName( + 'woocommerce-save-button' + )[ 0 ]; + if ( saveButton ) { + saveButton.style.display = 'none'; + } + } ); + return ( +
+ { error && ( + { + setError( null ); + } } + isDismissible + > + { error } + + ) } +

+ { __( + 'Blueprints are setup files that contain all the installation instructions. including plugins, themes and settings. Ease the setup process, allow teams to apply each others’ changes and much more.', + 'woocommerce' + ) } +

+

+ + Please{ ' ' } + + complete the survey + { ' ' } + to help shape the direction of this feature! + +

+

Import

+

+ You can import the schema on the{ ' ' } + + builder setup page + + { ', or use the import WP CLI command ' } +
+ wp wc blueprint import path-to-woo-blueprint.json. +

+

+

Export

+

+ Choose the items to include in your blueprint file, or use the + export WP CLI command
+ wp wc blueprint export save-to-path.json{ ' ' } +

+ { Object.entries( steps ).map( ( [ key, value ] ) => ( +
+ handleOnChange( key ) } + /> + +

+ { instructions[ key ] } +

+
+ ) ) } + + +

Options

+
+ { + setExportAsZip( ! exportAsZip ); + } } + /> + +
+

+ +

Export may take some time depending on your network speed.

+
+ ); +}; + +const BlueprintSlotfill = () => { + return ( + + + + ); +}; + +export const registerBlueprintSlotfill = () => { + registerPlugin( 'woocommerce-admin-blueprint-settings-slotfill', { + scope: 'woocommerce-blueprint-settings', + render: BlueprintSlotfill, + } ); +}; diff --git a/plugins/woocommerce-admin/client/blueprint/settings/style.scss b/plugins/woocommerce-admin/client/blueprint/settings/style.scss new file mode 100644 index 00000000000..f901ef196b0 --- /dev/null +++ b/plugins/woocommerce-admin/client/blueprint/settings/style.scss @@ -0,0 +1,38 @@ +.blueprint-settings-slotfill { + label { + cursor: pointer; + } + + button.is-primary { + margin-bottom: 7px; + } + button.is-link { + text-decoration: none; + &:active, + &:focus { + outline: none; + box-shadow: none; + } + } + + #download-link-container { + margin-bottom: 10px; + a { + text-decoration: none; + } + } + + .woo-blueprint-export-step-desc { + margin-top: 0.25em; + margin-bottom: 1em; + color: #828282; + } + + h3 { + margin-top: 1.5em; + } + + p { + max-width: 550px; + } +} diff --git a/plugins/woocommerce-admin/client/core-profiler/events.tsx b/plugins/woocommerce-admin/client/core-profiler/events.tsx index 84c717d17bf..b0b3819561c 100644 --- a/plugins/woocommerce-admin/client/core-profiler/events.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/events.tsx @@ -18,7 +18,10 @@ export type InitializationCompleteEvent = { payload: { optInDataSharing: boolean }; }; -export type IntroOptInEvent = IntroCompletedEvent | IntroSkippedEvent; +export type IntroOptInEvent = + | IntroCompletedEvent + | IntroSkippedEvent + | IntroBuilderEvent; export type IntroCompletedEvent = { type: 'INTRO_COMPLETED'; @@ -117,6 +120,11 @@ export type RedirectToWooHomeEvent = { type: 'REDIRECT_TO_WOO_HOME'; }; +export type IntroBuilderEvent = { + type: 'INTRO_BUILDER'; + payload: { optInDataSharing: false }; +}; // always false for now + export type CoreProfilerEvents = | InitializationCompleteEvent | IntroOptInEvent @@ -130,4 +138,5 @@ export type CoreProfilerEvents = | PluginsInstallationCompletedEvent | PluginsInstallationCompletedWithErrorsEvent | ExternalUrlUpdateEvent - | RedirectToWooHomeEvent; + | RedirectToWooHomeEvent + | IntroBuilderEvent; diff --git a/plugins/woocommerce-admin/client/core-profiler/index.tsx b/plugins/woocommerce-admin/client/core-profiler/index.tsx index 19f97c3684d..428566a4578 100644 --- a/plugins/woocommerce-admin/client/core-profiler/index.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/index.tsx @@ -57,6 +57,7 @@ import { POSSIBLY_DEFAULT_STORE_NAMES, } from './pages/BusinessInfo'; import { BusinessLocation } from './pages/BusinessLocation'; +import { BuilderIntro } from './pages/BuilderIntro'; import { getCountryStateOptions } from './services/country'; import { CoreProfilerLoader } from './components/loader/Loader'; import { Plugins } from './pages/Plugins'; @@ -300,7 +301,6 @@ const exitToWooHome = fromPromise( async () => { } ); const redirectToJetpackAuthPage = ( { - context, event, }: { context: CoreProfilerStateMachineContext; @@ -308,16 +308,7 @@ const redirectToJetpackAuthPage = ( { } ) => { const url = new URL( event.output.url ); url.searchParams.set( 'installed_ext_success', '1' ); - const selectedPlugin = context.pluginsSelected.find( - ( plugin ) => plugin === 'jetpack' || plugin === 'jetpack-boost' - ); - - if ( selectedPlugin ) { - const pluginName = - selectedPlugin === 'jetpack' ? 'jetpack-ai' : 'jetpack-boost'; - url.searchParams.set( 'plugin_name', pluginName ); - } - + url.searchParams.set( 'plugin_name', 'jetpack-ai' ); window.location.href = url.toString(); }; @@ -748,6 +739,13 @@ export const coreProfilerStateMachineDefinition = createMachine( { params: { step: 'skip-guided-setup' }, }, }, + { + target: '#introBuilder', + guard: { + type: 'hasStepInUrl', + params: { step: 'intro-builder' }, + }, + }, { target: 'introOptIn', }, @@ -831,6 +829,13 @@ export const coreProfilerStateMachineDefinition = createMachine( { } ), ], }, + INTRO_BUILDER: { + target: '#introBuilder', + actions: [ + 'assignOptInDataSharing', + 'updateTrackingOption', + ], + }, }, meta: { progress: 20, @@ -1143,6 +1148,30 @@ export const coreProfilerStateMachineDefinition = createMachine( { }, }, }, + introBuilder: { + id: 'introBuilder', + initial: 'uploadConfig', + entry: [ + { type: 'updateQueryStep', params: { step: 'intro-builder' } }, + ], + states: { + uploadConfig: { + meta: { + component: BuilderIntro, + }, + on: { + INTRO_SKIPPED: { + // if the user skips the intro, we set the optInDataSharing to false and go to the Business Location page + target: '#skipGuidedSetup', + actions: [ + 'assignOptInDataSharing', + 'updateTrackingOption', + ], + }, + }, + }, + }, + }, skipGuidedSetup: { id: 'skipGuidedSetup', initial: 'preSkipFlowBusinessLocation', @@ -1560,9 +1589,7 @@ export const CoreProfilerController = ( { hasJetpackSelectedForInstallation: ( { context } ) => { return ( context.pluginsSelected.find( - ( plugin ) => - plugin === 'jetpack' || - plugin === 'jetpack-boost' + ( plugin ) => plugin === 'jetpack' ) !== undefined ); }, @@ -1570,9 +1597,7 @@ export const CoreProfilerController = ( { return ( context.pluginsAvailable.find( ( plugin: Extension ) => - ( plugin.key === 'jetpack' || - plugin.key === 'jetpack-boost' ) && - plugin.is_activated + plugin.key === 'jetpack' && plugin.is_activated ) !== undefined ); }, diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/BuilderIntro.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/BuilderIntro.tsx new file mode 100644 index 00000000000..6e68b4421fc --- /dev/null +++ b/plugins/woocommerce-admin/client/core-profiler/pages/BuilderIntro.tsx @@ -0,0 +1,129 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Button } from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import { Navigation } from '../components/navigation/navigation'; +import { IntroOptInEvent } from '../events'; + +/** + * This is a temporary page for testing the blueprint import functionality. + * This will be replaced with more user-friendly design in the future. + */ +export const BuilderIntro = ( { + sendEvent, + navigationProgress = 80, +}: { + sendEvent: ( event: IntroOptInEvent ) => void; + navigationProgress: number; +} ) => { + const [ file, setFile ] = useState( null ); + const [ message, setMessage ] = useState( '' ); + const [ importing, setImporting ] = useState( false ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleFileChange = ( event: any ) => { + setFile( event.target.files[ 0 ] ); + }; + + const handleUpload = () => { + if ( ! file ) { + setMessage( 'Please select a file first.' ); + return; + } + + setImporting( true ); + + const formData = new FormData(); + formData.append( 'file', file ); + + if ( window?.wcSettings?.admin?.blueprint_upload_nonce ) { + formData.append( + 'blueprint_upload_nonce', + window.wcSettings.admin.blueprint_upload_nonce + ); + } + + apiFetch( { + path: '/blueprint/import', + method: 'POST', + body: formData, + } ) + .then( ( data ) => { + // @ts-expect-error tmp + if ( data.status === 'success' ) { + setMessage( + 'Schema imported successfully. Redirecting to ' + + // @ts-expect-error tmp + data.data.redirect + ); + + setImporting( false ); + + window.setTimeout( () => { + // @ts-expect-error tmp + window.location.href = data.data.redirect; + }, 2000 ); + } else { + setImporting( false ); + // @ts-expect-error tmp + setMessage( `Error: ${ data.message }` ); + // @ts-expect-error tmp + if ( data?.data?.result ) { + setMessage( + // @ts-expect-error tmp + JSON.stringify( data.data.result, null, 2 ) + ); + } + } + } ) + .catch( ( error ) => { + setImporting( false ); + setMessage( `Error: ${ error.message }` ); + } ); + }; + return ( + <> + + sendEvent( { + type: 'INTRO_SKIPPED', + payload: { optInDataSharing: false }, + } ) + } + /> +
+

+ { __( + 'Upload your Blueprint to provision your site', + 'woocommerce' + ) }{ ' ' } +

+ + + +
+
{ message }
+
+
+ + ); +}; diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx index 8a682cee24a..7d28a8e1395 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx @@ -90,7 +90,7 @@ export const selectIndustryMapping = { 'woocommerce' ), im_setting_up_a_store_for_a_client: __( - "Which industry is your client's business in?", + 'Which industry is your client’s business in?', 'woocommerce' ), }; @@ -238,7 +238,7 @@ export const BusinessInfo = ( { 'woocommerce' ) } subTitle={ __( - "We'll use this information to help you set up payments, shipping, and taxes, as well as recommending the best theme for your store.", + 'We’ll use this information to help you set up payments, shipping, and taxes, as well as recommending the best theme for your store.', 'woocommerce' ) } /> @@ -268,7 +268,7 @@ export const BusinessInfo = ( { />

{ __( - "Don't worry — you can always change it later!", + 'Don’t worry — you can always change it later!', 'woocommerce' ) }

@@ -390,7 +390,7 @@ export const BusinessInfo = ( { { createInterpolateElement( __( // translators: first tag is filled with the country name detected by geolocation, second tag is the country name selected by the user - "It looks like you're located in . Are you sure you want to create a store in ?", + 'It looks like you’re located in . Are you sure you want to create a store in ?', 'woocommerce' ), { diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/BusinessLocation.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/BusinessLocation.tsx index 71b7b3676e4..ab0160ddf09 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/BusinessLocation.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/BusinessLocation.tsx @@ -45,7 +45,7 @@ export const BusinessLocation = ( { 'woocommerce' ) } subTitle={ __( - "We'll use this information to help you set up payments, shipping, and taxes.", + 'We’ll use this information to help you set up payments, shipping, and taxes.', 'woocommerce' ) } /> diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/IntroOptIn.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/IntroOptIn.tsx index d46649a854a..e8861744aaa 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/IntroOptIn.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/IntroOptIn.tsx @@ -45,7 +45,7 @@ export const IntroOptIn = ( { title={ __( 'Welcome to Woo!', 'woocommerce' ) } subTitle={ interpolateComponents( { mixedString: __( - "It's great to have you here with us! We'll be guiding you through the setup process – first, answer a few questions to tailor your experience.", + 'It’s great to have you here with us! We’ll be guiding you through the setup process – first, answer a few questions to tailor your experience.', 'woocommerce' ), components: { @@ -65,7 +65,20 @@ export const IntroOptIn = ( { > { __( 'Set up my store', 'woocommerce' ) } - + { window.wcAdminFeatures?.blueprint && ( + + ) }
{ errorMessage }

) } -
+
{ context.pluginsAvailable.map( ( plugin ) => { const learnMoreLink = plugin.learn_more_link ? ( -
- -
- { pluginsWithAgreement.length > 0 && ( -

- { interpolateComponents( { - mixedString: sprintf( - /* translators: %s: a list of plugins, e.g. Jetpack */ - _n( - 'By installing %s plugin for free you agree to our {{link}}Terms of Service{{/link}}.', - 'By installing %s plugins for free you agree to our {{link}}Terms of Service{{/link}}.', - pluginsWithAgreement.length, - 'woocommerce' - ), - joinWithAnd( - pluginsWithAgreement.map( - ( plugin ) => plugin.name +

+
+ +
+ { pluginsWithAgreement.length > 0 && ( +

+ { interpolateComponents( { + mixedString: sprintf( + /* translators: %s: a list of plugins, e.g. Jetpack */ + _n( + 'By installing %s plugin for free you agree to our {{link}}Terms of Service{{/link}}.', + 'By installing %s plugins for free you agree to our {{link}}Terms of Service{{/link}}.', + pluginsWithAgreement.length, + 'woocommerce' + ), + joinWithAnd( + pluginsWithAgreement.map( + ( plugin ) => plugin.name + ) ) - ) - .map( composeListFormatParts ) - .join( '' ) - ), - components: { - span: , - link: ( - + .map( composeListFormatParts ) + .join( '' ) ), - }, - } ) } -

- ) } + components: { + span: , + link: ( + + ), + }, + } ) } +

+ ) } +
); diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/UserProfile.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/UserProfile.tsx index 87fd87058d8..b7980c993d4 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/UserProfile.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/UserProfile.tsx @@ -20,32 +20,32 @@ import { MultipleSelector } from '../components/multiple-selector/multiple-selec const businessOptions = [ { - title: __( "I'm just starting my business", 'woocommerce' ), + title: __( 'I’m just starting my business', 'woocommerce' ), value: 'im_just_starting_my_business' as const, }, { - title: __( "I'm already selling", 'woocommerce' ), + title: __( 'I’m already selling', 'woocommerce' ), value: 'im_already_selling' as const, }, { - title: __( "I'm setting up a store for a client", 'woocommerce' ), + title: __( 'I’m setting up a store for a client', 'woocommerce' ), value: 'im_setting_up_a_store_for_a_client' as const, }, ]; const sellingOnlineOptions = [ { - label: __( "Yes, I'm selling online", 'woocommerce' ), + label: __( 'Yes, I’m selling online', 'woocommerce' ), value: 'yes_im_selling_online' as const, key: 'yes_im_selling_online' as const, }, { - label: __( "No, I'm selling offline", 'woocommerce' ), + label: __( 'No, I’m selling offline', 'woocommerce' ), value: 'no_im_selling_offline' as const, key: 'no_im_selling_offline' as const, }, { - label: __( "I'm selling both online and offline", 'woocommerce' ), + label: __( 'I’m selling both online and offline', 'woocommerce' ), value: 'im_selling_both_online_and_offline' as const, key: 'im_selling_both_online_and_offline' as const, }, diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/tests/business-info.test.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/tests/business-info.test.tsx index cc00ca11ce8..e5618147188 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/tests/business-info.test.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/tests/business-info.test.tsx @@ -94,7 +94,7 @@ describe( 'BusinessInfo', () => { 'im_setting_up_a_store_for_a_client'; render( ); expect( - screen.getByText( /Which industry is your client's business in?/i ) + screen.getByText( /Which industry is your client’s business in?/i ) ).toBeInTheDocument(); } ); diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/tests/user-profile.test.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/tests/user-profile.test.tsx index b7382b672aa..fe888367891 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/tests/user-profile.test.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/tests/user-profile.test.tsx @@ -50,7 +50,7 @@ describe( 'UserProfile', () => { // @ts-ignore render( ); const radioInput = screen.getByLabelText< HTMLInputElement >( - "I'm already selling" + 'I’m already selling' ); // Replace with the label of your radio button // Perform the radio button selection @@ -65,7 +65,7 @@ describe( 'UserProfile', () => { expect( onlineSellingQuestion ).toBeInTheDocument(); } ); - it( 'should show online selling question when choosing "Yes, I\'m selling online"', () => { + it( 'should show online selling question when choosing "Yes, I’m selling online"', () => { render( // @ts-ignore 782px") {// sticky footer shouldn't apply to mobile layout + // since the height of each card is more or less fixed (actually its variable but we limit text to 2 lines), + // we can estimate the visual breakpoints that are required to "unstick" the footer from the bottom of the page. + &.rows-1 { + @media screen and ( max-height: 520px ) { + @include sticky-footer-scroll-padding; + } + } + &.rows-2 { + @media screen and ( max-height: 650px ) { + @include sticky-footer-scroll-padding; + } + } + &.rows-3 { + @media screen and ( max-height: 780px ) { + @include sticky-footer-scroll-padding; + } + } + &.rows-4 { + @media screen and ( max-height: 910px ) { + @include sticky-footer-scroll-padding; + } + } + } } .woocommerce-profiler-plugins-continue-button { @@ -380,9 +410,46 @@ .woocommerce-profiler-plugins-jetpack-agreement { color: $gray-700; font-size: 12px; - a { - color: inherit; + } + + .woocommerce-profiler-plugins__footer { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + + @mixin plugins-sticky-footer { + background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #fff 44.69%); + height: $sticky-footer-height; + position: fixed; + bottom: 0; } + + @include breakpoint(">782px") { // sticky footer shouldn't apply to mobile layout + // since the height of each card is more or less fixed (actually its variable but we limit text to 2 lines), + // we can estimate the visual breakpoints that are required to "unstick" the footer from the bottom of the page. + &.rows-1 { + @media screen and ( max-height: 520px ) { + @include plugins-sticky-footer; + } + } + &.rows-2 { + @media screen and ( max-height: 650px ) { + @include plugins-sticky-footer; + } + } + &.rows-3 { + @media screen and ( max-height: 780px ) { + @include plugins-sticky-footer; + } + } + &.rows-4 { + @media screen and ( max-height: 910px ) { + @include plugins-sticky-footer; + } + } + } + } .woocommerce-profiler-plugins-continue-button-container { @@ -632,3 +699,15 @@ padding-top: 32px !important; } } + +.woocommerce-profiler-builder-intro { + display: flex; + flex-direction: column; + padding: 30px; + gap: 20px; + align-items: center; +} + +.woocommerce-profiler-builder-intro-file-input { + margin-left: 100px; +} diff --git a/plugins/woocommerce-admin/client/core-profiler/utils/get-loader-stage-meta.tsx b/plugins/woocommerce-admin/client/core-profiler/utils/get-loader-stage-meta.tsx index 996d4c22f96..699e6dbb5fd 100644 --- a/plugins/woocommerce-admin/client/core-profiler/utils/get-loader-stage-meta.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/utils/get-loader-stage-meta.tsx @@ -28,7 +28,7 @@ const LightbulbStage = { ], }; const LayoutStage = { - title: __( "Extending your store's capabilities", 'woocommerce' ), + title: __( 'Extending your store’s capabilities', 'woocommerce' ), image: loader-lightbulb, paragraphs: [ { @@ -42,7 +42,7 @@ const LayoutStage = { }; const DevelopingStage = { - title: __( "Woo! Let's get your features ready", 'woocommerce' ), + title: __( 'Woo! Let’s get your features ready', 'woocommerce' ), image: loader-developng, paragraphs: [ { diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/auto-block-preview.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/auto-block-preview.tsx index 03f7c2d42d1..a962d373431 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/auto-block-preview.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/auto-block-preview.tsx @@ -246,6 +246,7 @@ function ScaledBlockPreview( { // @ts-ignore disabled prop exists scrolling={ isScrollable ? 'yes' : 'no' } tabIndex={ -1 } + canEnableZoomOutView={ true } readonly={ ! isFullComposabilityFeatureAndAPIAvailable() } diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/auto-block-preview-event-listener.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/auto-block-preview-event-listener.ts index c9cfa1ea2aa..1aa7ed1aece 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/auto-block-preview-event-listener.ts +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/auto-block-preview-event-listener.ts @@ -175,9 +175,10 @@ const addInertToAssemblerPatterns = ( const addInertToAllInnerBlocks = ( documentElement: HTMLElement ) => { const body = documentElement.ownerDocument.body; const observerChildList = new window.MutationObserver( () => { - const parentBlocks = body.getElementsByClassName( - 'block-editor-block-list__layout' - )[ 0 ].children; + const parentBlocks = + body.getElementsByClassName( + 'block-editor-block-list__layout' + )[ 0 ]?.children ?? []; for ( const parentBlock of parentBlocks ) { parentBlock.setAttribute( 'data-is-parent-block', 'true' ); @@ -440,7 +441,7 @@ export const useAddAutoBlockPreviewEventListenersAndObservers = ( unsubscribeCallbacks.push( removeEventListenerHidePopover ); } - // Add event listner to the button which will insert a default pattern + // Add event listener to the button which will insert a default pattern // when there are no patterns inserted in the block preview. const removePatternButtonClickListener = addPatternButtonClickListener( documentElement, diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-insert-pattern.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-insert-pattern.ts index 36674e292c3..b8b231836fd 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-insert-pattern.ts +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-insert-pattern.ts @@ -39,7 +39,7 @@ export const useInsertPattern = () => { currentTemplate?.id ?? '' ); - const blockToScroll = useRef< string | null >( null ); + const insertedPatternRef = useRef< string | null >( null ); // @ts-expect-error No types for this exist yet. const { insertBlocks } = useDispatch( blockEditorStore ); @@ -78,7 +78,7 @@ export const useInsertPattern = () => { undefined, false ); - blockToScroll.current = updatedBlocks[ 0 ].clientId; + insertedPatternRef.current = updatedBlocks[ 0 ].clientId; } else { const updatedBlocks = findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate( @@ -97,7 +97,7 @@ export const useInsertPattern = () => { undefined, false ); - blockToScroll.current = updatedBlocks[ 0 ].clientId; + insertedPatternRef.current = updatedBlocks[ 0 ].clientId; } trackEvent( @@ -110,5 +110,6 @@ export const useInsertPattern = () => { return { insertPattern, + insertedPattern: insertedPatternRef, }; }; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx index 375095a4ece..b2c43506ea5 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx @@ -45,12 +45,12 @@ import { addFilter } from '@wordpress/hooks'; import { CustomizeStoreComponent } from '../types'; import { Layout } from './layout'; import './style.scss'; -import { PreloadFonts } from './preload-fonts'; import { GoBackWarningModal } from './go-back-warning-modal'; import { onBackButtonClicked } from '../utils'; import { getNewPath } from '@woocommerce/navigation'; import useBodyClass from '../hooks/use-body-class'; - +import { OptInSubscribe } from './opt-in/opt-in'; +import { OptInContextProvider } from './opt-in/context'; import './tracking'; const { RouterProvider } = unlock( routerPrivateApis ); @@ -181,12 +181,14 @@ export const AssemblerHub: CustomizeStoreComponent = ( props ) => { ) } - - - - - - + + + + + + + + diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/opt-in/context.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/opt-in/context.tsx new file mode 100644 index 00000000000..a2efc98c311 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/opt-in/context.tsx @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { OPTIONS_STORE_NAME } from '@woocommerce/data'; +import { useSelect } from '@wordpress/data'; +import React, { createContext, useState } from '@wordpress/element'; +import type { ReactNode } from 'react'; + +export const enum OPTIN_FLOW_STATUS { + 'IDLE' = 'IDLE', + 'LOADING' = 'LOADING', + 'DONE' = 'DONE', +} + +export const OptInContext = createContext< { + optInFlowStatus: OPTIN_FLOW_STATUS; + setOptInFlowStatus: ( status: OPTIN_FLOW_STATUS ) => void; +} >( { + optInFlowStatus: OPTIN_FLOW_STATUS.IDLE, + setOptInFlowStatus: () => {}, +} ); + +export const OptInContextProvider = ( { + children, +}: { + children: ReactNode; +} ) => { + const isAllowTrackingEnabled = useSelect( + ( select ) => + select( OPTIONS_STORE_NAME ).getOption( + 'woocommerce_allow_tracking' + ) === 'yes', + [] + ); + + const [ optInFlowStatus, setOptInFlowStatus ] = + useState< OPTIN_FLOW_STATUS >( + isAllowTrackingEnabled + ? OPTIN_FLOW_STATUS.DONE + : OPTIN_FLOW_STATUS.IDLE + ); + + return ( + + { children } + + ); +}; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/opt-in/opt-in.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/opt-in/opt-in.tsx new file mode 100644 index 00000000000..584b9d7bcd7 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/opt-in/opt-in.tsx @@ -0,0 +1,161 @@ +/** + * External dependencies + */ +import { OPTIONS_STORE_NAME } from '@woocommerce/data'; +import apiFetch from '@wordpress/api-fetch'; +import { resolveSelect, select, subscribe, useDispatch } from '@wordpress/data'; +import { useContext, useEffect } from '@wordpress/element'; +// @ts-expect-error No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { store as coreStore } from '@wordpress/core-data'; +// @ts-expect-error No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +// @ts-expect-error No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; + +/** + * Internal dependencies + */ +import { FontFamily, FontFace } from '../../types/font'; +import { usePatterns } from '../hooks/use-patterns'; +import { installFontFamilies } from '../utils/fonts'; +import { FONT_FAMILIES_TO_INSTALL } from '../sidebar/global-styles/font-pairing-variations/constants'; +import { OptInContext, OPTIN_FLOW_STATUS } from './context'; + +const { useGlobalSetting } = unlock( blockEditorPrivateApis ); + +export const OptInSubscribe = () => { + const { setOptInFlowStatus } = useContext( OptInContext ); + + const [ enabledFontFamilies, setFontFamilies ]: [ + { + custom: Array< FontFamily >; + theme: Array< FontFamily >; + }, + ( font: { + custom: Array< FontFamily >; + theme: Array< FontFamily >; + } ) => void + ] = useGlobalSetting( 'typography.fontFamilies' ); + + const { + // @ts-expect-error No types for this exist yet. + __experimentalSaveSpecifiedEntityEdits: saveSpecifiedEntityEdits, + } = useDispatch( coreStore ); + + const installPatterns = async () => { + await apiFetch< { + success: boolean; + } >( { + path: '/wc-admin/patterns', + method: 'POST', + } ); + }; + + const installFonts = async () => { + await installFontFamilies(); + + const globalStylesId = + // @ts-expect-error No types for this exist yet. + select( coreStore ).__experimentalGetCurrentGlobalStylesId(); + + const installedFontFamilies = ( await resolveSelect( + coreStore + // @ts-expect-error No types for this exist yet. + ).getEntityRecords( 'postType', 'wp_font_family', { + _embed: true, + per_page: -1, + } ) ) as Array< { + id: number; + font_family_settings: FontFamily; + _embedded: { + font_faces: Array< { + font_face_settings: FontFace; + } >; + }; + } >; + + const parsedInstalledFontFamilies = ( installedFontFamilies || [] ).map( + ( fontFamilyPost ) => { + return { + id: fontFamilyPost.id, + ...fontFamilyPost.font_family_settings, + fontFace: + fontFamilyPost?._embedded?.font_faces.map( + ( face ) => face.font_face_settings + ) || [], + }; + } + ); + + const { custom } = enabledFontFamilies; + + const enabledFontSlugs = [ + ...( custom ? custom.map( ( font ) => font.slug ) : [] ), + ]; + + const fontFamiliesToEnable = parsedInstalledFontFamilies.reduce( + ( acc, font ) => { + if ( + enabledFontSlugs.includes( font.slug ) || + FONT_FAMILIES_TO_INSTALL[ font.slug ] === undefined + ) { + return acc; + } + + return [ + ...acc, + { + ...font, + }, + ]; + }, + [] as Array< FontFamily > + ); + + setFontFamilies( { + ...enabledFontFamilies, + custom: [ + ...( enabledFontFamilies.custom ?? [] ), + ...( fontFamiliesToEnable ?? [] ), + ], + } ); + + saveSpecifiedEntityEdits( 'root', 'globalStyles', globalStylesId, [ + 'settings.typography.fontFamilies', + ] ); + }; + + const { invalidateCache } = usePatterns(); + + useEffect( () => { + const unsubscribe = subscribe( async () => { + const isOptedIn = + select( OPTIONS_STORE_NAME ).getOption( + 'woocommerce_allow_tracking' + ) === 'yes'; + + if ( isOptedIn ) { + setOptInFlowStatus( OPTIN_FLOW_STATUS.LOADING ); + await installPatterns(); + invalidateCache(); + await installFonts(); + + setOptInFlowStatus( OPTIN_FLOW_STATUS.DONE ); + + unsubscribe(); + } + // @ts-expect-error The type is not updated. + }, OPTIONS_STORE_NAME ); + + return () => { + unsubscribe(); + }; + // We don't want to run this effect on every render, only once because it is a subscription. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [] ); + + return null; +}; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/preload-fonts.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/preload-fonts.tsx index 25624729f1b..9ee86a64894 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/preload-fonts.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/preload-fonts.tsx @@ -2,9 +2,6 @@ /** * External dependencies */ -// @ts-expect-error No types for this exist yet. -import { store as coreStore } from '@wordpress/core-data'; -import { useSelect, useDispatch } from '@wordpress/data'; import { privateApis as blockEditorPrivateApis, // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -16,26 +13,18 @@ import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; /** * Internal dependencies */ -import { - FONT_PAIRINGS, - FONT_FAMILIES_TO_INSTALL, -} from './sidebar/global-styles/font-pairing-variations/constants'; +import { FONT_PAIRINGS } from './sidebar/global-styles/font-pairing-variations/constants'; import { FontFamiliesLoader } from './sidebar/global-styles/font-pairing-variations/font-families-loader'; -import { useContext, useEffect, useMemo } from '@wordpress/element'; -import { FontFace, FontFamily } from '../types/font'; +import { useContext, useMemo } from '@wordpress/element'; +import { FontFamily } from '../types/font'; import { FontFamiliesLoaderDotCom } from './sidebar/global-styles/font-pairing-variations/font-families-loader-dot-com'; import { CustomizeStoreContext } from '.'; import { isAIFlow, isNoAIFlow } from '../guards'; const { useGlobalSetting } = unlock( blockEditorPrivateApis ); -let areFontsPreloaded = false; - export const PreloadFonts = () => { - // @ts-expect-error No types for this exist yet. - const { __experimentalSaveSpecifiedEntityEdits: saveSpecifiedEntityEdits } = - useDispatch( coreStore ); - const [ enabledFontFamilies, setFontFamilies ]: [ + const [ enabledFontFamilies ]: [ { custom: Array< FontFamily >; theme: Array< FontFamily >; @@ -55,99 +44,6 @@ export const PreloadFonts = () => { const { context } = useContext( CustomizeStoreContext ); - const { globalStylesId, installedFontFamilies } = useSelect( ( select ) => { - // @ts-expect-error No types for this exist yet. - const { __experimentalGetCurrentGlobalStylesId, getEntityRecords } = - select( coreStore ); - return { - globalStylesId: __experimentalGetCurrentGlobalStylesId(), - installedFontFamilies: getEntityRecords( - 'postType', - 'wp_font_family', - { _embed: true, per_page: -1 } - ) as Array< { - id: number; - font_family_settings: FontFamily; - _embedded: { - font_faces: Array< { - font_face_settings: FontFace; - } >; - }; - } >, - }; - } ); - - const parsedInstalledFontFamilies = useMemo( () => { - return ( - ( installedFontFamilies || [] ).map( ( fontFamilyPost ) => { - return { - id: fontFamilyPost.id, - ...fontFamilyPost.font_family_settings, - fontFace: - fontFamilyPost?._embedded?.font_faces.map( - ( face ) => face.font_face_settings - ) || [], - }; - } ) || [] - ); - }, [ installedFontFamilies ] ); - - useEffect( () => { - if ( - areFontsPreloaded || - installedFontFamilies === null || - enabledFontFamilies === null - ) { - return; - } - - const { custom } = enabledFontFamilies; - - const enabledFontSlugs = [ - ...( custom ? custom.map( ( font ) => font.slug ) : [] ), - ]; - - const fontFamiliesToEnable = parsedInstalledFontFamilies.reduce( - ( acc, font ) => { - if ( - enabledFontSlugs.includes( font.slug ) || - FONT_FAMILIES_TO_INSTALL[ font.slug ] === undefined - ) { - return acc; - } - - return [ - ...acc, - { - ...font, - }, - ]; - }, - [] as Array< FontFamily > - ); - - setFontFamilies( { - ...enabledFontFamilies, - custom: [ - ...( enabledFontFamilies.custom ?? [] ), - ...( fontFamiliesToEnable ?? [] ), - ], - } ); - - saveSpecifiedEntityEdits( 'root', 'globalStyles', globalStylesId, [ - 'settings.typography.fontFamilies', - ] ); - - areFontsPreloaded = true; - }, [ - enabledFontFamilies, - globalStylesId, - installedFontFamilies, - parsedInstalledFontFamilies, - saveSpecifiedEntityEdits, - setFontFamilies, - ] ); - const allFontChoices = FONT_PAIRINGS.map( ( fontPair ) => fontPair?.settings?.typography?.fontFamilies?.theme ); diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/index.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/index.tsx index 26c0a85c4a5..eb15bd471b4 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/index.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/index.tsx @@ -30,6 +30,10 @@ import { CustomizeStoreContext } from '~/customize-store/assembler-hub'; import { FlowType } from '~/customize-store/types'; import { FontFamily } from './font-families-loader-dot-com'; import { isAIFlow } from '~/customize-store/guards'; +import { + OptInContext, + OPTIN_FLOW_STATUS, +} from '~/customize-store/assembler-hub/opt-in/context'; export const FontPairing = () => { const { aiSuggestions, isLoading } = useSelect( ( select ) => { @@ -72,6 +76,8 @@ export const FontPairing = () => { ) === 'yes' ); + const { optInFlowStatus } = useContext( OptInContext ); + const fontPairings = useMemo( () => { if ( isAIFlow( context.flowType ) ) { return aiOnline && aiSuggestions?.lookAndFeel @@ -106,7 +112,15 @@ export const FontPairing = () => { } ); - if ( ! trackingAllowed || ! isFontLibraryAvailable ) { + // We only show the default fonts when: + // - user did not allow tracking + // - site doesn't have the Font Library available + // - opt-in flow is still processing + if ( + ! trackingAllowed || + ! isFontLibraryAvailable || + optInFlowStatus !== OPTIN_FLOW_STATUS.DONE + ) { return defaultFonts; } @@ -134,14 +148,15 @@ export const FontPairing = () => { }, [ aiOnline, aiSuggestions?.lookAndFeel, - baseFontFamilies, + baseFontFamilies.theme, context.flowType, custom, isFontLibraryAvailable, + optInFlowStatus, trackingAllowed, ] ); - if ( isLoading ) { + if ( isLoading || optInFlowStatus === OPTIN_FLOW_STATUS.LOADING ) { return (
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/sidebar-pattern-screen.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/sidebar-pattern-screen.tsx index be21f55418a..13a8b0862dc 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/sidebar-pattern-screen.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/sidebar-pattern-screen.tsx @@ -120,8 +120,6 @@ export const SidebarPatternScreen = ( { category }: { category: string } ) => { currentTemplate?.id ?? '' ); - const blockToScroll = useRef< string | null >( null ); - const isEditorLoading = useIsSiteEditorLoading(); const isSpinnerVisible = isLoading || isEditorLoading; @@ -156,6 +154,9 @@ export const SidebarPatternScreen = ( { category }: { category: string } ) => { }; }, [ isLoading, blocks, isSpinnerVisible ] ); + const { insertPattern, insertedPattern: blockToScroll } = + useInsertPattern(); + useEffect( () => { if ( isEditorLoading ) { return; @@ -175,7 +176,10 @@ export const SidebarPatternScreen = ( { category }: { category: string } ) => { ); if ( block ) { - block.scrollIntoView(); + block.scrollIntoView( { + behavior: 'smooth', + block: 'end', + } ); blockToScroll.current = null; } } @@ -188,9 +192,7 @@ export const SidebarPatternScreen = ( { category }: { category: string } ) => { return () => { observer.disconnect(); }; - }, [ isEditorLoading ] ); - - const { insertPattern } = useInsertPattern(); + }, [ blockToScroll, isEditorLoading ] ); return (
{ // @ts-ignore No types for this exist yet. __next40pxDefaultSize > - { isResolving ? : __( 'Save', 'woocommerce' ) } + { isResolving ? ( + + ) : ( + __( 'Finish customizing', 'woocommerce' ) + ) } ); diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx index cf7eeca1d1c..b1b10bab2e6 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx @@ -4,8 +4,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { createInterpolateElement, useContext } from '@wordpress/element'; -import { Link } from '@woocommerce/components'; +import { useContext } from '@wordpress/element'; import { PanelBody } from '@wordpress/components'; // @ts-ignore No types for this exist yet. import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; @@ -17,10 +16,8 @@ import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; */ import { CustomizeStoreContext } from '../'; import { SidebarNavigationScreen } from './sidebar-navigation-screen'; -import { ADMIN_URL } from '~/utils/admin-settings'; import { ColorPalette, ColorPanel } from './global-styles'; import { FlowType } from '~/customize-store/types'; -import { trackEvent } from '~/customize-store/tracking'; const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); @@ -71,11 +68,11 @@ export const SidebarNavigationScreenColorPalette = ( { : __( 'Choose your color palette', 'woocommerce' ); const description = aiOnline ? __( - 'Based on the info you shared, our AI tool recommends using this color palette. Want to change it? You can select or add new colors below, or update them later in Editor | Styles.', + 'Based on the info you shared, our AI tool recommends using this color palette. Want to change it? You can select or add new colors below, or update them later in Editor.', 'woocommerce' ) : __( - 'Choose the color palette that best suits your brand. Want to change it? Create your custom color palette below, or update it later in Editor | Styles.', + 'Choose the color palette that best suits your brand. Want to change it? Create your custom color palette below, or update it later in Editor.', 'woocommerce' ); @@ -83,44 +80,7 @@ export const SidebarNavigationScreenColorPalette = ( { { - trackEvent( - 'customize_your_store_assembler_hub_editor_link_click', - { - source: 'color-palette', - } - ); - window.open( - `${ ADMIN_URL }site-editor.php`, - '_blank' - ); - return false; - } } - href="" - /> - ), - StyleLink: ( - { - trackEvent( - 'customize_your_store_assembler_hub_style_link_click', - { - source: 'color-palette', - } - ); - window.open( - `${ ADMIN_URL }site-editor.php?path=%2Fwp_global_styles&canvas=edit`, - '_blank' - ); - return false; - } } - href="" - /> - ), - } ) } + description={ description } content={ } /> ); diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer/sidebar-navigation-screen-footer.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer/sidebar-navigation-screen-footer.tsx index f55d260306a..80404bbb693 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer/sidebar-navigation-screen-footer.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer/sidebar-navigation-screen-footer.tsx @@ -5,13 +5,11 @@ */ import { __ } from '@wordpress/i18n'; import { - createInterpolateElement, useCallback, useContext, useEffect, useMemo, } from '@wordpress/element'; -import { Link } from '@woocommerce/components'; import { Spinner } from '@wordpress/components'; // @ts-expect-error No types for this exist yet. import { store as coreStore } from '@wordpress/core-data'; @@ -20,7 +18,6 @@ import { store as coreStore } from '@wordpress/core-data'; * Internal dependencies */ import { SidebarNavigationScreen } from '../sidebar-navigation-screen'; -import { ADMIN_URL } from '~/utils/admin-settings'; import { useEditorBlocks } from '../../hooks/use-editor-blocks'; import { usePatternsByCategory } from '../../hooks/use-patterns'; import { HighlightedBlockContext } from '../../context/highlighted-block-context'; @@ -35,7 +32,6 @@ import { CustomizeStoreContext } from '~/customize-store/assembler-hub'; import { FlowType } from '~/customize-store/types'; import { footerTemplateId } from '~/customize-store/data/homepageTemplates'; import { useSelect } from '@wordpress/data'; -import { trackEvent } from '~/customize-store/tracking'; import './style.scss'; @@ -140,11 +136,11 @@ export const SidebarNavigationScreenFooter = ( { const description = aiOnline ? __( - "Select a new footer from the options below. Your footer includes your site's secondary navigation and will be added to every page. You can continue customizing this via the Editor.", + "Select a new footer from the options below. Your footer includes your site's secondary navigation and will be added to every page. You can continue customizing this via the Editor.", 'woocommerce' ) : __( - "Select a footer from the options below. Your footer includes your site's secondary navigation and will be added to every page. You can continue customizing this via the Editor later.", + "Select a footer from the options below. Your footer includes your site's secondary navigation and will be added to every page. You can continue customizing this via the Editor later.", 'woocommerce' ); @@ -155,26 +151,7 @@ export const SidebarNavigationScreenFooter = ( { resetHighlightedBlockClientId(); onNavigateBackClick(); } } - description={ createInterpolateElement( description, { - EditorLink: ( - { - trackEvent( - 'customize_your_store_assembler_hub_editor_link_click', - { - source: 'footer', - } - ); - window.open( - `${ ADMIN_URL }site-editor.php`, - '_blank' - ); - return false; - } } - href="" - /> - ), - } ) } + description={ description } content={ <>
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header/sidebar-navigation-screen-header.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header/sidebar-navigation-screen-header.tsx index 413d374aeeb..4c62164c547 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header/sidebar-navigation-screen-header.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header/sidebar-navigation-screen-header.tsx @@ -6,12 +6,10 @@ import { __ } from '@wordpress/i18n'; import { useCallback, - createInterpolateElement, useContext, useEffect, useMemo, } from '@wordpress/element'; -import { Link } from '@woocommerce/components'; import { Spinner } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; // @ts-expect-error No types for this exist yet. @@ -21,7 +19,6 @@ import { store as coreStore } from '@wordpress/core-data'; * Internal dependencies */ import { SidebarNavigationScreen } from '../sidebar-navigation-screen'; -import { ADMIN_URL } from '~/utils/admin-settings'; import { usePatternsByCategory } from '../../hooks/use-patterns'; import { useSelectedPattern } from '../../hooks/use-selected-pattern'; import { useEditorBlocks } from '../../hooks/use-editor-blocks'; @@ -35,7 +32,6 @@ import { import { CustomizeStoreContext } from '~/customize-store/assembler-hub'; import { FlowType } from '~/customize-store/types'; import { headerTemplateId } from '~/customize-store/data/homepageTemplates'; -import { trackEvent } from '~/customize-store/tracking'; import './style.scss'; @@ -140,31 +136,9 @@ export const SidebarNavigationScreenHeader = ( { resetHighlightedBlockClientId(); onNavigateBackClick(); } } - description={ createInterpolateElement( - __( - "Select a new header from the options below. Your header includes your site's navigation and will be added to every page. You can continue customizing this via the Editor.", - 'woocommerce' - ), - { - EditorLink: ( - { - trackEvent( - 'customize_your_store_assembler_hub_editor_link_click', - { - source: 'header', - } - ); - window.open( - `${ ADMIN_URL }site-editor.php`, - '_blank' - ); - return false; - } } - href="" - /> - ), - } + description={ __( + "Select a new header from the options below. Your header includes your site's navigation and will be added to every page. You can continue customizing this via the Editor.", + 'woocommerce' ) } content={ <> diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage-ptk/sidebar-navigation-screen-homepage-ptk.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage-ptk/sidebar-navigation-screen-homepage-ptk.tsx index 4ba091e6355..593f12e7b21 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage-ptk/sidebar-navigation-screen-homepage-ptk.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage-ptk/sidebar-navigation-screen-homepage-ptk.tsx @@ -32,7 +32,6 @@ import SidebarNavigationItem from '@wordpress/edit-site/build-module/components/ /** * Internal dependencies */ -import { ADMIN_URL } from '~/utils/admin-settings'; import { SidebarNavigationScreen } from '../sidebar-navigation-screen'; import { trackEvent } from '~/customize-store/tracking'; import { CustomizeStoreContext } from '../..'; @@ -164,7 +163,7 @@ export const SidebarNavigationScreenHomepagePTK = ( { const [ optInDataSharing, setIsOptInDataSharing ] = useState< boolean >( true ); - const [ isFetchingPatterns, setIsFetchingPatterns ] = useState( false ); + const [ isSettingTracking, setIsSettingTracking ] = useState( false ); const optIn = () => { trackEvent( @@ -181,7 +180,7 @@ export const SidebarNavigationScreenHomepagePTK = ( { const title = __( 'Design your homepage', 'woocommerce' ); const sidebarMessage = __( - 'Create an engaging homepage by adding and combining different patterns and layouts. You can continue customizing this page, including the content, later via the Editor.', + 'Create an engaging homepage by adding and combining different patterns and layouts. You can continue customizing this page, including the content, later via the Editor.', 'woocommerce' ); @@ -191,26 +190,7 @@ export const SidebarNavigationScreenHomepagePTK = ( { { - trackEvent( - 'customize_your_store_assembler_hub_editor_link_click', - { - source: 'homepage', - } - ); - window.open( - `${ ADMIN_URL }site-editor.php`, - '_blank' - ); - return false; - } } - href="" - /> - ), - } ) } + description={ sidebarMessage } content={
@@ -284,7 +264,7 @@ export const SidebarNavigationScreenHomepagePTK = ( {
diff --git a/plugins/woocommerce-admin/client/homescreen/activity-panel/orders/index.js b/plugins/woocommerce-admin/client/homescreen/activity-panel/orders/index.js index dbbed28530d..143b8a65d0c 100644 --- a/plugins/woocommerce-admin/client/homescreen/activity-panel/orders/index.js +++ b/plugins/woocommerce-admin/client/homescreen/activity-panel/orders/index.js @@ -321,7 +321,7 @@ function OrdersPanel( { unreadOrdersCount, orderStatuses } ) { return ( @@ -162,9 +161,6 @@ export const Layout = ( { > { isDashboardShown ? renderColumns() : renderTaskList() } { shouldShowMobileAppModal && } - { window.wcAdminFeatures.navigation && ( - - ) }
); diff --git a/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/components/useSendMagicLink.tsx b/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/components/useSendMagicLink.tsx index e162edfd22c..fa4fc7e276e 100644 --- a/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/components/useSendMagicLink.tsx +++ b/plugins/woocommerce-admin/client/homescreen/mobile-app-modal/components/useSendMagicLink.tsx @@ -61,7 +61,7 @@ export const useSendMagicLink = () => { createNotice( 'error', __( - "We couldn't send the link. Try again in a few seconds.", + 'We couldn’t send the link. Try again in a few seconds.', 'woocommerce' ) ); @@ -71,7 +71,7 @@ export const useSendMagicLink = () => { createNotice( 'error', __( - "Sorry, your account doesn't have sufficient permission.", + 'Sorry, your account doesn’t have sufficient permission.', 'woocommerce' ) ); @@ -80,7 +80,7 @@ export const useSendMagicLink = () => { } else { createNotice( 'error', - "We couldn't send the link. Try again in a few seconds." + 'We couldn’t send the link. Try again in a few seconds.' ); } } ); diff --git a/plugins/woocommerce-admin/client/inbox-panel/index.js b/plugins/woocommerce-admin/client/inbox-panel/index.js index 32e56c6c12d..06ab9f474a6 100644 --- a/plugins/woocommerce-admin/client/inbox-panel/index.js +++ b/plugins/woocommerce-admin/client/inbox-panel/index.js @@ -71,7 +71,7 @@ const renderEmptyCard = () => ( > { __( 'As things begin to happen in your store your inbox will start to fill up. ' + - "You'll see things like achievements, new feature announcements, extension recommendations and more!", + 'You’ll see things like achievements, new feature announcements, extension recommendations and more!', 'woocommerce' ) } diff --git a/plugins/woocommerce-admin/client/inbox-panel/test/index.js b/plugins/woocommerce-admin/client/inbox-panel/test/index.js index c26bbecf821..f6cd8f84419 100644 --- a/plugins/woocommerce-admin/client/inbox-panel/test/index.js +++ b/plugins/woocommerce-admin/client/inbox-panel/test/index.js @@ -203,7 +203,7 @@ describe( 'inbox_note_view event', () => { notesHaveResolved: true, isBatchUpdating: false, } ) ); - // The original InboxNotecard has a VisibilityDetector so I prefered to mock it and always call onNoteVisible + // The original InboxNotecard has a VisibilityDetector so I preferred to mock it and always call onNoteVisible InboxNoteCard.mockImplementation( ( { onNoteVisible, note } ) => { useEffect( () => onNoteVisible( note ), [] ); return
{ note.id }
; diff --git a/plugins/woocommerce-admin/client/index.js b/plugins/woocommerce-admin/client/index.js index a733eeb8fa1..45b109edf09 100644 --- a/plugins/woocommerce-admin/client/index.js +++ b/plugins/woocommerce-admin/client/index.js @@ -34,6 +34,7 @@ import { SettingsPaymentsWooCommercePaymentsWrapper, } from './settings-payments'; import { ErrorBoundary } from './error-boundary'; +import { registerBlueprintSlotfill } from './blueprint'; const appRoot = document.getElementById( 'root' ); const embeddedRoot = document.getElementById( 'woocommerce-embedded-root' ); @@ -111,6 +112,10 @@ if ( appRoot ) { ) { registerSiteVisibilitySlotFill(); } + + if ( window.wcAdminFeatures && window.wcAdminFeatures.blueprint === true ) { + registerBlueprintSlotfill(); + } } // Render the CustomerEffortScoreTracksContainer only if diff --git a/plugins/woocommerce-admin/client/launch-your-store/constants.ts b/plugins/woocommerce-admin/client/launch-your-store/constants.ts index a6a2bbdf15c..9b95ffc736c 100644 --- a/plugins/woocommerce-admin/client/launch-your-store/constants.ts +++ b/plugins/woocommerce-admin/client/launch-your-store/constants.ts @@ -8,7 +8,7 @@ export const COMING_SOON_PAGE_EDITOR_LINK = getAdminLink( ); export const SITE_VISIBILITY_DOC_LINK = - 'https://woocommerce.com/document/woocommerce-launch-your-store/'; + 'https://woocommerce.com/document/configuring-woocommerce-settings/coming-soon-mode/'; export const LAUNCH_YOUR_STORE_DOC_LINK = 'https://woocommerce.com/document/configuring-woocommerce-settings/#site-visibility'; diff --git a/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/WhatsNext.tsx b/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/WhatsNext.tsx index 53ea8959ccd..a3b646ed31d 100644 --- a/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/WhatsNext.tsx +++ b/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/WhatsNext.tsx @@ -78,7 +78,7 @@ const getActionsList = ( { activePlugins, allTasklists }: WhatsNextProps ) => { 'Give your shoppers more ways to pay by adding additional payment methods to your store.', 'woocommerce' ), - link: `${ ADMIN_URL }admin.php?page=wc-admin&task=payments`, + link: `${ ADMIN_URL }admin.php?page=wc-settings&tab=checkout`, linkText: __( 'Add payment methods', 'woocommerce' ), trackEvent: 'launch_you_store_congrats_payments_click', }; @@ -86,7 +86,7 @@ const getActionsList = ( { activePlugins, allTasklists }: WhatsNextProps ) => { const mailchimp = { title: __( 'Build customer relationships', 'woocommerce' ), description: __( - "Keep your shoppers up to date with what's new in your store and set up clever post-purchase automations.", + 'Keep your shoppers up to date with what’s new in your store and set up clever post-purchase automations.', 'woocommerce' ), link: isMailChimpActivated @@ -123,7 +123,7 @@ const getActionsList = ( { activePlugins, allTasklists }: WhatsNextProps ) => { const externalDocumentation = { title: __( 'Help is on hand', 'woocommerce' ), description: __( - "Detailed guides and our support team are always available if you're feeling stuck or need some guidance.", + 'Detailed guides and our support team are always available if you’re feeling stuck or need some guidance.', 'woocommerce' ), link: `https://woo.com/documentation/woocommerce/?utm_source=launch_your_store&utm_medium=product`, diff --git a/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/index.tsx b/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/index.tsx index c0c08602013..646df4a79de 100644 --- a/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/index.tsx +++ b/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/index.tsx @@ -148,7 +148,7 @@ export const LaunchYourStoreSuccess = ( { } ) : __( - "You've successfully launched your store and are ready to start selling! We can't wait to see your business grow.", + 'You’ve successfully launched your store and are ready to start selling! We can’t wait to see your business grow.', 'woocommerce' ) } @@ -193,7 +193,7 @@ export const LaunchYourStoreSuccess = ( { />

- { __( "What's next?", 'woocommerce' ) } + { __( 'What’s next?', 'woocommerce' ) }

{ + const [ pendingSubmitEvent, setPendingSubmitEvent ] = useState( null ); + const [ isConfirmModalOpen, setIsConfirmModalOpen ] = useState( false ); + const currentComingSoon = currentSetting?.woocommerce_coming_soon ?? 'no'; + + // Hooks into settings' "mainform" to show a confirmation modal when the form is submitted. + useEffect( () => { + const form = formRef.current; + const handleFormSubmit = ( event ) => { + const formData = new FormData( form ); + + // Only block submission when switching to coming soon mode from live. + if ( + currentComingSoon === 'no' && + formData.get( 'woocommerce_coming_soon' ) === 'yes' + ) { + event.preventDefault(); + setIsConfirmModalOpen( true ); + setPendingSubmitEvent( event ); + } + }; + if ( form ) { + form.addEventListener( 'submit', handleFormSubmit ); + } + + return () => { + if ( form ) { + form.removeEventListener( 'submit', handleFormSubmit ); + } + }; + }, [ currentSetting, formRef ] ); + + const cancelSubmit = () => { + setPendingSubmitEvent( null ); // Clear the pending submit + setIsConfirmModalOpen( false ); // Close the modal + + if ( saveButtonRef.current ) { + saveButtonRef.current.classList.remove( 'is-busy' ); + } + }; + + const confirmSubmit = () => { + if ( pendingSubmitEvent ) { + // WooCommerce checks for the "save" input. + if ( saveButtonRef.current && formRef.current ) { + const hiddenInput = document.createElement( 'input' ); + hiddenInput.type = 'hidden'; + hiddenInput.name = saveButtonRef.current.name || 'save'; + hiddenInput.value = + saveButtonRef.current.value || + __( 'Save changes', 'woocommerce' ); + formRef.current.appendChild( hiddenInput ); + } + + pendingSubmitEvent.target.submit(); + setPendingSubmitEvent( null ); + } + setIsConfirmModalOpen( false ); // Close the modal + }; + + return isConfirmModalOpen ? ( + +
+ { __( + 'Are you sure you want to switch from live to coming soon mode? Your site will not be visible, and customers won’t be able to make purchases during this time.', + 'woocommerce' + ) } +
+
+
+
+
+ + +
+
+ ) : null; +}; diff --git a/plugins/woocommerce-admin/client/launch-your-store/settings/components/confirmation-modal.scss b/plugins/woocommerce-admin/client/launch-your-store/settings/components/confirmation-modal.scss new file mode 100644 index 00000000000..be45ad905a6 --- /dev/null +++ b/plugins/woocommerce-admin/client/launch-your-store/settings/components/confirmation-modal.scss @@ -0,0 +1,24 @@ +.site-visibility-settings-confirmation-modal { + .site-visibility-settings-confirmation-modal__content { + margin-top: 15px; + } + + .site-visibility-settings-confirmation-modal__buttons { + display: flex; + flex-flow: row-reverse; + } + + // Hacky solution for a divider line that spans over its container's paddings. + .divider-container { + height: 1px; + margin-bottom: 30px; + margin-top: 25px; + + & > hr { + position: absolute; + width: 100%; + left: 0; + right: 0; + } + } +} diff --git a/plugins/woocommerce-admin/client/launch-your-store/settings/components/test/confirmation-modal.test.js b/plugins/woocommerce-admin/client/launch-your-store/settings/components/test/confirmation-modal.test.js new file mode 100644 index 00000000000..d7267e32cf1 --- /dev/null +++ b/plugins/woocommerce-admin/client/launch-your-store/settings/components/test/confirmation-modal.test.js @@ -0,0 +1,183 @@ +/** + * External dependencies + */ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +/** + * Internal dependencies + */ +import { ConfirmationModal } from '../confirmation-modal'; + +// Mock the necessary external dependencies +jest.mock( '@wordpress/components', () => ( { + Modal: jest.fn( ( { title, children, onRequestClose } ) => ( +
+
{ title }
+
{ children }
+ +
+ ) ), + Button: jest.fn( ( { children, onClick } ) => ( + + ) ), +} ) ); + +describe( 'ConfirmationModal', () => { + let formRef, saveButtonRef; + + const mockSelectComingSoon = ( value ) => { + // Set up form data + const input = document.createElement( 'input' ); + input.name = 'woocommerce_coming_soon'; + input.value = value; + formRef.current.appendChild( input ); + }; + + const fireSubmitEvent = () => { + // Simulate form submission + const submitEvent = new Event( 'submit', { + bubbles: true, + cancelable: true, + } ); + fireEvent( formRef.current, submitEvent ); + }; + + beforeEach( () => { + formRef = { current: document.createElement( 'form' ) }; + saveButtonRef = { current: document.createElement( 'button' ) }; + formRef.current.appendChild( saveButtonRef.current ); + document.body.appendChild( formRef.current ); + } ); + + afterEach( () => { + document.body.removeChild( formRef.current ); + } ); + + it( 'should prompt the modal if current setting is live and submit the form', () => { + const currentSetting = { woocommerce_coming_soon: 'no' }; + + render( + + ); + + const submitListener = jest.fn(); + formRef.current.onsubmit = submitListener; + + mockSelectComingSoon( 'yes' ); + fireSubmitEvent(); + + // Confirm modal is prompted + expect( + screen.getByText( 'Confirm switch to ‘Coming soon’ mode' ) + ).toBeInTheDocument(); + + // Simulate confirming submission + fireEvent.click( screen.getByText( 'Switch' ) ); + + // Ensure the form is submitted + expect( submitListener ).toHaveBeenCalled(); + } ); + + it( 'should prompt the modal if current setting is not set', () => { + render( + + ); + + mockSelectComingSoon( 'yes' ); + fireSubmitEvent(); + + // Confirm that the modal is not prompted + expect( + screen.queryByText( 'Confirm switch to ‘Coming soon’ mode' ) + ).toBeInTheDocument(); + } ); + + it( 'should not prompt the modal if current setting is already "coming soon"', () => { + const currentSetting = { woocommerce_coming_soon: 'yes' }; + + render( + + ); + + mockSelectComingSoon( 'yes' ); + fireSubmitEvent(); + + // Confirm that the modal is not prompted + expect( + screen.queryByText( 'Confirm switch to ‘Coming soon’ mode' ) + ).not.toBeInTheDocument(); + } ); + + it( 'should close the modal on cancel', () => { + const currentSetting = { woocommerce_coming_soon: 'no' }; + + render( + + ); + + mockSelectComingSoon( 'yes' ); + fireSubmitEvent(); + + // Confirm modal is prompted + expect( + screen.getByText( 'Confirm switch to ‘Coming soon’ mode' ) + ).toBeInTheDocument(); + + // Simulate canceling the modal + fireEvent.click( screen.getByText( 'Cancel' ) ); + + // Confirm that the modal is closed + expect( + screen.queryByText( 'Confirm switch to ‘Coming soon’ mode' ) + ).not.toBeInTheDocument(); + } ); + + it( 'should handle the save button correctly', () => { + const currentSetting = { woocommerce_coming_soon: 'no' }; + saveButtonRef.current.name = 'save'; + saveButtonRef.current.value = 'Save changes'; + + render( + + ); + + mockSelectComingSoon( 'yes' ); + fireSubmitEvent(); + + // Confirm modal is prompted + expect( + screen.getByText( 'Confirm switch to ‘Coming soon’ mode' ) + ).toBeInTheDocument(); + + // Simulate confirming submission + fireEvent.click( screen.getByText( 'Switch' ) ); + + // Check that the hidden input with "save" has been added to the form + const hiddenInput = + formRef.current.querySelector( 'input[name="save"]' ); + expect( hiddenInput ).toBeInTheDocument(); + expect( hiddenInput.value ).toBe( 'Save changes' ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/launch-your-store/settings/slotfill.js b/plugins/woocommerce-admin/client/launch-your-store/settings/slotfill.js index aba9bd41ee6..370675bc000 100644 --- a/plugins/woocommerce-admin/client/launch-your-store/settings/slotfill.js +++ b/plugins/woocommerce-admin/client/launch-your-store/settings/slotfill.js @@ -12,6 +12,7 @@ import { createInterpolateElement, createElement, useEffect, + useRef, } from '@wordpress/element'; import { registerPlugin } from '@wordpress/plugins'; import { __ } from '@wordpress/i18n'; @@ -29,6 +30,7 @@ import { COMING_SOON_PAGE_EDITOR_LINK, SITE_VISIBILITY_DOC_LINK, } from '../constants'; +import { ConfirmationModal } from './components/confirmation-modal'; const { Fill } = createSlotFill( SETTINGS_SLOT_FILL_CONSTANT ); @@ -45,6 +47,21 @@ const SiteVisibility = () => { const [ privateLink, setPrivateLink ] = useState( setting?.woocommerce_private_link || 'no' ); + const formRef = useRef( null ); + const saveButtonRef = useRef( null ); + + useEffect( () => { + const saveButton = document.getElementsByClassName( + 'woocommerce-save-button' + )[ 0 ]; + if ( saveButton ) { + saveButtonRef.current = saveButton; + } + const form = document.querySelector( '#mainform' ); + if ( form ) { + formRef.current = form; + } + }, [] ); useEffect( () => { const initValues = { @@ -115,9 +132,18 @@ const SiteVisibility = () => { />

{ __( 'Site visibility', 'woocommerce' ) }

- { __( - 'Manage how your site appears to visitors.', - 'woocommerce' + { createInterpolateElement( + __( + 'Manage how your site appears to visitors. Learn more', + 'woocommerce' + ), + { + a: createElement( 'a', { + target: '_blank', + rel: 'noreferrer', + href: SITE_VISIBILITY_DOC_LINK, + } ), + } ) }

@@ -145,6 +171,7 @@ const SiteVisibility = () => { ), { a: createElement( 'a', { + target: '_blank', href: COMING_SOON_PAGE_EDITOR_LINK, } ), } @@ -166,20 +193,13 @@ const SiteVisibility = () => { label={ <> { __( - 'Restrict to store pages only', + 'Apply to store pages only', 'woocommerce' ) }

- { createInterpolateElement( - __( - 'Display a "coming soon" message on your store pages — the rest of your site will remain visible.', - 'woocommerce' - ), - { - a: createElement( 'a', { - href: SITE_VISIBILITY_DOC_LINK, - } ), - } + { __( + 'Display a “coming soon” message on your store pages — the rest of your site will remain visible.', + 'woocommerce' ) }

@@ -266,6 +286,13 @@ const SiteVisibility = () => { ) }

+ { formRef.current && saveButtonRef.current ? ( + + ) : null }
); }; diff --git a/plugins/woocommerce-admin/client/launch-your-store/status/index.tsx b/plugins/woocommerce-admin/client/launch-your-store/status/index.tsx index 96f5bb28a2a..b5085fea068 100644 --- a/plugins/woocommerce-admin/client/launch-your-store/status/index.tsx +++ b/plugins/woocommerce-admin/client/launch-your-store/status/index.tsx @@ -1,12 +1,6 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; -import { Icon, moreVertical, edit, cog } from '@wordpress/icons'; -import { Dropdown, Button, MenuGroup, MenuItem } from '@wordpress/components'; -import { getAdminLink, getSetting } from '@woocommerce/settings'; -import clsx from 'clsx'; -import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies @@ -14,23 +8,8 @@ import { recordEvent } from '@woocommerce/tracks'; import './style.scss'; import { SiteVisibilityTour } from '../tour'; import { useSiteVisibilityTour } from '../tour/use-site-visibility-tour'; -import { COMING_SOON_PAGE_EDITOR_LINK } from '../constants'; -export const LaunchYourStoreStatus = ( { - comingSoon, - storePagesOnly, -}: { - comingSoon: string; - storePagesOnly: string; -} ) => { - const isComingSoon = comingSoon && comingSoon === 'yes'; - const isStorePagesOnly = - isComingSoon && storePagesOnly && storePagesOnly === 'yes'; - const comingSoonText = isStorePagesOnly - ? __( 'Store coming soon', 'woocommerce' ) - : __( 'Site coming soon', 'woocommerce' ); - const liveText = __( 'Live', 'woocommerce' ); - const dropdownText = isComingSoon ? comingSoonText : liveText; +export const LaunchYourStoreStatus = () => { const { showTour, setShowTour, onClose, shouldTourBeShown } = useSiteVisibilityTour(); @@ -44,75 +23,6 @@ export const LaunchYourStoreStatus = ( { } } /> ) } -
- ( - - ) } - renderContent={ () => ( - <> - - { - recordEvent( - 'launch_your_store_badge_menu_manage_site_visibility_click', - { - site_visibility: isComingSoon - ? 'coming_soon' - : 'live', - } - ); - } } - // @ts-expect-error Prop gets passed down to underlying button https://developer.wordpress.org/block-editor/reference-guides/components/menu-item/#props - href={ getAdminLink( - 'admin.php?page=wc-settings&tab=site-visibility' - ) } - > - - { __( - 'Manage site visibility', - 'woocommerce' - ) } - - { isComingSoon && - getSetting( 'currentThemeIsFSETheme' ) && ( - { - recordEvent( - 'launch_your_store_badge_menu_customize_coming_soon_click' - ); - } } - // @ts-expect-error Prop gets passed down to underlying button https://developer.wordpress.org/block-editor/reference-guides/components/menu-item/#props - href={ - COMING_SOON_PAGE_EDITOR_LINK - } - > - - { __( - 'Customize "Coming soon" page', - 'woocommerce' - ) } - - ) } - - - ) } - /> -
); }; diff --git a/plugins/woocommerce-admin/client/launch-your-store/tour/index.tsx b/plugins/woocommerce-admin/client/launch-your-store/tour/index.tsx index 0ffa96e8189..0edad4961a4 100644 --- a/plugins/woocommerce-admin/client/launch-your-store/tour/index.tsx +++ b/plugins/woocommerce-admin/client/launch-your-store/tour/index.tsx @@ -49,18 +49,19 @@ export const SiteVisibilityTour = ( { onClose }: { onClose: () => void } ) => { steps: [ { referenceElements: { - desktop: '.woocommerce-lys-status-pill', + desktop: + '#wp-admin-bar-woocommerce-site-visibility-badge', }, meta: { name: 'set-your-store-visibility', heading: __( - "Set your store's visibility", + 'Set your store’s visibility', 'woocommerce' ), descriptions: { desktop: createInterpolateElement( __( - 'Launch your store only when you\'re ready to by switching between "Coming soon" and "Live" modes. Build excitement by creating a custom page visitors will see before you\'re ready to go live. Discover more', + 'Launch your store only when you’re ready to by switching between "Coming soon" and "Live" modes. Build excitement by creating a custom page visitors will see before you’re ready to go live. Discover more', 'woocommerce' ), { diff --git a/plugins/woocommerce-admin/client/launch-your-store/tour/use-site-visibility-tour.tsx b/plugins/woocommerce-admin/client/launch-your-store/tour/use-site-visibility-tour.tsx index 3b6562d254f..52460350aec 100644 --- a/plugins/woocommerce-admin/client/launch-your-store/tour/use-site-visibility-tour.tsx +++ b/plugins/woocommerce-admin/client/launch-your-store/tour/use-site-visibility-tour.tsx @@ -1,50 +1,57 @@ /** * External dependencies */ -import { OPTIONS_STORE_NAME } from '@woocommerce/data'; -import { useSelect, dispatch } from '@wordpress/data'; +import { OPTIONS_STORE_NAME, useUserPreferences } from '@woocommerce/data'; +import { useSelect } from '@wordpress/data'; import { useState } from 'react'; -const LYS_TOUR_HIDDEN = 'woocommerce_launch_your_store_tour_hidden'; - export const useSiteVisibilityTour = () => { const [ showTour, setShowTour ] = useState( true ); - const { shouldTourBeShown } = useSelect( ( select ) => { - // Tour should only be shown if the user has not seen it before and the `woocommerce_show_lys_tour` option is "yes" (for sites upgrading from a previous WooCommerce version) - const { getCurrentUser } = select( 'core' ); - const wasTourShown = + // Tour should only be shown if the user has not seen it before and the `woocommerce_show_lys_tour` option is "yes" (for sites upgrading from a previous WooCommerce version) + const shouldStoreShowLYSTour = useSelect( + ( select ) => + select( OPTIONS_STORE_NAME ).getOption( + 'woocommerce_show_lys_tour' + ) === 'yes' + ); + + /** + * This is temporary to support sites upgrading from a previous version of WooCommerce. + * We used user meta to store the tour dismissal state but now we use WooCommerce meta instead. + * It will be removed in WC 9.4. + */ + const hasUserDismissedTourMeta = useSelect( ( select ) => { + const currentUser = select( 'core' ).getCurrentUser(); + if ( ! currentUser ) { + // If the user is not logged in, we don't want to show the tour. + return true; + } + + return ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - ( getCurrentUser() as { meta?: { [ key: string ]: string } } ) - ?.meta?.[ LYS_TOUR_HIDDEN ] === 'yes'; - - const { getOption } = select( OPTIONS_STORE_NAME ); - - const showLYSTourOption = getOption( 'woocommerce_show_lys_tour' ); - - const _shouldTourBeShown = - showLYSTourOption === 'yes' && ! wasTourShown; - - return { - shouldTourBeShown: _shouldTourBeShown, - }; + ( currentUser as { meta: { [ key: string ]: string } } ).meta + .woocommerce_launch_your_store_tour_hidden === 'yes' + ); } ); + const { + launch_your_store_tour_hidden: lysTourHidden, + updateUserPreferences, + } = useUserPreferences(); + const onClose = () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - dispatch( 'core' ).saveUser( { - id: window?.wcSettings?.currentUserId, - meta: { - woocommerce_launch_your_store_tour_hidden: 'yes', - }, + updateUserPreferences( { + launch_your_store_tour_hidden: 'yes', } ); }; return { onClose, - shouldTourBeShown, + shouldTourBeShown: + shouldStoreShowLYSTour && + ! ( hasUserDismissedTourMeta || lysTourHidden === 'yes' ), showTour, setShowTour, }; diff --git a/plugins/woocommerce-admin/client/layout/index.js b/plugins/woocommerce-admin/client/layout/index.js index 2cf1046a630..7b7986343bd 100644 --- a/plugins/woocommerce-admin/client/layout/index.js +++ b/plugins/woocommerce-admin/client/layout/index.js @@ -53,7 +53,6 @@ import { getAdminSetting } from '~/utils/admin-settings'; import { usePageClasses } from './hooks/use-page-classes'; import '~/activity-panel'; import '~/mobile-banner'; -import './navigation'; const StoreAlerts = lazy( () => import( /* webpackChunkName: "store-alerts" */ './store-alerts' ) @@ -125,15 +124,10 @@ function _Layout( { usePageClasses( page ); function recordPageViewTrack() { - const navigationFlag = { - has_navigation: !! window.wcNavigation, - }; - if ( isEmbedded ) { const path = document.location.pathname + document.location.search; recordPageView( path, { is_embedded: true, - ...navigationFlag, } ); return; } @@ -155,7 +149,6 @@ function _Layout( { jetpack_installed: installedPlugins.includes( 'jetpack' ), jetpack_active: activePlugins.includes( 'jetpack' ), jetpack_connected: isJetpackConnected, - ...navigationFlag, } ); } @@ -256,9 +249,6 @@ function _Layout( { { showPluginArea && ( <> - { window.wcAdminFeatures.navigation && ( - - ) } ) } diff --git a/plugins/woocommerce-admin/client/layout/navigation.js b/plugins/woocommerce-admin/client/layout/navigation.js deleted file mode 100644 index f95c801ee18..00000000000 --- a/plugins/woocommerce-admin/client/layout/navigation.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * External dependencies - */ -import { registerPlugin } from '@wordpress/plugins'; -import { WooHeaderNavigationItem } from '@woocommerce/admin-layout'; -import { - WooNavigationItem, - getNewPath, - pathIsExcluded, - isWCAdmin, -} from '@woocommerce/navigation'; -import { Link } from '@woocommerce/components'; -import { __ } from '@wordpress/i18n'; -import { useSelect } from '@wordpress/data'; -import { NAVIGATION_STORE_NAME } from '@woocommerce/data'; - -/** - * Internal dependencies - */ -import getReports from '../analytics/report/get-reports'; -import { getPages } from './controller'; -import Navigation from '~/navigation'; - -const NavigationPlugin = () => { - const { persistedQuery } = useSelect( ( select ) => { - return { - persistedQuery: select( NAVIGATION_STORE_NAME ).getPersistedQuery(), - }; - } ); - - if ( ! window.wcAdminFeatures.navigation ) { - return null; - } - - /** - * If the current page is embedded, stay with the default urls - * provided by Navigation because the router isn't present to - * respond to component's manipulation of the url. - */ - if ( ! isWCAdmin() ) { - return ( - - - - ); - } - - const reports = getReports().filter( ( item ) => item.navArgs ); - - const pages = getPages() - .filter( ( page ) => page.navArgs ) - .map( ( page ) => { - if ( page.path === '/analytics/settings' ) { - return { - ...page, - breadcrumbs: [ __( 'Analytics', 'woocommerce' ) ], - }; - } - return page; - } ); - - return ( - <> - - - - { pages.map( ( page ) => ( - - - { page.breadcrumbs[ page.breadcrumbs.length - 1 ] } - - - ) ) } - { reports.map( ( item ) => ( - - - { item.title } - - - ) ) } - - ); -}; - -registerPlugin( 'wc-admin-navigation', { - render: NavigationPlugin, - scope: 'woocommerce-navigation', -} ); diff --git a/plugins/woocommerce-admin/client/layout/store-alerts/index.js b/plugins/woocommerce-admin/client/layout/store-alerts/index.js index e8f94361663..d8c6cdbcddd 100644 --- a/plugins/woocommerce-admin/client/layout/store-alerts/index.js +++ b/plugins/woocommerce-admin/client/layout/store-alerts/index.js @@ -110,7 +110,7 @@ export const StoreAlerts = () => { const variant = idx === 0 ? 'secondary' : 'tertiary'; return ( - ); -}; - -export default FavoriteButton; diff --git a/plugins/woocommerce-admin/client/navigation/components/favorite-button/style.scss b/plugins/woocommerce-admin/client/navigation/components/favorite-button/style.scss deleted file mode 100644 index 7cdb36fd7cd..00000000000 --- a/plugins/woocommerce-admin/client/navigation/components/favorite-button/style.scss +++ /dev/null @@ -1,9 +0,0 @@ -.woocommerce-navigation-favorite-button.components-button { - .star-empty-icon { - color: $gray-600; - } - - .star-filled-icon { - color: $alert-yellow; - } -} diff --git a/plugins/woocommerce-admin/client/navigation/components/favorite-button/test/index.js b/plugins/woocommerce-admin/client/navigation/components/favorite-button/test/index.js deleted file mode 100644 index b33c6ed85ac..00000000000 --- a/plugins/woocommerce-admin/client/navigation/components/favorite-button/test/index.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * External dependencies - */ -import { render } from '@testing-library/react'; -import { useDispatch, useSelect } from '@wordpress/data'; -import userEvent from '@testing-library/user-event'; -import { createElement } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import FavoriteButton from '../'; - -jest.mock( '@wordpress/data', () => { - // Require the original module to not be mocked... - const originalModule = jest.requireActual( '@wordpress/data' ); - - return { - __esModule: true, // Use it when dealing with esModules - ...originalModule, - useDispatch: jest.fn().mockReturnValue( {} ), - useSelect: jest.fn().mockReturnValue( {} ), - }; -} ); - -describe( 'FavoriteButton', () => { - test( 'should not show when favorites are still resolving', () => { - useSelect.mockImplementation( () => ( { - favorites: [], - isResolving: true, - } ) ); - - const { container } = render( ); - - expect( - container.querySelector( '.woocommerce-navigation-favorite-button' ) - ).toBeNull(); - } ); - - test( 'should show the empty star when item is not favorited', () => { - useSelect.mockImplementation( () => ( { - favorites: [], - isResolving: false, - } ) ); - - const { container } = render( ); - - expect( container.querySelector( '.star-empty-icon' ) ).not.toBeNull(); - } ); - - test( 'should show the filled star when item is favorited', () => { - useSelect.mockImplementation( () => ( { - favorites: [ 'my-item' ], - isResolving: false, - } ) ); - - const { container } = render( ); - - expect( container.querySelector( '.star-filled-icon' ) ).not.toBeNull(); - } ); - - test( 'should remove the favorite when toggling a favorited item', () => { - useSelect.mockImplementation( () => ( { - favorites: [ 'my-item' ], - isResolving: false, - } ) ); - - const addFavorite = jest.fn(); - const removeFavorite = jest.fn(); - - useDispatch.mockReturnValue( { - addFavorite, - removeFavorite, - } ); - - const { container } = render( ); - - userEvent.click( - container.querySelector( '.woocommerce-navigation-favorite-button' ) - ); - - expect( addFavorite ).not.toHaveBeenCalled(); - expect( removeFavorite ).toHaveBeenCalled(); - } ); - - test( 'should add the favorite when toggling a unfavorited item', () => { - useSelect.mockImplementation( () => ( { - favorites: [], - isResolving: false, - } ) ); - - const addFavorite = jest.fn(); - const removeFavorite = jest.fn(); - - useDispatch.mockReturnValue( { - addFavorite, - removeFavorite, - } ); - - const { container } = render( ); - - userEvent.click( - container.querySelector( '.woocommerce-navigation-favorite-button' ) - ); - - expect( addFavorite ).toHaveBeenCalled(); - expect( removeFavorite ).not.toHaveBeenCalled(); - } ); -} ); diff --git a/plugins/woocommerce-admin/client/navigation/components/favorites-tooltip/index.js b/plugins/woocommerce-admin/client/navigation/components/favorites-tooltip/index.js deleted file mode 100644 index 4a9f5456d72..00000000000 --- a/plugins/woocommerce-admin/client/navigation/components/favorites-tooltip/index.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { NAVIGATION_STORE_NAME, OPTIONS_STORE_NAME } from '@woocommerce/data'; -import { useDispatch, useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { HighlightTooltip } from '~/activity-panel/highlight-tooltip'; - -const tooltipHiddenOption = 'woocommerce_navigation_favorites_tooltip_hidden'; - -export const FavoritesTooltip = () => { - const { isFavoritesResolving, isOptionResolving, isTooltipHidden } = - useSelect( ( select ) => { - const { getOption, isResolving } = select( OPTIONS_STORE_NAME ); - return { - isFavoritesResolving: select( - NAVIGATION_STORE_NAME - ).isResolving( 'getFavorites' ), - isOptionResolving: isResolving( 'getOption', [ - tooltipHiddenOption, - ] ), - isTooltipHidden: getOption( tooltipHiddenOption ) === 'yes', - }; - } ); - - const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); - - if ( isFavoritesResolving || isTooltipHidden || isOptionResolving ) { - return null; - } - - if ( document.body.classList.contains( 'is-wc-nav-folded' ) ) { - return null; - } - - return ( - - updateOptions( { - [ tooltipHiddenOption ]: 'yes', - } ) - } - useAnchor - /> - ); -}; - -export default FavoritesTooltip; diff --git a/plugins/woocommerce-admin/client/navigation/components/header/index.js b/plugins/woocommerce-admin/client/navigation/components/header/index.js deleted file mode 100644 index 43335d22b58..00000000000 --- a/plugins/woocommerce-admin/client/navigation/components/header/index.js +++ /dev/null @@ -1,133 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; -import { decodeEntities } from '@wordpress/html-entities'; -import { Icon, wordpress } from '@wordpress/icons'; -import { getSetting } from '@woocommerce/settings'; -import { useSelect } from '@wordpress/data'; -import { useEffect, useState } from '@wordpress/element'; -import clsx from 'clsx'; -import { debounce } from 'lodash'; -import { addHistoryListener } from '@woocommerce/navigation'; - -/** - * Internal dependencies - */ -import useIsScrolled from '../../../hooks/useIsScrolled'; - -const Header = () => { - const siteTitle = getSetting( 'siteTitle', '' ); - const homeUrl = getSetting( 'homeUrl', '' ); - const { isScrolled } = useIsScrolled(); - const [ isFolded, setIsFolded ] = useState( - document.body.classList.contains( false ) - ); - const navClasses = { - folded: 'is-wc-nav-folded', - expanded: 'is-wc-nav-expanded', - }; - - const foldNav = () => { - document.body.classList.add( navClasses.folded ); - document.body.classList.remove( navClasses.expanded ); - setIsFolded( true ); - }; - - const expandNav = () => { - document.body.classList.remove( navClasses.folded ); - document.body.classList.add( navClasses.expanded ); - setIsFolded( false ); - }; - - const toggleFolded = () => { - if ( document.body.classList.contains( navClasses.folded ) ) { - expandNav(); - } else { - foldNav(); - } - }; - - const foldOnMobile = ( screenWidth = document.body.clientWidth ) => { - if ( screenWidth <= 960 ) { - foldNav(); - } else { - expandNav(); - } - }; - - useEffect( () => { - foldOnMobile(); - const foldEvents = [ - { - eventName: 'orientationchange', - handler: ( e ) => foldOnMobile( e.target.screen.availWidth ), - }, - { - eventName: 'resize', - handler: debounce( () => foldOnMobile(), 200 ), - }, - ]; - - for ( const { eventName, handler } of foldEvents ) { - window.addEventListener( eventName, handler, false ); - } - - addHistoryListener( () => foldOnMobile() ); - }, [] ); - - let buttonIcon = ; - - const { isRequestingSiteIcon, siteIconUrl } = useSelect( ( select ) => { - const { isResolving } = select( 'core/data' ); - const { getEntityRecord } = select( 'core' ); - const siteData = - getEntityRecord( 'root', '__unstableBase', undefined ) || {}; - - return { - isRequestingSiteIcon: isResolving( 'core', 'getEntityRecord', [ - 'root', - '__unstableBase', - undefined, - ] ), - siteIconUrl: siteData.siteIconUrl, - }; - } ); - - if ( siteIconUrl ) { - buttonIcon = ( - { - ); - } else if ( isRequestingSiteIcon ) { - buttonIcon = null; - } - - const className = clsx( 'woocommerce-navigation-header', { - 'is-scrolled': isScrolled, - } ); - - return ( -
- - -
- ); -}; - -export default Header; diff --git a/plugins/woocommerce-admin/client/navigation/components/header/style.scss b/plugins/woocommerce-admin/client/navigation/components/header/style.scss deleted file mode 100644 index fa835f9c737..00000000000 --- a/plugins/woocommerce-admin/client/navigation/components/header/style.scss +++ /dev/null @@ -1,39 +0,0 @@ -.woocommerce-navigation-header { - display: flex; - align-items: center; - border: none; - border-radius: 0; - height: auto; - - .woocommerce-navigation-header__site-icon.components-button { - padding: 12px; - height: 60px; - color: #fff; - - &:hover, - &:focus, - &:not([aria-disabled="true"]):active { - color: #fff; - } - } - - .woocommerce-navigation-header__site-title.components-button { - padding-left: 0; - color: $gray-400; - font-weight: 600; - - &:hover, - &:focus, - &:active { - color: $gray-200; - } - } - - .woocommerce-navigation-header__site-title { - padding-top: 0; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - } -} diff --git a/plugins/woocommerce-admin/client/navigation/components/header/test/index.js b/plugins/woocommerce-admin/client/navigation/components/header/test/index.js deleted file mode 100644 index 0225d0c2273..00000000000 --- a/plugins/woocommerce-admin/client/navigation/components/header/test/index.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * External dependencies - */ -import { render } from '@testing-library/react'; -import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import Header from '../'; - -global.window.wcNavigation = {}; - -jest.mock( '@woocommerce/settings', () => ( { - ...jest.requireActual( '@woocommerce/settings' ), - getSetting: jest.fn( ( setting ) => { - const settings = { - homeUrl: 'https://fake-site-url.com', - siteTitle: 'Fake & Title <3', - }; - return settings[ setting ]; - } ), -} ) ); - -jest.mock( '@wordpress/data', () => { - // Require the original module to not be mocked... - const originalModule = jest.requireActual( '@wordpress/data' ); - - return { - __esModule: true, // Use it when dealing with esModules - ...originalModule, - useDispatch: jest.fn().mockReturnValue( {} ), - useSelect: jest.fn().mockReturnValue( {} ), - }; -} ); - -describe( 'Header', () => { - test( 'should not show the button when the site icon is requesting', () => { - useSelect.mockImplementation( () => ( { - isRequestingSiteIcon: true, - siteIconUrl: null, - } ) ); - - const { container } = render(
); - - expect( container.querySelector( 'img' ) ).toBeNull(); - } ); - - test( 'should show the button when the site icon has resolved', () => { - useSelect.mockImplementation( () => ( { - isRequestingSiteIcon: false, - siteIconUrl: '#', - } ) ); - - const { container } = render(
); - - expect( container.querySelector( 'img' ) ).not.toBeNull(); - } ); - - test( 'should start with the nav expanded in larger viewports', () => { - Object.defineProperty( window.HTMLElement.prototype, 'clientWidth', { - configurable: true, - value: 2000, - } ); - - render(
); - - expect( Object.values( document.body.classList ) ).toContain( - 'is-wc-nav-expanded' - ); - } ); - - test( 'should start with the nav folded when the viewport is smaller', () => { - Object.defineProperty( window.HTMLElement.prototype, 'clientWidth', { - configurable: true, - value: 480, - } ); - - render(
); - - expect( Object.values( document.body.classList ) ).toContain( - 'is-wc-nav-folded' - ); - } ); - - test( 'should decode site titles', () => { - const { container } = render(
); - - expect( - container.querySelector( - '.woocommerce-navigation-header__site-title' - ).textContent - ).toBe( 'Fake & Title <3' ); - } ); -} ); diff --git a/plugins/woocommerce-admin/client/navigation/components/intro-modal/images/nav-intro-1.png b/plugins/woocommerce-admin/client/navigation/components/intro-modal/images/nav-intro-1.png deleted file mode 100644 index 3beff125f77..00000000000 Binary files a/plugins/woocommerce-admin/client/navigation/components/intro-modal/images/nav-intro-1.png and /dev/null differ diff --git a/plugins/woocommerce-admin/client/navigation/components/intro-modal/images/nav-intro-2.png b/plugins/woocommerce-admin/client/navigation/components/intro-modal/images/nav-intro-2.png deleted file mode 100644 index fe69a4be837..00000000000 Binary files a/plugins/woocommerce-admin/client/navigation/components/intro-modal/images/nav-intro-2.png and /dev/null differ diff --git a/plugins/woocommerce-admin/client/navigation/components/intro-modal/images/nav-intro-3.png b/plugins/woocommerce-admin/client/navigation/components/intro-modal/images/nav-intro-3.png deleted file mode 100644 index ec1f6aaef34..00000000000 Binary files a/plugins/woocommerce-admin/client/navigation/components/intro-modal/images/nav-intro-3.png and /dev/null differ diff --git a/plugins/woocommerce-admin/client/navigation/components/intro-modal/index.js b/plugins/woocommerce-admin/client/navigation/components/intro-modal/index.js deleted file mode 100644 index 6925ac3f288..00000000000 --- a/plugins/woocommerce-admin/client/navigation/components/intro-modal/index.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Guide } from '@wordpress/components'; -import { OPTIONS_STORE_NAME } from '@woocommerce/data'; -import { recordEvent } from '@woocommerce/tracks'; -import { Text } from '@woocommerce/experimental'; -import { useState } from '@wordpress/element'; -import { useSelect, useDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import './style.scss'; -import NavInto1 from './images/nav-intro-1.png'; -import NavInto2 from './images/nav-intro-2.png'; -import NavInto3 from './images/nav-intro-3.png'; - -export const INTRO_MODAL_DISMISSED_OPTION_NAME = - 'woocommerce_navigation_intro_modal_dismissed'; - -export const IntroModal = () => { - const [ isOpen, setOpen ] = useState( true ); - - const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); - - const { isDismissed, isResolving } = useSelect( ( select ) => { - const { getOption, isResolving: isOptionResolving } = - select( OPTIONS_STORE_NAME ); - const dismissedOption = getOption( INTRO_MODAL_DISMISSED_OPTION_NAME ); - - return { - isDismissed: dismissedOption === 'yes', - isResolving: - typeof dismissedOption === 'undefined' || - isOptionResolving( 'getOption', [ - INTRO_MODAL_DISMISSED_OPTION_NAME, - ] ), - }; - } ); - - const dismissModal = () => { - updateOptions( { - [ INTRO_MODAL_DISMISSED_OPTION_NAME ]: 'yes', - } ); - recordEvent( 'navigation_intro_modal_close', {} ); - setOpen( false ); - }; - - if ( ! isOpen || isDismissed || isResolving ) { - return null; - } - - const getPage = ( title, description, imageUrl ) => { - return { - content: ( -
-
- - { title } - - - { description } - -
-
- { -
-
- ), - }; - }; - - return ( - - ); -}; diff --git a/plugins/woocommerce-admin/client/navigation/components/intro-modal/style.scss b/plugins/woocommerce-admin/client/navigation/components/intro-modal/style.scss deleted file mode 100644 index 0b7f00c33cb..00000000000 --- a/plugins/woocommerce-admin/client/navigation/components/intro-modal/style.scss +++ /dev/null @@ -1,76 +0,0 @@ -.woocommerce-navigation-intro-modal { - width: 670px; - - @include breakpoint( "<782px" ) { - width: 350px; - } - - .components-guide__page-control { - order: 3; - margin: $gap 0; - - li { - margin: 0; - } - } - - .components-modal__header { - display: none; - } - - .components-guide__container { - margin-top: 0; - } - - .components-guide__footer { - box-sizing: border-box; - margin: 0; - height: 0; - overflow: visible; - - .components-button { - position: absolute; - bottom: 100%; - margin-bottom: $gap; - } - - .components-guide__back-button { - display: none; - } - } - - &.components-modal__frame.components-guide { - height: auto; - } - - .woocommerce-navigation-intro-modal__page-wrapper { - display: grid; - grid-template-columns: 1fr 1fr; - - img { - max-width: 100%; - } - - @include breakpoint( "<782px" ) { - grid-template-columns: 1fr; - - .woocommerce-navigation-intro-modal__image-wrapper { - grid-row: 1; - max-height: 328px; - overflow: hidden; - } - } - } - - .woocommerce-navigation-intro-modal__page-text { - padding: $gap-large; - display: flex; - flex-direction: column; - justify-content: center; - - h2 { - font-weight: bold; - margin-bottom: $gap-smaller; - } - } -} diff --git a/plugins/woocommerce-admin/client/navigation/components/intro-modal/test/index.js b/plugins/woocommerce-admin/client/navigation/components/intro-modal/test/index.js deleted file mode 100644 index f81a797472b..00000000000 --- a/plugins/woocommerce-admin/client/navigation/components/intro-modal/test/index.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * External dependencies - */ -import { fireEvent, render, screen } from '@testing-library/react'; -import { useDispatch, useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { IntroModal, INTRO_MODAL_DISMISSED_OPTION_NAME } from '../'; - -global.window.wcNavigation = {}; - -jest.mock( '@wordpress/data', () => { - // Require the original module to not be mocked... - const originalModule = jest.requireActual( '@wordpress/data' ); - - return { - __esModule: true, // Use it when dealing with esModules - ...originalModule, - useDispatch: jest.fn().mockReturnValue( {} ), - useSelect: jest.fn().mockReturnValue( {} ), - }; -} ); - -describe( 'IntroModal', () => { - test( 'should not show when modal options are resolving', () => { - useSelect.mockImplementation( () => ( { - isResolving: true, - } ) ); - - const { container } = render( ); - - expect( container ).toBeEmptyDOMElement(); - } ); - - test( 'should not dismiss when the modal has already been dismissed', () => { - const updateOptions = jest.fn(); - useSelect.mockImplementation( () => ( { - isDismissed: true, - isResolving: false, - } ) ); - useDispatch.mockImplementation( () => ( { - updateOptions, - } ) ); - - const { container } = render( ); - - expect( container ).toBeEmptyDOMElement(); - expect( updateOptions ).not.toHaveBeenCalled(); - } ); - - test( 'should hide and update the dismissal option when closing the modal', () => { - const updateOptions = jest.fn(); - useSelect.mockImplementation( () => ( { - isResolving: false, - isDismissed: false, - } ) ); - useDispatch.mockImplementation( () => ( { - updateOptions, - } ) ); - - render( ); - - fireEvent.click( screen.queryByLabelText( 'Close dialog' ) ); - - expect( - screen.queryByText( 'A new navigation for WooCommerce' ) - ).toBeNull(); - expect( updateOptions ).toHaveBeenCalledWith( { - [ INTRO_MODAL_DISMISSED_OPTION_NAME ]: 'yes', - } ); - } ); -} ); diff --git a/plugins/woocommerce-admin/client/navigation/index.js b/plugins/woocommerce-admin/client/navigation/index.js deleted file mode 100644 index 9f0898b4317..00000000000 --- a/plugins/woocommerce-admin/client/navigation/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * External dependencies - */ -import { withNavigationHydration } from '@woocommerce/data'; - -/** - * Internal dependencies - */ -import './style.scss'; -import Container from './components/container'; - -const HydratedNavigation = withNavigationHydration( window.wcNavigation )( - Container -); - -export default HydratedNavigation; diff --git a/plugins/woocommerce-admin/client/navigation/style.scss b/plugins/woocommerce-admin/client/navigation/style.scss deleted file mode 100644 index 3d4098ebeb3..00000000000 --- a/plugins/woocommerce-admin/client/navigation/style.scss +++ /dev/null @@ -1,167 +0,0 @@ -@import "./stylesheets/variables.scss"; -@import "./components/container/style.scss"; -@import "./components/header/style.scss"; - -.woocommerce-navigation { - position: relative; - width: $navigation-width; - box-sizing: border-box; - background-color: $gray-900; - z-index: 1100; //Must be greater than z-index on .woocommerce-layout__header - - @media ( max-width: 960px ) { - width: $header-height; - height: $header-height; - } - - .components-navigation { - box-sizing: border-box; - } - - .components-navigation__menu-title { - overflow: visible; - } - - .components-navigation__menu { - scrollbar-color: $gray-700 $gray-900; - scrollbar-width: thin; - - &::-webkit-scrollbar-thumb { - border-radius: 10px; - background-color: $gray-700; - } - - - &::-webkit-scrollbar-thumb:hover { - background-color: $gray-700; - width: 8px; - height: 8px; - } - - - &::-webkit-scrollbar { - width: 8px; - height: 8px; - } - } -} - -.woocommerce-navigation__wrapper { - background-color: $gray-900; - position: absolute; - top: $header-height; - width: 100%; - height: calc(100vh - #{$header-height + $adminbar-height}); - overflow-y: auto; -} - -.is-wp-toolbar-disabled .woocommerce-navigation__wrapper { - height: calc(100vh - #{$header-height}); -} - -body.is-wc-nav-expanded { - .woocommerce-navigation { - width: $navigation-width; - height: 100%; - } - - font > .xdebug-error { - margin-left: calc(#{$navigation-width} + #{$gap}); - } -} - -body.is-wc-nav-folded { - .woocommerce-navigation { - width: $header-height; - height: $header-height; - overflow: hidden; - - .woocommerce-navigation-header { - > * { - display: none; - } - } - - .woocommerce-navigation-header__site-icon { - display: block; - } - - .components-navigation { - display: none; - } - } - - .woocommerce-transient-notices { - left: $gap; - } - - #wpbody { - padding-left: 0; - } -} - -.has-woocommerce-navigation { - #adminmenuwrap, - #adminmenuback { - display: none !important; - } - - &.woocommerce_page_wc-reports, - &.woocommerce_page_wc-settings, - &.woocommerce_page_wc-status { - .woo-nav-tab-wrapper { - display: none; - } - - .woocommerce .subsubsub { - font-size: 14px; - margin: 5px 0; - } - } - - #wpcontent, - #wpfooter { - margin-left: 0; - - @media ( max-width: 960px ) { - margin-left: 0; - } - } - - #wpbody { - padding-left: $navigation-width; - - @media ( max-width: 960px ) { - padding-left: 0; - } - } - - .woocommerce-layout__header.is-embed-loading { - &::before { - content: ""; - position: fixed; - width: $navigation-width; - height: 100%; - background: $gray-900; - - @include breakpoint( "<960px" ) { - width: $header-height; - height: $header-height; - } - } - } - - #woocommerce-embedded-root.is-embed-loading { - margin-bottom: -$adminbar-height; - } - - &:not(.is-wp-toolbar-disabled) { - #wpbody-content { - margin-top: $adminbar-height; - } - } - - font > .xdebug-error { - margin-top: $header-height; - } -} diff --git a/plugins/woocommerce-admin/client/navigation/stylesheets/_variables.scss b/plugins/woocommerce-admin/client/navigation/stylesheets/_variables.scss deleted file mode 100644 index ba6cc601576..00000000000 --- a/plugins/woocommerce-admin/client/navigation/stylesheets/_variables.scss +++ /dev/null @@ -1,22 +0,0 @@ -// WooCommerce Navigation -$navigation-width: 240px; -$navigation-x-padding: 30px; - -// WordPress defaults. -$admin-menu-width: 160px; -$admin-bar-height: 32px; -$admin-bar-height-mobile: 46px; - -// Gaps and gutters. -$fallback-gutter: 24px; -$fallback-gutter-large: 40px; -$gutter: var(--main-gap); -$gutter-large: var(--large-gap); - -$gap-largest: 40px; -$gap-larger: 36px; -$gap-large: 24px; -$gap: 16px; -$gap-small: 12px; -$gap-smaller: 8px; -$gap-smallest: 4px; diff --git a/plugins/woocommerce-admin/client/navigation/test/sample-test.js b/plugins/woocommerce-admin/client/navigation/test/sample-test.js deleted file mode 100644 index b68cb362003..00000000000 --- a/plugins/woocommerce-admin/client/navigation/test/sample-test.js +++ /dev/null @@ -1,5 +0,0 @@ -describe( 'Sample test', () => { - it( 'should pass', () => { - expect( true ).not.toBeNull(); - } ); -} ); diff --git a/plugins/woocommerce-admin/client/navigation/test/utils.js b/plugins/woocommerce-admin/client/navigation/test/utils.js deleted file mode 100644 index 477f635b6a1..00000000000 --- a/plugins/woocommerce-admin/client/navigation/test/utils.js +++ /dev/null @@ -1,521 +0,0 @@ -/** - * External dependencies - */ -import { getAdminLink } from '@woocommerce/settings'; - -/** - * Internal dependencies - */ -import { - getDefaultMatchExpression, - getFullUrl, - getMappedItemsCategories, - getMatchingItem, - getMatchScore, - sortMenuItems, -} from '../utils'; - -const originalLocation = window.location; -global.window = Object.create( window ); -global.window.wcNavigation = {}; - -const sampleMenuItems = [ - { - id: 'main', - title: 'Main page', - url: 'admin.php?page=wc-admin', - }, - { - id: 'path', - title: 'Page with Path', - url: 'admin.php?page=wc-admin&path=/test-path', - }, - { - id: 'hash', - title: 'Page with Hash', - url: 'admin.php?page=wc-admin&path=/test-path#anchor', - }, - { - id: 'multiple-args', - title: 'Page with multiple arguments', - url: 'admin.php?page=wc-admin&path=/test-path§ion=section-name', - }, - { - id: 'multiple-args-plus-one', - title: 'Page with same multiple arguments plus an additional one', - url: 'admin.php?page=wc-admin&path=/test-path§ion=section-name&version=22', - }, - { - id: 'hash-and-multiple-args', - title: 'Page with multiple arguments and a hash', - url: 'admin.php?page=wc-admin&path=/test-path§ion=section-name#anchor', - }, -]; - -const runGetMatchingItemTests = ( items ) => { - it( 'should get the closest matched item', () => { - window.location = new URL( getAdminLink( 'admin.php?page=wc-admin' ) ); - const matchingItem = getMatchingItem( items ); - expect( matchingItem.id ).toBe( 'main' ); - } ); - - it( 'should match the item without hash if a better match does not exist', () => { - window.location = new URL( - getAdminLink( 'admin.php?page=wc-admin#hash' ) - ); - const matchingItem = getMatchingItem( items ); - expect( matchingItem.id ).toBe( 'main' ); - } ); - - it( 'should exactly match the item with a hash if it exists', () => { - window.location = new URL( - getAdminLink( 'admin.php?page=wc-admin&path=/test-path#anchor' ) - ); - const matchingItem = getMatchingItem( items ); - expect( matchingItem.id ).toBe( 'hash' ); - } ); - - it( 'should roughly match the item if all menu item arguments exist', () => { - window.location = new URL( - getAdminLink( - 'admin.php?page=wc-admin&path=/test-path§ion=section-name' - ) - ); - const matchingItem = getMatchingItem( items ); - expect( matchingItem.id ).toBe( 'multiple-args' ); - } ); - - it( 'should match an item with irrelevant query parameters', () => { - window.location = new URL( - getAdminLink( - 'admin.php?page=wc-admin&path=/test-path§ion=section-name&foo=bar' - ) - ); - const matchingItem = getMatchingItem( items ); - expect( matchingItem.id ).toBe( 'multiple-args' ); - } ); - - it( 'should match an item with similar query args plus one additional arg', () => { - window.location = new URL( - getAdminLink( - 'admin.php?page=wc-admin&path=/test-path§ion=section-name&version=22' - ) - ); - const matchingItem = getMatchingItem( items ); - expect( matchingItem.id ).toBe( 'multiple-args-plus-one' ); - } ); - - it( 'should match an item with query parameters in mixed order', () => { - window.location = new URL( - getAdminLink( - 'admin.php?foo=bar&page=wc-admin&path=/test-path§ion=section-name' - ) - ); - const matchingItem = getMatchingItem( items ); - expect( matchingItem.id ).toBe( 'multiple-args' ); - } ); - - it( 'should match an item with query parameters and a hash', () => { - window.location = new URL( - getAdminLink( - 'admin.php?foo=bar&page=wc-admin&path=/test-path§ion=section-name#anchor' - ) - ); - const matchingItem = getMatchingItem( items ); - expect( matchingItem.id ).toBe( 'hash-and-multiple-args' ); - } ); -}; - -describe( 'getMatchingItem', () => { - beforeAll( () => { - delete window.location; - } ); - - afterAll( () => { - window.location = originalLocation; - } ); - - runGetMatchingItemTests( sampleMenuItems ); - // re-run the tests with sampleMenuItems in reverse order. - runGetMatchingItemTests( sampleMenuItems.reverse() ); -} ); - -describe( 'getDefaultMatchExpression', () => { - it( 'should return the regex for the path without query args', () => { - expect( getDefaultMatchExpression( 'http://wordpress.org' ) ).toBe( - '^http:\\/\\/wordpress\\.org' - ); - } ); - - it( 'should return the regex for the path and query args', () => { - expect( - getDefaultMatchExpression( - 'http://wordpress.org?param1=a¶m2=b' - ) - ).toBe( - '^http:\\/\\/wordpress\\.org(?=.*[?|&]param1=a(&|$|#))(?=.*[?|&]param2=b(&|$|#))' - ); - } ); - - it( 'should return the regex with hash if present', () => { - expect( - getDefaultMatchExpression( - 'http://wordpress.org?param1=a¶m2=b#hash' - ) - ).toBe( - '^http:\\/\\/wordpress\\.org(?=.*[?|&]param1=a(&|$|#))(?=.*[?|&]param2=b(&|$|#))(.*#hash$)' - ); - } ); -} ); - -describe( 'getMatchScore', () => { - beforeAll( () => { - delete window.location; - window.location = new URL( getAdminLink( '/' ) ); - } ); - - afterAll( () => { - window.location = originalLocation; - } ); - - it( 'should return max safe integer if the url is an exact match', () => { - expect( - getMatchScore( - new URL( getAdminLink( 'admin.php?page=testpage' ) ), - getAdminLink( 'admin.php?page=testpage' ) - ) - ).toBe( Number.MAX_SAFE_INTEGER ); - } ); - - it( 'should return matching path and parameter count', () => { - expect( - getMatchScore( - new URL( - getFullUrl( - '/wp-admin/admin.php?page=testpage&extra_param=a' - ) - ), - '/wp-admin/admin.php?page=testpage' - ) - ).toBe( 2 ); - } ); - - it( 'should return 0 if the URL does not meet match criteria', () => { - expect( - getMatchScore( - new URL( getAdminLink( 'admin.php?page=different-page' ) ), - getAdminLink( 'admin.php?page=testpage' ) - ) - ).toBe( 0 ); - } ); - - it( 'should return match count for a custom match expression', () => { - expect( - getMatchScore( - new URL( - getAdminLink( 'admin.php?page=different-page¶m1=a' ) - ), - getAdminLink( 'admin.php?page=testpage' ), - 'param1=a' - ) - ).toBe( 1 ); - } ); - - it( 'should return 0 for custom match expression that does not match', () => { - expect( - getMatchScore( - new URL( - getAdminLink( 'admin.php?page=different-page¶m1=b' ) - ), - getAdminLink( 'admin.php?page=testpage' ), - 'param1=a' - ) - ).toBe( 0 ); - } ); - - it( 'should return match count if params match but are out of order', () => { - expect( - getMatchScore( - new URL( getAdminLink( 'admin.php?param1=a&page=testpage' ) ), - getAdminLink( 'admin.php?page=testpage' ) - ) - ).toBe( 2 ); - } ); - - it( 'should return match count if multiple params match but are out of order', () => { - expect( - getMatchScore( - new URL( - getAdminLink( 'admin.php?param1=a&page=testpage¶m2=b' ) - ), - getAdminLink( 'admin.php?page=testpage¶m1=a' ) - ) - ).toBe( 3 ); - } ); -} ); - -describe( 'getFullUrl', () => { - beforeAll( () => { - delete window.location; - window.location = new URL( getAdminLink( '/' ) ); - } ); - - afterAll( () => { - window.location = originalLocation; - } ); - - it( 'should get the full admin URL from a path', () => { - expect( getFullUrl( 'admin.php?page=testpage' ) ).toBe( - getAdminLink( 'admin.php?page=testpage' ) - ); - } ); - - it( 'should return the same URL from an already complete URL', () => { - expect( getFullUrl( getAdminLink( 'admin.php?page=testpage' ) ) ).toBe( - getAdminLink( 'admin.php?page=testpage' ) - ); - } ); -} ); - -describe( 'sortMenuItems', () => { - it( 'should return an array of items sorted by the order property', () => { - const menuItems = [ - { id: 'second', title: 'second', order: 2 }, - { id: 'first', title: 'three', order: 1 }, - { id: 'third', title: 'four', order: 3 }, - ]; - - const sortedItems = sortMenuItems( menuItems ); - - expect( sortedItems[ 0 ].id ).toBe( 'first' ); - expect( sortedItems[ 1 ].id ).toBe( 'second' ); - expect( sortedItems[ 2 ].id ).toBe( 'third' ); - } ); - - it( 'should sort items alphabetically if order is the same', () => { - const menuItems = [ - { id: 'third', title: 'z', order: 2 }, - { id: 'first', title: 'first', order: 1 }, - { id: 'second', title: 'a', order: 2 }, - ]; - - const sortedItems = sortMenuItems( menuItems ); - - expect( sortedItems[ 0 ].id ).toBe( 'first' ); - expect( sortedItems[ 1 ].id ).toBe( 'second' ); - expect( sortedItems[ 2 ].id ).toBe( 'third' ); - } ); -} ); - -describe( 'getMappedItemsCategories', () => { - it( 'should get the default category when none are provided', () => { - const menuItems = [ - { - id: 'child-one', - title: 'child-one', - isCategory: false, - parent: 'woocommerce', - menuId: 'plugins', - }, - ]; - const { categories, items } = getMappedItemsCategories( menuItems ); - - expect( items.woocommerce ).toBeDefined(); - expect( items.woocommerce.plugins ).toBeDefined(); - expect( items.woocommerce.plugins.length ).toBe( 1 ); - - expect( Object.keys( categories ).length ).toBe( 1 ); - expect( categories.woocommerce ).toBeDefined(); - } ); - - it( 'should get a map of all items and categories', () => { - const menuItems = [ - { - id: 'child-one', - title: 'child-one', - isCategory: false, - parent: 'parent', - menuId: 'plugins', - }, - { - id: 'child-two', - title: 'child-two', - isCategory: false, - parent: 'parent', - menuId: 'plugins', - }, - { - id: 'parent', - title: 'parent', - isCategory: true, - parent: 'woocommerce', - menuId: 'plugins', - }, - ]; - const { categories, items } = getMappedItemsCategories( menuItems ); - - expect( items.woocommerce ).toBeDefined(); - expect( items.woocommerce.plugins ).toBeDefined(); - expect( items.woocommerce.plugins.length ).toBe( 1 ); - - expect( items.parent ).toBeDefined(); - expect( items.parent.plugins ).toBeDefined(); - expect( items.parent.plugins.length ).toBe( 2 ); - - expect( Object.keys( categories ).length ).toBe( 2 ); - expect( categories.parent ).toBeDefined(); - expect( categories.woocommerce ).toBeDefined(); - } ); - - it( 'should handle multiple depths', () => { - const menuItems = [ - { - id: 'grand-child', - title: 'grand-child', - isCategory: false, - parent: 'child', - menuId: 'plugins', - }, - { - id: 'child', - title: 'child', - isCategory: true, - parent: 'grand-parent', - menuId: 'plugins', - }, - { - id: 'grand-parent', - title: 'grand-parent', - isCategory: true, - parent: 'woocommerce', - menuId: 'plugins', - }, - ]; - const { categories, items } = getMappedItemsCategories( menuItems ); - - expect( items[ 'grand-parent' ] ).toBeDefined(); - expect( items[ 'grand-parent' ] ).toBeDefined(); - expect( items[ 'grand-parent' ].plugins.length ).toBe( 1 ); - - expect( items.child ).toBeDefined(); - expect( items.child.plugins.length ).toBe( 1 ); - - expect( items[ 'grand-child' ] ).not.toBeDefined(); - - expect( Object.keys( categories ).length ).toBe( 3 ); - } ); - - it( 'should group by menuId', () => { - const menuItems = [ - { - id: 'parent', - title: 'parent', - isCategory: true, - parent: 'woocommerce', - menuId: 'primary', - }, - { - id: 'primary-one', - title: 'primary-one', - isCategory: false, - parent: 'parent', - menuId: 'primary', - }, - { - id: 'primary-two', - title: 'primary-two', - isCategory: false, - parent: 'parent', - menuId: 'primary', - }, - ]; - const { items } = getMappedItemsCategories( menuItems ); - - expect( items.parent ).toBeDefined(); - expect( items.parent.primary ).toBeDefined(); - expect( items.parent.primary.length ).toBe( 2 ); - } ); - - it( 'should group children only if their menuId matches parent', () => { - const menuItems = [ - { - id: 'plugin-one', - title: 'plugin-one', - isCategory: false, - parent: 'parent', - menuId: 'plugins', - }, - { - id: 'plugin-two', - title: 'plugin-two', - isCategory: false, - parent: 'parent', - menuId: 'plugins', - }, - { - id: 'parent', - title: 'parent', - isCategory: true, - parent: 'woocommerce', - menuId: 'plugins', - }, - { - id: 'primary-one', - title: 'primary-one', - isCategory: false, - parent: 'parent', - menuId: 'primary', - }, - { - id: 'primary-two', - title: 'primary-two', - isCategory: false, - parent: 'parent', - menuId: 'primary', - }, - ]; - const { items } = getMappedItemsCategories( menuItems ); - - expect( items.parent ).toBeDefined(); - expect( items.parent.plugins ).toBeDefined(); - expect( items.parent.plugins.length ).toBe( 2 ); - - expect( items.primary ).not.toBeDefined(); - } ); - - it( 'should ignore bad menu IDs', () => { - const menuItems = [ - { - id: 'parent', - title: 'parent', - isCategory: false, - parent: 'woocommerce', - menuId: 'badId', - }, - { - id: 'primary-one', - title: 'primary-one', - isCategory: false, - parent: 'woocommerce', - menuId: 'primary', - }, - { - id: 'primary-two', - title: 'primary-two', - isCategory: false, - parent: 'woocommerce', - menuId: 'primary', - }, - ]; - const { categories, items } = getMappedItemsCategories( menuItems ); - - expect( items.woocommerce ).toBeDefined(); - expect( items.woocommerce.primary ).toBeDefined(); - expect( items.woocommerce.primary.length ).toBe( 2 ); - - expect( items.woocommerce ).toBeDefined(); - expect( items.woocommerce.badId ).not.toBeDefined(); - - expect( Object.keys( categories ).length ).toBe( 1 ); - } ); -} ); diff --git a/plugins/woocommerce-admin/client/navigation/utils.ts b/plugins/woocommerce-admin/client/navigation/utils.ts deleted file mode 100644 index 04c1bc4b1ac..00000000000 --- a/plugins/woocommerce-admin/client/navigation/utils.ts +++ /dev/null @@ -1,259 +0,0 @@ -/** - * External dependencies - */ -import { getAdminLink } from '@woocommerce/settings'; - -type MenuId = 'primary' | 'favorites' | 'plugins' | 'secondary'; - -interface Item { - id: string; - matchExpression: string; - url: string; - order: number; - title: string; - parent: string; - menuId: MenuId; - capability: string; - isCategory: boolean; -} - -interface Category { - id: string; - isCategory: boolean; - menuId: MenuId; - migrate: boolean; - order: number; - parent: string; - title: string; - primary?: Item[]; - favorites?: Item[]; - plugins?: Item[]; - secondary?: Item[]; -} - -/** - * Get the full URL if a relative path is passed. - */ -export const getFullUrl = ( url: string ): string => { - if ( url.indexOf( 'http' ) === 0 ) { - return url; - } - - return getAdminLink( url ); -}; - -/** - * Get a default regex expression to match the path and provided params. - */ -export const getDefaultMatchExpression = ( url: string ): string => { - const escapedUrl = url.replace( /[-\/\\^$*+?.()|[\]{}]/gi, '\\$&' ); - const [ path, args, hash ] = escapedUrl.split( /\\\?|#/ ); - const hashExpression = hash ? `(.*#${ hash }$)` : ''; - const argsExpression = args - ? args.split( '&' ).reduce( ( acc, param ) => { - return `${ acc }(?=.*[?|&]${ param }(&|$|#))`; - }, '' ) - : ''; - return '^' + path + argsExpression + hashExpression; -}; - -/** - * Get a match score for a menu item given a location. - */ -export const getMatchScore = ( - location: Location, - itemUrl: string, - itemExpression: string | null = null -): number => { - if ( ! itemUrl ) { - return 0; - } - - const fullUrl = getFullUrl( itemUrl ); - const { href } = location; - - // Return highest possible score for exact match. - if ( fullUrl === href ) { - return Number.MAX_SAFE_INTEGER; - } - - const defaultExpression = getDefaultMatchExpression( fullUrl ); - const regexp = new RegExp( itemExpression || defaultExpression, 'i' ); - return ( decodeURIComponent( href ).match( regexp ) || [] ).length; -}; - -interface wcNavigation { - menuItems: Item[]; - rootBackLabel: string; - rootBackUrl: string; - historyPatched: boolean; -} - -declare global { - interface Window { - wcNavigation: wcNavigation; - } -} - -interface wcNavigation { - menuItems: Item[]; - rootBackLabel: string; - rootBackUrl: string; - historyPatched: boolean; -} - -declare global { - interface Window { - wcNavigation: wcNavigation; - } -} - -/** - * Get the closest matching item. - * - * @param {Array} items An array of items to match against. - */ -export const getMatchingItem = ( items: Item[] ): Item | null => { - let matchedItem = null; - let highestMatchScore = 0; - - items.forEach( ( item ) => { - const score = getMatchScore( - window.location, - item.url, - item.matchExpression - ); - if ( score > 0 && score >= highestMatchScore ) { - highestMatchScore = score; - matchedItem = item; - } - } ); - - return matchedItem || null; -}; - -/** - * Available menu IDs. - */ -export const menuIds: MenuId[] = [ - 'primary', - 'favorites', - 'plugins', - 'secondary', -]; - -interface Category { - id: string; - isCategory: boolean; - menuId: MenuId; - migrate: boolean; - order: number; - parent: string; - title: string; - primary?: Item[]; - favorites?: Item[]; - plugins?: Item[]; - secondary?: Item[]; -} - -/** - * Default categories for the menu. - */ -export const defaultCategories: { - [ key: string ]: Category; -} = { - woocommerce: { - id: 'woocommerce', - isCategory: true, - menuId: 'primary', - migrate: true, - order: 10, - parent: '', - title: 'WooCommerce', - }, -}; - -/** - * Sort an array of menu items by their order property. - * - * @param {Array} menuItems Array of menu items. - * @return {Array} Sorted menu items. - */ -export const sortMenuItems = ( menuItems: Item[] ): Item[] => { - return menuItems.sort( ( a, b ) => { - if ( a.order === b.order ) { - return a.title.localeCompare( b.title ); - } - - return a.order - b.order; - } ); -}; - -/** - * Get a flat tree structure of all Categories and their children grouped by menuId - * - * @param {Array} menuItems Array of menu items. - * @param {Function} currentUserCan Callback method passed the capability to determine if a menu item is visible. - * @return {Object} Mapped menu items and categories. - */ -export const getMappedItemsCategories = ( - menuItems: Item[], - currentUserCan: ( capability: string ) => boolean -): { - items: Record< string, Category | Record< string, Item[] > >; - categories: Record< string, Category | Item >; -} => { - const categories: { - [ key: string ]: Category | Item; - } = { ...defaultCategories }; - - const items = sortMenuItems( menuItems ).reduce( - ( - acc: { - [ key: string ]: Category | { [ key: string ]: Item[] }; - }, - item: Item - ) => { - // Set up the category if it doesn't yet exist. - if ( ! acc[ item.parent ] ) { - acc[ item.parent ] = {}; - menuIds.forEach( ( menuId ) => { - acc[ item.parent ][ menuId ] = []; - } ); - } - - // Incorrect menu ID. - if ( ! acc[ item.parent ][ item.menuId ] ) { - return acc; - } - - // User does not have permission to view this item. - if ( - currentUserCan && - item.capability && - ! currentUserCan( item.capability ) - ) { - return acc; - } - - // Add categories. - if ( item.isCategory ) { - categories[ item.id ] = item; - } - - const menuIdArray = acc[ item.parent ][ item.menuId ]; - - if ( menuIdArray ) { - menuIdArray.push( item ); - } - - return acc; - }, - {} - ); - - return { - items, - categories, - }; -}; diff --git a/plugins/woocommerce-admin/client/settings/settings-slots.js b/plugins/woocommerce-admin/client/settings/settings-slots.js index 13a355e2f91..bfa55ae964b 100644 --- a/plugins/woocommerce-admin/client/settings/settings-slots.js +++ b/plugins/woocommerce-admin/client/settings/settings-slots.js @@ -22,6 +22,10 @@ export const possiblyRenderSettingsSlots = () => { id: 'wc_settings_site_visibility_slotfill', scope: 'woocommerce-site-visibility-settings', }, + { + id: 'wc_settings_blueprint_slotfill', + scope: 'woocommerce-blueprint-settings', + }, ]; slots.forEach( ( slot ) => { diff --git a/plugins/woocommerce-admin/client/stylesheets/shared/_reset.scss b/plugins/woocommerce-admin/client/stylesheets/shared/_reset.scss index e1ea49f6558..7e8c69202ce 100644 --- a/plugins/woocommerce-admin/client/stylesheets/shared/_reset.scss +++ b/plugins/woocommerce-admin/client/stylesheets/shared/_reset.scss @@ -62,7 +62,7 @@ } @include breakpoint( "<600px" ) { - #wpadminbar { + div #wpadminbar { position: fixed; } diff --git a/plugins/woocommerce-admin/client/task-lists/components/task.tsx b/plugins/woocommerce-admin/client/task-lists/components/task.tsx index 67f12096dbc..ae38fd1267c 100644 --- a/plugins/woocommerce-admin/client/task-lists/components/task.tsx +++ b/plugins/woocommerce-admin/client/task-lists/components/task.tsx @@ -9,7 +9,7 @@ import { WooOnboardingTask } from '@woocommerce/onboarding'; import { getHistory, getNewPath } from '@woocommerce/navigation'; import { ONBOARDING_STORE_NAME, TaskType } from '@woocommerce/data'; import { useCallback } from '@wordpress/element'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, resolveSelect } from '@wordpress/data'; /** * Internal dependencies */ @@ -31,25 +31,28 @@ export const Task: React.FC< TaskProps > = ( { query, task } ) => { const { invalidateResolutionForStoreSelector, optimisticallyCompleteTask } = useDispatch( ONBOARDING_STORE_NAME ); - const updateBadge = useCallback( () => { - const badgeElements: Array< HTMLElement > | null = Array.from( - document.querySelectorAll( - '#adminmenu .woocommerce-task-list-remaining-tasks-badge' - ) + const updateBadge = useCallback( async () => { + const badgeElements = document.querySelectorAll( + '#adminmenu .woocommerce-task-list-remaining-tasks-badge' ); if ( ! badgeElements?.length ) { return; } - badgeElements.forEach( ( badgeElement ) => { - const currentBadgeCount = Number( badgeElement.innerText ); + const setupTaskList = await resolveSelect( + ONBOARDING_STORE_NAME + ).getTaskList( 'setup' ); + if ( ! setupTaskList ) { + return; + } - if ( currentBadgeCount === 1 ) { - badgeElement.remove(); - } else { - badgeElement.innerHTML = String( currentBadgeCount - 1 ); - } + const remainingTasksCount = setupTaskList.tasks.filter( + ( _task ) => ! _task.isComplete + ).length; + + badgeElements.forEach( ( badge ) => { + badge.textContent = remainingTasksCount.toString(); } ); }, [] ); @@ -64,7 +67,12 @@ export const Task: React.FC< TaskProps > = ( { query, task } ) => { invalidateResolutionForStoreSelector( 'getTaskLists' ); updateBadge(); }, - [ id ] + [ + id, + invalidateResolutionForStoreSelector, + optimisticallyCompleteTask, + updateBadge, + ] ); return ( diff --git a/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/components/Setup/Configure.js b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/components/Setup/Configure.js index 23886151378..f64becb576e 100644 --- a/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/components/Setup/Configure.js +++ b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/components/Setup/Configure.js @@ -150,7 +150,7 @@ export const Configure = ( { markConfigured, paymentGateway } ) => { { helpText || (

{ __( - "You can manage this payment gateway's settings by clicking the button below", + 'You can manage this payment gateway’s settings by clicking the button below', 'woocommerce' ) }

diff --git a/plugins/woocommerce-admin/client/task-lists/fills/components/load-sample-product-confirm-modal.tsx b/plugins/woocommerce-admin/client/task-lists/fills/components/load-sample-product-confirm-modal.tsx index 24423fc065f..4e0669b811b 100644 --- a/plugins/woocommerce-admin/client/task-lists/fills/components/load-sample-product-confirm-modal.tsx +++ b/plugins/woocommerce-admin/client/task-lists/fills/components/load-sample-product-confirm-modal.tsx @@ -28,7 +28,7 @@ export const LoadSampleProductConfirmModal: React.VFC< Props > = ( { > { __( - "We'll import images from WooCommerce.com to set up your sample products.", + 'We’ll import images from WooCommerce.com to set up your sample products.', 'woocommerce' ) } diff --git a/plugins/woocommerce-admin/client/task-lists/fills/import-products/test/index.tsx b/plugins/woocommerce-admin/client/task-lists/fills/import-products/test/index.tsx index 48d6275688c..d15b969ae51 100644 --- a/plugins/woocommerce-admin/client/task-lists/fills/import-products/test/index.tsx +++ b/plugins/woocommerce-admin/client/task-lists/fills/import-products/test/index.tsx @@ -19,7 +19,7 @@ global.fetch = jest.fn().mockImplementation( () => ); const confirmModalText = - "We'll import images from WooCommerce.com to set up your sample products."; + 'We’ll import images from WooCommerce.com to set up your sample products.'; describe( 'Products', () => { beforeEach( () => { diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/constants.tsx b/plugins/woocommerce-admin/client/task-lists/fills/products/constants.tsx index 1dd3bac2ee5..907c687a17b 100644 --- a/plugins/woocommerce-admin/client/task-lists/fills/products/constants.tsx +++ b/plugins/woocommerce-admin/client/task-lists/fills/products/constants.tsx @@ -82,9 +82,6 @@ export const PrintfulAdvertProductPlacement = { title: ( { __( 'Print-on-demand products', 'woocommerce' ) } -
- { __( 'Promoted', 'woocommerce' ) } -
), content: __( @@ -99,7 +96,11 @@ export const PrintfulAdvertProductPlacement = { src={ PrintfulIcon } /> ), - after: , + after: ( +
+ { __( 'Promoted', 'woocommerce' ) } +
+ ), onClick: () => { recordEvent( 'tasklist_product_printful_advert_click' ); window.open( 'https://woocommerce.com/products/printful', '_blank' ); @@ -116,7 +117,7 @@ export const ImportCSVItem = { content: __( 'Import your products from a CSV file.', 'woocommerce' ), className: 'woocommerce-products-list__item-advert', before: , - after: , + after: null, onClick: () => { recordEvent( 'tasklist_add_product', { method: 'import', diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/stack.scss b/plugins/woocommerce-admin/client/task-lists/fills/products/stack.scss index 13ad6bd76ff..45cfb87b5cf 100644 --- a/plugins/woocommerce-admin/client/task-lists/fills/products/stack.scss +++ b/plugins/woocommerce-admin/client/task-lists/fills/products/stack.scss @@ -57,17 +57,19 @@ flex-direction: row; align-items: center; gap: 8px; - - .woocommerce-label { - background-color: rgba(var(--wp-admin-theme-color--rgb), 0.1); - color: var(--wp-admin-theme-color, #000); - border-radius: 2px; - padding: 4px 8px; - font-weight: 400; - } } } + + .woocommerce-label { + background-color: rgba(var(--wp-admin-theme-color--rgb), 0.1); + color: var(--wp-admin-theme-color, #000); + border-radius: 2px; + padding: 4px 8px; + font-size: 12px; + font-weight: 400; + line-height: 16px; /* 133.333% */ + } } } diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/test/index.tsx b/plugins/woocommerce-admin/client/task-lists/fills/products/test/index.tsx index eaf725ef5e1..36bcd02d9b8 100644 --- a/plugins/woocommerce-admin/client/task-lists/fills/products/test/index.tsx +++ b/plugins/woocommerce-admin/client/task-lists/fills/products/test/index.tsx @@ -42,7 +42,7 @@ global.fetch = jest.fn().mockImplementation( () => jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) ); const confirmModalText = - "We'll import images from WooCommerce.com to set up your sample products."; + 'We’ll import images from WooCommerce.com to set up your sample products.'; describe( 'Products', () => { beforeEach( () => { diff --git a/plugins/woocommerce-admin/client/task-lists/fills/shipping/index.js b/plugins/woocommerce-admin/client/task-lists/fills/shipping/index.js index da45e5fb7f3..9bdf2a3f498 100644 --- a/plugins/woocommerce-admin/client/task-lists/fills/shipping/index.js +++ b/plugins/woocommerce-admin/client/task-lists/fills/shipping/index.js @@ -182,7 +182,7 @@ export class Shipping extends Component { createNotice( 'success', __( - "📦 Shipping is done! Don't worry, you can always change it later", + '📦 Shipping is done! Don’t worry, you can always change it later', 'woocommerce' ) ); diff --git a/plugins/woocommerce-admin/client/task-lists/fills/tax/components/partners.tsx b/plugins/woocommerce-admin/client/task-lists/fills/tax/components/partners.tsx index 91c55548b3d..8b243e86285 100644 --- a/plugins/woocommerce-admin/client/task-lists/fills/tax/components/partners.tsx +++ b/plugins/woocommerce-admin/client/task-lists/fills/tax/components/partners.tsx @@ -56,7 +56,7 @@ export const Partners: React.FC< TaxChildProps > = ( { onDisable(); } } > - { __( "I don't charge sales tax", 'woocommerce' ) } + { __( 'I don’t charge sales tax', 'woocommerce' ) } diff --git a/plugins/woocommerce-admin/client/task-lists/fills/tax/index.tsx b/plugins/woocommerce-admin/client/task-lists/fills/tax/index.tsx index 23e5952d0ce..c4c95cdb95e 100644 --- a/plugins/woocommerce-admin/client/task-lists/fills/tax/index.tsx +++ b/plugins/woocommerce-admin/client/task-lists/fills/tax/index.tsx @@ -122,7 +122,7 @@ export const Tax: React.FC< TaxProps > = ( { onComplete, query, task } ) => { createNotice( 'success', __( - "You're awesome! One less item on your to-do list ✅", + 'You’re awesome! One less item on your to-do list ✅', 'woocommerce' ) ); diff --git a/plugins/woocommerce-admin/client/task-lists/progress-title/default-progress-title.tsx b/plugins/woocommerce-admin/client/task-lists/progress-title/default-progress-title.tsx index 45c06e34ac1..ca2be82ba3e 100644 --- a/plugins/woocommerce-admin/client/task-lists/progress-title/default-progress-title.tsx +++ b/plugins/woocommerce-admin/client/task-lists/progress-title/default-progress-title.tsx @@ -56,12 +56,12 @@ export const DefaultProgressTitle: React.FC< DefaultProgressTitleProps > = ( { : __( 'Welcome to your store', 'woocommerce' ); } if ( completedCount <= 3 ) { - return __( "Let's get you started", 'woocommerce' ) + ' 🚀'; + return __( 'Let’s get you started', 'woocommerce' ) + ' 🚀'; } if ( completedCount > 3 && completedCount < 6 ) { - return __( "You're on the right track", 'woocommerce' ); + return __( 'You’re on the right track', 'woocommerce' ); } - return __( "You're almost there", 'woocommerce' ); + return __( 'You’re almost there', 'woocommerce' ); }, [ completedCount, hasVisitedTasks, tasksCount ] ); if ( loading ) { diff --git a/plugins/woocommerce-admin/client/task-lists/progress-title/test/default-progress-title.test.tsx b/plugins/woocommerce-admin/client/task-lists/progress-title/test/default-progress-title.test.tsx index 4dd25686444..4ab493c3d35 100644 --- a/plugins/woocommerce-admin/client/task-lists/progress-title/test/default-progress-title.test.tsx +++ b/plugins/woocommerce-admin/client/task-lists/progress-title/test/default-progress-title.test.tsx @@ -49,7 +49,7 @@ describe( 'default-progress-title', () => { ).toBeInTheDocument(); } ); - it( 'should render "Let\'s get you started" when has task visited and task completed count <= 3', () => { + it( 'should render "Let’s get you started" when has task visited and task completed count <= 3', () => { ( useSelect as jest.Mock ).mockImplementation( ( fn ) => fn( () => ( { getTaskList: () => ( { @@ -60,11 +60,11 @@ describe( 'default-progress-title', () => { ); render( ); expect( - screen.getByText( "Let's get you started", { exact: false } ) + screen.getByText( 'Let’s get you started', { exact: false } ) ).toBeInTheDocument(); } ); - it( 'should render "You\'re on the right track" when has task visited and task completed count > 3', () => { + it( 'should render "You’re on the right track" when has task visited and task completed count > 3', () => { ( useSelect as jest.Mock ).mockImplementation( ( fn ) => fn( () => ( { getTaskList: () => ( { @@ -81,11 +81,11 @@ describe( 'default-progress-title', () => { ); render( ); expect( - screen.getByText( "You're on the right track", { exact: false } ) + screen.getByText( 'You’re on the right track', { exact: false } ) ).toBeInTheDocument(); } ); - it( 'should render "You\'re almost there" when has task visited and task completed count > 5', () => { + it( 'should render "You’re almost there" when has task visited and task completed count > 5', () => { ( useSelect as jest.Mock ).mockImplementation( ( fn ) => fn( () => ( { getTaskList: () => ( { @@ -104,7 +104,7 @@ describe( 'default-progress-title', () => { ); render( ); expect( - screen.getByText( "You're almost there", { exact: false } ) + screen.getByText( 'You’re almost there', { exact: false } ) ).toBeInTheDocument(); } ); } ); diff --git a/plugins/woocommerce-admin/client/task-lists/reminder-bar/reminder-bar.tsx b/plugins/woocommerce-admin/client/task-lists/reminder-bar/reminder-bar.tsx index 1a96b202e3c..64bb0e04d76 100644 --- a/plugins/woocommerce-admin/client/task-lists/reminder-bar/reminder-bar.tsx +++ b/plugins/woocommerce-admin/client/task-lists/reminder-bar/reminder-bar.tsx @@ -52,7 +52,7 @@ const ReminderText: React.FC< ReminderTextProps > = ( { ) : /* translators: 1: remaining tasks count */ __( - "🚀 You're doing great! {{strongText}}%1$d steps left{{/strongText}} to get your store up and running. {{setupLink}}Continue setup{{/setupLink}}", + '🚀 You’re doing great! {{strongText}}%1$d steps left{{/strongText}} to get your store up and running. {{setupLink}}Continue setup{{/setupLink}}', 'woocommerce' ); diff --git a/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/launch-your-store.tsx b/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/launch-your-store.tsx index bbe78e5a4fb..355e0682eb0 100644 --- a/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/launch-your-store.tsx +++ b/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/launch-your-store.tsx @@ -34,7 +34,7 @@ const LaunchYourStoreHeader = ( {

{ __( - "It's time to celebrate – you're ready to launch your store! Woo! Hit the button to preview your store and make it public.", + 'It’s time to celebrate – you’re ready to launch your store! Woo! Hit the button to preview your store and make it public.', 'woocommerce' ) }

diff --git a/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/store-details.js b/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/store-details.js index 67fc76a2442..c66eb139a6b 100644 --- a/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/store-details.js +++ b/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/store-details.js @@ -26,7 +26,7 @@ const StoreDetailsHeader = ( { task, goToTask } ) => {

{ __( - "Get your store up and running in no time. Add your store's address to set up shipping, tax and payments faster.", + 'Get your store up and running in no time. Add your store’s address to set up shipping, tax and payments faster.', 'woocommerce' ) }

diff --git a/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/woocommerce-payments.js b/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/woocommerce-payments.js index a6f21627e19..47fe2ffb4d6 100644 --- a/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/woocommerce-payments.js +++ b/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/woocommerce-payments.js @@ -54,7 +54,7 @@ const WoocommercePaymentsHeader = ( { task, trackClick } ) => { className="svg-background" />
-

{ __( "It's time to get paid", 'woocommerce' ) }

+

{ __( 'It’s time to get paid', 'woocommerce' ) }

{ incentive?.task_header_content ? (

{ __( - "You've completed store setup", + 'You’ve completed store setup', 'woocommerce' ) } diff --git a/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-list-completed.tsx b/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-list-completed.tsx index d19bd0806c3..a91359b9b4c 100644 --- a/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-list-completed.tsx +++ b/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-list-completed.tsx @@ -33,7 +33,7 @@ export const TaskListCompleted = ( { Completed

{ __( - "You've completed store setup", + 'You’ve completed store setup', 'woocommerce' ) }

diff --git a/plugins/woocommerce-admin/client/typings/global.d.ts b/plugins/woocommerce-admin/client/typings/global.d.ts index 4caf97d3849..7e0aeebf77c 100644 --- a/plugins/woocommerce-admin/client/typings/global.d.ts +++ b/plugins/woocommerce-admin/client/typings/global.d.ts @@ -27,6 +27,7 @@ declare global { symbol: string; }; currentUserId: number; + blueprint_upload_nonce?: string; }; }; wcAdminFeatures: { @@ -59,6 +60,7 @@ declare global { 'shipping-smart-defaults': boolean; 'shipping-setting-tour': boolean; 'launch-your-store': boolean; + 'blueprint': boolean; 'reactify-classic-payments-settings': boolean; }; wp: { diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/marketing-coupons/index.js b/plugins/woocommerce-admin/client/wp-admin-scripts/marketing-coupons/index.js index 0d3382f1e5b..5b593bff05d 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/marketing-coupons/index.js +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/marketing-coupons/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render } from '@wordpress/element'; +import { createRoot } from '@wordpress/element'; /** * Internal dependencies @@ -13,9 +13,7 @@ const postForm = document.getElementById( 'posts-filter' ); if ( postForm ) { const couponRoot = document.createElement( 'div' ); couponRoot.setAttribute( 'id', 'coupon-root' ); - - render( - , - postForm.parentNode.appendChild( couponRoot ) + createRoot( postForm.parentNode.appendChild( couponRoot ) ).render( + ); } diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/navigation-opt-out/container.js b/plugins/woocommerce-admin/client/wp-admin-scripts/navigation-opt-out/container.js deleted file mode 100644 index 14b429f0354..00000000000 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/navigation-opt-out/container.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Component } from '@wordpress/element'; -import { Button, Modal } from '@wordpress/components'; - -export class NavigationOptOutContainer extends Component { - constructor( props ) { - super( props ); - this.state = { - isModalOpen: true, - }; - } - - render() { - const { isModalOpen } = this.state; - if ( ! isModalOpen ) { - return null; - } - - if ( ! window.surveyData || ! window.surveyData.url ) { - return null; - } - - return ( - this.setState( { isModalOpen: false } ) } - className="woocommerce-navigation-opt-out-modal" - > -

- { __( - "Take this 2-minute survey to share why you're opting out of the new navigation", - 'woocommerce' - ) } -

- -
- - - -
-
- ); - } -} diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/navigation-opt-out/index.js b/plugins/woocommerce-admin/client/wp-admin-scripts/navigation-opt-out/index.js deleted file mode 100644 index 9f52860e34a..00000000000 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/navigation-opt-out/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * External dependencies - */ -import { render } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { NavigationOptOutContainer } from './container'; -import './style.scss'; - -const navigationOptOutRoot = document.createElement( 'div' ); -navigationOptOutRoot.setAttribute( 'id', 'navigation-opt-out-root' ); - -render( - , - document.body.appendChild( navigationOptOutRoot ) -); diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/navigation-opt-out/style.scss b/plugins/woocommerce-admin/client/wp-admin-scripts/navigation-opt-out/style.scss deleted file mode 100644 index 5797952fab1..00000000000 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/navigation-opt-out/style.scss +++ /dev/null @@ -1,8 +0,0 @@ -.woocommerce-navigation-opt-out-modal__actions { - text-align: right; - margin-top: $gap-large; - - .components-button.is-primary { - margin-left: $gap; - } -} diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/onboarding-homepage-notice/index.js b/plugins/woocommerce-admin/client/wp-admin-scripts/onboarding-homepage-notice/index.js index c12455cbf8b..d4fee7e468c 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/onboarding-homepage-notice/index.js +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/onboarding-homepage-notice/index.js @@ -70,7 +70,7 @@ const onboardingHomepageNotice = () => { dispatch( 'core/notices' ).removeNotice( 'SAVE_POST_NOTICE_ID' ); dispatch( 'core/notices' ).createSuccessNotice( - __( "🏠 Nice work creating your store's homepage!", 'woocommerce' ), + __( '🏠 Nice work creating your store’s homepage!', 'woocommerce' ), { id: 'WOOCOMMERCE_ONBOARDING_HOME_PAGE_NOTICE', type: notificationType, diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/onboarding-tax-notice/index.js b/plugins/woocommerce-admin/client/wp-admin-scripts/onboarding-tax-notice/index.js index 5b94d57b28b..8cf91d192a0 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/onboarding-tax-notice/index.js +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/onboarding-tax-notice/index.js @@ -41,7 +41,7 @@ const showTaxCompletionNotice = () => { saveButton.classList.add( 'has-tax' ); dispatch( 'core/notices' ).createSuccessNotice( - __( "You've added your first tax rate!", 'woocommerce' ), + __( 'You’ve added your first tax rate!', 'woocommerce' ), { id: 'WOOCOMMERCE_ONBOARDING_TAX_NOTICE', actions: [ diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/index.js b/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/index.js index f71b921073d..a1b6a03c11f 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/index.js +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/print-shipping-label-banner/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render } from '@wordpress/element'; +import { createRoot } from '@wordpress/element'; import { withPluginsHydration } from '@woocommerce/data'; /** @@ -19,4 +19,7 @@ const HydratedShippingBanner = withPluginsHydration( { ...getAdminSetting( 'plugins' ), jetpackStatus: getAdminSetting( 'dataEndpoints', {} ).jetpackStatus, } )( ShippingBanner ); -render( , metaBox ); + +createRoot( metaBox ).render( + +); diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/shipping-settings-region-picker/index.js b/plugins/woocommerce-admin/client/wp-admin-scripts/shipping-settings-region-picker/index.js index f19859390eb..75f7ba982a1 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/shipping-settings-region-picker/index.js +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/shipping-settings-region-picker/index.js @@ -2,26 +2,34 @@ * External dependencies */ import { createRoot } from '@wordpress/element'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies */ import { RegionPicker } from './region-picker'; import { ShippingCurrencyContext } from './currency-context'; +import { recursivelyTransformLabels } from './utils'; const shippingZoneRegionPickerRoot = document.getElementById( 'wc-shipping-zone-region-picker-root' ); -const options = window.shippingZoneMethodsLocalizeScript?.region_options ?? []; +const options = + recursivelyTransformLabels( + window.shippingZoneMethodsLocalizeScript?.region_options, + decodeEntities + ) ?? []; const initialValues = window.shippingZoneMethodsLocalizeScript?.locations ?? []; -const ShippingApp = () => ( -
- - -
-); +const ShippingApp = () => { + return ( +
+ + +
+ ); +}; if ( shippingZoneRegionPickerRoot ) { createRoot( shippingZoneRegionPickerRoot ).render( ); diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/shipping-settings-region-picker/test/utils.js b/plugins/woocommerce-admin/client/wp-admin-scripts/shipping-settings-region-picker/test/utils.js new file mode 100644 index 00000000000..64587943ae0 --- /dev/null +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/shipping-settings-region-picker/test/utils.js @@ -0,0 +1,121 @@ +/** + * External dependencies + */ +import { decodeEntities } from '@wordpress/html-entities'; +/** + * Internal dependencies + */ +import { recursivelyTransformLabels } from '../utils'; + +describe( 'recursivelyTransformLabels', () => { + function toUpperCase( label ) { + return label.toUpperCase(); + } + + test( 'Single node with label', () => { + const node = { label: 'test' }; + const result = recursivelyTransformLabels( node, toUpperCase ); + expect( result.label ).toBe( 'TEST' ); + } ); + + test( 'Single node without label', () => { + const node = { value: 42 }; + const result = recursivelyTransformLabels( node, toUpperCase ); + expect( result.value ).toBe( 42 ); + expect( result.label ).toBeUndefined(); + } ); + + test( 'Nested nodes', () => { + const node = { + label: 'parent', + value: 'par', + children: [ + { label: 'child1', value: 'ch1' }, + { label: 'child2', value: 'ch2' }, + ], + }; + const result = recursivelyTransformLabels( node, toUpperCase ); + expect( result.label ).toBe( 'PARENT' ); + expect( result.value ).toBe( 'par' ); + expect( result.children[ 0 ].label ).toBe( 'CHILD1' ); + expect( result.children[ 0 ].value ).toBe( 'ch1' ); + expect( result.children[ 1 ].label ).toBe( 'CHILD2' ); + expect( result.children[ 1 ].value ).toBe( 'ch2' ); + } ); + + test( 'Nested nodes with mixed children', () => { + const node = { + label: 'parent', + children: [ + { label: 'child1' }, + { value: 42 }, + { label: 'child2' }, + ], + }; + const result = recursivelyTransformLabels( node, toUpperCase ); + expect( result.label ).toBe( 'PARENT' ); + expect( result.children[ 0 ].label ).toBe( 'CHILD1' ); + expect( result.children[ 1 ].value ).toBe( 42 ); + expect( result.children[ 1 ].label ).toBeUndefined(); + expect( result.children[ 2 ].label ).toBe( 'CHILD2' ); + } ); + + test( 'Array of nodes', () => { + const node = [ { label: 'node1' }, { label: 'node2' } ]; + const result = recursivelyTransformLabels( node, toUpperCase ); + expect( result[ 0 ].label ).toBe( 'NODE1' ); + expect( result[ 1 ].label ).toBe( 'NODE2' ); + } ); + + test( 'Deeply nested nodes', () => { + const node = { + label: 'root', + children: [ + { + label: 'branch1', + value: 'br1', + children: [ + { label: 'leaf1', value: 'le1' }, + { label: 'leaf2' }, + ], + }, + { label: 'branch2' }, + ], + }; + const result = recursivelyTransformLabels( node, toUpperCase ); + expect( result.label ).toBe( 'ROOT' ); + expect( result.children[ 0 ].label ).toBe( 'BRANCH1' ); + expect( result.children[ 0 ].value ).toBe( 'br1' ); + expect( result.children[ 0 ].children[ 0 ].label ).toBe( 'LEAF1' ); + expect( result.children[ 0 ].children[ 0 ].value ).toBe( 'le1' ); + expect( result.children[ 0 ].children[ 1 ].label ).toBe( 'LEAF2' ); + expect( result.children[ 1 ].label ).toBe( 'BRANCH2' ); + } ); +} ); + +describe( 'recursivelyTransformLabels and decodeEntities should work together', () => { + test( 'Deeply nested nodes', () => { + const node = { + label: 'root', + children: [ + { + label: 'branch1', + value: 'br1', + children: [ + { label: 'Curaçao', value: 'le1' }, + { label: 'leaf2' }, + ], + }, + { label: 'branch2' }, + ], + }; + const result = recursivelyTransformLabels( node, decodeEntities ); + expect( result.label ).toBe( 'root' ); + expect( result.children[ 0 ].label ).toBe( 'branch1' ); + expect( result.children[ 0 ].value ).toBe( 'br1' ); + expect( result.children[ 0 ].children[ 0 ].label ).toBe( 'Curaçao' ); + expect( result.children[ 0 ].children[ 0 ].value ).toBe( 'le1' ); + expect( result.children[ 0 ].children[ 1 ].label ).toBe( 'leaf2' ); + expect( result.children[ 1 ].label ).toBe( 'branch2' ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/shipping-settings-region-picker/utils.js b/plugins/woocommerce-admin/client/wp-admin-scripts/shipping-settings-region-picker/utils.js new file mode 100644 index 00000000000..1d34d598ef4 --- /dev/null +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/shipping-settings-region-picker/utils.js @@ -0,0 +1,14 @@ +export const recursivelyTransformLabels = ( node, transform ) => { + if ( Array.isArray( node ) ) { + return node.map( ( element ) => { + return recursivelyTransformLabels( element, transform ); + } ); + } + if ( node.label ) { + node.label = transform( node.label ); + } + if ( node.children ) { + node.children = recursivelyTransformLabels( node.children, transform ); + } + return node; +}; diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/wc-addons-tour/index.tsx b/plugins/woocommerce-admin/client/wp-admin-scripts/wc-addons-tour/index.tsx index e8c69ba2aba..6bcf1e75a44 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/wc-addons-tour/index.tsx +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/wc-addons-tour/index.tsx @@ -1,7 +1,9 @@ /** * External dependencies */ -import { render } from '@wordpress/element'; +// @ts-expect-error -- @wordpress/element doesn't export createRoot until WP6.2 +// eslint-disable-next-line @woocommerce/dependency-group +import { createRoot } from '@wordpress/element'; /** * Internal dependencies @@ -10,4 +12,5 @@ import WCAddonsTour from '../../guided-tours/wc-addons-tour/index'; const root = document.createElement( 'div' ); root.setAttribute( 'id', 'wc-addons-tour-root' ); -render( , document.body.appendChild( root ) ); + +createRoot( document.body.appendChild( root ) ).render( ); diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/index.js b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/index.js index dc4e0670e88..ccaab793a41 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/index.js +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render } from '@wordpress/element'; +import { createRoot } from '@wordpress/element'; /** * Internal dependencies @@ -27,7 +27,7 @@ const { const container = document.createElement( 'div' ); container.setAttribute( 'id', 'woo-product-usage-notice' ); -render( +createRoot( document.body.appendChild( container ) ).render( , - document.body.appendChild( container ) + /> ); diff --git a/plugins/woocommerce-admin/docs/data.md b/plugins/woocommerce-admin/docs/data.md index 596825fa497..898bbf0cacc 100644 --- a/plugins/woocommerce-admin/docs/data.md +++ b/plugins/woocommerce-admin/docs/data.md @@ -22,7 +22,7 @@ The `SqlQuery` class is a SQL Query statement object. Its properties consist of ## Reports Data Stores -The base DataStore `Automattic\WooCommerce\Admin\API\Reports\DataStore` extends the `SqlQuery` class. The implementation data store classes use the following `SqlQuery` instances: +The base DataStore `Automattic\WooCommerce\Admin\API\Reports\DataStore` extends the `SqlQuery` class. There is `StatsDataStoreTrait` that adds Interval & Total Queries. The implementation data store classes use the following `SqlQuery` instances: | Data Store | Context | Class Query | Sub Query | Interval Query | Total Query | | ---------- | ------- | ----------- | --------- | -------------- | ----------- | @@ -40,6 +40,7 @@ The base DataStore `Automattic\WooCommerce\Admin\API\Reports\DataStore` extends | Taxes | taxes | Yes | Yes | - | - | | Tax Stats | tax_stats | Yes | - | Yes | Yes | | Variations | variations | Yes | Yes | - | - | +| StatsDataStoreTrait | n/a | n/a | - | Yes | Yes | Query contexts are named as follows: diff --git a/plugins/woocommerce-admin/docs/examples/extensions/add-navigation-items/js/index.js b/plugins/woocommerce-admin/docs/examples/extensions/add-navigation-items/js/index.js deleted file mode 100644 index 561e2ab7379..00000000000 --- a/plugins/woocommerce-admin/docs/examples/extensions/add-navigation-items/js/index.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; -import { registerPlugin } from '@wordpress/plugins'; -import { WooNavigationItem } from '@woocommerce/navigation'; - -const MyPlugin = () => { - const handleClick = () => { - alert( 'Menu item clicked!' ); - }; - - return ( - - - - ); -}; - -registerPlugin( 'my-plugin', { - render: MyPlugin, - scope: 'woocommerce-navigation', -} ); diff --git a/plugins/woocommerce-admin/docs/examples/extensions/add-navigation-items/woocommerce-admin-add-navigation-items-example.php b/plugins/woocommerce-admin/docs/examples/extensions/add-navigation-items/woocommerce-admin-add-navigation-items-example.php deleted file mode 100644 index 4b43e00f21d..00000000000 --- a/plugins/woocommerce-admin/docs/examples/extensions/add-navigation-items/woocommerce-admin-add-navigation-items-example.php +++ /dev/null @@ -1,77 +0,0 @@ - 'example-plugin', - 'title' => 'Example Plugin', - 'capability' => 'view_woocommerce_reports', - 'url' => 'https://www.google.com', - ) - ); - - \Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_plugin_category( - array( - 'id' => 'example-category', - 'title' => 'Example Category', - 'capability' => 'view_woocommerce_reports', - ) - ); - - \Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_plugin_item( - array( - 'id' => 'example-category-child-1', - 'parent' => 'example-category', - 'title' => 'Sub Menu Child 1', - 'capability' => 'view_woocommerce_reports', - 'url' => 'https://www.google.com', - ) - ); - - \Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_plugin_item( - array( - 'id' => 'example-category-child-2', - 'parent' => 'example-category', - 'title' => 'Sub Menu Child 2', - 'capability' => 'view_woocommerce_reports', - 'url' => 'https://www.google.com', - ) - ); -} -add_filter( 'admin_menu', 'add_navigation_items_register_items' ); diff --git a/plugins/woocommerce-admin/docs/features/navigation.md b/plugins/woocommerce-admin/docs/features/navigation.md deleted file mode 100644 index 26592cd2442..00000000000 --- a/plugins/woocommerce-admin/docs/features/navigation.md +++ /dev/null @@ -1,125 +0,0 @@ -# WooCommerce Navigation - -The WooCommerce Navigation feature is a navigational project designed to create a more intuitive and functional WooCommerce specific navigation. - -This API will allow you to add in your own items to the navigation and register pages with the new navigation screens. - -### Getting started - -This feature is hidden behind a feature flag and can be turned on or off by visiting WooCommerce -> Settngs -> Advanced -> Features and checking the box next to the `Navigation` option. It can also by controlled programmatically by setting the option `woocommerce_navigation_enable` to `yes` or `no`. - -The fastest way to get started is by creating an example plugin from WooCommerce Admin. Enter the following command: - -`WC_EXT=add-navigation-items pnpm example --filter=@woocommerce/admin-library` - -This will create a new plugin that covers various features of the navigation and helps to register some initial items and categories within the new navigation menu. After running the command above, you can make edits directly to the files at `docs/examples/extensions/add-navigation-items` and they will be built and copied to your `wp-content/add-navigation-items` folder on save. - -If you need to enable the WP Toolbar for debugging purposes in the new navigation, you can add the following filter to do so: - -`add_filter( 'woocommerce_navigation_wp_toolbar_disabled', '__return_false' );` - -### Adding a menu category - -Categories in the new navigation are menu items that house child menu items. - -Clicking on a category will not navigate to a new page, but instead open the child menu. Note that categories without menu items will not be shown in the menu. - -* `id` - (string) The unique ID of the menu item. Required. -* `title` - (string) Title of the menu item. Required. -* `parent` - (string) Parent menu item ID. -* `capability` - (string) Capability to view this menu item. - -```php -\Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_plugin_category( - array( - 'id' => 'example-category', - 'title' => 'Example Category', - ) -); -``` - -Categories can also contain more categories by specifying the `parent` property for the child category. There is no limit on the level of nesting. - -```php -\Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_plugin_category( - array( - 'id' => 'example-nested-category', - 'parent' => 'example-category', - 'title' => 'Example Nested Category', - ) -); -``` - -### Adding a menu item - -Adding an item, much like a category, can be added directly to the menu or to an existing category. Typically this will create a link to the specified URL or callback unless overridden by JavaScript using the slot/fill approach described below. - -* `id` - (string) The unique ID of the menu item. Required. -* `title` - (string) Title of the menu item. Required. -* `parent` - (string) Parent menu item ID. -* `capability` - (string) Capability to view this menu item. -* `url` - (string) URL or callback to be used. Required. -* `migrate` - (bool) Whether or not to hide the item in the wp admin menu. -* `matchExpression` - (string) An optional regex string to compare against the current location and mark the item active. - -```php -\Automattic\WooCommerce\Admin\Features\Navigation\Menu::add_plugin_item( - array( - 'id' => 'example-plugin', - 'title' => 'Example Plugin', - 'capability' => 'view_woocommerce_reports', - 'url' => 'https://www.google.com', - ) -); -``` - -### Registering plugin screens - -In order to show the new navigation in place of the traditional WordPress menu on a given page, the screen ID must be registered to identify a page as supporting the new WooCommerce navigation. - -When adding items, the navigation will automatically add support for the screen via the URL or callback provided with an item. However, custom post types and taxonomies need to be registered with the navigation to work on the custom post type page. - - -```php -\Automattic\WooCommerce\Admin\Features\Navigation\Screen::register_post_type( 'my-custom-post-type' ); -\Automattic\WooCommerce\Admin\Features\Navigation\Screen::register_taxonomy( 'my-custom-taxonomy' ); -``` - -You can also manually add a screen without registering an item. - -```php -\Automattic\WooCommerce\Admin\Features\Navigation\Screen::add_screen( 'my-plugin-page' ); -``` - -### Slot/fill items - -Using slot fill we can update items on the front-end of the site using JavaScript. This is useful for modern JavaScript routing, more intricate interactions with menu items, or updating URLs and hyperlink text without reloading the page. - -In order to use slot fill, you can import the `WooNavigationItem` component from `@woocommerce/navigation` and match the `item` prop with the ID of the item you'd like to modify the behavior of. - - -```js -import { __ } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; -import { registerPlugin } from "@wordpress/plugins"; -import { useHistory } from "react-router-dom"; -import { WooNavigationItem } from "@woocommerce/navigation"; - -const MyPlugin = () => { - const history = useHistory(); - - const handleClick = () => { - history.push( '/my-plugin-path' ); - } - - return ( - - - - ); -}; - -registerPlugin( 'my-plugin', { render: MyPlugin, scope: 'woocommerce-navigation' } ); -``` diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 3106b273088..8e84eeb1cd5 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -22,7 +22,7 @@ "lint": "pnpm --if-present '/^lint:lang:.*$/'", "lint:fix": "pnpm --if-present '/^lint:fix:lang:.*$/'", "lint:fix:lang:css": "stylelint '**/*.scss' --fix --ip 'storybook/wordpress'", - "lint:fix:lang:js": "pnpm lint:js --fix --ext=js,ts,tsx", + "lint:fix:lang:js": "pnpm lint:lang:js --fix --ext=js,ts,tsx", "lint:lang:css": "stylelint '**/*.scss'", "lint:lang:js": "eslint ./client --ext=js,ts,tsx", "test:js": "jest --config client/jest.config.js", @@ -242,7 +242,7 @@ }, "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "config": { "ci": { diff --git a/plugins/woocommerce-admin/webpack.config.js b/plugins/woocommerce-admin/webpack.config.js index 7f3fa90597e..cadd1251831 100644 --- a/plugins/woocommerce-admin/webpack.config.js +++ b/plugins/woocommerce-admin/webpack.config.js @@ -48,7 +48,6 @@ const wcAdminPackages = [ // See ./client/wp-admin-scripts/README.md for more details const wpAdminScripts = [ 'marketing-coupons', - 'navigation-opt-out', 'onboarding-homepage-notice', 'onboarding-product-notice', 'onboarding-product-import-notice', diff --git a/plugins/woocommerce-beta-tester/api/api.php b/plugins/woocommerce-beta-tester/api/api.php index 62a4b4df2af..65b1d151aa8 100644 --- a/plugins/woocommerce-beta-tester/api/api.php +++ b/plugins/woocommerce-beta-tester/api/api.php @@ -55,6 +55,7 @@ require 'tools/trigger-update-callbacks.php'; require 'tools/reset-cys.php'; require 'tools/set-block-template-logging-threshold.php'; require 'tools/set-coming-soon-mode.php'; +require 'tools/fake-woo-payments-gateway.php'; require 'tracks/class-tracks-debug-log.php'; require 'features/features.php'; require 'rest-api-filters/class-wca-test-helper-rest-api-filters.php'; @@ -63,3 +64,5 @@ require 'live-branches/manifest.php'; require 'live-branches/install.php'; require 'remote-spec-validator/class-wca-test-helper-remote-spec-validator.php'; require 'remote-inbox-notifications/class-wca-test-helper-remote-inbox-notifications.php'; +require 'remote-logging/remote-logging.php'; +require 'tools/wccom-request-errors.php'; diff --git a/plugins/woocommerce-beta-tester/api/options/rest-api.php b/plugins/woocommerce-beta-tester/api/options/rest-api.php index b48f53a4b31..cc5ee9f481a 100644 --- a/plugins/woocommerce-beta-tester/api/options/rest-api.php +++ b/plugins/woocommerce-beta-tester/api/options/rest-api.php @@ -61,6 +61,7 @@ function wca_test_helper_delete_option( $request ) { ) ); + wp_cache_flush(); return new WP_REST_RESPONSE( null, 204 ); } diff --git a/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php b/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php new file mode 100644 index 00000000000..6e7d9b1487b --- /dev/null +++ b/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php @@ -0,0 +1,125 @@ + 'GET', + ) +); + +register_woocommerce_admin_test_helper_rest_route( + '/remote-logging/toggle', + 'toggle_remote_logging', + array( + 'methods' => 'POST', + 'args' => array( + 'enable' => array( + 'required' => true, + 'type' => 'boolean', + 'sanitize_callback' => 'rest_sanitize_boolean', + ), + ), + ) +); + +register_woocommerce_admin_test_helper_rest_route( + '/remote-logging/log-event', + 'log_remote_event', + array( + 'methods' => 'POST', + ) +); + +register_woocommerce_admin_test_helper_rest_route( + '/remote-logging/reset-rate-limit', + 'reset_php_rate_limit', + array( + 'methods' => 'POST', + ) +); + + +/** + * Get the remote logging status. + * + * @return WP_REST_Response The response object. + */ +function get_remote_logging_status() { + $remote_logger = wc_get_container()->get( RemoteLogger::class ); + + return new WP_REST_Response( + array( + 'isEnabled' => $remote_logger->is_remote_logging_allowed(), + 'wpEnvironment' => wp_get_environment_type(), + ), + 200 + ); +} + +/** + * Toggle remote logging on or off. + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response The response object. + */ +function toggle_remote_logging( $request ) { + $enable = $request->get_param( 'enable' ); + + if ( $enable ) { + update_option( 'woocommerce_feature_remote_logging_enabled', 'yes' ); + update_option( 'woocommerce_allow_tracking', 'yes' ); + update_option( 'woocommerce_remote_variant_assignment', 1 ); + } else { + update_option( 'woocommerce_feature_remote_logging_enabled', 'no' ); + } + + $remote_logger = wc_get_container()->get( RemoteLogger::class ); + return new WP_REST_Response( + array( + 'isEnabled' => $remote_logger->is_remote_logging_allowed(), + ), + 200 + ); +} + + +/** + * Log a remote event for testing purposes. + * + * @return WP_REST_Response The response object. + */ +function log_remote_event() { + $remote_logger = wc_get_container()->get( RemoteLogger::class ); + $result = $remote_logger->handle( + time(), + 'critical', + 'Test PHP event from WC Beta Tester', + array( 'source' => 'wc-beta-tester' ) + ); + + if ( $result ) { + return new WP_REST_Response( array( 'message' => 'Remote event logged successfully.' ), 200 ); + } else { + return new WP_REST_Response( array( 'message' => 'Failed to log remote event.' ), 500 ); + } +} + +/** + * Reset the PHP rate limit. + * + * @return WP_REST_Response The response object. + */ +function reset_php_rate_limit() { + global $wpdb; + $wpdb->query( + "DELETE FROM {$wpdb->prefix}wc_rate_limits" + ); + wp_cache_flush(); + + return new WP_REST_Response( array( 'success' => true ), 200 ); +} diff --git a/plugins/woocommerce-beta-tester/api/tools/class-fake-wcpayments.php b/plugins/woocommerce-beta-tester/api/tools/class-fake-wcpayments.php new file mode 100644 index 00000000000..01fa658c3a4 --- /dev/null +++ b/plugins/woocommerce-beta-tester/api/tools/class-fake-wcpayments.php @@ -0,0 +1,62 @@ +id = 'woocommerce_payments'; + $this->has_fields = true; + $this->method_title = 'WooPayments'; + $this->method_description = $this->get_method_description(); + + $this->description = ''; + $this->supports = array( + 'products', + 'refunds', + ); + } + + /** + * Returns true if the gateway needs additional configuration, false if it's ready to use. + * + * @see WC_Payment_Gateway::needs_setup + * @return bool + */ + public function needs_setup() { + return false; + } + + /** + * Check if the payment gateway is connected. This method is also used by + * external plugins to check if a connection has been established. + */ + public function is_connected() { + return true; + } + + /** + * Get the connection URL. + * Called directly by WooCommerce Core. + * + * @return string Connection URL. + */ + public function get_connection_url() { + return ''; + } + + /** + * Checks if the gateway is enabled, and also if it's configured enough to accept payments from customers. + * + * Use parent method value alongside other business rules to make the decision. + * + * @return bool Whether the gateway is enabled and ready to accept payments. + */ + public function is_available() { + return true; + } +} diff --git a/plugins/woocommerce-beta-tester/api/tools/fake-woo-payments-gateway.php b/plugins/woocommerce-beta-tester/api/tools/fake-woo-payments-gateway.php new file mode 100644 index 00000000000..c1f5d9f00e7 --- /dev/null +++ b/plugins/woocommerce-beta-tester/api/tools/fake-woo-payments-gateway.php @@ -0,0 +1,73 @@ + 'GET', + ) +); + +register_woocommerce_admin_test_helper_rest_route( + '/tools/fake-wcpay-completion/v1', + 'tools_set_fake_wcpay_completion', + array( + 'methods' => 'POST', + 'args' => array( + 'enabled' => array( + 'type' => 'enum', + 'enum' => array( 'yes', 'no' ), + 'required' => true, + 'description' => 'Whether to enable or disable fake WooPayments completion', + ), + ), + ) +); + +/** + * Get the current status of fake WooPayments completion. + */ +function tools_get_fake_wcpay_completion_status() { + return new WP_REST_Response( array( 'enabled' => get_option( 'wc_beta_tester_fake_wcpay_completion', 'no' ) ), 200 ); +} + +/** + * A tool to enable/disable fake WooPayments completion. + * + * @param WP_REST_Request $request Request object. + */ +function tools_set_fake_wcpay_completion( $request ) { + $enabled = $request->get_param( 'enabled' ); + update_option( 'wc_beta_tester_fake_wcpay_completion', $enabled ); + + return new WP_REST_Response( array( 'enabled' => $enabled ), 200 ); +} + + +if ( + 'yes' === get_option( 'wc_beta_tester_fake_wcpay_completion', 'no' ) && + class_exists( 'WC_Payment_Gateway_WCPay' ) +) { + add_filter( 'woocommerce_payment_gateways', 'tools_fake_wcpay' ); + add_filter( 'woocommerce_available_payment_gateways', 'tools_fake_wcpay' ); + + require_once dirname( __FILE__ ) . '/class-fake-wcpayments.php'; + + /** + * Fake WooPayments completion. + * + * @param array $gateways List of available payment gateways. + */ + function tools_fake_wcpay( $gateways ) { + $gateways['woocommerce_payments'] = new Fake_WCPayments(); + return $gateways; + } +} diff --git a/plugins/woocommerce-beta-tester/api/tools/wccom-request-errors.php b/plugins/woocommerce-beta-tester/api/tools/wccom-request-errors.php new file mode 100644 index 00000000000..4f2662763c3 --- /dev/null +++ b/plugins/woocommerce-beta-tester/api/tools/wccom-request-errors.php @@ -0,0 +1,48 @@ + 'POST', + 'args' => array( + 'mode' => array( + 'description' => 'wccom request error mode', + 'type' => 'enum', + 'enum' => array( 'timeout', 'error', 'disabled' ), + ), + ), + ) +); + +register_woocommerce_admin_test_helper_rest_route( + '/tools/get-wccom-request-errors/v1', + 'tools_get_wccom_request_errors', + array( + 'methods' => 'GET', + ) +); + +/** + * A tool to set wccom request errors mode. + * + * @param WP_REST_Request $request Request object. + */ +function tools_set_wccom_request_errors( $request ) { + $mode = $request->get_param( 'mode' ); + + update_option( 'wc_admin_test_helper_modify_wccom_request_responses', $mode ); + + return new WP_REST_Response( $mode, 200 ); +} + +/** + * A tool to get wccom request mode. + */ +function tools_get_wccom_request_errors() { + $mode = get_option( 'wc_admin_test_helper_modify_wccom_request_responses', 'disabled' ); + + return new WP_REST_Response( $mode, 200 ); +} diff --git a/plugins/woocommerce-beta-tester/changelog.txt b/plugins/woocommerce-beta-tester/changelog.txt index 69e7efc3554..f0e0e1948ec 100644 --- a/plugins/woocommerce-beta-tester/changelog.txt +++ b/plugins/woocommerce-beta-tester/changelog.txt @@ -1,5 +1,17 @@ *** WooCommerce Beta Tester Changelog *** -2024-06-25 - versio 2.3.2 +2024-08-08 - version 2.4.0 +* Patch - Changed from using React.render to React.createRoot for wc beta tester as it has been deprecated since React 18 +* Minor - Fix "Creation of dynamic property WC_Beta_Tester::$wporg_data is deprecated" on PHP 8.2 +* Patch - Add reactify-classic-payments-settings feature flag +* Minor - Add remote logging tool +* Minor - Add tool to force the coming soon landing pages to display on the front-end +* Minor - Product Editor Dev Tools: Improve expression evaluation tooling support. +* Patch - Minor tooling tweaks (zip compression level, composer invocation) +* Patch - Monorepo: minor tweaks in zip building script (use frozen lock file when installing dependecies). +* Patch - Monorepo: tweak Webpack loaders paths filtering for better build perfromance. +* Patch - Whitelist line with maybe_unserialize() function call from QIT security tests. + +2024-06-25 - version 2.3.2 * Minor - Adds a new tool to the WCA Test Helper that helps import remote inbox notifications from staging or production for testing purposes #48735 * Minor - Remove the new old experience #47814 * Patch - Fix "Live Branches" item not shown in the WooCommerce menu #47691 diff --git a/plugins/woocommerce-beta-tester/changelog/48806-dev-bump-beta-tester-version-to-2.3.2 b/plugins/woocommerce-beta-tester/changelog/48806-dev-bump-beta-tester-version-to-2.3.2 deleted file mode 100644 index 1e1ff9cf11d..00000000000 --- a/plugins/woocommerce-beta-tester/changelog/48806-dev-bump-beta-tester-version-to-2.3.2 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Bump the version of beta tester to 2.3.2 \ No newline at end of file diff --git a/plugins/woocommerce-beta-tester/changelog/48825-update-fix-lint-errors b/plugins/woocommerce-beta-tester/changelog/48825-update-fix-lint-errors deleted file mode 100644 index ab426d2d2ad..00000000000 --- a/plugins/woocommerce-beta-tester/changelog/48825-update-fix-lint-errors +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: tweak -Comment: This only contains lint fixes. - diff --git a/plugins/woocommerce-beta-tester/changelog/49310-dev-pin-block-env-package b/plugins/woocommerce-beta-tester/changelog/49310-dev-pin-block-env-package deleted file mode 100644 index 0802125e226..00000000000 --- a/plugins/woocommerce-beta-tester/changelog/49310-dev-pin-block-env-package +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: tweak -Comment: bump wp-env to 9.7.0, include blocks in syncpack - diff --git a/plugins/woocommerce-beta-tester/changelog/49983-patch-6 b/plugins/woocommerce-beta-tester/changelog/49983-patch-6 deleted file mode 100644 index c93a73c169d..00000000000 --- a/plugins/woocommerce-beta-tester/changelog/49983-patch-6 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: fix - -Fix "Creation of dynamic property WC_Beta_Tester::$wporg_data is deprecated" on PHP 8.2 \ No newline at end of file diff --git a/plugins/woocommerce-beta-tester/changelog/50372-48150-move-patterns-endpoint b/plugins/woocommerce-beta-tester/changelog/50372-48150-move-patterns-endpoint new file mode 100644 index 00000000000..e9739278cd0 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/50372-48150-move-patterns-endpoint @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +CYS - Move the "ai/patterns" endpoint to woocommerce admin API. \ No newline at end of file diff --git a/plugins/woocommerce-beta-tester/changelog/50467-add-beta-tester-wcpay-test-order b/plugins/woocommerce-beta-tester/changelog/50467-add-beta-tester-wcpay-test-order new file mode 100644 index 00000000000..20882c69409 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/50467-add-beta-tester-wcpay-test-order @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add WCPay test order meta data \ No newline at end of file diff --git a/plugins/woocommerce-beta-tester/changelog/50508-fix-beta-tester-build-deps b/plugins/woocommerce-beta-tester/changelog/50508-fix-beta-tester-build-deps new file mode 100644 index 00000000000..6a650438fd3 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/50508-fix-beta-tester-build-deps @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak +Comment: Adjustment to the build step. + diff --git a/plugins/woocommerce-beta-tester/changelog/50518-fix-beta-tester-build b/plugins/woocommerce-beta-tester/changelog/50518-fix-beta-tester-build new file mode 100644 index 00000000000..49408a2eae2 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/50518-fix-beta-tester-build @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak +Comment: Fix build step + diff --git a/plugins/woocommerce-beta-tester/changelog/50568-update-wp-env b/plugins/woocommerce-beta-tester/changelog/50568-update-wp-env new file mode 100644 index 00000000000..39d93fa45b0 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/50568-update-wp-env @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak +Comment: This updates the configuration for the local wp-env development environment so that the mysql database ports remain consistent between sessions. + diff --git a/plugins/woocommerce-beta-tester/changelog/50654-add-beta-tester-wccom-endpoints-mods b/plugins/woocommerce-beta-tester/changelog/50654-add-beta-tester-wccom-endpoints-mods new file mode 100644 index 00000000000..c5e7e461085 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/50654-add-beta-tester-wccom-endpoints-mods @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add a tool to simulate server problems with woocommerce.com \ No newline at end of file diff --git a/plugins/woocommerce-beta-tester/changelog/50828-dev-constrain-pnpm-version b/plugins/woocommerce-beta-tester/changelog/50828-dev-constrain-pnpm-version new file mode 100644 index 00000000000..fdead95ceb5 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/50828-dev-constrain-pnpm-version @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix pnpm version to 9.1.3 to avoid dependency installation issues. \ No newline at end of file diff --git a/plugins/woocommerce-beta-tester/changelog/add-beta-tester-coming-soon-tool b/plugins/woocommerce-beta-tester/changelog/add-beta-tester-coming-soon-tool deleted file mode 100644 index c60a03d3bbf..00000000000 --- a/plugins/woocommerce-beta-tester/changelog/add-beta-tester-coming-soon-tool +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Add tool to force the coming soon landing pages to display on the front-end diff --git a/plugins/woocommerce-beta-tester/changelog/add-beta-tester-tool-fake-wcpay b/plugins/woocommerce-beta-tester/changelog/add-beta-tester-tool-fake-wcpay new file mode 100644 index 00000000000..35549787397 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/add-beta-tester-tool-fake-wcpay @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add fake WooPayments completion tool diff --git a/plugins/woocommerce-beta-tester/changelog/add-reactify-classic-payments-settings-feature-flag b/plugins/woocommerce-beta-tester/changelog/add-reactify-classic-payments-settings-feature-flag deleted file mode 100644 index de54eabf17a..00000000000 --- a/plugins/woocommerce-beta-tester/changelog/add-reactify-classic-payments-settings-feature-flag +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: add - -Add reactify-classic-payments-settings feature flag diff --git a/plugins/woocommerce-beta-tester/changelog/dev-beta-tester-prepare-release-2-4-0 b/plugins/woocommerce-beta-tester/changelog/dev-beta-tester-prepare-release-2-4-0 new file mode 100644 index 00000000000..ebcc9cba491 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/dev-beta-tester-prepare-release-2-4-0 @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Bump the version of beta tester to 2.4.0 diff --git a/plugins/woocommerce-beta-tester/changelog/dev-try-faster-building-zip b/plugins/woocommerce-beta-tester/changelog/dev-try-faster-building-zip deleted file mode 100644 index 310400b9388..00000000000 --- a/plugins/woocommerce-beta-tester/changelog/dev-try-faster-building-zip +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Monorepo: minor tweaks in zip building script (use frozen lock file when installing dependecies). diff --git a/plugins/woocommerce-beta-tester/changelog/dev-tune-up-zip-generation b/plugins/woocommerce-beta-tester/changelog/dev-tune-up-zip-generation deleted file mode 100644 index 76c199f300f..00000000000 --- a/plugins/woocommerce-beta-tester/changelog/dev-tune-up-zip-generation +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Minor tooling tweaks (zip compression level, composer invocation) diff --git a/plugins/woocommerce-beta-tester/changelog/dev-webpack-loaders-scannig-paths-tweaks b/plugins/woocommerce-beta-tester/changelog/dev-webpack-loaders-scannig-paths-tweaks deleted file mode 100644 index 30f765e3fca..00000000000 --- a/plugins/woocommerce-beta-tester/changelog/dev-webpack-loaders-scannig-paths-tweaks +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Monorepo: tweak Webpack loaders paths filtering for better build perfromance. diff --git a/plugins/woocommerce-beta-tester/changelog/fix-remote-logging-tool-cache b/plugins/woocommerce-beta-tester/changelog/fix-remote-logging-tool-cache new file mode 100644 index 00000000000..abc16ae3346 --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/fix-remote-logging-tool-cache @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Flush cache after deleting option and resetting PHP rate limit diff --git a/plugins/woocommerce-beta-tester/changelog/fix-wcadmin-react18-wc-beta-tester b/plugins/woocommerce-beta-tester/changelog/fix-wcadmin-react18-wc-beta-tester deleted file mode 100644 index 697c3f38fd6..00000000000 --- a/plugins/woocommerce-beta-tester/changelog/fix-wcadmin-react18-wc-beta-tester +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Changed from using React.render to React.createRoot for wc beta tester as it has been deprecated since React 18 \ No newline at end of file diff --git a/plugins/woocommerce-beta-tester/changelog/update-dev-tools-expression-evaluation b/plugins/woocommerce-beta-tester/changelog/update-dev-tools-expression-evaluation deleted file mode 100644 index 81868de608c..00000000000 --- a/plugins/woocommerce-beta-tester/changelog/update-dev-tools-expression-evaluation +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Product Editor Dev Tools: Improve expression evaluation tooling support. diff --git a/plugins/woocommerce-beta-tester/changelog/update-qit-false-positive b/plugins/woocommerce-beta-tester/changelog/update-qit-false-positive deleted file mode 100644 index 4ef13c4c93c..00000000000 --- a/plugins/woocommerce-beta-tester/changelog/update-qit-false-positive +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Whitelist line with maybe_unserialize() function call from QIT security tests. diff --git a/plugins/woocommerce-beta-tester/composer.json b/plugins/woocommerce-beta-tester/composer.json index a4c917b746c..8176b9faf2a 100644 --- a/plugins/woocommerce-beta-tester/composer.json +++ b/plugins/woocommerce-beta-tester/composer.json @@ -6,7 +6,7 @@ "license": "GPL-3.0-or-later", "prefer-stable": true, "minimum-stability": "dev", - "version": "2.3.2", + "version": "2.4.0", "require": { "composer/installers": "~1.7" }, diff --git a/plugins/woocommerce-beta-tester/composer.lock b/plugins/woocommerce-beta-tester/composer.lock index f7876fed498..dc3d2bef61e 100644 --- a/plugins/woocommerce-beta-tester/composer.lock +++ b/plugins/woocommerce-beta-tester/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": "b373f29961b944cda3ae2f767107523c", + "content-hash": "56d8be12506c10d7b5acbd1b4c6030b4", "packages": [ { "name": "composer/installers", @@ -363,16 +363,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -380,11 +380,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -410,7 +411,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.12.0" }, "funding": [ { @@ -418,7 +419,7 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "phar-io/manifest", @@ -594,28 +595,28 @@ }, { "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.2", + "version": "1.3.3", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26" + "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", - "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/293975b465e0e709b571cbf0c957c6c0a7b9a2ac", + "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac", "shasum": "" }, "require": { "phpcompatibility/php-compatibility": "^9.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "paragonie/random_compat": "dev-master", "paragonie/sodium_compat": "dev-master" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, "type": "phpcodesniffer-standard", @@ -645,22 +646,37 @@ ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" }, - "time": "2022-10-25T01:46:02+00:00" + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-04-24T21:30:46+00:00" }, { "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.4", + "version": "2.1.5", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5" + "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", - "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/01c1ff2704a58e46f0cb1ca9d06aee07b3589082", + "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082", "shasum": "" }, "require": { @@ -668,10 +684,10 @@ "phpcompatibility/phpcompatibility-paragonie": "^1.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7" + "dealerdirect/phpcodesniffer-composer-installer": "^1.0" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, "type": "phpcodesniffer-standard", @@ -700,9 +716,24 @@ ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy", "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" }, - "time": "2022-10-24T09:00:36+00:00" + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-04-24T21:37:59+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -866,28 +897,29 @@ }, { "name": "phpspec/prophecy", - "version": "v1.16.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "be8cac52a0827776ff9ccda8c381ac5b71aeb359" + "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be8cac52a0827776ff9ccda8c381ac5b71aeb359", - "reference": "be8cac52a0827776ff9ccda8c381ac5b71aeb359", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/67a759e7d8746d501c41536ba40cd9c0a07d6a87", + "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || 8.0.* || 8.1.* || 8.2.*", + "doctrine/instantiator": "^1.2 || ^2.0", + "php": "^7.2 || 8.0.* || 8.1.* || 8.2.* || 8.3.*", "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" + "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0", + "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0" }, "require-dev": { "phpspec/phpspec": "^6.0 || ^7.0", - "phpunit/phpunit": "^8.0 || ^9.0" + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0" }, "type": "library", "extra": { @@ -920,6 +952,7 @@ "keywords": [ "Double", "Dummy", + "dev", "fake", "mock", "spy", @@ -927,9 +960,9 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.16.0" + "source": "https://github.com/phpspec/prophecy/tree/v1.19.0" }, - "time": "2022-11-29T15:06:56+00:00" + "time": "2024-02-29T11:52:51+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1000,16 +1033,16 @@ }, { "name": "phpunit/php-file-iterator", - "version": "2.0.5", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5" + "reference": "69deeb8664f611f156a924154985fbd4911eb36b" }, "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/69deeb8664f611f156a924154985fbd4911eb36b", + "reference": "69deeb8664f611f156a924154985fbd4911eb36b", "shasum": "" }, "require": { @@ -1048,7 +1081,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/2.0.6" }, "funding": [ { @@ -1056,7 +1089,7 @@ "type": "github" } ], - "time": "2021-12-02T12:42:26+00:00" + "time": "2024-03-01T13:39:50+00:00" }, { "name": "phpunit/php-text-template", @@ -1105,16 +1138,16 @@ }, { "name": "phpunit/php-timer", - "version": "2.1.3", + "version": "2.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662" + "reference": "a691211e94ff39a34811abd521c31bd5b305b0bb" }, "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/a691211e94ff39a34811abd521c31bd5b305b0bb", + "reference": "a691211e94ff39a34811abd521c31bd5b305b0bb", "shasum": "" }, "require": { @@ -1152,7 +1185,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/2.1.4" }, "funding": [ { @@ -1160,7 +1193,7 @@ "type": "github" } ], - "time": "2020-11-30T08:20:02+00:00" + "time": "2024-03-01T13:42:41+00:00" }, { "name": "phpunit/php-token-stream", @@ -1362,16 +1395,16 @@ }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.2", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" + "reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54" }, "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/code-unit-reverse-lookup/zipball/92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54", + "reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54", "shasum": "" }, "require": { @@ -1405,7 +1438,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/1.0.3" }, "funding": [ { @@ -1413,7 +1446,7 @@ "type": "github" } ], - "time": "2020-11-30T08:15:22+00:00" + "time": "2024-03-01T13:45:45+00:00" }, { "name": "sebastian/comparator", @@ -1491,16 +1524,16 @@ }, { "name": "sebastian/diff", - "version": "3.0.3", + "version": "3.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211" + "reference": "98ff311ca519c3aa73ccd3de053bdb377171d7b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211", - "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/98ff311ca519c3aa73ccd3de053bdb377171d7b6", + "reference": "98ff311ca519c3aa73ccd3de053bdb377171d7b6", "shasum": "" }, "require": { @@ -1545,7 +1578,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/3.0.6" }, "funding": [ { @@ -1553,20 +1586,20 @@ "type": "github" } ], - "time": "2020-11-30T07:59:04+00:00" + "time": "2024-03-02T06:16:36+00:00" }, { "name": "sebastian/environment", - "version": "4.2.4", + "version": "4.2.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0" + "reference": "56932f6049a0482853056ffd617c91ffcc754205" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", - "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/56932f6049a0482853056ffd617c91ffcc754205", + "reference": "56932f6049a0482853056ffd617c91ffcc754205", "shasum": "" }, "require": { @@ -1608,7 +1641,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/4.2.5" }, "funding": [ { @@ -1616,24 +1649,24 @@ "type": "github" } ], - "time": "2020-11-30T07:53:42+00:00" + "time": "2024-03-01T13:49:59+00:00" }, { "name": "sebastian/exporter", - "version": "3.1.5", + "version": "3.1.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6" + "reference": "1939bc8fd1d39adcfa88c5b35335910869214c56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/73a9676f2833b9a7c36968f9d882589cd75511e6", - "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/1939bc8fd1d39adcfa88c5b35335910869214c56", + "reference": "1939bc8fd1d39adcfa88c5b35335910869214c56", "shasum": "" }, "require": { - "php": ">=7.0", + "php": ">=7.2", "sebastian/recursion-context": "^3.0" }, "require-dev": { @@ -1685,7 +1718,7 @@ ], "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/3.1.6" }, "funding": [ { @@ -1693,7 +1726,7 @@ "type": "github" } ], - "time": "2022-09-14T06:00:17+00:00" + "time": "2024-03-02T06:21:38+00:00" }, { "name": "sebastian/global-state", @@ -1752,16 +1785,16 @@ }, { "name": "sebastian/object-enumerator", - "version": "3.0.4", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2" + "reference": "ac5b293dba925751b808e02923399fb44ff0d541" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", - "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/ac5b293dba925751b808e02923399fb44ff0d541", + "reference": "ac5b293dba925751b808e02923399fb44ff0d541", "shasum": "" }, "require": { @@ -1797,7 +1830,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/3.0.5" }, "funding": [ { @@ -1805,20 +1838,20 @@ "type": "github" } ], - "time": "2020-11-30T07:40:27+00:00" + "time": "2024-03-01T13:54:02+00:00" }, { "name": "sebastian/object-reflector", - "version": "1.1.2", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d" + "reference": "1d439c229e61f244ff1f211e5c99737f90c67def" }, "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/1d439c229e61f244ff1f211e5c99737f90c67def", + "reference": "1d439c229e61f244ff1f211e5c99737f90c67def", "shasum": "" }, "require": { @@ -1852,7 +1885,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/1.1.3" }, "funding": [ { @@ -1860,20 +1893,20 @@ "type": "github" } ], - "time": "2020-11-30T07:37:18+00:00" + "time": "2024-03-01T13:56:04+00:00" }, { "name": "sebastian/recursion-context", - "version": "3.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb" + "reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c" }, "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/9bfd3c6f1f08c026f542032dfb42813544f7d64c", + "reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c", "shasum": "" }, "require": { @@ -1915,7 +1948,7 @@ "homepage": "http://www.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/3.0.2" }, "funding": [ { @@ -1923,20 +1956,20 @@ "type": "github" } ], - "time": "2020-11-30T07:34:24+00:00" + "time": "2024-03-01T14:07:30+00:00" }, { "name": "sebastian/resource-operations", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3" + "reference": "72a7f7674d053d548003b16ff5a106e7e0e06eee" }, "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/72a7f7674d053d548003b16ff5a106e7e0e06eee", + "reference": "72a7f7674d053d548003b16ff5a106e7e0e06eee", "shasum": "" }, "require": { @@ -1966,8 +1999,7 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "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/2.0.3" }, "funding": [ { @@ -1975,7 +2007,7 @@ "type": "github" } ], - "time": "2020-11-30T07:30:19+00:00" + "time": "2024-03-01T13:59:09+00:00" }, { "name": "sebastian/version", @@ -2026,16 +2058,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.7.1", + "version": "3.10.2", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/86e5f5dd9a840c46810ebe5ff1885581c42a3017", + "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017", "shasum": "" }, "require": { @@ -2045,11 +2077,11 @@ "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "bin": [ - "bin/phpcs", - "bin/phpcbf" + "bin/phpcbf", + "bin/phpcs" ], "type": "library", "extra": { @@ -2064,21 +2096,45 @@ "authors": [ { "name": "Greg Sherwood", - "role": "lead" + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" }, - "time": "2022-06-18T07:21:10+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-07-21T23:26:44+00:00" }, { "name": "symfony/console", @@ -2235,16 +2291,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { @@ -2258,9 +2314,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -2298,7 +2351,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" }, "funding": [ { @@ -2314,7 +2367,7 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/process", @@ -2379,16 +2432,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -2417,7 +2470,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -2425,7 +2478,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2024-03-03T12:36:25+00:00" }, { "name": "webmozart/assert", @@ -2646,5 +2699,5 @@ "platform-overrides": { "php": "7.2" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-wccom-requests.php b/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-wccom-requests.php new file mode 100644 index 00000000000..d7a581aa43c --- /dev/null +++ b/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-wccom-requests.php @@ -0,0 +1,74 @@ +wc_beta_tester_modify_wccom_responses(); + } + + /** + * Modify responses based on option value. + */ + public function wc_beta_tester_modify_wccom_responses() { + $mode = get_option( 'wc_admin_test_helper_modify_wccom_request_responses', 'disabled' ); + + if ( 'disabled' === $mode ) { + return; + } + + if ( 'timeout' === $mode ) { + add_filter( 'pre_http_request', array( $this, 'override_http_request_timeout' ), 10, 3 ); + } + + if ( 'error' === $mode ) { + add_filter( 'pre_http_request', array( $this, 'override_http_request_error' ), 10, 3 ); + } + } + + /** + * Override the http request with a timeout. + */ + public function override_http_request_timeout( $response, $args, $url ) { + if ( strpos( $url, 'https://woocommerce.com/wp-json/' ) !== false || strpos( $url, 'woocommerce.test/wp-json/' ) !== false ) { + sleep( 6 ); // 6 seconds + return new WP_Error( 'http_request_timeout', 'Mock timeout error' ); + } + + return false; + } + + /** + * Override the http request with an error. + */ + public function override_http_request_error( $response, $args, $url ) { + if ( strpos( $url, 'https://woocommerce.com/wp-json/' ) !== false ) { + return array( + 'response' => array( + 'code' => 500, + 'message' => 'Internal Server Error', + ), + 'body' => 'Server Error', + 'headers' => array(), + 'cookies' => array(), + 'filename' => null, + ); + } + + return false; + } +} + +new WC_Beta_Tester_WCCOM_Requests(); diff --git a/plugins/woocommerce-beta-tester/package.json b/plugins/woocommerce-beta-tester/package.json index aa0072e6cc4..503f1c40de8 100644 --- a/plugins/woocommerce-beta-tester/package.json +++ b/plugins/woocommerce-beta-tester/package.json @@ -8,7 +8,7 @@ "url": "git://github.com/woocommerce/woocommerce-beta-tester.git" }, "title": "WooCommerce Beta Tester", - "version": "2.3.2", + "version": "2.4.0", "homepage": "http://github.com/woocommerce/woocommerce-beta-tester", "devDependencies": { "@types/react": "^17.0.71", @@ -17,7 +17,7 @@ "@types/wordpress__plugins": "3.0.0", "@woocommerce/dependency-extraction-webpack-plugin": "workspace:*", "@woocommerce/eslint-plugin": "workspace:*", - "@wordpress/env": "^9.7.0", + "@wordpress/env": "^10.1.0", "@wordpress/prettier-config": "2.17.0", "@wordpress/scripts": "^19.2.4", "eslint": "^8.55.0", @@ -35,6 +35,7 @@ "@woocommerce/data": "workspace:*", "@woocommerce/expression-evaluation": "workspace:*", "@woocommerce/product-editor": "workspace:*", + "@woocommerce/remote-logging": "workspace:*", "@wordpress/api-fetch": "wp-6.0", "@wordpress/components": "wp-6.0", "@wordpress/compose": "wp-6.0", @@ -63,7 +64,8 @@ "build_step": "pnpm run build:zip" }, "scripts": { - "build": "pnpm build:admin && pnpm uglify", + "build": "pnpm --if-present --workspace-concurrency=Infinity --stream --filter=\"$npm_package_name...\" '/^build:project:.*$/' && pnpm build:project", + "build:project": "pnpm build:admin && pnpm uglify", "build:admin": "wp-scripts build", "build:dev": "pnpm lint:js && pnpm build", "build:zip": "./bin/build-zip.sh", @@ -89,7 +91,7 @@ }, "engines": { "node": "^20.11.1", - "pnpm": "^9.1.0" + "pnpm": "9.1.3" }, "lint-staged": { "*.php": [ diff --git a/plugins/woocommerce-beta-tester/src/app/app.js b/plugins/woocommerce-beta-tester/src/app/app.js index a612bef66c6..6f0fc4bfa42 100644 --- a/plugins/woocommerce-beta-tester/src/app/app.js +++ b/plugins/woocommerce-beta-tester/src/app/app.js @@ -7,14 +7,14 @@ import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies */ -import { AdminNotes } from '../admin-notes'; import { default as Tools } from '../tools'; import { default as Options } from '../options'; import { default as Experiments } from '../experiments'; import { default as Features } from '../features'; import { default as RestAPIFilters } from '../rest-api-filters'; -import RemoteSpecValidator from '../remote-spec-validator'; import RemoteInboxNotifications from '../remote-inbox-notifications'; +import RemoteLogging from '../remote-logging'; +import Payments from '../payments'; const tabs = applyFilters( 'woocommerce_admin_test_helper_tabs', [ { @@ -47,6 +47,16 @@ const tabs = applyFilters( 'woocommerce_admin_test_helper_tabs', [ title: 'Remote Inbox Notifications', content: , }, + { + name: 'remote-logging', + title: 'Remote Logging', + content: , + }, + { + name: 'woocommerce-payments', + title: 'WCPay', + content: , + }, ] ); export function App() { diff --git a/plugins/woocommerce-beta-tester/src/index.js b/plugins/woocommerce-beta-tester/src/index.js index ab4fced9c3e..70b8dc79fe9 100644 --- a/plugins/woocommerce-beta-tester/src/index.js +++ b/plugins/woocommerce-beta-tester/src/index.js @@ -2,6 +2,7 @@ * External dependencies */ import { createRoot } from '@wordpress/element'; +import { addFilter } from '@wordpress/hooks'; /** * Internal dependencies @@ -10,6 +11,7 @@ import { App } from './app'; import './index.scss'; import './example-fills/experimental-woocommerce-wcpay-feature'; import { registerProductEditorDevTools } from './product-editor-dev-tools'; +import { registerExceptionFilter } from './remote-logging/register-exception-filter'; const appRoot = document.getElementById( 'woocommerce-admin-test-helper-app-root' @@ -20,3 +22,4 @@ if ( appRoot ) { } registerProductEditorDevTools(); +registerExceptionFilter(); diff --git a/plugins/woocommerce-beta-tester/src/options/data/actions.js b/plugins/woocommerce-beta-tester/src/options/data/actions.js index 1f3398281f3..b3a722b5c92 100644 --- a/plugins/woocommerce-beta-tester/src/options/data/actions.js +++ b/plugins/woocommerce-beta-tester/src/options/data/actions.js @@ -43,24 +43,26 @@ export function setNotice( notice ) { } export function* deleteOption( optionName ) { - try { - yield apiFetch( { - method: 'DELETE', - path: `${ API_NAMESPACE }/options/${ optionName }`, - } ); - yield { - type: TYPES.DELETE_OPTION, - optionName, - }; - } catch { - throw new Error(); - } + yield apiFetch( { + method: 'DELETE', + path: `${ API_NAMESPACE }/options/${ optionName }`, + } ); + yield { + type: TYPES.DELETE_OPTION, + optionName, + }; } export function* saveOption( optionName, newOptionValue ) { try { const payload = {}; - payload[ optionName ] = JSON.parse( newOptionValue ); + try { + // If the option value is a JSON string, parse it. + payload[ optionName ] = JSON.parse( newOptionValue ); + } catch ( error ) { + // If it's not a JSON string, just use the value as is. + payload[ optionName ] = newOptionValue; + } yield apiFetch( { method: 'POST', path: '/wc-admin/options', @@ -71,11 +73,11 @@ export function* saveOption( optionName, newOptionValue ) { status: 'success', message: optionName + ' has been saved.', } ); - } catch { + } catch ( error ) { yield setNotice( { status: 'error', message: 'Unable to save ' + optionName, } ); - throw new Error(); + throw error; } } diff --git a/plugins/woocommerce-beta-tester/src/options/data/resolvers.js b/plugins/woocommerce-beta-tester/src/options/data/resolvers.js index 6c1ec7ce2ad..60bc82fb6a6 100644 --- a/plugins/woocommerce-beta-tester/src/options/data/resolvers.js +++ b/plugins/woocommerce-beta-tester/src/options/data/resolvers.js @@ -17,14 +17,10 @@ export function* getOptions( search ) { yield setLoadingState( true ); - try { - const response = yield apiFetch( { - path, - } ); - yield setOptions( response ); - } catch ( error ) { - throw new Error(); - } + const response = yield apiFetch( { + path, + } ); + yield setOptions( response ); } export function* getOptionForEditing( optionName ) { @@ -41,21 +37,17 @@ export function* getOptionForEditing( optionName ) { const path = '/wc-admin/options?options=' + optionName; - try { - const response = yield apiFetch( { - path, - } ); + const response = yield apiFetch( { + path, + } ); - let content = response[ optionName ]; - if ( typeof content === 'object' ) { - content = JSON.stringify( response[ optionName ], null, 2 ); - } - - yield setOptionForEditing( { - name: optionName, - content, - } ); - } catch ( error ) { - throw new Error( error ); + let content = response[ optionName ]; + if ( typeof content === 'object' ) { + content = JSON.stringify( response[ optionName ], null, 2 ); } + + yield setOptionForEditing( { + name: optionName, + content, + } ); } diff --git a/plugins/woocommerce-beta-tester/src/payments/index.js b/plugins/woocommerce-beta-tester/src/payments/index.js new file mode 100644 index 00000000000..f340b904f37 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/payments/index.js @@ -0,0 +1,160 @@ +/** + * External dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { ORDERS_STORE_NAME } from '@woocommerce/data'; +import { ToggleControl } from '@wordpress/components'; +import apiFetch from '@wordpress/api-fetch'; + +const metaKey = '_wcpay_mode'; + +const Payments = () => { + const { + orders = [], + isRequesting, + isError, + } = useSelect( ( select ) => { + const { getOrders, hasFinishedResolution, getOrdersError } = + select( ORDERS_STORE_NAME ); + + const query = { + page: 1, + per_page: 10, + }; + const orders = getOrders( query, null ); + const isRequesting = hasFinishedResolution( 'getOrders', [ query ] ); + + return { + orders, + isError: Boolean( getOrdersError( orders ) ), + isRequesting, + }; + } ); + + const { getOrderSuccess } = useDispatch( ORDERS_STORE_NAME ); + + const isTestOrder = ( order ) => + order.meta_data.find( ( metaItem ) => metaItem.key === metaKey ) + ?.value === 'test'; + + const onToggle = async ( order, isChecked ) => { + const data = { + meta_data: [ + { + key: metaKey, + value: isChecked ? 'test' : 'live', + }, + ], + }; + + try { + const updatedOrder = await apiFetch( { + path: `/wc/v3/orders/${ order.id }`, + method: 'PUT', + data: data, + headers: { + 'Content-Type': 'application/json', + }, + } ); + getOrderSuccess( order.id, updatedOrder ); + } catch ( error ) { + throw error; + } + }; + + const renderOrders = ( orders ) => { + return orders.map( ( order ) => { + return ( + + + { `${ order?.billing?.first_name } ${ order?.billing?.last_name }` } + + + { order.id } + + + { order.date_created_gmt } + + + { order.status } + + + { order.total } + + + + onToggle( order, isChecked ) + } + /> + + + ); + } ); + }; + + return ( + <> +

WooCommerce Payments

+ + + + + + + + + + + + + { ! isRequesting && + orders?.length && + renderOrders( orders ) } + +
+ Order + + ID + + Date + + Status + + Total + + WCPay Test Order +
+ { ! isRequesting && orders?.length === 0 && ( +

No orders found.

+ ) } + + ); +}; + +export default Payments; diff --git a/plugins/woocommerce-beta-tester/src/remote-logging/index.tsx b/plugins/woocommerce-beta-tester/src/remote-logging/index.tsx new file mode 100644 index 00000000000..26e5b985f37 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/remote-logging/index.tsx @@ -0,0 +1,323 @@ +/** + * External dependencies + */ + +import { useState, useEffect } from '@wordpress/element'; +import { Button, ToggleControl, Notice, Spinner } from '@wordpress/components'; +import apiFetch from '@wordpress/api-fetch'; +import { log, init as initRemoteLogging } from '@woocommerce/remote-logging'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore no types +// eslint-disable-next-line @woocommerce/dependency-group +import { dispatch } from '@wordpress/data'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore no types +// eslint-disable-next-line @woocommerce/dependency-group +import { STORE_KEY as OPTIONS_STORE_NAME } from '../options/data/constants'; + +export const API_NAMESPACE = '/wc-admin-test-helper'; + +interface RemoteLoggingStatus { + isEnabled: boolean; + wpEnvironment: string; +} + +interface NoticeState { + status: 'success' | 'error' | 'warning' | 'info'; + message: string; +} + +function RemoteLogging() { + const [ isRemoteLoggingEnabled, setIsRemoteLoggingEnabled ] = useState< + boolean | null + >( null ); + const [ wpEnvironment, setWpEnvironment ] = useState< string >( '' ); + const [ notice, setNotice ] = useState< NoticeState | null >( null ); + + useEffect( () => { + const fetchRemoteLoggingStatus = async () => { + try { + const response: RemoteLoggingStatus = await apiFetch( { + path: `${ API_NAMESPACE }/remote-logging/status`, + } ); + setIsRemoteLoggingEnabled( response.isEnabled ); + setWpEnvironment( response.wpEnvironment ); + } catch ( error ) { + setNotice( { + status: 'error', + message: 'Failed to fetch remote logging status.', + } ); + } + }; + + fetchRemoteLoggingStatus(); + }, [] ); + + const toggleRemoteLogging = async () => { + try { + const response: RemoteLoggingStatus = await apiFetch( { + path: `${ API_NAMESPACE }/remote-logging/toggle`, + method: 'POST', + data: { enable: ! isRemoteLoggingEnabled }, + } ); + setIsRemoteLoggingEnabled( response.isEnabled ); + + window.wcSettings.isRemoteLoggingEnabled = response.isEnabled; + } catch ( error ) { + setNotice( { + status: 'error', + message: `Failed to update remote logging status. ${ JSON.stringify( + error + ) }`, + } ); + } + + if ( window.wcSettings.isRemoteLoggingEnabled ) { + initRemoteLogging( { + errorRateLimitMs: 60000, // 1 minute + } ); + } + }; + + const simulatePhpException = async ( context: 'core' | 'beta-tester' ) => { + try { + await dispatch( OPTIONS_STORE_NAME ).saveOption( + 'wc_beta_tester_simulate_woocommerce_php_error', + context + ); + setNotice( { + status: 'success', + message: `Please refresh your browser to trigger the PHP exception in ${ context } context.`, + } ); + } catch ( error ) { + setNotice( { + status: 'error', + message: `Failed to trigger PHP exception test in ${ context } context. ${ JSON.stringify( + error + ) }`, + } ); + } + }; + + const logPhpEvent = async () => { + try { + await apiFetch( { + path: `${ API_NAMESPACE }/remote-logging/log-event`, + method: 'POST', + } ); + setNotice( { + status: 'success', + message: 'Remote event logged successfully.', + } ); + } catch ( error ) { + setNotice( { + status: 'error', + message: `Failed to log remote event.`, + } ); + } + }; + + const resetPhpRateLimit = async () => { + try { + await apiFetch( { + path: `${ API_NAMESPACE }/remote-logging/reset-rate-limit`, + method: 'POST', + } ); + setNotice( { + status: 'success', + message: 'PHP rate limit reset successfully.', + } ); + } catch ( error ) { + setNotice( { + status: 'error', + message: `Failed to reset PHP rate limit. ${ JSON.stringify( + error + ) }`, + } ); + } + }; + + const simulateException = async ( context: 'core' | 'beta-tester' ) => { + try { + await dispatch( OPTIONS_STORE_NAME ).saveOption( + 'wc_beta_tester_simulate_woocommerce_js_error', + context + ); + + if ( context === 'core' ) { + setNotice( { + status: 'success', + message: `Please go to WooCommerce pages to trigger the JS exception in woocommerce context.`, + } ); + } else { + setNotice( { + status: 'success', + message: + 'Please refresh your browser to trigger the JS exception in woocommerce beta tester context.', + } ); + } + } catch ( error ) { + setNotice( { + status: 'error', + message: `Failed to set up JS exception test`, + } ); + } + }; + + const logJsEvent = async () => { + try { + const result = await log( + 'info', + 'Test JS event from WooCommerce Beta Tester', + { + extra: { + source: 'wc-beta-tester', + }, + } + ); + + if ( ! result ) { + throw new Error(); + } + + setNotice( { + status: 'success', + message: 'JS event logged successfully.', + } ); + } catch ( error ) { + setNotice( { + status: 'error', + message: + 'Failed to log JS event. Try enabling debug mode `window.localStorage.setItem( "debug", "wc:remote-logging" )` to see the details.', + } ); + } + }; + + const resetJsRateLimit = () => { + window.localStorage.removeItem( + 'wc_remote_logging_last_error_sent_time' + ); + setNotice( { + status: 'success', + message: 'JS rate limit reset successfully.', + } ); + }; + + if ( isRemoteLoggingEnabled === null ) { + return ; + } + + return ( +
+

Remote Logging

+ { notice && ( +
+ setNotice( null ) } + > + { notice.message } + +
+ ) } + + { ! isRemoteLoggingEnabled && ( +

+ Enable remote logging to test log event functionality. +

+ ) } + + { ( wpEnvironment === 'local' || + wpEnvironment === 'development' ) && ( +
+ + Warning: You are in a { wpEnvironment } environment. + Remote logging may not work as expected. Please set + WP_ENVIRONMENT_TYPE to{ ' ' } + production + in your wp-config.php file to test remote logging. + +
+ ) } + + + +
+

PHP Integration

+

Test PHP remote logging functionality:

+
+ + + + +
+ +
+ +

JavaScript Integration

+

Test JavaScript remote logging functionality:

+
+ + + + +
+
+ ); +} + +export default RemoteLogging; diff --git a/plugins/woocommerce-beta-tester/src/remote-logging/register-exception-filter.tsx b/plugins/woocommerce-beta-tester/src/remote-logging/register-exception-filter.tsx new file mode 100644 index 00000000000..210610d6575 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/remote-logging/register-exception-filter.tsx @@ -0,0 +1,93 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @woocommerce/dependency-group */ + +/** + * External dependencies + */ +import { addFilter } from '@wordpress/hooks'; + +import apiFetch from '@wordpress/api-fetch'; +// @ts-ignore no types +import { dispatch } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { API_NAMESPACE } from './'; +// @ts-ignore no types +import { STORE_KEY as OPTIONS_STORE_NAME } from '../options/data/constants'; + +/** + * Retrieves the options for simulating a WooCommerce JavaScript error. + * + * @return {Promise} The options if available, null otherwise. + */ +const getSimulateErrorOptions = async () => { + try { + const path = `${ API_NAMESPACE }/options?search=wc_beta_tester_simulate_woocommerce_js_error`; + + const options = await apiFetch< + [ + { + option_value: string; + option_name: string; + option_id: number; + } + ] + >( { + path, + } ); + return options && options.length > 0 ? options : null; + } catch ( error ) { + // eslint-disable-next-line no-console + console.error( 'Error retrieving simulate error options:', error ); + return null; + } +}; + +/** + * Deletes the option used for simulating WooCommerce JavaScript errors. + */ +const deleteSimulateErrorOption = async () => { + await dispatch( OPTIONS_STORE_NAME ).deleteOption( + 'wc_beta_tester_simulate_woocommerce_js_error' + ); +}; + +/** + * Adds a filter to throw an exception in the WooCommerce core context. + */ +const addCoreExceptionFilter = () => { + addFilter( 'woocommerce_admin_pages_list', 'wc-beta-tester', () => { + deleteSimulateErrorOption(); + + throw new Error( + 'Test JS exception in WC Core context via WC Beta Tester' + ); + } ); +}; + +/** + * Throws an exception specific to the WooCommerce Beta Tester context. + */ +const throwBetaTesterException = () => { + throw new Error( 'Test JS exception from WooCommerce Beta Tester' ); +}; + +/** + * Registers an exception filter for simulating JavaScript errors in WooCommerce. + * This function is used for testing purposes in the WooCommerce Beta Tester plugin. + */ +export const registerExceptionFilter = async () => { + const options = await getSimulateErrorOptions(); + if ( ! options ) { + return; + } + + const context = options[ 0 ].option_value; + if ( context === 'core' ) { + addCoreExceptionFilter(); + } else { + deleteSimulateErrorOption(); + throwBetaTesterException(); + } +}; diff --git a/plugins/woocommerce-beta-tester/src/tools/commands/fake-woo-payments.js b/plugins/woocommerce-beta-tester/src/tools/commands/fake-woo-payments.js new file mode 100644 index 00000000000..ef41cf76fb6 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/tools/commands/fake-woo-payments.js @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { STORE_KEY } from '../data/constants'; + +export const FAKE_WOO_PAYMENTS_ACTION_NAME = 'fakeWooPayments'; + +export const FakeWooPayments = () => { + const isEnabled = useSelect( ( select ) => + select( STORE_KEY ).getIsFakeWooPaymentsEnabled() + ); + const getDescription = () => { + switch ( isEnabled ) { + case 'yes': + return 'Enabled 🟢'; + case 'no': + return 'Disabled 🔴'; + case 'error': + return 'Error 🙁'; + default: + return 'Loading ...'; + } + }; + + return
{ getDescription() }
; +}; diff --git a/plugins/woocommerce-beta-tester/src/tools/commands/index.js b/plugins/woocommerce-beta-tester/src/tools/commands/index.js index e077ae389bf..66ba187eefa 100644 --- a/plugins/woocommerce-beta-tester/src/tools/commands/index.js +++ b/plugins/woocommerce-beta-tester/src/tools/commands/index.js @@ -15,6 +15,15 @@ import { SetComingSoonMode, UPDATE_COMING_SOON_MODE_ACTION_NAME, } from './set-coming-soon-mode'; +import { + FakeWooPayments, + FAKE_WOO_PAYMENTS_ACTION_NAME, +} from './fake-woo-payments'; + +import { + UPDATE_WCCOM_REQUEST_ERRORS_MODE, + SetWccomRequestErrros, +} from './set-wccom-request-errors'; export default [ { @@ -87,4 +96,14 @@ export default [ description: , action: UPDATE_COMING_SOON_MODE_ACTION_NAME, }, + { + command: 'Force errors on woocommerce.com requests', + description: , + action: UPDATE_WCCOM_REQUEST_ERRORS_MODE, + }, + { + command: 'Toggle Fake WooPayments Completion Status', + description: , + action: FAKE_WOO_PAYMENTS_ACTION_NAME, + }, ]; diff --git a/plugins/woocommerce-beta-tester/src/tools/commands/set-wccom-request-errors.js b/plugins/woocommerce-beta-tester/src/tools/commands/set-wccom-request-errors.js new file mode 100644 index 00000000000..d4c108665a6 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/tools/commands/set-wccom-request-errors.js @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import { SelectControl } from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { STORE_KEY } from '../data/constants'; + +export const UPDATE_WCCOM_REQUEST_ERRORS_MODE = 'updateWccomRequestErrorsMode'; + +const OPTIONS = [ + { label: 'Timeout after 6 seconds', value: 'timeout' }, + { label: '500', value: 'error' }, + { label: 'Disabled', value: 'disabled' }, +]; + +export const SetWccomRequestErrros = () => { + const errorsMode = useSelect( + ( select ) => select( STORE_KEY ).getWccomRequestErrorsMode(), + [] + ); + const { updateCommandParams } = useDispatch( STORE_KEY ); + + function onChange( mode ) { + updateCommandParams( UPDATE_WCCOM_REQUEST_ERRORS_MODE, { + mode, + } ); + } + + return ( +
+ { ! errorsMode ? ( +

Loading ...

+ ) : ( + + ) } +
+ ); +}; diff --git a/plugins/woocommerce-beta-tester/src/tools/data/actions.js b/plugins/woocommerce-beta-tester/src/tools/data/actions.js index 9e34802dd2c..e2cc1e67cb4 100644 --- a/plugins/woocommerce-beta-tester/src/tools/data/actions.js +++ b/plugins/woocommerce-beta-tester/src/tools/data/actions.js @@ -229,7 +229,7 @@ export function* resetCustomizeYourStore() { } ); yield apiFetch( { - path: '/wc/private/ai/patterns', + path: '/wc-admin/ai/patterns', method: 'DELETE', } ); } ); @@ -272,3 +272,38 @@ export function* updateComingSoonMode( params ) { } ); } ); } + +export function* updateWccomRequestErrorsMode( params ) { + yield runCommand( 'Update wccom request errors mode', function* () { + yield apiFetch( { + path: API_NAMESPACE + '/tools/set-wccom-request-errors/v1', + method: 'POST', + data: params, + } ); + } ); +} + +export function* fakeWooPayments( params ) { + yield runCommand( 'Toggle Fake WooPayments Completion', function* () { + const newStatus = params.enabled === 'yes' ? 'no' : 'yes'; + + yield apiFetch( { + path: API_NAMESPACE + '/tools/fake-wcpay-completion/v1', + method: 'POST', + data: { + enabled: newStatus, + }, + } ); + + yield updateCommandParams( 'fakeWooPayments', { + enabled: newStatus, + } ); + + yield updateMessage( + 'Toggle Fake WooPayments Completion', + `Fake WooPayments completion ${ + newStatus === 'yes' ? 'disabled' : 'enabled' + }` + ); + } ); +} diff --git a/plugins/woocommerce-beta-tester/src/tools/data/reducer.js b/plugins/woocommerce-beta-tester/src/tools/data/reducer.js index 9a15d468c83..481561117d8 100644 --- a/plugins/woocommerce-beta-tester/src/tools/data/reducer.js +++ b/plugins/woocommerce-beta-tester/src/tools/data/reducer.js @@ -13,6 +13,8 @@ const DEFAULT_STATE = { updateComingSoonMode: {}, updateBlockTemplateLoggingThreshold: {}, runSelectedUpdateCallbacks: {}, + updateWccomRequestErrorsMode: {}, + fakeWooPayments: {}, }, status: '', dbUpdateVersions: [], diff --git a/plugins/woocommerce-beta-tester/src/tools/data/resolvers.js b/plugins/woocommerce-beta-tester/src/tools/data/resolvers.js index 5f55565925d..0e839b27760 100644 --- a/plugins/woocommerce-beta-tester/src/tools/data/resolvers.js +++ b/plugins/woocommerce-beta-tester/src/tools/data/resolvers.js @@ -17,6 +17,8 @@ import { import { UPDATE_BLOCK_TEMPLATE_LOGGING_THRESHOLD_ACTION_NAME } from '../commands/update-block-template-logging-threshold'; import { UPDATE_COMING_SOON_MODE_ACTION_NAME } from '../commands/set-coming-soon-mode'; import { TRIGGER_UPDATE_CALLBACKS_ACTION_NAME } from '../commands/trigger-update-callbacks'; +import { UPDATE_WCCOM_REQUEST_ERRORS_MODE } from '../commands/set-wccom-request-errors'; +import { FAKE_WOO_PAYMENTS_ACTION_NAME } from '../commands/fake-woo-payments'; export function* getCronJobs() { const path = `${ API_NAMESPACE }/tools/get-cron-list/v1`; @@ -117,3 +119,34 @@ export function* getComingSoonMode() { throw new Error( error ); } } + +export function* getWccomRequestErrorsMode() { + const path = `${ API_NAMESPACE }/tools/get-wccom-request-errors/v1`; + + try { + const mode = yield apiFetch( { + path, + method: 'GET', + } ); + + yield updateCommandParams( UPDATE_WCCOM_REQUEST_ERRORS_MODE, { + mode: mode || 'disabled', + } ); + } catch ( error ) { + throw new Error( error ); + } +} + +export function* getIsFakeWooPaymentsEnabled() { + try { + const response = yield apiFetch( { + path: API_NAMESPACE + '/tools/fake-wcpay-completion/v1', + method: 'GET', + } ); + yield updateCommandParams( FAKE_WOO_PAYMENTS_ACTION_NAME, { + enabled: response.enabled || 'no', + } ); + } catch ( error ) { + throw new Error( error ); + } +} diff --git a/plugins/woocommerce-beta-tester/src/tools/data/selectors.js b/plugins/woocommerce-beta-tester/src/tools/data/selectors.js index 1ac46fc713e..bca03b9bbf9 100644 --- a/plugins/woocommerce-beta-tester/src/tools/data/selectors.js +++ b/plugins/woocommerce-beta-tester/src/tools/data/selectors.js @@ -37,3 +37,11 @@ export function getBlockTemplateLoggingThreshold( state ) { export function getComingSoonMode( state ) { return state.params.updateComingSoonMode.mode; } + +export function getWccomRequestErrorsMode( state ) { + return state.params.updateWccomRequestErrorsMode.mode; +} + +export function getIsFakeWooPaymentsEnabled( state ) { + return state.params.fakeWooPayments.enabled; +} diff --git a/plugins/woocommerce-beta-tester/typing/global.d.ts b/plugins/woocommerce-beta-tester/typing/global.d.ts new file mode 100644 index 00000000000..64c87d09659 --- /dev/null +++ b/plugins/woocommerce-beta-tester/typing/global.d.ts @@ -0,0 +1,9 @@ +declare global { + interface Window { + wcSettings: { + isRemoteLoggingEnabled: boolean; + }; + } +} + +export {}; diff --git a/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php b/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php index ca0e6b53604..bc1d9a76d72 100644 --- a/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php +++ b/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce Beta Tester * Plugin URI: https://github.com/woocommerce/woocommerce-beta-tester * Description: Run bleeding edge versions of WooCommerce. This will replace your installed version of WooCommerce with the latest tagged release - use with caution, and not on production sites. - * Version: 2.3.2 + * Version: 2.4.0 * Author: WooCommerce * Author URI: https://woocommerce.com/ * Requires at least: 5.8 @@ -30,7 +30,7 @@ if ( ! defined( 'WC_BETA_TESTER_FILE' ) ) { } if ( ! defined( 'WC_BETA_TESTER_VERSION' ) ) { - define( 'WC_BETA_TESTER_VERSION', '2.3.2' ); // WRCS: DEFINED_VERSION. + define( 'WC_BETA_TESTER_VERSION', '2.4.0' ); // WRCS: DEFINED_VERSION. } /** @@ -64,6 +64,7 @@ function _wc_beta_tester_bootstrap() { // Tools. include dirname( __FILE__ ) . '/includes/class-wc-beta-tester-version-picker.php'; include dirname( __FILE__ ) . '/includes/class-wc-beta-tester-override-coming-soon-options.php'; + include dirname( __FILE__ ) . '/includes/class-wc-beta-tester-wccom-requests.php'; register_activation_hook( __FILE__, array( 'WC_Beta_Tester', 'activate' ) ); @@ -138,5 +139,28 @@ add_action( } ); + +/** + * Simulate a WooCommerce error for remote logging testing. + * + * @throws Exception A simulated WooCommerce error if the option is set. + */ +function simulate_woocommerce_error() { + throw new Exception( 'Simulated WooCommerce error for remote logging test' ); +} + +$simulate_error = get_option( 'wc_beta_tester_simulate_woocommerce_php_error', false ); + +if ( $simulate_error ) { + delete_option( 'wc_beta_tester_simulate_woocommerce_php_error' ); + + if ( 'core' === $simulate_error ) { + add_action( 'woocommerce_loaded', 'simulate_woocommerce_error' ); + } elseif ( 'beta-tester' === $simulate_error ) { + throw new Exception( 'Test PHP exception from WooCommerce Beta Tester' ); + } +} + + // Initialize the live branches feature. require_once dirname( __FILE__ ) . '/includes/class-wc-beta-tester-live-branches.php'; diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart-form/block.json b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart-form/block.json index 7eff52de309..f25ab844939 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart-form/block.json +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart-form/block.json @@ -10,9 +10,13 @@ "default": false } }, - "keywords": [ "WooCommerce" ], - "usesContext": [ "postId" ], + "keywords": [ + "WooCommerce" + ], + "usesContext": [ + "postId" + ], "textdomain": "woocommerce", - "apiVersion": 2, + "apiVersion": 3, "$schema": "https://schemas.wp.org/trunk/block.json" -} +} \ No newline at end of file diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/average-rating/block.json b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/average-rating/block.json index a3785daacac..db05491ea58 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/average-rating/block.json +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/average-rating/block.json @@ -1,17 +1,20 @@ { "name": "woocommerce/product-average-rating", - "version": "1.0.0", "title": "Product Average Rating (Beta)", "description": "Display the average rating of a product", + "apiVersion": 3, "category": "woocommerce-product-elements", "attributes": { "textAlign": { "type": "string" } }, - "keywords": [ "WooCommerce" ], - "ancestor": [ "woocommerce/single-product" ], + "keywords": [ + "WooCommerce" + ], + "ancestor": [ + "woocommerce/single-product" + ], "textdomain": "woocommerce", - "apiVersion": 2, "$schema": "https://schemas.wp.org/trunk/block.json" -} +} \ No newline at end of file diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/average-rating/index.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/average-rating/index.tsx index 6933405fd81..c3e836d0958 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/average-rating/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/average-rating/index.tsx @@ -12,6 +12,7 @@ import edit from './edit'; import { supports } from './support'; registerBlockType( metadata, { + apiVersion: 3, icon: { src: ( ( 'woocommerce/product-button', { }, get addToCartText(): string { const context = getContext(); + const inTheCartText = state.inTheCartText || ''; // We use the temporary number of items when there's no animation, or the // second part of the animation hasn't started. - if ( + const showTemporaryNumber = context.animationStatus === AnimationStatus.IDLE || - context.animationStatus === AnimationStatus.SLIDE_OUT - ) { - return getButtonText( - context.addToCartText, - state.inTheCartText!, - context.temporaryNumberOfItems - ); - } + context.animationStatus === AnimationStatus.SLIDE_OUT; + const numberOfItems = showTemporaryNumber + ? context.temporaryNumberOfItems + : state.numberOfItemsInTheCart; + return getButtonText( context.addToCartText, - state.inTheCartText!, - state.numberOfItemsInTheCart + inTheCartText, + numberOfItems ); }, get displayViewCart(): boolean { diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/button/index.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/button/index.tsx index d67a9223e1d..4ebb6647356 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/button/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/button/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @wordpress/no-unsafe-wp-apis */ /** * External dependencies */ @@ -54,6 +55,7 @@ const featurePluginSupport = { }; // @ts-expect-error: `metadata` currently does not have a type definition in WordPress core registerBlockType( metadata, { + apiVersion: 3, icon: { src: ( = { ), }; +const sizeUnits: { value: string; label: string }[] = [ + { + value: 'px', + label: 'px', + }, + { + value: 'em', + label: 'em', + }, + { + value: 'rem', + label: 'rem', + }, + { + value: '%', + label: '%', + }, + { + value: 'vw', + label: 'vw', + }, + { + value: 'vh', + label: 'vh', + }, +]; + export const ImageSizeSettings = ( { scale, width, @@ -60,12 +87,7 @@ export const ImageSizeSettings = ( { setAttributes( { height: value } ); } } value={ height } - units={ [ - { - value: 'px', - label: 'px', - }, - ] } + units={ sizeUnits } /> { height && ( { }; export default ( props: Props ) => { - // It is necessary because this block has to support serveral contexts: + // It is necessary because this block has to support several contexts: // - Inside `All Products Block` -> `withProductDataContext` HOC // - Inside `Products Block` -> Gutenberg Context // - Inside `Single Product Template` -> Gutenberg Context diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/price/index.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/price/index.tsx index 3dd27385f03..a2e8a06f520 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/price/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/price/index.tsx @@ -18,7 +18,6 @@ import { const blockConfig = { ...sharedConfig, - apiVersion: 2, title, description, usesContext: [ 'query', 'queryId', 'postId' ], diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/price/supports.ts b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/price/supports.ts index d471cf1f180..598a43d2939 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/price/supports.ts +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/price/supports.ts @@ -1,3 +1,4 @@ +/* eslint-disable @wordpress/no-unsafe-wp-apis */ /** * External dependencies */ diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-details/block.json b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-details/block.json index 586c17fc76f..6e8df5f272d 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-details/block.json +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-details/block.json @@ -5,7 +5,9 @@ "title": "Product Details", "description": "Display a product's description, attributes, and reviews.", "category": "woocommerce-product-elements", - "keywords": [ "WooCommerce" ], + "keywords": [ + "WooCommerce" + ], "supports": { "align": true, "spacing": { @@ -13,6 +15,6 @@ } }, "textdomain": "woocommerce", - "apiVersion": 2, + "apiVersion": 3, "$schema": "https://schemas.wp.org/trunk/block.json" -} +} \ No newline at end of file diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-details/style.scss b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-details/style.scss index 03d146135a3..5a426110c5d 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-details/style.scss +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-details/style.scss @@ -41,16 +41,7 @@ html body .wp-block-woocommerce-product-details.is-style-minimal { display: inline-block; } - &:hover { - opacity: 1; - - a { - color: inherit; - border: none; - text-decoration: none; - } - } - + &:hover, &.active { color: inherit; background: inherit; @@ -68,6 +59,7 @@ html body .wp-block-woocommerce-product-details.is-style-minimal { display: block; border-left-width: 2px; border-bottom-width: 0; + margin-bottom: 2px; } &:first-child { diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-image-gallery/block.json b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-image-gallery/block.json index 59f926dd28c..0457b390c27 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-image-gallery/block.json +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-image-gallery/block.json @@ -9,9 +9,15 @@ "align": true, "multiple": false }, - "keywords": [ "WooCommerce" ], - "usesContext": [ "postId", "postType", "queryId" ], + "keywords": [ + "WooCommerce" + ], + "usesContext": [ + "postId", + "postType", + "queryId" + ], "textdomain": "woocommerce", - "apiVersion": 2, + "apiVersion": 3, "$schema": "https://schemas.wp.org/trunk/block.json" -} +} \ No newline at end of file diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-meta/block.json b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-meta/block.json index 6ffe742562c..ad5a6e5617c 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-meta/block.json +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-meta/block.json @@ -9,9 +9,15 @@ "align": true, "reusable": false }, - "keywords": [ "WooCommerce" ], - "usesContext": [ "postId", "postType", "queryId" ], + "keywords": [ + "WooCommerce" + ], + "usesContext": [ + "postId", + "postType", + "queryId" + ], "textdomain": "woocommerce", - "apiVersion": 2, + "apiVersion": 3, "$schema": "https://schemas.wp.org/trunk/block.json" -} +} \ No newline at end of file diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-reviews/block.json b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-reviews/block.json index 7a7fe4fb2db..a3b81107744 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-reviews/block.json +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/product-reviews/block.json @@ -5,11 +5,15 @@ "title": "Product Reviews", "description": "A block that shows the reviews for a product.", "category": "woocommerce-product-elements", - "keywords": [ "WooCommerce" ], + "keywords": [ + "WooCommerce" + ], "supports": {}, "attributes": {}, - "usesContext": [ "postId" ], + "usesContext": [ + "postId" + ], "textdomain": "woocommerce", - "apiVersion": 2, + "apiVersion": 3, "$schema": "https://schemas.wp.org/trunk/block.json" -} +} \ No newline at end of file diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/rating-counter/block.json b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/rating-counter/block.json index cc78687bb2f..d3e97976492 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/rating-counter/block.json +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/rating-counter/block.json @@ -26,13 +26,21 @@ "default": false } }, - "usesContext": [ "query", "queryId", "postId" ], - "keywords": [ "WooCommerce" ], + "usesContext": [ + "query", + "queryId", + "postId" + ], + "keywords": [ + "WooCommerce" + ], "supports": { "align": true }, - "ancestor": [ "woocommerce/single-product" ], + "ancestor": [ + "woocommerce/single-product" + ], "textdomain": "woocommerce", - "apiVersion": 2, + "apiVersion": 3, "$schema": "https://schemas.wp.org/trunk/block.json" -} +} \ No newline at end of file diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/rating-counter/index.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/rating-counter/index.tsx index 112bf44be73..cc390d5d61a 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/rating-counter/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/rating-counter/index.tsx @@ -12,6 +12,7 @@ import edit from './edit'; import { supports } from './support'; registerBlockType( metadata, { + apiVersion: 3, icon: { src: ( = { /> ), }, + apiVersion: 3, supports: { html: false, }, diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sku/edit.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sku/edit.tsx index 17032101036..f75e4e60056 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sku/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sku/edit.tsx @@ -60,9 +60,9 @@ const Edit = ( {
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/stock-indicator/index.ts b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/stock-indicator/index.ts index 25f3c7b80b0..447a47a255f 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/stock-indicator/index.ts +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/stock-indicator/index.ts @@ -20,7 +20,6 @@ import { const blockConfig: BlockConfiguration = { ...sharedConfig, - apiVersion: 2, title, description, icon: { src: icon }, diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/summary/index.ts b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/summary/index.ts index bbeec6f903e..3be1618d733 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/summary/index.ts +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/summary/index.ts @@ -23,7 +23,6 @@ const blockConfig: BlockConfiguration = { // Product Summary is not expected to be available in Product Collection, // Products (Beta) or Single Product blocks. They use core/post-summary variation. ancestor: [ 'woocommerce/all-products' ], - apiVersion: 2, title, description, icon: { src: icon }, diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/title/index.ts b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/title/index.ts index 5cfeb7cb072..cb1c1eaa890 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/title/index.ts +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/title/index.ts @@ -24,7 +24,6 @@ const blockConfig: BlockConfiguration = { // Product Title is not expected to be available in Product Collection // nor Products (Beta). They use core/post-title variation. ancestor: [ 'woocommerce/all-products', 'woocommerce/single-product' ], - apiVersion: 2, title, description, icon: { src: icon }, diff --git a/plugins/woocommerce-blocks/assets/js/atomic/utils/register-block-single-product-template.ts b/plugins/woocommerce-blocks/assets/js/atomic/utils/register-block-single-product-template.ts index 58f842c362d..88236a7ed4b 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/utils/register-block-single-product-template.ts +++ b/plugins/woocommerce-blocks/assets/js/atomic/utils/register-block-single-product-template.ts @@ -44,13 +44,14 @@ export const registerBlockSingleProductTemplate = ( { blockMetadata = blockName; } + const editSiteStore = select( 'core/edit-site' ); + subscribe( () => { const previousTemplateId = currentTemplateId; - const store = select( 'core/edit-site' ); // With GB 16.3.0 the return type can be a number: https://github.com/WordPress/gutenberg/issues/53230 currentTemplateId = parseTemplateId( - store?.getEditedPostId< string | number | undefined >() + editSiteStore?.getEditedPostId< string | number | undefined >() ); const hasChangedTemplate = previousTemplateId !== currentTemplateId; const hasTemplateId = Boolean( currentTemplateId ); @@ -108,7 +109,11 @@ export const registerBlockSingleProductTemplate = ( { // This subscribe callback could be invoked with the core/blocks store // which would cause infinite registration loops because of the `registerBlockType` call. // This local cache helps prevent that. - if ( ! isBlockRegistered && isAvailableOnPostEditor ) { + if ( + ! isBlockRegistered && + isAvailableOnPostEditor && + ! editSiteStore + ) { if ( isVariationBlock ) { blocksRegistered.add( variationName ); registerBlockVariation( diff --git a/plugins/woocommerce-blocks/assets/js/base/components/block-error-boundary/types.ts b/plugins/woocommerce-blocks/assets/js/base/components/block-error-boundary/types.ts index d91b90f7b99..06676bc2720 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/block-error-boundary/types.ts +++ b/plugins/woocommerce-blocks/assets/js/base/components/block-error-boundary/types.ts @@ -18,7 +18,7 @@ interface BlockErrorBase { */ text?: React.ReactNode | undefined; /** - * Text preceeding the error message. + * Text preceding the error message. */ errorMessagePrefix?: string | undefined; /** @@ -26,7 +26,7 @@ interface BlockErrorBase { */ button?: React.ReactNode; /** - * Controls wether to show the error block or fail silently + * Controls whether to show the error block or fail silently. */ showErrorBlock?: boolean; } diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/coupon/index.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/coupon/index.tsx index 4d0f25b0c12..df91b57a98a 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/coupon/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/coupon/index.tsx @@ -2,13 +2,14 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { useState } from '@wordpress/element'; +import { useState, useRef } from '@wordpress/element'; import Button from '@woocommerce/base-components/button'; import LoadingMask from '@woocommerce/base-components/loading-mask'; import { withInstanceId } from '@wordpress/compose'; import { ValidatedTextInput, ValidationInputError, + ValidatedTextInputHandle, Panel, } from '@woocommerce/blocks-components'; import { useSelect } from '@wordpress/data'; @@ -55,6 +56,7 @@ export const TotalsCoupon = ( { validationErrorId: store.getValidationErrorId( textInputId ), }; } ); + const inputRef = useRef< ValidatedTextInputHandle >( null ); const handleCouponSubmit: MouseEventHandler< HTMLButtonElement > = ( e: MouseEvent< HTMLButtonElement > @@ -65,6 +67,8 @@ export const TotalsCoupon = ( { if ( result ) { setCouponValue( '' ); setIsCouponFormVisible( false ); + } else if ( inputRef.current?.focus ) { + inputRef.current.focus(); } } ); } else { @@ -104,6 +108,7 @@ export const TotalsCoupon = ( { focusOnMount={ true } validateOnMount={ false } showError={ false } + ref={ inputRef } />