diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 677999b111b..3e3b424196e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -41,9 +41,8 @@ If you have questions about the process to contribute code or want to discuss de - Make sure to write good and detailed commit messages (see [this post](https://chris.beams.io/posts/git-commit/) for more on this) and follow all the applicable sections of the pull request template. - Please avoid modifying the changelog directly or updating the .pot files. These will be updated by the WooCommerce team. -If you are contributing code to the (Javascript-driven) WooCommerce Admin project or to Gutenberg blocks, note that these are developed in external packages. +If you are contributing code to the (Javascript-driven) Gutenberg blocks, note that it's developed in an external package. -- [WooCommerce Admin](https://github.com/woocommerce/woocommerce-admin) - [Blocks](https://github.com/woocommerce/woocommerce-gutenberg-products-block) ## Feature Requests šŸš€ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b5140fcd7f0..dd08989d3c3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -25,13 +25,10 @@ Closes # . * [ ] Have you added an explanation of what your changes do and why you'd like us to include them? * [ ] Have you written new tests for your changes, as applicable? * [ ] Have you successfully run tests with your changes locally? +* [ ] Have you created a changelog file by running `pnpm nx affected --target=changelog`? -### Changelog entry - -> Enter a summary of all changes on this Pull Request. This will appear in the changelog if accepted. - ### FOR PR REVIEWER ONLY: * [ ] I have reviewed that everything is sanitized/escaped appropriately for any SQL or XSS injection possibilities. I made sure Linting is not ignored or disabled. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b54e225a1b..ac8ab5115ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: - name: Build Admin feature config working-directory: ./ - run: pnpm nx build:feature-config woocommerce-admin + run: pnpm nx build:feature-config woocommerce - name: Add PHP8 Compatibility. run: | diff --git a/.github/workflows/pr-code-coverage.yml b/.github/workflows/pr-code-coverage.yml index 816398c1854..c32ef4a016a 100644 --- a/.github/workflows/pr-code-coverage.yml +++ b/.github/workflows/pr-code-coverage.yml @@ -61,7 +61,7 @@ jobs: - name: Build Admin feature config working-directory: ./ run: | - pnpm nx build:feature-config woocommerce-admin + pnpm nx build:feature-config woocommerce - name: Init DB and WP run: pnpm nx install-unit-test-db woocommerce diff --git a/.github/workflows/pr-lint-monorepo.yml b/.github/workflows/pr-lint-monorepo.yml new file mode 100644 index 00000000000..7c952b3a641 --- /dev/null +++ b/.github/workflows/pr-lint-monorepo.yml @@ -0,0 +1,26 @@ +name: Run lint checks potentially affecting projects across the monorepo +on: pull_request +concurrency: + group: changelogger-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true +jobs: + changelogger_used: + name: Changelogger use + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + + - name: Check change files are touched for touched projects + env: + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: php tools/monorepo/check-changelogger-use.php --debug "$BASE" "$HEAD" diff --git a/.github/workflows/pr-unit-tests.yml b/.github/workflows/pr-unit-tests.yml index ac5a5b8de58..9dcaedf3ee4 100644 --- a/.github/workflows/pr-unit-tests.yml +++ b/.github/workflows/pr-unit-tests.yml @@ -69,7 +69,7 @@ jobs: - name: Build Admin feature config working-directory: ./ run: | - pnpm nx build:feature-config woocommerce-admin + pnpm nx build:feature-config woocommerce - name: Add PHP8 Compatibility. run: | diff --git a/.github/workflows/scripts/add-post-merge-comment.php b/.github/workflows/scripts/add-post-merge-comment.php index ba2e13baf2d..afd6ec803f4 100644 --- a/.github/workflows/scripts/add-post-merge-comment.php +++ b/.github/workflows/scripts/add-post-merge-comment.php @@ -57,7 +57,6 @@ echo "The pull request was merged by: $merger_user_name\n"; $comment_body = "Hi @$merger_user_name, thanks for merging this pull request. Please take a look at these follow-up tasks you may need to perform: -- [ ] Add the `release: add changelog` label - [ ] Add the `release: add testing instructions` label"; $add_comment_mutation = " diff --git a/.husky/post-merge b/.husky/post-merge new file mode 100755 index 00000000000..48bc4affda4 --- /dev/null +++ b/.husky/post-merge @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +pnpm install +pnpm nx affected --target="composer-install" --base=ORIG_HEAD --head=HEAD diff --git a/plugins/woocommerce-admin/.husky/pre-commit b/.husky/pre-commit similarity index 100% rename from plugins/woocommerce-admin/.husky/pre-commit rename to .husky/pre-commit diff --git a/plugins/woocommerce-admin/.husky/pre-push b/.husky/pre-push similarity index 61% rename from plugins/woocommerce-admin/.husky/pre-push rename to .husky/pre-push index db541032602..e52fd7c5635 100755 --- a/plugins/woocommerce-admin/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -node bin/pre-push-hook.js +./bin/pre-push.sh diff --git a/bin/pre-push.sh b/bin/pre-push.sh new file mode 100755 index 00000000000..9b2346b136f --- /dev/null +++ b/bin/pre-push.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +PROTECTED_BRANCH="trunk" +CURRENT_BRANCH=$(git branch --show-current) +if [ $PROTECTED_BRANCH = $CURRENT_BRANCH ]; then + if [ "$TERM" = "dumb" ]; then + >&2 echo "Sorry, you are unable to push to $PROTECTED_BRANCH using a GUI client! Please use git CLI." + exit 1 + fi + + printf "%sYou're about to push to $PROTECTED_BRANCH, is that what you intended? [y/N]: %s" "$(tput setaf 3)" "$(tput sgr0)" + read -r PROCEED < /dev/tty + echo + + if [ "$(echo "${PROCEED:-n}" | tr "[:upper:]" "[:lower:]")" = "y" ]; then + echo "$(tput setaf 2)Brace yourself! Pushing to the $PROTECTED_BRANCH branch...$(tput sgr0)" + echo + exit 0 + fi + + echo "$(tput setaf 2)Push to $PROTECTED_BRANCH cancelled!$(tput sgr0)" + echo + exit 1 +fi diff --git a/changelog.txt b/changelog.txt index 2d055307fe0..ee3248e0e05 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,103 @@ == Changelog == += 6.4.0 2022-04-12 = + +**WooCommerce** + +- Add - Scaffolding for the custom orders table feature. ([#31692](https://github.com/woocommerce/woocommerce/pull/31692)) +- Add - Add DB table structure for custom order tables. ([#31811](https://github.com/woocommerce/woocommerce/pull/31811)) +- Add - Primary key for the product attributes lookup table. ([#32067](https://github.com/woocommerce/woocommerce/pull/32067)) +- Add - Tracks to the dashboard status widget and setup widget. ([#31857](https://github.com/woocommerce/woocommerce/pull/31857)) +- Add - Check around setup widget display when features are disabled. ([#31884](https://github.com/woocommerce/woocommerce/pull/31884)) +- Add - 'woocommerce_get_formatted_meta_data_include_all_meta_lines' filter hook. This can be used to control whether metadata lines are shown in the order meta box. ([#30948](https://github.com/woocommerce/woocommerce/pull/30948)) +- Enhancement - Introduce rate_limit_remaining column in the wc_rate_limits table. ([#32041](https://github.com/woocommerce/woocommerce/pull/32041)) +- Tweak - Update PayPal Standard JS used in the admin environment to avoid deprecated functionality. ([#32076](https://github.com/woocommerce/woocommerce/pull/32076)) +- Tweak - Change level of escaping used to render the CSV import error log. ([#32000](https://github.com/woocommerce/woocommerce/pull/32000)) +- Tweak - Make the payment_url field available via the REST API's orders endpoint. ([#31826](https://github.com/woocommerce/woocommerce/pull/31826)) +- Tweak - Rename WC_API_Exception code woocommerce_api_cannot_edit_product_catgory into woocommerce_api_cannot_edit_product_category ([#31785](https://github.com/woocommerce/woocommerce/pull/31785)) +- Tweak - Updated default email color to new Woo purple ([#30586](https://github.com/woocommerce/woocommerce/pull/30586)) +- Fix - Avoid depending on the presence of a theme header template to clear the cart after payment is made. ([#31877](https://github.com/woocommerce/woocommerce/pull/31877)) +- Fix - Payments tab tracking. ([#31844](https://github.com/woocommerce/woocommerce/pull/31844)) +- Fix - Remove unnecessary duplicate style in email-styles template. ([#31860](https://github.com/woocommerce/woocommerce/pull/31860)) +- Fix - incorrect position value for registering menu pages. ([#31779](https://github.com/woocommerce/woocommerce/pull/31779)) +- Fix - SZL currency symbol. Updated from 'L' to 'E'. ([#30602](https://github.com/woocommerce/woocommerce/pull/30602)) +- Fix - Removed execution of at least one hook ignoring the `woocommerce_load_webhooks_limit` filter value. ([#29002](https://github.com/woocommerce/woocommerce/pull/29002)) +- Dev - Added has_options() to REST API v3 product endpoint response. ([#32031](https://github.com/woocommerce/woocommerce/pull/32031)) +- Dev - Added woocommerce_admin_order_should_render_refunds hook to allow control over the refunds UI within the order editor. ([#31414](https://github.com/woocommerce/woocommerce/pull/31414)) + +**WooCommerce Admin - 3.3.0 & 3.3.1 & 3.3.2** + +- Add - Add asynchronous plugin install and activation endpoints ([#8079](https://github.com/woocommerce/woocommerce-admin/pull/8079)) +- Performance - Avoid expensive get_notes() queries in CouponPageMoved admin_init actions by using new Notes::get_note_by_name() helper method. ([#8202](https://github.com/woocommerce/woocommerce-admin/pull/8202)) +- Enhancement - Add chart color filter for overriding default chart colors. ([#8258](https://github.com/woocommerce/woocommerce-admin/pull/8258)) +- Enhancement - Added Typescript type declarations to build for @woocommerce/components ([#8282](https://github.com/woocommerce/woocommerce-admin/pull/8282)) +- Enhancement - Increase color selection limit to ten and add additional colors. ([#8258](https://github.com/woocommerce/woocommerce-admin/pull/8258)) +- Enhancement - Made @woocommerce/components/Stepper a Typescript file ([#8286](https://github.com/woocommerce/woocommerce-admin/pull/8286)) +- Enhancement - Prompts a modal to save any unsaved changes when the users try to move to a different step ([#8278](https://github.com/woocommerce/woocommerce-admin/pull/8278)) +- Tweak - OBW: Override Country/Region label line-height style to avoid truncated descenders. ([#8186](https://github.com/woocommerce/woocommerce-admin/pull/8186)) +- Tweak - Show single success message for theme install and activation ([#8236](https://github.com/woocommerce/woocommerce-admin/pull/8236)) +- Tweak - Use WC_VERSION as cache buster for assets ([#8308](https://github.com/woocommerce/woocommerce-admin/pull/8308)) +- Update - Adjust time range and add an image for the Jetpack Backup note. ([#8293](https://github.com/woocommerce/woocommerce-admin/pull/8293)) +- Update - Implement MailChimp API request threshold for MailchimpScheduler. ([#8342](https://github.com/woocommerce/woocommerce-admin/pull/8342)) +- Update - Reintroduce CES on product add, product update, and order update. ([#8238](https://github.com/woocommerce/woocommerce-admin/pull/8238)) +- Update - Replace mysql image with mariadb ([#8220](https://github.com/woocommerce/woocommerce-admin/pull/8220)) +- Update - Update country support list for WooCommerce Payments Task. ([#8517](https://github.com/woocommerce/woocommerce-admin/pull/8517)) +- Fix - Fix handling of paid themes in purchase task. ([#8493](https://github.com/woocommerce/woocommerce-admin/pull/8493)) +- Fix - Make sure the paid extension task is also shown for themes. ([#8412](https://github.com/woocommerce/woocommerce-admin/pull/8412)) +- Fix - Reintroduce emphasis on inbox note action button. ([#8411](https://github.com/woocommerce/woocommerce-admin/pull/8411)) +- Fix - Remove class ExtendedPayments. ([#8461](https://github.com/woocommerce/woocommerce-admin/pull/8461)) +- Fix - Added random IDs to SVG checkmarks in stepper component ([#8222](https://github.com/woocommerce/woocommerce-admin/pull/8222)) +- Fix - Fix Google Listings plugin is always shown in free features despite already activated. ([#8330](https://github.com/woocommerce/woocommerce-admin/pull/8330)) +- Fix - Fix hidden notes in `admin/notes` endpoint when the user is not in the tasklist experiment. ([#8328](https://github.com/woocommerce/woocommerce-admin/pull/8328)) +- Fix - Fix missing product name in variation analytic page for the deleted products. ([#8255](https://github.com/woocommerce/woocommerce-admin/pull/8255)) +- Fix - Fix payments extensions displayed below the offline payments options. ([#8232](https://github.com/woocommerce/woocommerce-admin/pull/8232)) +- Fix - Fix setup wizard title and flash of content ([#8201](https://github.com/woocommerce/woocommerce-admin/pull/8201)) +- Fix - Fix too many pending run_remote_notifications actions. ([#8285](https://github.com/woocommerce/woocommerce-admin/pull/8285)) +- Fix - Fix view logic for Setup additional payment providers task. ([#8391](https://github.com/woocommerce/woocommerce-admin/pull/8391)) +- Fix - OBW: fix copy on Business Details when "WooCommerce Shipping" is not listed ([#8324](https://github.com/woocommerce/woocommerce-admin/pull/8324)) +- Fix - Only add product data on REST requests and task list ([#8235](https://github.com/woocommerce/woocommerce-admin/pull/8235)) +- Fix - Stop showing actioned inbox items ([#8394](https://github.com/woocommerce/woocommerce-admin/pull/8394)) +- Fix - WC Payments task is not visible after installing the plugin ([#8514](https://github.com/woocommerce/woocommerce-admin/pull/8514)) +- Fix - PHP warning when default param is missing in payments spec. ([#8519](https://github.com/woocommerce/woocommerce-admin/pull/8519)) +- Dev - Added a test for tracks event recording for PaymentGatewaySuggestions ([#8306](https://github.com/woocommerce/woocommerce-admin/pull/8306)) +- Dev - Add README to hook reference generation script ([#8004](https://github.com/woocommerce/woocommerce-admin/pull/8004)) +- Dev - Add reset WooCommerce functionality to E2E tests, so tests have a fresh state. ([#8219](https://github.com/woocommerce/woocommerce-admin/pull/8219)) +- Dev - Enabled optional typescript checking on ./client subfolder ([#8372](https://github.com/woocommerce/woocommerce-admin/pull/8372)) +- Dev - Fix formatting and add filter param for changelog types for the testing instructions script. ([#8256](https://github.com/woocommerce/woocommerce-admin/pull/8256)) +- Dev - Refactor MerchantEmailNotifications ([#8304](https://github.com/woocommerce/woocommerce-admin/pull/8304)) +- Dev - Remove preloaded countries from data endpoints and use data store instead. ([#8380](https://github.com/woocommerce/woocommerce-admin/pull/8380)) +- Dev - Remove unused pre loaded setting data displaying all the routes. ([#8379](https://github.com/woocommerce/woocommerce-admin/pull/8379)) +- Dev - Remove unused task styling classes ([#8234](https://github.com/woocommerce/woocommerce-admin/pull/8234)) +- Dev - Update dependencies to support react 17 and drop support for IE11. ([#8305](https://github.com/woocommerce/woocommerce-admin/pull/8305)) +- Dev - Update task list data structure to better handle new designs. ([#8332](https://github.com/woocommerce/woocommerce-admin/pull/8332)) + +**WooCommerce Blocks - 7.2.0 & 7.2.1** + +- Enhancement - Add Global Styles support to the Product Price block. ([5950](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5950)) +- Enhancement - Add Global Styles support to the Add To Cart Button block. ([5816](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5816)) +- Enhancement - Store API - Introduced `wc/store/v1` namespace. ([5911](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5911)) +- Enhancement - Renamed WooCommerce block templates to more e-commerce related names. ([5935](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5935)) +- Enhancement - Featured Product block: Add the ability to reset to a previously set custom background image. ([5886](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5886)) +- Enhancement - Add a remove image button to the WooCommerce Feature Category block. ([5719](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5719)) +- Enhancement - Add support for the global style for the On-Sale Badge block. ([5565](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5565)) +- Enhancement - Add support for the global style for the Attribute Filter block. ([5557](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5557)) +- Enhancement - Category List block: Add support for global style. ([5516](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5516)) +- Fix - Fixed typo in `wooocommerce_store_api_validate_add_to_cart` and `wooocommerce_store_api_validate_cart_item` hook names. ([5926](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5926)) +- Fix - Fix loading WC core translations in locales where WC Blocks is not localized for some strings. ([5910](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5910)) +- Fix - Fixed an issue where clear customizations functionality was not working for WooCommerce templates. ([5746](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5746)) +- Fix - Fixed hover and focus states for button components. ([5712](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5712)) +- Fix - Add to Cart button on Products listing blocks will respect the "Redirect to the cart page after successful addition" setting. ([5708](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5708)) +- Fix - Fixes Twenty Twenty Two issues with sales price and added to cart "View Cart" call out styling in the "Products by Category" block. ([5684](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5684)) +- Fix - StoreAPI: Clear all wc notice types in the cart validation context [#5983](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5983) +- Fix - Don't trigger class deprecations notices if headers are already sent [#6074](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/6074) +- Various - Remove v1 string from Store Keys. ([5987](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5987)) +- Various - Introduce the `InvalidCartException` for handling cart validation. ([5904](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5904)) +- Various - Renamed Store API custom headers to remove `X-WC-Store-API` prefixes. [#5983](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5983) +- Various - Normalised Store API error codes [#5992](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5992) +- Various - Deprecated `woocommerce_blocks_checkout_order_processed` in favour of `woocommerce_store_api_checkout_order_processed` +- Various - Deprecated `woocommerce_blocks_checkout_update_order_meta` in favour of `woocommerce_store_api_checkout_update_order_meta` +- Various - Deprecated `woocommerce_blocks_checkout_update_order_from_request` in favour of `woocommerce_store_api_checkout_update_order_from_request` + = 6.3.1 2022-03-10 = **WooCommerce** diff --git a/package.json b/package.json index 3f7f9789ea7..2ff6a3d8460 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "url": "https://github.com/woocommerce/woocommerce/issues" }, "scripts": { - "preinstall": "npx only-allow pnpm" + "preinstall": "npx only-allow pnpm", + "postinstall": "pnpm git:update-hooks", + "git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && husky install" }, "devDependencies": { "@automattic/nx-composer": "^0.1.0", @@ -30,7 +32,9 @@ "@wordpress/prettier-config": "^1.1.1", "chalk": "^4.1.2", "glob": "^7.2.0", + "husky": "^7.0.4", "jest": "^27.3.1", + "lint-staged": "^12.3.7", "mkdirp": "^1.0.4", "node-stream-zip": "^1.15.0", "prettier": "npm:wp-prettier@^2.2.1-beta-1", diff --git a/packages/js/README.md b/packages/js/README.md index 25ca7a90007..5f82f22a152 100644 --- a/packages/js/README.md +++ b/packages/js/README.md @@ -36,13 +36,13 @@ To create a new package, add a new folder to `/packages`, containingā€¦ "author": "Automattic", "license": "GPL-2.0-or-later", "keywords": [ "wordpress", "woocommerce" ], - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/[_YOUR_PACKAGE_]/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/main/packages/[_YOUR_PACKAGE_]/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git" + "url": "https://github.com/woocommerce/woocommerce.git" }, "bugs": { - "url": "https://github.com/woocommerce/woocommerce-admin/issues" + "url": "https://github.com/woocommerce/woocommerce/issues" }, "main": "build/index.js", "module": "build-module/index.js", diff --git a/packages/js/admin-e2e-tests/CHANGELOG.md b/packages/js/admin-e2e-tests/CHANGELOG.md index d764841fc75..5836c843674 100644 --- a/packages/js/admin-e2e-tests/CHANGELOG.md +++ b/packages/js/admin-e2e-tests/CHANGELOG.md @@ -1,5 +1,11 @@ # Unreleased +- Add E2E tests to disabled welcome modal #32505 + +- Update test for payment task. #32467 + +- Increase timeout threshold for payment task. #32605 + # 1.0.0 - Add returned type annotations and remove unused vars. #8020 diff --git a/packages/js/admin-e2e-tests/package.json b/packages/js/admin-e2e-tests/package.json index be6a8efe6d2..38c82638c0a 100644 --- a/packages/js/admin-e2e-tests/package.json +++ b/packages/js/admin-e2e-tests/package.json @@ -3,10 +3,10 @@ "version": "1.0.0", "author": "Automattic", "description": "E2E tests for the new WooCommerce interface.", - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/admin-e2e-tests/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/admin-e2e-tests/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git" + "url": "https://github.com/woocommerce/woocommerce.git" }, "keywords": [ "woocommerce", @@ -56,5 +56,10 @@ "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "lint": "eslint src", "prepack": "pnpm run clean && pnpm run build" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts b/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts index 5224a8825a6..ea711a63e85 100644 --- a/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts +++ b/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts @@ -21,8 +21,20 @@ export class PaymentsSetup extends BasePage { await waitForElementByText( 'h1', 'Set up payments' ); } - async closeHelpModal(): Promise< void > { - await this.clickButtonWithText( 'Got it' ); + async possiblyCloseHelpModal(): Promise< void > { + try { + await this.clickButtonWithText( 'Got it' ); + } catch ( e ) {} + } + + async showOtherPaymentMethods(): Promise< void > { + const selector = '.woocommerce-task-payments button.toggle-button'; + await this.page.waitForSelector( selector ); + const toggleButton = await this.page.$( + `${ selector }[aria-expanded=false]` + ); + await toggleButton?.click(); + await waitForElementByText( 'h2', 'Offline payment methods' ); } async goToPaymentMethodSetup( @@ -41,14 +53,6 @@ export class PaymentsSetup extends BasePage { } } - async methodHasBeenSetup( method: PaymentMethod ): Promise< void > { - const selector = `.woocommerce-task-payment-${ method }`; - await this.page.waitForSelector( selector ); - expect( - await getElementByText( '*', 'Manage', selector ) - ).toBeDefined(); - } - async enableCashOnDelivery(): Promise< void > { await this.page.waitForSelector( '.woocommerce-task-payment-cod' ); await this.clickButtonWithText( 'Enable' ); diff --git a/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts b/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts index a79b6852e19..d1c6c90e06a 100644 --- a/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts +++ b/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts @@ -24,12 +24,7 @@ export class WcHomescreen extends BasePage { } async possiblyDismissWelcomeModal(): Promise< void > { - const modalText = 'Welcome to your WooCommerce storeā€™s online HQ!'; - const modal = await waitForElementByTextWithoutThrow( - 'h2', - modalText, - 10 - ); + const modal = await this.isWelcomeModalVisible(); if ( modal ) { await this.clickButtonWithText( 'Next' ); @@ -41,6 +36,16 @@ export class WcHomescreen extends BasePage { } } + async isWelcomeModalVisible(): Promise< boolean > { + const modalText = 'Welcome to your WooCommerce storeā€™s online HQ!'; + const modal = await waitForElementByTextWithoutThrow( + 'h2', + modalText, + 10 + ); + return modal; + } + async getTaskList(): Promise< Array< string | null > > { await page.waitForSelector( '.woocommerce-task-card .woocommerce-task-list__item-title' diff --git a/packages/js/admin-e2e-tests/src/pages/WcSettings.ts b/packages/js/admin-e2e-tests/src/pages/WcSettings.ts index ebf95a46033..0b5f78cf0a7 100644 --- a/packages/js/admin-e2e-tests/src/pages/WcSettings.ts +++ b/packages/js/admin-e2e-tests/src/pages/WcSettings.ts @@ -42,8 +42,22 @@ export class WcSettings extends BasePage { ); } + async paymentMethodIsEnabled( method = '' ): Promise< boolean > { + await this.navigate( 'checkout' ); + await waitForElementByText( 'h2', 'Payment methods' ); + const className = await getAttribute( + `tr[data-gateway_id=${ method }] .woocommerce-input-toggle`, + 'className' + ); + return ( + ( className as string ).indexOf( + 'woocommerce-input-toggle--disabled' + ) === -1 + ); + } + async cleanPaymentMethods(): Promise< void > { - this.navigate( 'checkout' ); + await this.navigate( 'checkout' ); await waitForElementByText( 'h2', 'Payment methods' ); const paymentMethods = await page.$$( 'span.woocommerce-input-toggle' ); for ( const method of paymentMethods ) { diff --git a/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts b/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts index f0772672b86..717cacf4186 100644 --- a/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts +++ b/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts @@ -597,10 +597,37 @@ const testBusinessDetailsForm = () => { } ); }; +const testAdminHomescreen = () => { + describe( 'Homescreen', () => { + const profileWizard = new OnboardingWizard( page ); + const homeScreen = new WcHomescreen( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + await resetWooCommerceState(); + await profileWizard.navigate(); + await profileWizard.skipStoreSetup(); + } ); + + afterAll( async () => { + await login.logout(); + } ); + + it( 'should not show welcome modal', async () => { + await homeScreen.isDisplayed(); + await expect( homeScreen.isWelcomeModalVisible() ).resolves.toBe( + false + ); + } ); + } ); +}; + module.exports = { testAdminOnboardingWizard, testSelectiveBundleWCPay, testDifferentStoreCurrenciesWCPay, testSubscriptionsInclusion, testBusinessDetailsForm, + testAdminHomescreen, }; diff --git a/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts b/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts index 0515dce47f3..1571fea5fcc 100644 --- a/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts +++ b/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts @@ -48,11 +48,13 @@ const testAdminPaymentSetupTask = () => { it( 'Can visit the payment setup task from the homescreen if the setup wizard has been skipped', async () => { await homeScreen.clickOnTaskList( 'Set up payments' ); - await paymentsSetup.closeHelpModal(); + await paymentsSetup.possiblyCloseHelpModal(); await paymentsSetup.isDisplayed(); } ); - it( 'Saving valid bank account transfer details enables the payment method', async () => { + it.skip( 'Saving valid bank account transfer details enables the payment method', async () => { + await paymentsSetup.showOtherPaymentMethods(); + await waitForTimeout( 500 ); await paymentsSetup.goToPaymentMethodSetup( 'bacs' ); await bankTransferSetup.saveAccountDetails( { accountNumber: '1234', @@ -62,27 +64,28 @@ const testAdminPaymentSetupTask = () => { iban: '12 3456 7890', swiftCode: 'ABBA', } ); - - await homeScreen.isDisplayed(); - await waitForTimeout( 1000 ); - await homeScreen.clickOnTaskList( 'Set up payments' ); - await paymentsSetup.isDisplayed(); - await paymentsSetup.methodHasBeenSetup( 'bacs' ); + await waitForTimeout( 1500 ); + expect( await settings.paymentMethodIsEnabled( 'bacs' ) ).toBe( + true + ); + await homeScreen.navigate(); } ); - it( 'Enabling cash on delivery enables the payment method', async () => { + it.skip( 'Enabling cash on delivery enables the payment method', async () => { await settings.cleanPaymentMethods(); await homeScreen.navigate(); await homeScreen.isDisplayed(); await waitForTimeout( 1000 ); await homeScreen.clickOnTaskList( 'Set up payments' ); - await paymentsSetup.enableCashOnDelivery(); - await homeScreen.navigate(); - await homeScreen.isDisplayed(); - await waitForTimeout( 1000 ); - await homeScreen.clickOnTaskList( 'Set up payments' ); + await paymentsSetup.possiblyCloseHelpModal(); await paymentsSetup.isDisplayed(); - await paymentsSetup.methodHasBeenSetup( 'cod' ); + await paymentsSetup.showOtherPaymentMethods(); + await waitForTimeout( 500 ); + await paymentsSetup.enableCashOnDelivery(); + await waitForTimeout( 1500 ); + expect( await settings.paymentMethodIsEnabled( 'cod' ) ).toBe( + true + ); } ); } ); }; diff --git a/packages/js/api-core-tests/package.json b/packages/js/api-core-tests/package.json index 4c4bf1fa0d3..f83420e77d3 100644 --- a/packages/js/api-core-tests/package.json +++ b/packages/js/api-core-tests/package.json @@ -33,5 +33,10 @@ }, "bin": { "wc-api-tests": "bin/wc-api-tests.sh" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/api/package.json b/packages/js/api/package.json index af4790ba749..913125e6bcd 100644 --- a/packages/js/api/package.json +++ b/packages/js/api/package.json @@ -52,5 +52,10 @@ }, "publishConfig": { "access": "public" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/components/CHANGELOG.md b/packages/js/components/CHANGELOG.md index 798bde85839..e7782c11e57 100644 --- a/packages/js/components/CHANGELOG.md +++ b/packages/js/components/CHANGELOG.md @@ -1,5 +1,10 @@ # Unreleased +- Fix documentation for `TableCard` component +- Update dependency `@wordpress/hooks` to ^3.5.0 +- Update dependency `@wordpress/icons` to ^8.1.0 +- Add `className` prop for Pill component. #32605 + # 10.0.0 - Replace deprecated wp.compose.withState with wp.element.useState. #8338 - Add missing dependencies. #8349 diff --git a/packages/js/components/package.json b/packages/js/components/package.json index b9ec712baa5..646fa4ffec3 100644 --- a/packages/js/components/package.json +++ b/packages/js/components/package.json @@ -9,13 +9,13 @@ "woocommerce", "components" ], - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/components/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/components/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git" + "url": "https://github.com/woocommerce/woocommerce.git" }, "bugs": { - "url": "https://github.com/woocommerce/woocommerce-admin/issues" + "url": "https://github.com/woocommerce/woocommerce/issues" }, "main": "build/index.js", "module": "build-module/index.js", @@ -39,10 +39,10 @@ "@wordpress/deprecated": "^3.3.1", "@wordpress/dom": "^3.3.2", "@wordpress/element": "^4.1.1", - "@wordpress/hooks": "^2.12.3", + "@wordpress/hooks": "^3.5.0", "@wordpress/html-entities": "^3.3.1", "@wordpress/i18n": "^4.3.1", - "@wordpress/icons": "^6.3.0", + "@wordpress/icons": "^8.1.0", "@wordpress/keycodes": "^3.3.1", "@wordpress/url": "^3.4.1", "@wordpress/viewport": "^4.1.2", @@ -123,5 +123,11 @@ "test:nobuild": "jest --config ./jest.config.json", "test:update-snapshots": "pnpm run test:nobuild -- --updateSnapshot", "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/components/src/advanced-filters/README.md b/packages/js/components/src/advanced-filters/README.md index e9cbc16bd8c..72da82a2994 100644 --- a/packages/js/components/src/advanced-filters/README.md +++ b/packages/js/components/src/advanced-filters/README.md @@ -124,7 +124,7 @@ const config = { }; ``` -`type`: A string Autocompleter type used by the [Search Component](https://github.com/woocommerce/woocommerce-admin/tree/main/packages/components/src/search). +`type`: A string Autocompleter type used by the [Search Component](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/components/src/search). `getLabels`: A function returning a Promise resolving to an array of objects with `id` and `label` properties. ### Date diff --git a/packages/js/components/src/pill/pill.js b/packages/js/components/src/pill/pill.js index 662bb11fa45..33f91cf9bdf 100644 --- a/packages/js/components/src/pill/pill.js +++ b/packages/js/components/src/pill/pill.js @@ -2,16 +2,17 @@ * External dependencies */ import { createElement } from '@wordpress/element'; +import classnames from 'classnames'; /** * Internal dependencies */ import { Text } from '../experimental'; -export function Pill( { children } ) { +export function Pill( { children, className } ) { return ( + ! task.isDismissed && + ( ! task.isSnoozed || task.snoozedUntil < nowTimestamp ) + ); +} diff --git a/packages/js/data/src/plugins/constants.ts b/packages/js/data/src/plugins/constants.ts index 1050edb792c..50fbb1985f2 100644 --- a/packages/js/data/src/plugins/constants.ts +++ b/packages/js/data/src/plugins/constants.ts @@ -55,4 +55,8 @@ export const pluginNames = { 'google-listings-and-ads': __( 'Google Listings and Ads', 'woocommerce' ), 'woo-razorpay': __( 'Razorpay', 'woocommerce' ), mailpoet: __( 'MailPoet', 'woocommerce' ), + 'pinterest-for-woocommerce': __( + 'Pinterest for WooCommerce', + 'woocommerce' + ), }; diff --git a/packages/js/data/src/plugins/types.ts b/packages/js/data/src/plugins/types.ts index 0988f81972a..54a0299ba39 100644 --- a/packages/js/data/src/plugins/types.ts +++ b/packages/js/data/src/plugins/types.ts @@ -39,6 +39,7 @@ export type Plugin = { recommendation_priority?: number; is_visible?: boolean; is_local_partner?: boolean; + is_offline?: boolean; }; type PaypalOnboardingState = 'unknown' | 'start' | 'progressive' | 'onboarded'; diff --git a/packages/js/date/package.json b/packages/js/date/package.json index eb6240d2f7c..743f7d34905 100644 --- a/packages/js/date/package.json +++ b/packages/js/date/package.json @@ -9,13 +9,13 @@ "woocommerce", "date" ], - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/date/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/date/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git" + "url": "https://github.com/woocommerce/woocommerce.git" }, "bugs": { - "url": "https://github.com/woocommerce/woocommerce-admin/issues" + "url": "https://github.com/woocommerce/woocommerce/issues" }, "main": "build/index.js", "module": "build-module/index.js", @@ -52,5 +52,11 @@ "test": "pnpm run build && pnpm run test:nobuild", "test:nobuild": "jest --config ./jest.config.json", "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/dependency-extraction-webpack-plugin/package.json b/packages/js/dependency-extraction-webpack-plugin/package.json index eccc6705f2b..fe6cdd0cacf 100644 --- a/packages/js/dependency-extraction-webpack-plugin/package.json +++ b/packages/js/dependency-extraction-webpack-plugin/package.json @@ -8,13 +8,13 @@ "wordpress", "woocommerce" ], - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/dependency-extraction-webpack-plugin/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/dependency-extraction-webpack-plugin/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git" + "url": "https://github.com/woocommerce/woocommerce.git" }, "bugs": { - "url": "https://github.com/woocommerce/woocommerce-admin/issues" + "url": "https://github.com/woocommerce/woocommerce/issues" }, "main": "src/index.js", "dependencies": { @@ -34,5 +34,10 @@ "typescript": "^4.6.2", "webpack": "^5.70.0", "webpack-cli": "^3.3.12" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/e2e-core-tests/package.json b/packages/js/e2e-core-tests/package.json index bbc55e8eadc..475cb1aa8fe 100644 --- a/packages/js/e2e-core-tests/package.json +++ b/packages/js/e2e-core-tests/package.json @@ -48,5 +48,10 @@ "clean": "rm -rf ./build ./build-module", "compile": "node ./../bin/build.js", "build": "./bin/build.sh && pnpm run clean && pnpm run compile" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/e2e-environment/package.json b/packages/js/e2e-environment/package.json index 07de1318f83..f30f3a535c2 100644 --- a/packages/js/e2e-environment/package.json +++ b/packages/js/e2e-environment/package.json @@ -77,5 +77,10 @@ }, "bin": { "wc-e2e": "bin/wc-e2e.sh" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/e2e-utils/package.json b/packages/js/e2e-utils/package.json index 867a4bab905..f83dd1c01cb 100644 --- a/packages/js/e2e-utils/package.json +++ b/packages/js/e2e-utils/package.json @@ -45,5 +45,10 @@ "build": "pnpm run clean && pnpm run compile", "prepare": "pnpm run build", "lint": "eslint src" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/eslint-plugin/package.json b/packages/js/eslint-plugin/package.json index 787d6b7011d..df23e6f4649 100644 --- a/packages/js/eslint-plugin/package.json +++ b/packages/js/eslint-plugin/package.json @@ -10,14 +10,14 @@ "eslint", "plugin" ], - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/eslint-plugin/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/eslint-plugin/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git", + "url": "https://github.com/woocommerce/woocommerce.git", "directory": "packages/eslint-plugin" }, "bugs": { - "url": "https://github.com/woocommerce/woocommerce-admin/issues" + "url": "https://github.com/woocommerce/woocommerce/issues" }, "files": [ "configs", diff --git a/packages/js/experimental/CHANGELOG.md b/packages/js/experimental/CHANGELOG.md index 1e9c72b068d..c43445f78c6 100644 --- a/packages/js/experimental/CHANGELOG.md +++ b/packages/js/experimental/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Update dependency `@wordpress/icons` to ^8.1.0 + # 3.0.1 - Update all js packages with minor/patch version changes. #8392 diff --git a/packages/js/experimental/package.json b/packages/js/experimental/package.json index 8a7530f6085..e78452c4d6a 100644 --- a/packages/js/experimental/package.json +++ b/packages/js/experimental/package.json @@ -9,13 +9,13 @@ "woocommerce", "experimental" ], - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/experimental/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/experimental/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git" + "url": "https://github.com/woocommerce/woocommerce.git" }, "bugs": { - "url": "https://github.com/woocommerce/woocommerce-admin/issues" + "url": "https://github.com/woocommerce/woocommerce/issues" }, "main": "build/index.js", "module": "build-module/index.js", @@ -29,7 +29,7 @@ "@wordpress/components": "^19.5.0", "@wordpress/element": "^4.1.1", "@wordpress/i18n": "^4.3.1", - "@wordpress/icons": "^6.3.0", + "@wordpress/icons": "^8.1.0", "@wordpress/keycodes": "^3.3.1", "classnames": "^2.3.1", "dompurify": "^2.3.6", @@ -83,5 +83,11 @@ "test": "pnpm run build && pnpm run test:nobuild", "test:nobuild": "jest --config ./jest.config.json", "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/experimental/src/experimental-list/task-item/index.tsx b/packages/js/experimental/src/experimental-list/task-item/index.tsx index 8aa2864d296..59b8ecd2ffd 100644 --- a/packages/js/experimental/src/experimental-list/task-item/index.tsx +++ b/packages/js/experimental/src/experimental-list/task-item/index.tsx @@ -130,7 +130,7 @@ export const TaskItem: React.FC< TaskItemProps > = ( { }, [ expanded ] ); const className = classnames( 'woocommerce-task-list__item', { - complete: completed, + 'is-complete': completed, expanded: isTaskExpanded, 'level-2': level === 2 && ! completed, 'level-1': level === 1 && ! completed, diff --git a/packages/js/experimental/src/experimental-list/task-item/style.scss b/packages/js/experimental/src/experimental-list/task-item/style.scss index f7fb5eb81df..7a95dc775f0 100644 --- a/packages/js/experimental/src/experimental-list/task-item/style.scss +++ b/packages/js/experimental/src/experimental-list/task-item/style.scss @@ -121,7 +121,7 @@ $task-alert-yellow: #f0b849; left: 5px; } - &.complete { + &.is-complete { .woocommerce-task__icon { background-color: var(--wp-admin-theme-color); } @@ -136,7 +136,7 @@ $task-alert-yellow: #f0b849; } } - &:not(.complete) { + &:not(.is-complete) { .woocommerce-task__icon { border: 1px solid $gray-100; background: $white; diff --git a/packages/js/explat/CHANGELOG.md b/packages/js/explat/CHANGELOG.md index 9a709dc19f6..1c2faa25278 100644 --- a/packages/js/explat/CHANGELOG.md +++ b/packages/js/explat/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Update dependency `@wordpress/hooks` to ^3.5.0 + # 2.1.0 - Add missing dependencies. #8349 diff --git a/packages/js/explat/package.json b/packages/js/explat/package.json index 533172d328f..f6c3d728f8e 100644 --- a/packages/js/explat/package.json +++ b/packages/js/explat/package.json @@ -10,13 +10,13 @@ "abtest", "explat" ], - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/explat/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/explat/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git" + "url": "https://github.com/woocommerce/woocommerce.git" }, "bugs": { - "url": "https://github.com/woocommerce/woocommerce-admin/issues" + "url": "https://github.com/woocommerce/woocommerce/issues" }, "main": "build/index.js", "module": "build-module/index.js", @@ -28,7 +28,7 @@ "@automattic/explat-client": "^0.0.3", "@automattic/explat-client-react-helpers": "^0.0.4", "@wordpress/api-fetch": "^6.0.1", - "@wordpress/hooks": "^2.12.3", + "@wordpress/hooks": "^3.5.0", "cookie": "^0.4.2", "qs": "^6.10.3" }, @@ -54,5 +54,11 @@ "test": "pnpm run build && pnpm run test:nobuild", "test:nobuild": "jest --config ./jest.config.json", "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/js-tests/package.json b/packages/js/js-tests/package.json index c6de210f262..ad6cfa050a5 100644 --- a/packages/js/js-tests/package.json +++ b/packages/js/js-tests/package.json @@ -4,14 +4,14 @@ "description": "JavaScript test tooling.", "author": "Automattic", "license": "GPL-2.0-or-later", - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/js-tests/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/js-tests/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git", + "url": "https://github.com/woocommerce/woocommerce.git", "directory": "packages/js-tests" }, "bugs": { - "url": "https://github.com/woocommerce/woocommerce-admin/issues" + "url": "https://github.com/woocommerce/woocommerce/issues" }, "private": true, "main": "build/util/index.js", @@ -40,5 +40,10 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "typescript": "^4.6.2" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/js-tests/src/setup-globals.js b/packages/js/js-tests/src/setup-globals.js index 201fc59265f..b95d9e74d2c 100644 --- a/packages/js/js-tests/src/setup-globals.js +++ b/packages/js/js-tests/src/setup-globals.js @@ -107,7 +107,7 @@ wooCommercePackages.forEach( ( lib ) => { } ); } ); -const config = require( '../../../../plugins/woocommerce-admin/config/development.json' ); +const config = require( '../../../../plugins/woocommerce/client/admin/config/development.json' ); // Check if test is jsdom or node if ( global.window ) { diff --git a/packages/js/navigation/CHANGELOG.md b/packages/js/navigation/CHANGELOG.md index b90c2da966b..81fd1afc1cd 100644 --- a/packages/js/navigation/CHANGELOG.md +++ b/packages/js/navigation/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Update dependency `@wordpress/hooks` to ^3.5.0 + # 7.0.1 - Add missing dependencies. #8349 diff --git a/packages/js/navigation/package.json b/packages/js/navigation/package.json index c4b578c2cc6..6c4e5f064c8 100644 --- a/packages/js/navigation/package.json +++ b/packages/js/navigation/package.json @@ -9,13 +9,13 @@ "woocommerce", "navigation" ], - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/navigation/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/navigation/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git" + "url": "https://github.com/woocommerce/woocommerce.git" }, "bugs": { - "url": "https://github.com/woocommerce/woocommerce-admin/issues" + "url": "https://github.com/woocommerce/woocommerce/issues" }, "main": "build/index.js", "module": "build-module/index.js", @@ -25,7 +25,7 @@ "@wordpress/components": "^19.5.0", "@wordpress/compose": "^5.1.2", "@wordpress/element": "^4.1.1", - "@wordpress/hooks": "^2.12.3", + "@wordpress/hooks": "^3.5.0", "@wordpress/notices": "^3.3.2", "@wordpress/url": "^3.4.1", "history": "^4.10.1", @@ -57,5 +57,11 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "typescript": "^4.6.2" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/notices/CHANGELOG.md b/packages/js/notices/CHANGELOG.md index c738abfb1d1..e0a22a3fc5c 100644 --- a/packages/js/notices/CHANGELOG.md +++ b/packages/js/notices/CHANGELOG.md @@ -2,6 +2,8 @@ # Unreleased +- Update dependency `@wordpress/a11y` to ^3.5.0 + # 4.0.1 - Update all js packages with minor/patch version changes. #8392 diff --git a/packages/js/notices/package.json b/packages/js/notices/package.json index ca89ed63893..7de5f5eb01c 100644 --- a/packages/js/notices/package.json +++ b/packages/js/notices/package.json @@ -22,7 +22,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@wordpress/a11y": "^2.15.3", + "@wordpress/a11y": "^3.5.0", "@wordpress/data": "^6.3.0", "@wordpress/notices": "^3.3.2" }, @@ -51,5 +51,10 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "typescript": "^4.6.2" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/number/package.json b/packages/js/number/package.json index d7daf0f01d0..1abd24f9f50 100644 --- a/packages/js/number/package.json +++ b/packages/js/number/package.json @@ -8,13 +8,13 @@ "wordpress", "woocommerce" ], - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/number/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/number/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git" + "url": "https://github.com/woocommerce/woocommerce.git" }, "bugs": { - "url": "https://github.com/woocommerce/woocommerce-admin/issues" + "url": "https://github.com/woocommerce/woocommerce/issues" }, "main": "build/index.js", "module": "build-module/index.js", @@ -45,5 +45,11 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "typescript": "^4.6.2" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/onboarding/CHANGELOG.md b/packages/js/onboarding/CHANGELOG.md index e6cb194609c..5054025a93e 100644 --- a/packages/js/onboarding/CHANGELOG.md +++ b/packages/js/onboarding/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Update TaskList types. + # 3.0.1 - Add missing dependency. diff --git a/packages/js/onboarding/package.json b/packages/js/onboarding/package.json index 8ee2642b336..07bfb1e640d 100644 --- a/packages/js/onboarding/package.json +++ b/packages/js/onboarding/package.json @@ -9,13 +9,13 @@ "woocommerce", "onboarding" ], - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/onboarding/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/onboarding/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git" + "url": "https://github.com/woocommerce/woocommerce.git" }, "bugs": { - "url": "https://github.com/woocommerce/woocommerce-admin/issues" + "url": "https://github.com/woocommerce/woocommerce/issues" }, "main": "build/index.js", "module": "build-module/index.js", @@ -59,5 +59,10 @@ "start": "concurrently \"tsc --build --watch\" \"webpack --watch\"", "prepack": "pnpm run clean && pnpm run build", "lint": "eslint src" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/style-build/package.json b/packages/js/style-build/package.json index 96926105d6c..0e2b9db68ec 100644 --- a/packages/js/style-build/package.json +++ b/packages/js/style-build/package.json @@ -8,18 +8,18 @@ "wordpress", "woocommerce" ], - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/style-build/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/style-build/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git" + "url": "https://github.com/woocommerce/woocommerce.git" }, "bugs": { - "url": "https://github.com/woocommerce/woocommerce-admin/issues" + "url": "https://github.com/woocommerce/woocommerce/issues" }, "main": "index.js", "dependencies": { "@automattic/color-studio": "^2.5.0", - "@wordpress/base-styles": "^3.6.0", + "@wordpress/base-styles": "^4.3.0", "@wordpress/postcss-plugins-preset": "^1.6.0", "css-loader": "^3.6.0", "mini-css-extract-plugin": "^2.6.0", @@ -42,5 +42,10 @@ "ts-jest": "^27.1.3", "typescript": "^4.6.2", "webpack": "^5.70.0" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/tracks/package.json b/packages/js/tracks/package.json index 314b4c9abe5..fe85d715a72 100644 --- a/packages/js/tracks/package.json +++ b/packages/js/tracks/package.json @@ -9,13 +9,13 @@ "woocommerce", "tracks" ], - "homepage": "https://github.com/woocommerce/woocommerce-admin/tree/main/packages/tracks/README.md", + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/tracks/README.md", "repository": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce-admin.git" + "url": "https://github.com/woocommerce/woocommerce.git" }, "bugs": { - "url": "https://github.com/woocommerce/woocommerce-admin/issues" + "url": "https://github.com/woocommerce/woocommerce/issues" }, "main": "build/index.js", "module": "build-module/index.js", @@ -42,5 +42,10 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "typescript": "^4.6.2" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/phpcs.xml b/phpcs.xml index 525a3e938d6..70bf2d649ca 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -27,7 +27,7 @@ - + @@ -128,4 +128,16 @@ src/Internal/Admin/ src/Admin/ + + + + src/Internal/Admin/ + src/Admin/ + + + + + src/Internal/Admin/ + src/Admin/ + diff --git a/plugins/woocommerce-admin/.github/workflows/daily-e2e.yml b/plugins/woocommerce-admin/.github/workflows/daily-e2e.yml deleted file mode 100644 index 5977439770c..00000000000 --- a/plugins/woocommerce-admin/.github/workflows/daily-e2e.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: 'Daily E2E Tests' - -on: - schedule: - - cron: '0 0 * * *' - -jobs: - e2e-tests: - runs-on: ubuntu-18.04 - strategy: - fail-fast: false - matrix: - wordpress: ['https://wordpress.org/latest.zip', 'https://wordpress.org/nightly-builds/wordpress-latest.zip'] - woocommerce: ['https://downloads.wordpress.org/plugin/woocommerce.zip', 'https://github.com/woocommerce/woocommerce/releases/download/nightly/woocommerce-trunk-nightly.zip'] - exclude: - - {'wordpress': 'https://wordpress.org/nightly-builds/wordpress-latest.zip', 'woocommerce': 'https://github.com/woocommerce/woocommerce/releases/download/nightly/woocommerce-trunk-nightly.zip'} - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - name: Install PHP dependencies - run: | - composer install --no-dev - - name: Setup Node.js - uses: actions/setup-node@v2-beta - with: - node-version: '14' - - name: Install PNPM and install dependencies - uses: pnpm/action-setup@v2.2.1 - with: - version: ^6.24.2 - run_install: true - - name: Build - run: | - composer require wp-cli/i18n-command - pnpm run build:feature-config - pnpm run build - - name: Setup wp-env - env: - WP_ENV_CONFIG: '{ core: "${{ matrix.wordpress }}", plugins: [ ".", "${{ matrix.woocommerce }}" ] }' - run: | - pnpm -g i @wordpress/env - printf '%s\n' "$WP_ENV_CONFIG" > .wp-env-override.json - WP_ENV_TESTS_PORT=8084 wp-env start - wp-env run tests-cli "wp post create --post_type=page --post_status=publish --post_title='Ready' --post_content='E2E-tests.'" - - name: Test - env: - WC_E2E_SCREENSHOTS: 1 - run: | - pnpm exec wc-e2e test:e2e - - name: Archive e2e test screenshots - if: ${{ always() }} - uses: actions/upload-artifact@v2 - with: - name: e2e-screenshots - path: tests/e2e/screenshots - if-no-files-found: ignore - retention-days: 5 diff --git a/plugins/woocommerce-admin/.github/workflows/daily-php.yml b/plugins/woocommerce-admin/.github/workflows/daily-php.yml deleted file mode 100644 index 766333cd5d9..00000000000 --- a/plugins/woocommerce-admin/.github/workflows/daily-php.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: 'Daily PHP Tests' - -on: - schedule: - - cron: '0 0 * * *' - -jobs: - daily-test-php: - name: "Test PHP" - runs-on: ubuntu-18.04 - strategy: - fail-fast: false - matrix: - wordpress: ['latest', 'nightly'] - woocommerce: ['latest', 'nightly'] - exclude: - - {'wordpress': 'nightly', 'woocommerce': 'nightly'} - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - name: Setup Node.js - uses: actions/setup-node@v2-beta - with: - node-version: '14' - - name: Install PNPM and install dependencies - uses: pnpm/action-setup@v2.2.1 - with: - version: ^6.24.2 - run_install: true - - name: Build - run: | - pnpm run build:feature-config - composer install --no-dev - shell: bash - - name: Run the PHP unit tests - env: - WP_VERSION: ${{ matrix.wordpress }} - WC_VERSION: ${{ matrix.woocommerce }} - run: pnpm run test:php - shell: bash diff --git a/plugins/woocommerce-admin/.github/workflows/e2e.yml b/plugins/woocommerce-admin/.github/workflows/e2e.yml deleted file mode 100644 index f6d0aec120f..00000000000 --- a/plugins/woocommerce-admin/.github/workflows/e2e.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: E2E tests -on: [pull_request] - -jobs: - e2e-tests: - runs-on: ubuntu-18.04 - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.7.0 - with: - access_token: ${{ github.token }} - - name: Check out repository code - uses: actions/checkout@v2 - - name: Setup PHP - uses: shivammathur/setup-php@2.9.0 - with: - php-version: '7.3' - - name: Install PHP dependencies - run: | - composer self-update 2.0.6 - composer i - - name: Setup Node.js - uses: actions/setup-node@v2-beta - with: - node-version: '14' - - name: Install PNPM and install dependencies - uses: pnpm/action-setup@v2.2.1 - with: - version: ^6.24.2 - run_install: true - - name: Build and run E2E Tests - env: - WC_E2E_SCREENSHOTS: 1 - E2E_SLACK_CHANNEL: ${{ secrets.E2E_SLACK_CHANNEL }} - E2E_SLACK_TOKEN: ${{ secrets.E2E_SLACK_TOKEN }} - WP_VERSION: '5.8.0' - run: | - composer require wp-cli/i18n-command - pnpm run build - pnpm run e2e:docker-up - sleep 10 - pnpm exec wc-e2e test:e2e - - name: Archive e2e test screenshots - if: ${{ always() }} - uses: actions/upload-artifact@v2 - with: - name: e2e-screenshots - path: tests/e2e/screenshots - if-no-files-found: ignore - retention-days: 5 diff --git a/plugins/woocommerce-admin/.github/workflows/gh-pages.yml b/plugins/woocommerce-admin/.github/workflows/gh-pages.yml deleted file mode 100644 index 74bc643012d..00000000000 --- a/plugins/woocommerce-admin/.github/workflows/gh-pages.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Publish docs -on: - push: - branches: - - main -jobs: - deploy: - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@v2 - - - name: Setup Node.js - uses: actions/setup-node@v2-beta - with: - node-version: '14' - - - name: Install PNPM and install dependencies - uses: pnpm/action-setup@v2.2.1 - with: - version: ^6.24.2 - run_install: true - - - name: Build - run: | - pnpm run build - pnpm run docs - - - name: Deploy docs - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_branch: gh-pages - publish_dir: ./docs diff --git a/plugins/woocommerce-admin/.github/workflows/lint-php.yml b/plugins/woocommerce-admin/.github/workflows/lint-php.yml deleted file mode 100644 index 456b3ffba26..00000000000 --- a/plugins/woocommerce-admin/.github/workflows/lint-php.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Lint the PHP -on: [pull_request] - -jobs: - lint-php: - runs-on: ubuntu-latest - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.7.0 - with: - access_token: ${{ github.token }} - - name: Check out repository code - uses: actions/checkout@v2 - - name: Determine changed files - id: changed-files - uses: wyrihaximus/github-action-files-in-commit@v1.0 - - name: Setup PHP - uses: shivammathur/setup-php@2.9.0 - with: - php-version: 7.3 - - name: Lint the PHP - env: - CHANGED_FILES: ${{ steps.changed-files.outputs.files }} - run: bin/phpcs.sh - shell: bash diff --git a/plugins/woocommerce-admin/.github/workflows/test-php.yml b/plugins/woocommerce-admin/.github/workflows/test-php.yml deleted file mode 100644 index 4311062ce3a..00000000000 --- a/plugins/woocommerce-admin/.github/workflows/test-php.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Run PHP unit tests -on: [pull_request] - -jobs: - test-php: - env: - WP_CORE_DIR: '/tmp/wordpress' - COMPOSER_DEV: 1 - - runs-on: ubuntu-18.04 - strategy: - fail-fast: false - matrix: - php: ['7.1', '7.2', '7.3'] - wordpress: ['5.4', '5.6'] - woocommerce: ['4.8.0', '4.9.1'] - phpunit: ['7.5.20'] - composer: ['2.0.6'] - include: - - php: '7.0' - wordpress: '5.6' - woocommerce: 'latest' - phpunit: '6.5.9' - composer: '1.10.19' - - php: '7.0' - wordpress: '5.6' - woocommerce: '4.9.1' - phpunit: '6.5.9' - composer: '2.0.6' - - php: '8.0' - wordpress: '5.6' - woocommerce: '5.1.0' - phpunit: '7.5.20' - composer: '2.0.6' - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.7.0 - with: - access_token: ${{ github.token }} - - name: Check out repository code - uses: actions/checkout@v2 - - name: Setup PHP - uses: shivammathur/setup-php@2.9.0 - with: - php-version: ${{matrix.php}} - tools: phpunit:${{matrix.phpunit}} - extensions: mysqli - - name: Setup Node.js - uses: actions/setup-node@v2-beta - with: - node-version: '14' - - name: Install PNPM and install dependencies - uses: pnpm/action-setup@v2.2.1 - with: - version: ^6.24.2 - run_install: true - - name: Set up the tests - env: - WP_VERSION: ${{matrix.wordpress}} - WC_VERSION: ${{matrix.woocommerce}} - PHP_UNIT: ${{matrix.phpunit}} - COMPOSER_VERSION: ${{matrix.composer}} - run: | - sudo /etc/init.d/mysql start - bash bin/ci/gh-install-wp-tests.sh wc_admin_test root 'root' localhost - cd "$WP_CORE_DIR/wp-content/plugins/woocommerce-admin/" - pnpm run build:feature-config - composer install - node --version - pnpm --version - timedatectl - - name: Add PHP8 Compatibility. - run: | - if [ "$(php -r "echo version_compare(PHP_VERSION,'8.0','>=');")" ]; then - cd "$WP_CORE_DIR/wp-content/plugins/woocommerce-admin/" - composer install - curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip - unzip -d /tmp/phpunit-7.5-fork /tmp/phpunit-7.5-fork.zip - composer bin phpunit config --unset platform - composer bin phpunit config repositories.0 '{"type": "path", "url": "/tmp/phpunit-7.5-fork/phpunit-add-compatibility-with-php8-to-phpunit-7", "options": {"symlink": false}}' - composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-reqs - rm -rf ./vendor/phpunit/ - composer dump-autoload - fi - - name: Run the PHP unit tests - run: bin/phpunit.sh - shell: bash diff --git a/plugins/woocommerce-admin/Gruntfile.js b/plugins/woocommerce-admin/Gruntfile.js deleted file mode 100755 index 610e8164a89..00000000000 --- a/plugins/woocommerce-admin/Gruntfile.js +++ /dev/null @@ -1,62 +0,0 @@ -/* eslint-disable */ -module.exports = function( grunt ) { - 'use strict'; - - // Project configuration - grunt.initConfig( { - makepot: { - target: { - options: { - domainPath: '/languages', - exclude: [ '.git/*', 'bin/*', 'node_modules/*', 'tests/*' ], - mainFile: 'woocommerce-admin.php', - potFilename: 'woocommerce-admin.pot', - potHeaders: { - poedit: true, - 'x-poedit-keywordslist': true, - }, - type: 'wp-plugin', - updateTimestamp: true, - }, - }, - }, - - checktextdomain: { - options: { - text_domain: 'woocommerce', - keywords: [ - '__:1,2d', - '_e:1,2d', - '_x:1,2c,3d', - 'esc_html__:1,2d', - 'esc_html_e:1,2d', - 'esc_html_x:1,2c,3d', - 'esc_attr__:1,2d', - 'esc_attr_e:1,2d', - 'esc_attr_x:1,2c,3d', - '_ex:1,2c,3d', - '_n:1,2,4d', - '_nx:1,2,4c,5d', - '_n_noop:1,2,3d', - '_nx_noop:1,2,3c,4d', - ], - }, - files: { - src: [ - '**/*.php', // Include all files/ - '!node_modules/**', // Exclude node_modules/ - '!tests/**', // Exclude tests/ - '!vendor/**', // Exclude vendor/ - '!tmp/**', // Exclude tmp/ - ], - expand: true, - }, - }, - } ); - - // Load NPM tasks to be used here. - grunt.loadNpmTasks( 'grunt-wp-i18n' ); - grunt.loadNpmTasks( 'grunt-checktextdomain' ); - - grunt.util.linefeed = '\n'; -}; diff --git a/plugins/woocommerce-admin/README.md b/plugins/woocommerce-admin/README.md index 59d6f0acc72..8f8020b53a0 100644 --- a/plugins/woocommerce-admin/README.md +++ b/plugins/woocommerce-admin/README.md @@ -1,120 +1,13 @@ # WooCommerce Admin -This is a feature plugin for a modern, javascript-driven WooCommerce Admin experience. - -## Prerequisites - -[WordPress 5.6 or greater](https://wordpress.org/download/) and [WooCommerce 5.7.0 or greater](https://wordpress.org/plugins/woocommerce/) should be installed prior to activating the WooCommerce Admin feature plugin. - -For better debugging, it's also recommended you add `define( 'SCRIPT_DEBUG', true );` to your wp-config. This will load the unminified version of all libraries, and specifically the development build of React. +This is a javascript-driven, React-based admin interface for WooCommerce. ## Development -After cloning the repo, install dependencies: - -- `pnpm install` to install JavaScript dependencies. -- `composer install` to gather PHP dependencies. - -Now you can build the files using one of these commands: - -- `pnpm run build` : Build a production version -- `pnpm run dev` : Build a development version -- `pnpm start` : Build a development version, watch files for changes -- `pnpm run build:release` : Build a WordPress plugin ZIP file (`woocommerce-admin.zip` will be created in the repository root) -- `DRY_RUN=1 pnpm run build:release` : Builds a Wordpress plugin ZIP **without** pushing it to Github and creating a release. - -For more helper scripts [see here](./CONTRIBUTING.md#helper-scripts) - -For some debugging tools/help [see here](./CONTRIBUTING.md#debugging) - -For local development setup using Docker [see here](./docker/wc-admin-wp-env/README.md) - -### Typescript - -The `npm run ts:check` command will check your TypeScript files for errors, and has been added to `.vscode/tasks.json`. -Running this task in vscode will highlight the errors in your editor file navigator. - -If you allow the `npm run ts:check:watch` command to run automatically as configured, it will run in the background and pick up any errors as you save the files. -Note: Even if you don't run this task, the IDE uses its language server to pick up type errors in files that are open. This is only necessary for picking up errors -across the entire repository even when they haven't been opened in the IDE. - -### Testing - -#### End-to-end tests - -Tests live in `./tests/e2e`. An existing build is required prior running, please refer to the section above for steps. E2E tests use the `@woocommerce/e2e-environment` package which hosts a Docker container for testing, by default the container can be accessed at `http://localhost:8084` - -All the commands from `@woocommerce/e2e-environment` can be run through `pnpm exec`. - -``` -# Set up the e2e environment -pnpm i -pnpm exec wc-e2e docker:up -``` - -Run tests using: - -``` -pnpm exec wc-e2e test:e2e-dev -``` - -or in headless mode: - -``` -pnpm exec wc-e2e test:e2e -``` - -Run a single test by adding the path to the file name: - -``` -pnpm exec wc-e2e test:e2e-dev tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.ts -``` - -### Documentation - -There is documentation in 2 forms available in the repo. A static set of documentation supported by docsify and also a Storybook containing component documentation for `@woocommerce/components`. - -To view the docsify docs locally you can do: - -``` -pnpm install -cd docs -pnpm exec docsify serve -``` - -When deployed the docsify docs also host an embedded version of the storybook docs. To generate that and test it locally in docsify you'll need to run: - -``` -pnpm install -pnpm run docs -cd docs -pnpm exec docsify serve -``` - -Then navigate to `Components` from the left hand menu in the docs. - -If you would like to view the storybook docs hosted standalone, then you can run: - -``` -pnpm install -pnpm run storybook -``` - -If you would like to view the storybook docs in right-to-left styling, you can run this instead: - -``` -pnpm install -pnpm run storybook-rtl -``` +Please refer to the [WooCommerce Admin Development](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment#wooCommerce-admin-development) +## End-to-end tests +Please refer to the [WooCommerce End to End Tests](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/README.md) ## Common Issues -If you're encountering any issue setting things up, chances are we have been there too. Please have a look at our [wiki](https://github.com/woocommerce/woocommerce-admin/wiki/Common-Issues) for a list of common problems. - -## Privacy - -If you have enabled WooCommerce usage tracking ( option `woocommerce_allow_tracking` ) then, in addition to the tracking described in https://woocommerce.com/usage-tracking/, this plugin also sends information about the actions that site administrators perform to Automattic - see https://automattic.com/privacy/#information-we-collect-automatically for more information. - -## Contributing - -There are many ways to contribute ā€“ reporting bugs, adding translations, feature suggestions and fixing bugs. For full details, please see [CONTRIBUTING.md](./CONTRIBUTING.md) +If you're encountering any issue setting things up, chances are we have been there too. Please have a look at our [wiki](https://github.com/woocommerce/woocommerce/wiki/Common-Issues) for a list of common problems. diff --git a/plugins/woocommerce-admin/babel.config.js b/plugins/woocommerce-admin/babel.config.js index 93a8add31f2..70215466892 100644 --- a/plugins/woocommerce-admin/babel.config.js +++ b/plugins/woocommerce-admin/babel.config.js @@ -22,16 +22,7 @@ module.exports = function ( api ) { ], ignore: [ 'packages/**/node_modules' ], env: { - production: { - plugins: [ - [ - '@wordpress/babel-plugin-makepot', - { - output: 'languages/woocommerce-admin.po', - }, - ], - ], - }, + production: {}, storybook: { plugins: [ diff --git a/plugins/woocommerce-admin/bin/combine-pot-files.php b/plugins/woocommerce-admin/bin/combine-pot-files.php deleted file mode 100644 index fa82f605ddf..00000000000 --- a/plugins/woocommerce-admin/bin/combine-pot-files.php +++ /dev/null @@ -1,104 +0,0 @@ - $original ) { - // Use the complete message section to match strings to be translated. - if ( isset( $originals_1[ $message ] ) ) { - $original = array_merge( $original, $originals_1[ $message ] ); - unset( $originals_1[ $message ] ); - } - - fwrite( $fh, implode( "\n", $original ) ); - fwrite( $fh, "\n" . $message ."\n\n" ); -} - -foreach ( $originals_1 as $message => $original ) { - fwrite( $fh, implode( "\n", $original ) ); - fwrite( $fh, "\n" . $message ."\n\n" ); -} - -fclose( $fh ); - -echo "Created {$target_file}\n"; diff --git a/plugins/woocommerce-admin/bin/make-i18n-json.sh b/plugins/woocommerce-admin/bin/make-i18n-json.sh deleted file mode 100755 index 5bc91bacef1..00000000000 --- a/plugins/woocommerce-admin/bin/make-i18n-json.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -# Check for required version -WPCLI_VERSION=`wp cli version | cut -f2 -d' '` -if [ ${WPCLI_VERSION:0:1} -lt "2" -o ${WPCLI_VERSION:0:1} -eq "2" -a ${WPCLI_VERSION:2:1} -lt "1" ]; then - echo WP-CLI version 2.1.0 or greater is required to make JSON translation files - exit -fi - -# Substitute JS source references with build references -for T in `find languages -name "*.po"` - do - sed \ - -e 's/ client\/[^:]*:/ dist\/app\/index.js:/gp' \ - -e 's/ packages\/components[^:]*:/ dist\/components\/index.js:/gp' \ - -e 's/ packages\/date[^:]*:/ dist\/date\/index.js:/gp' \ - $T | uniq > $T-build - rm $T - mv $T-build $T - done - -# Make the JSON files -wp i18n make-json languages --no-purge \ No newline at end of file diff --git a/plugins/woocommerce-admin/bin/starter-pack/README.md b/plugins/woocommerce-admin/bin/starter-pack/README.md index 2ab0b68740d..eb549bd5668 100644 --- a/plugins/woocommerce-admin/bin/starter-pack/README.md +++ b/plugins/woocommerce-admin/bin/starter-pack/README.md @@ -5,12 +5,12 @@ Scaffold a modern JavaScript WordPress plugin with WooCommerce tooling. ## Includes - [wp-scripts](https://github.com/WordPress/gutenberg/tree/master/packages/scripts) -- [WooCommerce Dependency Extraction Webpack Plugin](https://github.com/woocommerce/woocommerce-admin/tree/main/packages/dependency-extraction-webpack-plugin) -- [WooCommerce ESLint Plugin with WordPress Prettier](https://github.com/woocommerce/woocommerce-admin/tree/main/packages/eslint-plugin) +- [WooCommerce Dependency Extraction Webpack Plugin](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/dependency-extraction-webpack-plugin) +- [WooCommerce ESLint Plugin with WordPress Prettier](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/eslint-plugin) ### Usage -At the root of a [WooCommerce Admin](https://github.com/woocommerce/woocommerce-admin) installation, run the create extension command. +At the root of a [WooCommerce Admin](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-admin) installation, run the create extension command. ``` pnpm run create-wc-extension diff --git a/plugins/woocommerce-admin/changelogs/feature-32164_new_task_list_version_2 b/plugins/woocommerce-admin/changelogs/feature-32164_new_task_list_version_2 new file mode 100644 index 00000000000..77b6ac86e61 --- /dev/null +++ b/plugins/woocommerce-admin/changelogs/feature-32164_new_task_list_version_2 @@ -0,0 +1,4 @@ +Significance: minor +Type: Add + +Add support for sections in our TaskList class and create a new sectional task list component. #32302 diff --git a/plugins/woocommerce-admin/client/activity-panel/panels/help.js b/plugins/woocommerce-admin/client/activity-panel/panels/help.js index b13474e4f0b..a27469c37c1 100644 --- a/plugins/woocommerce-admin/client/activity-panel/panels/help.js +++ b/plugins/woocommerce-admin/client/activity-panel/panels/help.js @@ -100,6 +100,10 @@ function getMarketingItems( props ) { link: 'https://woocommerce.com/document/google-listings-and-ads/?utm_medium=product#get-started', }, + activePlugins.includes( 'pinterest-for-woocommerce' ) && { + title: __( 'Set up Pinterest for WooCommerce', 'woocommerce' ), + link: 'https://woocommerce.com/products/pinterest-for-woocommerce/', + }, activePlugins.includes( 'mailchimp-for-woocommerce' ) && { title: __( 'Connect Mailchimp for WooCommerce', 'woocommerce' ), link: diff --git a/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js b/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js index ce28b7673a4..7ee8b89db32 100644 --- a/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js @@ -121,7 +121,7 @@ Leaderboard.propTypes = { */ query: PropTypes.object, /** - * Which column should be the row header, defaults to the first item (`0`) (see `Table` props). + * An array of arrays of display/value object pairs (see `Table` props). */ rows: PropTypes.arrayOf( PropTypes.arrayOf( diff --git a/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx b/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx index a1bb9c58f35..1187d102815 100644 --- a/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx +++ b/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx @@ -2,13 +2,13 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { COUNTRIES_STORE_NAME, Country } from '@woocommerce/data'; +import { COUNTRIES_STORE_NAME, Country, Locale } from '@woocommerce/data'; import { decodeEntities } from '@wordpress/html-entities'; -import { escapeRegExp } from 'lodash'; +import { escapeRegExp, has } from 'lodash'; import { useEffect, useMemo, useState, useRef } from '@wordpress/element'; import { SelectControl, TextControl } from '@woocommerce/components'; import { Spinner } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; +import { useSelect, select as wpDataSelect } from '@wordpress/data'; /** * Internal dependencies @@ -21,10 +21,24 @@ const storeAddressFields = [ 'city', 'countryState', 'postCode', -]; +] as const; type Option = { key: string; label: string }; +/** + * Type guard to ensure that the specified locale object has a .required property + * + * @param fieldName field of Locale + * @param locale unknown object to be checked + * @return Boolean indicating if locale has a .required property + */ +const isLocaleRecord = ( + fieldName: keyof Locale, + locale: unknown +): locale is Record< keyof Locale, { required: boolean } > => { + return !! locale && has( locale, `${ fieldName }.required` ); +}; + /** * Check if a given address field is required for the locale. * @@ -33,11 +47,11 @@ type Option = { key: string; label: string }; * @return {boolean} Field requirement. */ export function isAddressFieldRequired( - fieldName: string, - locale: unknown = {} + fieldName: keyof Locale, + locale: Locale = {} ): boolean { - if ( locale[ fieldName ]?.hasOwnProperty( 'required' ) ) { - return locale[ fieldName ]?.required as boolean; + if ( isLocaleRecord( fieldName, locale ) ) { + return locale[ fieldName ].required; } if ( fieldName === 'address_2' ) { @@ -53,18 +67,19 @@ export function isAddressFieldRequired( * @param {Object} locale The store locale. * @return {Function} Validator function. */ -export function getStoreAddressValidator( locale = {} ) { +export function getStoreAddressValidator( locale: Locale = {} ) { /** * Form validator. * * @param {Object} values Keyed values of all fields in the form. * @return {Object} Key value of fields and error messages, { myField: 'This field is required' } */ - return ( values ) => { + return ( + values: Record< typeof storeAddressFields[ number ], string > + ) => { const errors: { [ key: string ]: string; } = {}; - if ( isAddressFieldRequired( 'address_1', locale ) && ! values.addressLine1.trim().length @@ -100,30 +115,33 @@ export function getStoreAddressValidator( locale = {} ) { * @return {Object} Select options, { value: 'US:GA', label: 'United States - Georgia' } */ export function getCountryStateOptions( countries: Country[] ) { - const countryStateOptions = countries.reduce( ( acc, country ) => { - if ( ! country.states.length ) { - acc.push( { - key: country.code, - label: decodeEntities( country.name ), + const countryStateOptions = countries.reduce( + ( acc: Option[], country ) => { + if ( ! country.states.length ) { + acc.push( { + key: country.code, + label: decodeEntities( country.name ), + } ); + + return acc; + } + + const countryStates = country.states.map( ( state ) => { + return { + key: country.code + ':' + state.code, + label: + decodeEntities( country.name ) + + ' ā€” ' + + decodeEntities( state.name ), + }; } ); + acc.push( ...countryStates ); + return acc; - } - - const countryStates = country.states.map( ( state ) => { - return { - key: country.code + ':' + state.code, - label: - decodeEntities( country.name ) + - ' ā€” ' + - decodeEntities( state.name ), - }; - } ); - - acc.push( ...countryStates ); - - return acc; - }, [] ); + }, + [] + ); return countryStateOptions; } @@ -195,9 +213,7 @@ export function useGetCountryStateAutofill( ): JSX.Element { const [ autofillCountry, setAutofillCountry ] = useState( '' ); const [ autofillState, setAutofillState ] = useState( '' ); - const isAutofillChange: { - current: boolean; - } = useRef(); + const isAutofillChange = useRef< boolean >(); // Sync the autofill fields on first render and the countryState value changes. useEffect( () => { @@ -242,7 +258,7 @@ export function useGetCountryStateAutofill( const isCountryAbbreviation = autofillCountry.length < 3; const isStateAbbreviation = autofillState.length < 3 && !! autofillState.match( /^[\w]+$/ ); - let filteredOptions = []; + let filteredOptions: Option[] = []; if ( autofillCountry.length && autofillState.length ) { filteredOptions = options.filter( ( option ) => @@ -337,7 +353,7 @@ export function StoreAddress( { hasFinishedResolution, countries, loadingCountries, - } = useSelect( ( select ) => { + } = useSelect( ( select: typeof wpDataSelect ) => { const { getLocale, getCountries, diff --git a/plugins/woocommerce-admin/client/dashboard/components/settings/general/test/store-address.js b/plugins/woocommerce-admin/client/dashboard/components/settings/general/test/store-address.js index 4f05554f9ba..70473a74cf8 100644 --- a/plugins/woocommerce-admin/client/dashboard/components/settings/general/test/store-address.js +++ b/plugins/woocommerce-admin/client/dashboard/components/settings/general/test/store-address.js @@ -7,7 +7,11 @@ import { render, fireEvent } from '@testing-library/react'; /** * Internal dependencies */ -import { useGetCountryStateAutofill, getStateFilter } from '../store-address'; +import { + isAddressFieldRequired, + useGetCountryStateAutofill, + getStateFilter, +} from '../store-address'; const AutofillWrapper = ( { options, value, onChange } ) => { const [ values, setValues ] = useState( { countryState: value || '' } ); @@ -211,3 +215,28 @@ describe( 'getStateFilter', () => { } ); } ); + +describe( 'isAddressFieldRequired', () => { + it( 'should return true if fieldName is not a key in locale', () => { + expect( isAddressFieldRequired( 'address_1', { foo: 'bar' } ) ).toBe( + true + ); + } ); + it( 'should return true if locale object marks it as required', () => { + expect( + isAddressFieldRequired( 'address_1', { + address_1: { required: true }, + } ) + ).toBe( true ); + } ); + it( 'should return false if locale object marks it as not required', () => { + expect( + isAddressFieldRequired( 'address_1', { + address_1: { required: false }, + } ) + ).toBe( false ); + } ); + it( 'should return false if fieldName is address_2', () => { + expect( isAddressFieldRequired( 'address_2', {} ) ).toBe( false ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/header/index.js b/plugins/woocommerce-admin/client/header/index.js index 5ef3c246801..711b04fce3c 100644 --- a/plugins/woocommerce-admin/client/header/index.js +++ b/plugins/woocommerce-admin/client/header/index.js @@ -6,7 +6,9 @@ import { useEffect, useLayoutEffect, useRef } from '@wordpress/element'; import classnames from 'classnames'; import { decodeEntities } from '@wordpress/html-entities'; import { getSetting } from '@woocommerce/settings'; +import { ONBOARDING_STORE_NAME } from '@woocommerce/data'; import { Text, useSlot } from '@woocommerce/experimental'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -93,12 +95,18 @@ export const Header = ( { sections, isEmbedded = false, query } ) => { } }, [ isEmbedded, sections, siteTitle ] ); - const tasksReminderFeature = - window.wcAdminFeatures[ 'tasklist-setup-experiment-1' ]; + const { hasTasksReminderFeature } = useSelect( ( select ) => { + const taskLists = select( ONBOARDING_STORE_NAME ).getTaskLists(); + return { + hasTasksReminderFeature: taskLists.some( + ( list ) => list.id === 'setup_experiment_1' + ), + }; + } ); return (
- { tasksReminderFeature && ( + { hasTasksReminderFeature && ( import( /* webpackChunkName: "tasks" */ '../tasks' ) @@ -63,6 +64,8 @@ export const Layout = ( { query, taskListComplete, hasTaskList, + showingProgressHeader, + isLoadingTaskLists, shouldShowWelcomeModal, shouldShowWelcomeFromCalypsoModal, isTaskListHidden, @@ -140,7 +143,9 @@ export const Layout = ( { { ! isLoadingExperimentAssignment && ! isLoadingTwoColExperimentAssignment && - ! isRunningTaskListExperiment && ( + ! isRunningTaskListExperiment && + ! isLoadingTaskLists && + ! showingProgressHeader && ( list.isVisible ), + hasTaskList: getAdminSetting( 'visibleTaskListIds', [] ).length > 0, + showingProgressHeader: !! taskLists.find( + ( list ) => list.isVisible && list.displayProgressHeader + ), taskListComplete: getTaskList( 'setup' )?.isComplete, installTimestamp, installTimestampHasResolved, diff --git a/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx b/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx index a1bd79bb71f..93d25671df2 100644 --- a/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx +++ b/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx @@ -79,10 +79,21 @@ const PaymentRecommendations: React.FC = () => { [ isInstalled ] ); + const supportsWCPayments = + paymentGatewaySuggestions && + paymentGatewaySuggestions.filter( ( paymentGatewaySuggestion ) => { + return ( + paymentGatewaySuggestion.id.indexOf( + 'woocommerce_payments' + ) === 0 + ); + } ).length === 1; + const triggeredPageViewRef = useRef( false ); const shouldShowRecommendations = paymentGatewaySuggestions && paymentGatewaySuggestions.length > 0 && + ! supportsWCPayments && ! isDismissed; useEffect( () => { diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js index 10dfadf55f3..71ecdb0bf33 100644 --- a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js +++ b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js @@ -201,7 +201,10 @@ class BusinessDetails extends Component { const promises = [ this.persistProfileItems( { - business_extensions: businessExtensions, + business_extensions: [ + ...businessExtensions, + ...alreadyActivatedExtensions, + ], } ), ]; diff --git a/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss b/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss index e4376a5e49c..10c1235e642 100644 --- a/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss +++ b/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss @@ -1,3 +1,4 @@ +@import 'node_modules/@wordpress/base-styles/colors.native'; // By using CSS variables, we can switch the spacing rhythm using a single media query. :root { --large-gap: 40px; @@ -37,6 +38,14 @@ max-width: 100%; line-height: 1; } + + .components-panel__body > .components-panel__body-title, + .woocommerce-experimental-list__item, + .woocommerce-inbox-message { + &:hover { + background: $gray-0; + } + } } body.woocommerce-page { diff --git a/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.scss b/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.scss index e51ab9b22a7..9336e24de55 100644 --- a/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.scss +++ b/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.scss @@ -19,9 +19,9 @@ $progress-complete-color: #007cba; appearance: none; border: 1px solid #ddd; border-radius: 16px; - height: 10px; + height: 12px; width: 100%; - margin-bottom: 20px; + margin-bottom: 0; // Firefox & { @@ -49,5 +49,6 @@ $progress-complete-color: #007cba; .woocommerce-card__menu { position: absolute; right: 0; + top: 7px; } } diff --git a/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.tsx b/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.tsx index aa541e27db4..46264572b75 100644 --- a/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.tsx +++ b/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.tsx @@ -4,7 +4,11 @@ import { __, sprintf } from '@wordpress/i18n'; import { useMemo } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; -import { ONBOARDING_STORE_NAME, TaskListType } from '@woocommerce/data'; +import { + getVisibleTasks, + ONBOARDING_STORE_NAME, + TaskListType, +} from '@woocommerce/data'; import { getSetting } from '@woocommerce/settings'; /** @@ -20,37 +24,38 @@ type ProgressHeaderProps = { export const ProgressHeader: React.FC< ProgressHeaderProps > = ( { taskListId, } ) => { - const { loading, tasksCount, completedCount, hasVisitedTasks } = useSelect( - ( select ) => { - const taskList: TaskListType = select( - ONBOARDING_STORE_NAME - ).getTaskList( taskListId ); - const finishedResolution = select( - ONBOARDING_STORE_NAME - ).hasFinishedResolution( 'getTaskList', [ taskListId ] ); - const nowTimestamp = Date.now(); - const visibleTasks = taskList?.tasks.filter( - ( task ) => - ! task.isDismissed && - ( ! task.isSnoozed || task.snoozedUntil < nowTimestamp ) - ); + const { + loading, + tasksCount, + completedCount, + hasVisitedTasks, + disabledCompletedCount, + } = useSelect( ( select ) => { + const taskList: TaskListType = select( + ONBOARDING_STORE_NAME + ).getTaskList( taskListId ); + const finishedResolution = select( + ONBOARDING_STORE_NAME + ).hasFinishedResolution( 'getTaskList', [ taskListId ] ); + const visibleTasks = getVisibleTasks( taskList?.tasks ); - return { - loading: ! finishedResolution, - tasksCount: visibleTasks?.length, - completedCount: visibleTasks?.filter( - ( task ) => task.isComplete - ).length, - hasVisitedTasks: - visibleTasks?.filter( ( task ) => task.isVisited ).length > - 0, - }; - } - ); + return { + loading: ! finishedResolution, + tasksCount: visibleTasks?.length, + completedCount: visibleTasks?.filter( ( task ) => task.isComplete ) + .length, + disabledCompletedCount: visibleTasks?.filter( + ( task ) => task.isComplete && task.isDisabled + ).length, + hasVisitedTasks: + visibleTasks?.filter( ( task ) => task.isVisited ).length > 0, + }; + } ); const progressTitle = useMemo( () => { if ( - ( ! hasVisitedTasks && completedCount < 2 ) || + ( ! hasVisitedTasks && + completedCount < 2 + disabledCompletedCount ) || completedCount === tasksCount ) { const siteTitle = getSetting( 'siteTitle' ); @@ -71,7 +76,7 @@ export const ProgressHeader: React.FC< ProgressHeaderProps > = ( { return __( 'You are almost there', 'woocommerce' ); }, [ completedCount, hasVisitedTasks ] ); - if ( loading || completedCount === tasksCount ) { + if ( loading ) { return null; } @@ -85,22 +90,26 @@ export const ProgressHeader: React.FC< ProgressHeaderProps > = ( {

{ progressTitle }

-

- { sprintf( - /* translators: 1: completed tasks, 2: total tasks */ - __( - 'Follow these steps to start selling quickly. %1$d out of %2$d complete.', - 'woocommerce' - ), - completedCount, - tasksCount - ) } -

- + { completedCount !== tasksCount ? ( + <> +

+ { sprintf( + /* translators: 1: completed tasks, 2: total tasks */ + __( + 'Follow these steps to start selling quickly. %1$d out of %2$d complete.', + 'woocommerce' + ), + completedCount, + tasksCount + ) } +

+ + + ) : null }
); diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Action.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Action.js index e16ff3644b1..3b3c4ec66f2 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Action.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Action.js @@ -24,7 +24,8 @@ export const Action = ( { markConfigured, onSetUp = () => {}, onSetupCallback, - setupButtonText = __( 'Set up', 'woocommerce' ), + setupButtonText = __( 'Get started', 'woocommerce' ), + externalLink = null, } ) => { const [ isBusy, setIsBusy ] = useState( false ); @@ -41,6 +42,11 @@ export const Action = ( { selected: getPluginTrackKey( id ), } ); + if ( ! hasPlugins && externalLink ) { + window.location.href = externalLink; + return; + } + if ( onSetupCallback ) { setIsBusy( true ); await new Promise( onSetupCallback ) @@ -84,17 +90,19 @@ export const Action = ( { ); + const EnableButton = () => ( + + ); + if ( ! hasSetup ) { if ( ! isEnabled ) { - return ( - - ); + return ; } return ; @@ -110,6 +118,10 @@ export const Action = ( { } if ( ! needsSetup ) { + if ( ! isEnabled ) { + return ; + } + return ; } diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/Item.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/Item.js index aabd4e12b06..af919e6970b 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/Item.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/Item.js @@ -4,8 +4,10 @@ import classnames from 'classnames'; import { Fragment } from '@wordpress/element'; import { CardBody, CardMedia, CardDivider } from '@wordpress/components'; -import { RecommendedRibbon, SetupRequired } from '@woocommerce/onboarding'; +import { SetupRequired } from '@woocommerce/onboarding'; +import { Pill } from '@woocommerce/components'; import { Text, useSlot } from '@woocommerce/experimental'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -15,7 +17,7 @@ import './List.scss'; export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => { const { - image, + image_72x72: image72x72, content, id, plugins = [], @@ -27,6 +29,7 @@ export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => { requiredSettings, settingsUrl: manageUrl, is_local_partner: isLocalPartner, + external_link: externalLink, } = paymentGateway; const connectSlot = useSlot( @@ -39,9 +42,9 @@ export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => { Boolean( setupSlot?.fills?.length ); const hasSetup = Boolean( - plugins.length || requiredSettings.length || hasFills + plugins.length || requiredSettings.length || hasFills || externalLink ); - const showRecommendedRibbon = isRecommended && needsSetup; + const showRecommended = isRecommended && needsSetup; const classes = classnames( 'woocommerce-task-payment', @@ -57,14 +60,20 @@ export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => { className={ classes } > - { + {
- { showRecommendedRibbon && ( - - ) } - { title } + { title } + { showRecommended && ( + + { isLocalPartner + ? __( 'Local Partner', 'woocommerce' ) + : __( 'Recommended', 'woocommerce' ) } + + ) } { isInstalled && needsSetup && !! plugins.length && ( ) } @@ -85,6 +94,7 @@ export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => { isRecommended={ isRecommended } isLoading={ loading } markConfigured={ markConfigured } + externalLink={ externalLink } />
diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js index 9375f94938a..289243ed3c9 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { Card, CardHeader } from '@wordpress/components'; +import { Card, CardHeader, CardFooter } from '@wordpress/components'; /** * Internal dependencies @@ -15,10 +15,11 @@ export const List = ( { markConfigured, recommendation, paymentGateways, + footerLink, } ) => { return ( - { heading } + { heading && { heading } } { paymentGateways.map( ( paymentGateway ) => { const { id } = paymentGateway; return ( @@ -30,6 +31,9 @@ export const List = ( { /> ); } ) } + { footerLink && ( + { footerLink } + ) } ); }; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.scss b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.scss index dd2b03ecafe..da05a93d13e 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.scss +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.scss @@ -6,14 +6,15 @@ overflow: hidden; .components-card__media { - width: 170px; + width: 85px; flex-shrink: 0; + align-self: flex-start; img, svg, .is-placeholder { margin: auto; - max-width: 100px; + max-width: 36px; display: block; } @@ -41,6 +42,15 @@ color: $studio-gray-80; margin-top: 0; margin-bottom: $gap-smaller; + + .woocommerce-pill { + margin-left: 8px; + + &.pill-green { + color: #008a20; + border-color: #008a20; + } + } } .woocommerce-task-payment__content { diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/test/list.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/test/list.js index 8e7d4caca25..6c7331eec61 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/test/list.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/test/list.js @@ -49,7 +49,7 @@ describe( 'PaymentGatewaySuggestions > List', () => { expect( queryByRole( 'button' ) ).toHaveTextContent( 'Enable' ); } ); - it( 'should display the "Set up" button when setup is required', () => { + it( 'should display the "Get started" button when setup is required', () => { const props = { ...defaultProps, paymentGateways: [ @@ -63,7 +63,7 @@ describe( 'PaymentGatewaySuggestions > List', () => { const { queryByRole } = render( ); - expect( queryByRole( 'button' ) ).toHaveTextContent( 'Set up' ); + expect( queryByRole( 'button' ) ).toHaveTextContent( 'Get started' ); } ); it( 'should display the SetupRequired component when appropriate', () => { @@ -138,7 +138,7 @@ describe( 'PaymentGatewaySuggestions > List', () => { expect( queryByText( 'Recommended' ) ).not.toBeInTheDocument(); } ); - it( 'should display Manage button if not enabled and does have setup', () => { + it( 'should display Manage button if enabled and does have setup', () => { const props = { ...defaultProps, paymentGateways: [ @@ -180,6 +180,7 @@ describe( 'PaymentGatewaySuggestions > List', () => { ...mockGateway, plugins: [ 'nope' ], needsSetup: false, + enabled: true, }, ], }; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Configure.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Configure.js index 528d62f63a3..b80430d58e9 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Configure.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Configure.js @@ -156,7 +156,7 @@ export const Configure = ( { markConfigured, paymentGateway } ) => {

) } ); diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/test/configure.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/test/configure.js index daacf3cc803..81d63b0930e 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/test/configure.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/test/configure.js @@ -81,7 +81,7 @@ describe( 'Configure', () => { const { container } = render( ); const button = container.querySelector( 'a' ); - expect( button.textContent ).toBe( 'Set up' ); + expect( button.textContent ).toBe( 'Get started' ); expect( button.href ).toBe( mockGateway.settingsUrl ); } ); } ); diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js new file mode 100644 index 00000000000..95ad0f2b078 --- /dev/null +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js @@ -0,0 +1,40 @@ +/** + * External dependencies + */ +import { Button } from '@wordpress/components'; +import ChevronUpIcon from 'gridicons/dist/chevron-up'; +import ChevronDownIcon from 'gridicons/dist/chevron-down'; +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import './Toggle.scss'; + +export const Toggle = ( { children, heading, onToggle } ) => { + const [ isShow, setIsShow ] = useState( false ); + const onClick = () => { + onToggle( isShow ); + setIsShow( ! isShow ); + }; + + return ( +
+ + { isShow ? children : null } +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.scss b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.scss new file mode 100644 index 00000000000..b1605309872 --- /dev/null +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.scss @@ -0,0 +1,9 @@ +.woocommerce-task-payments { + .toggle-button { + margin: $gap-small 0; + + .gridicon { + margin-left: 4px; + } + } +} diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/index.js new file mode 100644 index 00000000000..87fdc434811 --- /dev/null +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/index.js @@ -0,0 +1 @@ +export { Toggle } from './Toggle'; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js index a908df8304f..185f050c284 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js @@ -7,26 +7,44 @@ import { OPTIONS_STORE_NAME, ONBOARDING_STORE_NAME, PAYMENT_GATEWAYS_STORE_NAME, + SETTINGS_STORE_NAME, } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; import { useMemo, useCallback, useEffect } from '@wordpress/element'; import { registerPlugin } from '@wordpress/plugins'; import { WooOnboardingTask } from '@woocommerce/onboarding'; import { getNewPath } from '@woocommerce/navigation'; +import { getAdminLink } from '@woocommerce/settings'; +import { Button } from '@wordpress/components'; +import ExternalIcon from 'gridicons/dist/external'; /** * Internal dependencies */ import { List, Placeholder as ListPlaceholder } from './components/List'; import { Setup, Placeholder as SetupPlaceholder } from './components/Setup'; +import { Toggle } from './components/Toggle/Toggle'; import { WCPaySuggestion } from './components/WCPay'; import { getPluginSlug } from '~/utils'; +import { getCountryCode } from '~/dashboard/utils'; import './plugins/Bacs'; import './payment-gateway-suggestions.scss'; const comparePaymentGatewaysByPriority = ( a, b ) => a.recommendation_priority - b.recommendation_priority; +const isGatewayWCPay = ( gateway ) => + gateway.plugins?.length === 1 && + gateway.plugins[ 0 ] === 'woocommerce-payments'; + +const isGatewayOtherCategory = ( gateway, countryCode ) => + gateway.category_other && + gateway.category_other.indexOf( countryCode ) !== -1; + +const isGatewayAdditionalCategory = ( gateway, countryCode ) => + gateway.category_additional && + gateway.category_additional.indexOf( countryCode ) !== -1; + export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { const { updatePaymentGateway } = useDispatch( PAYMENT_GATEWAYS_STORE_NAME ); const { @@ -34,7 +52,10 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { paymentGatewaySuggestions, installedPaymentGateways, isResolving, + countryCode, } = useSelect( ( select ) => { + const { getSettings } = select( SETTINGS_STORE_NAME ); + const { general: settings = {} } = getSettings( 'general' ); return { getPaymentGateway: select( PAYMENT_GATEWAYS_STORE_NAME ) .getPaymentGateway, @@ -48,6 +69,7 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { paymentGatewaySuggestions: select( ONBOARDING_STORE_NAME ).getPaymentGatewaySuggestions(), + countryCode: getCountryCode( settings.woocommerce_default_country ), }; }, [] ); @@ -179,7 +201,28 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { return gateway; }, [ isResolving, query, paymentGateways ] ); - const [ wcPayGateway, enabledGateways, additionalGateways ] = useMemo( + const isWCPayOrOtherCategoryDoneSetup = useMemo( () => { + for ( const [ , gateway ] of paymentGateways.entries() ) { + if ( ! gateway.installed || gateway.needsSetup ) { + continue; + } + + if ( isGatewayWCPay( gateway ) ) { + return true; + } + + if ( isGatewayOtherCategory( gateway, countryCode ) ) { + return true; + } + } + return false; + }, [ countryCode, paymentGateways ] ); + + const isWCPaySupported = + Array.from( paymentGateways.values() ).findIndex( isGatewayWCPay ) !== + -1; + + const [ wcPayGateway, offlineGateways, additionalGateways ] = useMemo( () => Array.from( paymentGateways.values() ) .sort( ( a, b ) => { @@ -196,18 +239,36 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { } ) .reduce( ( all, gateway ) => { - const [ wcPay, enabled, additional ] = all; + const [ wcPay, offline, additional ] = all; // WCPay is handled separately when not installed and configured if ( - gateway.plugins?.length === 1 && - gateway.plugins[ 0 ] === 'woocommerce-payments' && + isGatewayWCPay( gateway ) && ! ( gateway.installed && ! gateway.needsSetup ) ) { wcPay.push( gateway ); + } else if ( gateway.is_offline ) { + offline.push( gateway ); } else if ( gateway.enabled ) { - enabled.push( gateway ); - } else { + // Enabled gateways should be ignored. + } else if ( isWCPayOrOtherCategoryDoneSetup ) { + // If WCPay or "other" gateway is enabled in an WCPay-eligible country, only + // allow to list "additional" gateways. + if ( + isGatewayAdditionalCategory( + gateway, + countryCode + ) + ) { + additional.push( gateway ); + } + } else if ( ! isWCPaySupported ) { + // When WCPay-ineligible, just show all gateways. + additional.push( gateway ); + } else if ( + isGatewayOtherCategory( gateway, countryCode ) + ) { + // When nothing is set up and eligible for WCPay, only show "other" gateways. additional.push( gateway ); } @@ -215,9 +276,26 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { }, [ [], [], [] ] ), - [ paymentGateways ] + [ + countryCode, + isWCPaySupported, + isWCPayOrOtherCategoryDoneSetup, + paymentGateways, + ] ); + const trackSeeMore = () => { + recordEvent( 'tasklist_payment_see_more', {} ); + }; + + const trackToggle = ( isShow ) => { + recordEvent( 'tasklist_payment_show_toggle', { + toggle: isShow ? 'hide' : 'show', + payment_method_count: + offlineGateways.length + additionalGateways.length, + } ); + }; + if ( query.id && ! currentGateway ) { return ; } @@ -231,32 +309,62 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { ); } + const additionalSection = !! additionalGateways.length && ( + + { __( 'See more', 'woocommerce' ) } + + + ) + } + > + ); + + const offlineSection = !! offlineGateways.length && ( + + ); + return (
{ ! paymentGateways.size && } - { !! wcPayGateway.length && ( - - ) } - - { !! enabledGateways.length && ( - - ) } - - { !! additionalGateways.length && ( - + { wcPayGateway.length ? ( + <> + + + { additionalSection } + { offlineSection } + + + ) : ( + <> + { additionalSection } + { offlineSection } + ) }
); diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss index 0716dc3d3fa..4f72919997a 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss @@ -22,6 +22,14 @@ margin: 0; } + .components-card__footer { + a.components-button { + .gridicon { + margin-left: 4px; + } + } + } + .woocommerce-task-payment__recommended-pill { border: 1px solid $studio-gray-5; border-radius: 28px; @@ -38,6 +46,10 @@ .components-card__divider:last-child { display: none; } + + .woocommerce-task-payment-wcpay { + margin-bottom: 0; + } } // @todo This can be migrated into the PaymentMethod component once the payment-gateway-suggestions feature is enabled. diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js index eae438fe47b..c1d710c865b 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js @@ -31,6 +31,8 @@ const paymentGatewaySuggestions = [ plugins: [ 'woocommerce-gateway-stripe' ], is_visible: true, recommendation_priority: 3, + category_other: [ 'US' ], + category_additional: [], }, { id: 'ppcp-gateway', @@ -41,6 +43,8 @@ const paymentGatewaySuggestions = [ 'http://localhost:8888/wp-content/plugins/woocommerce/assets/images/paypal.png', plugins: [ 'woocommerce-paypal-payments' ], is_visible: true, + category_other: [ 'US' ], + category_additional: [ 'US' ], }, { id: 'cod', @@ -49,6 +53,7 @@ const paymentGatewaySuggestions = [ image: 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/cod.svg', is_visible: true, + is_offline: true, }, { id: 'bacs', @@ -57,6 +62,7 @@ const paymentGatewaySuggestions = [ image: 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/bacs.svg', is_visible: true, + is_offline: true, }, { id: 'woocommerce_payments:non-us', @@ -80,11 +86,17 @@ const paymentGatewaySuggestions = [ 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/eway.png', plugins: [ 'woocommerce-gateway-eway' ], is_visible: true, + category_other: [ 'US' ], + category_additional: [ 'US' ], }, ]; +const paymentGatewaySuggestionsWithoutWCPay = paymentGatewaySuggestions.filter( + ( p ) => p.title !== 'WooCommerce Payments' +); + describe( 'PaymentGatewaySuggestions', () => { - test( 'should render payment gateway lists', () => { + test( 'should render only WCPay if its suggested', () => { const onComplete = jest.fn(); const query = {}; useSelect.mockImplementation( () => ( { @@ -109,6 +121,38 @@ describe( 'PaymentGatewaySuggestions', () => { ( e ) => e.textContent ); + expect( paymentTitles ).toEqual( [] ); + + expect( + container.getElementsByTagName( 'title' )[ 0 ].textContent + ).toBe( 'WooCommerce Payments' ); + } ); + + test( 'should render all payment gateways if no WCPay', () => { + const onComplete = jest.fn(); + const query = {}; + useSelect.mockImplementation( () => ( { + isResolving: false, + getPaymentGateway: jest.fn(), + paymentGatewaySuggestions: paymentGatewaySuggestionsWithoutWCPay, + installedPaymentGateways: [], + } ) ); + + const { container } = render( + + ); + + const paymentTitleElements = container.querySelectorAll( + '.woocommerce-task-payment__title > span:first-child' + ); + + const paymentTitles = Array.from( paymentTitleElements ).map( + ( e ) => e.textContent + ); + expect( paymentTitles ).toEqual( [ 'Stripe', 'PayPal Payments', @@ -116,10 +160,6 @@ describe( 'PaymentGatewaySuggestions', () => { 'Cash on delivery', 'Direct bank transfer', ] ); - - expect( - container.getElementsByTagName( 'title' )[ 0 ].textContent - ).toBe( 'WooCommerce Payments' ); } ); test( 'should the payment gateway offline options at the bottom', () => { @@ -128,7 +168,7 @@ describe( 'PaymentGatewaySuggestions', () => { useSelect.mockImplementation( () => ( { isResolving: false, getPaymentGateway: jest.fn(), - paymentGatewaySuggestions, + paymentGatewaySuggestions: paymentGatewaySuggestionsWithoutWCPay, installedPaymentGateways: [], } ) ); @@ -154,7 +194,7 @@ describe( 'PaymentGatewaySuggestions', () => { useSelect.mockImplementation( () => ( { isResolving: false, getPaymentGateway: jest.fn(), - paymentGatewaySuggestions, + paymentGatewaySuggestions: paymentGatewaySuggestionsWithoutWCPay, installedPaymentGateways: [ { id: 'ppcp-gateway', @@ -178,13 +218,56 @@ describe( 'PaymentGatewaySuggestions', () => { expect( getByText( 'Finish setup' ) ).toBeInTheDocument(); } ); - test( 'should record event correctly when finish setup is clicked', () => { + + test( 'should show "category_additional" gateways only after WCPay is set up', () => { const onComplete = jest.fn(); const query = {}; useSelect.mockImplementation( () => ( { isResolving: false, getPaymentGateway: jest.fn(), paymentGatewaySuggestions, + installedPaymentGateways: [ + { + id: 'woocommerce_payments', + title: 'WooCommerce Payments', + plugins: [ 'woocommerce-payments' ], + is_visible: true, + needs_setup: false, + }, + ], + countryCode: 'US', + } ) ); + + const { container } = render( + + ); + + const paymentTitleElements = container.querySelectorAll( + '.woocommerce-task-payment__title' + ); + + const paymentTitles = Array.from( paymentTitleElements ).map( + ( e ) => e.textContent + ); + + expect( paymentTitles ).toEqual( [ + 'PayPal Payments', + 'Eway', + 'Cash on delivery', + 'Direct bank transfer', + ] ); + } ); + + test( 'should record event correctly when finish setup is clicked', () => { + const onComplete = jest.fn(); + const query = {}; + useSelect.mockImplementation( () => ( { + isResolving: false, + getPaymentGateway: jest.fn(), + paymentGatewaySuggestions: paymentGatewaySuggestionsWithoutWCPay, installedPaymentGateways: [ { id: 'ppcp-gateway', @@ -211,4 +294,62 @@ describe( 'PaymentGatewaySuggestions', () => { selected: 'ppcp_gateway', } ); } ); + + test( 'should record event correctly when other payment methods is clicked', () => { + const onComplete = jest.fn(); + const query = {}; + useSelect.mockImplementation( () => ( { + isResolving: false, + getPaymentGateway: jest.fn(), + paymentGatewaySuggestions, + installedPaymentGateways: [], + countryCode: 'US', + } ) ); + + render( + + ); + + fireEvent.click( screen.getByText( 'Other payment methods' ) ); + + // By default it's hidden, so when toggle it shows. + // Second call after "tasklist_payments_options". + expect( + recordEvent.mock.calls[ recordEvent.mock.calls.length - 1 ] + ).toEqual( [ + 'tasklist_payment_show_toggle', + { + toggle: 'show', + payment_method_count: paymentGatewaySuggestions.length - 1, // Minus one for WCPay since it's not counted in "other payment methods". + }, + ] ); + } ); + + test( 'should record event correctly when see more is clicked', () => { + const onComplete = jest.fn(); + const query = {}; + useSelect.mockImplementation( () => ( { + isResolving: false, + getPaymentGateway: jest.fn(), + paymentGatewaySuggestions, + installedPaymentGateways: [], + countryCode: 'US', + } ) ); + + render( + + ); + + fireEvent.click( screen.getByText( 'Other payment methods' ) ); + fireEvent.click( screen.getByText( 'See more' ) ); + expect( + recordEvent.mock.calls[ recordEvent.mock.calls.length - 1 ] + ).toEqual( [ 'tasklist_payment_see_more', {} ] ); + } ); } ); diff --git a/plugins/woocommerce-admin/client/tasks/fills/purchase.js b/plugins/woocommerce-admin/client/tasks/fills/purchase.js index 31a894c9158..72ae6efcd48 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/purchase.js +++ b/plugins/woocommerce-admin/client/tasks/fills/purchase.js @@ -15,7 +15,7 @@ import { ONBOARDING_STORE_NAME, PLUGINS_STORE_NAME } from '@woocommerce/data'; import CartModal from '../../dashboard/components/cart-modal'; import { getCategorizedOnboardingProducts } from '../../dashboard/utils'; -const PurchaseTaskItem = () => { +const PurchaseTaskItem = ( { defaultTaskItem } ) => { const [ cartModalOpen, setCartModalOpen ] = useState( false ); const { installedPlugins, productTypes, profileItems } = useSelect( @@ -47,25 +47,32 @@ const PurchaseTaskItem = () => { installedPlugins ); const { remainingProducts } = groupedProducts; + const DefaultTaskItem = defaultTaskItem; + return ( + <> + { + if ( remainingProducts.length ) { + toggleCartModal(); + } + } } + /> + { cartModalOpen && ( + toggleCartModal() } + onClickPurchaseLater={ () => toggleCartModal() } + /> + ) } + + ); +}; + +const PurchaseTaskItemFill = () => { return ( - { ( { defaultTaskItem: DefaultTaskItem } ) => ( - <> - { - if ( remainingProducts.length ) { - toggleCartModal(); - } - } } - /> - { cartModalOpen && ( - toggleCartModal() } - onClickPurchaseLater={ () => toggleCartModal() } - /> - ) } - + { ( { defaultTaskItem } ) => ( + ) } ); @@ -73,5 +80,5 @@ const PurchaseTaskItem = () => { registerPlugin( 'woocommerce-admin-task-purchase', { scope: 'woocommerce-tasks', - render: PurchaseTaskItem, + render: PurchaseTaskItemFill, } ); diff --git a/plugins/woocommerce-admin/client/tasks/reminder-bar/reminder-bar.tsx b/plugins/woocommerce-admin/client/tasks/reminder-bar/reminder-bar.tsx index 6eb2e071b0b..ea8ccd4c771 100644 --- a/plugins/woocommerce-admin/client/tasks/reminder-bar/reminder-bar.tsx +++ b/plugins/woocommerce-admin/client/tasks/reminder-bar/reminder-bar.tsx @@ -14,6 +14,7 @@ import { getAdminLink } from '@woocommerce/settings'; import { close as closeIcon } from '@wordpress/icons'; import interpolateComponents from '@automattic/interpolate-components'; import { useEffect } from '@wordpress/element'; +import { getQuery } from '@woocommerce/navigation'; /** * Internal dependencies @@ -27,7 +28,7 @@ type ReminderBarProps = { }; type ReminderTextProps = { - remainingCount: number; + remainingCount: number | null; }; const REMINDER_BAR_HIDDEN_OPTION = 'woocommerce_task_list_reminder_bar_hidden'; @@ -66,7 +67,6 @@ const ReminderText: React.FC< ReminderTextProps > = ( { remainingCount } ) => { export const TasksReminderBar: React.FC< ReminderBarProps > = ( { taskListId = 'setup_experiment_1', - pageTitle, updateBodyMargin, } ) => { const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); @@ -119,13 +119,18 @@ export const TasksReminderBar: React.FC< ReminderBarProps > = ( { }; } ); + const isHomescreen = + getQuery().page && getQuery().page === 'wc-admin' && ! getQuery().path; + const isActiveTaskPage = Boolean( getQuery().wc_onboarding_active_task ); + const hideReminderBar = loading || taskListHidden || taskListComplete || reminderBarHidden || completedTasksCount === 0 || - [ 'Home', 'Shipping', 'Tax', 'Payments' ].includes( pageTitle ); + isHomescreen || + isActiveTaskPage; useEffect( () => { updateBodyMargin(); diff --git a/plugins/woocommerce-admin/client/tasks/task-list.tsx b/plugins/woocommerce-admin/client/tasks/task-list.tsx index 43601642724..25168c76142 100644 --- a/plugins/woocommerce-admin/client/tasks/task-list.tsx +++ b/plugins/woocommerce-admin/client/tasks/task-list.tsx @@ -6,7 +6,11 @@ import { useEffect, useRef, useState } from '@wordpress/element'; import { Card, CardHeader } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { Badge } from '@woocommerce/components'; -import { ONBOARDING_STORE_NAME, TaskListType } from '@woocommerce/data'; +import { + getVisibleTasks, + ONBOARDING_STORE_NAME, + TaskListType, +} from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; import { Text, List, CollapsibleList } from '@woocommerce/experimental'; @@ -45,12 +49,7 @@ export const TaskList: React.FC< TaskListProps > = ( { }; } ); const prevQueryRef = useRef( query ); - const nowTimestamp = Date.now(); - const visibleTasks = tasks.filter( - ( task ) => - ! task.isDismissed && - ( ! task.isSnoozed || task.snoozedUntil < nowTimestamp ) - ); + const visibleTasks = getVisibleTasks( tasks ); const incompleteTasks = tasks.filter( ( task ) => ! task.isComplete && ! task.isDismissed diff --git a/plugins/woocommerce-admin/client/tasks/tasks.scss b/plugins/woocommerce-admin/client/tasks/tasks.scss index 07719e15b8a..1ba8c76f427 100644 --- a/plugins/woocommerce-admin/client/tasks/tasks.scss +++ b/plugins/woocommerce-admin/client/tasks/tasks.scss @@ -236,7 +236,7 @@ } .woocommerce-task-list__setup_experiment_1 { - .woocommerce-experimental-list .woocommerce-experimental-list__item.complete { + .woocommerce-experimental-list .woocommerce-experimental-list__item.is-complete { text-decoration: line-through; .woocommerce-task-list__item-title { diff --git a/plugins/woocommerce-admin/client/tasks/tasks.tsx b/plugins/woocommerce-admin/client/tasks/tasks.tsx index 8e0e61be921..552693006c0 100644 --- a/plugins/woocommerce-admin/client/tasks/tasks.tsx +++ b/plugins/woocommerce-admin/client/tasks/tasks.tsx @@ -4,7 +4,7 @@ import { __ } from '@wordpress/i18n'; import { MenuGroup, MenuItem } from '@wordpress/components'; import { check } from '@wordpress/icons'; -import { Fragment, useEffect, lazy, Suspense } from '@wordpress/element'; +import { Fragment, useEffect } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { ONBOARDING_STORE_NAME, OPTIONS_STORE_NAME } from '@woocommerce/data'; import { useExperiment } from '@woocommerce/explat'; @@ -15,37 +15,44 @@ import { recordEvent } from '@woocommerce/tracks'; */ import { DisplayOption } from '~/activity-panel/display-options'; import { Task } from './task'; -import { TasksPlaceholder } from './placeholder'; +import { TasksPlaceholder, TasksPlaceholderProps } from './placeholder'; import './tasks.scss'; -import { TaskListProps } from './task-list'; +import { TaskListProps, TaskList } from './task-list'; +import { TaskList as TwoColumnTaskList } from '../two-column-tasks/task-list'; +import { SectionedTaskList } from '../two-column-tasks/sectioned-task-list'; +import TwoColumnTaskListPlaceholder from '../two-column-tasks/placeholder'; import '../two-column-tasks/style.scss'; +import { getAdminSetting } from '~/utils/admin-settings'; +import { SectionedTaskListPlaceholder } from '~/two-column-tasks/sectioned-task-list-placeholder'; export type TasksProps = { query: { task?: string }; }; -const TaskList = lazy( - () => import( /* webpackChunkName: "task-list" */ './task-list' ) -); - -const TwoColumnTaskList = lazy( - () => - import( - /* webpackChunkName: "two-column-task-list" */ '../two-column-tasks/task-list' - ) -); - -function getTaskListComponent( - taskListId: string -): React.LazyExoticComponent< React.FC< TaskListProps > > { +function getTaskListComponent( taskListId: string ): React.FC< TaskListProps > { switch ( taskListId ) { case 'setup_experiment_1': return TwoColumnTaskList; + case 'setup_experiment_2': + return SectionedTaskList; default: return TaskList; } } +function getTaskListPlaceholderComponent( + taskListId: string +): React.FC< TasksPlaceholderProps > { + switch ( taskListId ) { + case 'setup_experiment_1': + return TwoColumnTaskListPlaceholder; + case 'setup_experiment_2': + return SectionedTaskListPlaceholder; + default: + return TasksPlaceholder; + } +} + export const Tasks: React.FC< TasksProps > = ( { query } ) => { const { task } = query; const { hideTaskList } = useDispatch( ONBOARDING_STORE_NAME ); @@ -56,9 +63,9 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => { const { isResolving, taskLists } = useSelect( ( select ) => { return { - isResolving: select( ONBOARDING_STORE_NAME ).isResolving( - 'getTaskLists' - ), + isResolving: ! select( + ONBOARDING_STORE_NAME + ).hasFinishedResolution( 'getTaskLists' ), taskLists: select( ONBOARDING_STORE_NAME ).getTaskLists(), }; } ); @@ -108,8 +115,13 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => { return null; } + const taskListIds = getAdminSetting( 'visibleTaskListIds', [] ); + const TaskListPlaceholderComponent = getTaskListPlaceholderComponent( + taskListIds[ 0 ] + ); + if ( isResolving ) { - return ; + return ; } if ( currentTask ) { @@ -121,7 +133,7 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => { } if ( isLoadingExperiment ) { - return ; + return ; } return taskLists @@ -131,17 +143,7 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => { : ! id.endsWith( 'two_column' ) ) .map( ( taskList ) => { - const { - id, - eventPrefix, - isComplete, - isHidden, - isVisible, - isToggleable, - title, - tasks, - displayProgressHeader, - } = taskList; + const { id, isHidden, isVisible, isToggleable } = taskList; if ( ! isVisible ) { return null; @@ -151,22 +153,14 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => { return ( - - - + { isToggleable && ( { jest.mock( '@woocommerce/explat' ); jest.mock( '@woocommerce/tracks' ); -jest.mock( '../task-list', () => ( { id } ) =>
task-list:{ id }
); -jest.mock( '../../two-column-tasks/task-list', () => ( { id } ) => ( -
two-column-list:{ id }
-) ); +jest.mock( '../task-list', () => ( { + TaskList: ( { id } ) =>
task-list:{ id }
, +} ) ); + +jest.mock( '../../two-column-tasks/task-list', () => ( { + TaskList: ( { id } ) =>
two-column-list:{ id }
, +} ) ); jest.mock( '../task', () => ( { Task: ( { query } ) =>
task:{ query.task }
, diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss new file mode 100644 index 00000000000..067d50e0266 --- /dev/null +++ b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss @@ -0,0 +1,26 @@ +.woocommerce-task-section-header__container { + display: flex; + + .woocommerce-task-header__illustration { + max-width: 150px; + width: 34%; + margin-left: auto; + margin-right: 7%; + display: flex; + align-items: center; + + .illustration-background { + max-width: 100%; + } + } + + .woocommerce-task-header__contents p { + font-size: 16px; + } + + .woocommerce-task-header__contents h1 { + font-size: 20px; + line-height: 28px; + padding: 0; + } +} diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx new file mode 100644 index 00000000000..c84a892084e --- /dev/null +++ b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx @@ -0,0 +1,30 @@ +/** + * Internal dependencies + */ +import './section-header.scss'; + +type Props = { + title: string; + description: string; + image: string; +}; + +const SectionHeader: React.FC< Props > = ( { title, description, image } ) => { + return ( +
+
+

{ title }

+

{ description }

+
+
+ { +
+
+ ); +}; + +export default SectionHeader; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/index.js b/plugins/woocommerce-admin/client/two-column-tasks/index.js index 11288fe0d30..d09c973f864 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/index.js +++ b/plugins/woocommerce-admin/client/two-column-tasks/index.js @@ -3,7 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; -import { ONBOARDING_STORE_NAME, OPTIONS_STORE_NAME } from '@woocommerce/data'; +import { ONBOARDING_STORE_NAME } from '@woocommerce/data'; /** * Internal dependencies @@ -14,25 +14,7 @@ import TaskList from './task-list'; import TaskListPlaceholder from './placeholder'; import { Task } from '../tasks/task'; -const taskDashboardSelect = ( select ) => { - const { getOption, hasFinishedResolution } = select( OPTIONS_STORE_NAME ); - - return { - keepCompletedTaskList: getOption( - 'woocommerce_task_list_keep_completed' - ), - isResolving: ! hasFinishedResolution( 'getOption', [ - 'woocommerce_task_list_keep_completed', - ] ), - }; -}; - const TaskDashboard = ( { query, twoColumns } ) => { - const { - keepCompletedTaskList, - isResolving: isResolvingOptions, - } = useSelect( taskDashboardSelect ); - const { task } = query; const { isResolving, taskLists } = useSelect( ( select ) => { @@ -72,7 +54,7 @@ const TaskDashboard = ( { query, twoColumns } ) => { return null; } - if ( isResolving || isResolvingOptions || ! taskLists[ 0 ] ) { + if ( isResolving || ! taskLists[ 0 ] ) { return ; } @@ -104,7 +86,7 @@ const TaskDashboard = ( { query, twoColumns } ) => { id={ taskList.id } eventName="tasklist" twoColumns={ twoColumns } - keepCompletedTaskList={ keepCompletedTaskList } + keepCompletedTaskList={ taskList.keepCompletedTaskList } dismissedTasks={ dismissedTasks || [] } isComplete={ isTaskListComplete } query={ query } diff --git a/plugins/woocommerce-admin/client/two-column-tasks/placeholder.js b/plugins/woocommerce-admin/client/two-column-tasks/placeholder.tsx similarity index 87% rename from plugins/woocommerce-admin/client/two-column-tasks/placeholder.js rename to plugins/woocommerce-admin/client/two-column-tasks/placeholder.tsx index 70358aee2ca..2cbea8720db 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/placeholder.js +++ b/plugins/woocommerce-admin/client/two-column-tasks/placeholder.tsx @@ -8,7 +8,15 @@ import classnames from 'classnames'; */ import './style.scss'; -const TaskListPlaceholder = ( props ) => { +type TasksPlaceholderProps = { + numTasks?: number; + twoColumns?: boolean; + query: { + task?: string; + }; +}; + +const TaskListPlaceholder: React.FC< TasksPlaceholderProps > = ( props ) => { const { numTasks = 5, twoColumns = false } = props; return ( diff --git a/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx b/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx new file mode 100644 index 00000000000..54c0b8dec77 --- /dev/null +++ b/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { Badge } from '@woocommerce/components'; +import { TaskListSection, TaskType } from '@woocommerce/data'; +import { Icon, check } from '@wordpress/icons'; +import { Text } from '@woocommerce/experimental'; + +/** + * Internal dependencies + */ +import SectionHeader from './headers/section-header'; + +type SectionPanelTitleProps = { + section: TaskListSection; + active: boolean; + tasks: TaskType[]; +}; + +export const SectionPanelTitle: React.FC< SectionPanelTitleProps > = ( { + section, + active, + tasks, +} ) => { + if ( active ) { + return ( +
+
+ +
+
+ ); + } + + const uncompletedTasksCount = tasks.filter( + ( task ) => ! task.isComplete && section.tasks.includes( task.id ) + ).length; + const isComplete = section.isComplete || uncompletedTasksCount === 0; + + return ( + <> + + { section.title } + + { ! isComplete && } + { isComplete && ( +
+ +
+ ) } + + ); +}; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list-placeholder.tsx b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list-placeholder.tsx new file mode 100644 index 00000000000..70b234eaf0a --- /dev/null +++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list-placeholder.tsx @@ -0,0 +1,75 @@ +/** + * Internal dependencies + */ +import './style.scss'; + +type TasksPlaceholderProps = { + numTasks?: number; + query: { + task?: string; + }; +}; + +const SectionedTaskListPlaceholder: React.FC< TasksPlaceholderProps > = ( + props +) => { + const { numTasks = 3 } = props; + + return ( +
+
+
+
+
+
+
+
    + { Array.from( new Array( numTasks ) ).map( ( v, i ) => ( +
  • +
    +
    +
    +
    +
    +
    +
  • + ) ) } +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +}; + +export { SectionedTaskListPlaceholder }; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss new file mode 100644 index 00000000000..d9e379ad043 --- /dev/null +++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss @@ -0,0 +1,121 @@ +.woocommerce-sectioned-task-list { + .components-panel { + width: 100%; + background: transparent; + border: 0; + } + + .components-panel__body { + padding-bottom: 0; + margin-bottom: $gap-smaller; + background: #fff; + border: 1px solid $gray-200; + + &.is-opened { + padding-bottom: 0; + } + + .components-panel__body-title { + margin-bottom: 0; + border-bottom: 1px solid #e0e0e0; + + &:hover { + border-bottom: 1px solid #e0e0e0; + } + + > .components-button { + font-size: 20px; + font-weight: 400; + padding-top: 20px; + padding-bottom: 20px; + } + + .components-panel__arrow { + right: $gap-large; + } + } + .wooocommerce-task-card__header-container { + width: 100%; + border-bottom: none; + } + .components-panel__body-toggle { + box-shadow: none; + padding-left: $gap-large; + } + &.is-opened .components-panel__body-toggle { + width: 100%; + padding: 0; + .components-panel__arrow { + top: 32px; + } + } + + .woocommerce-experimental-list { + width: calc(100% + 32px); + margin: 0 -16px; + } + } + ul li.woocommerce-task-list__item { + padding-top: $gap; + padding-bottom: $gap; + + &.is-disabled { + pointer-events: none; + } + + &:not(.is-complete) + .woocommerce-task-list__item-before + .woocommerce-task__icon { + border-color: $gray-300; + } + } + + .woocommerce-task-list__item.is-complete .woocommerce-task__icon { + background-color: $alert-green; + } + + .components-panel__body-title { + .woocommerce-badge { + width: 28px; + height: 28px; + } + .woocommerce-task__icon { + margin-left: $gap; + background-color: $alert-green; + border-radius: 50%; + width: 24px; + height: 24px; + svg { + fill: #fff; + position: relative; + } + } + } + + > .is-loading { + border: none; + margin-bottom: 8px; + + .woocommerce-task-list__item .woocommerce-task-list__item-before { + padding: 0 0 0 $gap-large; + } + + &.components-panel__body .components-panel__body-title .woocommerce-task-list__item-text { + width: 50%; + + .is-placeholder { + width: 100%; + } + } + + &.components-panel__body .woocommerce-task-list__item-after { + margin-left: $gap; + + .is-placeholder { + height: 24px; + width: 24px; + border-radius: 50%; + } + } + } +} diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx new file mode 100644 index 00000000000..2703c84d0cf --- /dev/null +++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx @@ -0,0 +1,276 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useEffect, useRef, useState } from '@wordpress/element'; +import { Panel, PanelBody, PanelRow } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { updateQueryString } from '@woocommerce/navigation'; +import { + OPTIONS_STORE_NAME, + ONBOARDING_STORE_NAME, + TaskType, + getVisibleTasks, +} from '@woocommerce/data'; +import { recordEvent } from '@woocommerce/tracks'; +import { List, TaskItem } from '@woocommerce/experimental'; +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import '../tasks/task-list.scss'; +import './sectioned-task-list.scss'; +import TaskListCompleted from './completed'; +import { TaskListProps } from '~/tasks/task-list'; +import { ProgressHeader } from '~/task-lists/progress-header'; +import { SectionPanelTitle } from './section-panel-title'; + +type PanelBodyProps = Omit< PanelBody.Props, 'title' | 'onToggle' > & { + title: string | React.ReactNode | undefined; + onToggle?: ( isOpen: boolean ) => void; +}; +const PanelBodyWithUpdatedType = PanelBody as React.ComponentType< PanelBodyProps >; + +export const SectionedTaskList: React.FC< TaskListProps > = ( { + query, + id, + eventPrefix, + tasks, + keepCompletedTaskList, + isComplete, + sections, + displayProgressHeader, +} ) => { + const { createNotice } = useDispatch( 'core/notices' ); + const { updateOptions, dismissTask, undoDismissTask } = useDispatch( + OPTIONS_STORE_NAME + ); + const { profileItems } = useSelect( ( select ) => { + const { getProfileItems } = select( ONBOARDING_STORE_NAME ); + return { + profileItems: getProfileItems(), + }; + } ); + const { hideTaskList } = useDispatch( ONBOARDING_STORE_NAME ); + const [ openPanel, setOpenPanel ] = useState< string | null >( + sections?.find( ( section ) => ! section.isComplete )?.id || null + ); + + const prevQueryRef = useRef( query ); + + const visibleTasks = getVisibleTasks( tasks ); + + const recordTaskListView = () => { + if ( query.task ) { + return; + } + + recordEvent( `${ eventPrefix }view`, { + number_tasks: visibleTasks.length, + store_connected: profileItems.wccom_connected, + } ); + }; + + useEffect( () => { + recordTaskListView(); + }, [] ); + + useEffect( () => { + const { task: prevTask } = prevQueryRef.current; + const { task } = query; + + if ( prevTask !== task ) { + window.document.documentElement.scrollTop = 0; + prevQueryRef.current = query; + } + }, [ query ] ); + + const onDismissTask = ( taskId: string ) => { + dismissTask( taskId ); + createNotice( 'success', __( 'Task dismissed' ), { + actions: [ + { + label: __( 'Undo', 'woocommerce-admin' ), + onClick: () => undoDismissTask( taskId ), + }, + ], + } ); + }; + + const hideTasks = () => { + hideTaskList( id ); + }; + + const keepTasks = () => { + const updateOptionsParams = { + woocommerce_task_list_keep_completed: 'yes', + }; + + updateOptions( { + ...updateOptionsParams, + } ); + }; + + let selectedHeaderCard = visibleTasks.find( + ( listTask ) => listTask.isComplete === false + ); + + // If nothing is selected, default to the last task since everything is completed. + if ( ! selectedHeaderCard ) { + selectedHeaderCard = visibleTasks[ visibleTasks.length - 1 ]; + } + + const trackClick = ( task: TaskType ) => { + recordEvent( `${ eventPrefix }_click`, { + task_name: task.id, + } ); + }; + + const goToTask = ( task: TaskType ) => { + trackClick( task ); + updateQueryString( { task: task.id } ); + }; + + const onTaskSelected = ( task: TaskType ) => { + goToTask( task ); + }; + + const getSectionTasks = ( sectionTaskIds: string[] ) => { + return visibleTasks.filter( ( task ) => + sectionTaskIds.includes( task.id ) + ); + }; + + if ( ! visibleTasks.length ) { + return
; + } + + if ( isComplete && ! keepCompletedTaskList ) { + return ( + <> + + + ); + } + + return ( + <> + { displayProgressHeader ? ( + + ) : null } +
+ + { ( sections || [] ).map( ( section ) => ( + + } + opened={ openPanel === section.id } + onToggle={ ( isOpen: boolean ) => { + if ( ! isOpen && openPanel === section.id ) { + recordEvent( + `${ eventPrefix }section_closed`, + { + id: section.id, + all: true, + } + ); + setOpenPanel( null ); + } else { + if ( openPanel ) { + recordEvent( + `${ eventPrefix }section_closed`, + { + id: openPanel, + all: false, + } + ); + } + setOpenPanel( section.id ); + } + if ( isOpen ) { + recordEvent( + `${ eventPrefix }section_opened`, + { + id: section.id, + } + ); + } + } } + initialOpen={ false } + > + + + { getSectionTasks( section.tasks ).map( + ( task ) => { + const className = classnames( + 'woocommerce-task-list__item', + { + 'is-complete': + task.isComplete, + 'is-disabled': + task.isDisabled, + } + ); + return ( + { + if ( + ! task.isDisabled + ) { + onTaskSelected( + task + ); + } + } } + onDismiss={ + task.isDismissable + ? () => + onDismissTask( + task.id + ) + : undefined + } + action={ () => {} } + actionLabel={ + task.actionLabel + } + /> + ); + } + ) } + + + + ) ) } + +
+ + ); +}; + +export default SectionedTaskList; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/style.scss b/plugins/woocommerce-admin/client/two-column-tasks/style.scss index bf3a58ac0db..16aa121bdcd 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/style.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/style.scss @@ -19,10 +19,10 @@ flex: 1; } - .woocommerce-ellipsis-menu.setup { + .woocommerce-ellipsis-menu { position: absolute; - top: 20px; - right: 16px; + top: $gap; + right: $gap-large; } .woocommerce-task-card.is-loading { @@ -103,7 +103,7 @@ margin: 0 auto; justify-content: space-between; - ul li.complete .woocommerce-task-list__item-title { + ul li.is-complete .woocommerce-task-list__item-title { font-weight: 600; color: $gray-600; } @@ -130,11 +130,11 @@ } .woocommerce-task-header__contents { - max-width: 380px; + max-width: calc(60% - 2%); } .svg-background { - right: 0.5%; + right: 2%; width: 40%; } } @@ -143,6 +143,13 @@ @include single-column; } + &.two-columns .svg-background { + top: 50%; + bottom: 50%; + margin-top: auto; + margin-bottom: auto; + } + ul { display: flex; li { @@ -171,10 +178,13 @@ height: 100%; } } - .woocommerce-task-list__item:not(.complete) .woocommerce-task__icon { + .woocommerce-task-list__item:not(.is-complete) .woocommerce-task__icon { border: 1px solid var(--wp-admin-theme-color); background: transparent; } + .woocommerce-task-list__item.is-complete:not(.complete) .woocommerce-task__icon { + border: none; + } .woocommerce-task-list__item-before { display: block; @@ -196,7 +206,7 @@ } @for $i from 1 through 10 { - .woocommerce-task-list__item:not(.complete).index-#{$i} .woocommerce-task__icon::after { + .woocommerce-task-list__item:not(.is-complete).index-#{$i} .woocommerce-task__icon::after { content: '#{$i}'; @extend .numbered-circle; color: var(--wp-admin-theme-color); @@ -229,10 +239,6 @@ position: absolute; z-index: 0; right: 6%; - top: 50%; - bottom: 50%; - margin-top: auto; - margin-bottom: auto; .admin-theme-color { fill: var(--wp-admin-theme-color); diff --git a/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx b/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx index 0bb00f0751a..9f9c4f468e2 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx @@ -16,6 +16,7 @@ import { ONBOARDING_STORE_NAME, TaskType, useUserPreferences, + getVisibleTasks, } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; import { List, TaskItem } from '@woocommerce/experimental'; @@ -63,12 +64,7 @@ export const TaskList: React.FC< TaskListProps > = ( { const prevQueryRef = useRef( query ); - const nowTimestamp = Date.now(); - const visibleTasks = tasks.filter( - ( task ) => - ! task.isDismissed && - ( ! task.isSnoozed || task.snoozedUntil < nowTimestamp ) - ); + const visibleTasks = getVisibleTasks( tasks ); const recordTaskListView = () => { if ( query.task ) { @@ -295,7 +291,7 @@ export const TaskList: React.FC< TaskListProps > = ( { const className = classnames( 'woocommerce-task-list__item index-' + index, { - complete: task.isComplete, + 'is-complete': task.isComplete, 'is-active': task.id === activeTaskId, } ); diff --git a/plugins/woocommerce-admin/config/plugin.json b/plugins/woocommerce-admin/config/plugin.json deleted file mode 100644 index cf262718b37..00000000000 --- a/plugins/woocommerce-admin/config/plugin.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "features": { - "activity-panels": true, - "analytics": true, - "coupons": true, - "customer-effort-score-tracks": true, - "homescreen": true, - "marketing": true, - "minified-js": true, - "mobile-app-banner": true, - "navigation": true, - "onboarding": true, - "onboarding-tasks": true, - "remote-inbox-notifications": true, - "remote-free-extensions": true, - "payment-gateway-suggestions": true, - "settings": false, - "shipping-label-banner": true, - "subscriptions": true, - "store-alerts": true, - "transient-notices": true, - "wc-pay-promotion": true, - "wc-pay-welcome-page": true, - "tasklist-setup-experiment-1": false - } -} diff --git a/plugins/woocommerce-admin/docs/.nojekyll b/plugins/woocommerce-admin/docs/.nojekyll deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/plugins/woocommerce-admin/docs/README.md b/plugins/woocommerce-admin/docs/README.md index 31fb6545f6e..2a2bb84447e 100644 --- a/plugins/woocommerce-admin/docs/README.md +++ b/plugins/woocommerce-admin/docs/README.md @@ -1,35 +1,9 @@ # WooCommerce Admin -This is a feature plugin for a modern, javascript-driven WooCommerce Admin experience. - -## Prerequisites - -[WordPress 5.4 or greater](https://wordpress.org/download/) and [WooCommerce 4.8.0 or greater](https://wordpress.org/plugins/woocommerce/) should be installed prior to activating the WooCommerce Admin feature plugin. - -For better debugging, it's also recommended you add `define( 'SCRIPT_DEBUG', true );` to your wp-config. This will load the unminified version of all libraries, and specifically the development build of React. - -## Development - -After cloning the repo, install dependencies: - -- `pnpm install` to install JavaScript dependencies. -- `composer install` to gather PHP dependencies. - -Now you can build the files using one of these commands: - -- `pnpm run build` : Build a production version -- `pnpm run dev` : Build a development version -- `pnpm start` : Build a development version, watch files for changes -- `pnpm run build:release` : Build a WordPress plugin ZIP file (`woocommerce-admin.zip` will be created in the repository root) - -For more helper scripts [see here](./CONTRIBUTING.md#helper-scripts) - -For some debugging tools/help [see here](./CONTRIBUTING.md#debugging) - -## Privacy - -If you have enabled WooCommerce usage tracking ( option `woocommerce_allow_tracking` ) then, in addition to the tracking described in https://woocommerce.com/usage-tracking/, this plugin also sends information about the actions that site administrators perform to Automattic - see https://automattic.com/privacy/#information-we-collect-automatically for more information. - -## Contributing - -There are many ways to contribute ā€“ reporting bugs, adding translations, feature suggestions and fixing bugs. For full details, please see [CONTRIBUTING.md](./CONTRIBUTING.md) +- [CSS Structure](stylesheets.md) +- [Data](data.md) +- [Examples](examples/) +- [Features](features/) +- [Layout](layout.md) +- [Page Controller](page-controller.md) +- [woocommerce.com](woocommerce.com/) diff --git a/plugins/woocommerce-admin/docs/_sidebar.md b/plugins/woocommerce-admin/docs/_sidebar.md deleted file mode 100644 index 64494ed4f0a..00000000000 --- a/plugins/woocommerce-admin/docs/_sidebar.md +++ /dev/null @@ -1,7 +0,0 @@ -- [Overview](/) -- [Components](components/) -- [Features](features/) -- [Data](data) -- [Layout](layout) -- [CSS Structure](stylesheets) -- [Examples](examples/) diff --git a/plugins/woocommerce-admin/docs/components/README.md b/plugins/woocommerce-admin/docs/components/README.md deleted file mode 100644 index 2142fa90cf8..00000000000 --- a/plugins/woocommerce-admin/docs/components/README.md +++ /dev/null @@ -1 +0,0 @@ -[storybook](storybook/index.html ':include :type=iframe width=100% height=100%') diff --git a/plugins/woocommerce-admin/docs/data.md b/plugins/woocommerce-admin/docs/data.md index ec1061ddabe..98600969167 100644 --- a/plugins/woocommerce-admin/docs/data.md +++ b/plugins/woocommerce-admin/docs/data.md @@ -1,7 +1,7 @@ Data ==== -WooCommerce Admin data stores implement the [`SqlQuery` class](https://github.com/woocommerce/woocommerce-admin/blob/main/src/API/Reports/SqlQuery.php). +WooCommerce Admin data stores implement the [`SqlQuery` class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/API/Reports/SqlQuery.php). ### SqlQuery Class @@ -81,4 +81,4 @@ function my_custom_product_stats( $clauses ) { $clauses[] = ', SUM( sample_column ) as sample_total'; return $clauses; } -``` \ No newline at end of file +``` diff --git a/plugins/woocommerce-admin/docs/examples/_sidebar.md b/plugins/woocommerce-admin/docs/examples/_sidebar.md deleted file mode 100644 index f4b7a7ef134..00000000000 --- a/plugins/woocommerce-admin/docs/examples/_sidebar.md +++ /dev/null @@ -1,5 +0,0 @@ -* [Home](/) - -* [Examples](examples/) - - * [Activity Panel Inbox](examples/activity-panel-inbox.md) diff --git a/plugins/woocommerce-admin/docs/examples/extensions/README.md b/plugins/woocommerce-admin/docs/examples/extensions/README.md index b74cceac443..b4d39518ec7 100644 --- a/plugins/woocommerce-admin/docs/examples/extensions/README.md +++ b/plugins/woocommerce-admin/docs/examples/extensions/README.md @@ -13,10 +13,10 @@ pnpm install Build the example extension by running the pnpm script and passing the example name. ```bash -pnpm run example -- --ext= +WC_EXT= pnpm nx example woocommerce-admin ``` -Go to your WordPress installation's plugins page and activate the plugin. WooCommerce Analytics reports will now reflect the changes made by the example extension. +Include the output plugin in your `.wp-env.json` and `.wp-env.override.json` and restart the WordPress instance. WooCommerce Analytics reports will now reflect the changes made by the example extension. You can make changes to Javascript and PHP files in the example and see changes reflected upon refresh. diff --git a/plugins/woocommerce-admin/docs/examples/extensions/examples.config.js b/plugins/woocommerce-admin/docs/examples/extensions/examples.config.js index f5527356bd4..36ff57ad22a 100644 --- a/plugins/woocommerce-admin/docs/examples/extensions/examples.config.js +++ b/plugins/woocommerce-admin/docs/examples/extensions/examples.config.js @@ -10,13 +10,11 @@ const woocommerceAdminConfig = require( path.resolve( ) ); const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); -const extArg = process.argv.find( ( arg ) => arg.startsWith( '--ext=' ) ); - -if ( ! extArg ) { +if ( ! process.env.WC_EXT ) { throw new Error( 'Please provide an extension.' ); } -const extension = extArg.slice( 6 ); +const extension = process.env.WC_EXT; const extensionPath = path.join( __dirname, `${ extension }/js/index.js` ); if ( ! fs.existsSync( extensionPath ) ) { @@ -33,7 +31,7 @@ const webpackConfig = { output: { filename: '[name]/dist/index.js', path: path.resolve( __dirname ), - libraryTarget: 'this', + libraryTarget: 'window', }, externals: woocommerceAdminConfig.externals, module: { @@ -89,12 +87,17 @@ const webpackConfig = { }, }, plugins: [ - new CopyWebpackPlugin( [ - { - from: path.join( __dirname, `${ extension }/` ), - to: path.resolve( __dirname, `../../../../${ extension }/` ), - }, - ] ), + new CopyWebpackPlugin( { + patterns: [ + { + from: path.join( __dirname, `${ extension }/` ), + to: path.resolve( + __dirname, + `../../../../${ extension }/` + ), + }, + ], + } ), new MiniCssExtractPlugin( { filename: '[name]/dist/style.css', } ), diff --git a/plugins/woocommerce-admin/docs/features/_sidebar.md b/plugins/woocommerce-admin/docs/features/_sidebar.md deleted file mode 100644 index 125acd6b675..00000000000 --- a/plugins/woocommerce-admin/docs/features/_sidebar.md +++ /dev/null @@ -1,6 +0,0 @@ -* [Home](/) - -* [Features](features/) - - * [Feature Flags](features/feature-flags.md) - * [Onboarding](features/onboarding.md) diff --git a/plugins/woocommerce-admin/docs/features/feature-flags.md b/plugins/woocommerce-admin/docs/features/feature-flags.md index 5b1a98cac19..96082791103 100644 --- a/plugins/woocommerce-admin/docs/features/feature-flags.md +++ b/plugins/woocommerce-admin/docs/features/feature-flags.md @@ -1,14 +1,14 @@ # Feature Flags -Features inside the `woocommerce-admin` repository can be in various states of completeness. In addition to the development copy of `woocommerce-admin`, feature plugin versions are bundled, and code is merged to WooCommerce core. To provide a way for improved control over how these features are released in these different environments, `woocommerce-admin` has a system for feature flags. +Features inside the `woocommerce` repository can be in various states of completeness. To provide a way for improved control over how these features are released in these different environments, `woocommerce` has a system for feature flags. We currently support the following environments: | Environment | Description | |-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| development | Development - All features should be enabled in development. These flags are also used in both JS and PHP tests. Ran using `pnpm start`. | -| plugin | Plugin - A packaged release of the featured plugin, for GitHub WordPress.org. Ran using `pnpm run-script build:release`. | | -| core | Core - assets/files ready and stable enough for core merge. Ran using `pnpm pack`. (@todo update this with publish command). +| development | Development - All features should be enabled in development. These flags are also used in both JS and PHP tests. Ran using `pnpm start`. | | +| core | Core - assets/files ready and stable enough. Ran using `pnpm build` & `pnpm pack`. + ## Adding a new flag @@ -16,16 +16,6 @@ We currently support the following environments: Flags can be added to the files located in the `config/` directory. Make sure to add a flag for each environment and explicitly set the flag to false. Please add new feature flags alphabetically so they are easy to find. -## Building custom plugin builds - -Sometimes it is useful to create a test zip of a plugin, separate from the released WordPress.org version. This makes internal testing easier for non developers, removing the requirment of using Git and NPM commands. These releases are usually uploaded to GitHub releases as a pre release. - -You can use the `build:release` command with the `--slug` and `--features` arguments to create a custom build. Base feature flags will be pulled from `config/plugin.json` and your additional changes are overlaid on top. When the build is complete, a `woocommerce-admin-$slug.zip` file will be generated. - -For example, to create a `woocommerce-admin-onboarding.zip` build by enabling onboarding in addition to the feature flags defined in `config/plugin.json`, you would run: - -`pnpm run build:release -- --slug onboarding --features '{"onboarding":true}'`. - ## Basic Use - Client The `window.wcAdminFeatures` constant is a global variable containing the feature flags. diff --git a/plugins/woocommerce-admin/docs/features/navigation.md b/plugins/woocommerce-admin/docs/features/navigation.md index eb73d1d9199..7ef025f0c31 100644 --- a/plugins/woocommerce-admin/docs/features/navigation.md +++ b/plugins/woocommerce-admin/docs/features/navigation.md @@ -8,9 +8,9 @@ This API will allow you to add in your own items to the navigation and register 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. Inside your `woocommerce-admin` directory, enter the following command: +The fastest way to get started is by creating an example plugin from WooCommerce Admin. Enter the following command: -`pnpm run example -- --ext=add-navigation-items` +`WC_EXT=add-navigation-items pnpm nx example woocommerce-admin` This will create a new plugin that covers various features of the navigation and helps to register some intial 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. diff --git a/plugins/woocommerce-admin/docs/features/onboarding.md b/plugins/woocommerce-admin/docs/features/onboarding.md index b097764c9ca..4328d8aaf74 100644 --- a/plugins/woocommerce-admin/docs/features/onboarding.md +++ b/plugins/woocommerce-admin/docs/features/onboarding.md @@ -35,7 +35,7 @@ To power the new onboarding flow client side, new REST API endpoints have been i * `woocommerce_admin_onboarding_plugins_whitelist` filters the list of plugins that can installed & activated via onboarding. This acts as a whitelist so only certain plugins can be used via the `/wc-admin/onboarding/profile/install` and `/wc-admin/onboarding/profile/activate` endpoints. * `woocommerce_admin_onboarding_themes` filters the themes displayed in the profile wizard. * `woocommerce_admin_onboarding_jetpack_connect_redirect_url` filters the Jetpack connection redirect URL outlined in the Jetpack connection section below. -* `woocommerce_admin_onboarding_task_list` filters the list of tasks on the task list dashboard. This allows extensions to add new tasks. See [the extension docs](https://github.com/woocommerce/woocommerce-admin/tree/42015d17a919e8f9e54ba75869c50b04b8dc9241/docs/examples/extensions) for an example of how to do this. +* `woocommerce_admin_onboarding_task_list` filters the list of tasks on the task list dashboard. This allows extensions to add new tasks. See [the extension docs](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-admin/docs/examples/extensions) for an example of how to do this. * `woocommerce_rest_onboarding_profile_collection_params` filters the collection parameters for requests to `/wc-admin/onboarding/profile`. * `woocommerce_rest_onboarding_profile_object_query` filters the query arguments for requests to `/wc-admin/onboarding/profile`. * `woocommerce_rest_onboarding_prepare_onboarding_profile` filters the response for requests to `/wc-admin/onboarding/profile`. @@ -48,7 +48,7 @@ A few new WordPress options have been introduced to store information and settin * `woocommerce_task_list_hidden_lists`. This option houses the task lists that have been hidden from view by the user. * `woocommerce_task_list_welcome_modal_dismissed`. This option is used to show a congratulations modal during the transition between the profile wizard and task list. -We also use existing options from WooCommerce Core or extensions like WooCommerce Shipping & Tax or Stripe. The list below may not be complete, as new tasks are introduced, but you can generally find usage of these by searching for the [getOptions selector](https://github.com/woocommerce/woocommerce-admin/search?q=getOptions&unscoped_q=getOptions). +We also use existing options from WooCommerce Core or extensions like WooCommerce Shipping & Tax or Stripe. The list below may not be complete, as new tasks are introduced, but you can generally find usage of these by searching for the [getOptions selector](https://github.com/woocommerce/woocommerce/search?q=getOptions&unscoped_q=getOptions). * `woocommerce_setup_jetpack_opted_in` and `wc_connect_options` are both used to control Jetpack's Terms of Service opt-in, which is necessary to set for a user during the connection process, so that they can use services like automated tax rates. * `woocommerce_allow_tracking` is used to control Tracks opt-in, allowing us to gather usage data from WooCommerce Admin and WooCommerce core. @@ -59,7 +59,7 @@ We also use existing options from WooCommerce Core or extensions like WooCommerc During the profile wizard, merchants can select paid product type extensions (like WooCommerce Memberships) or a paid theme. To make installation easier and to finish purchasing, it is necessary to make a [WooCommerce.com connection](https://woocommerce.com/document/managing-woocommerce-com-subscriptions/). We also prompt users to connect on the task list if they chose extensions in the profile wizard, but did not finish connecting. -To make the connection from the new onboarding experience possible, we build our own connection endpoints [/wc-admin/plugins/request-wccom-connect](https://github.com/woocommerce/woocommerce-admin/blob/61b771c2643c24334ea062ab3521073beaf50019/src/API/OnboardingPlugins.php#L298-L355) and [/wc-admin/plugins/finish-wccom-connect](https://github.com/woocommerce/woocommerce-admin/blob/61b771c2643c24334ea062ab3521073beaf50019/src/API/OnboardingPlugins.php#L357-L417). +To make the connection from the new onboarding experience possible, we build our own connection endpoints [/wc-admin/plugins/request-wccom-connect](https://github.com/woocommerce/woocommerce/blob/feba6a8dcd55d4f5c7edc05478369c76df082293/plugins/woocommerce/src/Admin/API/Plugins.php#L419-L476) and [/wc-admin/plugins/finish-wccom-connect](https://github.com/woocommerce/woocommerce/blob/feba6a8dcd55d4f5c7edc05478369c76df082293/plugins/woocommerce/src/Admin/API/Plugins.php#L478-L538). Both of these endpoints use WooCommerce Core's `WC_Helper_API` directly. The main difference with our connection (compared to the connection on the subscriptions page) is the addition of two additional query string parameters: @@ -72,7 +72,7 @@ To disconnect from WooCommerce.com, go to `WooCommerce > Extensions > WooCommerc Using Jetpack & WooCommerce Shipping & Tax allows us to offer additional features to new WooCommerce users as well as simplify parts of the setup process. For example, we can do automated tax calculations for certain countries, significantly simplifying the tax task. To make this work, the user needs to be connected to a WordPress.com account. This also means development and testing of these features needs to be done on a Jetpack connected site. Search the MGS & the Feld Guide for additional resources on testing Jetpack with local setups. -We have a special Jetpack connection flow designed specifically for WooCommerce onboarding, so that the user feels that they are connecting as part of a cohesive experience. To access this flow, we have a custom Jetpack connection endpoint [/wc-admin/plugins/connect-jetpack](https://github.com/woocommerce/woocommerce-admin/blob/61b771c2643c24334ea062ab3521073beaf50019/src/API/OnboardingPlugins.php#L273-L296). +We have a special Jetpack connection flow designed specifically for WooCommerce onboarding, so that the user feels that they are connecting as part of a cohesive experience. To access this flow, we have a custom Jetpack connection endpoint [/wc-admin/plugins/connect-jetpack](https://github.com/woocommerce/woocommerce/blob/feba6a8dcd55d4f5c7edc05478369c76df082293/plugins/woocommerce/src/Admin/API/Plugins.php#L395-L417). We use Jetpack's `build_connect_url` function directly, but add the following two query parameters: @@ -110,14 +110,3 @@ Logic for the Calypso flows are gated behind two separate [Calypso feature flags ### Testing If you are running the development version of WooCommerce Admin, and have [`WP_DEBUG`](https://codex.wordpress.org/WP_DEBUG) set to `true`, two Calypso connection buttons are displayed under the `WooCommerce > Settings > Help > Setup Wizard` menu, making it easier to access and test the flows. - -## Building the onboarding feature plugin - -The `onboarding` feature flag is enabled in the main WooCommerce Admin plugin build. That means the published version of the plugin on WordPress.org contains the onboarding feature, but it is visually off by default. See the "enable onboarding" section above. - -Sometimes, it may be necessary to generate a separate build of the plugin between public releases for internal testing or debugging. This can be done using the [building custom plugin builds](https://github.com/woocommerce/woocommerce-admin/blob/main/docs/feature-flags.md#building-custom-plugin-builds) feature of our build system. - -* Switch to the latest `main` branch and pull down any changes -* Run `pnpm run build:release -- --slug onboarding --features '{"onboarding":true}'` -* A special `woocommerce-admin-onboarding.zip` release will be generated, containing the latest onboarding code -* Make sure to follow the directions in the "enabling onboarding" section above to properly use the build diff --git a/plugins/woocommerce-admin/docs/features/payment-gateway-suggestions.md b/plugins/woocommerce-admin/docs/features/payment-gateway-suggestions.md index 54ad567064b..a81c112250e 100644 --- a/plugins/woocommerce-admin/docs/features/payment-gateway-suggestions.md +++ b/plugins/woocommerce-admin/docs/features/payment-gateway-suggestions.md @@ -8,9 +8,9 @@ After merchants click on a recommendation, plugins from this source will then wa Gateway suggestions are retreived from a REST API and can be added via a remote JSON data source or filtered with the `woocommerce_admin_payment_gateway_suggestion_specs` filter. -To quickly get started with an example plugin, run the following from your `woocommerce-admin` directory: +To quickly get started with an example plugin, run the following: -`pnpm run example -- --ext=payment-gateway-suggestions` +`WC_EXT=payment-gateway-suggestions pnpm nx example woocommerce-admin` This will create a new plugin that when activated will add two new gateway suggestions. The first is a simple gateway demonstrating how configuration fields can be pulled from the gateway class to create a configuration form. The second gateway shows a more customized approach via SlotFill. @@ -41,7 +41,7 @@ The data source schema defines the recommended payment gateways and required plu ] ``` -The specs use the [rule processor](https://github.com/woocommerce/woocommerce-admin/blob/main/src/RemoteInboxNotifications/README.md#rule) to determine if a gateway should be shown using the `is_visible` property. +The specs use the [rule processor](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/src/Admin/RemoteInboxNotifications#rule) to determine if a gateway should be shown using the `is_visible` property. ## Payment Gateway Configs @@ -64,13 +64,13 @@ By default, the client will generate a payment gateway setup form from the setti ### WooPaymentGatewayConfigure -To customize the configuration form used in the payment setup, you can use [WooPaymentGatewayConfigure](https://github.com/woocommerce/woocommerce-admin/tree/main/packages/onboarding/src/components/WooPaymentGatewayConfigure). +To customize the configuration form used in the payment setup, you can use [WooPaymentGatewayConfigure](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/onboarding/src/components/WooPaymentGatewayConfigure). This will leave the default gateway installation and stepper in place, but allow the form to be customized as needed. ### WooPaymentGatewaySetup -To completely override the stepper and default installation behavior, the gateway can be SlotFilled using [WooPaymentGatewaySetup](https://github.com/woocommerce/woocommerce-admin/tree/main/packages/onboarding/src/components/WooPaymentGatewaySetup). +To completely override the stepper and default installation behavior, the gateway can be SlotFilled using [WooPaymentGatewaySetup](https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/onboarding/src/components/WooPaymentGatewaySetup). ## Post install setup diff --git a/plugins/woocommerce-admin/docs/index.html b/plugins/woocommerce-admin/docs/index.html deleted file mode 100644 index bce714671e1..00000000000 --- a/plugins/woocommerce-admin/docs/index.html +++ /dev/null @@ -1,70 +0,0 @@ - - - - - WooCommerce Admin - - - - - - - - - - - - -
- - - - - - diff --git a/plugins/woocommerce-admin/docs/page-controller.md b/plugins/woocommerce-admin/docs/page-controller.md index 381db8bfb30..ce4fd1eb29b 100644 --- a/plugins/woocommerce-admin/docs/page-controller.md +++ b/plugins/woocommerce-admin/docs/page-controller.md @@ -181,6 +181,6 @@ addFilter( 'woocommerce_admin_pages_list', 'my-namespace', ( pages ) => { ### Further Reading -- Check out the [`PageController`](../src/PageController.php) class. -- See how we're [connecting existing WooCommerce pages](../includes/page-controller-functions.php). -- See how we're [registering Analytics Reports](../src/Features/Analytics.php). +- Check out the [`PageController`](../woocommerce/src/Admin/PageController.php) class. +- See how we're [connecting existing WooCommerce pages](../woocommerce/includes/react-admin/page-controller-functions.php). +- See how we're [registering Analytics Reports](../woocommerce/src/Internal/Admin/Analytics.php). diff --git a/plugins/woocommerce-admin/docs/stylesheets.md b/plugins/woocommerce-admin/docs/stylesheets.md index 496b6486837..cc8918c279f 100644 --- a/plugins/woocommerce-admin/docs/stylesheets.md +++ b/plugins/woocommerce-admin/docs/stylesheets.md @@ -8,65 +8,4 @@ ## Naming: Component classes -To avoid class name collisions between elements of the woo app and to the enclosing WordPress dashboard, class names **must** adhere to the following guidelines: - -Any default export of a folder's `index.js` **must** be prefixed with `woocommerce-` followed by the directory name in which it resides: - -``` -.woocommerce-[ directory name ] -``` - -(Example: `.woocommerce-card` from `components/card/index.js`) - -For any descendant of the top-level (`index.js`) element, prefix using the top-level element's class name separated by two underscores: - -``` -.woocommerce-[ directory name ]__[ descendant description ] -``` - -(Example: `.woocommerce-card__title`, or `.woocommerce-ellipsis-menu__item`) - -For optional variations of an element or its descendants, you may use a modifier class, but you **must not** apply styles to the modifier class directly; only as an additional selector to the element to which the modifier applies: - -``` -.woocommerce-[ directory name ].is-[ modifier description ] -.woocommerce-[ directory name ]__[ descendant description ].is-[ modifier description ] -``` - -(Example: `.woocommerce-ellipsis-menu__item.is-active` ) - -In all of the above cases, except in separating the top-level element from its descendants, you **must** use dash delimiters when expressing multiple terms of a name. You can use `.is-*` or `.has-*` to describe element states. - -You may observe that these conventions adhere closely to the [BEM (Blocks, Elements, Modifiers)](http://getbem.com/introduction/) CSS methodology, with minor adjustments to the application of modifiers. - -## Naming: Layout classes - -All layout classes use the `.woocommerce-layout__` prefix: - -``` -.woocommerce-layout__[ section ] -``` - -(Example: `.woocommerce-layout__activity-panel` ) - -If the section has children elements, prefix a description with the section class name: - -``` -.woocommerce-layout__[ section ]-[ descendant description ] -``` - -(Example: `.woocommerce-layout__activity-panel-title` ) - -## Naming: Dashboard classes - -All dashboard components use the `.woocommerce-dashboard__` prefix: - -``` -.woocommerce-dashboard__[ section ] -``` - -(Example: `.woocommerce-dashboard__widget` ) - -## Naming: Analytics classes - -All analytics components use the `.woocommerce-analytics__` prefix. +Please refer to [CSS SASS coding guidelines and naming conventions](https://github.com/woocommerce/woocommerce/wiki/CSS-SASS-coding-guidelines-and-naming-conventions) diff --git a/plugins/woocommerce-admin/docs/woocommerce.com/README.md b/plugins/woocommerce-admin/docs/woocommerce.com/README.md index a3d165d5c16..2ed099f6070 100644 --- a/plugins/woocommerce-admin/docs/woocommerce.com/README.md +++ b/plugins/woocommerce-admin/docs/woocommerce.com/README.md @@ -1,6 +1,6 @@ # WooCommerce Admin -The WooCommerce Admin plugin is where the next iteration of the administrative experience for WooCommerce is being developed. The project is being built on top of the modern JavaScript packages that are being released from the [core WordPress editor project](https://github.com/wordpress/gutenberg). By building with these `@wordpress/components` - WooCommerce Admin seeks to create a cohesive experience with the latest WordPress core design system, and to deliver a fast and modern set of tools to manage one's WooCommerce Store with. +The WooCommerce Admin is where the next iteration of the administrative experience for WooCommerce is being developed. The project is being built on top of the modern JavaScript packages that are being released from the [core WordPress editor project](https://github.com/wordpress/gutenberg). By building with these `@wordpress/components` - WooCommerce Admin seeks to create a cohesive experience with the latest WordPress core design system, and to deliver a fast and modern set of tools to manage one's WooCommerce Store with. Currently, development efforts have been focused on two primary areas: @@ -10,7 +10,7 @@ Currently, development efforts have been focused on two primary areas: ## Analytics -With WooCommerce Admin installed, a new Analytics menu item is created in the wp-admin menu system. This menu item, and the reports contained insde of it are available to all wp-admin users that have the `view_woocommerce_reports` capability, so per a standard WooCommerce install this would give `shop_manager`s and `administrator`s access to the reports. +With WooCommerce installed, a new Analytics menu item is created in the wp-admin menu system. This menu item, and the reports contained insde of it are available to all wp-admin users that have the `view_woocommerce_reports` capability, so per a standard WooCommerce install this would give `shop_manager`s and `administrator`s access to the reports. Each report is quite unique with its own set of filtering options and chart types. To learn more about each individual report, please view the pages below: diff --git a/plugins/woocommerce-admin/docs/woocommerce.com/analytics-orders-report.md b/plugins/woocommerce-admin/docs/woocommerce.com/analytics-orders-report.md index 84b097997d9..7e6e0916c6b 100644 --- a/plugins/woocommerce-admin/docs/woocommerce.com/analytics-orders-report.md +++ b/plugins/woocommerce-admin/docs/woocommerce.com/analytics-orders-report.md @@ -2,7 +2,7 @@ The Orders Report provides insight about your store's orders. -By default, orders with non-excluded statuses are listed by order date descending. Excluded statuses can be edited on the [Settings page](https://github.com/woocommerce/woocommerce-admin/blob/main/docs/woocommerce.com/analytics-settings.md#excluded-statuses) +By default, orders with non-excluded statuses are listed by order date descending. Excluded statuses can be edited on the [Settings page](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce-admin/docs/woocommerce.com/analytics-settings.md#excluded-statuses) Refunded orders cannot be excluded from the orders report. Refunded orders have two rows in the report: one for the date of the original order and one for the date of refund. diff --git a/plugins/woocommerce-admin/languages/README.md b/plugins/woocommerce-admin/languages/README.md deleted file mode 100644 index 4c0bdd32b0e..00000000000 --- a/plugins/woocommerce-admin/languages/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Languages - -## Contributing a Translation -If you'd like to contribute a translation, please follow the Localizing section in [CONTRIBUTING.md](https://github.com/woocommerce/woocommerce-admin/blob/main/CONTRIBUTING.md). - -## Generating POT - -The generated POT template file is not included in this repository. To create this file locally, follow instructions from [README.md](https://github.com/woocommerce/woocommerce-admin/blob/main/README.md) to install the project, then run the following command: - -``` -pnpm run i18n lang=xx_YY -``` - -After the build completes, you'll find a `woocommerce-admin-xx_YY.po` (eg. `woocommerce-admin-fr_FR.po`) strings file in this directory. - -## Generating JSON - -To generate JSON from your translations, save your translation file in this directory then run the following command: - -``` -pnpm run i18n:json -``` diff --git a/plugins/woocommerce-admin/lint-staged.config.js b/plugins/woocommerce-admin/lint-staged.config.js deleted file mode 100644 index 0dc6e3b021a..00000000000 --- a/plugins/woocommerce-admin/lint-staged.config.js +++ /dev/null @@ -1,47 +0,0 @@ -module.exports = { - '*.scss': [ 'pnpm run lint:css-fix' ], - 'client/**/*.(t|j)s?(x)': [ - 'wp-scripts format-js', - 'wp-scripts lint-js', - 'pnpm run test-staged', - ], - 'packages/**/*.(t|j)s?(x)': ( packageFiles ) => { - const globalScripts = [ - `wp-scripts format-js ${ packageFiles.join( ' ' ) }`, - `wp-scripts lint-js ${ packageFiles.join( ' ' ) }`, - ]; - - const filesByPackage = packageFiles.reduce( - ( packages, packageFile ) => { - const packageNameMatch = packageFile.match( - /\/packages\/([a-z0-9\-]+)\// - ); - - if ( ! packageNameMatch ) { - return packages; - } - - const packageName = packageNameMatch[ 1 ]; - - if ( Array.isArray( packages[ packageName ] ) ) { - packages[ packageName ].push( packageFile ); - } else { - packages[ packageName ] = [ packageFile ]; - } - - return packages; - }, - {} - ); - - const workspaceScripts = Object.keys( filesByPackage ).map( - ( packageName ) => - `pnpm --filter @woocommerce/${ packageName } run test-staged -- ${ filesByPackage[ - packageName - ].join( ' ' ) }` - ); - - return globalScripts.concat( workspaceScripts ); - }, - '*.php': [ 'php -d display_errors=1 -l', 'composer run-script phpcs' ], -}; diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 50d499eea0c..967537c1f7d 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -1,92 +1,62 @@ { "name": "@woocommerce/admin-library", "version": "3.3.0", - "homepage": "https://woocommerce.github.io/woocommerce-admin/", - "repository": { - "type": "git", - "url": "https://github.com:woocommerce/woocommerce-admin.git" - }, "license": "GPL-3.0-or-later", "author": "Automattic", - "files": [ - "dist/**/*.css", - "dist/**/*.js", - "dist/feature-config-core.php", - "includes/class-wc-admin-loader.php", - "includes/features/**/*.php", - "languages/**/*.json", - "license.txt" - ], + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-admin/README.md", + "repository": { + "type": "git", + "url": "https://github.com:woocommerce/woocommerce.git" + }, "scripts": { - "preinstall": "npx only-allow pnpm", - "prebuild": "pnpm run install-if-deps-outdated", - "run:packages": "pnpm run --filter ../../packages/js/", - "packages:fix:textdomain": "node ./bin/package-update-textdomain.js", - "build": "pnpm run build:feature-config && cross-env NODE_ENV=production webpack", "analyze": "cross-env NODE_ENV=production ANALYZE=true webpack", - "postbuild": "pnpm run -s i18n:pot && pnpm run -s i18n:build", - "build:feature-config": "php bin/generate-feature-config.php", + "prebuild": "pnpm run install-if-deps-outdated", + "build": "WC_ADMIN_PHASE=core pnpm run build:feature-config && cross-env NODE_ENV=production WC_ADMIN_PHASE=core webpack", + "build-storybook": "build-storybook -c ./storybook/.storybook", + "build:feature-config": "php ../woocommerce/bin/generate-feature-config.php", "build:packages": "cross-env NODE_ENV=production pnpm run:packages -- build", - "build:release": "./bin/build-plugin-zip.sh", - "clean": "rimraf ./dist && pnpm run:packages -- clean --parallel", + "clean": "rimraf ../woocommerce/assets/client/admin/* && pnpm run:packages -- clean --parallel", + "client:watch": "cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && cross-env WC_ADMIN_PHASE=development webpack --watch", + "create-hook-reference": "node ./bin/hook-reference/index.js", + "create-wc-extension": "node ./bin/starter-pack/starter-pack.js", "predev": "pnpm run -s install-if-deps-outdated", "dev": "cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && cross-env WC_ADMIN_PHASE=development pnpm run build:packages && cross-env WC_ADMIN_PHASE=development webpack", - "client:watch": "cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && cross-env WC_ADMIN_PHASE=development webpack --watch", - "packages:watch": "cross-env WC_ADMIN_PHASE=development pnpm run:packages -- start --parallel", - "docs": "./bin/import-wp-css-storybook.sh && BABEL_ENV=storybook STORYBOOK=true pnpm exec build-storybook -c storybook/.storybook -o ./docs/components/storybook", - "i18n": "pnpm run -s i18n:js && pnpm run -s i18n:check && pnpm run -s i18n:pot && pnpm run -s i18n:build", - "i18n:build": "php bin/combine-pot-files.php languages/woocommerce-admin.po languages/woocommerce-admin.pot", - "i18n:check": "grunt checktextdomain", - "i18n:js": "pnpm run clean && cross-env NODE_ENV=production babel client packages -o /dev/null", - "i18n:json": "./bin/make-i18n-json.sh", - "i18n:pot": "grunt makepot", + "example": "webpack --config docs/examples/extensions/examples.config.js --watch", + "preinstall": "npx only-allow pnpm", "install-if-deps-outdated": "node bin/install-if-deps-outdated.js", "install-if-no-packages": "node bin/install-if-no-packages.js", - "labels:dry": "github-label-sync --labels ./.github/label-sync-config.json --allow-added-labels --dry-run woocommerce/woocommerce-admin", - "labels:sync": "github-label-sync --labels ./.github/label-sync-config.json --allow-added-labels woocommerce/woocommerce-admin", "lint": "pnpm run lint:js && pnpm run lint:css", "lint:css": "stylelint '**/*.scss'", "lint:css-fix": "stylelint '**/*.scss' --fix --ip 'storybook/wordpress'", "lint:js": "wp-scripts lint-js ./client --ext=js,ts,tsx", - "lint:js-packages": "wp-scripts lint-js ../../packages/js --ext=js,ts,tsx", "lint:js-fix": "pnpm run lint:js -- --fix --ext=js,ts,tsx", - "lint:php": "./vendor/bin/phpcs --standard=phpcs.xml.dist $(git ls-files | grep .php$)", - "lint:php-fix": "./vendor/bin/phpcbf --standard=phpcs.xml.dist $(git ls-files | grep .php$)", - "ts:check": "tsc --build ./tsconfig.json --pretty", - "ts:check:watch": "npm run ts:check -- --watch", - "reformat-files": "wp-scripts format-js -- --ignore-path .eslintignore", + "lint:js-packages": "wp-scripts lint-js ../../packages/js --ext=js,ts,tsx", + "lint:js-pre-commit": "wp-scripts lint-js --ext=js,ts,tsx", "prepack": "pnpm install && pnpm run lint && pnpm run test && cross-env WC_ADMIN_PHASE=core pnpm run build", + "packages:fix:textdomain": "node ./bin/package-update-textdomain.js", + "packages:watch": "cross-env WC_ADMIN_PHASE=development pnpm run:packages -- start --parallel", + "pre-release": "./bin/pre-release.sh", "publish-packages:check": "pnpm run build:packages && pnpm publish --dry-run --filter ../../packages/js/ --publish-branch main --report-summary && cat ../../pnpm-publish-summary.json && rimraf ../../pnpm-publish-summary.json", "publish-packages:dev": "pnpm run build:packages && pnpm publish --filter ../../packages/js/ --publish-branch main --tag next", "publish-packages:prod": "pnpm run build:packages && pnpm publish --filter ../../packages/js/ --publish-branch main", + "reformat-files": "wp-scripts format-js -- --ignore-path .eslintignore", + "run:packages": "pnpm run --filter ../../packages/js/", "prestart": "pnpm run install-if-deps-outdated", "start": "cross-env WC_ADMIN_PHASE=development pnpm run build:packages && cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && concurrently \"cross-env WC_ADMIN_PHASE=development webpack --watch\" \"cross-env WC_ADMIN_PHASE=development pnpm run:packages -- start --parallel\"", "start:package": "pnpm run:packages -- start --parallel", - "pretest": "pnpm run -s install-if-no-packages", - "test:debug": "node --inspect-brk ./node_modules/.bin/jest --config client/jest.config.js --watch --runInBand --no-cache", - "test:client": "jest --config client/jest.config.js", - "test:packages": "pnpm run --filter ../../packages/js/ --filter !api-core-tests test", - "test": "pnpm nx build @woocommerce/js-tests && pnpm run test:client", - "test:e2e": "pnpm run build && test -z \"$(docker ps | grep woocommerce-admin-e2e)\" || pnpm exec wc-e2e docker:down && pnpm run e2e:docker-up && pnpm exec wc-e2e test:e2e", - "e2e:docker-up": "WC_E2E_FOLDER=../../../ pnpm exec wc-e2e docker:up ./tests/e2e/docker/initialize.sh", - "test-staged": "pnpm run test:client -- --bail --findRelatedTests", - "test:help": "wp-scripts test-unit-js --help", - "test:php": "docker-compose -f docker/wc-admin-php-test-suite/docker-compose.yml run --rm phpunit", - "posttest:php": "docker-compose -f docker/wc-admin-php-test-suite/docker-compose.yml down", - "test:update-snapshots": "pnpm run test:client -- --updateSnapshot && pnpm run --filter @woocommerce/components test:update-snapshots", - "test:watch": "tsc --build || concurrently \"pnpm run test:client -- --watch\" \"pnpm run:packages -- test:nobuild --parallel -- --watch\"", - "test:zip": "pnpm run clean && composer i && ./bin/build-test-zip.sh", - "example": "webpack --config docs/examples/extensions/examples.config.js --watch", - "pre-release": "./bin/pre-release.sh", - "create-wc-extension": "node ./bin/starter-pack/starter-pack.js", "storybook": "./bin/import-wp-css-storybook.sh && BABEL_ENV=storybook STORYBOOK=true start-storybook -c ./storybook/.storybook -p 6007 --ci", "storybook-rtl": "USE_RTL_STYLE=true pnpm run storybook", - "build-storybook": "build-storybook -c ./storybook/.storybook", - "changelog": "node ./bin/changelog --changelogSrcType='ZENHUB_RELEASE'", - "wp-env-mysql-port": "node ./docker/wc-admin-wp-env/mysql-port.js", - "create-hook-reference": "node ./bin/hook-reference/index.js", - "changelogger": "./vendor/bin/changelogger", - "test-instruction-logger": "./bin/test-instruction-logger/bin/test-instruction-logger" + "pretest": "pnpm run -s install-if-no-packages", + "test": "pnpm nx build @woocommerce/js-tests && pnpm run test:client", + "test-staged": "pnpm run test:client -- --bail --findRelatedTests", + "test:client": "jest --config client/jest.config.js", + "test:debug": "node --inspect-brk ./node_modules/.bin/jest --config client/jest.config.js --watch --runInBand --no-cache", + "test:help": "wp-scripts test-unit-js --help", + "test:packages": "pnpm run --filter ../../packages/js/ --filter !api-core-tests test", + "test:update-snapshots": "pnpm run test:client -- --updateSnapshot && pnpm run --filter @woocommerce/components test:update-snapshots", + "test:watch": "tsc --build || concurrently \"pnpm run test:client -- --watch\" \"pnpm run:packages -- test:nobuild --parallel -- --watch\"", + "ts:check": "tsc --build ./tsconfig.json --pretty", + "ts:check:watch": "npm run ts:check -- --watch" }, "dependencies": { "@automattic/explat-client": "^0.0.3", @@ -96,9 +66,9 @@ "@woocommerce/api": "^0.2.0", "@woocommerce/e2e-environment": "^0.3.0", "@woocommerce/e2e-utils": "^0.2.0", - "@wordpress/a11y": "^2.15.3", + "@wordpress/a11y": "^3.5.0", "@wordpress/api-fetch": "^6.0.1", - "@wordpress/base-styles": "^3.6.0", + "@wordpress/base-styles": "^4.3.0", "@wordpress/components": "^19.5.0", "@wordpress/compose": "^5.1.2", "@wordpress/core-data": "^4.1.2", @@ -107,10 +77,10 @@ "@wordpress/dom": "^3.3.2", "@wordpress/dom-ready": "^3.3.1", "@wordpress/element": "^4.1.1", - "@wordpress/hooks": "^2.12.3", + "@wordpress/hooks": "^3.5.0", "@wordpress/html-entities": "^3.3.1", "@wordpress/i18n": "^4.3.1", - "@wordpress/icons": "^6.3.0", + "@wordpress/icons": "^8.1.0", "@wordpress/keycodes": "^3.3.1", "@wordpress/notices": "^3.3.2", "@wordpress/plugins": "^4.1.3", @@ -205,7 +175,6 @@ "@woocommerce/onboarding": "workspace:*", "@woocommerce/style-build": "workspace:*", "@woocommerce/tracks": "workspace:*", - "@wordpress/babel-plugin-makepot": "^2.1.3", "@wordpress/babel-preset-default": "^6.5.1", "@wordpress/browserslist-config": "^4.1.1", "@wordpress/custom-templated-path-webpack-plugin": "^2.1.2", @@ -222,14 +191,13 @@ "babel-loader": "^8.2.3", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-es2015-template-literals": "^6.22.0", - "chalk": "^5.0.0", + "chalk": "^4.1.2", "comment-parser": "^1.3.0", "concurrently": "^7.0.0", "config": "^3.3.7", "copy-webpack-plugin": "^10.2.4", "cross-env": "^7.0.3", "css-loader": "^6.7.0", - "docsify-cli": "^4.4.3", "eslint": "^8.10.0", "eslint-import-resolver-typescript": "^2.5.0", "eslint-import-resolver-webpack": "^0.13.2", @@ -238,14 +206,9 @@ "expose-loader": "^3.1.0", "fork-ts-checker-webpack-plugin": "^6.5.0", "fs-extra": "^8.1.0", - "grunt": "^1.4.1", - "grunt-checktextdomain": "^1.0.1", - "grunt-wp-i18n": "^1.0.3", - "husky": "^7.0.0", "jest": "^27.5.1", "jest-environment-jsdom": "~27.5.0", "jest-environment-node": "^27.5.1", - "lint-staged": "^12.3.5", "md5": "^2.3.0", "merge-config": "^2.0.0", "mini-css-extract-plugin": "^2.6.0", @@ -282,6 +245,14 @@ "peerDependencies": { "@wordpress/data": "^6.3.0" }, + "lint-staged": { + "*.scss": [ "pnpm lint:css-fix" ], + "client/**/*.(t|j)s?(x)": [ + "pnpm reformat-files", + "pnpm wp-scripts lint-js", + "pnpm test-staged" + ] + }, "engines": { "node": "^16.13.1", "pnpm": "^6.24.2" diff --git a/plugins/woocommerce-admin/tests/bootstrap.php b/plugins/woocommerce-admin/tests/bootstrap.php deleted file mode 100755 index 3ebb080a43f..00000000000 --- a/plugins/woocommerce-admin/tests/bootstrap.php +++ /dev/null @@ -1,218 +0,0 @@ -tests_dir = dirname( __FILE__ ); - $this->plugin_dir = dirname( $this->tests_dir ); - $this->wc_core_dir = getenv( 'WC_CORE_DIR' ) ? getenv( 'WC_CORE_DIR' ) : dirname( $this->plugin_dir ) . '/woocommerce'; - $this->wp_tests_dir = getenv( 'WP_TESTS_DIR' ) ? getenv( 'WP_TESTS_DIR' ) : rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib'; - - $wc_tests_framework_base_dir = $this->wc_core_dir . '/tests'; - - if ( ! is_dir( $wc_tests_framework_base_dir . '/framework' ) ) { - $wc_tests_framework_base_dir .= '/legacy'; - } - $this->wc_core_tests_dir = $wc_tests_framework_base_dir; - - // load test function so tests_add_filter() is available. - require_once $this->wp_tests_dir . '/includes/functions.php'; - - // load WC. - tests_add_filter( 'muplugins_loaded', array( $this, 'load_wc' ) ); - - // install WC. - tests_add_filter( 'setup_theme', array( $this, 'install_wc' ) ); - - // Set up WC-Admin config. - tests_add_filter( 'woocommerce_admin_get_feature_config', array( $this, 'add_development_features' ) ); - - /* - * Load PHPUnit Polyfills for the WP testing suite. - * @see https://github.com/WordPress/wordpress-develop/pull/1563/ - */ - define( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH', __DIR__ . '/../vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php' ); - - // load the WP testing environment. - require_once $this->wp_tests_dir . '/includes/bootstrap.php'; - - // load WC testing framework. - $this->includes(); - - // replace LegacyProxy class to MockableLegacyProxy from WC container. - $this->replace_legacy_proxy(); - } - - /** - * Load WooCommerce Admin. - */ - public function load_wc() { - define( 'WC_TAX_ROUNDING_MODE', 'auto' ); - define( 'WC_USE_TRANSACTIONS', false ); - update_option( 'woocommerce_enable_coupons', 'yes' ); - update_option( 'woocommerce_calc_taxes', 'yes' ); - update_option( 'woocommerce_onboarding_opt_in', 'yes' ); - - require_once $this->wc_core_dir . '/woocommerce.php'; - require $this->plugin_dir . '/vendor/autoload.php'; - require $this->plugin_dir . '/woocommerce-admin.php'; - } - - /** - * Install WooCommerce after the test environment and WC have been loaded. - */ - public function install_wc() { - // Clean existing install first. - define( 'WP_UNINSTALL_PLUGIN', true ); - define( 'WC_REMOVE_ALL_DATA', true ); - include $this->plugin_dir . '/uninstall.php'; - - WC_Install::install(); - - // Initialize the WC API extensions. - \Automattic\WooCommerce\Internal\Admin\Install::create_tables(); - \Automattic\WooCommerce\Internal\Admin\Install::create_events(); - - // Reload capabilities after install, see https://core.trac.wordpress.org/ticket/28374. - if ( version_compare( $GLOBALS['wp_version'], '4.7', '<' ) ) { - $GLOBALS['wp_roles']->reinit(); - } else { - $GLOBALS['wp_roles'] = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - wp_roles(); - } - - echo esc_html( 'Installing WooCommerce and WooCommerce Admin...' . PHP_EOL ); - } - - /** - * Load WC-specific test cases and factories. - */ - public function includes() { - // WooCommerce test classes. - $wc_tests_framework_base_dir = $this->wc_core_tests_dir; - - // Framework. - require_once $wc_tests_framework_base_dir . '/framework/class-wc-unit-test-factory.php'; - require_once $wc_tests_framework_base_dir . '/framework/class-wc-mock-session-handler.php'; - require_once $wc_tests_framework_base_dir . '/framework/class-wc-mock-wc-data.php'; - require_once $wc_tests_framework_base_dir . '/framework/class-wc-mock-wc-object-query.php'; - require_once $wc_tests_framework_base_dir . '/framework/class-wc-mock-payment-gateway.php'; - require_once $this->tests_dir . '/framework/class-wc-mock-enhanced-payment-gateway.php'; - require_once $wc_tests_framework_base_dir . '/framework/class-wc-payment-token-stub.php'; - require_once $wc_tests_framework_base_dir . '/framework/vendor/class-wp-test-spy-rest-server.php'; - - // Test cases. - require_once $wc_tests_framework_base_dir . '/includes/wp-http-testcase.php'; - require_once $wc_tests_framework_base_dir . '/framework/class-wc-unit-test-case.php'; - require_once $wc_tests_framework_base_dir . '/framework/class-wc-api-unit-test-case.php'; - require_once $wc_tests_framework_base_dir . '/framework/class-wc-rest-unit-test-case.php'; - - // Helpers. - require_once $wc_tests_framework_base_dir . '/framework/helpers/class-wc-helper-product.php'; - require_once $wc_tests_framework_base_dir . '/framework/helpers/class-wc-helper-coupon.php'; - require_once $wc_tests_framework_base_dir . '/framework/helpers/class-wc-helper-fee.php'; - require_once $wc_tests_framework_base_dir . '/framework/helpers/class-wc-helper-shipping.php'; - require_once $wc_tests_framework_base_dir . '/framework/helpers/class-wc-helper-customer.php'; - require_once $wc_tests_framework_base_dir . '/framework/helpers/class-wc-helper-order.php'; - require_once $wc_tests_framework_base_dir . '/framework/helpers/class-wc-helper-shipping-zones.php'; - require_once $wc_tests_framework_base_dir . '/framework/helpers/class-wc-helper-payment-token.php'; - require_once $wc_tests_framework_base_dir . '/framework/helpers/class-wc-helper-settings.php'; - - // Include wc-admin helpers. - require_once $this->tests_dir . '/framework/helpers/class-wc-helper-reports.php'; - require_once $this->tests_dir . '/framework/helpers/class-wc-helper-admin-notes.php'; - require_once $this->tests_dir . '/framework/helpers/class-wc-test-action-queue.php'; - require_once $this->tests_dir . '/framework/helpers/class-wc-helper-queue.php'; - } - - /** - * Use the `development` features for testing. - * - * @param array $flags Existing feature flags. - * @return array Filtered feature flags. - */ - public function add_development_features( $flags ) { - $config = json_decode( file_get_contents( $this->plugin_dir . '/config/development.json' ) ); // @codingStandardsIgnoreLine. - foreach ( $config->features as $feature => $bool ) { - $flags[ $feature ] = $bool; - } - return $flags; - } - - /** - * Get the single class instance. - * @return WC_Admin_Unit_Tests_Bootstrap - */ - public static function instance() { - if ( is_null( self::$instance ) ) { - self::$instance = new self(); - } - - return self::$instance; - } - - /** - * Replace LegacyProxy to MockableLegacyProxy from the WC container. - * - * @throws \Exception Thrown when reflection fails. - */ - private function replace_legacy_proxy() { - try { - $inner_container_property = new \ReflectionProperty( \Automattic\WooCommerce\Container::class, 'container' ); - } catch ( ReflectionException $ex ) { - throw new \Exception( "Error when trying to get the private 'container' property from the " . \Automattic\WooCommerce\Container::class . ' class using reflection during unit testing bootstrap, has the property been removed or renamed?' ); - } - - $inner_container_property->setAccessible( true ); - $inner_container = $inner_container_property->getValue( wc_get_container() ); - - $inner_container->replace( LegacyProxy::class, MockableLegacyProxy::class ); - $inner_container->reset_all_resolved(); - - $GLOBALS['wc_container'] = $inner_container; - } -} - -WC_Admin_Unit_Tests_Bootstrap::instance(); diff --git a/plugins/woocommerce-admin/tests/e2e/config/default.json b/plugins/woocommerce-admin/tests/e2e/config/default.json deleted file mode 100644 index be040ef9036..00000000000 --- a/plugins/woocommerce-admin/tests/e2e/config/default.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "url": "http://localhost:8084/", - "appName": "woocommerce-admin-e2e", - "users": { - "admin": { - "username": "admin", - "password": "password" - }, - "customer": { - "username": "customer", - "password": "password" - } - }, - "products": { - "simple": { - "name": "Simple product" - }, - "variable": { - "name": "Variable Product with Three Variations" - }, - "grouped": { - "name": "Grouped Product with Three Children", - "groupedProducts": [ - { - "name": "Base Unit", - "regularPrice": "29.99" - }, - { - "name": "Add-on A", - "regularPrice": "11.95" - }, - { - "name": "Add-on B", - "regularPrice": "18.97" - } - ] - } - }, - "addresses": { - "admin": { - "store": { - "firstname": "John", - "lastname": "Doe", - "company": "Automattic", - "country": "United States (US)", - "addressfirstline": "addr 1", - "addresssecondline": "addr 2", - "countryandstate": "United States (US) -- California", - "city": "San Francisco", - "state": "CA", - "postcode": "94107", - "email": "john.doe@example.com" - } - }, - "customer": { - "billing": { - "firstname": "John", - "lastname": "Doe", - "company": "Automattic", - "country": "United States (US)", - "addressfirstline": "addr 1", - "addresssecondline": "addr 2", - "city": "San Francisco", - "state": "CA", - "postcode": "94107", - "phone": "123456789", - "email": "john.doe@example.com" - }, - "shipping": { - "firstname": "John", - "lastname": "Doe", - "company": "Automattic", - "country": "United States (US)", - "addressfirstline": "addr 1", - "addresssecondline": "addr 2", - "city": "San Francisco", - "state": "CA", - "postcode": "94107" - } - } - }, - "onboardingwizard": { - "industry": "Test industry", - "numberofproducts": "1 - 10", - "sellingelsewhere": "No", - "sellingOnAnotherPlatform": "Yes, on another platform", - "number_employees": "< 10", - "revenue": "Up to $2,500.00", - "other_platform_name": "Etsy" - }, - "settings": { - "shipping": { - "zonename": "United States", - "zoneregions": "United States (US)", - "shippingmethod": "Free shipping" - } - } -} diff --git a/plugins/woocommerce-admin/tests/e2e/config/env.setup.js b/plugins/woocommerce-admin/tests/e2e/config/env.setup.js deleted file mode 100644 index 6647b49d176..00000000000 --- a/plugins/woocommerce-admin/tests/e2e/config/env.setup.js +++ /dev/null @@ -1,5 +0,0 @@ -global.process.env = { - ...global.process.env, - // Gutenberg test util functions expect the test url to be at :8889, we change it to 8084. - WP_BASE_URL: 'http://localhost:8084', -}; diff --git a/plugins/woocommerce-admin/tests/e2e/config/jest-puppeteer.config.js b/plugins/woocommerce-admin/tests/e2e/config/jest-puppeteer.config.js deleted file mode 100644 index 30af9325973..00000000000 --- a/plugins/woocommerce-admin/tests/e2e/config/jest-puppeteer.config.js +++ /dev/null @@ -1,10 +0,0 @@ -const { useE2EJestPuppeteerConfig } = require( '@woocommerce/e2e-environment' ); - -const puppeteerConfig = useE2EJestPuppeteerConfig( { - launch: { - browserContext: 'incognito', - args: [ '--incognito' ], - }, -} ); - -module.exports = puppeteerConfig; diff --git a/plugins/woocommerce-admin/tests/e2e/config/jest.config.js b/plugins/woocommerce-admin/tests/e2e/config/jest.config.js deleted file mode 100644 index 96983f0607d..00000000000 --- a/plugins/woocommerce-admin/tests/e2e/config/jest.config.js +++ /dev/null @@ -1,23 +0,0 @@ -const path = require( 'path' ); -const { useE2EJestConfig } = require( '@woocommerce/e2e-environment' ); - -const config = useE2EJestConfig( { - moduleFileExtensions: [ 'js', 'ts', 'tsx' ], - roots: [ path.resolve( __dirname, '../specs' ) ], - testMatch: [ '**/*.(test|spec).(js|ts|tsx)', '*.(test|spec).(js|ts|tsx)' ], - testTimeout: 30000, - transform: { - '\\.[jt]sx?$': [ - 'babel-jest', - { - configFile: path.join( - __dirname, - '../../../', - 'babel.config.js' - ), - }, - ], - }, -} ); - -module.exports = config; diff --git a/plugins/woocommerce-admin/tests/e2e/docker/initialize.sh b/plugins/woocommerce-admin/tests/e2e/docker/initialize.sh deleted file mode 100755 index 335c5ea7736..00000000000 --- a/plugins/woocommerce-admin/tests/e2e/docker/initialize.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -echo "Initializing WooCommerce E2E" - -# Turn off error display temporarily. This is to prevent deprecated function -# notices from breaking the display of some screens and then E2E tests. -# Message was for WC_Admin_Notes_Deactivate_Plugin usage in core WC. -wp config set WP_DEBUG_DISPLAY false --raw -wp config set JETPACK_AUTOLOAD_DEV true --raw -wp plugin install woocommerce --activate -wp plugin install https://github.com/woocommerce/woocommerce-reset/zipball/trunk/ --activate -wp theme install twentynineteen --activate -wp plugin activate woocommerce-admin -wp user create customer customer@woocommercecoree2etestsuite.com --user_pass=password --role=customer --path=/var/www/html - -# we cannot create API keys for the API, so we using basic auth, this plugin allows that. -wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate diff --git a/plugins/woocommerce-admin/tests/e2e/specs/activate-and-setup/basic-setup.test.tsx b/plugins/woocommerce-admin/tests/e2e/specs/activate-and-setup/basic-setup.test.tsx deleted file mode 100644 index 4b26f2133a4..00000000000 --- a/plugins/woocommerce-admin/tests/e2e/specs/activate-and-setup/basic-setup.test.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const { testAdminBasicSetup } = require( '@woocommerce/admin-e2e-tests' ); - -testAdminBasicSetup(); diff --git a/plugins/woocommerce-admin/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.tsx b/plugins/woocommerce-admin/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.tsx deleted file mode 100644 index a688787906d..00000000000 --- a/plugins/woocommerce-admin/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -const { - testAdminOnboardingWizard, - testSelectiveBundleWCPay, - testDifferentStoreCurrenciesWCPay, - testSubscriptionsInclusion, - testBusinessDetailsForm, -} = require( '@woocommerce/admin-e2e-tests' ); - -testAdminOnboardingWizard(); -testSelectiveBundleWCPay(); -testDifferentStoreCurrenciesWCPay(); -testSubscriptionsInclusion(); -testBusinessDetailsForm(); diff --git a/plugins/woocommerce-admin/tests/e2e/specs/analytics/analytics-overview.test.tsx b/plugins/woocommerce-admin/tests/e2e/specs/analytics/analytics-overview.test.tsx deleted file mode 100644 index 9bc1b4e7467..00000000000 --- a/plugins/woocommerce-admin/tests/e2e/specs/analytics/analytics-overview.test.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const { testAdminAnalyticsOverview } = require( '@woocommerce/admin-e2e-tests' ); - -testAdminAnalyticsOverview(); diff --git a/plugins/woocommerce-admin/tests/e2e/specs/analytics/analytics.test.tsx b/plugins/woocommerce-admin/tests/e2e/specs/analytics/analytics.test.tsx deleted file mode 100644 index 5a38d6f3234..00000000000 --- a/plugins/woocommerce-admin/tests/e2e/specs/analytics/analytics.test.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const { testAdminAnalyticsPages } = require( '@woocommerce/admin-e2e-tests' ); - -testAdminAnalyticsPages(); diff --git a/plugins/woocommerce-admin/tests/e2e/specs/homescreen/activity-panel.test.ts b/plugins/woocommerce-admin/tests/e2e/specs/homescreen/activity-panel.test.ts deleted file mode 100644 index b8865709e32..00000000000 --- a/plugins/woocommerce-admin/tests/e2e/specs/homescreen/activity-panel.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -const { testAdminHomescreenActivityPanel } = require( '@woocommerce/admin-e2e-tests' ); - -testAdminHomescreenActivityPanel(); diff --git a/plugins/woocommerce-admin/tests/e2e/specs/homescreen/task-list.test.ts b/plugins/woocommerce-admin/tests/e2e/specs/homescreen/task-list.test.ts deleted file mode 100644 index ae76561b357..00000000000 --- a/plugins/woocommerce-admin/tests/e2e/specs/homescreen/task-list.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -const { testAdminHomescreenTasklist } = require( '@woocommerce/admin-e2e-tests' ); - -testAdminHomescreenTasklist(); diff --git a/plugins/woocommerce-admin/tests/e2e/specs/marketing/coupons.test.tsx b/plugins/woocommerce-admin/tests/e2e/specs/marketing/coupons.test.tsx deleted file mode 100644 index 28629da035c..00000000000 --- a/plugins/woocommerce-admin/tests/e2e/specs/marketing/coupons.test.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const { testAdminCouponsPage } = require( '@woocommerce/admin-e2e-tests' ); - -testAdminCouponsPage(); diff --git a/plugins/woocommerce-admin/tests/e2e/specs/tasks/payment.test.tsx b/plugins/woocommerce-admin/tests/e2e/specs/tasks/payment.test.tsx deleted file mode 100644 index a0600e5d656..00000000000 --- a/plugins/woocommerce-admin/tests/e2e/specs/tasks/payment.test.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const { testAdminPaymentSetupTask } = require( '@woocommerce/admin-e2e-tests' ); - -testAdminPaymentSetupTask(); diff --git a/plugins/woocommerce-admin/uninstall.php b/plugins/woocommerce-admin/uninstall.php deleted file mode 100644 index e349b99deb9..00000000000 --- a/plugins/woocommerce-admin/uninstall.php +++ /dev/null @@ -1,23 +0,0 @@ - { Object.entries( assets ).forEach( diff --git a/plugins/woocommerce-admin/webpack.config.js b/plugins/woocommerce-admin/webpack.config.js index dbbf852059c..1a2a4536a00 100644 --- a/plugins/woocommerce-admin/webpack.config.js +++ b/plugins/woocommerce-admin/webpack.config.js @@ -36,12 +36,8 @@ const wcAdminPackages = [ 'tracks', 'onboarding', ]; - -const entryPoints = {}; -wcAdminPackages.forEach( ( name ) => { - entryPoints[ name ] = `../../packages/js/${ name }`; -} ); - +// wpAdminScripts are loaded on wp-admin pages outside the context of WooCommerce Admin +// See ./client/wp-admin-scripts/README.md for more details const wpAdminScripts = [ 'marketing-coupons', 'navigation-opt-out', @@ -53,45 +49,55 @@ const wpAdminScripts = [ 'beta-features-tracking-modal', 'payment-method-promotions', ]; -wpAdminScripts.forEach( ( name ) => { - entryPoints[ name ] = `./client/wp-admin-scripts/${ name }`; -} ); +const getEntryPoints = () => { + const entryPoints = { + app: './client/index.js', + }; + wcAdminPackages.forEach( ( name ) => { + entryPoints[ name ] = `../../packages/js/${ name }`; + } ); + wpAdminScripts.forEach( ( name ) => { + entryPoints[ name ] = `./client/wp-admin-scripts/${ name }`; + } ); + return entryPoints; +}; -const suffix = WC_ADMIN_PHASE === 'core' ? '' : '.min'; +// WordPress.orgā€™s translation infrastructure ignores files named ā€œ.min.jsā€ so we need to name our JS files without min when releasing the plugin. +const outputSuffix = WC_ADMIN_PHASE === 'core' ? '' : '.min'; const webpackConfig = { mode: NODE_ENV, - entry: { - app: './client/index.js', - ...entryPoints, - }, + entry: getEntryPoints(), output: { filename: ( data ) => { + // Output wpAdminScripts to wp-admin-scripts folder + // See https://github.com/woocommerce/woocommerce-admin/pull/3061 return wpAdminScripts.includes( data.chunk.name ) - ? `wp-admin-scripts/[name]${ suffix }.js` - : `[name]/index${ suffix }.js`; + ? `wp-admin-scripts/[name]${ outputSuffix }.js` + : `[name]/index${ outputSuffix }.js`; }, - chunkFilename: `chunks/[name]${ suffix }.js`, + chunkFilename: `chunks/[name]${ outputSuffix }.js`, path: path.join( __dirname, '/../woocommerce/assets/client/admin' ), - library: [ 'wc', '[modulename]' ], - libraryTarget: 'window', + library: { + // Expose the exports of entry points so we can consume the libraries in window.wc.[modulename] with WooCommerceDependencyExtractionWebpackPlugin. + name: [ 'wc', '[modulename]' ], + type: 'window', + }, + // A unique name of the webpack build to avoid multiple webpack runtimes to conflict when using globals. uniqueName: '__wcAdmin_webpackJsonp', }, module: { rules: [ { - test: /\.js$/, + test: /\.(t|j)sx?$/, parser: { + // Disable AMD to fix an issue where underscore and lodash where clashing + // See https://github.com/woocommerce/woocommerce-admin/pull/1004 and https://github.com/Automattic/woocommerce-services/pull/1522 amd: false, }, - }, - { - test: /\.(t|j)sx?$/, exclude: [ - // Exclude node_modules/ but not node_modules/debug* and node_modules/explat-client-react-helpers - // explat-client-react-helpers module contains optional chaining operators which need to be processed via babel loader for webpack 4. - // see webpack issue for details: https://github.com/webpack/webpack/issues/10227#issue-547480527 - /node_modules(\/|\\)\.pnpm(\/|\\)(?!(debug|\@automattic\+explat-client-react-helpers))/, + // Exclude node_modules/.pnpm but not node_modules/.pnpm/debug* + /node_modules(\/|\\)\.pnpm(\/|\\)(?!(debug))/, ], use: { loader: 'babel-loader', @@ -101,6 +107,8 @@ const webpackConfig = { [ '@babel/preset-env', { + // Add polyfills such as Array.flat based on their usage in the code + // See https://github.com/woocommerce/woocommerce-admin/pull/6411/ corejs: '3', useBuiltIns: 'usage', }, @@ -119,25 +127,26 @@ const webpackConfig = { ], }, resolve: { - fallback:{ - 'crypto': 'empty' + fallback: { + // Reduce bundle size by omitting Node crypto library. + // See https://github.com/woocommerce/woocommerce-admin/pull/5768 + crypto: 'empty', }, extensions: [ '.json', '.js', '.jsx', '.ts', '.tsx' ], alias: { '~': path.resolve( __dirname + '/client' ), - 'gutenberg-components': path.resolve( - __dirname, - 'node_modules/@wordpress/components/src' - ), }, }, plugins: [ ...styleConfig.plugins, + // Runs TypeScript type checker on a separate process. new ForkTsCheckerWebpackPlugin(), new CustomTemplatedPathPlugin( { modulename( outputPath, data ) { const entryName = get( data, [ 'chunk', 'name' ] ); if ( entryName ) { + // Convert the dash-case name to a camel case module name. + // For example, 'csv-export' -> 'csvExport' return entryName.replace( /-([a-z])/g, ( match, letter ) => letter.toUpperCase() ); @@ -145,27 +154,31 @@ const webpackConfig = { return outputPath; }, } ), - new CopyWebpackPlugin({ - + // The package build process doesn't handle extracting CSS from JS files, so we copy them separately. + new CopyWebpackPlugin( { patterns: wcAdminPackages.map( ( packageName ) => ( { from: `../../packages/js/${ packageName }/build-style/*.css`, to: `./${ packageName }/[name][ext]`, - noErrorOnMissing: true - } ) ) - } - ), + noErrorOnMissing: true, + } ) ), + } ), // We reuse this Webpack setup for Storybook, where we need to disable dependency extraction. ! process.env.STORYBOOK && new WooCommerceDependencyExtractionWebpackPlugin(), + // Reduces data for moment-timezone. new MomentTimezoneDataPlugin( { - startYear: 2000, // This strips out timezone data before the year 2000 to make a smaller file. + // This strips out timezone data before the year 2000 to make a smaller file. + startYear: 2000, } ), process.env.ANALYZE && new BundleAnalyzerPlugin(), - // Partially replace with __webpack_get_script_filename__ in app once using Webpack 5.x. + // Adds the script version parameter to the chunk URLs for cache busting + // TODO: Partially replace with __webpack_get_script_filename__ in app with Webpack 5.x. // The CSS chunk portion will need to remain, as it originates in MiniCssExtractPlugin. new AsyncChunkSrcVersionParameterPlugin(), - WC_ADMIN_PHASE !== 'core' && + // We only want to generate unminified files in the development phase. + WC_ADMIN_PHASE === 'development' && + // Generate unminified files to load the unminified version when `define( 'SCRIPT_DEBUG', true );` is set in wp-config. new UnminifyWebpackPlugin( { test: /\.js($|\?)/i, mainEntry: 'app/index.min.js', @@ -174,12 +187,18 @@ const webpackConfig = { optimization: { minimize: NODE_ENV !== 'development', splitChunks: { - name: false - } + // Not to generate chunk names because it caused a stressful workflow when deploying the plugin to WP.org + // See https://github.com/woocommerce/woocommerce-admin/pull/5229 + name: false, + }, }, }; -if ( webpackConfig.mode !== 'production' && WC_ADMIN_PHASE !== 'core' ) { +// Use the source map if we're in development mode, . +if ( + webpackConfig.mode === 'development' || + WC_ADMIN_PHASE === 'development' +) { webpackConfig.devtool = process.env.SOURCEMAP || 'source-map'; } diff --git a/plugins/woocommerce-admin/woocommerce-admin.php b/plugins/woocommerce-admin/woocommerce-admin.php deleted file mode 100755 index 484ed9865f9..00000000000 --- a/plugins/woocommerce-admin/woocommerce-admin.php +++ /dev/null @@ -1,143 +0,0 @@ - -
-

- composer install', - '' . esc_html( str_replace( ABSPATH, '', __DIR__ ) ) . '' - ); - ?> -

-
-

'; - printf( - /* Translators: %1$s is referring to a php constant name, %2$s is referring to the wp-config.php file. */ - esc_html__( 'WooCommerce Admin development mode requires the %1$s constant to be defined and true in your %2$s file. Otherwise you are loading the admin package from WooCommerce core.', 'woocommerce-admin' ), - 'JETPACK_AUTOLOAD_DEV', - 'wp-config.php' - ); - echo '

'; - } - ); -} - -/** - * If we're missing expected files, notify users that the plugin needs to be built. - */ -if ( ! woocommerce_admin_check_build_files() ) { - add_action( - 'admin_notices', - function() { - echo '

'; - printf( - /* Translators: %1$s, %2$s, and %3$s are all build commands to be run in order. */ - esc_html__( 'You have installed a development version of WooCommerce Admin which requires files to be built. From the plugin directory, run %1$s and %2$s to install dependencies, then %3$s to build the files.', 'woocommerce-admin' ), - 'composer install', - 'pnpm install', - 'pnpm run build' - ); - printf( - /* translators: 1: URL of GitHub Repository build page */ - esc_html__( 'Or you can download a pre-built version of the plugin by visiting the releases page in the repository.', 'woocommerce-admin' ), - 'https://github.com/woocommerce/woocommerce-admin/releases' - ); - echo '

'; - } - ); -} - -FeaturePlugin::instance()->init(); diff --git a/plugins/woocommerce-beta-tester/package.json b/plugins/woocommerce-beta-tester/package.json index b2bf1ba26da..c9044a0682f 100644 --- a/plugins/woocommerce-beta-tester/package.json +++ b/plugins/woocommerce-beta-tester/package.json @@ -11,8 +11,6 @@ "homepage": "http://github.com/woocommerce/woocommerce-beta-tester", "devDependencies": { "eslint": "5.16.0", - "husky": "1.3.1", - "lint-staged": "8.1.5", "uglify-js": "^3.5.3" }, "assets": { @@ -32,28 +30,17 @@ "node": ">=10.15.0", "npm": ">=6.4.1" }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } - }, - "lint-staged": { - "linters": { - "*.php": [ - "php -d display_errors=1 -l", - "composer run-script phpcs-pre-commit" - ], - "*.js": [ - "eslint --fix", - "git add" - ] - }, - "ignore": [ - "*.min.js" - ] - }, "woorelease": { "svn_reauth": "true", "wp_org_slug": "woocommerce-beta-tester" + }, + "lint-staged": { + "*.php": [ + "php -d display_errors=1 -l", + "composer --working-dir=./plugins/woocommerce-beta-tester run-script phpcs-pre-commit" + ], + "!(*min).js": [ + "eslint --fix" + ] } } diff --git a/.wp-env.json b/plugins/woocommerce/.wp-env.json similarity index 80% rename from .wp-env.json rename to plugins/woocommerce/.wp-env.json index 76975dbd699..848e60698c8 100644 --- a/.wp-env.json +++ b/plugins/woocommerce/.wp-env.json @@ -1,7 +1,7 @@ { "phpVersion": "7.4", "core": null, - "plugins": [ "./plugins/woocommerce" ], + "plugins": [ "." ], "config": { "JETPACK_AUTOLOAD_DEV": true, "WP_DEBUG_LOG": true, diff --git a/plugins/woocommerce/NEXT_CHANGELOG.md b/plugins/woocommerce/NEXT_CHANGELOG.md index ef1933df641..6a8f33db4c0 100644 --- a/plugins/woocommerce/NEXT_CHANGELOG.md +++ b/plugins/woocommerce/NEXT_CHANGELOG.md @@ -2,4 +2,4 @@ --- -[See changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/77ccfc56ca5680f3bc1496d8b2f93befa28e1483/changelog.txt). +[See changelogs for previous versions](https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/changelog.txt). diff --git a/plugins/woocommerce/assets/images/onboarding/pinterest.png b/plugins/woocommerce/assets/images/onboarding/pinterest.png new file mode 100644 index 00000000000..2951353d8fd Binary files /dev/null and b/plugins/woocommerce/assets/images/onboarding/pinterest.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/affirm.png b/plugins/woocommerce/assets/images/payment_methods/72x72/affirm.png new file mode 100644 index 00000000000..9401ad1b670 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/affirm.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/afterpay.png b/plugins/woocommerce/assets/images/payment_methods/72x72/afterpay.png new file mode 100644 index 00000000000..28b89a010d1 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/afterpay.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/amazonpay.png b/plugins/woocommerce/assets/images/payment_methods/72x72/amazonpay.png new file mode 100644 index 00000000000..1116fe95b2f Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/amazonpay.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/bacs.png b/plugins/woocommerce/assets/images/payment_methods/72x72/bacs.png new file mode 100644 index 00000000000..e9b706beb8a Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/bacs.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/cod.png b/plugins/woocommerce/assets/images/payment_methods/72x72/cod.png new file mode 100644 index 00000000000..f2ba5f68888 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/cod.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/eway.png b/plugins/woocommerce/assets/images/payment_methods/72x72/eway.png new file mode 100644 index 00000000000..74884fa1b03 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/eway.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/klarna.png b/plugins/woocommerce/assets/images/payment_methods/72x72/klarna.png new file mode 100644 index 00000000000..a97abed5981 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/klarna.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/mercadopago.png b/plugins/woocommerce/assets/images/payment_methods/72x72/mercadopago.png new file mode 100644 index 00000000000..5f61e693d6e Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/mercadopago.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/mollie.png b/plugins/woocommerce/assets/images/payment_methods/72x72/mollie.png new file mode 100644 index 00000000000..3ec67896237 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/mollie.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/payfast.png b/plugins/woocommerce/assets/images/payment_methods/72x72/payfast.png new file mode 100644 index 00000000000..96440121c30 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/payfast.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/paypal.png b/plugins/woocommerce/assets/images/payment_methods/72x72/paypal.png new file mode 100644 index 00000000000..07768e74fd4 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/paypal.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/paystack.png b/plugins/woocommerce/assets/images/payment_methods/72x72/paystack.png new file mode 100644 index 00000000000..362446dfb00 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/paystack.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/payu.png b/plugins/woocommerce/assets/images/payment_methods/72x72/payu.png new file mode 100644 index 00000000000..50eaa0f8250 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/payu.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/razorpay.png b/plugins/woocommerce/assets/images/payment_methods/72x72/razorpay.png new file mode 100644 index 00000000000..01dedd7bfa6 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/razorpay.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/square.png b/plugins/woocommerce/assets/images/payment_methods/72x72/square.png new file mode 100644 index 00000000000..3ffe4877222 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/square.png differ diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/stripe.png b/plugins/woocommerce/assets/images/payment_methods/72x72/stripe.png new file mode 100644 index 00000000000..0175f2947d6 Binary files /dev/null and b/plugins/woocommerce/assets/images/payment_methods/72x72/stripe.png differ diff --git a/plugins/woocommerce/assets/images/task_list/basics-section-illustration.png b/plugins/woocommerce/assets/images/task_list/basics-section-illustration.png new file mode 100644 index 00000000000..988a281048f Binary files /dev/null and b/plugins/woocommerce/assets/images/task_list/basics-section-illustration.png differ diff --git a/plugins/woocommerce/assets/images/task_list/expand-section-illustration.png b/plugins/woocommerce/assets/images/task_list/expand-section-illustration.png new file mode 100644 index 00000000000..e8bd17b154f Binary files /dev/null and b/plugins/woocommerce/assets/images/task_list/expand-section-illustration.png differ diff --git a/plugins/woocommerce/assets/images/task_list/sales-section-illustration.png b/plugins/woocommerce/assets/images/task_list/sales-section-illustration.png new file mode 100644 index 00000000000..133800215bc Binary files /dev/null and b/plugins/woocommerce/assets/images/task_list/sales-section-illustration.png differ diff --git a/plugins/woocommerce/bin/composer/mozart/composer.lock b/plugins/woocommerce/bin/composer/mozart/composer.lock index 4c8ffa51e84..c151f23629d 100644 --- a/plugins/woocommerce/bin/composer/mozart/composer.lock +++ b/plugins/woocommerce/bin/composer/mozart/composer.lock @@ -162,16 +162,16 @@ }, { "name": "league/mime-type-detection", - "version": "1.9.0", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "aa70e813a6ad3d1558fc927863d47309b4c23e69" + "reference": "3e4a35d756eedc67096f30240a68a3149120dae7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/aa70e813a6ad3d1558fc927863d47309b4c23e69", - "reference": "aa70e813a6ad3d1558fc927863d47309b4c23e69", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3e4a35d756eedc67096f30240a68a3149120dae7", + "reference": "3e4a35d756eedc67096f30240a68a3149120dae7", "shasum": "" }, "require": { @@ -202,7 +202,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.9.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.10.0" }, "funding": [ { @@ -214,7 +214,7 @@ "type": "tidelift" } ], - "time": "2021-11-21T11:48:40+00:00" + "time": "2022-04-11T12:49:04+00:00" }, { "name": "psr/container", @@ -266,16 +266,16 @@ }, { "name": "symfony/console", - "version": "v5.4.5", + "version": "v5.4.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "d8111acc99876953f52fe16d4c50eb60940d49ad" + "reference": "900275254f0a1a2afff1ab0e11abd5587a10e1d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/d8111acc99876953f52fe16d4c50eb60940d49ad", - "reference": "d8111acc99876953f52fe16d4c50eb60940d49ad", + "url": "https://api.github.com/repos/symfony/console/zipball/900275254f0a1a2afff1ab0e11abd5587a10e1d6", + "reference": "900275254f0a1a2afff1ab0e11abd5587a10e1d6", "shasum": "" }, "require": { @@ -345,7 +345,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.5" + "source": "https://github.com/symfony/console/tree/v5.4.7" }, "funding": [ { @@ -361,20 +361,20 @@ "type": "tidelift" } ], - "time": "2022-02-24T12:45:35+00:00" + "time": "2022-03-31T17:09:19+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", "shasum": "" }, "require": { @@ -412,7 +412,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" }, "funding": [ { @@ -428,7 +428,7 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/finder", @@ -987,22 +987,22 @@ }, { "name": "symfony/service-contracts", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc" + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/24d9dc654b83e91aa59f9d167b131bc3b5bea24c", + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c", "shasum": "" }, "require": { "php": ">=7.2.5", "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1" + "symfony/deprecation-contracts": "^2.1|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -1050,7 +1050,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.1" }, "funding": [ { @@ -1066,7 +1066,7 @@ "type": "tidelift" } ], - "time": "2021-11-04T16:48:04+00:00" + "time": "2022-03-13T20:07:29+00:00" }, { "name": "symfony/string", @@ -1167,5 +1167,5 @@ "platform-overrides": { "php": "7.3" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.1.0" } diff --git a/plugins/woocommerce/bin/composer/phpcs/composer.lock b/plugins/woocommerce/bin/composer/phpcs/composer.lock index 8dce372de81..6cf9a6b7cc1 100644 --- a/plugins/woocommerce/bin/composer/phpcs/composer.lock +++ b/plugins/woocommerce/bin/composer/phpcs/composer.lock @@ -416,5 +416,5 @@ "platform-overrides": { "php": "7.0" }, - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.1.0" } diff --git a/plugins/woocommerce/bin/composer/phpunit/composer.lock b/plugins/woocommerce/bin/composer/phpunit/composer.lock index 8e227b9dd3a..7fc2f48ec7a 100644 --- a/plugins/woocommerce/bin/composer/phpunit/composer.lock +++ b/plugins/woocommerce/bin/composer/phpunit/composer.lock @@ -1697,5 +1697,5 @@ "platform-overrides": { "php": "7.0" }, - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.1.0" } diff --git a/plugins/woocommerce/bin/composer/wp/composer.lock b/plugins/woocommerce/bin/composer/wp/composer.lock index b06f269acc1..8fee101ebe0 100644 --- a/plugins/woocommerce/bin/composer/wp/composer.lock +++ b/plugins/woocommerce/bin/composer/wp/composer.lock @@ -7,6 +7,64 @@ "content-hash": "4d4f2befccefe100869d30305083672b", "packages": [], "packages-dev": [ + { + "name": "eftec/bladeone", + "version": "3.52", + "source": { + "type": "git", + "url": "https://github.com/EFTEC/BladeOne.git", + "reference": "a19bf66917de0b29836983db87a455a4f6e32148" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/EFTEC/BladeOne/zipball/a19bf66917de0b29836983db87a455a4f6e32148", + "reference": "a19bf66917de0b29836983db87a455a4f6e32148", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.6" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16.1", + "phpunit/phpunit": "^5.7", + "squizlabs/php_codesniffer": "^3.5.4" + }, + "suggest": { + "eftec/bladeonehtml": "Extension to create forms", + "ext-mbstring": "This extension is used if it's active" + }, + "type": "library", + "autoload": { + "psr-4": { + "eftec\\bladeone\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jorge Patricio Castro Castillo", + "email": "jcastro@eftec.cl" + } + ], + "description": "The standalone version Blade Template Engine from Laravel in a single php file", + "homepage": "https://github.com/EFTEC/BladeOne", + "keywords": [ + "blade", + "php", + "template", + "templating", + "view" + ], + "support": { + "issues": "https://github.com/EFTEC/BladeOne/issues", + "source": "https://github.com/EFTEC/BladeOne/tree/3.52" + }, + "time": "2021-04-17T13:49:01+00:00" + }, { "name": "gettext/gettext", "version": "v4.8.6", @@ -376,19 +434,20 @@ }, { "name": "wp-cli/i18n-command", - "version": "v2.2.13", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/wp-cli/i18n-command.git", - "reference": "77ece9e2c914bb56ea72b9ee9f00556dece07b3f" + "reference": "bcb1a8159679cafdf1da884dbe5830122bae2c4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/i18n-command/zipball/77ece9e2c914bb56ea72b9ee9f00556dece07b3f", - "reference": "77ece9e2c914bb56ea72b9ee9f00556dece07b3f", + "url": "https://api.github.com/repos/wp-cli/i18n-command/zipball/bcb1a8159679cafdf1da884dbe5830122bae2c4d", + "reference": "bcb1a8159679cafdf1da884dbe5830122bae2c4d", "shasum": "" }, "require": { + "eftec/bladeone": "3.52", "gettext/gettext": "^4.8", "mck89/peast": "^1.13.11", "wp-cli/wp-cli": "^2.5" @@ -434,9 +493,9 @@ "homepage": "https://github.com/wp-cli/i18n-command", "support": { "issues": "https://github.com/wp-cli/i18n-command/issues", - "source": "https://github.com/wp-cli/i18n-command/tree/v2.2.13" + "source": "https://github.com/wp-cli/i18n-command/tree/v2.3.0" }, - "time": "2022-01-13T01:40:51+00:00" + "time": "2022-04-06T15:32:48+00:00" }, { "name": "wp-cli/mustangostang-spyc", @@ -625,5 +684,5 @@ "platform-overrides": { "php": "7.0" }, - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.1.0" } diff --git a/plugins/woocommerce-admin/bin/generate-feature-config.php b/plugins/woocommerce/bin/generate-feature-config.php similarity index 70% rename from plugins/woocommerce-admin/bin/generate-feature-config.php rename to plugins/woocommerce/bin/generate-feature-config.php index 7174b0110b9..ffe637c880b 100644 --- a/plugins/woocommerce-admin/bin/generate-feature-config.php +++ b/plugins/woocommerce/bin/generate-feature-config.php @@ -8,16 +8,15 @@ /** * Get phase for feature flags * - development: All features should be enabled in development. - * - plugin: For the standalone feature plugin, for GitHub and WordPress.org. * - core: Stable features for WooCommerce core merge. */ $phase = getenv( 'WC_ADMIN_PHASE' ); -if ( ! in_array( $phase, array( 'development', 'plugin', 'core' ), true ) ) { - $phase = 'plugin'; // Default to plugin when running `pnpm run build`. +if ( ! in_array( $phase, array( 'development', 'core' ), true ) ) { + $phase = 'core'; // Default to core when running `pnpm run build`. } -$config_json = file_get_contents( __DIR__ . '/../config/' . $phase . '.json' ); +$config_json = file_get_contents( __DIR__ . '/../client/admin/config/' . $phase . '.json' ); $config = json_decode( $config_json ); $write = "&2 echo "Sorry, you are unable to push to trunk using a GUI client! Please use git CLI." - exit 1 - fi - - printf "%sYou're about to push to trunk, is that what you intended? [y/N]: %s" "$(tput setaf 3)" "$(tput sgr0)" - read -r PROCEED < /dev/tty - echo - - if [ "$(echo "${PROCEED:-n}" | tr "[:upper:]" "[:lower:]")" = "y" ]; then - echo "$(tput setaf 2)Brace yourself! Pushing to the trunk branch...$(tput sgr0)" - echo - exit 0 - fi - - echo "$(tput setaf 2)Push to trunk cancelled!$(tput sgr0)" - echo - exit 1 - fi -fi diff --git a/plugins/woocommerce/changelog/fix-exclude-drafts-in-reports b/plugins/woocommerce/changelog/fix-exclude-drafts-in-reports new file mode 100644 index 00000000000..354111e694b --- /dev/null +++ b/plugins/woocommerce/changelog/fix-exclude-drafts-in-reports @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Don't include draft orders in reports diff --git a/plugins/woocommerce/changelog/remove-admin-tests-folder b/plugins/woocommerce/changelog/remove-admin-tests-folder new file mode 100644 index 00000000000..7d8c33d848d --- /dev/null +++ b/plugins/woocommerce/changelog/remove-admin-tests-folder @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Simply remove woocommerce-admin tests folder + + diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-7.4.1 b/plugins/woocommerce/changelog/update-woocommerce-blocks-7.4.1 new file mode 100644 index 00000000000..85231c63d41 --- /dev/null +++ b/plugins/woocommerce/changelog/update-woocommerce-blocks-7.4.1 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Woo Blocks 7.3.0 & 7.4.1 diff --git a/plugins/woocommerce-admin/config/core.json b/plugins/woocommerce/client/admin/config/core.json similarity index 88% rename from plugins/woocommerce-admin/config/core.json rename to plugins/woocommerce/client/admin/config/core.json index 77477829a62..9a36495bc54 100644 --- a/plugins/woocommerce-admin/config/core.json +++ b/plugins/woocommerce/client/admin/config/core.json @@ -20,7 +20,6 @@ "store-alerts": true, "transient-notices": true, "wc-pay-promotion": true, - "wc-pay-welcome-page": true, - "tasklist-setup-experiment-1": false + "wc-pay-welcome-page": true } } diff --git a/plugins/woocommerce-admin/config/development.json b/plugins/woocommerce/client/admin/config/development.json similarity index 88% rename from plugins/woocommerce-admin/config/development.json rename to plugins/woocommerce/client/admin/config/development.json index a7c481c930b..b407ceeeb20 100644 --- a/plugins/woocommerce-admin/config/development.json +++ b/plugins/woocommerce/client/admin/config/development.json @@ -20,7 +20,6 @@ "store-alerts": true, "transient-notices": true, "wc-pay-promotion": true, - "wc-pay-welcome-page": true, - "tasklist-setup-experiment-1": false + "wc-pay-welcome-page": true } } diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index 47977376e15..0c4777b486a 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -21,7 +21,7 @@ "pelago/emogrifier": "^6.0", "psr/container": "1.0.0", "woocommerce/action-scheduler": "3.4.0", - "woocommerce/woocommerce-blocks": "7.2.1" + "woocommerce/woocommerce-blocks": "7.4.1" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4", diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock index 5899a2c583d..0ff06545e2f 100644 --- a/plugins/woocommerce/composer.lock +++ b/plugins/woocommerce/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "362a4c80079f72e193bb35b1e22c8598", + "content-hash": "03a229b123645dbf035c87685be1043a", "packages": [ { "name": "automattic/jetpack-autoloader", @@ -681,16 +681,16 @@ }, { "name": "woocommerce/woocommerce-blocks", - "version": "v7.2.0", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-gutenberg-products-block.git", - "reference": "52fd37a82aa522b9e39fcad11a67b3560c0ea9f0" + "reference": "bde2a5771ddc7970c2114da621c28b0f7b6296ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/52fd37a82aa522b9e39fcad11a67b3560c0ea9f0", - "reference": "52fd37a82aa522b9e39fcad11a67b3560c0ea9f0", + "url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/bde2a5771ddc7970c2114da621c28b0f7b6296ca", + "reference": "bde2a5771ddc7970c2114da621c28b0f7b6296ca", "shasum": "" }, "require": { @@ -734,9 +734,9 @@ ], "support": { "issues": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues", - "source": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/v7.2.0" + "source": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/v7.4.1" }, - "time": "2022-03-15T11:18:07+00:00" + "time": "2022-04-14T16:44:52+00:00" } ], "packages-dev": [ @@ -1198,16 +1198,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.6.0", + "version": "1.6.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706" + "reference": "77a32518733312af16a44300404e945338981de3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/93ebd0014cab80c4ea9f5e297ea48672f1b87706", - "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", + "reference": "77a32518733312af16a44300404e945338981de3", "shasum": "" }, "require": { @@ -1242,9 +1242,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" }, - "time": "2022-01-04T19:58:01+00:00" + "time": "2022-03-15T21:29:03+00:00" }, { "name": "phpspec/prophecy", @@ -3021,5 +3021,5 @@ "platform-overrides": { "php": "7.2" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.1.0" } diff --git a/plugins/woocommerce/i18n/countries.php b/plugins/woocommerce/i18n/countries.php index 73eb9ea040f..f38ed899f4d 100644 --- a/plugins/woocommerce/i18n/countries.php +++ b/plugins/woocommerce/i18n/countries.php @@ -225,7 +225,7 @@ return array( 'SD' => __( 'Sudan', 'woocommerce' ), 'SR' => __( 'Suriname', 'woocommerce' ), 'SJ' => __( 'Svalbard and Jan Mayen', 'woocommerce' ), - 'SZ' => __( 'Swaziland', 'woocommerce' ), + 'SZ' => __( 'Eswatini', 'woocommerce' ), 'SE' => __( 'Sweden', 'woocommerce' ), 'CH' => __( 'Switzerland', 'woocommerce' ), 'SY' => __( 'Syria', 'woocommerce' ), diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-data.php b/plugins/woocommerce/includes/abstracts/abstract-wc-data.php index eda6c546b7f..a9642de97c4 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-data.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-data.php @@ -640,7 +640,7 @@ abstract class WC_Data { } } if ( ! empty( $this->cache_group ) ) { - $cache_key = WC_Cache_Helper::get_cache_prefix( $this->cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $this->get_id() ) . 'object_meta_' . $this->get_id(); + $cache_key = self::generate_meta_cache_key( $this->get_id(), $this->cache_group ); wp_cache_delete( $cache_key, $this->cache_group ); } } diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php index abb2e63fbe9..83d59992a42 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php @@ -201,7 +201,8 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { } catch ( Exception $e ) { $message_id = $this->get_id() ? $this->get_id() : __( '(no ID)', 'woocommerce' ); - $this->handle_exception( $e, + $this->handle_exception( + $e, wp_kses_post( sprintf( /* translators: 1: Order ID or "(no ID)" if not known. */ @@ -551,15 +552,17 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { $old_status = $this->get_status(); $new_status = 'wc-' === substr( $new_status, 0, 3 ) ? substr( $new_status, 3 ) : $new_status; + $status_exceptions = array( 'auto-draft', 'trash' ); + // If setting the status, ensure it's set to a valid status. if ( true === $this->object_read ) { // Only allow valid new status. - if ( ! in_array( 'wc-' . $new_status, $this->get_valid_statuses(), true ) && 'trash' !== $new_status && 'auto-draft' !== $new_status ) { + if ( ! in_array( 'wc-' . $new_status, $this->get_valid_statuses(), true ) && ! in_array( $new_status, $status_exceptions, true ) ) { $new_status = 'pending'; } // If the old status is set but unknown (e.g. draft) assume its pending for action usage. - if ( $old_status && ! in_array( 'wc-' . $old_status, $this->get_valid_statuses(), true ) && 'trash' !== $old_status ) { + if ( $old_status && ! in_array( 'wc-' . $old_status, $this->get_valid_statuses(), true ) && ! in_array( $old_status, $status_exceptions, true ) ) { $old_status = 'pending'; } } diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php index 68a85d7cbe4..71ffba8a229 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php @@ -1202,44 +1202,90 @@ class WC_Product extends WC_Abstract_Legacy_Product { /** * Set downloads. * - * @since 3.0.0 + * @throws WC_Data_Exception If an error relating to one of the downloads is encountered. + * * @param array $downloads_array Array of WC_Product_Download objects or arrays. + * + * @since 3.0.0 */ public function set_downloads( $downloads_array ) { - $downloads = array(); - $errors = array(); + // When the object is first hydrated, only the previously persisted downloads will be passed in. + $existing_downloads = $this->get_object_read() ? (array) $this->get_prop( 'downloads' ) : $downloads_array; + $downloads = array(); + $errors = array(); + + $downloads_array = $this->build_downloads_map( $downloads_array ); + $existing_downloads = $this->build_downloads_map( $existing_downloads ); foreach ( $downloads_array as $download ) { - if ( is_a( $download, 'WC_Product_Download' ) ) { - $download_object = $download; - } else { - $download_object = new WC_Product_Download(); - - // If we don't have a previous hash, generate UUID for download. - if ( empty( $download['download_id'] ) ) { - $download['download_id'] = wp_generate_uuid4(); - } - - $download_object->set_id( $download['download_id'] ); - $download_object->set_name( $download['name'] ); - $download_object->set_file( $download['file'] ); - } + $download_id = $download->get_id(); + $is_new = ! isset( $existing_downloads[ $download_id ] ); try { - $download_object->check_is_valid(); - $downloads[ $download_object->get_id() ] = $download_object; + $download->check_is_valid( $this->get_object_read() ); + $downloads[ $download_id ] = $download; } catch ( Exception $e ) { - if ( $this->get_object_read() ) { + // We only add error messages for newly added downloads (let's not overwhelm the user if there are + // multiple existing files which are problematic). + if ( $is_new ) { $errors[] = $e->getMessage(); } - } - } - if ( $errors ) { - $this->error( 'product_invalid_download', $errors[0] ); + // If the problem is with an existing download, disable it. + if ( ! $is_new ) { + $download->set_enabled( false ); + $downloads[ $download_id ] = $download; + } + } } $this->set_prop( 'downloads', $downloads ); + + if ( $errors && $this->get_object_read() ) { + $this->error( 'product_invalid_download', $errors[0] ); + } + } + + /** + * Takes an array of downloadable file representations and converts it into an array of + * WC_Product_Download objects, indexed by download ID. + * + * @param array[]|WC_Product_Download[] $downloads Download data to be re-mapped. + * + * @return WC_Product_Download[] + */ + private function build_downloads_map( array $downloads ): array { + $downloads_map = array(); + + foreach ( $downloads as $download_data ) { + // If the item is already a WC_Product_Download we can add it to the map and move on. + if ( is_a( $download_data, 'WC_Product_Download' ) ) { + $downloads_map[ $download_data->get_id() ] = $download_data; + continue; + } + + // If the item is not an array, there is nothing else we can do (bad data). + if ( ! is_array( $download_data ) ) { + continue; + } + + // Otherwise, transform the array to a WC_Product_Download and add to the map. + $download_object = new WC_Product_Download(); + + // If we don't have a previous hash, generate UUID for download. + if ( empty( $download_data['download_id'] ) ) { + $download_data['download_id'] = wp_generate_uuid4(); + } + + $download_object->set_id( $download_data['download_id'] ); + $download_object->set_name( $download_data['name'] ); + $download_object->set_file( $download_data['file'] ); + $download_object->set_enabled( isset( $download_data['enabled'] ) ? $download_data['enabled'] : true ); + + $downloads_map[ $download_object->get_id() ] = $download_object; + } + + return $downloads_map; } /** diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-addons.php b/plugins/woocommerce/includes/admin/class-wc-admin-addons.php index 9fca11d5ec8..b47f5ecd3e1 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-addons.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-addons.php @@ -38,8 +38,7 @@ class WC_Admin_Addons { $raw_featured = wp_safe_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/1.0/featured', array( - 'headers' => $headers, - 'user-agent' => 'WooCommerce Addons Page', + 'headers' => $headers, ) ); @@ -82,8 +81,7 @@ class WC_Admin_Addons { $raw_featured = wp_safe_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/2.0/featured' . $parameter_string, array( - 'headers' => $headers, - 'user-agent' => 'WooCommerce Addons Page', + 'headers' => $headers, ) ); @@ -106,6 +104,7 @@ class WC_Admin_Addons { /* translators: %d: HTTP error code. */ $message = sprintf( esc_html( + /* translators: Error code */ __( 'Our request to the featured API got error code %d.', 'woocommerce' @@ -136,6 +135,13 @@ class WC_Admin_Addons { self::output_featured( $featured ); } + /** + * Check if the error is due to an SSL error + * + * @param string $error_message Error message. + * + * @return bool True if SSL error, false otherwise + */ public static function is_ssl_error( $error_message ) { return false !== stripos( $error_message, 'cURL error 35' ); } @@ -192,14 +198,23 @@ class WC_Admin_Addons { $response_code = (int) wp_remote_retrieve_response_code( $raw_extensions ); if ( 200 !== $response_code ) { do_action( 'woocommerce_page_wc-addons_connection_error', $response_code ); - return new WP_Error( 'error', __( "Our request to the search API got response code $response_code.", 'woocommerce' ) ); + return new WP_Error( + 'error', + sprintf( + esc_html( + /* translators: Error code */ + __( 'Our request to the search API got response code %s.', 'woocommerce' ) + ), + $response_code + ) + ); } $addons = json_decode( wp_remote_retrieve_body( $raw_extensions ) ); if ( ! is_object( $addons ) || ! isset( $addons->products ) ) { do_action( 'woocommerce_page_wc-addons_connection_error', 'Empty or malformed response' ); - return new WP_Error( 'error', __( "Our request to the search API got a malformed response.", 'woocommerce' ) ); + return new WP_Error( 'error', __( 'Our request to the search API got a malformed response.', 'woocommerce' ) ); } return $addons; @@ -258,7 +273,7 @@ class WC_Admin_Addons { if ( ! empty( $section->endpoint ) ) { $section_data = get_transient( 'wc_addons_section_' . $section_id ); if ( false === $section_data ) { - $raw_section = wp_safe_remote_get( esc_url_raw( $section->endpoint ), array( 'user-agent' => 'WooCommerce Addons Page' ) ); + $raw_section = wp_safe_remote_get( esc_url_raw( $section->endpoint ) ); if ( ! is_wp_error( $raw_section ) ) { $section_data = json_decode( wp_remote_retrieve_body( $raw_section ) ); @@ -963,6 +978,13 @@ class WC_Admin_Addons {
@@ -972,9 +994,9 @@ class WC_Admin_Addons {

WooCommerce.com, where you\'ll find the most popular WooCommerce extensions.', 'woocommerce' diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php index 3528be7e071..1f6542918b8 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php @@ -61,7 +61,7 @@ class WC_Admin_Menus { $menu[] = array( '', 'read', 'separator-woocommerce', '', 'wp-menu-separator woocommerce' ); // WPCS: override ok. } - add_menu_page( __( 'WooCommerce', 'woocommerce' ), __( 'WooCommerce', 'woocommerce' ), 'edit_others_shop_orders', 'woocommerce', null, $woocommerce_icon, 55 ); + add_menu_page( __( 'WooCommerce', 'woocommerce' ), __( 'WooCommerce', 'woocommerce' ), 'edit_others_shop_orders', 'woocommerce', null, $woocommerce_icon, '55.5' ); add_submenu_page( 'edit.php?post_type=product', __( 'Attributes', 'woocommerce' ), __( 'Attributes', 'woocommerce' ), 'manage_product_terms', 'product_attributes', array( $this, 'attributes_page' ) ); } @@ -73,7 +73,7 @@ class WC_Admin_Menus { if ( self::can_view_woocommerce_menu_item() ) { add_submenu_page( 'woocommerce', __( 'Reports', 'woocommerce' ), __( 'Reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ) ); } else { - add_menu_page( __( 'Sales reports', 'woocommerce' ), __( 'Sales reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ), 'dashicons-chart-bar', 56 ); + add_menu_page( __( 'Sales reports', 'woocommerce' ), __( 'Sales reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ), 'dashicons-chart-bar', '55.6' ); } } diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php b/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php index 2eaef024a14..2a5ca1281f0 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-post-types.php @@ -928,7 +928,7 @@ class WC_Admin_Post_Types { return false; } - $old_price = $product->{"get_{$price_type}_price"}(); + $old_price = (float) $product->{"get_{$price_type}_price"}(); $price_changed = false; $change_price = absint( $request_data[ "change_{$price_type}_price" ] ); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php index 69506066749..a6a2499c7c1 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php @@ -7,6 +7,7 @@ */ use Automattic\Jetpack\Constants; +use Automattic\WooCommerce\Utilities\ArrayUtil; if ( ! defined( 'ABSPATH' ) ) { exit; @@ -273,6 +274,12 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) : } break; + case 'info': + echo ''; + echo wp_kses_post( wpautop( wptexturize( $value['text'] ) ) ); + echo ''; + break; + // Section Ends. case 'sectionend': if ( ! empty( $value['id'] ) ) { @@ -417,6 +424,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) : // Radio inputs. case 'radio': $option_value = $value['value']; + $disabled_values = ArrayUtil::get_value_or_default($value, 'disabled', array()); ?> @@ -435,6 +443,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) : name="" value="" type="radio" + style="" class="" diff --git a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php index 848b36ad77b..17b13f81653 100644 --- a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php +++ b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php @@ -86,7 +86,6 @@ class WC_Product_CSV_Importer_Controller { /** * Check whether a file is a valid CSV file. * - * @todo Replace this method with wc_is_file_valid_csv() function. * @param string $file File path. * @param bool $check_path Whether to also check the file is located in a valid location (Default: true). * @return bool @@ -98,17 +97,7 @@ class WC_Product_CSV_Importer_Controller { * @param bool $check_import_file_path If the import file path should be checked. * @param string $file Path of the file to be checked. */ - if ( $check_path && apply_filters( 'woocommerce_product_csv_importer_check_import_file_path', true, $file ) && false !== stripos( $file, '://' ) ) { - return false; - } - - $valid_filetypes = self::get_valid_csv_filetypes(); - $filetype = wp_check_filetype( $file, $valid_filetypes ); - if ( in_array( $filetype['type'], $valid_filetypes, true ) ) { - return true; - } - - return false; + return wc_is_file_valid_csv( $file, $check_path ); } /** diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-general.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-general.php index db243413896..cff71f755d8 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-general.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-general.php @@ -88,9 +88,13 @@ defined( 'ABSPATH' ) || exit; get_downloads( 'edit' ); + $downloadable_files = $product_object->get_downloads( 'edit' ); + $disabled_downloads_count = 0; + if ( $downloadable_files ) { foreach ( $downloadable_files as $key => $file ) { + $disabled_download = isset( $file['enabled'] ) && false === $file['enabled']; + $disabled_downloads_count += (int) $disabled_download; include __DIR__ . '/html-product-download.php'; } } @@ -98,7 +102,7 @@ defined( 'ABSPATH' ) || exit; - + + + + * + ', + '' + ); + ?> + + diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-download.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-download.php index 621b785c4cd..437af94dc91 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-download.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-download.php @@ -1,4 +1,14 @@ " name="_wc_file_names[]" value="" /> - + + + + * + + diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-variation-download.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-variation-download.php index 6c39752cc0f..f8eba6dd29d 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-variation-download.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-variation-download.php @@ -1,4 +1,15 @@ " name="_wc_variation_file_names[][]" value="" /> - + + + + * + + diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php index 2bfdc85b8f3..5402c3576d1 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php @@ -404,10 +404,13 @@ defined( 'ABSPATH' ) || exit; get_downloads( 'edit' ); + $downloadable_files = $variation_object->get_downloads( 'edit' ); + $disabled_downloads_count = 0; - if ( $downloads ) { - foreach ( $downloads as $key => $file ) { + if ( $downloadable_files ) { + foreach ( $downloadable_files as $key => $file ) { + $disabled_download = isset( $file['enabled'] ) && false === $file['enabled']; + $disabled_downloads_count += (int) $disabled_download; include __DIR__ . '/html-product-variation-download.php'; } } @@ -415,7 +418,7 @@ defined( 'ABSPATH' ) || exit;

- + + + + * + ', + '' + ); + ?> + +
diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php index 6d4362ecb83..f3a798954f6 100644 --- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php +++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php @@ -5,6 +5,8 @@ * @package WooCommerce\Admin */ +use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayments; + defined( 'ABSPATH' ) || exit; if ( class_exists( 'WC_Settings_Payment_Gateways', false ) ) { @@ -201,6 +203,27 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { echo ''; } + /** + * Add "Other payment methods" link in WooCommerce -> Settings -> Payments + * When the store is in WC Payments eligible country. + * See https://github.com/woocommerce/woocommerce/issues/32130 for more details. + */ + if ( WooCommercePayments::is_supported() ) { + $columns_count = count( $columns ); + $link_text = __( 'Other payment methods', 'woocommerce' ); + $external_link_icon = ''; + echo ''; + // phpcs:ignore -- ignoring the error since the value is harded. + echo ""; + echo ""; + // phpcs:ignore + echo $link_text; + // phpcs:ignore + echo $external_link_icon; + echo ''; + echo ''; + echo ''; + } ?> diff --git a/plugins/woocommerce/includes/class-wc-auth.php b/plugins/woocommerce/includes/class-wc-auth.php index 23fdf0861ce..38aede56ad0 100644 --- a/plugins/woocommerce/includes/class-wc-auth.php +++ b/plugins/woocommerce/includes/class-wc-auth.php @@ -324,6 +324,36 @@ class WC_Auth { // Login endpoint. if ( 'login' === $route && ! is_user_logged_in() ) { + /** + * If a merchant is using the WordPress SSO (handled through Jetpack) + * to manage their authorisation then it is likely they'll find that + * their username and password do not work through this form. We + * instead need to redirect them to the WordPress login so that they + * can then be redirected back here with a valid token. + */ + + // Check if Jetpack is installed and activated. + if ( class_exists( 'Jetpack' ) && Jetpack::connection()->is_active() ) { + + // Check if the user is using the WordPress.com SSO. + if ( Jetpack::is_module_active( 'sso' ) ) { + + $redirect_url = $this->build_url( $data, 'authorize' ); + + // Build the SSO URL. + $login_url = Jetpack_SSO::get_instance()->build_sso_button_url( + array( + 'redirect_to' => rawurlencode( esc_url_raw( $redirect_url ) ), + 'action' => 'login', + ) + ); + + // Perform the redirect. + wp_safe_redirect( $login_url ); + exit; + } + } + wc_get_template( 'auth/form-login.php', array( diff --git a/plugins/woocommerce/includes/class-wc-download-handler.php b/plugins/woocommerce/includes/class-wc-download-handler.php index 97f0961fed5..93e0cebad01 100644 --- a/plugins/woocommerce/includes/class-wc-download-handler.php +++ b/plugins/woocommerce/includes/class-wc-download-handler.php @@ -276,9 +276,17 @@ class WC_Download_Handler { // Paths that begin with '//' are always remote URLs. if ( '//' === substr( $file_path, 0, 2 ) ) { + $file_path = ( is_ssl() ? 'https:' : 'http:' ) . $file_path; + + /** + * Filter the remote filepath for download. + * + * @since 6.5.0 + * @param string $file_path File path. + */ return array( 'remote_file' => true, - 'file_path' => is_ssl() ? 'https:' . $file_path : 'http:' . $file_path, + 'file_path' => apply_filters( 'woocommerce_download_parse_remote_file_path', $file_path ), ); } @@ -297,9 +305,16 @@ class WC_Download_Handler { $file_path = $parsed_file_path['path']; } + /** + * Filter the filepath for download. + * + * @since 6.5.0 + * @param string $file_path File path. + * @param bool $remote_file Remote File Indicator. + */ return array( 'remote_file' => $remote_file, - 'file_path' => $file_path, + 'file_path' => apply_filters( 'woocommerce_download_parse_file_path', $file_path, $remote_file ), ); } diff --git a/plugins/woocommerce/includes/class-wc-form-handler.php b/plugins/woocommerce/includes/class-wc-form-handler.php index b182f6a6de0..3ec4872d871 100644 --- a/plugins/woocommerce/includes/class-wc-form-handler.php +++ b/plugins/woocommerce/includes/class-wc-form-handler.php @@ -347,7 +347,7 @@ class WC_Form_Handler { do_action( 'woocommerce_save_account_details', $user->ID ); - wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); + wp_safe_redirect( wc_get_endpoint_url( 'edit-account', '', wc_get_page_permalink( 'myaccount' ) ) ); exit; } } diff --git a/plugins/woocommerce/includes/class-wc-geo-ip.php b/plugins/woocommerce/includes/class-wc-geo-ip.php index c79be25b985..d856be2848a 100644 --- a/plugins/woocommerce/includes/class-wc-geo-ip.php +++ b/plugins/woocommerce/includes/class-wc-geo-ip.php @@ -843,7 +843,7 @@ class WC_Geo_IP { 'Sao Tome and Principe', 'El Salvador', 'Syrian Arab Republic', - 'Swaziland', + 'Eswatini', 'Turks and Caicos Islands', 'Chad', 'French Southern Territories', diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php index 2bac9ea3334..d19c54df066 100644 --- a/plugins/woocommerce/includes/class-wc-install.php +++ b/plugins/woocommerce/includes/class-wc-install.php @@ -7,6 +7,7 @@ */ use Automattic\Jetpack\Constants; +use Automattic\WooCommerce\Admin\Notes\Notes; use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator; use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories; use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchronize as Download_Directories_Sync; @@ -158,10 +159,15 @@ class WC_Install { 'wc_update_product_lookup_tables', 'wc_update_400_increase_size_of_column', 'wc_update_400_reset_action_scheduler_migration_status', + 'wc_admin_update_0201_order_status_index', + 'wc_admin_update_0230_rename_gross_total', + 'wc_admin_update_0251_remove_unsnooze_action', 'wc_update_400_db_version', ), '4.4.0' => array( 'wc_update_440_insert_attribute_terms_for_variable_products', + 'wc_admin_update_110_remove_facebook_note', + 'wc_admin_update_130_remove_dismiss_action_from_tracking_opt_in_note', 'wc_update_440_db_version', ), '4.5.0' => array( @@ -170,6 +176,8 @@ class WC_Install { ), '5.0.0' => array( 'wc_update_500_fix_product_review_count', + 'wc_admin_update_160_remove_facebook_note', + 'wc_admin_update_170_homescreen_layout', 'wc_update_500_db_version', ), '5.6.0' => array( @@ -178,15 +186,22 @@ class WC_Install { ), '6.0.0' => array( 'wc_update_600_migrate_rate_limit_options', + 'wc_admin_update_270_delete_report_downloads', + 'wc_admin_update_271_update_task_list_options', + 'wc_admin_update_280_order_status', + 'wc_admin_update_290_update_apperance_task_option', + 'wc_admin_update_290_delete_default_homepage_layout_option', 'wc_update_600_db_version', ), '6.3.0' => array( 'wc_update_630_create_product_attributes_lookup_table', + 'wc_admin_update_300_update_is_read_from_last_read', 'wc_update_630_db_version', ), '6.4.0' => array( 'wc_update_640_add_primary_key_to_product_attributes_lookup_table', 'wc_update_640_approved_download_directories', + 'wc_admin_update_340_remove_is_primary_from_note_action', 'wc_update_640_db_version', ), ); @@ -215,9 +230,18 @@ class WC_Install { * This check is done on all requests and runs if the versions do not match. */ public static function check_version() { - if ( ! Constants::is_defined( 'IFRAME_REQUEST' ) && version_compare( get_option( 'woocommerce_version' ), WC()->version, '<' ) ) { + $wc_version = get_option( 'woocommerce_version' ); + $wc_code_version = WC()->version; + $requires_update = version_compare( $wc_version, $wc_code_version, '<' ); + if ( ! Constants::is_defined( 'IFRAME_REQUEST' ) && $requires_update ) { self::install(); do_action( 'woocommerce_updated' ); + do_action_deprecated( 'woocommerce_admin_updated', array(), $wc_code_version, 'woocommerce_updated' ); + // If there is no woocommerce_version option, consider it as a new install. + if ( ! $wc_version ) { + do_action( 'woocommerce_newly_installed' ); + do_action_deprecated( 'woocommerce_admin_newly_installed', array(), $wc_code_version, 'woocommerce_newly_installed' ); + } } } @@ -321,7 +345,7 @@ class WC_Install { } // Check if we are not already running this routine. - if ( 'yes' === get_transient( 'wc_installing' ) ) { + if ( self::is_installing() ) { return; } @@ -334,10 +358,12 @@ class WC_Install { self::create_tables(); self::verify_base_tables(); self::create_options(); + self::migrate_options(); self::create_roles(); self::setup_environment(); self::create_terms(); self::create_cron_jobs(); + self::delete_obsolete_notes(); self::create_files(); self::maybe_create_pages(); self::maybe_set_activation_transients(); @@ -347,8 +373,22 @@ class WC_Install { delete_transient( 'wc_installing' ); + // Use add_option() here to avoid overwriting this value with each + // plugin version update. We base plugin age off of this value. + add_option( 'woocommerce_admin_install_timestamp', time() ); + do_action( 'woocommerce_flush_rewrite_rules' ); do_action( 'woocommerce_installed' ); + do_action( 'woocommerce_admin_installed' ); + } + + /** + * Returns true if we're installing. + * + * @return bool + */ + private static function is_installing() { + return 'yes' === get_transient( 'wc_installing' ); } /** @@ -581,6 +621,12 @@ class WC_Install { wp_schedule_event( time() + MINUTE_IN_SECONDS, 'fifteendays', 'woocommerce_geoip_updater' ); wp_schedule_event( time() + 10, apply_filters( 'woocommerce_tracker_event_recurrence', 'daily' ), 'woocommerce_tracker_send_event' ); wp_schedule_event( time() + ( 3 * HOUR_IN_SECONDS ), 'daily', 'woocommerce_cleanup_rate_limits' ); + + if ( ! wp_next_scheduled( 'wc_admin_daily' ) ) { + wp_schedule_event( time(), 'daily', 'wc_admin_daily' ); + } + // Note: this is potentially redundant when the core package exists. + wp_schedule_single_event( time() + 10, 'generate_category_lookup_table' ); } /** @@ -690,6 +736,94 @@ class WC_Install { } } + /** + * Delete obsolete notes. + */ + public static function delete_obsolete_notes() { + global $wpdb; + $obsolete_notes_names = array( + 'wc-admin-welcome-note', + 'wc-admin-store-notice-setting-moved', + 'wc-admin-store-notice-giving-feedback', + 'wc-admin-learn-more-about-product-settings', + 'wc-admin-onboarding-profiler-reminder', + 'wc-admin-historical-data', + 'wc-admin-review-shipping-settings', + 'wc-admin-home-screen-feedback', + 'wc-admin-effortless-payments-by-mollie', + 'wc-admin-google-ads-and-marketing', + 'wc-admin-marketing-intro', + 'wc-admin-draw-attention', + 'wc-admin-need-some-inspiration', + 'wc-admin-choose-niche', + 'wc-admin-start-dropshipping-business', + 'wc-admin-filter-by-product-variations-in-reports', + 'wc-admin-learn-more-about-variable-products', + 'wc-admin-getting-started-ecommerce-webinar', + 'wc-admin-navigation-feedback', + 'wc-admin-navigation-feedback-follow-up', + ); + + $additional_obsolete_notes_names = apply_filters( + 'woocommerce_admin_obsolete_notes_names', + array() + ); + + if ( is_array( $additional_obsolete_notes_names ) ) { + $obsolete_notes_names = array_merge( + $obsolete_notes_names, + $additional_obsolete_notes_names + ); + } + + foreach ( $obsolete_notes_names as $obsolete_notes_name ) { + $wpdb->delete( $wpdb->prefix . 'wc_admin_notes', array( 'name' => $obsolete_notes_name ) ); + $wpdb->delete( $wpdb->prefix . 'wc_admin_note_actions', array( 'name' => $obsolete_notes_name ) ); + } + } + + /** + * Migrate option values to their new keys/names. + */ + public static function migrate_options() { + + $migrated_options = array( + 'woocommerce_onboarding_profile' => 'wc_onboarding_profile', + 'woocommerce_admin_install_timestamp' => 'wc_admin_install_timestamp', + 'woocommerce_onboarding_opt_in' => 'wc_onboarding_opt_in', + 'woocommerce_admin_import_stats' => 'wc_admin_import_stats', + 'woocommerce_admin_version' => 'wc_admin_version', + 'woocommerce_admin_last_orders_milestone' => 'wc_admin_last_orders_milestone', + 'woocommerce_admin-wc-helper-last-refresh' => 'wc-admin-wc-helper-last-refresh', + 'woocommerce_admin_report_export_status' => 'wc_admin_report_export_status', + 'woocommerce_task_list_complete' => 'woocommerce_task_list_complete', + 'woocommerce_task_list_hidden' => 'woocommerce_task_list_hidden', + 'woocommerce_extended_task_list_complete' => 'woocommerce_extended_task_list_complete', + 'woocommerce_extended_task_list_hidden' => 'woocommerce_extended_task_list_hidden', + ); + + wc_maybe_define_constant( 'WC_ADMIN_MIGRATING_OPTIONS', true ); + + foreach ( $migrated_options as $new_option => $old_option ) { + $old_option_value = get_option( $old_option, false ); + + // Continue if no option value was previously set. + if ( false === $old_option_value ) { + continue; + } + + if ( '1' === $old_option_value ) { + $old_option_value = 'yes'; + } elseif ( '0' === $old_option_value ) { + $old_option_value = 'no'; + } + + update_option( $new_option, $old_option_value ); + if ( $new_option !== $old_option ) { + delete_option( $old_option ); + } + } + } /** * Add the default terms for WC taxonomies - product types and order statuses. Modify this at your own risk. */ @@ -762,7 +896,7 @@ class WC_Install { * woocommerce_tax_rates - Tax Rates are stored inside 2 tables making tax queries simple and efficient. * woocommerce_tax_rate_locations - Each rate can be applied to more than one postcode/city hence the second table. */ - private static function create_tables() { + public static function create_tables() { global $wpdb; $wpdb->hide_errors(); @@ -1096,6 +1230,119 @@ CREATE TABLE {$wpdb->prefix}wc_product_download_directories ( enabled TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (url_id), KEY `url` (`url`) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_order_stats ( + order_id bigint(20) unsigned NOT NULL, + parent_id bigint(20) unsigned DEFAULT 0 NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + date_created_gmt datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + num_items_sold int(11) DEFAULT 0 NOT NULL, + total_sales double DEFAULT 0 NOT NULL, + tax_total double DEFAULT 0 NOT NULL, + shipping_total double DEFAULT 0 NOT NULL, + net_total double DEFAULT 0 NOT NULL, + returning_customer boolean DEFAULT NULL, + status varchar(200) NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (order_id), + KEY date_created (date_created), + KEY customer_id (customer_id), + KEY status (status({$max_index_length})) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_order_product_lookup ( + order_item_id BIGINT UNSIGNED NOT NULL, + order_id BIGINT UNSIGNED NOT NULL, + product_id BIGINT UNSIGNED NOT NULL, + variation_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + product_qty INT NOT NULL, + product_net_revenue double DEFAULT 0 NOT NULL, + product_gross_revenue double DEFAULT 0 NOT NULL, + coupon_amount double DEFAULT 0 NOT NULL, + tax_amount double DEFAULT 0 NOT NULL, + shipping_amount double DEFAULT 0 NOT NULL, + shipping_tax_amount double DEFAULT 0 NOT NULL, + PRIMARY KEY (order_item_id), + KEY order_id (order_id), + KEY product_id (product_id), + KEY customer_id (customer_id), + KEY date_created (date_created) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_order_tax_lookup ( + order_id BIGINT UNSIGNED NOT NULL, + tax_rate_id BIGINT UNSIGNED NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + shipping_tax double DEFAULT 0 NOT NULL, + order_tax double DEFAULT 0 NOT NULL, + total_tax double DEFAULT 0 NOT NULL, + PRIMARY KEY (order_id, tax_rate_id), + KEY tax_rate_id (tax_rate_id), + KEY date_created (date_created) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_order_coupon_lookup ( + order_id BIGINT UNSIGNED NOT NULL, + coupon_id BIGINT NOT NULL, + date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, + discount_amount double DEFAULT 0 NOT NULL, + PRIMARY KEY (order_id, coupon_id), + KEY coupon_id (coupon_id), + KEY date_created (date_created) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_admin_notes ( + note_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + name varchar(255) NOT NULL, + type varchar(20) NOT NULL, + locale varchar(20) NOT NULL, + title longtext NOT NULL, + content longtext NOT NULL, + content_data longtext NULL default null, + status varchar(200) NOT NULL, + source varchar(200) NOT NULL, + date_created datetime NOT NULL default '0000-00-00 00:00:00', + date_reminder datetime NULL default null, + is_snoozable boolean DEFAULT 0 NOT NULL, + layout varchar(20) DEFAULT '' NOT NULL, + image varchar(200) NULL DEFAULT NULL, + is_deleted boolean DEFAULT 0 NOT NULL, + is_read boolean DEFAULT 0 NOT NULL, + icon varchar(200) NOT NULL default 'info', + PRIMARY KEY (note_id) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_admin_note_actions ( + action_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + note_id BIGINT UNSIGNED NOT NULL, + name varchar(255) NOT NULL, + label varchar(255) NOT NULL, + query longtext NOT NULL, + status varchar(255) NOT NULL, + actioned_text varchar(255) NOT NULL, + nonce_action varchar(255) NULL DEFAULT NULL, + nonce_name varchar(255) NULL DEFAULT NULL, + PRIMARY KEY (action_id), + KEY note_id (note_id) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_customer_lookup ( + customer_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED DEFAULT NULL, + username varchar(60) DEFAULT '' NOT NULL, + first_name varchar(255) NOT NULL, + last_name varchar(255) NOT NULL, + email varchar(100) NULL default NULL, + date_last_active timestamp NULL default null, + date_registered timestamp NULL default null, + country char(2) DEFAULT '' NOT NULL, + postcode varchar(20) DEFAULT '' NOT NULL, + city varchar(100) DEFAULT '' NOT NULL, + state varchar(100) DEFAULT '' NOT NULL, + PRIMARY KEY (customer_id), + UNIQUE KEY user_id (user_id), + KEY email (email) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_category_lookup ( + category_tree_id BIGINT UNSIGNED NOT NULL, + category_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (category_tree_id,category_id) ) $collate; "; @@ -1134,6 +1381,16 @@ CREATE TABLE {$wpdb->prefix}wc_product_download_directories ( "{$wpdb->prefix}wc_reserved_stock", "{$wpdb->prefix}wc_rate_limits", wc_get_container()->get( DataRegenerator::class )->get_lookup_table_name(), + + // WCA Tables. + "{$wpdb->prefix}wc_order_stats", + "{$wpdb->prefix}wc_order_product_lookup", + "{$wpdb->prefix}wc_order_tax_lookup", + "{$wpdb->prefix}wc_order_coupon_lookup", + "{$wpdb->prefix}wc_admin_notes", + "{$wpdb->prefix}wc_admin_note_actions", + "{$wpdb->prefix}wc_customer_lookup", + "{$wpdb->prefix}wc_category_lookup", ); /** diff --git a/plugins/woocommerce/includes/class-wc-order-refund.php b/plugins/woocommerce/includes/class-wc-order-refund.php index c384d044d65..b24ccc60619 100644 --- a/plugins/woocommerce/includes/class-wc-order-refund.php +++ b/plugins/woocommerce/includes/class-wc-order-refund.php @@ -83,7 +83,7 @@ class WC_Order_Refund extends WC_Abstract_Order { * * @since 2.2 * @param string $context What the value is for. Valid values are view and edit. - * @return int|float + * @return string */ public function get_reason( $context = 'view' ) { return $this->get_prop( 'reason', $context ); @@ -219,7 +219,7 @@ class WC_Order_Refund extends WC_Abstract_Order { * Get refund reason. * * @deprecated 3.0 - * @return int|float + * @return string */ public function get_refund_reason() { wc_deprecated_function( 'get_refund_reason', '3.0', 'get_reason' ); diff --git a/plugins/woocommerce/includes/class-wc-product-download.php b/plugins/woocommerce/includes/class-wc-product-download.php index a0f47b516e7..6f4f234e594 100644 --- a/plugins/woocommerce/includes/class-wc-product-download.php +++ b/plugins/woocommerce/includes/class-wc-product-download.php @@ -25,9 +25,10 @@ class WC_Product_Download implements ArrayAccess { * @var array */ protected $data = array( - 'id' => '', - 'name' => '', - 'file' => '', + 'id' => '', + 'name' => '', + 'file' => '', + 'enabled' => true, ); /** @@ -106,10 +107,20 @@ class WC_Product_Download implements ArrayAccess { public function check_is_valid( bool $auto_add_to_approved_directory_list = true ) { $download_file = $this->get_file(); + if ( ! $this->data['enabled'] ) { + throw new Exception( + sprintf( + /* translators: %s: Downloadable file. */ + __( 'The downloadable file %s cannot be used as it has been disabled.', 'woocommerce' ), + '' . basename( $download_file ) . '' + ) + ); + } + if ( ! $this->is_allowed_filetype() ) { throw new Exception( sprintf( - /* translators: %1$s: Downloadable file */ + /* translators: 1: Downloadable file, 2: List of allowed filetypes. */ __( 'The downloadable file %1$s cannot be used as it does not have an allowed file type. Allowed types include: %2$s', 'woocommerce' ), '' . basename( $download_file ) . '', '' . implode( ', ', array_keys( $this->get_allowed_mime_types() ) ) . '' @@ -121,7 +132,7 @@ class WC_Product_Download implements ArrayAccess { if ( ! $this->file_exists() ) { throw new Exception( sprintf( - /* translators: %s: Downloadable file */ + /* translators: %s: Downloadable file */ __( 'The downloadable file %s cannot be used as it does not exist on the server.', 'woocommerce' ), '' . $download_file . '' ) @@ -201,35 +212,42 @@ class WC_Product_Download implements ArrayAccess { return; } - $download_file = $this->get_file(); + $download_file = $this->get_file(); + + /** + * Controls whether shortcodes should be resolved and validated using the Approved Download Directory feature. + * + * @param bool $should_validate + */ + if ( apply_filters( 'woocommerce_product_downloads_approved_directory_validation_for_shortcodes', true ) && 'shortcode' === $this->get_type_of_file_path() ) { + $download_file = do_shortcode( $download_file ); + } + $is_site_administrator = is_multisite() ? current_user_can( 'manage_sites' ) : current_user_can( 'manage_options' ); $valid_storage_directory = $download_directories->is_valid_path( $download_file ); - if ( ! $valid_storage_directory && $auto_add_to_approved_directory_list ) { + if ( $valid_storage_directory ) { + return; + } + + if ( $auto_add_to_approved_directory_list ) { try { // Add the parent URL to the approved directories list, but *do not enable it* unless the current user is a site admin. $download_directories->add_approved_directory( ( new URL( $download_file ) )->get_parent_url(), $is_site_administrator ); - } catch ( Exception $e ) { - /* translators: %s: Downloadable file */ - throw new Exception( - sprintf( - /* translators: %1$s is the downloadable file path, %2$s is an opening link tag, %3%s is a closing link tag. */ - __( 'The downloadable file %1$s cannot be used: it is not located in an approved directory. Please contact a site administrator and request their approval. %2$sLearn more.%3$s', 'woocommerce' ), - '' . $download_file . '', - '', // @todo update to working link (see https://github.com/Automattic/woocommerce/issues/181) - '' - ) - ); + $valid_storage_directory = $download_directories->is_valid_path( $download_file ); + } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + // At this point, $valid_storage_directory will be false. Fall-through so the appropriate exception is + // triggered (same as if the storage directory was invalid and $auto_add_to_approved_directory_list was false. } } - if ( ! $valid_storage_directory && ! $is_site_administrator ) { + if ( ! $valid_storage_directory ) { throw new Exception( sprintf( /* translators: %1$s is the downloadable file path, %2$s is an opening link tag, %3%s is a closing link tag. */ __( 'The downloadable file %1$s cannot be used: it is not located in an approved directory. Please contact a site administrator for help. %2$sLearn more.%3$s', 'woocommerce' ), '' . $download_file . '', - '', // @todo update to working link (see https://github.com/Automattic/woocommerce/issues/181) + '', '' ) ); @@ -292,6 +310,15 @@ class WC_Product_Download implements ArrayAccess { } } + /** + * Sets the status of the download to enabled (true) or disabled (false). + * + * @param bool $enabled True indicates the downloadable file is enabled, false indicates it is disabled. + */ + public function set_enabled( bool $enabled = true ) { + $this->data['enabled'] = $enabled; + } + /* |-------------------------------------------------------------------------- | Getters @@ -336,6 +363,15 @@ class WC_Product_Download implements ArrayAccess { return $this->data['file']; } + /** + * Get status of the download. + * + * @return bool + */ + public function get_enabled(): bool { + return $this->data['enabled']; + } + /* |-------------------------------------------------------------------------- | ArrayAccess/Backwards compatibility. diff --git a/plugins/woocommerce/includes/class-wc-tracker.php b/plugins/woocommerce/includes/class-wc-tracker.php index a7e9c6fc082..bda59973044 100644 --- a/plugins/woocommerce/includes/class-wc-tracker.php +++ b/plugins/woocommerce/includes/class-wc-tracker.php @@ -181,15 +181,17 @@ class WC_Tracker { * @return array */ public static function get_theme_info() { - $theme_data = wp_get_theme(); - $theme_child_theme = wc_bool_to_string( is_child_theme() ); - $theme_wc_support = wc_bool_to_string( current_theme_supports( 'woocommerce' ) ); + $theme_data = wp_get_theme(); + $theme_child_theme = wc_bool_to_string( is_child_theme() ); + $theme_wc_support = wc_bool_to_string( current_theme_supports( 'woocommerce' ) ); + $theme_is_block_theme = wc_bool_to_string( wc_current_theme_is_fse_theme() ); return array( 'name' => $theme_data->Name, // @phpcs:ignore 'version' => $theme_data->Version, // @phpcs:ignore 'child_theme' => $theme_child_theme, 'wc_support' => $theme_wc_support, + 'block_theme' => $theme_is_block_theme, ); } diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php index 5970ed38b90..92a51a6bf57 100644 --- a/plugins/woocommerce/includes/class-woocommerce.php +++ b/plugins/woocommerce/includes/class-woocommerce.php @@ -29,7 +29,7 @@ final class WooCommerce { * * @var string */ - public $version = '6.4.0'; + public $version = '6.5.0'; /** * WooCommerce Schema version. diff --git a/plugins/woocommerce/includes/react-admin/wc-admin-update-functions.php b/plugins/woocommerce/includes/react-admin/wc-admin-update-functions.php index 368d284d3bf..c9f2a689192 100644 --- a/plugins/woocommerce/includes/react-admin/wc-admin-update-functions.php +++ b/plugins/woocommerce/includes/react-admin/wc-admin-update-functions.php @@ -7,11 +7,9 @@ * @package WooCommerce\Admin */ -use Automattic\WooCommerce\Internal\Admin\Install as Installer; use \Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists; use \Automattic\WooCommerce\Admin\Notes\Notes; use \Automattic\WooCommerce\Internal\Admin\Notes\UnsecuredReportFiles; -use \Automattic\WooCommerce\Internal\Admin\Notes\DeactivatePlugin; use \Automattic\WooCommerce\Admin\ReportExporter; /** @@ -40,13 +38,6 @@ function wc_admin_update_0201_order_status_index() { $wpdb->query( $wpdb->prepare( "ALTER TABLE {$wpdb->prefix}wc_order_stats ADD INDEX status (status(%d))", $max_index_length ) ); } -/** - * Update DB Version. - */ -function wc_admin_update_0201_db_version() { - Installer::update_db_version( '0.20.1' ); -} - /** * Rename "gross_total" to "total_sales". * See: https://github.com/woocommerce/woocommerce-admin/issues/3175 @@ -60,13 +51,6 @@ function wc_admin_update_0230_rename_gross_total() { $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_order_stats CHANGE COLUMN `gross_total` `total_sales` double DEFAULT 0 NOT NULL" ); } -/** - * Update DB Version. - */ -function wc_admin_update_0230_db_version() { - Installer::update_db_version( '0.23.0' ); -} - /** * Remove the note unsnoozing scheduled action. */ @@ -75,13 +59,6 @@ function wc_admin_update_0251_remove_unsnooze_action() { as_unschedule_action( Notes::UNSNOOZE_HOOK, null, 'wc-admin-notes' ); } -/** - * Update DB Version. - */ -function wc_admin_update_0251_db_version() { - Installer::update_db_version( '0.25.1' ); -} - /** * Remove Facebook Extension note. */ @@ -89,13 +66,6 @@ function wc_admin_update_110_remove_facebook_note() { Notes::delete_notes_with_name( 'wc-admin-facebook-extension' ); } -/** - * Update DB Version. - */ -function wc_admin_update_110_db_version() { - Installer::update_db_version( '1.1.0' ); -} - /** * Remove Dismiss action from tracking opt-in admin note. */ @@ -106,21 +76,13 @@ function wc_admin_update_130_remove_dismiss_action_from_tracking_opt_in_note() { } /** + * Update DB Version. */ function wc_admin_update_130_db_version() { Installer::update_db_version( '1.3.0' ); } -/** - * Change the deactivate plugin note type to 'info'. - */ -function wc_admin_update_140_change_deactivate_plugin_note_type() { - global $wpdb; - - $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}wc_admin_notes SET type = 'info' WHERE name = %s", DeactivatePlugin::NOTE_NAME ) ); -} - /** * Update DB Version. */ @@ -135,13 +97,6 @@ function wc_admin_update_160_remove_facebook_note() { Notes::delete_notes_with_name( 'wc-admin-facebook-marketing-expert' ); } -/** - * Update DB Version. - */ -function wc_admin_update_160_db_version() { - Installer::update_db_version( '1.6.0' ); -} - /** * Set "two column" homescreen layout as default for existing stores. */ @@ -149,13 +104,6 @@ function wc_admin_update_170_homescreen_layout() { add_option( 'woocommerce_default_homepage_layout', 'two_columns', '', 'no' ); } -/** - * Update DB Version. - */ -function wc_admin_update_170_db_version() { - Installer::update_db_version( '1.7.0' ); -} - /** * Delete the preexisting export files. */ @@ -241,13 +189,6 @@ function wc_admin_update_270_delete_report_downloads() { } } -/** - * Update DB Version. - */ -function wc_admin_update_270_db_version() { - Installer::update_db_version( '2.7.0' ); -} - /** * Update the old task list options. */ @@ -267,13 +208,6 @@ function wc_admin_update_271_update_task_list_options() { delete_option( 'woocommerce_extended_task_list_hidden' ); } -/** - * Update DB Version. - */ -function wc_admin_update_271_db_version() { - Installer::update_db_version( '2.7.1' ); -} - /** * Update order stats `status`. */ @@ -289,13 +223,6 @@ function wc_admin_update_280_order_status() { ); } -/** - * Update DB Version. - */ -function wc_admin_update_280_db_version() { - Installer::update_db_version( '2.8.0' ); -} - /** * Update the old task list options. */ @@ -317,13 +244,6 @@ function wc_admin_update_290_delete_default_homepage_layout_option() { delete_option( 'woocommerce_default_homepage_layout' ); } -/** - * Update DB Version. - */ -function wc_admin_update_290_db_version() { - Installer::update_db_version( '2.9.0' ); -} - /** * Use woocommerce_admin_activity_panel_inbox_last_read from the user meta to set wc_admin_notes.is_read col. */ @@ -349,15 +269,6 @@ function wc_admin_update_300_update_is_read_from_last_read() { } } -/** - * Update DB Version. - */ -function wc_admin_update_300_db_version() { - Installer::update_db_version( '3.0.0' ); -} - - - /** * Delete "is_primary" column from the wc_admin_notes table. */ @@ -365,10 +276,3 @@ function wc_admin_update_340_remove_is_primary_from_note_action() { global $wpdb; $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_admin_note_actions DROP COLUMN `is_primary`" ); } - -/** - * Update DB Version. - */ -function wc_admin_update_340_db_version() { - Installer::update_db_version( '3.4.0' ); -} diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php index e560a127026..8bda913c60a 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php @@ -258,6 +258,25 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller { // Expand meta_data to include user-friendly values. $formatted_meta_data = $item->get_all_formatted_meta_data( null ); + + // Filter out product variations. + if ( isset( $product ) && 'true' === $this->request['order_item_display_meta'] ) { + $order_item_name = $data['name']; + $data['meta_data'] = array_filter( + $data['meta_data'], + function( $meta ) use ( $product, $order_item_name ) { + $display_value = wp_kses_post( rawurldecode( (string) $meta->value ) ); + + // Skip items with values already in the product details area of the product name. + if ( $product && $product->is_type( 'variation' ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) { + return false; + } + + return true; + } + ); + } + $data['meta_data'] = array_map( array( $this, 'merge_meta_item_with_formatted_meta_display_attributes' ), $data['meta_data'], @@ -1879,6 +1898,13 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller { 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); + $params['order_item_display_meta'] = array( + 'default' => false, + 'description' => __( 'Only show meta which is meant to be displayed for an order.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'rest_sanitize_boolean', + 'validate_callback' => 'rest_validate_request_arg', + ); return $params; } diff --git a/plugins/woocommerce/includes/tracks/class-wc-site-tracking.php b/plugins/woocommerce/includes/tracks/class-wc-site-tracking.php index e924d250448..d2bb5297688 100644 --- a/plugins/woocommerce/includes/tracks/class-wc-site-tracking.php +++ b/plugins/woocommerce/includes/tracks/class-wc-site-tracking.php @@ -161,6 +161,7 @@ class WC_Site_Tracking { include_once WC_ABSPATH . 'includes/tracks/events/class-wc-coupons-tracking.php'; include_once WC_ABSPATH . 'includes/tracks/events/class-wc-order-tracking.php'; include_once WC_ABSPATH . 'includes/tracks/events/class-wc-coupon-tracking.php'; + include_once WC_ABSPATH . 'includes/tracks/events/class-wc-theme-tracking.php'; $tracking_classes = array( 'WC_Extensions_Tracking', @@ -172,6 +173,7 @@ class WC_Site_Tracking { 'WC_Coupons_Tracking', 'WC_Order_Tracking', 'WC_Coupon_Tracking', + 'WC_Theme_Tracking', ); foreach ( $tracking_classes as $tracking_class ) { diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-theme-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-theme-tracking.php new file mode 100644 index 00000000000..51e23a7fceb --- /dev/null +++ b/plugins/woocommerce/includes/tracks/events/class-wc-theme-tracking.php @@ -0,0 +1,56 @@ +track_initial_theme(); + add_action( 'switch_theme', array( $this, 'track_activated_theme' ) ); + } + + /** + * Tracks the sites current theme the first time this code is run, and will only be run once. + */ + public function track_initial_theme() { + $has_been_initially_tracked = get_option( 'wc_has_tracked_default_theme' ); + + if ( $has_been_initially_tracked ) { + return; + } + + $this->track_activated_theme(); + add_option( 'wc_has_tracked_default_theme', 1 ); + } + + /** + * Send a Tracks event when a theme is activated so that we can track active block themes. + */ + public function track_activated_theme() { + $is_block_theme = false; + $theme_object = wp_get_theme(); + + if ( function_exists( 'wc_current_theme_is_fse_theme' ) ) { + $is_block_theme = wc_current_theme_is_fse_theme(); + } + + $properties = array( + 'block_theme' => $is_block_theme, + 'theme_name' => $theme_object->get( 'Name' ), + 'theme_version' => $theme_object->get( 'Version' ), + ); + + WC_Tracks::record_event( 'activated_theme', $properties ); + } +} diff --git a/plugins/woocommerce/includes/wc-core-functions.php b/plugins/woocommerce/includes/wc-core-functions.php index 83ffafe812e..177dbf2eff3 100644 --- a/plugins/woocommerce/includes/wc-core-functions.php +++ b/plugins/woocommerce/includes/wc-core-functions.php @@ -1775,19 +1775,8 @@ function wc_uasort_comparison( $a, $b ) { * @return int */ function wc_ascii_uasort_comparison( $a, $b ) { - // 'setlocale' is required for compatibility with PHP 8. - // Without it, 'iconv' will return '?'s instead of transliterated characters. - $prev_locale = setlocale( LC_CTYPE, 0 ); - setlocale( LC_ALL, 'C.UTF-8' ); - - // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged - if ( function_exists( 'iconv' ) && defined( 'ICONV_IMPL' ) && @strcasecmp( ICONV_IMPL, 'unknown' ) !== 0 ) { - $a = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $a ); - $b = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $b ); - } - // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged - - setlocale( LC_ALL, $prev_locale ); + $a = remove_accents( $a ); + $b = remove_accents( $b ); return strcmp( $a, $b ); } diff --git a/plugins/woocommerce/includes/wc-user-functions.php b/plugins/woocommerce/includes/wc-user-functions.php index 0c30c4f1aeb..a874c92a052 100644 --- a/plugins/woocommerce/includes/wc-user-functions.php +++ b/plugins/woocommerce/includes/wc-user-functions.php @@ -614,6 +614,11 @@ function wc_get_customer_available_downloads( $customer_id ) { $download_file = $_product->get_file( $result->download_id ); + // If the downloadable file has been disabled (it may be located in an untrusted location) then do not return it. + if ( ! $download_file->get_enabled() ) { + continue; + } + // Download name will be 'Product Name' for products with a single downloadable file, and 'Product Name - File X' for products with multiple files. $download_name = apply_filters( 'woocommerce_downloadable_product_name', diff --git a/plugins/woocommerce/legacy/css/admin.scss b/plugins/woocommerce/legacy/css/admin.scss index be221579281..b5fd23b2ae4 100644 --- a/plugins/woocommerce/legacy/css/admin.scss +++ b/plugins/woocommerce/legacy/css/admin.scss @@ -5110,6 +5110,14 @@ img.help_tip { margin: 1px 0; } + &.file_url { + /* Reduce the size of this field to make space for a warning asterisk. */ + input { + display: inline-block; + width: 96%; + } + } + .upload_file_button { width: auto; float: right; @@ -5160,6 +5168,11 @@ img.help_tip { color: #333; } } + + /* Warning asterisk (indicates if there is a problem with a downloadable file). */ + span.disabled { + color: var( --wc-red ); + } } } diff --git a/plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js b/plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js index f21f41a3066..79d3e7b322b 100644 --- a/plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js +++ b/plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js @@ -350,7 +350,7 @@ jQuery( function( $ ) { .on( 'click','.downloadable_files a.delete', this.input_changed ); $( document.body ) - .on( 'change', '#variable_product_options .woocommerce_variations :input', this.input_changed ) + .on( 'change input', '#variable_product_options .woocommerce_variations :input', this.input_changed ) .on( 'change', '.variations-defaults select', this.defaults_changed ); var postForm = $( 'form#post' ); @@ -705,13 +705,18 @@ jQuery( function( $ ) { /** * Add new class when have changes in some input */ - input_changed: function() { + input_changed: function( event ) { $( this ) .closest( '.woocommerce_variation' ) .addClass( 'variation-needs-update' ); $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); + // Do not trigger 'woocommerce_variations_input_changed' for 'input' events for backwards compat. + if ( 'input' === event.type && $( this ).is( ':text' ) ) { + return; + } + $( '#variable_product_options' ).trigger( 'woocommerce_variations_input_changed' ); }, diff --git a/plugins/woocommerce/legacy/js/admin/settings.js b/plugins/woocommerce/legacy/js/admin/settings.js index 702b0ae1a19..8464191f0e3 100644 --- a/plugins/woocommerce/legacy/js/admin/settings.js +++ b/plugins/woocommerce/legacy/js/admin/settings.js @@ -1,183 +1,265 @@ /* global woocommerce_settings_params, wp */ -( function( $, params, wp ) { - $( function() { +( function ( $, params, wp ) { + $( function () { // Sell Countries - $( 'select#woocommerce_allowed_countries' ).on( 'change', function() { - if ( 'specific' === $( this ).val() ) { - $( this ).closest('tr').next( 'tr' ).hide(); - $( this ).closest('tr').next().next( 'tr' ).show(); - } else if ( 'all_except' === $( this ).val() ) { - $( this ).closest('tr').next( 'tr' ).show(); - $( this ).closest('tr').next().next( 'tr' ).hide(); - } else { - $( this ).closest('tr').next( 'tr' ).hide(); - $( this ).closest('tr').next().next( 'tr' ).hide(); - } - }).trigger( 'change' ); + $( 'select#woocommerce_allowed_countries' ) + .on( 'change', function () { + if ( 'specific' === $( this ).val() ) { + $( this ).closest( 'tr' ).next( 'tr' ).hide(); + $( this ).closest( 'tr' ).next().next( 'tr' ).show(); + } else if ( 'all_except' === $( this ).val() ) { + $( this ).closest( 'tr' ).next( 'tr' ).show(); + $( this ).closest( 'tr' ).next().next( 'tr' ).hide(); + } else { + $( this ).closest( 'tr' ).next( 'tr' ).hide(); + $( this ).closest( 'tr' ).next().next( 'tr' ).hide(); + } + } ) + .trigger( 'change' ); // Ship Countries - $( 'select#woocommerce_ship_to_countries' ).on( 'change', function() { - if ( 'specific' === $( this ).val() ) { - $( this ).closest('tr').next( 'tr' ).show(); - } else { - $( this ).closest('tr').next( 'tr' ).hide(); - } - }).trigger( 'change' ); + $( 'select#woocommerce_ship_to_countries' ) + .on( 'change', function () { + if ( 'specific' === $( this ).val() ) { + $( this ).closest( 'tr' ).next( 'tr' ).show(); + } else { + $( this ).closest( 'tr' ).next( 'tr' ).hide(); + } + } ) + .trigger( 'change' ); // Stock management - $( 'input#woocommerce_manage_stock' ).on( 'change', function() { - if ( $( this ).is(':checked') ) { - $( this ).closest('tbody').find( '.manage_stock_field' ).closest( 'tr' ).show(); - } else { - $( this ).closest('tbody').find( '.manage_stock_field' ).closest( 'tr' ).hide(); - } - }).trigger( 'change' ); + $( 'input#woocommerce_manage_stock' ) + .on( 'change', function () { + if ( $( this ).is( ':checked' ) ) { + $( this ) + .closest( 'tbody' ) + .find( '.manage_stock_field' ) + .closest( 'tr' ) + .show(); + } else { + $( this ) + .closest( 'tbody' ) + .find( '.manage_stock_field' ) + .closest( 'tr' ) + .hide(); + } + } ) + .trigger( 'change' ); // Color picker $( '.colorpick' ) - - .iris({ - change: function( event, ui ) { - $( this ).parent().find( '.colorpickpreview' ).css({ backgroundColor: ui.color.toString() }); + .iris( { + change: function ( event, ui ) { + $( this ) + .parent() + .find( '.colorpickpreview' ) + .css( { backgroundColor: ui.color.toString() } ); }, hide: true, - border: true - }) + border: true, + } ) - .on( 'click focus', function( event ) { + .on( 'click focus', function ( event ) { event.stopPropagation(); $( '.iris-picker' ).hide(); $( this ).closest( 'td' ).find( '.iris-picker' ).show(); $( this ).data( 'originalValue', $( this ).val() ); - }) + } ) - .on( 'change', function() { + .on( 'change', function () { if ( $( this ).is( '.iris-error' ) ) { var original_value = $( this ).data( 'originalValue' ); - if ( original_value.match( /^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/ ) ) { - $( this ).val( $( this ).data( 'originalValue' ) ).trigger( 'change' ); + if ( + original_value.match( + /^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/ + ) + ) { + $( this ) + .val( $( this ).data( 'originalValue' ) ) + .trigger( 'change' ); } else { $( this ).val( '' ).trigger( 'change' ); } } - }); + } ); - $( 'body' ).on( 'click', function() { + $( 'body' ).on( 'click', function () { $( '.iris-picker' ).hide(); - }); + } ); // Edit prompt - $( function() { + $( function () { var changed = false; + let $check_column = $( '.wp-list-table .check-column' ); + + $( 'input, textarea, select, checkbox' ).on( 'change', function ( + event + ) { + // Toggling WP List Table checkboxes should not trigger navigation warnings. + if ( + $check_column.length && + $check_column.has( event.target ) + ) { + return; + } - $( 'input, textarea, select, checkbox' ).on( 'change', function() { if ( ! changed ) { - window.onbeforeunload = function() { + window.onbeforeunload = function () { return params.i18n_nav_warning; }; changed = true; } - }); + } ); - $( '.submit :input' ).on( 'click', function() { - window.onbeforeunload = ''; - }); - }); + $( '.submit :input, input#search-submit' ).on( + 'click', + function () { + window.onbeforeunload = ''; + } + ); + } ); // Sorting - $( 'table.wc_gateways tbody, table.wc_shipping tbody' ).sortable({ + $( 'table.wc_gateways tbody, table.wc_shipping tbody' ).sortable( { items: 'tr', cursor: 'move', axis: 'y', handle: 'td.sort', scrollSensitivity: 40, - helper: function( event, ui ) { - ui.children().each( function() { + helper: function ( event, ui ) { + ui.children().each( function () { $( this ).width( $( this ).width() ); - }); + } ); ui.css( 'left', '0' ); return ui; }, - start: function( event, ui ) { + start: function ( event, ui ) { ui.item.css( 'background-color', '#f6f6f6' ); }, - stop: function( event, ui ) { + stop: function ( event, ui ) { ui.item.removeAttr( 'style' ); ui.item.trigger( 'updateMoveButtons' ); - } - }); + }, + } ); // Select all/none - $( '.woocommerce' ).on( 'click', '.select_all', function() { - $( this ).closest( 'td' ).find( 'select option' ).prop( 'selected', true ); + $( '.woocommerce' ).on( 'click', '.select_all', function () { + $( this ) + .closest( 'td' ) + .find( 'select option' ) + .prop( 'selected', true ); $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); return false; - }); + } ); - $( '.woocommerce' ).on( 'click', '.select_none', function() { - $( this ).closest( 'td' ).find( 'select option' ).prop( 'selected', false ); + $( '.woocommerce' ).on( 'click', '.select_none', function () { + $( this ) + .closest( 'td' ) + .find( 'select option' ) + .prop( 'selected', false ); $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); return false; - }); + } ); // Re-order buttons. - $( '.wc-item-reorder-nav').find( '.wc-move-up, .wc-move-down' ).on( 'click', function() { - var moveBtn = $( this ), - $row = moveBtn.closest( 'tr' ); + $( '.wc-item-reorder-nav' ) + .find( '.wc-move-up, .wc-move-down' ) + .on( 'click', function () { + var moveBtn = $( this ), + $row = moveBtn.closest( 'tr' ); - moveBtn.trigger( 'focus' ); + moveBtn.trigger( 'focus' ); - var isMoveUp = moveBtn.is( '.wc-move-up' ), - isMoveDown = moveBtn.is( '.wc-move-down' ); + var isMoveUp = moveBtn.is( '.wc-move-up' ), + isMoveDown = moveBtn.is( '.wc-move-down' ); - if ( isMoveUp ) { - var $previewRow = $row.prev( 'tr' ); + if ( isMoveUp ) { + var $previewRow = $row.prev( 'tr' ); - if ( $previewRow && $previewRow.length ) { - $previewRow.before( $row ); - wp.a11y.speak( params.i18n_moved_up ); + if ( $previewRow && $previewRow.length ) { + $previewRow.before( $row ); + wp.a11y.speak( params.i18n_moved_up ); + } + } else if ( isMoveDown ) { + var $nextRow = $row.next( 'tr' ); + + if ( $nextRow && $nextRow.length ) { + $nextRow.after( $row ); + wp.a11y.speak( params.i18n_moved_down ); + } } - } else if ( isMoveDown ) { - var $nextRow = $row.next( 'tr' ); - if ( $nextRow && $nextRow.length ) { - $nextRow.after( $row ); - wp.a11y.speak( params.i18n_moved_down ); - } - } + moveBtn.trigger( 'focus' ); // Re-focus after the container was moved. + moveBtn.closest( 'table' ).trigger( 'updateMoveButtons' ); + } ); - moveBtn.trigger( 'focus' ); // Re-focus after the container was moved. - moveBtn.closest( 'table' ).trigger( 'updateMoveButtons' ); - } ); + $( '.wc-item-reorder-nav' ) + .closest( 'table' ) + .on( 'updateMoveButtons', function () { + var table = $( this ), + lastRow = $( this ).find( 'tbody tr:last' ), + firstRow = $( this ).find( 'tbody tr:first' ); - $( '.wc-item-reorder-nav').closest( 'table' ).on( 'updateMoveButtons', function() { - var table = $( this ), - lastRow = $( this ).find( 'tbody tr:last' ), - firstRow = $( this ).find( 'tbody tr:first' ); + table + .find( '.wc-item-reorder-nav .wc-move-disabled' ) + .removeClass( 'wc-move-disabled' ) + .attr( { tabindex: '0', 'aria-hidden': 'false' } ); + firstRow + .find( '.wc-item-reorder-nav .wc-move-up' ) + .addClass( 'wc-move-disabled' ) + .attr( { tabindex: '-1', 'aria-hidden': 'true' } ); + lastRow + .find( '.wc-item-reorder-nav .wc-move-down' ) + .addClass( 'wc-move-disabled' ) + .attr( { tabindex: '-1', 'aria-hidden': 'true' } ); + } ); - table.find( '.wc-item-reorder-nav .wc-move-disabled' ).removeClass( 'wc-move-disabled' ) - .attr( { 'tabindex': '0', 'aria-hidden': 'false' } ); - firstRow.find( '.wc-item-reorder-nav .wc-move-up' ).addClass( 'wc-move-disabled' ) - .attr( { 'tabindex': '-1', 'aria-hidden': 'true' } ); - lastRow.find( '.wc-item-reorder-nav .wc-move-down' ).addClass( 'wc-move-disabled' ) - .attr( { 'tabindex': '-1', 'aria-hidden': 'true' } ); - } ); + $( '.wc-item-reorder-nav' ) + .closest( 'table' ) + .trigger( 'updateMoveButtons' ); - $( '.wc-item-reorder-nav').closest( 'table' ).trigger( 'updateMoveButtons' ); - - - $( '.submit button' ).on( 'click', function() { + $( '.submit button' ).on( 'click', function () { if ( - $( 'select#woocommerce_allowed_countries' ).val() === 'specific' && + $( 'select#woocommerce_allowed_countries' ).val() === + 'specific' && ! $( '[name="woocommerce_specific_allowed_countries[]"]' ).val() ) { - if ( window.confirm( woocommerce_settings_params.i18n_no_specific_countries_selected ) ) { + if ( + window.confirm( + woocommerce_settings_params.i18n_no_specific_countries_selected + ) + ) { return true; } return false; } } ); - }); -})( jQuery, woocommerce_settings_params, wp ); + $( '#settings-other-payment-methods' ).on( 'click', function ( e ) { + if ( + typeof window.wcTracks.recordEvent === 'undefined' && + typeof window.wc.tracks.recordEvent === 'undefined' + ) { + return; + } + + var recordEvent = + window.wc.tracks.recordEvent || window.wcTracks.recordEvent; + + var payment_methods = $.map( + $( + 'td.wc_payment_gateways_wrapper tbody tr[data-gateway_id] ' + ), + function ( tr ) { + return $( tr ).attr( 'data-gateway_id' ); + } + ); + + recordEvent( 'settings_payments_recommendations_other_options', { + available_payment_methods: payment_methods, + } ); + } ); + } ); +} )( jQuery, woocommerce_settings_params, wp ); diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index 1f20ec694d6..ac076b7e508 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -1,7 +1,7 @@ { "name": "woocommerce", "title": "WooCommerce", - "version": "6.4.0", + "version": "6.5.0", "homepage": "https://woocommerce.com/", "repository": { "type": "git", @@ -14,7 +14,8 @@ "scripts": { "preinstall": "npx only-allow pnpm", "build": "./bin/build-zip.sh", - "build:core": "pnpm nx build woocommerce-admin && pnpm nx build woocommerce-legacy-assets && pnpm run makepot", + "build:feature-config": "php bin/generate-feature-config.php", + "build:core": "WC_ADMIN_PHASE=core pnpm run build:feature-config && pnpm nx build woocommerce-admin && pnpm nx build woocommerce-legacy-assets && pnpm run makepot", "build:zip": "pnpm run build", "lint:js": "eslint assets/js --ext=js", "docker:down": "pnpx wc-e2e docker:down", @@ -27,8 +28,7 @@ "test:e2e-dev": "pnpx wc-e2e test:e2e-dev", "test:unit": "./vendor/bin/phpunit -c ./phpunit.xml", "makepot": "composer run-script makepot", - "packages:fix:textdomain": "node ./bin/package-update-textdomain.js", - "git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && node ./node_modules/husky/husky.js install" + "packages:fix:textdomain": "node ./bin/package-update-textdomain.js" }, "devDependencies": { "@babel/cli": "7.12.8", @@ -55,15 +55,13 @@ "chai-as-promised": "7.1.1", "config": "3.3.3", "cross-env": "6.0.3", - "deasync": "0.1.21", + "deasync": "0.1.26", "eslint": "6.8.0", "eslint-config-wpcalypso": "5.0.0", "eslint-plugin-jest": "23.20.0", "github-contributors-list": "https://github.com/woocommerce/github-contributors-list/tarball/master", - "husky": "4.3.0", "istanbul": "1.0.0-alpha.2", "jest": "^25.1.0", - "lint-staged": "9.5.0", "mocha": "7.2.0", "prettier": "npm:wp-prettier@2.0.5", "stylelint": "^13.8.0", @@ -72,35 +70,19 @@ "webpack-cli": "3.3.12", "wp-textdomain": "1.0.1" }, - "engines": { - "node": "^16.13.1", - "pnpm": "^6.24.2" - }, - "husky": { - "hooks": { - "post-merge": "./bin/post-merge.sh", - "pre-commit": "lint-staged", - "pre-push": "./bin/pre-push.sh" - } - }, "lint-staged": { "*.php": [ "php -d display_errors=1 -l", "composer run-script phpcs-pre-commit" ], - "*.scss": [ - "stylelint --syntax=scss --fix", - "git add" - ], - "*.js": [ - "eslint --fix", - "git add" - ], - "*.ts": [ - "eslint --fix", - "git add" + "!(*min).js": [ + "eslint --fix" ] }, + "engines": { + "node": "^16.13.1", + "pnpm": "^6.24.2" + }, "browserslist": [ "> 0.1%", "ie 8", diff --git a/plugins/woocommerce/project.json b/plugins/woocommerce/project.json index 9fbc742dca1..89df485f665 100644 --- a/plugins/woocommerce/project.json +++ b/plugins/woocommerce/project.json @@ -57,7 +57,7 @@ "executor": "@nrwl/workspace:run-commands", "options": { "commands": [ - "pnpm nx build:feature-config woocommerce-admin", + "pnpm nx build:feature-config woocommerce", "pnpm nx watch-assets woocommerce" ] } @@ -142,12 +142,6 @@ "script": "packages:fix:textdomain" } }, - "git-update-hooks": { - "executor": "@nrwl/workspace:run-script", - "options": { - "script": "git:update-hooks" - } - }, "make-collection": { "executor": "@nrwl/workspace:run-script", "options": { diff --git a/plugins/woocommerce/readme.txt b/plugins/woocommerce/readme.txt index 5fe323beb36..63cfa9ee1dc 100644 --- a/plugins/woocommerce/readme.txt +++ b/plugins/woocommerce/readme.txt @@ -4,7 +4,7 @@ Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, d Requires at least: 5.7 Tested up to: 5.9 Requires PHP: 7.0 -Stable tag: 6.3.0 +Stable tag: 6.4.0 License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -160,6 +160,6 @@ WooCommerce comes with some sample data you can use to see how products look; im == Changelog == -= 6.4.0 2022-XX-XX = += 6.5.0 2022-XX-XX = [See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/changelog.txt). diff --git a/plugins/woocommerce/src/Admin/API/Reports/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/DataStore.php index aebda100324..bf2a95cf2ac 100644 --- a/plugins/woocommerce/src/Admin/API/Reports/DataStore.php +++ b/plugins/woocommerce/src/Admin/API/Reports/DataStore.php @@ -577,7 +577,7 @@ class DataStore extends SqlQuery { */ protected static function get_excluded_report_order_statuses() { $excluded_statuses = \WC_Admin_Settings::get_option( 'woocommerce_excluded_report_order_statuses', array( 'pending', 'failed', 'cancelled' ) ); - $excluded_statuses = array_merge( array( 'trash' ), array_map( 'esc_sql', $excluded_statuses ) ); + $excluded_statuses = array_merge( array( 'auto-draft', 'trash' ), array_map( 'esc_sql', $excluded_statuses ) ); return apply_filters( 'woocommerce_analytics_excluded_order_statuses', $excluded_statuses ); } diff --git a/plugins/woocommerce/src/Admin/Composer/Package.php b/plugins/woocommerce/src/Admin/Composer/Package.php index 9d12915973c..a4ed70bac7f 100644 --- a/plugins/woocommerce/src/Admin/Composer/Package.php +++ b/plugins/woocommerce/src/Admin/Composer/Package.php @@ -11,7 +11,6 @@ namespace Automattic\WooCommerce\Admin\Composer; defined( 'ABSPATH' ) || exit; -use Automattic\WooCommerce\Internal\Admin\Notes\DeactivatePlugin; use Automattic\WooCommerce\Admin\Notes\Notes; use Automattic\WooCommerce\Admin\Notes\NotesUnavailableException; use Automattic\WooCommerce\Internal\Admin\FeaturePlugin; @@ -51,21 +50,10 @@ class Package { // Avoid double initialization when the feature plugin is in use. if ( defined( 'WC_ADMIN_VERSION_NUMBER' ) ) { self::$active_version = WC_ADMIN_VERSION_NUMBER; - - // Check version after WooCommerce is initialized. - add_action( 'woocommerce_init', array( __CLASS__, 'check_outdated_wca_plugin' ) ); - - // Register a deactivation hook for the feature plugin. - register_deactivation_hook( WC_ADMIN_PLUGIN_FILE, array( __CLASS__, 'on_deactivation' ) ); - return; } $feature_plugin_instance = FeaturePlugin::instance(); - $satisfied_dependencies = is_callable( array( $feature_plugin_instance, 'has_satisfied_dependencies' ) ) && $feature_plugin_instance->has_satisfied_dependencies(); - if ( ! $satisfied_dependencies ) { - return; - } // Indicate to the feature plugin that the core package exists. if ( ! defined( 'WC_ADMIN_PACKAGE_EXISTS' ) ) { @@ -116,39 +104,6 @@ class Package { return dirname( __DIR__ ); } - /** - * Add deactivation hook for versions of the plugin that don't have the deactivation note. - */ - public static function on_deactivation() { - if ( ! self::is_notes_initialized() ) { - return; - } - - $update_version = new DeactivatePlugin(); - $update_version::delete_note(); - } - - /** - * Checks if embedded WCA version is newer than standalone WCA - * and adds/removes DeactivatePlugin note as necessary. - */ - public static function check_outdated_wca_plugin() { - - if ( ! self::is_notes_initialized() ) { - return; - } - - $update_version = new DeactivatePlugin(); - - if ( version_compare( WC_ADMIN_VERSION_NUMBER, self::VERSION, '<' ) ) { - if ( method_exists( $update_version, 'possibly_add_note' ) ) { - $update_version::possibly_add_note(); - } - } else { - $update_version::delete_note(); - } - } - /** * Checks if notes have been initialized. */ diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/DeprecatedOptions.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/DeprecatedOptions.php index bd5670edff2..60ffbadb185 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/DeprecatedOptions.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/DeprecatedOptions.php @@ -6,7 +6,7 @@ namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\TaskList; -use Automattic\WooCommerce\Internal\Admin\Install; +use WC_Install; /** * DeprecatedOptions class. @@ -30,9 +30,9 @@ class DeprecatedOptions { * @return string */ public static function get_deprecated_options( $pre_option, $option ) { - if ( Install::is_installing() ) { + if ( defined( 'WC_INSTALLING' ) && WC_INSTALLING === true ) { return $pre_option; - }; + } $hidden = get_option( 'woocommerce_task_list_hidden_lists', array() ); switch ( $option ) { diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php index f6300aa5ea1..6285b7f5d6b 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php @@ -358,10 +358,6 @@ abstract class Task { * Track task completion if task is viewable. */ public function possibly_track_completion() { - if ( ! $this->can_view() ) { - return; - } - if ( ! $this->is_complete() ) { return; } @@ -407,6 +403,15 @@ abstract class Task { return true; } + /** + * Check if task is disabled. + * + * @return bool + */ + public function is_disabled() { + return false; + } + /** * Check if the task is complete. * @@ -455,6 +460,7 @@ abstract class Task { 'isSnoozed' => $this->is_snoozed(), 'isSnoozeable' => $this->is_snoozeable(), 'isVisited' => $this->is_visited(), + 'isDisabled' => $this->is_disabled(), 'snoozedUntil' => $this->get_snoozed_until(), 'additionalData' => self::convert_object_to_camelcase( $this->get_additional_data() ), 'eventPrefix' => $this->prefix_event( '' ), diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php index ddfa282d70e..4053546fc9a 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php @@ -96,6 +96,20 @@ class TaskList { */ public $options = array(); + /** + * Array of TaskListSection. + * + * @var array + */ + private $sections = array(); + + /** + * Key value map of task class and id used for sections. + * + * @var array + */ + public $task_class_id_map = array(); + /** * Constructor * @@ -112,6 +126,7 @@ class TaskList { 'options' => array(), 'visible' => true, 'display_progress_header' => false, + 'sections' => array(), ); $data = wp_parse_args( $data, $defaults ); @@ -132,6 +147,12 @@ class TaskList { } $this->possibly_remove_reminder_bar(); + $this->sections = array_map( + function( $section ) { + return new TaskListSection( $section, $this ); + }, + $data['sections'] + ); } /** @@ -243,7 +264,9 @@ class TaskList { return; } - $this->tasks[] = $task; + $task_class_name = substr( get_class( $task ), strrpos( get_class( $task ), '\\' ) + 1 ); + $this->task_class_id_map[ $task_class_name ] = $task->get_id(); + $this->tasks[] = $task; } /** @@ -279,6 +302,15 @@ class TaskList { ); } + /** + * Get task list sections. + * + * @return array + */ + public function get_sections() { + return $this->sections; + } + /** * Track list completion of viewable tasks. */ @@ -329,6 +361,15 @@ class TaskList { return $this->get_list_id() . '_tasklist_' . $event_name; } + /** + * Returns option to keep completed task list. + * + * @return string + */ + public function get_keep_completed_task_list() { + return get_option( 'woocommerce_task_list_keep_completed', 'no' ); + } + /** * Remove reminder bar four weeks after store creation. */ @@ -350,20 +391,30 @@ class TaskList { */ public function get_json() { $this->possibly_track_completion(); + $tasks_json = array(); + foreach ( $this->tasks as $task ) { + $json = $task->get_json(); + if ( $json['canView'] ) { + $tasks_json[] = $json; + } + } + return array( 'id' => $this->get_list_id(), 'title' => $this->title, 'isHidden' => $this->is_hidden(), 'isVisible' => $this->is_visible(), 'isComplete' => $this->is_complete(), - 'tasks' => array_map( - function( $task ) { - return $task->get_json(); - }, - $this->get_viewable_tasks() - ), + 'tasks' => $tasks_json, 'eventPrefix' => $this->prefix_event( '' ), 'displayProgressHeader' => $this->display_progress_header, + 'keepCompletedTaskList' => $this->get_keep_completed_task_list(), + 'sections' => array_map( + function( $section ) { + return $section->get_json(); + }, + $this->sections + ), ); } } diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php new file mode 100644 index 00000000000..bc343781dd3 --- /dev/null +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php @@ -0,0 +1,122 @@ + '', + 'title' => '', + 'description' => '', + 'image' => '', + 'tasks' => array(), + ); + + $data = wp_parse_args( $data, $defaults ); + + $this->task_list = $task_list; + $this->id = $data['id']; + $this->title = $data['title']; + $this->description = $data['description']; + $this->image = $data['image']; + $this->task_names = $data['task_names']; + } + + /** + * Returns if section is complete. + * + * @return boolean; + */ + private function is_complete() { + $complete = true; + foreach ( $this->task_names as $task_name ) { + if ( null !== $this->task_list && isset( $this->task_list->task_class_id_map[ $task_name ] ) ) { + $task = $this->task_list->get_task( $this->task_list->task_class_id_map[ $task_name ] ); + if ( $task->can_view() && ! $task->is_complete() ) { + $complete = false; + break; + } + } + } + return $complete; + } + + /** + * Get the list for use in JSON. + * + * @return array + */ + public function get_json() { + return array( + 'id' => $this->id, + 'title' => $this->title, + 'description' => $this->description, + 'image' => $this->image, + 'tasks' => array_map( + function( $task_name ) { + if ( null !== $this->task_list && isset( $this->task_list->task_class_id_map[ $task_name ] ) ) { + return $this->task_list->task_class_id_map[ $task_name ]; + } + return ''; + }, + $this->task_names + ), + 'isComplete' => $this->is_complete(), + ); + } +} diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php index 436adcff3ff..b50b78455ac 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php @@ -70,6 +70,34 @@ class TaskLists { self::init_default_lists(); add_action( 'admin_init', array( __CLASS__, 'set_active_task' ), 5 ); add_action( 'init', array( __CLASS__, 'init_tasks' ) ); + add_filter( 'woocommerce_admin_shared_settings', array( __CLASS__, 'task_list_preloaded_settings' ), 20 ); + } + + /** + * Check if an experiment is the treatment or control. + * + * @param string $name Name prefix of experiment. + * @return bool + */ + public static function is_experiment_treatment( $name ) { + $anon_id = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : ''; + $allow_tracking = 'yes' === get_option( 'woocommerce_allow_tracking' ); + $abtest = new \WooCommerce\Admin\Experimental_Abtest( + $anon_id, + 'woocommerce', + $allow_tracking + ); + + $date = new \DateTime(); + $date->setTimeZone( new \DateTimeZone( 'UTC' ) ); + + $experiment_name = sprintf( + '%s_%s_%s', + $name, + $date->format( 'Y' ), + $date->format( 'm' ) + ); + return $abtest->get_variation( $experiment_name ) === 'treatment'; } /** @@ -92,7 +120,8 @@ class TaskLists { 'Appearance', ), 'event_prefix' => 'tasklist_', - 'visible' => ! Features::is_enabled( 'tasklist-setup-experiment-1' ), + 'visible' => ! self::is_experiment_treatment( 'woocommerce_tasklist_setup_experiment_1' ) + && ! self::is_experiment_treatment( 'woocommerce_tasklist_setup_experiment_2' ), ) ); @@ -116,7 +145,65 @@ class TaskLists { 'options' => array( 'use_completed_title' => true, ), - 'visible' => Features::is_enabled( 'tasklist-setup-experiment-1' ), + 'visible' => self::is_experiment_treatment( 'woocommerce_tasklist_setup_experiment_1' ), + ) + ); + + self::add_list( + array( + 'id' => 'setup_experiment_2', + 'hidden_id' => 'setup', + 'title' => __( 'Get ready to start selling', 'woocommerce' ), + 'tasks' => array( + 'StoreCreation', + 'StoreDetails', + 'Products', + 'WooCommercePayments', + 'Payments', + 'Tax', + 'Shipping', + 'Marketing', + 'Appearance', + ), + 'event_prefix' => 'tasklist_', + 'visible' => self::is_experiment_treatment( 'woocommerce_tasklist_setup_experiment_2' ) + && ! self::is_experiment_treatment( 'woocommerce_tasklist_setup_experiment_1' ), + 'options' => array( + 'use_completed_title' => true, + ), + 'display_progress_header' => true, + 'sections' => array( + array( + 'id' => 'basics', + 'title' => __( 'Cover the basics', 'woocommerce' ), + 'description' => __( 'Make sure youā€™ve got everything you need to start sellingā€”from business details to products.', 'woocommerce' ), + 'image' => plugins_url( + '/assets/images/task_list/basics-section-illustration.png', + WC_ADMIN_PLUGIN_FILE + ), + 'task_names' => array( 'StoreCreation', 'StoreDetails', 'Products', 'Payments', 'WooCommercePayments' ), + ), + array( + 'id' => 'sales', + 'title' => __( 'Get ready to sell', 'woocommerce' ), + 'description' => __( 'Easily set up the backbone of your storeā€™s operations and get ready to accept first orders.', 'woocommerce' ), + 'image' => plugins_url( + '/assets/images/task_list/sales-section-illustration.png', + WC_ADMIN_PLUGIN_FILE + ), + 'task_names' => array( 'Shipping', 'Tax' ), + ), + array( + 'id' => 'expand', + 'title' => __( 'Customize & expand', 'woocommerce' ), + 'description' => __( 'Personalize your storeā€™s design and grow your business by enabling new sales channels.', 'woocommerce' ), + 'image' => plugins_url( + '/assets/images/task_list/expand-section-illustration.png', + WC_ADMIN_PLUGIN_FILE + ), + 'task_names' => array( 'Appearance', 'Marketing' ), + ), + ), ) ); @@ -321,7 +408,7 @@ class TaskLists { return array_filter( self::get_lists(), function ( $task_list ) { - return ! $task_list->is_hidden(); + return $task_list->is_visible(); } ); } @@ -373,4 +460,17 @@ class TaskLists { return null; } + + /** + * Add visible list ids to component settings. + * + * @param array $settings Component settings. + * + * @return array + */ + public static function task_list_preloaded_settings( $settings ) { + $settings['visibleTaskListIds'] = array_keys( self::get_visible() ); + + return $settings; + } } diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php index 6277a0acd55..e479c4292db 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php @@ -73,8 +73,11 @@ class AdditionalPayments extends Payments { $woocommerce_payments = new WooCommercePayments(); - if ( ! $woocommerce_payments->is_requested() || ( $woocommerce_payments->is_supported() && ! $woocommerce_payments->is_connected() ) ) { - // Hide task if WC Pay is installed via OBW, in supported country, but not connected. + if ( ! $woocommerce_payments->is_requested() || ! $woocommerce_payments->is_supported() || ! $woocommerce_payments->is_connected() ) { + // Hide task if WC Pay is not installed via OBW, or is not connected, or the store is located in a country that is not supported by WC Pay. + return false; + } + if ( $this->get_parent_id() === 'extended_two_column' && WooCommercePayments::is_connected() ) { return false; } diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php index 21db5d4aa30..36f4fd1b752 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php @@ -39,6 +39,9 @@ class Appearance extends Task { * @return string */ public function get_title() { + if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { + return __( 'Make your store stand out with unique design', 'woocommerce' ); + } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You personalized your store', 'woocommerce' ); @@ -54,6 +57,9 @@ class Appearance extends Task { * @return string */ public function get_content() { + if ( count( $this->task_list->get_sections() ) > 0 ) { + return __( 'Upload your logo to adapt the store to your brandā€™s personality.', 'woocommerce' ); + } return __( 'Add your logo, create a homepage, and start designing your store.', 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php index 59dc41f51c0..81a35205140 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php @@ -25,6 +25,9 @@ class Marketing extends Task { * @return string */ public function get_title() { + if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { + return __( 'Grow your business with marketing tools', 'woocommerce' ); + } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You added sales channels', 'woocommerce' ); @@ -40,6 +43,9 @@ class Marketing extends Task { * @return string */ public function get_content() { + if ( count( $this->task_list->get_sections() ) > 0 ) { + return __( 'Promote your store in other sales channels, like email, Google, and Facebook.', 'woocommerce' ); + } return __( 'Add recommended marketing tools to reach new customers and grow your business', 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php index 9d7029f74f0..6cd523c6f63 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php @@ -25,6 +25,9 @@ class Payments extends Task { * @return string */ public function get_title() { + if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { + return __( 'Add a way to get paid', 'woocommerce' ); + } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You set up payments', 'woocommerce' ); @@ -40,6 +43,9 @@ class Payments extends Task { * @return string */ public function get_content() { + if ( count( $this->task_list->get_sections() ) > 0 ) { + return __( 'Let your customers pay the way they like.', 'woocommerce' ); + } return __( 'Choose payment providers and enable payment methods at checkout.', 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php index f632fa01ce0..2394ef9b288 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php @@ -36,6 +36,9 @@ class Products extends Task { * @return string */ public function get_title() { + if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { + return __( 'Create or upload your first products', 'woocommerce' ); + } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You added products', 'woocommerce' ); @@ -51,6 +54,9 @@ class Products extends Task { * @return string */ public function get_content() { + if ( count( $this->task_list->get_sections() ) > 0 ) { + return __( 'Add products to sell and build your catalog.', 'woocommerce' ); + } return __( 'Start by adding the first product to your store. You can add your products manually, via CSV, or import them from another service.', 'woocommerce' @@ -98,7 +104,7 @@ class Products extends Task { return; } - if ( ! $this->is_active() || $this->is_complete() ) { + if ( ! $this->is_active() || ! $this->is_complete() ) { return; } @@ -112,6 +118,9 @@ class Products extends Task { WC_VERSION, true ); + + // Clear the active task transient to only show notice once per active session. + delete_transient( self::ACTIVE_TASK_TRANSIENT ); } /** diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php index 1a3adb553a6..4af52c248b2 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php @@ -24,6 +24,9 @@ class Shipping extends Task { * @return string */ public function get_title() { + if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { + return __( 'Select how to ship your products', 'woocommerce' ); + } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You added shipping costs', 'woocommerce' ); @@ -39,6 +42,9 @@ class Shipping extends Task { * @return string */ public function get_content() { + if ( count( $this->task_list->get_sections() ) > 0 ) { + return __( 'Set delivery costs and enable extra features, like shipping label printing.', 'woocommerce' ); + } return __( "Set your store location and where you'll ship to.", 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php new file mode 100644 index 00000000000..03a178779a5 --- /dev/null +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php @@ -0,0 +1,77 @@ +task_list->get_sections() ) > 0 && ! $this->is_complete() ) { + return __( 'Get taxes out of your mind', 'woocommerce' ); + } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You added tax rates', 'woocommerce' ); @@ -80,6 +83,9 @@ class Tax extends Task { * @return string */ public function get_content() { + if ( count( $this->task_list->get_sections() ) > 0 ) { + return __( 'Have sales tax calculated automatically, or add the rates manually.', 'woocommerce' ); + } return self::can_use_automated_taxes() ? __( 'Good news! WooCommerce Services and Jetpack can automate your sales tax calculations for you.', diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php index 55b30784880..051a64dbbaa 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php @@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks; use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task; use Automattic\WooCommerce\Admin\PluginsHelper; +use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\Init as Suggestions; /** * WooCommercePayments Task @@ -19,15 +20,6 @@ class WooCommercePayments extends Task { return 'woocommerce-payments'; } - /** - * Parent ID. - * - * @return string - */ - public function get_parent_id() { - return 'setup'; - } - /** * Title. * @@ -146,22 +138,19 @@ class WooCommercePayments extends Task { * @return bool */ public static function is_supported() { - return in_array( - WC()->countries->get_base_country(), - array( - 'US', - 'PR', - 'AU', - 'CA', - 'DE', - 'ES', - 'FR', - 'GB', - 'IE', - 'IT', - 'NZ', - ), - true + $suggestions = Suggestions::get_suggestions(); + $suggestion_plugins = array_merge( + ...array_filter( + array_column( $suggestions, 'plugins' ), + function( $plugins ) { + return is_array( $plugins ); + } + ) ); + $woocommerce_payments_ids = array_search( 'woocommerce-payments', $suggestion_plugins, true ); + if ( false !== $woocommerce_payments_ids ) { + return true; + } + return false; } } diff --git a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php index 1b1a76e74e2..31b7556793e 100644 --- a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php +++ b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php @@ -25,71 +25,32 @@ class DefaultPaymentGateways { 'id' => 'payfast', 'title' => __( 'PayFast', 'woocommerce' ), 'content' => __( 'The PayFast extension for WooCommerce enables you to accept payments by Credit Card and EFT via one of South Africaā€™s most popular payment gateways. No setup fees or monthly subscription costs. Selecting this extension will configure your store to use South African rands as the selected currency.', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/payfast.png', + 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/payfast.png', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/payfast.png', 'plugins' => array( 'woocommerce-payfast-gateway' ), 'is_visible' => array( - (object) array( - 'type' => 'base_location_country', - 'value' => 'ZA', - 'operation' => '=', - ), + self::get_rules_for_countries( array( 'ZA', 'GH', 'NG' ) ), self::get_rules_for_cbd( false ), ), + 'category_other' => array( 'ZA', 'GH', 'NG' ), + 'category_additional' => array(), ), array( 'id' => 'stripe', 'title' => __( ' Stripe', 'woocommerce' ), 'content' => __( 'Accept debit and credit cards in 135+ currencies, methods such as Alipay, and one-touch checkout with Apple Pay.', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/stripe.png', + 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/stripe.png', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/stripe.png', 'plugins' => array( 'woocommerce-gateway-stripe' ), 'is_visible' => array( // https://stripe.com/global. self::get_rules_for_countries( - array( - 'AU', - 'AT', - 'BE', - 'BG', - 'BR', - 'CA', - 'CY', - 'CZ', - 'DK', - 'EE', - 'FI', - 'FR', - 'DE', - 'GR', - 'HK', - 'IN', - 'IE', - 'IT', - 'JP', - 'LV', - 'LT', - 'LU', - 'MY', - 'MT', - 'MX', - 'NL', - 'NZ', - 'NO', - 'PL', - 'PT', - 'RO', - 'SG', - 'SK', - 'SI', - 'ES', - 'SE', - 'CH', - 'GB', - 'US', - 'PR', - ) + array( 'AU', 'AT', 'BE', 'BG', 'BR', 'CA', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HK', 'IN', 'IE', 'IT', 'JP', 'LV', 'LT', 'LU', 'MY', 'MT', 'MX', 'NL', 'NZ', 'NO', 'PL', 'PT', 'RO', 'SG', 'SK', 'SI', 'ES', 'SE', 'CH', 'GB', 'US', 'PR', 'HU', 'SL', 'ID', 'MY', 'SI', 'PR' ) ), self::get_rules_for_cbd( false ), ), + 'category_other' => array( 'AU', 'AT', 'BE', 'BG', 'BR', 'CA', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HK', 'IN', 'IE', 'IT', 'JP', 'LV', 'LT', 'LU', 'MY', 'MT', 'MX', 'NL', 'NZ', 'NO', 'PL', 'PT', 'RO', 'SG', 'SK', 'SI', 'ES', 'SE', 'CH', 'GB', 'US', 'PR', 'HU', 'SL', 'ID', 'MY', 'SI', 'PR' ), + 'category_additional' => array(), 'recommendation_priority' => 3, ), array( @@ -97,89 +58,81 @@ class DefaultPaymentGateways { 'title' => __( 'Paystack', 'woocommerce' ), 'content' => __( 'Paystack helps African merchants accept one-time and recurring payments online with a modern, safe, and secure payment gateway.', 'woocommerce' ), 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/paystack.png', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/paystack.png', 'plugins' => array( 'woo-paystack' ), 'is_visible' => array( self::get_rules_for_countries( array( 'ZA', 'GH', 'NG' ) ), self::get_rules_for_cbd( false ), ), + 'category_other' => array( 'ZA', 'GH', 'NG' ), + 'category_additional' => array(), ), array( 'id' => 'kco', 'title' => __( 'Klarna Checkout', 'woocommerce' ), 'content' => __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/klarna-black.png', + 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/klarna-black.png', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/klarna.png', 'plugins' => array( 'klarna-checkout-for-woocommerce' ), 'is_visible' => array( self::get_rules_for_countries( array( 'SE', 'FI', 'NO' ) ), self::get_rules_for_cbd( false ), ), + 'category_other' => array( 'SE', 'FI', 'NO' ), + 'category_additional' => array(), ), array( 'id' => 'klarna_payments', 'title' => __( 'Klarna Payments', 'woocommerce' ), 'content' => __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/klarna-black.png', + 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/klarna-black.png', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/klarna.png', 'plugins' => array( 'klarna-payments-for-woocommerce' ), 'is_visible' => array( self::get_rules_for_countries( - array( - 'DK', - 'DE', - 'AT', - 'NL', - 'CH', - 'BE', - 'SP', - 'PL', - 'FR', - 'IT', - 'GB', - ) + array( 'US', 'CA', 'DK', 'DE', 'AT', 'NL', 'CH', 'BE', 'SP', 'PL', 'FR', 'IT', 'GB', 'ES', 'FI', 'NO', 'SE', 'ES', 'FI', 'NO', 'SE' ) ), self::get_rules_for_cbd( false ), ), + 'category_other' => array(), + 'category_additional' => array( 'US', 'CA', 'DK', 'DE', 'AT', 'NL', 'CH', 'BE', 'SP', 'PL', 'FR', 'IT', 'GB', 'ES', 'FI', 'NO', 'SE', 'ES', 'FI', 'NO', 'SE' ), ), array( 'id' => 'mollie_wc_gateway_banktransfer', 'title' => __( 'Mollie', 'woocommerce' ), 'content' => __( 'Effortless payments by Mollie: Offer global and local payment methods, get onboarded in minutes, and supported in your language.', 'woocommerce' ), 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/mollie.svg', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/mollie.png', 'plugins' => array( 'mollie-payments-for-woocommerce' ), 'is_visible' => array( self::get_rules_for_countries( - array( - 'FR', - 'DE', - 'GB', - 'AT', - 'CH', - 'ES', - 'IT', - 'PL', - 'FI', - 'NL', - 'BE', - ) + array( 'FR', 'DE', 'GB', 'AT', 'CH', 'ES', 'IT', 'PL', 'FI', 'NL', 'BE' ) ), ), + 'category_other' => array( 'FR', 'DE', 'GB', 'AT', 'CH', 'ES', 'IT', 'PL', 'FI', 'NL', 'BE' ), + 'category_additional' => array(), ), array( 'id' => 'woo-mercado-pago-custom', 'title' => __( 'Mercado Pago Checkout Pro & Custom', 'woocommerce' ), 'content' => __( 'Accept credit and debit cards, offline (cash or bank transfer) and logged-in payments with money in Mercado Pago. Safe and secure payments with the leading payment processor in LATAM.', 'woocommerce' ), 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/mercadopago.png', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/mercadopago.png', 'plugins' => array( 'woocommerce-mercadopago' ), 'is_visible' => array( self::get_rules_for_countries( array( 'AR', 'BR', 'CL', 'CO', 'MX', 'PE', 'UY' ) ), ), 'recommendation_priority' => 2, 'is_local_partner' => true, + 'category_other' => array( 'AR', 'BR', 'CL', 'CO', 'MX', 'PE', 'UY' ), + 'category_additional' => array(), ), array( 'id' => 'ppcp-gateway', 'title' => __( 'PayPal Payments', 'woocommerce' ), 'content' => __( "Safe and secure payments using credit cards or your customer's PayPal account.", 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/paypal.png', + 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/paypal.png', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/paypal.png', 'plugins' => array( 'woocommerce-paypal-payments' ), 'is_visible' => array( (object) array( @@ -189,24 +142,30 @@ class DefaultPaymentGateways { ), self::get_rules_for_cbd( false ), ), + 'category_other' => array( 'US', 'CA', 'AT', 'BE', 'BG', 'HR', 'CH', 'CY', 'CZ', 'DK', 'EE', 'ES', 'FI', 'FR', 'DE', 'GB', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SK', 'SL', 'SE', 'MX', 'BR', 'AR', 'CL', 'CO', 'EC', 'PE', 'UY', 'VE', 'AU', 'NZ', 'HK', 'JP', 'SG', 'CN', 'ID', 'ZA', 'NG', 'GH' ), + 'category_additional' => array( 'US', 'CA', 'AT', 'BE', 'BG', 'HR', 'CH', 'CY', 'CZ', 'DK', 'EE', 'ES', 'FI', 'FR', 'DE', 'GB', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SK', 'SL', 'SE', 'MX', 'BR', 'AR', 'CL', 'CO', 'EC', 'PE', 'UY', 'VE', 'AU', 'NZ', 'HK', 'JP', 'SG', 'CN', 'ID', 'IN', 'ZA', 'NG', 'GH' ), ), array( 'id' => 'cod', 'title' => __( 'Cash on delivery', 'woocommerce' ), 'content' => __( 'Take payments in cash upon delivery.', 'woocommerce' ), 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/cod.svg', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/cod.png', 'is_visible' => array( self::get_rules_for_cbd( false ), ), + 'is_offline' => true, ), array( 'id' => 'bacs', 'title' => __( 'Direct bank transfer', 'woocommerce' ), 'content' => __( 'Take payments via bank transfer.', 'woocommerce' ), 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/bacs.svg', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/bacs.png', 'is_visible' => array( self::get_rules_for_cbd( false ), ), + 'is_offline' => true, ), array( 'id' => 'woocommerce_payments', @@ -216,6 +175,7 @@ class DefaultPaymentGateways { 'woocommerce' ), 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg', 'plugins' => array( 'woocommerce-payments' ), 'description' => 'With WooCommerce Payments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies. Track cash flow and manage recurring revenue directly from your storeā€™s dashboard - with no setup costs or monthly fees.', 'is_visible' => array( @@ -258,6 +218,7 @@ class DefaultPaymentGateways { 'woocommerce' ), 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg', 'plugins' => array( 'woocommerce-payments' ), 'description' => 'With WooCommerce Payments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies. Track cash flow and manage recurring revenue directly from your storeā€™s dashboard - with no setup costs or monthly fees.', 'is_visible' => array( @@ -292,6 +253,7 @@ class DefaultPaymentGateways { 'woocommerce' ), 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg', 'plugins' => array( 'woocommerce-payments' ), 'description' => 'With WooCommerce Payments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies ā€“ with no setup costs or monthly fees ā€“ and you can now accept in-person payments with the Woo mobile app.', 'is_visible' => array( @@ -323,6 +285,7 @@ class DefaultPaymentGateways { 'title' => __( 'Razorpay', 'woocommerce' ), 'content' => __( 'The official Razorpay extension for WooCommerce allows you to accept credit cards, debit cards, netbanking, wallet, and UPI payments.', 'woocommerce' ), 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/razorpay.svg', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/razorpay.png', 'plugins' => array( 'woo-razorpay' ), 'is_visible' => array( (object) array( @@ -332,12 +295,15 @@ class DefaultPaymentGateways { ), self::get_rules_for_cbd( false ), ), + 'category_other' => array( 'IN' ), + 'category_additional' => array(), ), array( 'id' => 'payubiz', 'title' => __( 'PayU for WooCommerce', 'woocommerce' ), 'content' => __( 'Enable PayUā€™s exclusive plugin for WooCommerce to start accepting payments in 100+ payment methods available in India including credit cards, debit cards, UPI, & more!', 'woocommerce' ), 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/payu.svg', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/payu.png', 'plugins' => array( 'payu-india' ), 'is_visible' => array( (object) array( @@ -347,23 +313,29 @@ class DefaultPaymentGateways { ), self::get_rules_for_cbd( false ), ), + 'category_other' => array( 'IN' ), + 'category_additional' => array(), ), array( 'id' => 'eway', 'title' => __( 'Eway', 'woocommerce' ), 'content' => __( 'The Eway extension for WooCommerce allows you to take credit card payments directly on your store without redirecting your customers to a third party site to make payment.', 'woocommerce' ), 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/eway.png', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/eway.png', 'plugins' => array( 'woocommerce-gateway-eway' ), 'is_visible' => array( self::get_rules_for_countries( array( 'AU', 'NZ' ) ), self::get_rules_for_cbd( false ), ), + 'category_other' => array( 'AU', 'NZ' ), + 'category_additional' => array(), ), array( 'id' => 'square_credit_card', 'title' => __( 'Square', 'woocommerce' ), 'content' => __( 'Securely accept credit and debit cards with one low rate, no surprise fees (custom rates available). Sell online and in store and track sales and inventory in one place.', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/square-black.png', + 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/square-black.png', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/square.png', 'plugins' => array( 'woocommerce-square' ), 'is_visible' => array( (object) array( @@ -374,12 +346,54 @@ class DefaultPaymentGateways { self::get_rules_for_cbd( true ), ), array( - self::get_rules_for_countries( array( 'US', 'CA', 'JP', 'GB', 'AU', 'IE', 'FR', 'ES' ) ), + self::get_rules_for_countries( array( 'US', 'CA', 'JP', 'GB', 'AU', 'IE', 'FR', 'ES', 'FI' ) ), self::get_rules_for_selling_venues( array( 'brick-mortar', 'brick-mortar-other' ) ), ), ), ), ), + 'category_other' => array( 'US', 'CA', 'JP', 'GB', 'AU', 'IE', 'FR', 'ES', 'FI' ), + 'category_additional' => array(), + ), + array( + 'id' => 'afterpay', + 'title' => __( 'Afterpay', 'woocommerce' ), + 'content' => __( 'Afterpay allows customers to receive products immediately and pay for purchases over four installments, always interest-free.', 'woocommerce' ), + 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/afterpay.png', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/afterpay.png', + 'plugins' => array( 'afterpay-gateway-for-woocommerce' ), + 'is_visible' => array( + self::get_rules_for_countries( array( 'US', 'CA' ) ), + ), + 'category_other' => array(), + 'category_additional' => array( 'US', 'CA' ), + ), + array( + 'id' => 'amazon_payments_advanced', + 'title' => __( 'Amazon Pay', 'woocommerce' ), + 'content' => __( 'Enable a familiar, fast checkout for hundreds of millions of active Amazon customers globally.', 'woocommerce' ), + 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/amazonpay.png', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/amazonpay.png', + 'plugins' => array( 'woocommerce-gateway-amazon-payments-advanced' ), + 'is_visible' => array( + self::get_rules_for_countries( array( 'US', 'CA' ) ), + ), + 'category_other' => array(), + 'category_additional' => array( 'US', 'CA' ), + ), + array( + 'id' => 'affirm', + 'title' => __( 'Affirm', 'woocommerce' ), + 'content' => __( 'Affirmā€™s tailored Buy Now Pay Later programs remove price as a barrier, turning browsers into buyers, increasing average order value, and expanding your customer base.', 'woocommerce' ), + 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/affirm.png', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/affirm.png', + 'plugins' => array(), + 'external_link' => 'https://woocommerce.com/products/woocommerce-gateway-affirm', + 'is_visible' => array( + self::get_rules_for_countries( array( 'US', 'CA' ) ), + ), + 'category_other' => array(), + 'category_additional' => array( 'US', 'CA' ), ), ); } diff --git a/plugins/woocommerce/src/Admin/Notes/DataStore.php b/plugins/woocommerce/src/Admin/Notes/DataStore.php index 0a7abdf954b..49b14e500b3 100644 --- a/plugins/woocommerce/src/Admin/Notes/DataStore.php +++ b/plugins/woocommerce/src/Admin/Notes/DataStore.php @@ -11,6 +11,9 @@ defined( 'ABSPATH' ) || exit; * WC Admin Note Data Store (Custom Tables) */ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface { + // Extensions should define their own contexts and use them to avoid applying woocommerce_note_where_clauses when not needed. + const WC_ADMIN_NOTE_OPER_GLOBAL = 'global'; + /** * Method to create a new note in the database. * @@ -323,10 +326,11 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter /** * Return an ordered list of notes. * - * @param array $args Query arguments. + * @param array $args Query arguments. + * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed. * @return array An array of objects containing a note id. */ - public function get_notes( $args = array() ) { + public function get_notes( $args = array(), $context = self::WC_ADMIN_NOTE_OPER_GLOBAL ) { global $wpdb; $defaults = array( @@ -338,7 +342,7 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter $args = wp_parse_args( $args, $defaults ); $offset = $args['per_page'] * ( $args['page'] - 1 ); - $where_clauses = $this->get_notes_where_clauses( $args ); + $where_clauses = $this->get_notes_where_clauses( $args, $context ); $query = $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared @@ -378,16 +382,18 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter * * @param string $type Comma separated list of note types. * @param string $status Comma separated list of statuses. + * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed. * @return array An array of objects containing a note id. */ - public function get_notes_count( $type = array(), $status = array() ) { + public function get_notes_count( $type = array(), $status = array(), $context = self::WC_ADMIN_NOTE_OPER_GLOBAL ) { global $wpdb; $where_clauses = $this->get_notes_where_clauses( array( 'type' => $type, 'status' => $status, - ) + ), + $context ); if ( ! empty( $where_clauses ) ) { @@ -425,10 +431,11 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter * Applies woocommerce_note_where_clauses filter. * * @uses args_to_where_clauses - * @param array $args Array of args to pass. + * @param array $args Array of args to pass. + * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed. * @return string Where clauses for the query. */ - public function get_notes_where_clauses( $args = array() ) { + public function get_notes_where_clauses( $args = array(), $context = self::WC_ADMIN_NOTE_OPER_GLOBAL ) { $where_clauses = $this->args_to_where_clauses( $args ); /** @@ -438,8 +445,9 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter * * @param string $where_clauses The generated WHERE clause. * @param array $args The original arguments for the request. + * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed. */ - return apply_filters( 'woocommerce_note_where_clauses', $where_clauses, $args ); + return apply_filters( 'woocommerce_note_where_clauses', $where_clauses, $args, $context ); } /** diff --git a/plugins/woocommerce/src/Admin/Notes/DeprecatedNotes.php b/plugins/woocommerce/src/Admin/Notes/DeprecatedNotes.php index 972c60cce13..6eafd2debb3 100644 --- a/plugins/woocommerce/src/Admin/Notes/DeprecatedNotes.php +++ b/plugins/woocommerce/src/Admin/Notes/DeprecatedNotes.php @@ -118,27 +118,6 @@ class WC_Admin_Notes_Customize_Store_With_Blocks extends DeprecatedClassFacade { protected static $deprecated_in_version = '1.7.0'; } -/** - * WC_Admin_Notes_Deactivate_Plugin. - * - * @deprecated since 1.7.0, use DeactivatePlugin - */ -class WC_Admin_Notes_Deactivate_Plugin extends DeprecatedClassFacade { - /** - * The name of the non-deprecated class that this facade covers. - * - * @var string - */ - protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\DeactivatePlugin'; - - /** - * The version that this class was deprecated in. - * - * @var string - */ - protected static $deprecated_in_version = '1.7.0'; -} - /** * WC_Admin_Notes_Edit_Products_On_The_Move. * diff --git a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/OptionRuleProcessor.php b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/OptionRuleProcessor.php index 57ad552174a..3ef30a9b97e 100644 --- a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/OptionRuleProcessor.php +++ b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/OptionRuleProcessor.php @@ -42,7 +42,7 @@ class OptionRuleProcessor implements RuleProcessorInterface { } if ( isset( $rule->transformers ) && is_array( $rule->transformers ) ) { - $option_value = TransformerService::apply( $option_value, $rule->transformers, $rule->default ); + $option_value = TransformerService::apply( $option_value, $rule->transformers, $default ); } return ComparisonOperation::compare( diff --git a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php index cecefeb26f9..04493617406 100644 --- a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php +++ b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php @@ -40,7 +40,7 @@ class RemoteInboxNotificationsEngine { // Hook into WCA updated. This is hooked up here rather than in // on_admin_init because that runs too late to hook into the action. add_action( - 'woocommerce_admin_updated', + 'woocommerce_updated', function() { $next_hook = WC()->queue()->get_next( 'woocommerce_run_on_woocommerce_admin_updated', diff --git a/plugins/woocommerce/src/Container.php b/plugins/woocommerce/src/Container.php index 711a6e16408..300e72d4aa4 100644 --- a/plugins/woocommerce/src/Container.php +++ b/plugins/woocommerce/src/Container.php @@ -6,6 +6,7 @@ namespace Automattic\WooCommerce; use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer; +use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\COTMigrationServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\DownloadPermissionsAdjusterServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\AssignDefaultCategoryServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrdersDataStoreServiceProvider; @@ -47,6 +48,7 @@ final class Container implements \Psr\Container\ContainerInterface { ProxiesServiceProvider::class, RestockRefundedItemsAdjusterServiceProvider::class, UtilsClassesServiceProvider::class, + COTMigrationServiceProvider::class, ); /** diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php new file mode 100644 index 00000000000..f69d9dd3e32 --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -0,0 +1,586 @@ +schema_config = MigrationHelper::escape_schema_for_backtick( $this->get_schema_config() ); + $this->meta_column_mapping = $this->get_meta_column_config(); + $this->core_column_mapping = $this->get_core_column_mapping(); + $this->errors = array(); + } + + /** + * Specify schema config the source and destination table. + * + * @return array Schema, must of the form: + * array( + 'source' => array( + 'entity' => array( + 'table_name' => $source_table_name, + 'meta_rel_column' => $column_meta, Name of column in source table which is referenced by meta table. + 'destination_rel_column' => $column_dest, Name of column in source table which is refenced by destination table, + 'primary_key' => $primary_key, Primary key of the source table + ), + 'meta' => array( + 'table' => $meta_table_name, + 'meta_key_column' => $meta_key_column_name, + 'meta_value_column' => $meta_value_column_name, + 'entity_id_column' => $entity_id_column, Name of the column having entity IDs. + ), + ), + 'destination' => array( + 'table_name' => $table_name, Name of destination table, + 'source_rel_column' => $column_source_id, Name of the column in destination table which is referenced by source table. + 'primary_key' => $table_primary_key, + 'primary_key_type' => $type bool|int|string|decimal + ) + */ + abstract public function get_schema_config(); + + /** + * Specify column config from the source table. + * + * @return array Config, must be of the form: + * array( + * '$source_column_name_1' => array( // $source_column_name_1 is column name in source table, or a select statement. + * 'type' => 'type of value, could be string/int/date/float.', + * 'destination' => 'name of the column in column name where this data should be inserted in.', + * ), + * '$source_column_name_2' => array( + * ...... + * ), + * .... + * ). + */ + abstract public function get_core_column_mapping(); + + /** + * Specify meta keys config from source meta table. + * + * @return array Config, must be of the form. + * array( + * '$meta_key_1' => array( // $meta_key_1 is the name of meta_key in source meta table. + * 'type' => 'type of value, could be string/int/date/float', + * 'destination' => 'name of the column in column name where this data should be inserted in.', + * ), + * '$meta_key_2' => array( + * ...... + * ), + * .... + * ). + */ + abstract public function get_meta_column_config(); + + /** + * Generate SQL for data insertion. + * + * @param array $batch Data to generate queries for. Will be 'data' array returned by `$this->fetch_data_for_migration_for_ids()` method. + * + * @return string Generated queries for insertion for this batch, would be of the form: + * INSERT IGNORE INTO $table_name ($columns) values + * ($value for row 1) + * ($value for row 2) + * ... + */ + public function generate_insert_sql_for_batch( $batch ) { + $table = $this->schema_config['destination']['table_name']; + + list( $value_sql, $column_sql ) = $this->generate_column_clauses( array_merge( $this->core_column_mapping, $this->meta_column_mapping ), $batch ); + + return "INSERT IGNORE INTO $table (`$column_sql`) VALUES $value_sql;"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, -- $insert_query is hardcoded, $value_sql is already escaped. + } + + /** + * Generate SQL for data updating. + * + * @param array $batch Data to generate queries for. Will be `data` array returned by fetch_data_for_migration_for_ids() method. + * + * @param array $entity_row_mapping Maps rows to update data with their original IDs. Will be returned by `generate_update_sql_for_batch`. + * + * @return string Generated queries for batch update. Would be of the form: + * INSERT INTO $table ( $columns ) VALUES + * ($value for row 1) + * ($valye for row 2) + * ... + * ON DUPLICATE KEY UPDATE + * $column1 = VALUES($column1) + * $column2 = VALUES($column2) + * ... + */ + public function generate_update_sql_for_batch( $batch, $entity_row_mapping ) { + $table = $this->schema_config['destination']['table_name']; + + $destination_primary_id_schema = $this->get_destination_table_primary_id_schema(); + foreach ( $batch as $entity_id => $row ) { + $batch[ $entity_id ][ $destination_primary_id_schema['destination_primary_key']['destination'] ] = $entity_row_mapping[ $entity_id ]->destination_id; + } + + list( $value_sql, $column_sql, $columns ) = $this->generate_column_clauses( + array_merge( $destination_primary_id_schema, $this->core_column_mapping, $this->meta_column_mapping ), + $batch + ); + + $duplicate_update_key_statement = $this->generate_on_duplicate_statement_clause( $columns ); + + return "INSERT INTO $table (`$column_sql`) VALUES $value_sql $duplicate_update_key_statement;"; + } + + /** + * Generate schema for primary ID column of destination table. + * + * @return array[] Schema for primary ID column. + */ + protected function get_destination_table_primary_id_schema() { + return array( + 'destination_primary_key' => array( + 'destination' => $this->schema_config['destination']['primary_key'], + 'type' => $this->schema_config['destination']['primary_key_type'], + ), + ); + } + + /** + * Generate values and columns clauses to be used in INSERT and INSERT..ON DUPLICATE KEY UPDATE statements. + * + * @param array $columns_schema Columns config for destination table. + * @param array $batch Actual data to migrate as returned by `data` in `fetch_data_for_migration_for_ids` method. + * + * @return array SQL clause for values, columns placeholders, and columns. + */ + protected function generate_column_clauses( $columns_schema, $batch ) { + global $wpdb; + + $columns = array(); + $placeholders = array(); + foreach ( $columns_schema as $prev_column => $schema ) { + $columns[] = $schema['destination']; + $placeholders[] = MigrationHelper::get_wpdb_placeholder_for_type( $schema['type'] ); + } + $placeholders = "'" . implode( "', '", $placeholders ) . "'"; + + $values = array(); + foreach ( array_values( $batch ) as $row ) { + $query_params = array(); + foreach ( $columns as $column ) { + $query_params[] = $row[ $column ] ?? null; + } + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $placeholders can only contain combination of placeholders described in MigrationHelper::get_wpdb_placeholder_for_type + $value_string = '(' . $wpdb->prepare( $placeholders, $query_params ) . ')'; + $values[] = $value_string; + } + + $value_sql = implode( ',', $values ); + + $column_sql = implode( '`, `', $columns ); + + return array( $value_sql, $column_sql, $columns ); + } + + /** + * Generates ON DUPLICATE KEY UPDATE clause to be used in migration. + * + * @param array $columns List of column names. + * + * @return string SQL clause for INSERT...ON DUPLICATE KEY UPDATE + */ + private function generate_on_duplicate_statement_clause( $columns ) { + $update_value_statements = array(); + foreach ( $columns as $column ) { + $update_value_statements[] = "$column = VALUES( $column )"; + } + $update_value_clause = implode( ', ', $update_value_statements ); + + return "ON DUPLICATE KEY UPDATE $update_value_clause"; + } + + /** + * Process next migration batch, uses option `wc_cot_migration` to checkpoints of what have been processed so far. + * + * @param array $entity_ids List of entity IDs to perform migrations for. + * + * @return array List of errors happened during migration. + */ + public function process_migration_batch_for_ids( $entity_ids ) { + $data = $this->fetch_data_for_migration_for_ids( $entity_ids ); + + foreach ( $data['errors'] as $entity_id => $error ) { + $this->errors[ $entity_id ] = "Error in importing post id $entity_id: " . $error->get_message(); + } + + if ( count( $data['data'] ) === 0 ) { + return array(); + } + + $entity_ids = array_keys( $data['data'] ); + $already_migrated = $this->get_already_migrated_records( $entity_ids ); + + $to_insert = array_diff_key( $data['data'], $already_migrated ); + $this->process_insert_batch( $to_insert ); + + $to_update = array_intersect_key( $data['data'], $already_migrated ); + $this->process_update_batch( $to_update, $already_migrated ); + + return array( + 'errors' => $this->errors, + ); + } + + /** + * Process batch for insertion into destination table. + * + * @param array $batch Data to insert, will be of the form as returned by `data` in `fetch_data_for_migration_for_ids`. + */ + protected function process_insert_batch( $batch ) { + global $wpdb; + if ( 0 === count( $batch ) ) { + return; + } + $queries = $this->generate_insert_sql_for_batch( $batch ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Queries should already be prepared. + $result = $wpdb->query( $queries ); + $wpdb->query( 'COMMIT;' ); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? + if ( count( $batch ) !== $result ) { + $this->errors[] = 'Error with batch: ' . $wpdb->last_error; + } + } + + /** + * Process batch for update into destination table. + * + * @param array $batch Data to insert, will be of the form as returned by `data` in `fetch_data_for_migration_for_ids`. + * @param array $already_migrated Maps rows to update data with their original IDs. + */ + protected function process_update_batch( $batch, $already_migrated ) { + global $wpdb; + if ( 0 === count( $batch ) ) { + return; + } + $queries = $this->generate_update_sql_for_batch( $batch, $already_migrated ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Queries should already be prepared. + $result = $wpdb->query( $queries ); + $wpdb->query( 'COMMIT;' ); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? + if ( count( $batch ) !== $result ) { + $this->errors[] = 'Error with batch: ' . $wpdb->last_error; + } + } + + + /** + * Fetch data for migration. + * + * @param array $entity_ids Entity IDs to fetch data for. + * + * @return array[] Data along with errors (if any), will of the form: + * array( + * 'data' => array( + * 'id_1' => array( 'column1' => value1, 'column2' => value2, ...), + * ..., + * ), + * 'errors' => array( + * 'id_1' => array( 'column1' => error1, 'column2' => value2, ...), + * ..., + * ) + */ + public function fetch_data_for_migration_for_ids( $entity_ids ) { + global $wpdb; + + if ( empty( $entity_ids ) ) { + return array( + 'data' => array(), + 'errors' => array(), + ); + } + + $entity_table_query = $this->build_entity_table_query( $entity_ids ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_entity_table_query is already prepared. + $entity_data = $wpdb->get_results( $entity_table_query ); + if ( empty( $entity_data ) ) { + return array( + 'data' => array(), + 'errors' => array(), + ); + } + $entity_meta_rel_ids = array_column( $entity_data, 'entity_meta_rel_id' ); + + $meta_table_query = $this->build_meta_data_query( $entity_meta_rel_ids ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_meta_data_query is already prepared. + $meta_data = $wpdb->get_results( $meta_table_query ); + + return $this->process_and_sanitize_data( $entity_data, $meta_data ); + } + + /** + * Fetch id mappings for records that are already inserted, or can be considered duplicates. + * + * @param array $entity_ids List of entity IDs to verify. + * + * @return array Already migrated entities, would be of the form + * array( + * '$source_id1' => array( + * 'source_id' => $source_id1, + * 'destination_id' => $destination_id1, + * ), + * ... + * ) + */ + public function get_already_migrated_records( $entity_ids ) { + global $wpdb; + $source_table = $this->schema_config['source']['entity']['table_name']; + $source_destination_join_column = $this->schema_config['source']['entity']['destination_rel_column']; + $source_primary_key_column = $this->schema_config['source']['entity']['primary_key']; + + $destination_table = $this->schema_config['destination']['table_name']; + $destination_source_join_column = $this->schema_config['destination']['source_rel_column']; + $destination_primary_key_column = $this->schema_config['destination']['primary_key']; + + $entity_id_placeholder = implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) ); + + $already_migrated_entity_ids = $wpdb->get_results( + $wpdb->prepare( + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- All columns and table names are hardcoded. + " +SELECT source.`$source_primary_key_column` as source_id, destination.`$destination_primary_key_column` as destination_id +FROM `$destination_table` destination +JOIN `$source_table` source ON source.`$source_destination_join_column` = destination.`$destination_source_join_column` +WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) + ", + $entity_ids + ) + // phpcs:enable + ); + + return array_column( $already_migrated_entity_ids, null, 'source_id' ); + } + + + /** + * Helper method to build query used to fetch data from core source table. + * + * @param array $entity_ids List of entity IDs to fetch. + * + * @return string Query that can be used to fetch data. + */ + protected function build_entity_table_query( $entity_ids ) { + global $wpdb; + $source_entity_table = $this->schema_config['source']['entity']['table_name']; + $source_meta_rel_id_column = "`$source_entity_table`.`{$this->schema_config['source']['entity']['meta_rel_column']}`"; + $source_primary_key_column = "`$source_entity_table`.`{$this->schema_config['source']['entity']['primary_key']}`"; + + $where_clause = "$source_primary_key_column IN (" . implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) ) . ')'; + $entity_keys = array(); + foreach ( $this->core_column_mapping as $column_name => $column_schema ) { + if ( isset( $column_schema['select_clause'] ) ) { + $select_clause = $column_schema['select_clause']; + $entity_keys[] = "$select_clause AS $column_name"; + } else { + $entity_keys[] = "$source_entity_table.$column_name"; + } + } + $entity_column_string = implode( ', ', $entity_keys ); + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $source_meta_rel_id_column, $source_destination_rel_id_column etc is escaped for backticks. $where clause and $order_by should already be escaped. + $query = $wpdb->prepare( + " +SELECT + $source_meta_rel_id_column as entity_meta_rel_id, + $entity_column_string +FROM `$source_entity_table` +WHERE $where_clause; +", + $entity_ids + ); + + // phpcs:enable + + return $query; + } + + /** + * Helper method to build query that will be used to fetch data from source meta table. + * + * @param array $entity_ids List of IDs to fetch metadata for. + * + * @return string Query for fetching meta data. + */ + protected function build_meta_data_query( $entity_ids ) { + global $wpdb; + $meta_table = $this->schema_config['source']['meta']['table_name']; + $meta_keys = array_keys( $this->meta_column_mapping ); + $meta_key_column = $this->schema_config['source']['meta']['meta_key_column']; + $meta_value_column = $this->schema_config['source']['meta']['meta_value_column']; + $meta_table_relational_key = $this->schema_config['source']['meta']['entity_id_column']; + + $meta_column_string = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) ); + $entity_id_string = implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) ); + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $meta_table_relational_key, $meta_key_column, $meta_value_column and $meta_table is escaped for backticks. $entity_id_string and $meta_column_string are placeholders. + $query = $wpdb->prepare( + " +SELECT `$meta_table_relational_key` as entity_id, `$meta_key_column` as meta_key, `$meta_value_column` as meta_value +FROM `$meta_table` +WHERE + `$meta_table_relational_key` IN ( $entity_id_string ) + AND `$meta_key_column` IN ( $meta_column_string ); +", + array_merge( + $entity_ids, + $meta_keys + ) + ); + + // phpcs:enable + + return $query; + } + + /** + * Helper function to validate and combine data before we try to insert. + * + * @param array $entity_data Data from source table. + * @param array $meta_data Data from meta table. + * + * @return array[] Validated and combined data with errors. + */ + private function process_and_sanitize_data( $entity_data, $meta_data ) { + $sanitized_entity_data = array(); + $error_records = array(); + $this->process_and_sanitize_entity_data( $sanitized_entity_data, $error_records, $entity_data ); + $this->processs_and_sanitize_meta_data( $sanitized_entity_data, $error_records, $meta_data ); + + return array( + 'data' => $sanitized_entity_data, + 'errors' => $error_records, + ); + } + + /** + * Helper method to sanitize core source table. + * + * @param array $sanitized_entity_data Array containing sanitized data for insertion. + * @param array $error_records Error records. + * @param array $entity_data Original source data. + */ + private function process_and_sanitize_entity_data( &$sanitized_entity_data, &$error_records, $entity_data ) { + foreach ( $entity_data as $entity ) { + $row_data = array(); + foreach ( $this->core_column_mapping as $column_name => $schema ) { + $custom_table_column_name = $schema['destination'] ?? $column_name; + $value = $entity->$column_name; + $value = $this->validate_data( $value, $schema['type'] ); + if ( is_wp_error( $value ) ) { + $error_records[ $entity->primary_key_id ][ $custom_table_column_name ] = $value->get_error_message(); + } else { + $row_data[ $custom_table_column_name ] = $value; + } + } + $sanitized_entity_data[ $entity->entity_meta_rel_id ] = $row_data; + } + } + + /** + * Helper method to sanitize soure meta data. + * + * @param array $sanitized_entity_data Array containing sanitized data for insertion. + * @param array $error_records Error records. + * @param array $meta_data Original source data. + */ + private function processs_and_sanitize_meta_data( &$sanitized_entity_data, &$error_records, $meta_data ) { + foreach ( $meta_data as $datum ) { + $column_schema = $this->meta_column_mapping[ $datum->meta_key ]; + $value = $this->validate_data( $datum->meta_value, $column_schema['type'] ); + if ( is_wp_error( $value ) ) { + $error_records[ $datum->entity_id ][ $column_schema['destination'] ] = "{$value->get_error_code()}: {$value->get_error_message()}"; + } else { + $sanitized_entity_data[ $datum->entity_id ][ $column_schema['destination'] ] = $value; + } + } + } + + /** + * Validate and transform data so that we catch as many errors as possible before inserting. + * + * @param mixed $value Actual data value. + * @param string $type Type of data, could be decimal, int, date, string. + * + * @return float|int|mixed|string|\WP_Error + */ + private function validate_data( $value, $type ) { + switch ( $type ) { + case 'decimal': + $value = (float) $value; + break; + case 'int': + $value = (int) $value; + break; + case 'bool': + $value = wc_string_to_bool( $value ); + break; + case 'date': + try { + if ( '' === $value ) { + $value = null; + } else { + $value = ( new \DateTime( $value ) )->format( 'Y-m-d H:i:s' ); + } + } catch ( \Exception $e ) { + return new \WP_Error( $e->getMessage() ); + } + break; + case 'date_epoch': + try { + if ( '' === $value ) { + $value = null; + } else { + $value = ( new \DateTime( "@$value" ) )->format( 'Y-m-d H:i:s' ); + } + } catch ( \Exception $e ) { + return new \WP_Error( $e->getMessage() ); + } + break; + } + + return $value; + } +} diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php new file mode 100644 index 00000000000..f043b70ffe2 --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php @@ -0,0 +1,161 @@ +schema_config = $schema_config; + } + + /** + * Generate insert sql queries for batches. + * + * @param array $batch Data to generate queries for. + * @param string $insert_switch Insert switch to use. + * + * @return string + */ + public function generate_insert_sql_for_batch( $batch, $insert_switch ) { + global $wpdb; + + $insert_query = MigrationHelper::get_insert_switch( $insert_switch ); + + $meta_key_column = $this->schema_config['destination']['meta']['meta_key_column']; + $meta_value_column = $this->schema_config['destination']['meta']['meta_value_column']; + $entity_id_column = $this->schema_config['destination']['meta']['entity_id_column']; + $column_sql = "(`$entity_id_column`, `$meta_key_column`, `$meta_value_column`)"; + $table = $this->schema_config['destination']['meta']['table_name']; + + $entity_id_column_placeholder = MigrationHelper::get_wpdb_placeholder_for_type( $this->schema_config['destination']['meta']['entity_id_type'] ); + $placeholder_string = "$entity_id_column_placeholder, %s, %s"; + $values = array(); + foreach ( array_values( $batch ) as $row ) { + $query_params = array( + $row->destination_entity_id, + $row->meta_key, + $row->meta_value, + ); + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $placeholder_string is hardcoded. + $value_sql = $wpdb->prepare( "$placeholder_string", $query_params ); + $values[] = $value_sql; + } + + $values_sql = implode( '), (', $values ); + + return "$insert_query INTO $table $column_sql VALUES ($values_sql)"; + } + + /** + * Fetch data for migration. + * + * @param array $order_post_ids Array of IDs to fetch data for. + * + * @return array[] Data along with errors (if any), will of the form: + * array( + * 'data' => array( + * 'id_1' => array( 'column1' => value1, 'column2' => value2, ...), + * ..., + * ), + * 'errors' => array( + * 'id_1' => array( 'column1' => error1, 'column2' => value2, ...), + * ..., + * ) + */ + public function fetch_data_for_migration_for_ids( $order_post_ids ) { + global $wpdb; + if ( empty( $order_post_ids ) ) { + return array( + 'data' => array(), + 'errors' => array(), + ); + } + + $meta_query = $this->build_meta_table_query( $order_post_ids ); + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Meta query has interpolated variables, but they should all be escaped for backticks. + $meta_data_rows = $wpdb->get_results( $meta_query ); + if ( empty( $meta_data_rows ) ) { + return array( + 'data' => array(), + 'errors' => array(), + ); + } + + return array( + 'data' => $meta_data_rows, + 'errors' => array(), + ); + } + + /** + * Helper method to build query used to fetch data from source meta table. + * + * @param string $entity_ids List of entity IDs to build meta query for. + * + * @return string Query that can be used to fetch data. + */ + private function build_meta_table_query( $entity_ids ) { + global $wpdb; + $source_meta_table = $this->schema_config['source']['meta']['table_name']; + $source_meta_key_column = $this->schema_config['source']['meta']['meta_key_column']; + $source_meta_value_column = $this->schema_config['source']['meta']['meta_value_column']; + $source_entity_id_column = $this->schema_config['source']['meta']['entity_id_column']; + $order_by = "$source_entity_id_column ASC"; + + $where_clause = "$source_entity_id_column IN (" . implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) ) . ')'; + + $destination_entity_table = $this->schema_config['destination']['entity']['table_name']; + $destination_entity_id_column = $this->schema_config['destination']['entity']['id_column']; + $destination_source_id_mapping_column = $this->schema_config['destination']['entity']['source_id_column']; + + if ( $this->schema_config['source']['excluded_keys'] ) { + $key_placeholder = implode( ',', array_fill( 0, count( $this->schema_config['source']['excluded_keys'] ), '%s' ) ); + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $source_meta_key_column is escated for backticks, $key_placeholder is hardcoded. + $exclude_clause = $wpdb->prepare( "source.$source_meta_key_column NOT IN ( $key_placeholder )", $this->schema_config['source']['excluded_keys'] ); + $where_clause = "$where_clause AND $exclude_clause"; + } + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + return $wpdb->prepare( + " +SELECT + source.`$source_entity_id_column` as source_entity_id, + destination.`$destination_entity_id_column` as destination_entity_id, + source.`$source_meta_key_column` as meta_key, + source.`$source_meta_value_column` as meta_value +FROM `$source_meta_table` source +JOIN `$destination_entity_table` destination ON destination.`$destination_source_id_mapping_column` = source.`$source_entity_id_column` +WHERE $where_clause ORDER BY $order_by +", + $entity_ids + ); + // phpcs:enable + } +} diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php new file mode 100644 index 00000000000..6e94d9106e4 --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -0,0 +1,232 @@ +order_table_migrator = new WPPostToOrderTableMigrator(); + $this->billing_address_table_migrator = new WPPostToOrderAddressTableMigrator( 'billing' ); + $this->shipping_address_table_migrator = new WPPostToOrderAddressTableMigrator( 'shipping' ); + $this->operation_data_table_migrator = new WPPostToOrderOpTableMigrator(); + + $meta_data_config = $this->get_config_for_meta_table(); + + $this->meta_table_migrator = new MetaToMetaTableMigrator( $meta_data_config ); + + $this->error_logger = new MigrationErrorLogger(); + } + + /** + * Generate config for meta data migration. + * + * @return array Meta data migration config. + */ + private function get_config_for_meta_table() { + global $wpdb; + // TODO: Remove hardcoding. + $this->table_names = array( + 'orders' => $wpdb->prefix . 'wc_orders', + 'addresses' => $wpdb->prefix . 'wc_order_addresses', + 'op_data' => $wpdb->prefix . 'wc_order_operational_data', + 'meta' => $wpdb->prefix . 'wc_orders_meta', + ); + + $excluded_columns = array_keys( $this->order_table_migrator->get_meta_column_config() ); + $excluded_columns = array_merge( $excluded_columns, array_keys( $this->billing_address_table_migrator->get_meta_column_config() ) ); + $excluded_columns = array_merge( $excluded_columns, array_keys( $this->shipping_address_table_migrator->get_meta_column_config() ) ); + $excluded_columns = array_merge( $excluded_columns, array_keys( $this->operation_data_table_migrator->get_meta_column_config() ) ); + + return array( + 'source' => array( + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'entity_id_column' => 'post_id', + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + ), + 'excluded_keys' => $excluded_columns, + ), + 'destination' => array( + 'meta' => array( + 'table_name' => $this->table_names['meta'], + 'entity_id_column' => 'order_id', + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'entity_id_type' => 'int', + ), + 'entity' => array( + 'table_name' => $this->table_names['orders'], + 'source_id_column' => 'post_id', + 'id_column' => 'id', + ), + ), + ); + } + + /** + * Process next migration batch, uses option `wc_cot_migration` to checkpoints of what have been processed so far. + * + * @param int $batch_size Batch size of records to migrate. + * + * @return bool True if migration is completed, false if there are still records to process. + */ + public function process_next_migration_batch( $batch_size = 100 ) { + $order_post_ids = $this->get_next_batch_ids( $batch_size ); + if ( 0 === count( $order_post_ids ) ) { + return true; + } + $this->process_migration_for_ids( $order_post_ids ); + $last_post_migrated = max( $order_post_ids ); + $this->update_checkpoint( $last_post_migrated ); + return false; + } + + /** + * Process migration for specific order post IDs. + * + * @param array $order_post_ids List of post IDs to migrate. + */ + public function process_migration_for_ids( $order_post_ids ) { + $this->order_table_migrator->process_migration_batch_for_ids( $order_post_ids ); + $this->billing_address_table_migrator->process_migration_batch_for_ids( $order_post_ids ); + $this->shipping_address_table_migrator->process_migration_batch_for_ids( $order_post_ids ); + $this->operation_data_table_migrator->process_migration_batch_for_ids( $order_post_ids ); + // TODO: Add resilience for meta migrations. + // $this->process_meta_migration( $order_post_ids ); + // TODO: Return merged error array. + } + + /** + * Process migration for metadata for given post ids. + * + * @param array $order_post_ids Post IDs. + */ + private function process_meta_migration( $order_post_ids ) { + global $wpdb; + $data_to_migrate = $this->meta_table_migrator->fetch_data_for_migration_for_ids( $order_post_ids ); + $insert_queries = $this->meta_table_migrator->generate_insert_sql_for_batch( $data_to_migrate['data'], 'insert' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $insert_queries should already be escaped in the generating function. + $result = $wpdb->query( $insert_queries ); + if ( count( $data_to_migrate['data'] ) !== $result ) { + // TODO: Find and log entity ids that were not inserted. + echo 'error'; + } + } + + /** + * Method to migrate single record. + * + * @param int $post_id Post ID of record to migrate. + */ + public function process_single( $post_id ) { + $this->process_migration_for_ids( array( $post_id ) ); + // TODO: Return error. + } + + /** + * Helper function to get where clause to send to MetaToCustomTableMigrator instance. + * + * @param int $batch_size Number of orders in batch. + * + * @return array List of IDs in the current patch. + */ + private function get_next_batch_ids( $batch_size ) { + global $wpdb; + + $checkpoint = $this->get_checkpoint(); + $post_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE ID > %d AND post_type = %s ORDER BY ID ASC LIMIT %d ", + $checkpoint['id'], + 'shop_order', + $batch_size + ) + ); + + return $post_ids; + } + + /** + * Current checkpoint status. + * + * @return false|mixed|void + */ + private function get_checkpoint() { + return get_option( 'wc_cot_migration', array( 'id' => 0 ) ); + } + + /** + * Updates current checkpoint + * + * @param int $id Order ID. + */ + public function update_checkpoint( $id ) { + return update_option( 'wc_cot_migration', array( 'id' => $id ), false ); + } + + /** + * Remove checkpoint. + * + * @return bool Whether checkpoint was removed. + */ + public function delete_checkpoint() { + return delete_option( 'wp_cot_migration' ); + } +} diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php new file mode 100644 index 00000000000..bbb2be8dda2 --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php @@ -0,0 +1,191 @@ +type = $type; + parent::__construct(); + } + + /** + * Get schema config for wp_posts and wc_order_address table. + * + * @return array Config. + */ + public function get_schema_config() { + global $wpdb; + // TODO: Remove hardcoding. + $this->table_names = array( + 'orders' => $wpdb->prefix . 'wc_orders', + 'addresses' => $wpdb->prefix . 'wc_order_addresses', + 'op_data' => $wpdb->prefix . 'wc_order_operational_data', + 'meta' => $wpdb->prefix . 'wc_orders_meta', + ); + + return array( + 'source' => array( + 'entity' => array( + 'table_name' => $this->table_names['orders'], + 'meta_rel_column' => 'post_id', + 'destination_rel_column' => 'id', + 'primary_key' => 'post_id', + ), + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'entity_id_column' => 'post_id', + ), + ), + 'destination' => array( + 'table_name' => $this->table_names['addresses'], + 'source_rel_column' => 'order_id', + 'primary_key' => 'id', + 'primary_key_type' => 'int', + ), + ); + } + + /** + * Get columns config. + * + * @return \string[][] Config. + */ + public function get_core_column_mapping() { + $type = $this->type; + + return array( + 'id' => array( + 'type' => 'int', + 'destination' => 'order_id', + ), + 'type' => array( + 'type' => 'string', + 'destination' => 'address_type', + 'select_clause' => "'$type'", + ), + ); + } + + /** + * Get meta data config. + * + * @return \string[][] Config. + */ + public function get_meta_column_config() { + $type = $this->type; + + return array( + "_{$type}_first_name" => array( + 'type' => 'string', + 'destination' => 'first_name', + ), + "_{$type}_last_name" => array( + 'type' => 'string', + 'destination' => 'last_name', + ), + "_{$type}_company" => array( + 'type' => 'string', + 'destination' => 'company', + ), + "_{$type}_address_1" => array( + 'type' => 'string', + 'destination' => 'address_1', + ), + "_{$type}_address_2" => array( + 'type' => 'string', + 'destination' => 'address_2', + ), + "_{$type}_city" => array( + 'type' => 'string', + 'destination' => 'city', + ), + "_{$type}_state" => array( + 'type' => 'string', + 'destination' => 'state', + ), + "_{$type}_postcode" => array( + 'type' => 'string', + 'destination' => 'postcode', + ), + "_{$type}_country" => array( + 'type' => 'string', + 'destination' => 'country', + ), + "_{$type}_email" => array( + 'type' => 'string', + 'destination' => 'email', + ), + "_{$type}_phone" => array( + 'type' => 'string', + 'destination' => 'phone', + ), + ); + } + + /** + * We overwrite this method to add a subclause to only fetch address of current type. + * + * @param array $entity_ids List of entity IDs to verify. + * + * @return array Already migrated entities, would be of the form + * array( + * '$source_id1' => array( + * 'source_id' => $source_id1, + * 'destination_id' => $destination_id1, + * ), + * ... + * ) + */ + public function get_already_migrated_records( $entity_ids ) { + global $wpdb; + $source_table = $this->schema_config['source']['entity']['table_name']; + $source_destination_join_column = $this->schema_config['source']['entity']['destination_rel_column']; + $source_primary_key_column = $this->schema_config['source']['entity']['primary_key']; + + $destination_table = $this->schema_config['destination']['table_name']; + $destination_source_join_column = $this->schema_config['destination']['source_rel_column']; + $destination_primary_key_column = $this->schema_config['destination']['primary_key']; + + $address_type = $this->type; + + $entity_id_placeholder = implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) ); + + $already_migrated_entity_ids = $wpdb->get_results( + $wpdb->prepare( + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- All columns and table names are hardcoded. + " +SELECT source.`$source_primary_key_column` as source_id, destination.`$destination_primary_key_column` as destination_id +FROM `$destination_table` destination +JOIN `$source_table` source ON source.`$source_destination_join_column` = destination.`$destination_source_join_column` +WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) AND destination.`address_type` = '$address_type' + ", + $entity_ids + ) + // phpcs:enable + ); + + return array_column( $already_migrated_entity_ids, null, 'source_id' ); + } +} diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php new file mode 100644 index 00000000000..54217a421c4 --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php @@ -0,0 +1,139 @@ +table_names = array( + 'orders' => $wpdb->prefix . 'wc_orders', + 'addresses' => $wpdb->prefix . 'wc_order_addresses', + 'op_data' => $wpdb->prefix . 'wc_order_operational_data', + 'meta' => $wpdb->prefix . 'wc_orders_meta', + ); + + return array( + 'source' => array( + 'entity' => array( + 'table_name' => $this->table_names['orders'], + 'meta_rel_column' => 'post_id', + 'destination_rel_column' => 'id', + 'primary_key' => 'post_id', + ), + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'entity_id_column' => 'post_id', + ), + ), + 'destination' => array( + 'table_name' => $this->table_names['op_data'], + 'source_rel_column' => 'order_id', + 'primary_key' => 'id', + 'primary_key_type' => 'int', + ), + ); + } + + + /** + * Get columns config. + * + * @return \string[][] Config. + */ + public function get_core_column_mapping() { + return array( + 'id' => array( + 'type' => 'int', + 'destination' => 'order_id', + ), + ); + } + + + /** + * Get meta data config. + * + * @return \string[][] Config. + */ + public function get_meta_column_config() { + return array( + '_created_via' => array( + 'type' => 'string', + 'destination' => 'created_via', + ), + '_order_version' => array( + 'type' => 'string', + 'destination' => 'woocommerce_version', + ), + '_prices_include_tax' => array( + 'type' => 'bool', + 'destination' => 'prices_include_tax', + ), + '_recorded_coupon_usage_counts' => array( + 'type' => 'bool', + 'destination' => 'coupon_usages_are_counted', + ), + '_download_permissions_granted' => array( + 'type' => 'bool', + 'destination' => 'download_permission_granted', + ), + '_cart_hash' => array( + 'type' => 'string', + 'destination' => 'cart_hash', + ), + '_new_order_email_sent' => array( + 'type' => 'bool', + 'destination' => 'new_order_email_sent', + ), + '_order_key' => array( + 'type' => 'string', + 'destination' => 'order_key', + ), + '_order_stock_reduced' => array( + 'type' => 'bool', + 'destination' => 'order_stock_reduced', + ), + '_date_paid' => array( + 'type' => 'date_epoch', + 'destination' => 'date_paid_gmt', + ), + '_date_completed' => array( + 'type' => 'date_epoch', + 'destination' => 'date_completed_gmt', + ), + '_order_shipping_tax' => array( + 'type' => 'decimal', + 'destination' => 'shipping_tax_amount', + ), + '_order_shipping' => array( + 'type' => 'decimal', + 'destination' => 'shipping_total_amount', + ), + '_cart_discount_tax' => array( + 'type' => 'decimal', + 'destination' => 'discount_tax_amount', + ), + '_cart_discount' => array( + 'type' => 'decimal', + 'destination' => 'discount_total_amount', + ), + ); + } +} diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php new file mode 100644 index 00000000000..e18b3876dfe --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php @@ -0,0 +1,132 @@ +table_names = array( + 'orders' => $wpdb->prefix . 'wc_orders', + 'addresses' => $wpdb->prefix . 'wc_order_addresses', + 'op_data' => $wpdb->prefix . 'wc_order_operational_data', + 'meta' => $wpdb->prefix . 'wc_orders_meta', + ); + + return array( + 'source' => array( + 'entity' => array( + 'table_name' => $wpdb->posts, + 'meta_rel_column' => 'ID', + 'destination_rel_column' => 'ID', + 'primary_key' => 'ID', + ), + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'entity_id_column' => 'post_id', + ), + ), + 'destination' => array( + 'table_name' => $this->table_names['orders'], + 'source_rel_column' => 'post_id', + 'primary_key' => 'id', + 'primary_key_type' => 'int', + ), + ); + } + + /** + * Get columns config. + * + * @return \string[][] Config. + */ + public function get_core_column_mapping() { + return array( + 'ID' => array( + 'type' => 'int', + 'destination' => 'post_id', + ), + 'post_status' => array( + 'type' => 'string', + 'destination' => 'status', + ), + 'post_date_gmt' => array( + 'type' => 'date', + 'destination' => 'date_created_gmt', + ), + 'post_modified_gmt' => array( + 'type' => 'date', + 'destination' => 'date_updated_gmt', + ), + 'post_parent' => array( + 'type' => 'int', + 'destination' => 'parent_order_id', + ), + ); + } + + /** + * Get meta data config. + * + * @return \string[][] Config. + */ + public function get_meta_column_config() { + return array( + '_order_currency' => array( + 'type' => 'string', + 'destination' => 'currency', + ), + '_order_tax' => array( + 'type' => 'decimal', + 'destination' => 'tax_amount', + ), + '_order_total' => array( + 'type' => 'decimal', + 'destination' => 'total_amount', + ), + '_customer_user' => array( + 'type' => 'int', + 'destination' => 'customer_id', + ), + '_billing_email' => array( + 'type' => 'string', + 'destination' => 'billing_email', + ), + '_payment_method' => array( + 'type' => 'string', + 'destination' => 'payment_method', + ), + '_payment_method_title' => array( + 'type' => 'string', + 'destination' => 'payment_method_title', + ), + '_customer_ip_address' => array( + 'type' => 'string', + 'destination' => 'ip_address', + ), + '_customer_user_agent' => array( + 'type' => 'string', + 'destination' => 'user_agent', + ), + '_transaction_id' => array( + 'type' => 'string', + 'destination' => 'transaction_id', + ), + ); + } +} diff --git a/plugins/woocommerce/src/Database/Migrations/MigrationErrorLogger.php b/plugins/woocommerce/src/Database/Migrations/MigrationErrorLogger.php new file mode 100644 index 00000000000..191dce8963d --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/MigrationErrorLogger.php @@ -0,0 +1,17 @@ + '%d', + 'decimal' => '%f', + 'string' => '%s', + 'date' => '%s', + 'date_epoch' => '%s', + 'bool' => '%d', + ); + + /** + * Get insert clause for appropriate switch. + * + * @param string $switch Name of the switch to use. + * + * @return string Insert clause. + */ + public static function get_insert_switch( $switch ) { + switch ( $switch ) { + case 'insert_ignore': + $insert_query = 'INSERT IGNORE'; + break; + case 'replace': // delete and then insert. + $insert_query = 'REPLACE'; + break; + case 'update': + $insert_query = 'UPDATE'; + break; + case 'insert': + default: + $insert_query = 'INSERT'; + } + + return $insert_query; + } + + /** + * Helper method to escape backtick in various schema fields. + * + * @param array $schema_config Schema config. + * + * @return array Schema config escaped for backtick. + */ + public static function escape_schema_for_backtick( $schema_config ) { + array_walk( $schema_config['source']['entity'], array( self::class, 'escape_and_add_backtick' ) ); + array_walk( $schema_config['source']['meta'], array( self::class, 'escape_and_add_backtick' ) ); + array_walk( $schema_config['destination'], array( self::class, 'escape_and_add_backtick' ) ); + return $schema_config; + } + + /** + * Helper method to escape backtick in column and table names. + * WP does not provide a method to escape table/columns names yet, but hopefully soon in @link https://core.trac.wordpress.org/ticket/52506 + * + * @param string|array $identifier Column or table name. + * + * @return array|string|string[] Escaped identifier. + */ + public static function escape_and_add_backtick( $identifier ) { + return '`' . str_replace( '`', '``', $identifier ) . '`'; + } + + /** + * Return $wpdb->prepare placeholder for data type. + * + * @param string $type Data type. + * + * @return string $wpdb placeholder. + */ + public static function get_wpdb_placeholder_for_type( $type ) { + return self::$wpdb_placeholder_for_type[ $type ]; + } + +} diff --git a/plugins/woocommerce/src/Internal/Admin/CategoryLookup.php b/plugins/woocommerce/src/Internal/Admin/CategoryLookup.php index 9bc8fbf153d..ee16d188c40 100644 --- a/plugins/woocommerce/src/Internal/Admin/CategoryLookup.php +++ b/plugins/woocommerce/src/Internal/Admin/CategoryLookup.php @@ -5,8 +5,6 @@ namespace Automattic\WooCommerce\Internal\Admin; -use Automattic\WooCommerce\Internal\Admin\Install; - defined( 'ABSPATH' ) || exit; /** @@ -64,8 +62,6 @@ class CategoryLookup { public function regenerate() { global $wpdb; - // Delete existing data and ensure schema is current. - Install::create_tables(); $wpdb->query( "TRUNCATE TABLE $wpdb->wc_category_lookup" ); $terms = get_terms( diff --git a/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php b/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php index 66d8195a41d..49dac42a47b 100644 --- a/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php +++ b/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php @@ -8,7 +8,6 @@ namespace Automattic\WooCommerce\Internal\Admin; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\API; -use Automattic\WooCommerce\Internal\Admin\Install; use \Automattic\WooCommerce\Admin\Notes\Notes; use \Automattic\WooCommerce\Internal\Admin\Notes\OrderMilestones; use \Automattic\WooCommerce\Internal\Admin\Notes\WooSubscriptionsNotes; @@ -78,8 +77,6 @@ class FeaturePlugin { require_once WC_ADMIN_ABSPATH . '/includes/react-admin/wc-admin-update-functions.php'; require_once WC_ADMIN_ABSPATH . '/includes/react-admin/class-experimental-abtest.php'; - register_activation_hook( WC_ADMIN_PLUGIN_FILE, array( $this, 'on_activation' ) ); - register_deactivation_hook( WC_ADMIN_PLUGIN_FILE, array( $this, 'on_deactivation' ) ); if ( did_action( 'plugins_loaded' ) ) { self::on_plugins_loaded(); } else { @@ -91,55 +88,12 @@ class FeaturePlugin { } } - /** - * Install DB and create cron events when activated. - * - * @return void - */ - public function on_activation() { - Install::create_tables(); - Install::create_events(); - } - - /** - * Remove WooCommerce Admin scheduled actions on deactivate. - * - * @return void - */ - public function on_deactivation() { - // Don't clean up if the WooCommerce Admin package is in core. - // NOTE: Any future divergence from the core package will need to be accounted for here. - if ( defined( 'WC_ADMIN_PACKAGE_EXISTS' ) && WC_ADMIN_PACKAGE_EXISTS ) { - return; - } - - // Check if we are deactivating due to dependencies not being satisfied. - // If WooCommerce is disabled we can't include files that depend upon it. - if ( ! $this->has_satisfied_dependencies() ) { - return; - } - - $this->includes(); - ReportsSync::clear_queued_actions(); - Notes::clear_queued_actions(); - wp_clear_scheduled_hook( 'wc_admin_daily' ); - wp_clear_scheduled_hook( 'generate_category_lookup_table' ); - } - /** * Setup plugin once all other plugins are loaded. * * @return void */ public function on_plugins_loaded() { - $this->load_plugin_textdomain(); - - if ( ! $this->has_satisfied_dependencies() ) { - add_action( 'admin_init', array( $this, 'deactivate_self' ) ); - add_action( 'admin_notices', array( $this, 'render_dependencies_notice' ) ); - return; - } - $this->hooks(); $this->includes(); } @@ -164,19 +118,11 @@ class FeaturePlugin { define( 'WC_ADMIN_VERSION_NUMBER', '3.3.0' ); } - /** - * Load Localisation files. - */ - protected function load_plugin_textdomain() { - load_plugin_textdomain( 'woocommerce-admin', false, basename( dirname( __DIR__ ) ) . '/languages' ); - } - /** * Include WC Admin classes. */ public function includes() { // Initialize Database updates, option migrations, and Notes. - Install::init(); Events::instance()->init(); Notes::init(); @@ -229,65 +175,6 @@ class FeaturePlugin { WCAdminAssets::get_instance(); } - /** - * Get an array of dependency error messages. - * - * @return array - */ - protected function get_dependency_errors() { - $errors = array(); - $wordpress_version = get_bloginfo( 'version' ); - $minimum_wordpress_version = '5.4'; - $minimum_woocommerce_version = '4.8'; - $wordpress_minimum_met = version_compare( $wordpress_version, $minimum_wordpress_version, '>=' ); - $woocommerce_minimum_met = class_exists( 'WooCommerce' ) && version_compare( WC_VERSION, $minimum_woocommerce_version, '>=' ); - - if ( ! $woocommerce_minimum_met ) { - $errors[] = sprintf( - /* translators: 1: URL of WooCommerce plugin, 2: The minimum WooCommerce version number */ - __( 'The WooCommerce Admin feature plugin requires WooCommerce %2$s or greater to be installed and active.', 'woocommerce' ), - 'https://wordpress.org/plugins/woocommerce/', - $minimum_woocommerce_version - ); - } - - if ( ! $wordpress_minimum_met ) { - $errors[] = sprintf( - /* translators: 1: URL of WordPress.org, 2: The minimum WordPress version number */ - __( 'The WooCommerce Admin feature plugin requires WordPress %2$s or greater to be installed and active.', 'woocommerce' ), - 'https://wordpress.org/', - $minimum_wordpress_version - ); - } - - return $errors; - } - - /** - * Returns true if all dependencies for the wc-admin plugin are loaded. - * - * @return bool - */ - public function has_satisfied_dependencies() { - $dependency_errors = $this->get_dependency_errors(); - return 0 === count( $dependency_errors ); - } - - /** - * Deactivates this plugin. - */ - public function deactivate_self() { - deactivate_plugins( plugin_basename( WC_ADMIN_PLUGIN_FILE ) ); - unset( $_GET['activate'] ); // phpcs:ignore CSRF ok. - } - - /** - * Notify users of the plugin requirements. - */ - public function render_dependencies_notice() { - $message = $this->get_dependency_errors(); - printf( '

%s

', implode( ' ', $message ) ); /* phpcs:ignore xss ok. */ - } /** * Overwrites the allowed features array using a local `feature-config.php` file. diff --git a/plugins/woocommerce/src/Internal/Admin/Install.php b/plugins/woocommerce/src/Internal/Admin/Install.php deleted file mode 100644 index dc401bc3461..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Install.php +++ /dev/null @@ -1,591 +0,0 @@ - array( - 'wc_admin_update_0201_order_status_index', - 'wc_admin_update_0201_db_version', - ), - '0.23.0' => array( - 'wc_admin_update_0230_rename_gross_total', - 'wc_admin_update_0230_db_version', - ), - '0.25.1' => array( - 'wc_admin_update_0251_remove_unsnooze_action', - 'wc_admin_update_0251_db_version', - ), - '1.1.0' => array( - 'wc_admin_update_110_remove_facebook_note', - 'wc_admin_update_110_db_version', - ), - '1.3.0' => array( - 'wc_admin_update_130_remove_dismiss_action_from_tracking_opt_in_note', - 'wc_admin_update_130_db_version', - ), - '1.4.0' => array( - 'wc_admin_update_140_change_deactivate_plugin_note_type', - 'wc_admin_update_140_db_version', - ), - '1.6.0' => array( - 'wc_admin_update_160_remove_facebook_note', - 'wc_admin_update_160_db_version', - ), - '1.7.0' => array( - 'wc_admin_update_170_homescreen_layout', - 'wc_admin_update_170_db_version', - ), - '2.7.0' => array( - 'wc_admin_update_270_delete_report_downloads', - 'wc_admin_update_270_db_version', - ), - '2.7.1' => array( - 'wc_admin_update_271_update_task_list_options', - 'wc_admin_update_271_db_version', - ), - '2.8.0' => array( - 'wc_admin_update_280_order_status', - 'wc_admin_update_280_db_version', - ), - '2.9.0' => array( - 'wc_admin_update_290_update_apperance_task_option', - 'wc_admin_update_290_delete_default_homepage_layout_option', - 'wc_admin_update_290_db_version', - ), - '3.0.0' => array( - 'wc_admin_update_300_update_is_read_from_last_read', - 'wc_admin_update_300_db_version', - ), - '3.4.0' => array( - 'wc_admin_update_340_remove_is_primary_from_note_action', - 'wc_admin_update_340_db_version', - ), - ); - - /** - * Migrated option names mapping. New => old. - * - * @var array - */ - protected static $migrated_options = array( - 'woocommerce_onboarding_profile' => 'wc_onboarding_profile', - 'woocommerce_admin_install_timestamp' => 'wc_admin_install_timestamp', - 'woocommerce_onboarding_opt_in' => 'wc_onboarding_opt_in', - 'woocommerce_admin_import_stats' => 'wc_admin_import_stats', - 'woocommerce_admin_version' => 'wc_admin_version', - 'woocommerce_admin_last_orders_milestone' => 'wc_admin_last_orders_milestone', - 'woocommerce_admin-wc-helper-last-refresh' => 'wc-admin-wc-helper-last-refresh', - 'woocommerce_admin_report_export_status' => 'wc_admin_report_export_status', - 'woocommerce_task_list_complete' => 'woocommerce_task_list_complete', - 'woocommerce_task_list_hidden' => 'woocommerce_task_list_hidden', - 'woocommerce_extended_task_list_complete' => 'woocommerce_extended_task_list_complete', - 'woocommerce_extended_task_list_hidden' => 'woocommerce_extended_task_list_hidden', - ); - - /** - * Hook in tabs. - */ - public static function init() { - if ( ( is_admin() && ! wp_doing_ajax() ) || wp_doing_cron() || defined( 'WP_CLI' ) ) { - add_action( 'init', array( __CLASS__, 'check_version' ), 5 ); - } - add_filter( 'wpmu_drop_tables', array( __CLASS__, 'wpmu_drop_tables' ) ); - - // Add wc-admin report tables to list of WooCommerce tables. - add_filter( 'woocommerce_install_get_tables', array( __CLASS__, 'add_tables' ) ); - } - - /** - * Migrate option values to their new keys/names. - */ - public static function migrate_options() { - wc_maybe_define_constant( 'WC_ADMIN_MIGRATING_OPTIONS', true ); - - foreach ( self::$migrated_options as $new_option => $old_option ) { - $old_option_value = get_option( $old_option, false ); - - // Continue if no option value was previously set. - if ( false === $old_option_value ) { - continue; - } - - if ( '1' === $old_option_value ) { - $old_option_value = 'yes'; - } elseif ( '0' === $old_option_value ) { - $old_option_value = 'no'; - } - - update_option( $new_option, $old_option_value ); - if ( $new_option !== $old_option ) { - delete_option( $old_option ); - } - } - } - - /** - * Check WC Admin version and run the updater is required. - * - * This check is done on all requests and runs if the versions do not match. - */ - public static function check_version() { - if ( defined( 'IFRAME_REQUEST' ) ) { - return; - } - - $version_option = get_option( self::VERSION_OPTION ); - $requires_update = version_compare( get_option( self::VERSION_OPTION ), WC_ADMIN_VERSION_NUMBER, '<' ); - - /* - * When included as part of Core, no `on_activation` hook as been called - * so there is no version in options. Make sure install gets called in this - * case as well as a regular version update - */ - if ( ! $version_option || $requires_update ) { - self::install(); - /** - * WooCommerce Admin has been installed or updated. - */ - do_action( 'woocommerce_admin_updated' ); - - if ( ! $version_option ) { - /** - * WooCommerce Admin has been installed. - */ - do_action( 'woocommerce_admin_newly_installed' ); - } - - if ( $requires_update ) { - /** - * An existing installation of WooCommerce Admin has been - * updated. - */ - do_action( 'woocommerce_admin_updated_existing' ); - } - } - - /* - * Add the version option if none is found, as would be the case when - * initialized via Core for the first time. - */ - if ( ! $version_option ) { - add_option( self::VERSION_OPTION, WC_ADMIN_VERSION_NUMBER ); - } - } - - /** - * Install WC Admin. - */ - public static function install() { - if ( ! is_blog_installed() ) { - return; - } - - // Check if we are not already running this routine. - if ( self::is_installing() ) { - return; - } - - // If we made it till here nothing is running yet, lets set the transient now. - set_transient( 'wc_admin_installing', 'yes', MINUTE_IN_SECONDS * 10 ); - - self::migrate_options(); - self::create_tables(); - self::create_events(); - self::delete_obsolete_notes(); - self::maybe_update_db_version(); - - delete_transient( 'wc_admin_installing' ); - - // Use add_option() here to avoid overwriting this value with each - // plugin version update. We base plugin age off of this value. - add_option( 'woocommerce_admin_install_timestamp', time() ); - do_action( 'woocommerce_admin_installed' ); - } - - /** - * Check if the installer is installing. - * - * @return bool - */ - public static function is_installing() { - return 'yes' === get_transient( 'wc_admin_installing' ); - } - - /** - * Get database schema. - * - * @return string - */ - protected static function get_schema() { - global $wpdb; - - $collate = $wpdb->has_cap( 'collation' ) ? $wpdb->get_charset_collate() : ''; - - // Max DB index length. See wp_get_db_schema(). - $max_index_length = 191; - - $tables = " - CREATE TABLE {$wpdb->prefix}wc_order_stats ( - order_id bigint(20) unsigned NOT NULL, - parent_id bigint(20) unsigned DEFAULT 0 NOT NULL, - date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, - date_created_gmt datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, - num_items_sold int(11) DEFAULT 0 NOT NULL, - total_sales double DEFAULT 0 NOT NULL, - tax_total double DEFAULT 0 NOT NULL, - shipping_total double DEFAULT 0 NOT NULL, - net_total double DEFAULT 0 NOT NULL, - returning_customer boolean DEFAULT NULL, - status varchar(200) NOT NULL, - customer_id BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (order_id), - KEY date_created (date_created), - KEY customer_id (customer_id), - KEY status (status({$max_index_length})) - ) $collate; - CREATE TABLE {$wpdb->prefix}wc_order_product_lookup ( - order_item_id BIGINT UNSIGNED NOT NULL, - order_id BIGINT UNSIGNED NOT NULL, - product_id BIGINT UNSIGNED NOT NULL, - variation_id BIGINT UNSIGNED NOT NULL, - customer_id BIGINT UNSIGNED NULL, - date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, - product_qty INT NOT NULL, - product_net_revenue double DEFAULT 0 NOT NULL, - product_gross_revenue double DEFAULT 0 NOT NULL, - coupon_amount double DEFAULT 0 NOT NULL, - tax_amount double DEFAULT 0 NOT NULL, - shipping_amount double DEFAULT 0 NOT NULL, - shipping_tax_amount double DEFAULT 0 NOT NULL, - PRIMARY KEY (order_item_id), - KEY order_id (order_id), - KEY product_id (product_id), - KEY customer_id (customer_id), - KEY date_created (date_created) - ) $collate; - CREATE TABLE {$wpdb->prefix}wc_order_tax_lookup ( - order_id BIGINT UNSIGNED NOT NULL, - tax_rate_id BIGINT UNSIGNED NOT NULL, - date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, - shipping_tax double DEFAULT 0 NOT NULL, - order_tax double DEFAULT 0 NOT NULL, - total_tax double DEFAULT 0 NOT NULL, - PRIMARY KEY (order_id, tax_rate_id), - KEY tax_rate_id (tax_rate_id), - KEY date_created (date_created) - ) $collate; - CREATE TABLE {$wpdb->prefix}wc_order_coupon_lookup ( - order_id BIGINT UNSIGNED NOT NULL, - coupon_id BIGINT NOT NULL, - date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, - discount_amount double DEFAULT 0 NOT NULL, - PRIMARY KEY (order_id, coupon_id), - KEY coupon_id (coupon_id), - KEY date_created (date_created) - ) $collate; - CREATE TABLE {$wpdb->prefix}wc_admin_notes ( - note_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - name varchar(255) NOT NULL, - type varchar(20) NOT NULL, - locale varchar(20) NOT NULL, - title longtext NOT NULL, - content longtext NOT NULL, - content_data longtext NULL default null, - status varchar(200) NOT NULL, - source varchar(200) NOT NULL, - date_created datetime NOT NULL default '0000-00-00 00:00:00', - date_reminder datetime NULL default null, - is_snoozable boolean DEFAULT 0 NOT NULL, - layout varchar(20) DEFAULT '' NOT NULL, - image varchar(200) NULL DEFAULT NULL, - is_deleted boolean DEFAULT 0 NOT NULL, - is_read boolean DEFAULT 0 NOT NULL, - icon varchar(200) NOT NULL default 'info', - PRIMARY KEY (note_id) - ) $collate; - CREATE TABLE {$wpdb->prefix}wc_admin_note_actions ( - action_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - note_id BIGINT UNSIGNED NOT NULL, - name varchar(255) NOT NULL, - label varchar(255) NOT NULL, - query longtext NOT NULL, - status varchar(255) NOT NULL, - actioned_text varchar(255) NOT NULL, - nonce_action varchar(255) NULL DEFAULT NULL, - nonce_name varchar(255) NULL DEFAULT NULL, - PRIMARY KEY (action_id), - KEY note_id (note_id) - ) $collate; - CREATE TABLE {$wpdb->prefix}wc_customer_lookup ( - customer_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - user_id BIGINT UNSIGNED DEFAULT NULL, - username varchar(60) DEFAULT '' NOT NULL, - first_name varchar(255) NOT NULL, - last_name varchar(255) NOT NULL, - email varchar(100) NULL default NULL, - date_last_active timestamp NULL default null, - date_registered timestamp NULL default null, - country char(2) DEFAULT '' NOT NULL, - postcode varchar(20) DEFAULT '' NOT NULL, - city varchar(100) DEFAULT '' NOT NULL, - state varchar(100) DEFAULT '' NOT NULL, - PRIMARY KEY (customer_id), - UNIQUE KEY user_id (user_id), - KEY email (email) - ) $collate; - CREATE TABLE {$wpdb->prefix}wc_category_lookup ( - category_tree_id BIGINT UNSIGNED NOT NULL, - category_id BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (category_tree_id,category_id) - ) $collate; - "; - - return $tables; - } - - /** - * Create database tables. - */ - public static function create_tables() { - require_once ABSPATH . 'wp-admin/includes/upgrade.php'; - - dbDelta( self::get_schema() ); - } - - /** - * Return a list of tables. Used to make sure all WC Admin tables are dropped - * when uninstalling the plugin in a single site or multi site environment. - * - * @return array WC tables. - */ - public static function get_tables() { - global $wpdb; - - return array( - "{$wpdb->prefix}wc_order_stats", - "{$wpdb->prefix}wc_order_product_lookup", - "{$wpdb->prefix}wc_order_tax_lookup", - "{$wpdb->prefix}wc_order_coupon_lookup", - "{$wpdb->prefix}wc_admin_notes", - "{$wpdb->prefix}wc_admin_note_actions", - "{$wpdb->prefix}wc_customer_lookup", - "{$wpdb->prefix}wc_category_lookup", - ); - } - - /** - * Adds new tables. - * - * @param array $wc_tables List of WooCommerce tables. - * @return array - */ - public static function add_tables( $wc_tables ) { - return array_merge( - $wc_tables, - self::get_tables() - ); - } - - /** - * Uninstall tables when MU blog is deleted. - * - * @param array $tables List of tables that will be deleted by WP. - * - * @return string[] - */ - public static function wpmu_drop_tables( $tables ) { - return array_merge( $tables, self::get_tables() ); - } - - /** - * Get list of DB update callbacks. - * - * @return array - */ - public static function get_db_update_callbacks() { - return self::$db_updates; - } - - /** - * Is a DB update needed? - * - * @return boolean - */ - public static function needs_db_update() { - $current_db_version = get_option( self::VERSION_OPTION, null ); - $updates = self::get_db_update_callbacks(); - $update_versions = array_keys( $updates ); - usort( $update_versions, 'version_compare' ); - - return ! is_null( $current_db_version ) && version_compare( $current_db_version, end( $update_versions ), '<' ); - } - - /** - * See if we need to show or run database updates during install. - */ - private static function maybe_update_db_version() { - if ( self::needs_db_update() ) { - self::update(); - } else { - self::update_db_version(); - } - } - - /** - * Push all needed DB updates to the queue for processing. - */ - private static function update() { - $current_db_version = get_option( self::VERSION_OPTION ); - $loop = 0; - - foreach ( self::get_db_update_callbacks() as $version => $update_callbacks ) { - if ( version_compare( $current_db_version, $version, '<' ) ) { - $completed_version_updates = 0; - foreach ( $update_callbacks as $update_callback ) { - $pending_jobs = WC()->queue()->search( - array( - 'per_page' => 1, - 'hook' => 'woocommerce_run_update_callback', - 'search' => wp_json_encode( array( $update_callback ) ), - 'group' => 'woocommerce-db-updates', - 'status' => 'pending', - ) - ); - - $complete_jobs = WC()->queue()->search( - array( - 'per_page' => 1, - 'hook' => 'woocommerce_run_update_callback', - 'search' => wp_json_encode( array( $update_callback ) ), - 'group' => 'woocommerce-db-updates', - 'status' => 'complete', - ) - ); - - $completed_version_updates += count( $complete_jobs ); - - if ( empty( $pending_jobs ) && empty( $complete_jobs ) ) { - WC()->queue()->schedule_single( - time() + $loop, - 'woocommerce_run_update_callback', - array( $update_callback ), - 'woocommerce-db-updates' - ); - Cache::invalidate(); - } - - $loop++; - - } - - // Users have experienced concurrency issues where all update callbacks - // have run but the version option hasn't been updated. If all the updates - // for a version are complete, update the version option to reflect that. - // See: https:// github.com/woocommerce/woocommerce-admin/issues/5058. - if ( count( $update_callbacks ) === $completed_version_updates ) { - self::update_db_version( $version ); - } - } - } - } - - /** - * Update WC Admin version to current. - * - * @param string|null $version New WooCommerce Admin DB version or null. - */ - public static function update_db_version( $version = null ) { - update_option( self::VERSION_OPTION, is_null( $version ) ? WC_ADMIN_VERSION_NUMBER : $version ); - } - - /** - * Schedule cron events. - */ - public static function create_events() { - if ( ! wp_next_scheduled( 'wc_admin_daily' ) ) { - wp_schedule_event( time(), 'daily', 'wc_admin_daily' ); - } - // Note: this is potentially redundant when the core package exists. - wp_schedule_single_event( time() + 10, 'generate_category_lookup_table' ); - } - - /** - * Delete obsolete notes. - */ - protected static function delete_obsolete_notes() { - $obsolete_notes_names = array( - 'wc-admin-welcome-note', - 'wc-admin-store-notice-setting-moved', - 'wc-admin-store-notice-giving-feedback', - 'wc-admin-learn-more-about-product-settings', - 'wc-admin-onboarding-profiler-reminder', - 'wc-admin-historical-data', - 'wc-admin-review-shipping-settings', - 'wc-admin-home-screen-feedback', - 'wc-admin-effortless-payments-by-mollie', - 'wc-admin-google-ads-and-marketing', - 'wc-admin-marketing-intro', - 'wc-admin-draw-attention', - 'wc-admin-need-some-inspiration', - 'wc-admin-choose-niche', - 'wc-admin-start-dropshipping-business', - 'wc-admin-filter-by-product-variations-in-reports', - 'wc-admin-learn-more-about-variable-products', - 'wc-admin-getting-started-ecommerce-webinar', - 'wc-admin-navigation-feedback', - 'wc-admin-navigation-feedback-follow-up', - ); - - $additional_obsolete_notes_names = apply_filters( - 'woocommerce_admin_obsolete_notes_names', - array() - ); - - if ( is_array( $additional_obsolete_notes_names ) ) { - $obsolete_notes_names = array_merge( - $obsolete_notes_names, - $additional_obsolete_notes_names - ); - } - - Notes::delete_notes_with_name( $obsolete_notes_names ); - } - - /** - * Drop WooCommerce Admin tables. - * - * @return void - */ - public static function drop_tables() { - global $wpdb; - - $tables = self::get_tables(); - - foreach ( $tables as $table ) { - /* phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared */ - $wpdb->query( "DROP TABLE IF EXISTS {$table}" ); - /* phpcs:enable */ - } - } -} diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/DeactivatePlugin.php b/plugins/woocommerce/src/Internal/Admin/Notes/DeactivatePlugin.php deleted file mode 100644 index cbd8663e70a..00000000000 --- a/plugins/woocommerce/src/Internal/Admin/Notes/DeactivatePlugin.php +++ /dev/null @@ -1,111 +0,0 @@ -set_title( __( 'Deactivate old WooCommerce Admin version', 'woocommerce' ) ); - $note->set_content( __( 'Your current version of WooCommerce Admin is outdated and a newer version is included with WooCommerce. We recommend deactivating the plugin and using the stable version included with WooCommerce.', 'woocommerce' ) ); - $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); - $note->set_name( self::NOTE_NAME ); - $note->set_content_data( (object) array() ); - $note->set_source( 'woocommerce-admin' ); - $note->add_action( - 'deactivate-feature-plugin', - __( 'Deactivate', 'woocommerce' ), - wc_admin_url( '&action=deactivate-feature-plugin' ), - Note::E_WC_ADMIN_NOTE_UNACTIONED - ); - $note->add_nonce_to_action( 'deactivate-feature-plugin', 'deactivate-plugin_' . WC_ADMIN_PLUGIN_FILE, '' ); - return $note; - } - - /** - * Delete the note if the version is higher than the included. - */ - public static function delete_note() { - Notes::delete_notes_with_name( self::NOTE_NAME ); - } - - /** - * Deactivate feature plugin. - */ - public function deactivate_feature_plugin() { - if ( - ! isset( $_GET['page'] ) || - 'wc-admin' !== $_GET['page'] || - ! isset( $_GET['action'] ) || - 'deactivate-feature-plugin' !== $_GET['action'] || - ! defined( 'WC_ADMIN_PLUGIN_FILE' ) - ) { - return; - } - - $note = self::get_note(); - $action = $note->get_action( 'deactivate-feature-plugin' ); - - // Preserve compatability with notes populated before nonce implementation. - if ( ! isset( $_GET['_wpnonce'] ) && ( ! $action || ! isset( $action->nonce_action ) ) ) { - self::deactivate_redirect( wp_create_nonce( 'deactivate-plugin_' . WC_ADMIN_PLUGIN_FILE ) ); - return; - } - - $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; - - if ( ! wp_verify_nonce( $nonce, 'deactivate-plugin_' . WC_ADMIN_PLUGIN_FILE ) ) { - return; - } - - self::deactivate_redirect( $nonce ); - } - - /** - * Deactivation redirect - * - * @param string $nonce The nonce. - */ - public static function deactivate_redirect( $nonce ) { - - $deactivate_url = admin_url( 'plugins.php?action=deactivate&plugin=' . rawurlencode( WC_ADMIN_PLUGIN_FILE ) . '&plugin_status=all&paged=1&_wpnonce=' . $nonce ); - wp_safe_redirect( $deactivate_url ); - - exit; - } -} diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/ManageStoreActivityFromHomeScreen.php b/plugins/woocommerce/src/Internal/Admin/Notes/ManageStoreActivityFromHomeScreen.php index 434190c781e..026f6bf5024 100644 --- a/plugins/woocommerce/src/Internal/Admin/Notes/ManageStoreActivityFromHomeScreen.php +++ b/plugins/woocommerce/src/Internal/Admin/Notes/ManageStoreActivityFromHomeScreen.php @@ -29,7 +29,7 @@ class ManageStoreActivityFromHomeScreen { */ public function __construct() { add_action( - 'woocommerce_admin_updated_existing', + 'woocommerce_updated', array( $this, 'possibly_add_note' ) ); } diff --git a/plugins/woocommerce/src/Internal/Admin/Notes/OrderMilestones.php b/plugins/woocommerce/src/Internal/Admin/Notes/OrderMilestones.php index f3423fd43a6..e6a9bd351d4 100644 --- a/plugins/woocommerce/src/Internal/Admin/Notes/OrderMilestones.php +++ b/plugins/woocommerce/src/Internal/Admin/Notes/OrderMilestones.php @@ -83,7 +83,7 @@ class OrderMilestones { $this->allowed_statuses = apply_filters( 'woocommerce_admin_order_milestone_statuses', $this->allowed_statuses ); add_action( 'woocommerce_after_register_post_type', array( $this, 'init' ) ); - register_deactivation_hook( WC_ADMIN_PLUGIN_FILE, array( $this, 'clear_scheduled_event' ) ); + register_deactivation_hook( WC_PLUGIN_FILE, array( $this, 'clear_scheduled_event' ) ); } /** diff --git a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php index 6ec1c56dccb..3cf47a240df 100644 --- a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php +++ b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php @@ -52,6 +52,7 @@ class DefaultFreeExtensions { 'title' => __( 'Grow your store', 'woocommerce' ), 'plugins' => [ self::get_plugin( 'google-listings-and-ads:alt' ), + self::get_plugin( 'pinterest-for-woocommerce' ), ], ], ]; @@ -76,7 +77,7 @@ class DefaultFreeExtensions { '', '' ), - 'image_url' => plugins_url( 'images/onboarding/google-listings-and-ads.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/google-listings-and-ads.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart', 'is_built_by_wc' => true, 'is_visible' => [ @@ -94,28 +95,35 @@ class DefaultFreeExtensions { 'google-listings-and-ads:alt' => [ 'name' => __( 'Google Listings & Ads', 'woocommerce' ), 'description' => __( 'Reach more shoppers and drive sales for your store. Integrate with Google to list your products for free and launch paid ad campaigns.', 'woocommerce' ), - 'image_url' => plugins_url( 'images/onboarding/google-listings-and-ads.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/google-listings-and-ads.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart', 'is_built_by_wc' => true, ], + 'pinterest-for-woocommerce' => [ + 'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ), + 'description' => __( 'Get your products in front of Pinterest users searching for ideas and things to buy. Get started with Pinterest and make your entire product catalog browsable.', 'woocommerce' ), + 'image_url' => plugins_url( '/assets/images/onboarding/pinterest.png', WC_PLUGIN_FILE ), + 'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding', + 'is_built_by_wc' => false, + ], 'mailpoet' => [ 'name' => __( 'MailPoet', 'woocommerce' ), 'description' => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ), - 'image_url' => plugins_url( 'images/onboarding/mailpoet.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=mailpoet-newsletters', 'is_built_by_wc' => true, ], 'mailchimp-for-woocommerce' => [ 'name' => __( 'Mailchimp', 'woocommerce' ), 'description' => __( 'Send targeted campaigns, recover abandoned carts and much more with Mailchimp.', 'woocommerce' ), - 'image_url' => plugins_url( 'images/onboarding/mailchimp-for-woocommerce.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/mailchimp-for-woocommerce.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=mailchimp-woocommerce', 'is_built_by_wc' => false, ], 'creative-mail-by-constant-contact' => [ 'name' => __( 'Creative Mail for WooCommerce', 'woocommerce' ), 'description' => __( 'Create on-brand store campaigns, fast email promotions and customer retargeting with Creative Mail.', 'woocommerce' ), - 'image_url' => plugins_url( 'images/onboarding/creative-mail-by-constant-contact.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/creative-mail-by-constant-contact.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=creativemail', 'is_built_by_wc' => false, ], @@ -449,7 +457,7 @@ class DefaultFreeExtensions { 'mailpoet:alt' => [ 'name' => __( 'MailPoet', 'woocommerce' ), 'description' => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ), - 'image_url' => plugins_url( 'images/onboarding/mailpoet.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=mailpoet-newsletters', 'is_built_by_wc' => true, ], diff --git a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/Init.php b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/Init.php index 01813f31d2e..12a28bdec7b 100644 --- a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/Init.php +++ b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/Init.php @@ -20,7 +20,7 @@ class Init { */ public function __construct() { add_action( 'change_locale', array( __CLASS__, 'delete_specs_transient' ) ); - add_action( 'woocommerce_admin_updated', array( __CLASS__, 'delete_specs_transient' ) ); + add_action( 'woocommerce_updated', array( __CLASS__, 'delete_specs_transient' ) ); } /** diff --git a/plugins/woocommerce/src/Internal/Admin/Translations.php b/plugins/woocommerce/src/Internal/Admin/Translations.php index 9fde7e28ed4..0542cdc7dee 100644 --- a/plugins/woocommerce/src/Internal/Admin/Translations.php +++ b/plugins/woocommerce/src/Internal/Admin/Translations.php @@ -43,8 +43,40 @@ class Translations { // Handler for WooCommerce and WooCommerce Admin plugin activation. add_action( 'woocommerce_activated_plugin', array( $this, 'potentially_generate_translation_strings' ) ); add_action( 'activated_plugin', array( $this, 'potentially_generate_translation_strings' ) ); + + // Adding this filter to adjust the path after woocommerce-admin was merged into woocommerce core. + // Remove after the translations strings have been updated to the new path (probably woocommerce 6.6). + add_filter( 'load_script_textdomain_relative_path', array( $this, 'adjust_script_path' ), 10, 2 ); } + /** + * This filter is temporarily used to produce the correct i18n paths as they were moved in WC 6.5. + * + * @param string $relative Relative path to the script. + * @param string $src The script's source URL. + */ + public function adjust_script_path( $relative, $src ) { + // only rewrite the path if the translation file is from the old woocommerce-admin dist folder. + if ( false === strpos( $relative, 'assets/client/admin' ) ) { + return $relative; + } + + // translation filenames are always based on the unminified path. + if ( substr( $relative, -7 ) === '.min.js' ) { + $relative = substr( $relative, 0, -7 ) . '.js'; + } + + $file_base = 'woocommerce-' . determine_locale(); // e.g woocommerce-fr_FR. + $md5_filename = $file_base . '-' . md5( $relative ) . '.json'; + + $languages_path = WP_LANG_DIR . '/plugins/'; // get path to translations folder. + + if ( ! is_readable( $languages_path . $md5_filename ) ) { + return str_replace( 'assets/client/admin', 'packages/woocommerce-admin/dist', $relative ); + } else { + return $relative; + } + } /** * Generate a filename to cache translations from JS chunks. * @@ -101,8 +133,13 @@ class Translations { // Only combine "app" files (not scripts registered with WP). if ( + // paths for woocommerce < 6.5. can be removed from 6.6 onwards or when i18n json file names are updated. false === strpos( $reference_file, 'dist/chunks/' ) && - false === strpos( $reference_file, 'dist/app/index.js' ) + false === strpos( $reference_file, 'dist/app/index.js' ) && + + // paths for woocommerce >= 6.5 (post-merge of woocommerce-admin). + false === strpos( $reference_file, 'assets/admin/app/index.js' ) && + false === strpos( $reference_file, 'assets/admin/chunks/' ) ) { continue; } diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php index 4dc51ace4d0..acb13482d9d 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php @@ -92,6 +92,38 @@ class CustomOrdersTableController { 999, 2 ); + + add_filter( + 'updated_option', + function( $option, $old_value, $value ) { + $this->process_updated_option( $option, $old_value, $value ); + }, + 999, + 3 + ); + + add_filter( + 'pre_update_option', + function( $value, $option, $old_value ) { + return $this->process_pre_update_option( $option, $old_value, $value ); + }, + 999, + 3 + ); + + add_filter( + DataSynchronizer::PENDING_SYNCHRONIZATION_FINISHED_ACTION, + function() { + $this->process_sync_finished(); + } + ); + + add_action( + 'woocommerce_update_options_advanced_custom_data_stores', + function() { + $this->process_options_updated(); + } + ); } /** @@ -213,6 +245,7 @@ class CustomOrdersTableController { } $this->data_synchronizer->create_database_tables(); + update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' ); } /** @@ -258,40 +291,182 @@ class CustomOrdersTableController { return $settings; } - $title_item = array( - 'title' => __( 'Custom orders tables', 'woocommerce' ), - 'type' => 'title', - 'desc' => sprintf( - /* translators: %1$s = tag, %2$s = tag. */ - __( '%1$sWARNING:%2$s This feature is currently under development and may cause database instability. For contributors only.', 'woocommerce' ), - '', - '' - ), - ); - if ( $this->data_synchronizer->check_orders_table_exists() ) { - $settings[] = $title_item; + $settings[] = array( + 'title' => __( 'Custom orders tables', 'woocommerce' ), + 'type' => 'title', + 'id' => 'cot-title', + 'desc' => sprintf( + /* translators: %1$s = tag, %2$s = tag. */ + __( '%1$sWARNING:%2$s This feature is currently under development and may cause database instability. For contributors only.', 'woocommerce' ), + '', + '' + ), + ); + + $sync_status = $this->data_synchronizer->get_sync_status(); + $sync_is_pending = 0 !== $sync_status['current_pending_count']; $settings[] = array( - 'title' => __( 'Enable tables usage', 'woocommerce' ), - 'desc' => __( 'Use the custom orders tables as the main orders data store.', 'woocommerce' ), + 'title' => __( 'Data store for orders', 'woocommerce' ), 'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'default' => 'no', - 'type' => 'checkbox', + 'type' => 'radio', + 'options' => array( + 'yes' => __( 'Use the WooCommerce orders tables', 'woocommerce' ), + 'no' => __( 'Use the WordPress posts table', 'woocommerce' ), + ), 'checkboxgroup' => 'start', + 'disabled' => $sync_is_pending ? array( 'yes', 'no' ) : array(), ); + + if ( $sync_is_pending ) { + $initial_pending_count = $sync_status['initial_pending_count']; + $current_pending_count = $sync_status['current_pending_count']; + if ( $initial_pending_count ) { + $text = + sprintf( + /* translators: %1$s=current number of orders pending sync, %2$s=initial number of orders pending sync */ + _n( 'There\'s %1$s order (out of a total of %2$s) pending sync!', 'There are %1$s orders (out of a total of %2$s) pending sync!', $current_pending_count, 'woocommerce' ), + $current_pending_count, + $initial_pending_count + ); + } else { + $text = + /* translators: %s=initial number of orders pending sync */ + sprintf( _n( 'There\'s %s order pending sync!', 'There are %s orders pending sync!', $current_pending_count, 'woocommerce' ), $current_pending_count, 'woocommerce' ); + } + + if ( $this->data_synchronizer->pending_data_sync_is_in_progress() ) { + $text .= __( "
Synchronization for these orders is currently in progress.
The authoritative table can't be changed until sync completes.", 'woocommerce' ); + } else { + $text .= __( "
The authoritative table can't be changed until these orders are synchronized.", 'woocommerce' ); + } + + $settings[] = array( + 'type' => 'info', + 'id' => 'cot-out-of-sync-warning', + 'css' => 'color: #C00000', + 'text' => $text, + ); + } + + $settings[] = array( + 'desc' => __( 'Keep the posts table and the orders tables synchronized', 'woocommerce' ), + 'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, + 'type' => 'checkbox', + ); + + if ( $sync_is_pending ) { + if ( $this->data_synchronizer->data_sync_is_enabled() ) { + $message = $this->custom_orders_table_usage_is_enabled() ? + __( 'Switch to using the posts table as the authoritative data store for orders when sync finishes', 'woocommerce' ) : + __( 'Switch to using the orders table as the authoritative data store for orders when sync finishes', 'woocommerce' ); + $settings[] = array( + 'desc' => $message, + 'id' => DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, + 'type' => 'checkbox', + ); + } + } } else { - $title_item['desc'] = sprintf( - /* translators: %1$s = tag, %2$s = tag. */ - __( 'Create the tables first by going to %1$sWooCommerce > Status > Tools%2$s and running %1$sCreate the custom orders tables%2$s.', 'woocommerce' ), - '', - '' + $settings[] = array( + 'title' => __( 'Custom orders tables', 'woocommerce' ), + 'type' => 'title', + 'desc' => sprintf( + /* translators: %1$s = tag, %2$s = tag. */ + __( 'Create the tables first by going to %1$sWooCommerce > Status > Tools%2$s and running %1$sCreate the custom orders tables%2$s.', 'woocommerce' ), + '', + '' + ), ); - $settings[] = $title_item; } $settings[] = array( 'type' => 'sectionend' ); return $settings; } + + /** + * Handler for the individual setting updated hook. + * + * @param string $option Setting name. + * @param mixed $old_value Old value of the setting. + * @param mixed $value New value of the setting. + */ + private function process_updated_option( $option, $old_value, $value ) { + if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION === $option && 'no' === $value ) { + $this->data_synchronizer->cleanup_synchronization_state(); + } + } + + /** + * Handler for the setting pre-update hook. + * We use it to verify that authoritative orders table switch doesn't happen while sync is pending. + * + * @param string $option Setting name. + * @param mixed $old_value Old value of the setting. + * @param mixed $value New value of the setting. + * + * @throws \Exception Attempt to change the authoritative orders table while orders sync is pending. + */ + private function process_pre_update_option( $option, $old_value, $value ) { + if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option || $value === $old_value || false === $old_value ) { + return $value; + } + + $sync_is_pending = 0 !== $this->data_synchronizer->get_current_orders_pending_sync_count(); + if ( $sync_is_pending ) { + throw new \Exception( "The authoritative table for orders storage can't be changed while there are orders out of sync" ); + } + + return $value; + } + + /** + * Handler for the synchronization finished hook. + * Here we switch the authoritative table if needed. + */ + private function process_sync_finished() { + if ( $this->auto_flip_authoritative_table_enabled() ) { + return; + } + + update_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' ); + + if ( $this->custom_orders_table_usage_is_enabled() ) { + update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' ); + } else { + update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'yes' ); + } + } + + /** + * Is the automatic authoritative table switch setting set? + * + * @return bool + */ + private function auto_flip_authoritative_table_enabled(): bool { + return 'yes' === get_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION ); + } + + /** + * Handler for the all settings updated hook. + */ + private function process_options_updated() { + $data_sync_is_enabled = $this->data_synchronizer->data_sync_is_enabled(); + + // Disabling the sync implies disabling the automatic authoritative table switch too. + if ( ! $data_sync_is_enabled && $this->auto_flip_authoritative_table_enabled() ) { + update_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' ); + } + + // Enabling the sync implies starting it too, if needed. + // We do this check here, and not in process_pre_update_option, so that if for some reason + // the setting is enabled but no sync is in process, sync will start by just saving the + // settings even without modifying them. + if ( $data_sync_is_enabled && ! $this->data_synchronizer->pending_data_sync_is_in_progress() ) { + $this->data_synchronizer->start_synchronizing_pending_orders(); + } + } } diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php index 19894cfcb18..09bc4bf589c 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -17,6 +17,21 @@ defined( 'ABSPATH' ) || exit; */ class DataSynchronizer { + const ORDERS_DATA_SYNC_ENABLED_OPTION = 'woocommerce_custom_orders_table_data_sync_enabled'; + const INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION = 'woocommerce_initial_orders_pending_sync_count'; + const AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION = 'woocommerce_auto_flip_authoritative_table_roles'; + const PENDING_SYNC_IS_IN_PROGRESS_OPTION = 'woocommerce_custom_orders_table_pending_sync_in_progress'; + const ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK = 'woocommerce_run_orders_sync_callback'; + const PENDING_SYNCHRONIZATION_FINISHED_ACTION = 'woocommerce_orders_sync_finished'; + + // Allowed values for $type in get_ids_of_orders_pending_sync method. + const ID_TYPE_MISSING_IN_ORDERS_TABLE = 0; + const ID_TYPE_MISSING_IN_POSTS_TABLE = 1; + const ID_TYPE_DIFFERENT_UPDATE_DATE = 2; + + // TODO: Remove the usage of the fake pending orders count once development of the feature is complete. + const FAKE_ORDERS_PENDING_SYNC_COUNT_OPTION = 'woocommerce_fake_orders_pending_sync_count'; + /** * The data store object to use. * @@ -31,7 +46,17 @@ class DataSynchronizer { */ private $database_util; - // TODO: Add a constructor to handle hooks as appropriate. + /** + * Class constructor. + */ + public function __construct() { + add_action( + self::ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK, + function() { + $this->do_pending_orders_synchronization(); + } + ); + } /** * Class initialization, invoked by the DI container. @@ -74,5 +99,204 @@ class DataSynchronizer { } } + /** + * Is the data sync between old and new tables currently enabled? + * + * @return bool + */ + public function data_sync_is_enabled(): bool { + return 'yes' === get_option( self::ORDERS_DATA_SYNC_ENABLED_OPTION ); + } + /** + * Is a sync process currently in progress? + * + * @return bool + */ + public function pending_data_sync_is_in_progress(): bool { + return 'yes' === get_option( self::PENDING_SYNC_IS_IN_PROGRESS_OPTION ); + } + + /** + * Get the current sync process status. + * The information is meaningful only if pending_data_sync_is_in_progress return true. + * + * @return array + */ + public function get_sync_status() { + return array( + 'initial_pending_count' => (int) get_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION, 0 ), + 'current_pending_count' => $this->get_current_orders_pending_sync_count(), + 'auto_flip' => 'yes' === get_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION ), + 'sync_in_progress' => $this->pending_data_sync_is_in_progress(), + ); + } + + /** + * Calculate how many orders need to be synchronized currently. + * + * If an option whose name is given by self::FAKE_ORDERS_PENDING_SYNC_COUNT_OPTION exists, + * then the value of that option is returned. This is temporary, to ease testing the feature + * while it is in development. + * + * Otherwise a database query is performed to get how many orders match one of the following: + * + * - Existing in the authoritative table but not in the backup table. + * - Existing in both tables, but they have a different update date. + */ + public function get_current_orders_pending_sync_count(): int { + global $wpdb; + + // TODO: Remove the usage of the fake pending orders count once development of the feature is complete. + $count = get_option( self::FAKE_ORDERS_PENDING_SYNC_COUNT_OPTION ); + if ( false !== $count ) { + return (int) $count; + } + + $orders_table = $wpdb->prefix . 'wc_orders'; + + if ( 'yes' === get_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) ) { + $missing_orders_count_sql = "SELECT COUNT(1) FROM $orders_table WHERE post_id IS NULL"; + } else { + $missing_orders_count_sql = " +SELECT COUNT( posts.ID ) FROM $wpdb->posts posts +LEFT JOIN $orders_table orders ON posts.ID = orders.post_id +WHERE posts.post_type='shop_order' AND orders.post_id IS NULL +AND posts.post_status != 'auto-draft'"; + } + + $sql = " +SELECT( + ($missing_orders_count_sql) + + + (SELECT COUNT(1) FROM ( + SELECT orders.post_id FROM $orders_table orders + JOIN $wpdb->posts posts on posts.ID = orders.post_id + WHERE orders.date_updated_gmt != posts.post_modified_gmt + ) x) +) count"; + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + return (int) $wpdb->get_var( $wpdb->prepare( $sql ) ); + } + + /** + * Get a list of ids of orders than are out of sync. + * + * Valid values for $type are: + * + * ID_TYPE_MISSING_IN_ORDERS_TABLE: orders that exist in posts table but not in orders table. Returns post ids. + * ID_TYPE_MISSING_IN_POSTS_TABLE: orders that exist in orders table but not in posts table. Returns ids from orders table. + * ID_TYPE_DIFFERENT_UPDATE_DATE: orders that exist in both tables but have different last update dates. Returns ids from orders table. + * + * @param int $type One of ID_TYPE_MISSING_IN_ORDERS_TABLE, ID_TYPE_MISSING_IN_POSTS_TABLE, ID_TYPE_DIFFERENT_UPDATE_DATE. + * @param int $limit Maximum number of ids to return. + * @return array An array of order or post ids. + * @throws \Exception Invalid parameter. + */ + private function get_ids_of_orders_pending_sync( int $type, int $limit ) { + global $wpdb; + + if ( $limit < 1 ) { + throw new \Exception( '$limit must be at least 1' ); + } + + $orders_table = $wpdb->prefix . 'wc_orders'; + + switch ( $type ) { + case self::ID_TYPE_MISSING_IN_ORDERS_TABLE: + $sql = " +SELECT posts.ID FROM $wpdb->posts posts +LEFT JOIN $orders_table orders ON posts.ID = orders.post_id +WHERE posts.post_type='shop_order' AND orders.post_id IS NULL +AND posts.post_status != 'auto-draft'"; + break; + case self::ID_TYPE_MISSING_IN_POSTS_TABLE: + $sql = "SELECT id FROM $orders_table WHERE post_id IS NULL"; + break; + case self::ID_TYPE_DIFFERENT_UPDATE_DATE: + $sql = " +SELECT orders.id FROM $orders_table orders +JOIN $wpdb->posts posts on posts.ID = orders.post_id +WHERE orders.date_updated_gmt != posts.post_modified_gmt"; + break; + default: + throw new \Exception( 'Invalid $type, must be one of the ID_TYPE_... constants.' ); + } + + // phpcs:ignore WordPress.DB + return array_map( 'intval', $wpdb->get_col( $sql . " LIMIT $limit" ) ); + } + + /** + * Start an orders synchronization process. + * This will setup the appropriate status information and schedule the first synchronization batch. + */ + public function start_synchronizing_pending_orders() { + $initial_pending_count = $this->get_current_orders_pending_sync_count(); + if ( 0 === $initial_pending_count ) { + return; + } + + update_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION, $initial_pending_count ); + + $queue = WC()->get_instance_of( \WC_Queue::class ); + $queue->cancel_all( self::ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK ); + + update_option( self::PENDING_SYNC_IS_IN_PROGRESS_OPTION, 'yes' ); + $this->schedule_pending_orders_synchronization(); + } + + /** + * Schedule the next orders synchronization batch. + */ + private function schedule_pending_orders_synchronization() { + $queue = WC()->get_instance_of( \WC_Queue::class ); + $queue->schedule_single( + WC()->call_function( 'time' ) + 1, + self::ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK, + array(), + 'woocommerce-db-updates' + ); + } + + /** + * Run one orders synchronization batch. + */ + private function do_pending_orders_synchronization() { + if ( ! $this->pending_data_sync_is_in_progress() ) { + return; + } + + // TODO: Remove the usage of the fake pending orders count once development of the feature is complete. + $fake_count = get_option( self::FAKE_ORDERS_PENDING_SYNC_COUNT_OPTION ); + if ( false !== $fake_count ) { + update_option( 'woocommerce_fake_orders_pending_sync_count', (int) $fake_count - 1 ); + // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElse + } else { + // TODO: Use get_ids_of_orders_pending_sync to get a batch of order ids and syncrhonize them. + } + + if ( 0 === $this->get_current_orders_pending_sync_count() ) { + $this->cleanup_synchronization_state(); + + /** + * Hook to signal that the orders tables synchronization process has finished (nothing left to synchronize). + */ + do_action( self::PENDING_SYNCHRONIZATION_FINISHED_ACTION ); + } else { + $this->schedule_pending_orders_synchronization(); + } + } + + /** + * Cleanup all the synchronization status information, + * because the process has been disabled by the user via settings, + * or because there's nothing left to syncrhonize. + */ + public function cleanup_synchronization_state() { + delete_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION ); + delete_option( self::PENDING_SYNC_IS_IN_PROGRESS_OPTION ); + delete_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION ); + } } diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index db22608e63b..ed6b2b4a957 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -17,7 +17,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * * @return string The custom orders table name. */ - public function get_orders_table_name() { + public static function get_orders_table_name() { global $wpdb; return $wpdb->prefix . 'wc_orders'; } @@ -27,7 +27,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * * @return string The order addresses table name. */ - public function get_addresses_table_name() { + public static function get_addresses_table_name() { global $wpdb; return $wpdb->prefix . 'wc_order_addresses'; } @@ -37,11 +37,21 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * * @return string The orders operational data table name. */ - public function get_operational_data_table_name() { + public static function get_operational_data_table_name() { global $wpdb; return $wpdb->prefix . 'wc_order_operational_data'; } + /** + * Get the orders meta data table name. + * + * @return string Name of order meta data table. + */ + public static function get_meta_table_name() { + global $wpdb; + return $wpdb->prefix . 'wc_orders_meta'; + } + /** * Get the names of all the tables involved in the custom orders table feature. * @@ -52,6 +62,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements $this->get_orders_table_name(), $this->get_addresses_table_name(), $this->get_operational_data_table_name(), + $this->get_meta_table_name(), ); } @@ -185,6 +196,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements $orders_table_name = $this->get_orders_table_name(); $addresses_table_name = $this->get_addresses_table_name(); $operational_data_table_name = $this->get_operational_data_table_name(); + $meta_table = $this->get_meta_table_name(); $sql = " CREATE TABLE $orders_table_name ( @@ -234,7 +246,7 @@ CREATE TABLE $operational_data_table_name ( woocommerce_version varchar(20) NULL, prices_include_tax tinyint(1) NULL, coupon_usages_are_counted tinyint(1) NULL, - download_permissionis_granted tinyint(1) NULL, + download_permission_granted tinyint(1) NULL, cart_hash varchar(100) NULL, new_order_email_sent tinyint(1) NULL, order_key varchar(100) NULL, @@ -242,12 +254,21 @@ CREATE TABLE $operational_data_table_name ( date_paid_gmt datetime NULL, date_completed_gmt datetime NULL, shipping_tax_amount decimal(26, 8) NULL, - shopping_total_amount decimal(26, 8) NULL, + shipping_total_amount decimal(26, 8) NULL, discount_tax_amount decimal(26, 8) NULL, discount_total_amount decimal(26, 8) NULL, KEY order_id (order_id), KEY order_key (order_key) -);"; +); +CREATE TABLE $meta_table ( + id bigint(20) unsigned auto_increment primary key, + order_id bigint(20) unsigned null, + meta_key varchar(255), + meta_value text null, + KEY meta_key_value (meta_key, meta_value(100)) +); +"; + return $sql; } } diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php new file mode 100644 index 00000000000..2312b589eb6 --- /dev/null +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php @@ -0,0 +1,37 @@ +leagueContainer property or the `getLeagueContainer` method + * from the ContainerAwareTrait. + * + * @return void + */ + public function register() { + $this->share( WPPostToCOTMigrator::class ); + } +} diff --git a/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Synchronize.php b/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Synchronize.php index 0db1da8bd5e..d44c0404991 100644 --- a/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Synchronize.php +++ b/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Synchronize.php @@ -219,7 +219,18 @@ class Synchronize { $parent_url = _x( 'invalid URL', 'Approved product download URLs migration', 'woocommerce' ); try { - $parent_url = ( new URL( $downloadable->get_file() ) )->get_parent_url(); + $download_file = $downloadable->get_file(); + + /** + * Controls whether shortcodes should be resolved and validated using the Approved Download Directory feature. + * + * @param bool $should_validate + */ + if ( apply_filters( 'woocommerce_product_downloads_approved_directory_validation_for_shortcodes', true ) && 'shortcode' === $downloadable->get_type_of_file_path() ) { + $download_file = do_shortcode( $download_file ); + } + + $parent_url = ( new URL( $download_file ) )->get_parent_url(); $this->register->add_approved_directory( $parent_url, false ); } catch ( Exception $e ) { wc_get_logger()->log( diff --git a/plugins/woocommerce/src/Internal/Utilities/URL.php b/plugins/woocommerce/src/Internal/Utilities/URL.php index bf35943617e..60963b09ffa 100644 --- a/plugins/woocommerce/src/Internal/Utilities/URL.php +++ b/plugins/woocommerce/src/Internal/Utilities/URL.php @@ -34,6 +34,17 @@ class URL { */ private $is_absolute; + /** + * If the URL (or filepath) represents a directory other than the root directory. + * + * This is useful at different points in the process, when deciding whether to re-apply + * a trailing slash at the end of processing or when we need to calculate how many + * directory traversals are needed to form a (grand-)parent URL. + * + * @var bool + */ + private $is_non_root_directory; + /** * The components of the URL's path. * @@ -86,8 +97,12 @@ class URL { $this->components['drive'] = $matches[2]; } - // If there is no scheme, assume and prepend "file://". - if ( ! preg_match( '#^[a-z]+://#i', $this->url ) ) { + /* + * If there is no scheme, assume and prepend "file://". An exception is made for cases where the URL simply + * starts with exactly two forward slashes, which indicates 'any scheme' (most commonly, that is used when + * there is freedom to switch between 'http' and 'https'). + */ + if ( ! preg_match( '#^[a-z]+://#i', $this->url ) && ! preg_match( '#^//(?!/)#', $this->url ) ) { $this->url = 'file://' . $this->url; } @@ -104,19 +119,19 @@ class URL { ); } + $this->components = array_merge( $this->components, $parsed_components ); + // File URLs cannot have a host. However, the initial path segment *or* the Windows drive letter // (if present) may be incorrectly be interpreted as the host name. - if ( 'file' === $parsed_components['scheme'] && ! empty( $parsed_components['host'] ) ) { + if ( 'file' === $this->components['scheme'] && ! empty( $this->components['host'] ) ) { // If we do not have a drive letter, then simply merge the host and the path together. if ( null === $this->components['drive'] ) { - $parsed_components['path'] = $parsed_components['host'] . ( $parsed_components['path'] ?? '' ); + $this->components['path'] = $this->components['host'] . ( $this->components['path'] ?? '' ); } - // Always unset the host in this situation. - unset( $parsed_components['host'] ); + // Restore the host to null in this situation. + $this->components['host'] = null; } - - $this->components = array_merge( $this->components, $parsed_components ); } /** @@ -124,32 +139,65 @@ class URL { * without touching the filesystem. */ private function process_path() { - $segments = explode( '/', $this->components['path'] ); - $this->is_absolute = substr( $this->components['path'], 0, 1 ) === '/'; + $segments = explode( '/', $this->components['path'] ); + $this->is_absolute = substr( $this->components['path'], 0, 1 ) === '/' || ! empty( $this->components['host'] ); + $this->is_non_root_directory = substr( $this->components['path'], -1, 1 ) === '/' && strlen( $this->components['path'] ) > 1; + $resolve_traversals = 'file' !== $this->components['scheme'] || $this->is_absolute; + $retain_traversals = false; // Clean the path. foreach ( $segments as $part ) { // Drop empty segments. - if ( strlen( $part ) === 0 ) { + if ( strlen( $part ) === 0 || '.' === $part ) { continue; } // Directory traversals created with percent-encoding syntax should also be detected. $is_traversal = str_ireplace( '%2e', '.', $part ) === '..'; - // Unwind directory traversals. - if ( $is_traversal && count( $this->path_parts ) > 0 ) { - $this->path_parts = array_slice( $this->path_parts, 0, count( $this->path_parts ) - 1 ); - continue; + // Resolve directory traversals (if allowed: see further comment relating to this). + if ( $resolve_traversals && $is_traversal ) { + if ( count( $this->path_parts ) > 0 && ! $retain_traversals ) { + $this->path_parts = array_slice( $this->path_parts, 0, count( $this->path_parts ) - 1 ); + continue; + } elseif ( $this->is_absolute ) { + continue; + } } + /* + * Consider allowing directory traversals to be resolved (ie, the process that converts 'foo/bar/../baz' to + * 'foo/baz'). + * + * 1. For this decision point, we are only concerned with relative filepaths (in all other cases, + * $resolve_traversals will already be true). + * 2. This is a 'one time' and unidirectional operation. We only wish to flip from false to true, and we + * never wish to do this more than once. + * 3. We only flip the switch after we have examined all leading '..' traversal segments. + */ + if ( false === $resolve_traversals && '..' !== $part && 'file' === $this->components['scheme'] && ! $this->is_absolute ) { + $resolve_traversals = true; + } + + /* + * Set a flag indicating that traversals should be retained. This is done to ensure we don't prematurely + * discard traversals at the start of the path. + */ + $retain_traversals = $resolve_traversals && '..' === $part; + // Retain this part of the path. $this->path_parts[] = $part; } + // Protect against empty relative paths. + if ( count( $this->path_parts ) === 0 && ! $this->is_absolute ) { + $this->path_parts = array( '.' ); + $this->is_non_root_directory = true; + } + // Reform the path from the processed segments, appending a leading slash if it is absolute and restoring // the Windows drive letter if we have one. - $this->components['path'] = ( $this->is_absolute ? '/' : '' ) . implode( '/', $this->path_parts ); + $this->components['path'] = ( $this->is_absolute ? '/' : '' ) . implode( '/', $this->path_parts ) . ( $this->is_non_root_directory ? '/' : '' ); } /** @@ -170,6 +218,15 @@ class URL { $max_parent = count( $this->path_parts ); $parents = array(); + /* + * If we are looking at a relative path that begins with at least one traversal (example: "../../foo") + * then we should only return one parent URL (otherwise, we'd potentially have to return an infinite + * number of parent URLs since we can't know how far the tree extends). + */ + if ( $max_parent > 0 && ! $this->is_absolute && '..' === $this->path_parts[0] ) { + $max_parent = 1; + } + for ( $level = 1; $level <= $max_parent; $level++ ) { $parents[] = $this->get_parent_url( $level ); } @@ -187,23 +244,77 @@ class URL { * this is set to 1 (parent). 2 will yield the grand-parent, 3 will yield the great * grand-parent, etc. * + * If a level is specified that exceeds the number of path segments, this method will + * return false. + * * @param int $level Used to indicate the level of parent. * - * @return string + * @return string|false */ - public function get_parent_url( int $level = 1 ): string { + public function get_parent_url( int $level = 1 ) { if ( $level < 1 ) { $level = 1; } - $parent_path = implode( '/', array_slice( $this->path_parts, 0, count( $this->path_parts ) - $level ) ) . '/'; + $parts_count = count( $this->path_parts ); + $parent_path_parts_to_keep = $parts_count - $level; + + /* + * With the exception of file URLs, we do not allow obtaining (grand-)parent directories that require + * us to describe them using directory traversals. For example, given "http://hostname/foo/bar/baz.png" we do + * not permit determining anything more than 2 levels up (we cannot go beyond "http://hostname/"). + */ + if ( 'file' !== $this->components['scheme'] && $parent_path_parts_to_keep < 0 ) { + return false; + } + + // In the specific case of an absolute filepath describing the root directory, there can be no parent. + if ( 'file' === $this->components['scheme'] && $this->is_absolute && empty( $this->path_parts ) ) { + return false; + } + + // Handle cases where the path starts with one or more 'dot segments'. Since the path has already been + // processed, we can be confident that any such segments are at the start of the path. + if ( $parts_count > 0 && ( '.' === $this->path_parts[0] || '..' === $this->path_parts[0] ) ) { + // Determine the index of the last dot segment (ex: given the path '/../../foo' it would be 1). + $single_dots = array_keys( $this->path_parts, '.', true ); + $double_dots = array_keys( $this->path_parts, '..', true ); + $max_dot_index = max( array_merge( $single_dots, $double_dots ) ); + + // Prepend the required number of traversals and discard unnessary trailing segments. + $last_traversal = $max_dot_index + ( $this->is_non_root_directory ? 1 : 0 ); + $parent_path = str_repeat( '../', $level ) . join( '/', array_slice( $this->path_parts, 0, $last_traversal ) ); + } elseif ( $parent_path_parts_to_keep < 0 ) { + // For relative filepaths only, we use traversals to describe the requested parent. + $parent_path = untrailingslashit( str_repeat( '../', $parent_path_parts_to_keep * -1 ) ); + } else { + // Otherwise, in a very simple case, we just remove existing parts. + $parent_path = implode( '/', array_slice( $this->path_parts, 0, $parent_path_parts_to_keep ) ); + } + + if ( $this->is_relative() && '' === $parent_path ) { + $parent_path = '.'; + } + + // Append a trailing slash, since a parent is always a directory. The only exception is the current working directory. + $parent_path .= '/'; // For absolute paths, apply a leading slash (does not apply if we have a root path). if ( $this->is_absolute && 0 !== strpos( $parent_path, '/' ) ) { $parent_path = '/' . $parent_path; } - return $this->get_url( $this->get_path( $parent_path ) ); + // Form the parent URL (ditching the query and fragment, if set). + $parent_url = $this->get_url( + array( + 'path' => $parent_path, + 'query' => null, + 'fragment' => null, + ) + ); + + // We process the parent URL through a fresh instance of this class, for consistency. + return ( new self( $parent_url ) )->get_url(); } /** @@ -211,22 +322,29 @@ class URL { * * Borrows from https://www.php.net/manual/en/function.parse-url.php#106731 * - * @param string $path_override If provided this will be used as the URL path. + * @param array $component_overrides If provided, these will override values set in $this->components. * * @return string */ - public function get_url( string $path_override = null ): string { - $scheme = null !== $this->components['scheme'] ? $this->components['scheme'] . '://' : ''; - $host = null !== $this->components['host'] ? $this->components['host'] : ''; - $port = null !== $this->components['port'] ? ':' . $this->components['port'] : ''; + public function get_url( array $component_overrides = array() ): string { + $components = array_merge( $this->components, $component_overrides ); - $user = null !== $this->components['user'] ? $this->components['user'] : ''; - $pass = null !== $this->components['pass'] ? ':' . $this->components['pass'] : ''; + $scheme = null !== $components['scheme'] ? $components['scheme'] . '://' : '//'; + $host = null !== $components['host'] ? $components['host'] : ''; + $port = null !== $components['port'] ? ':' . $components['port'] : ''; + $path = $this->get_path( $components['path'] ); + + // Special handling for hostless URLs (typically, filepaths) referencing the current working directory. + if ( '' === $host && ( '' === $path || '.' === $path ) ) { + $path = './'; + } + + $user = null !== $components['user'] ? $components['user'] : ''; + $pass = null !== $components['pass'] ? ':' . $components['pass'] : ''; $user_pass = ( ! empty( $user ) || ! empty( $pass ) ) ? $user . $pass . '@' : ''; - $path = $path_override ?? $this->get_path(); - $query = null !== $this->components['query'] ? '?' . $this->components['query'] : ''; - $fragment = null !== $this->components['fragment'] ? '#' . $this->components['fragment'] : ''; + $query = null !== $components['query'] ? '?' . $components['query'] : ''; + $fragment = null !== $components['fragment'] ? '#' . $components['fragment'] : ''; return $scheme . $user_pass . $host . $port . $path . $query . $fragment; } diff --git a/plugins/woocommerce/templates/order/form-tracking.php b/plugins/woocommerce/templates/order/form-tracking.php index d26f1a062a9..2b2ce8de6b3 100644 --- a/plugins/woocommerce/templates/order/form-tracking.php +++ b/plugins/woocommerce/templates/order/form-tracking.php @@ -12,7 +12,7 @@ * * @see https://docs.woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 3.6.0 + * @version 6.5.0 */ defined( 'ABSPATH' ) || exit; @@ -22,13 +22,40 @@ global $post;
+ +

+ +

+ +
diff --git a/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js b/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js index 77af197d123..28ca08ce731 100644 --- a/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js +++ b/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js @@ -4,6 +4,7 @@ const { testDifferentStoreCurrenciesWCPay, testSubscriptionsInclusion, testBusinessDetailsForm, + testAdminHomescreen, } = require( '@woocommerce/admin-e2e-tests' ); const { withRestApi, IS_RETEST_MODE } = require( '@woocommerce/e2e-utils' ); @@ -17,3 +18,4 @@ testSelectiveBundleWCPay(); testDifferentStoreCurrenciesWCPay(); testSubscriptionsInclusion(); testBusinessDetailsForm(); +testAdminHomescreen(); diff --git a/plugins/woocommerce/tests/legacy/bootstrap.php b/plugins/woocommerce/tests/legacy/bootstrap.php index 6c29298d0a5..be42bad5755 100644 --- a/plugins/woocommerce/tests/legacy/bootstrap.php +++ b/plugins/woocommerce/tests/legacy/bootstrap.php @@ -194,10 +194,6 @@ class WC_Unit_Tests_Bootstrap { define( 'WC_REMOVE_ALL_DATA', true ); include $this->plugin_dir . '/uninstall.php'; - // Initialize the WC API extensions. - \Automattic\WooCommerce\Internal\Admin\Install::create_tables(); - \Automattic\WooCommerce\Internal\Admin\Install::create_events(); - WC_Install::install(); // Reload capabilities after install, see https://core.trac.wordpress.org/ticket/28374. @@ -259,7 +255,7 @@ class WC_Unit_Tests_Bootstrap { * @return array Filtered feature flags. */ public function add_development_features( $flags ) { - $config = json_decode( file_get_contents( $this->plugin_dir . '/../woocommerce-admin/config/development.json' ) ); // @codingStandardsIgnoreLine. + $config = json_decode( file_get_contents( $this->plugin_dir . '/client/admin/config/development.json' ) ); // @codingStandardsIgnoreLine. foreach ( $config->features as $feature => $bool ) { $flags[ $feature ] = $bool; } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php b/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php index 637506ea631..3fbab5add05 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php @@ -35,11 +35,14 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case { // Callback used by WP_HTTP_TestCase to decide whether to perform HTTP requests or to provide a mocked response. $this->http_responder = array( $this, 'mock_http_responses' ); $this->csv_file = dirname( __FILE__ ) . '/sample.csv'; - $this->sut = new WC_Product_CSV_Importer( $this->csv_file, array( - 'mapping' => $this->get_csv_mapped_items(), - 'parse' => true, - 'prevent_timeouts' => false, - ) ); + $this->sut = new WC_Product_CSV_Importer( + $this->csv_file, + array( + 'mapping' => $this->get_csv_mapped_items(), + 'parse' => true, + 'prevent_timeouts' => false, + ) + ); } /** @@ -112,7 +115,7 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case { $this->assertEquals( 0, count( $results['skipped'] ) ); $this->assertEquals( 7, - count( $results['imported'] ) , + count( $results['imported'] ), 'One import item references a downloadable file stored in an unapproved location: if the import is triggered by an admin user, that location will be automatically approved.' ); } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php index 88eaeffee34..ba2ee818c71 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php @@ -1,15 +1,15 @@ 'setup' ) ), + new TaskList( array( 'id' => 'extended' ) ), array( 'id' => 'wc-unit-test-task', ) diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/test-task.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/test-task.php index 2f3b81ba966..262a9503305 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/test-task.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/test-task.php @@ -96,15 +96,6 @@ class TestTask extends Task { return $this->content; } - /** - * Parent ID. - * - * @return string - */ - public function get_parent_id() { - return 'extended'; - } - /** * Level. * diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/install.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/install.php index 810fee1a8ef..62e771dbc92 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/install.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/install.php @@ -5,13 +5,13 @@ * @package WooCommerce\Admin\Tests */ -use Automattic\WooCommerce\Internal\Admin\Install; - /** * Tests for \Automattic\WooCommerce\Internal\Admin\Install class. */ class WC_Admin_Tests_Install extends WP_UnitTestCase { + const VERSION_OPTION = 'woocommerce_admin_version'; + /** * Integration test for database table creation. * @@ -40,8 +40,7 @@ class WC_Admin_Tests_Install extends WP_UnitTestCase { $query = 'DROP TABLE IF EXISTS ' . implode( ',', $tables ); $wpdb->query( $query ); // phpcs:ignore. - // Try to create the tables. - Install::create_tables(); + WC_Install::create_tables(); $result = $wpdb->get_col( "SHOW TABLES LIKE '{$wpdb->prefix}%'" ); // Check all the tables exist. @@ -50,27 +49,198 @@ class WC_Admin_Tests_Install extends WP_UnitTestCase { } } + + /** + * Run maybe_update_db_version and confirm the expected jobs are pushed to the queue. + * + * @dataProvider db_update_version_provider + * + * @param string $db_update_version WC version to test. + * @param int $expected_jobs_count # of expected jobs. + * + * @return void + */ + public function test_running_db_updates( $db_update_version, $expected_jobs_count ) { + update_option( 'woocommerce_db_version', $db_update_version ); + add_filter( + 'woocommerce_enable_auto_update_db', + function() { + return true; + } + ); + + $class = new ReflectionClass( WC_Install::class ); + $method = $class->getMethod( 'maybe_update_db_version' ); + $method->setAccessible( true ); + $method->invoke( $class ); + + $pending_jobs = WC_Helper_Queue::get_all_pending(); + $pending_jobs = array_filter( + $pending_jobs, + function( $pending_job ) { + return $pending_job->get_hook() === 'woocommerce_run_update_callback'; + } + ); + + $this->assertCount( $expected_jobs_count, $pending_jobs ); + } + + + /** + * Ensure that a DB version callback is defined when there are updates. + */ + public function test_db_update_callbacks_exist() { + $all_callbacks = \WC_Install::get_db_update_callbacks(); + + foreach ( $all_callbacks as $version => $version_callbacks ) { + // Verify all callbacks have been defined. + foreach ( $version_callbacks as $version_callback ) { + if ( strpos( $version_callback, 'wc_admin_update' ) === 0 ) { + $this->assertTrue( + function_exists( $version_callback ), + "Callback {$version_callback}() is not defined." + ); + } + } + } + } + + /** + * By the time we hit this test method, we should have the following cron jobs. + * - wc_admin_daily + * - generate_category_lookup_table + * + * @return void + */ + public function test_cron_job_creation() { + $this->assertNotFalse( wp_next_scheduled( 'wc_admin_daily' ) ); + $this->assertNotFalse( wp_next_scheduled( 'generate_category_lookup_table' ) ); + } + + /** + * Data provider that returns DB Update version string and # of expected pending jobs. + * + * @return array[] + */ + public function db_update_version_provider() { + return array( + // [DB Update version string, # of expected pending jobs] + array( '3.9.0', 33 ), + array( '4.0.0', 26 ), + array( '4.4.0', 22 ), + array( '4.5.0', 20 ), + array( '5.0.0', 16 ), + array( '5.6.0', 14 ), + array( '6.0.0', 7 ), + array( '6.3.0', 4 ), + array( '6.4.0', 0 ), + ); + } + /** * Test missed DB version number update. * See: https:// github.com/woocommerce/woocommerce-admin/issues/5058 */ public function test_missed_version_number_update() { + $this->markTestSkipped( 'We no longer update WooCommerce Admin versions' ); $old_version = '1.6.0'; // This should get updated to later versions as we add more migrations. // Simulate an upgrade from an older version. - update_option( Install::VERSION_OPTION, '1.6.0' ); - Install::install(); + update_option( self::VERSION_OPTION, '1.6.0' ); + WC_Install::install(); WC_Helper_Queue::run_all_pending(); // Simulate a collision/failure in version updating. - update_option( Install::VERSION_OPTION, '1.6.0' ); + update_option( self::VERSION_OPTION, '1.6.0' ); // The next update check should force update the skipped version number. - Install::install(); - $this->assertTrue( version_compare( $old_version, get_option( Install::VERSION_OPTION ), '<' ) ); + WC_Install::install(); + $this->assertTrue( version_compare( $old_version, get_option( self::VERSION_OPTION ), '<' ) ); // The following update check should bump the version to the current (no migrations left). - Install::install(); - $this->assertEquals( get_option( Install::VERSION_OPTION ), WC_ADMIN_VERSION_NUMBER ); + WC_Install::install(); + $this->assertEquals( get_option( self::VERSION_OPTION ), WC_ADMIN_VERSION_NUMBER ); } + + /** + * Test the following options are created. + * + * - woocommerce_admin_install_timestamp + * + * @return void + */ + public function test_options_are_set() { + delete_transient( 'wc_installing' ); + WC_Install::install(); + $options = array( 'woocommerce_admin_install_timestamp' ); + foreach ( $options as $option ) { + $this->assertNotFalse( get_option( $option ) ); + } + } + + /** + * Test woocommerce_admin_installed action. + * @return void + */ + public function test_woocommerce_admin_installed_action() { + delete_transient( 'wc_installing' ); + WC_Install::install(); + $this->assertTrue( did_action( 'woocommerce_admin_installed' ) > 0 ); + } + + /** + * Test woocommerce_updated action gets fired. + * + * @return void + */ + public function test_woocommerce_updated_action() { + $versions = array_keys( WC_Install::get_db_update_callbacks() ); + $prev_version = $versions[ count( $versions ) - 2 ]; + update_option( 'woocommerce_version', $prev_version ); + WC_Install::check_version(); + $this->assertTrue( did_action( 'woocommerce_updated' ) > 0 ); + } + + /** + * Test woocommerce_newly_installed action gets fired. + * @return void + */ + public function test_woocommerce_newly_installed_action() { + delete_option( 'woocommerce_version' ); + WC_Install::check_version(); + $this->assertTrue( did_action( 'woocommerce_newly_installed' ) > 0 ); + } + + /** + * Test migrate_options(); + * @return void + */ + public function test_migrate_options() { + delete_transient( 'wc_installing' ); + WC_Install::install(); + $this->assertTrue( defined( 'WC_ADMIN_MIGRATING_OPTIONS' ) ); + $migrated_options = array( + 'woocommerce_onboarding_profile' => 'wc_onboarding_profile', + 'woocommerce_admin_install_timestamp' => 'wc_admin_install_timestamp', + 'woocommerce_onboarding_opt_in' => 'wc_onboarding_opt_in', + 'woocommerce_admin_import_stats' => 'wc_admin_import_stats', + 'woocommerce_admin_version' => 'wc_admin_version', + 'woocommerce_admin_last_orders_milestone' => 'wc_admin_last_orders_milestone', + 'woocommerce_admin-wc-helper-last-refresh' => 'wc-admin-wc-helper-last-refresh', + 'woocommerce_admin_report_export_status' => 'wc_admin_report_export_status', + 'woocommerce_task_list_complete' => 'woocommerce_task_list_complete', + 'woocommerce_task_list_hidden' => 'woocommerce_task_list_hidden', + 'woocommerce_extended_task_list_complete' => 'woocommerce_extended_task_list_complete', + 'woocommerce_extended_task_list_hidden' => 'woocommerce_extended_task_list_hidden', + ); + + foreach ( $migrated_options as $new_option => $old_option ) { + $old_option_value = get_option( $old_option ); + if ( false === $old_option_value ) { + continue; + } + $this->assertNotFalse( get_option( $new_option ), $new_option ); + } + } + } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/notes/class-wc-tests-notes-data-store.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/notes/class-wc-tests-notes-data-store.php index c36be43720d..b4445dab708 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/notes/class-wc-tests-notes-data-store.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/notes/class-wc-tests-notes-data-store.php @@ -7,6 +7,7 @@ use \Automattic\WooCommerce\Admin\Notes\Notes; use \Automattic\WooCommerce\Admin\Notes\Note; +use \Automattic\WooCommerce\Admin\Notes\DataStore; /** * Class WC_Admin_Tests_Notes_Data_Store @@ -344,6 +345,92 @@ class WC_Admin_Tests_Notes_Data_Store extends WC_Unit_Test_Case { $this->assertEquals( $lookup_note_zero->get_id(), $get_note_zero->get_id() ); } + /** + * Test that get_notes properly handles the $context parameter + */ + public function test_get_notes_context_param() { + $data_store = WC_Data_Store::load( 'admin-note' ); + + // Create two Notes: one in context and one out of it. + $test_context = self::class; + $global_context = DataStore::WC_ADMIN_NOTE_OPER_GLOBAL; + $context_name = 'PHP_UNIT_IN_CONTEXT_TEST_NOTE'; + $out_of_context_name = 'PHP_UNIT_OUT_OF_CONTEXT_TEST_NOTE'; + + foreach ( array( $context_name, $out_of_context_name ) as $note_name ) { + $note = new Note(); + $note->set_name( $note_name ); + $note->set_title( 'PHPUNIT_CONTEXT_TEST_NOTE' ); + $note->set_content( 'PHPUNIT_CONTEXT_TEST_NOTE_CONTENT' ); + $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); + $note->set_source( 'PHPUNIT_TEST' ); + $note->set_is_snoozable( false ); + $note->set_layout( 'plain' ); + $note->set_image( '' ); + $note->add_action( + 'PHPUNIT_TEST_ACTION_SLUG', + 'PHPUNIT_TEST_ACTION_LABEL', + '?s=PHPUNIT_TEST_ACTION_URL' + ); + $note->set_is_deleted( false ); + $note->save(); + } + + // Add filter for 'woocommerce_note_where_clauses' that applies only in context. + $context_filter_hit_count = 0; + $context_filter_callback = function( $where_clauses, $args, $context ) use ( $test_context, $global_context, $context_name, &$context_filter_hit_count ) { + if ( $context === $test_context ) { + $context_filter_hit_count++; + $where_clauses .= ' AND name = "' . $context_name . '"'; + } + return $where_clauses; + }; + add_filter( 'woocommerce_note_where_clauses', $context_filter_callback, 10, 3 ); + + // Add filter for 'woocommerce_note_where_clauses' that applies in any context. + $no_context_filter_hit_count = 0; + $global_context_received = null; + $no_context_filter_callback = function( $where_clauses, $args, $context ) use ( $test_context, &$global_context_received, &$no_context_filter_hit_count ) { + // Record the context we get passed in. + if ( $test_context !== $context ) { + $global_context_received = $context; + } + + // Record that we're here. + $no_context_filter_hit_count++; + $where_clauses .= ' AND source = "PHPUNIT_TEST"'; + return $where_clauses; + }; + add_filter( 'woocommerce_note_where_clauses', $no_context_filter_callback, 10, 3 ); + + // Get note counts. + $no_context_note_count = $data_store->get_notes_count( array( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ), array() ); + $test_context_note_count = $data_store->get_notes_count( array( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ), array(), $test_context ); + + // The no context filter should have been hit twice, the context filter once. + $this->assertEquals( 2, $no_context_filter_hit_count ); + $this->assertEquals( 1, $context_filter_hit_count ); + + // There should be only one note that satisfies the context filter. + $this->assertEquals( 1, $test_context_note_count ); + + // There should be more than one note that satisfies the no-context filter. + $this->assertGreaterThan( 1, $no_context_note_count ); + + // Same tests with get_notes(). + $no_context_get_notes = $data_store->get_notes( array( 'type' => array( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ) ) ); + $test_context_get_notes = $data_store->get_notes( array( 'type' => array( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ) ), $test_context ); + + // There should only be one note in the context result set. + $this->assertEquals( 1, count( $test_context_get_notes ) ); + + // There should be more than one note in the no-context result set. + $this->assertGreaterThan( 1, count( $no_context_get_notes ) ); + + // When not explicitly passed, context should default to WC_ADMIN_NOTE_OPER_GLOBAL. + $this->assertEquals( $global_context, $global_context_received ); + } + /** * Delete notes created by this class's tests. */ diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/plugin-version.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/plugin-version.php deleted file mode 100644 index 4104bf21cb7..00000000000 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/plugin-version.php +++ /dev/null @@ -1,54 +0,0 @@ - 'Version' ) ); - - // Get plugin DB version. - $db_version = defined( 'WC_ADMIN_VERSION_NUMBER' ) ? constant( 'WC_ADMIN_VERSION_NUMBER' ) : false; - - // Compare all versions to the package.json value. - $this->assertEquals( $package->version, $plugin['Version'], 'Plugin header version does not match package.json' ); - $this->assertEquals( $package->version, $db_version, 'DB version constant does not match package.json' ); - } - - /** - * Ensure that a DB version callback is defined when there are updates. - */ - public function test_db_update_callbacks() { - $all_callbacks = \Automattic\WooCommerce\Internal\Admin\Install::get_db_update_callbacks(); - - foreach ( $all_callbacks as $version => $version_callbacks ) { - // Verify all callbacks have been defined. - foreach ( $version_callbacks as $version_callback ) { - $this->assertTrue( function_exists( $version_callback ), "Callback {$version_callback}() is not defined." ); - } - - // Verify there is a version update callback for each version. - $version_string = str_replace( '.', '', $version ); - $expected_callback = "wc_admin_update_{$version_string}_db_version"; - - $this->assertContains( $expected_callback, $version_callbacks, "Expected DB update callback {$expected_callback}() was not found." ); - } - } -} diff --git a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php index aaf81b31e7c..98f97525e9e 100644 --- a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php +++ b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php @@ -18,16 +18,18 @@ class WC_Abstract_Product_Test extends WC_Unit_Test_Case { $download_directories->add_approved_directory( 'https://always.trusted/' ); $problematic_file_source_id = $download_directories->add_approved_directory( 'https://new.supplier/' ); - $product = WC_Helper_Product::create_downloadable_product( array( + $product = WC_Helper_Product::create_downloadable_product( array( - 'name' => 'Book 1', - 'file' => 'https://always.trusted/123.pdf' - ), - array( - 'name' => 'Book 2', - 'file' => 'https://new.supplier/456.pdf' - ), - ) ); + array( + 'name' => 'Book 1', + 'file' => 'https://always.trusted/123.pdf', + ), + array( + 'name' => 'Book 2', + 'file' => 'https://new.supplier/456.pdf', + ), + ) + ); $this->assertCount( 2, @@ -36,17 +38,17 @@ class WC_Abstract_Product_Test extends WC_Unit_Test_Case { ); $download_directories->disable_by_id( $problematic_file_source_id ); + $product_downloads = wc_get_product( $product->get_id() )->get_downloads(); $this->assertCount( - 1, - wc_get_product( $product->get_id() )->get_downloads(), - 'If a trusted download directory is disabled, we expect any individual download files from that location will not be listed.' + 2, + $product_downloads, + 'If a trusted download directory rule is disabled, we still expect it to be fetched.' ); - $this->assertEquals( - 'Book 1', - current( wc_get_product( $product->get_id() )->get_downloads() )->get_name(), - 'Only individual download files that are stored in trusted locations will be fetched.' + $this->assertFalse( + next( $product_downloads )->get_enabled(), + 'If a trusted download directory rule is disabled, corresponding product downloads will also be marked as disabled.' ); $download_directories->set_mode( Download_Directories::MODE_DISABLED ); @@ -54,7 +56,7 @@ class WC_Abstract_Product_Test extends WC_Unit_Test_Case { $this->assertCount( 2, wc_get_product( $product->get_id() )->get_downloads(), - 'If the Approved Download Directories system is completely disabled, we expect all product downloads to be fetched irrespective of where they are stored.' + 'Disabling the Approved Download Directories system entirely does not impact our ability to fetch product downloads.' ); } } diff --git a/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php b/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php index 0cb0ad4dc15..8192a44a069 100644 --- a/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php +++ b/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php @@ -9,7 +9,7 @@ class WC_Product_Download_Test extends WC_Unit_Test_Case { * Test for file without extension. */ public function test_is_allowed_filetype_with_no_extension() { - $upload_dir = trailingslashit( wp_upload_dir()['basedir'] ); + $upload_dir = trailingslashit( wp_upload_dir()['basedir'] ); $file_path_with_no_extension = $upload_dir . 'upload_file'; if ( ! file_exists( $file_path_with_no_extension ) ) { // Copy an existing file without extension. @@ -24,7 +24,7 @@ class WC_Product_Download_Test extends WC_Unit_Test_Case { * Simulates test condition for windows when filename ends with a period. */ public function test_is_allowed_filetype_on_windows_with_period_at_end() { - $upload_dir = trailingslashit( wp_upload_dir()['basedir'] ); + $upload_dir = trailingslashit( wp_upload_dir()['basedir'] ); $file_path_with_period_at_end = $upload_dir . 'upload_file.'; if ( ! file_exists( $file_path_with_period_at_end ) ) { // Copy an existing file without extension. @@ -45,8 +45,20 @@ class WC_Product_Download_Test extends WC_Unit_Test_Case { $download_directories = wc_get_container()->get( Download_Directories::class ); $download_directories->set_mode( Download_Directories::MODE_ENABLED ); - $non_admin_user = wp_insert_user( array( 'user_login' => uniqid(), 'role' => 'editor', 'user_pass' => 'x' ) ); - $admin_user = wp_insert_user( array( 'user_login' => uniqid(), 'role' => 'administrator', 'user_pass' => 'x' ) ); + $non_admin_user = wp_insert_user( + array( + 'user_login' => uniqid(), + 'role' => 'editor', + 'user_pass' => 'x', + ) + ); + $admin_user = wp_insert_user( + array( + 'user_login' => uniqid(), + 'role' => 'administrator', + 'user_pass' => 'x', + ) + ); $ebook_url = 'https://external.site/books/ultimate-guide-to-stuff.pdf'; $podcast_url = 'https://external.site/podcasts/ultimate-guide-to-stuff.mp3'; @@ -63,4 +75,57 @@ class WC_Product_Download_Test extends WC_Unit_Test_Case { $this->expectExceptionMessage( 'cannot be used: it is not located in an approved directory' ); $download->check_is_valid(); } + + /** + * Test handling of filepaths described via shortcodes in relation to the Approved Download Directory + * feature. This is to simulate scenarios such as encountered when using the S3 Downloads extension. + */ + public function test_shortcode_resolution_for_approved_directory_rules() { + /** @var Download_Directories $download_directories */ + $download_directories = wc_get_container()->get( Download_Directories::class ); + $download_directories->set_mode( Download_Directories::MODE_ENABLED ); + $dynamic_filepath = 'https://fast.reliable.external.fileserver.com/bucket-123/textbook.pdf'; + + // We select an admin user because we wish to automatically add Approved Directory rules. + $admin_user = wp_insert_user( + array( + 'user_login' => uniqid(), + 'role' => 'administrator', + 'user_pass' => 'x', + ) + ); + wp_set_current_user( $admin_user ); + + add_shortcode( + 'dynamic-download', + function () { + return 'https://fast.reliable.external.fileserver.com/bucket-123/textbook.pdf'; + } + ); + + $this->assertFalse( + $download_directories->is_valid_path( $dynamic_filepath ), + 'Confirm the filepath returned by the test URL is not yet valid.' + ); + + $download = new WC_Product_Download(); + $download->set_file( '[dynamic-download]' ); + + $this->assertNull( + $download->check_is_valid(), + 'The downloadable file successfully validates (if it did not, an exception would be thrown).' + ); + + $this->assertTrue( + $download_directories->is_valid_path( $dynamic_filepath ), + 'Confirm the filepath returned by the test URL is now considered valid.' + ); + + remove_shortcode( 'dynamic-download' ); + + // Now the shortcode is removed (perhaps the parent plugin has been removed/disabled) it will not resolve + // and so the filepath will not validate. + $this->expectException( 'Error' ); + $download_directories->check_is_valid(); + } } diff --git a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php new file mode 100644 index 00000000000..e9e9c2e1034 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php @@ -0,0 +1,381 @@ +create_order_custom_table_if_not_exist(); + $this->data_store = wc_get_container()->get( OrdersTableDataStore::class ); + $this->sut = wc_get_container()->get( WPPostToCOTMigrator::class ); + } + + /** + * Test that migration for a normal order happens as expected. + */ + public function test_process_next_migration_batch_normal_order() { + $order = wc_get_order( $this->create_complex_wp_post_order() ); + $this->clear_all_orders_and_reset_checkpoint(); + $this->sut->process_next_migration_batch( 100 ); + + $this->assert_core_data_is_migrated( $order ); + $this->assert_order_addresses_are_migrated( $order ); + $this->assert_order_op_data_is_migrated( $order ); + } + + /** + * Test that already migrated order isn't migrated twice. + */ + public function test_process_next_migration_batch_already_migrated_order() { + global $wpdb; + $order = wc_get_order( $this->create_complex_wp_post_order() ); + $this->clear_all_orders_and_reset_checkpoint(); + + // Run the migration once. + $this->sut->process_next_migration_batch( 100 ); + + // Delete checkpoint and run migration again, assert there are still no duplicates. + $this->sut->update_checkpoint( 0 ); + $this->sut->process_next_migration_batch( 100 ); + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $this->assertEquals( + 1, + $wpdb->get_var( + " +SELECT COUNT(*) FROM {$this->data_store::get_orders_table_name()} +WHERE post_id = {$order->get_id()}" + ), + 'Order record is duplicated.' + ); + $order_id = $wpdb->get_var( "SELECT id FROM {$this->data_store::get_orders_table_name()} WHERE post_id = {$order->get_id()}" ); + $this->assertEquals( + 1, + $wpdb->get_var( + " +SELECT COUNT(*) FROM {$this->data_store::get_addresses_table_name()} +WHERE order_id = {$order_id} AND address_type = 'billing' +" + ) + ); + $this->assertEquals( + 1, + $wpdb->get_var( + " +SELECT COUNT(*) FROM {$this->data_store::get_addresses_table_name()} +WHERE order_id = {$order_id} AND address_type = 'shipping' +" + ) + ); + $this->assertEquals( + 1, + $wpdb->get_var( + " +SELECT COUNT(*) FROM {$this->data_store::get_operational_data_table_name()} +WHERE order_id = {$order_id} +" + ) + ); + // phpcs:enable + } + + /** + * Test that when an order is partially migrated, it can still be resumed as expected. + */ + public function test_process_next_migration_batch_interrupted_migrating_order() { + $this->markTestSkipped(); + } + + /** + * Test that invalid order data is not migrated but logged. + */ + public function test_process_next_migration_batch_invalid_order_data() { + $this->markTestSkipped(); + } + + /** + * Test when one order is invalid but other one is valid in a migration batch. + */ + public function test_process_next_migration_batch_invalid_valid_order_combo() { + $this->markTestSkipped(); + } + + /** + * Helper method to get order object from COT. + * + * @param WP_Post $post_order Post object for order. + * + * @return array|object|void|null DB object from COT. + */ + private function get_order_from_cot( $post_order ) { + global $wpdb; + $order_table = $this->data_store::get_orders_table_name(); + $query = "SELECT * FROM $order_table WHERE post_id = {$post_order->get_id()};"; + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + return $wpdb->get_row( $query ); + } + + /** + * Helper method to get address details from DB. + * + * @param int $order_id Order ID. + * @param string $address_type Address Type. + * + * @return array|object|void|null DB object. + */ + private function get_address_details_from_cot( $order_id, $address_type ) { + global $wpdb; + $address_table = $this->data_store::get_addresses_table_name(); + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + return $wpdb->get_row( "SELECT * FROM $address_table WHERE order_id = $order_id AND address_type = '$address_type';" ); + } + + /** + * Helper method to get operational details from COT. + * + * @param int $order_id Order ID. + * + * @return array|object|void|null DB Object. + */ + private function get_order_operational_data_from_cot( $order_id ) { + global $wpdb; + $operational_data_table = $this->data_store::get_operational_data_table_name(); + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + return $wpdb->get_row( "SELECT * FROM $operational_data_table WHERE order_id = $order_id;" ); + } + + /** + * Helper method to create complex wp_post based order. + * + * @return int Order ID + */ + private function create_complex_wp_post_order() { + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + $uniq_cust_id = wp_generate_password( 10, false ); + $customer = CustomerHelper::create_customer( "user$uniq_cust_id", $uniq_cust_id, "user$uniq_cust_id@example.com" ); + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '15.0000', + 'tax_rate_name' => 'tax', + 'tax_rate_priority' => '1', + 'tax_rate_order' => '1', + 'tax_rate_shipping' => '1', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + ShippingHelper::create_simple_flat_rate(); + + $order = OrderHelper::create_order(); + // Make sure this is a wp_post order. + $post = get_post( $order->get_id() ); + $this->assertNotNull( $post, 'Order is not created in wp_post table.' ); + $this->assertEquals( 'shop_order', $post->post_type, 'Order is not created in wp_post table.' ); + + $order->save(); + + $order->set_status( 'completed' ); + $order->set_currency( 'INR' ); + $order->set_customer_id( $customer->get_id() ); + $order->set_billing_email( $customer->get_billing_email() ); + + $payment_gateway = new WC_Mock_Payment_Gateway(); + $order->set_payment_method( 'mock' ); + $order->set_transaction_id( 'mock1' ); + + $order->set_customer_ip_address( '1.1.1.1' ); + $order->set_customer_user_agent( 'wc_unit_tests' ); + + $order->save(); + + $order->set_shipping_first_name( 'Albert' ); + $order->set_shipping_last_name( 'Einstein' ); + $order->set_shipping_company( 'The Olympia Academy' ); + $order->set_shipping_address_1( '112 Mercer Street' ); + $order->set_shipping_address_2( 'Princeton' ); + $order->set_shipping_city( 'New Jersey' ); + $order->set_shipping_postcode( '08544' ); + $order->set_shipping_phone( '299792458' ); + $order->set_shipping_country( 'US' ); + + $order->set_created_via( 'unit_tests' ); + $order->set_version( '0.0.2' ); + $order->set_prices_include_tax( true ); + wc_update_coupon_usage_counts( $order->get_id() ); + $order->get_data_store()->set_download_permissions_granted( $order, true ); + $order->set_cart_hash( '1234' ); + $order->update_meta_data( '_new_order_email_sent', 'true' ); + $order->update_meta_data( '_order_stock_reduced', 'true' ); + $order->set_date_paid( time() ); + $order->set_date_completed( time() ); + $order->calculate_shipping(); + + $order->save(); + $order->save_meta_data(); + + return $order->get_id(); + } + + /** + * Helper method to assert core data is migrated. + * + * @param WC_Order $order Order object. + */ + private function assert_core_data_is_migrated( $order ) { + $db_order = $this->get_order_from_cot( $order ); + + // Verify core data. + $this->assertEquals( $order->get_id(), $db_order->post_id ); + $this->assertEquals( 'wc-' . $order->get_status(), $db_order->status ); + $this->assertEquals( 'INR', $db_order->currency ); + $this->assertEquals( $order->get_customer_id(), $db_order->customer_id ); + $this->assertEquals( $order->get_billing_email(), $db_order->billing_email ); + $this->assertEquals( $order->get_payment_method(), $db_order->payment_method ); + $this->assertEquals( + $order->get_date_created()->date( DATE_ISO8601 ), + ( new WC_DateTime( $db_order->date_created_gmt ) )->date( DATE_ISO8601 ) + ); + $this->assertEquals( $order->get_date_modified()->date( DATE_ISO8601 ), ( new WC_DateTime( $db_order->date_updated_gmt ) )->date( DATE_ISO8601 ) ); + $this->assertEquals( $order->get_parent_id(), $db_order->parent_order_id ); + $this->assertEquals( $order->get_payment_method_title(), $db_order->payment_method_title ); + $this->assertEquals( $order->get_transaction_id(), $db_order->transaction_id ); + $this->assertEquals( $order->get_customer_ip_address(), $db_order->ip_address ); + $this->assertEquals( $order->get_customer_user_agent(), $db_order->user_agent ); + } + + /** + * Helper method to assert addresses are migrated. + * + * @param WC_Order $order Order object. + */ + private function assert_order_addresses_are_migrated( $order ) { + $db_order = $this->get_order_from_cot( $order ); + + // Verify order billing address. + $db_order_address = $this->get_address_details_from_cot( $db_order->id, 'billing' ); + $this->assertEquals( $order->get_billing_first_name(), $db_order_address->first_name ); + $this->assertEquals( $order->get_billing_last_name(), $db_order_address->last_name ); + $this->assertEquals( $order->get_billing_company(), $db_order_address->company ); + $this->assertEquals( $order->get_billing_address_1(), $db_order_address->address_1 ); + $this->assertEquals( $order->get_billing_address_2(), $db_order_address->address_2 ); + $this->assertEquals( $order->get_billing_city(), $db_order_address->city ); + $this->assertEquals( $order->get_billing_postcode(), $db_order_address->postcode ); + $this->assertEquals( $order->get_billing_country(), $db_order_address->country ); + $this->assertEquals( $order->get_billing_email(), $db_order_address->email ); + $this->assertEquals( $order->get_billing_phone(), $db_order_address->phone ); + + // Verify order shipping address. + $db_order_address = $this->get_address_details_from_cot( $db_order->id, 'shipping' ); + $this->assertEquals( $order->get_shipping_first_name(), $db_order_address->first_name ); + $this->assertEquals( $order->get_shipping_last_name(), $db_order_address->last_name ); + $this->assertEquals( $order->get_shipping_company(), $db_order_address->company ); + $this->assertEquals( $order->get_shipping_address_1(), $db_order_address->address_1 ); + $this->assertEquals( $order->get_shipping_address_2(), $db_order_address->address_2 ); + $this->assertEquals( $order->get_shipping_city(), $db_order_address->city ); + $this->assertEquals( $order->get_shipping_postcode(), $db_order_address->postcode ); + $this->assertEquals( $order->get_shipping_country(), $db_order_address->country ); + $this->assertEquals( $order->get_shipping_phone(), $db_order_address->phone ); + } + + /** + * Helper method to assert operational data is migrated. + * + * @param WC_Order $order Order object. + */ + private function assert_order_op_data_is_migrated( $order ) { + $db_order = $this->get_order_from_cot( $order ); + // Verify order operational data. + $db_order_op_data = $this->get_order_operational_data_from_cot( $db_order->id ); + $this->assertEquals( $order->get_created_via(), $db_order_op_data->created_via ); + $this->assertEquals( $order->get_version(), $db_order_op_data->woocommerce_version ); + $this->assertEquals( $order->get_prices_include_tax(), $db_order_op_data->prices_include_tax ); + $this->assertEquals( + wc_string_to_bool( $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ), + $db_order_op_data->coupon_usages_are_counted + ); + $this->assertEquals( + wc_string_to_bool( $order->get_data_store()->get_download_permissions_granted( $order ) ), + $db_order_op_data->download_permission_granted + ); + $this->assertEquals( $order->get_cart_hash(), $db_order_op_data->cart_hash ); + $this->assertEquals( + wc_string_to_bool( $order->get_meta( '_new_order_email_sent' ) ), + $db_order_op_data->new_order_email_sent + ); + $this->assertEquals( $order->get_order_key(), $db_order_op_data->order_key ); + $this->assertEquals( $order->get_data_store()->get_stock_reduced( $order ), $db_order_op_data->order_stock_reduced ); + $this->assertEquals( + $order->get_date_paid()->date( DATE_ISO8601 ), + ( new WC_DateTime( $db_order_op_data->date_paid_gmt ) )->date( DATE_ISO8601 ) + ); + $this->assertEquals( + $order->get_date_completed()->date( DATE_ISO8601 ), + ( new WC_DateTime( $db_order_op_data->date_completed_gmt ) )->date( DATE_ISO8601 ) + ); + $this->assertEquals( (float) $order->get_shipping_tax(), (float) $db_order_op_data->shipping_tax_amount ); + $this->assertEquals( (float) $order->get_shipping_total(), (float) $db_order_op_data->shipping_total_amount ); + $this->assertEquals( (float) $order->get_discount_tax(), (float) $db_order_op_data->discount_tax_amount ); + $this->assertEquals( (float) $order->get_discount_total(), (float) $db_order_op_data->discount_total_amount ); + } + + /** + * Helper method to clear checkout and truncate order tables. + */ + private function clear_all_orders_and_reset_checkpoint() { + global $wpdb; + $order_tables = $this->data_store->get_all_table_names(); + foreach ( $order_tables as $table ) { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( "TRUNCATE table $table;" ); + } + $this->sut->delete_checkpoint(); + } + + /** + * Helper method to create custom tables if not present. + */ + private function create_order_custom_table_if_not_exist() { + $order_table_controller = wc_get_container() + ->get( 'Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController' ); + $order_table_controller->show_feature(); + $this->synchronizer = wc_get_container() + ->get( DataSynchronizer::class ); + if ( ! $this->synchronizer->check_orders_table_exists() ) { + $this->synchronizer->create_database_tables(); + } + } +} diff --git a/plugins/woocommerce/tests/php/src/Internal/Utilities/URLTest.php b/plugins/woocommerce/tests/php/src/Internal/Utilities/URLTest.php index 415e12e2a2d..70f89a06cc0 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Utilities/URLTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Utilities/URLTest.php @@ -9,9 +9,13 @@ use WC_Unit_Test_Case; * A collection of tests for the filepath utility class. */ class URLTest extends WC_Unit_Test_Case { + + /** + * @testdox Test if it can be determined whether a URL is absolute or relative. + */ public function test_if_absolute_or_relative() { $this->assertTrue( - ( new URL( '/etc/foo/bar' ) )->is_absolute() , + ( new URL( '/etc/foo/bar' ) )->is_absolute(), 'Correctly determines if a Unix-style path is absolute.' ); @@ -26,152 +30,176 @@ class URLTest extends WC_Unit_Test_Case { ); } - public function test_directory_traversal_resolution() { - $this->assertEquals( - '/var/foo/foobar', - ( new URL( '/var/foo/bar/baz/../../foobar' ) )->get_path(), - 'Correctly resolves a path containing a directory traversal.' - ); + /** + * @dataProvider path_expectations + * + * @param string $source_path Source path to test. + * @param string $expected_resolution Expected result of the test. + */ + public function test_path_resolution( $source_path, $expected_resolution ) { + $this->assertEquals( $expected_resolution, ( new URL( $source_path ) )->get_path() ); + } - $this->assertEquals( - '/bazbar', - ( new URL( '/var/foo/../../../../bazbar' ) )->get_path(), - 'Correctly resolves a path containing a directory traversal, even if the traversals attempt to backtrack beyond the root directory.' - ); - - $this->assertEquals( - '../should/remain/relative', - ( new URL( 'relative/../../should/remain/relative' ) )->get_path(), - 'Simplifies a relative path containing directory traversals to the extent possible (without inspecting the filesystem).' + /** + * Expectations when requesting the path of a URL. + * + * @return string[][] + */ + public function path_expectations(): array { + return array( + array( '/var/foo/bar/baz/../../foobar', '/var/foo/foobar' ), + array( '/var/foo/../../../../bazbar', '/bazbar' ), + array( '././././.', './' ), + array( 'empty/segments//are/stripped', 'empty/segments/are/stripped' ), + array( '///nonempty/ /whitespace/ /is//kept', '/nonempty/ /whitespace/ /is/kept' ), + array( 'relative/../../should/remain/relative', '../should/remain/relative' ), + array( 'relative/../../../should/remain/relative', '../../should/remain/relative' ), + array( 'c:\\Windows\Server\HTTP\dump.xml', 'c:/Windows/Server/HTTP/dump.xml' ), ); } - public function test_can_get_normalized_string_representation() { - $this->assertEquals( - 'foo/bar/baz', - ( new URL( 'foo/bar//baz' ) )->get_path(), - 'Empty segments are discarded, remains as a relative path.' - ); + /** + * @dataProvider url_expectations + * + * @param string $source_url URL to test. + * @param string $expected_resolution Expected result of the test. + */ + public function test_url_resolution( $source_url, $expected_resolution ) { + $this->assertEquals( $expected_resolution, ( new URL( $source_url ) )->get_url() ); + } - $this->assertEquals( - '/foo/ /bar/ /baz/foobarbaz', - ( new URL( '///foo/ /bar/ /baz//foobarbaz' ) )->get_path(), - 'Empty segments are discarded, non-empty segments containing only whitespace are preserved, remains as an absolute path.' - ); - - $this->assertEquals( - 'c:/Windows/Server/HTTP/dump.xml', - ( new URL( 'c:\\Windows\Server\HTTP\dump.xml' ) )->get_path(), - 'String representations of Windows filepaths have forward slash separators and preserve the drive letter.' + /** + * Expectations when resolving URLs. + * + * @return string[][] + */ + public function url_expectations(): array { + return array( + array( '/../foo/bar/baz/bazooka/../../baz', 'file:///foo/bar/baz' ), + array( './a/b/c/./../././../b/c', 'file://a/b/c' ), + array( 'relative/path', 'file://relative/path' ), + array( '/absolute/path', 'file:///absolute/path' ), + array( '/var/www/network/%2econfig', 'file:///var/www/network/%2econfig' ), + array( '///foo', 'file:///foo' ), + array( '~/foo.txt', 'file://~/foo.txt' ), + array( 'baz///foo', 'file://baz/foo' ), + array( 'file:///etc/foo/bar', 'file:///etc/foo/bar' ), + array( 'foo://bar', 'foo://bar/' ), + array( 'foo://bar/baz-file', 'foo://bar/baz-file' ), + array( 'foo://bar/baz-dir/', 'foo://bar/baz-dir/' ), + array( 'https://foo.bar/parent/.%2e/asset.txt', 'https://foo.bar/asset.txt' ), + array( 'https://foo.bar/parent/%2E./asset.txt', 'https://foo.bar/asset.txt' ), + array( 'https://foo.bar/parent/%2E%2e/asset.txt', 'https://foo.bar/asset.txt' ), + array( 'https://foo.bar/parent/%2E.%2fasset.txt', 'https://foo.bar/parent/%2E.%2fasset.txt' ), + array( 'http://localhost?../../bar', 'http://localhost/?../../bar' ), + array( '//http.or.https/', '//http.or.https/' ), + array( '//schemaless/with-path', '//schemaless/with-path' ), ); } - public function test_can_get_normalized_url_representation() { - $this->assertEquals( - 'file://relative/path', - ( new URL( 'relative/path' ) )->get_url(), - 'Can obtain a URL representation of a relative filepath, even when the initial string was a plain filepath.' - ); + /** + * @dataProvider parent_url_expectations + * + * @param string $source_path Path to test. + * @param int $parent_level Parent level to use for the test. + * @param string|false $expectation Expected result of the test. + */ + public function test_can_obtain_parent_url( string $source_path, int $parent_level, $expectation ) { + $this->assertEquals( $expectation, ( new URL( $source_path ) )->get_parent_url( $parent_level ) ); + } - $this->assertEquals( - 'file:///absolute/path', - ( new URL( '/absolute/path' ) )->get_url(), - 'Can obtain a URL representation of an absolute filepath, even when the initial string was a plain filepath.' - ); - - $this->assertEquals( - 'file:///etc/foo/bar', - ( new URL( 'file:///etc/foo/bar' ) )->get_url(), - 'Can obtain a URL representation of a filepath, when the source filepath was also expressed as a URL.' + /** + * Expectations when resolving (grand-)parent URLs. + * + * @return array[] + */ + public function parent_url_expectations(): array { + return array( + array( '/', 1, false ), + array( '/', 2, false ), + array( './', 1, 'file://../' ), + array( '../', 1, 'file://../../' ), + array( 'relative-file.png', 1, 'file://./' ), + array( 'relative-file.png', 2, 'file://../' ), + array( '/var/dev/', 1, 'file:///var/' ), + array( '/var/../dev/./../foo/bar', 1, 'file:///foo/' ), + array( 'https://example.com', 1, false ), + array( 'https://example.com/foo', 1, 'https://example.com/' ), + array( 'https://example.com/foo/bar/baz/../cat/', 2, 'https://example.com/foo/' ), + array( 'https://example.com/foo/bar/baz/%2E%2E/dog/', 2, 'https://example.com/foo/' ), + array( 'file://./', 1, 'file://../' ), + array( 'file://./', 2, 'file://../../' ), + array( 'file://../../foo', 1, 'file://../../' ), + array( 'file://../../foo', 2, 'file://../../../' ), + array( 'file://../../', 1, 'file://../../../' ), + array( 'file://./../', 2, 'file://../../../' ), ); } - public function test_handling_of_percent_encoded_periods() { - $this->assertEquals( - 'https://foo.bar/asset.txt', - ( new URL( 'https://foo.bar/parent/.%2e/asset.txt' ) )->get_url(), - 'Directory traversals expressed using percent-encoding are still resolved (lowercase, one encoded period).' - ); - - $this->assertEquals( - 'https://foo.bar/asset.txt', - ( new URL( 'https://foo.bar/parent/%2E./asset.txt' ) )->get_url(), - 'Directory traversals expressed using percent-encoding are still resolved (uppercase, one encoded period).' - ); - - $this->assertEquals( - 'https://foo.bar/asset.txt', - ( new URL( 'https://foo.bar/parent/%2E%2e/asset.txt' ) )->get_url(), - 'Directory traversals expressed using percent-encoding are still resolved (mixed case, both periods encoded).' - ); - - $this->assertEquals( - 'https://foo.bar/parent/%2E.%2fasset.txt', - ( new URL( 'https://foo.bar/parent/%2E.%2fasset.txt' ) )->get_url(), - 'If the forward slash after a double period is URL encoded, there is no directory traversal (since this means the slash is a part of the segment and is not a separator).' - ); - - $this->assertEquals( - 'file:///var/www/network/%2econfig', - ( new URL( '/var/www/network/%2econfig' ) )->get_url(), - 'Use of percent-encoding in URLs is accepted and unnecessary conversion does not take place.' - ); + /** + * @dataProvider all_parent_url_expectations + * + * @param string $source_path Path to test. + * @param array $expectation Expected result of the test. + */ + public function test_can_obtain_all_parent_urls( string $source_path, array $expectation ) { + $this->assertEquals( $expectation, ( new URL( $source_path ) )->get_all_parent_urls() ); } - public function test_can_obtain_parent_url() { - $this->assertEquals( - 'file:///', - ( new URL( '/' ) )->get_parent_url(), - 'The parent of root directory "/" is "/".' - ); - - $this->assertEquals( - 'file:///var/', - ( new URL( '/var/dev/' ) )->get_parent_url(), - 'The parent URL will be trailingslashed.' - ); - - $this->assertEquals( - 'https://example.com/', - ( new URL( 'https://example.com' ) )->get_parent_url(), - 'The host name (for non-file URLs) is distinct from the path and will not be removed.' - ); - } - - public function test_can_obtain_all_parent_urls() { - $this->assertEquals( + /** + * Expectations when obtaining all possible parent URLs of a given URL/path. + * + * @return array[] + */ + public function all_parent_urL_expectations(): array { + return array( array( - 'https://local.web/wp-content/uploads/woocommerce_uploads/pdf_bucket/', - 'https://local.web/wp-content/uploads/woocommerce_uploads/', - 'https://local.web/wp-content/uploads/', - 'https://local.web/wp-content/', - 'https://local.web/', + 'https://local.web/wp-content/uploads/woocommerce_uploads/pdf_bucket/secret-sauce.pdf', + array( + 'https://local.web/wp-content/uploads/woocommerce_uploads/pdf_bucket/', + 'https://local.web/wp-content/uploads/woocommerce_uploads/', + 'https://local.web/wp-content/uploads/', + 'https://local.web/wp-content/', + 'https://local.web/', + ), + ), + array( + '/srv/websites/my.wp.site/public/test-file.doc', + array( + 'file:///srv/websites/my.wp.site/public/', + 'file:///srv/websites/my.wp.site/', + 'file:///srv/websites/', + 'file:///srv/', + 'file:///', + ), + ), + array( + 'C:\\Documents\\Web\\TestSite\\BackgroundTrack.mp3', + array( + 'file://C:/Documents/Web/TestSite/', + 'file://C:/Documents/Web/', + 'file://C:/Documents/', + 'file://C:/', + ), ), - ( new URL( 'https://local.web/wp-content/uploads/woocommerce_uploads/pdf_bucket/secret-sauce.pdf' ) )->get_all_parent_urls(), - 'All parent URLs can be derived, but the host name is never stripped.' - ); - - $this->assertEquals( array( - 'file:///srv/websites/my.wp.site/public/', - 'file:///srv/websites/my.wp.site/', - 'file:///srv/websites/', - 'file:///srv/', 'file:///', + array(), ), - ( new URL( '/srv/websites/my.wp.site/public/test-file.doc' ) )->get_all_parent_urls(), - 'All parent URLs can be derived for a filepath, up to and including the root directory.' - ); - - $this->assertEquals( array( - 'file://C:/Documents/Web/TestSite/', - 'file://C:/Documents/Web/', - 'file://C:/Documents/', - 'file://C:/', + 'relative/to/abspath', + array( + 'file://relative/to/', + 'file://relative/', + 'file://./', + ), + ), + array( + '../../some.file', + array( + 'file://../../', + ), ), - ( new URL( 'C:\\Documents\\Web\\TestSite\\BackgroundTrack.mp3' ) )->get_all_parent_urls(), - 'All parent URLs can be derived for a filepath, up to and including the root directory plus drive letter (Windows).' ); } } diff --git a/plugins/woocommerce/uninstall.php b/plugins/woocommerce/uninstall.php index 425683e0f2f..623877fecec 100644 --- a/plugins/woocommerce/uninstall.php +++ b/plugins/woocommerce/uninstall.php @@ -8,6 +8,9 @@ * @version 2.3.0 */ +use Automattic\WooCommerce\Admin\Notes\Notes; +use Automattic\WooCommerce\Admin\ReportsSync; + defined( 'WP_UNINSTALL_PLUGIN' ) || exit; global $wpdb, $wp_version; @@ -20,6 +23,9 @@ wp_clear_scheduled_hook( 'woocommerce_cleanup_logs' ); wp_clear_scheduled_hook( 'woocommerce_geoip_updater' ); wp_clear_scheduled_hook( 'woocommerce_tracker_send_event' ); wp_clear_scheduled_hook( 'woocommerce_cleanup_rate_limits' ); +wp_clear_scheduled_hook( 'wc_admin_daily' ); +wp_clear_scheduled_hook( 'generate_category_lookup_table' ); +wp_clear_scheduled_hook( 'wc_admin_unsnooze_admin_notes' ); /* * Only remove ALL product and page data if WC_REMOVE_ALL_DATA constant is set to true in user's @@ -28,9 +34,6 @@ wp_clear_scheduled_hook( 'woocommerce_cleanup_rate_limits' ); */ if ( defined( 'WC_REMOVE_ALL_DATA' ) && true === WC_REMOVE_ALL_DATA ) { // Drop WC Admin tables. - include_once dirname( __FILE__ ) . '/src/Internal/Admin/Install.php'; - \Automattic\WooCommerce\Internal\Admin\Install::drop_tables(); - include_once dirname( __FILE__ ) . '/includes/class-wc-install.php'; // Roles + caps. diff --git a/plugins/woocommerce/woocommerce.php b/plugins/woocommerce/woocommerce.php index 62470bec195..e80a3779ba3 100644 --- a/plugins/woocommerce/woocommerce.php +++ b/plugins/woocommerce/woocommerce.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce * Plugin URI: https://woocommerce.com/ * Description: An eCommerce toolkit that helps you sell anything. Beautifully. - * Version: 6.4.0-dev + * Version: 6.5.0-dev * Author: Automattic * Author URI: https://woocommerce.com * Text Domain: woocommerce diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3554369126f..a1ac8e52afa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,7 +20,9 @@ importers: '@wordpress/prettier-config': ^1.1.1 chalk: ^4.1.2 glob: ^7.2.0 + husky: ^7.0.4 jest: ^27.3.1 + lint-staged: ^12.3.7 lodash: ^4.17.21 mkdirp: ^1.0.4 node-stream-zip: ^1.15.0 @@ -48,7 +50,9 @@ importers: '@wordpress/prettier-config': 1.1.1 chalk: 4.1.2 glob: 7.2.0 + husky: 7.0.4 jest: 27.3.1 + lint-staged: 12.3.7 mkdirp: 1.0.4 node-stream-zip: 1.15.0 prettier: /wp-prettier/2.2.1-beta-1 @@ -177,10 +181,10 @@ importers: '@wordpress/dom': ^3.3.2 '@wordpress/element': ^4.1.1 '@wordpress/eslint-plugin': ^11.0.0 - '@wordpress/hooks': ^2.12.3 + '@wordpress/hooks': ^3.5.0 '@wordpress/html-entities': ^3.3.1 '@wordpress/i18n': ^4.3.1 - '@wordpress/icons': ^6.3.0 + '@wordpress/icons': ^8.1.0 '@wordpress/keycodes': ^3.3.1 '@wordpress/scripts': ^12.6.1 '@wordpress/url': ^3.4.1 @@ -230,10 +234,10 @@ importers: '@wordpress/deprecated': 3.4.1 '@wordpress/dom': 3.4.1 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 2.12.3 + '@wordpress/hooks': 3.5.0 '@wordpress/html-entities': 3.4.1 '@wordpress/i18n': 4.4.1 - '@wordpress/icons': 6.3.0 + '@wordpress/icons': 8.1.0 '@wordpress/keycodes': 3.4.1 '@wordpress/url': 3.5.1 '@wordpress/viewport': 4.2.1 @@ -428,7 +432,7 @@ importers: '@wordpress/deprecated': ^3.3.1 '@wordpress/element': ^4.1.1 '@wordpress/eslint-plugin': ^11.0.0 - '@wordpress/hooks': ^2.12.3 + '@wordpress/hooks': ^3.5.0 '@wordpress/i18n': ^4.3.1 '@wordpress/url': ^3.4.1 dompurify: ^2.3.6 @@ -451,7 +455,7 @@ importers: '@wordpress/data-controls': 2.4.1 '@wordpress/deprecated': 3.4.1 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 2.12.3 + '@wordpress/hooks': 3.5.0 '@wordpress/i18n': 4.4.1 '@wordpress/url': 3.5.1 dompurify: 2.3.6 @@ -725,7 +729,7 @@ importers: '@wordpress/element': ^4.1.1 '@wordpress/eslint-plugin': ^11.0.0 '@wordpress/i18n': ^4.3.1 - '@wordpress/icons': ^6.3.0 + '@wordpress/icons': ^8.1.0 '@wordpress/keycodes': ^3.3.1 classnames: ^2.3.1 concurrently: ^7.0.0 @@ -750,7 +754,7 @@ importers: '@wordpress/components': 19.6.1_@babel+core@7.17.8 '@wordpress/element': 4.2.1 '@wordpress/i18n': 4.4.1 - '@wordpress/icons': 6.3.0 + '@wordpress/icons': 8.1.0 '@wordpress/keycodes': 3.4.1 classnames: 2.3.1 dompurify: 2.3.6 @@ -795,7 +799,7 @@ importers: '@types/qs': ^6.9.7 '@wordpress/api-fetch': ^6.0.1 '@wordpress/eslint-plugin': ^11.0.0 - '@wordpress/hooks': ^2.12.3 + '@wordpress/hooks': ^3.5.0 cookie: ^0.4.2 eslint: ^8.12.0 jest: ^27.5.1 @@ -808,7 +812,7 @@ importers: '@automattic/explat-client': 0.0.3 '@automattic/explat-client-react-helpers': 0.0.4 '@wordpress/api-fetch': 6.1.1 - '@wordpress/hooks': 2.12.3 + '@wordpress/hooks': 3.5.0 cookie: 0.4.2 qs: 6.10.3 devDependencies: @@ -866,7 +870,7 @@ importers: '@wordpress/compose': ^5.1.2 '@wordpress/element': ^4.1.1 '@wordpress/eslint-plugin': ^11.0.0 - '@wordpress/hooks': ^2.12.3 + '@wordpress/hooks': ^3.5.0 '@wordpress/notices': ^3.3.2 '@wordpress/url': ^3.4.1 eslint: ^8.12.0 @@ -882,16 +886,16 @@ importers: '@wordpress/components': 19.6.1_@babel+core@7.17.8 '@wordpress/compose': 5.2.1 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 2.12.3 + '@wordpress/hooks': 3.5.0 '@wordpress/notices': 3.4.1 '@wordpress/url': 3.5.1 history: 4.10.1 qs: 6.10.3 devDependencies: '@babel/core': 7.17.8 + '@babel/runtime': 7.17.7 '@wordpress/eslint-plugin': 11.0.1_7c040a9b494a33cf8bf9079642892fb1 eslint: 8.12.0 - '@babel/runtime': 7.17.7 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -901,7 +905,7 @@ importers: packages/js/notices: specifiers: '@babel/core': ^7.17.5 - '@wordpress/a11y': ^2.15.3 + '@wordpress/a11y': ^3.5.0 '@wordpress/data': ^6.3.0 '@wordpress/eslint-plugin': ^11.0.0 '@wordpress/notices': ^3.3.2 @@ -912,7 +916,7 @@ importers: ts-jest: ^27.1.3 typescript: ^4.6.2 dependencies: - '@wordpress/a11y': 2.15.3 + '@wordpress/a11y': 3.5.0 '@wordpress/data': 6.4.1 '@wordpress/notices': 3.4.1 devDependencies: @@ -928,9 +932,9 @@ importers: packages/js/number: specifiers: '@babel/core': ^7.17.5 + '@babel/runtime': ^7.17.2 '@wordpress/eslint-plugin': ^11.0.0 eslint: ^8.12.0 - '@babel/runtime': ^7.17.2 jest: ^27.5.1 jest-cli: ^27.5.1 locutus: ^2.0.16 @@ -941,9 +945,9 @@ importers: locutus: 2.0.16 devDependencies: '@babel/core': 7.17.8 + '@babel/runtime': 7.17.7 '@wordpress/eslint-plugin': 11.0.1_7c040a9b494a33cf8bf9079642892fb1 eslint: 8.12.0 - '@babel/runtime': 7.17.7 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -1007,7 +1011,7 @@ importers: specifiers: '@automattic/color-studio': ^2.5.0 '@babel/core': ^7.17.5 - '@wordpress/base-styles': ^3.6.0 + '@wordpress/base-styles': ^4.3.0 '@wordpress/eslint-plugin': ^11.0.0 '@wordpress/postcss-plugins-preset': ^1.6.0 css-loader: ^3.6.0 @@ -1025,7 +1029,7 @@ importers: webpack-rtl-plugin: ^2.0.0 dependencies: '@automattic/color-studio': 2.5.0 - '@wordpress/base-styles': 3.6.0 + '@wordpress/base-styles': 4.3.0 '@wordpress/postcss-plugins-preset': 1.6.0 css-loader: 3.6.0_webpack@5.70.0 mini-css-extract-plugin: 2.6.0_webpack@5.70.0 @@ -1093,15 +1097,13 @@ importers: chai-as-promised: 7.1.1 config: 3.3.3 cross-env: 6.0.3 - deasync: 0.1.21 + deasync: 0.1.26 eslint: 6.8.0 eslint-config-wpcalypso: 5.0.0 eslint-plugin-jest: 23.20.0 github-contributors-list: https://github.com/woocommerce/github-contributors-list/tarball/master - husky: 4.3.0 istanbul: 1.0.0-alpha.2 jest: ^25.1.0 - lint-staged: 9.5.0 mocha: 7.2.0 prettier: npm:wp-prettier@2.0.5 stylelint: ^13.8.0 @@ -1134,15 +1136,13 @@ importers: chai-as-promised: 7.1.1_chai@4.2.0 config: 3.3.3 cross-env: 6.0.3 - deasync: 0.1.21 + deasync: 0.1.26 eslint: 6.8.0 eslint-config-wpcalypso: 5.0.0_eslint@6.8.0 eslint-plugin-jest: 23.20.0_eslint@6.8.0+typescript@3.9.7 github-contributors-list: '@github.com/woocommerce/github-contributors-list/tarball/master' - husky: 4.3.0 istanbul: 1.0.0-alpha.2 jest: 25.5.4 - lint-staged: 9.5.0 mocha: 7.2.0 prettier: /wp-prettier/2.0.5 stylelint: 13.13.1 @@ -1226,11 +1226,10 @@ importers: '@woocommerce/onboarding': workspace:* '@woocommerce/style-build': workspace:* '@woocommerce/tracks': workspace:* - '@wordpress/a11y': ^2.15.3 + '@wordpress/a11y': ^3.5.0 '@wordpress/api-fetch': ^6.0.1 - '@wordpress/babel-plugin-makepot': ^2.1.3 '@wordpress/babel-preset-default': ^6.5.1 - '@wordpress/base-styles': ^3.6.0 + '@wordpress/base-styles': ^4.3.0 '@wordpress/browserslist-config': ^4.1.1 '@wordpress/components': ^19.5.0 '@wordpress/compose': ^5.1.2 @@ -1242,10 +1241,10 @@ importers: '@wordpress/dom-ready': ^3.3.1 '@wordpress/element': ^4.1.1 '@wordpress/eslint-plugin': ^11.0.0 - '@wordpress/hooks': ^2.12.3 + '@wordpress/hooks': ^3.5.0 '@wordpress/html-entities': ^3.3.1 '@wordpress/i18n': ^4.3.1 - '@wordpress/icons': ^6.3.0 + '@wordpress/icons': ^8.1.0 '@wordpress/jest-preset-default': ^8.0.1 '@wordpress/keycodes': ^3.3.1 '@wordpress/notices': ^3.3.2 @@ -1265,7 +1264,7 @@ importers: babel-loader: ^8.2.3 babel-plugin-transform-class-properties: ^6.24.1 babel-plugin-transform-es2015-template-literals: ^6.22.0 - chalk: ^5.0.0 + chalk: ^4.1.2 classnames: ^2.3.1 comment-parser: ^1.3.0 concurrently: ^7.0.0 @@ -1275,7 +1274,6 @@ importers: cross-env: ^7.0.3 css-loader: ^6.7.0 debug: ^4.3.3 - docsify-cli: ^4.4.3 dompurify: ^2.3.6 eslint: ^8.10.0 eslint-import-resolver-typescript: ^2.5.0 @@ -1288,15 +1286,10 @@ importers: github-label-sync: ^2.0.2 grapheme-splitter: ^1.0.4 gridicons: ^3.4.0 - grunt: ^1.4.1 - grunt-checktextdomain: ^1.0.1 - grunt-wp-i18n: ^1.0.3 history: ^4.10.1 - husky: ^7.0.0 jest: ^27.5.1 jest-environment-jsdom: ~27.5.0 jest-environment-node: ^27.5.1 - lint-staged: ^12.3.5 lodash: ^4.17.21 md5: ^2.3.0 memize: ^1.1.0 @@ -1348,9 +1341,9 @@ importers: '@woocommerce/api': link:../../packages/js/api '@woocommerce/e2e-environment': link:../../packages/js/e2e-environment '@woocommerce/e2e-utils': link:../../packages/js/e2e-utils - '@wordpress/a11y': 2.15.3 + '@wordpress/a11y': 3.5.0 '@wordpress/api-fetch': 6.1.1 - '@wordpress/base-styles': 3.6.0 + '@wordpress/base-styles': 4.3.0 '@wordpress/components': 19.6.1_978f344c876a57c1143ffe356b90df31 '@wordpress/compose': 5.2.1_react@17.0.2 '@wordpress/core-data': 4.2.1_react@17.0.2 @@ -1359,10 +1352,10 @@ importers: '@wordpress/dom': 3.4.1 '@wordpress/dom-ready': 3.4.1 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 2.12.3 + '@wordpress/hooks': 3.5.0 '@wordpress/html-entities': 3.4.1 '@wordpress/i18n': 4.4.1 - '@wordpress/icons': 6.3.0 + '@wordpress/icons': 8.1.0 '@wordpress/keycodes': 3.4.1 '@wordpress/notices': 3.4.1_react@17.0.2 '@wordpress/plugins': 4.2.1_react@17.0.2 @@ -1456,7 +1449,6 @@ importers: '@woocommerce/onboarding': link:../../packages/js/onboarding '@woocommerce/style-build': link:../../packages/js/style-build '@woocommerce/tracks': link:../../packages/js/tracks - '@wordpress/babel-plugin-makepot': 2.1.3_@babel+core@7.17.8 '@wordpress/babel-preset-default': 6.6.1 '@wordpress/browserslist-config': 4.1.2 '@wordpress/custom-templated-path-webpack-plugin': 2.1.2_webpack@5.70.0 @@ -1473,14 +1465,13 @@ importers: babel-loader: 8.2.3_fa907c5a4f16ccc493e21649ccc59574 babel-plugin-transform-class-properties: 6.24.1 babel-plugin-transform-es2015-template-literals: 6.22.0 - chalk: 5.0.1 + chalk: 4.1.2 comment-parser: 1.3.0 concurrently: 7.0.0 config: 3.3.7 copy-webpack-plugin: 10.2.4_webpack@5.70.0 cross-env: 7.0.3 css-loader: 6.7.1_webpack@5.70.0 - docsify-cli: 4.4.4 eslint: 8.11.0 eslint-import-resolver-typescript: 2.5.0_fe22d862ffeecaee86c93a006d59e41e eslint-import-resolver-webpack: 0.13.2_bac363bc2c2f46a65300020741b6cf5e @@ -1489,14 +1480,9 @@ importers: expose-loader: 3.1.0_webpack@5.70.0 fork-ts-checker-webpack-plugin: 6.5.0_10568ae13669cc833891d65cd6879aa0 fs-extra: 8.1.0 - grunt: 1.4.1 - grunt-checktextdomain: 1.0.1_grunt@1.4.1 - grunt-wp-i18n: 1.0.3 - husky: 7.0.4 jest: 27.5.1 jest-environment-jsdom: 27.5.1 jest-environment-node: 27.5.1 - lint-staged: 12.3.7 md5: 2.3.0 merge-config: 2.0.0 mini-css-extract-plugin: 2.6.0_webpack@5.70.0 @@ -1533,13 +1519,9 @@ importers: plugins/woocommerce-beta-tester: specifiers: eslint: 5.16.0 - husky: 1.3.1 - lint-staged: 8.1.5 uglify-js: ^3.5.3 devDependencies: eslint: 5.16.0 - husky: 1.3.1 - lint-staged: 8.1.5 uglify-js: 3.14.5 plugins/woocommerce/legacy: @@ -1775,7 +1757,7 @@ packages: '@babel/traverse': 7.16.3 '@babel/types': 7.16.0 convert-source-map: 1.8.0 - debug: 4.3.2 + debug: 4.3.3 gensync: 1.0.0-beta.2 json5: 2.2.0 semver: 6.3.0 @@ -3812,7 +3794,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.0 - '@babel/helper-plugin-utils': 7.16.7 + '@babel/helper-plugin-utils': 7.14.5 dev: true /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.16.12: @@ -3821,7 +3803,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.16.7 + '@babel/helper-plugin-utils': 7.14.5 /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.17.8: resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} @@ -3829,7 +3811,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.16.7 + '@babel/helper-plugin-utils': 7.14.5 /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.12.9: resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} @@ -3944,7 +3926,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.0 - '@babel/helper-plugin-utils': 7.16.7 + '@babel/helper-plugin-utils': 7.14.5 dev: true /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.16.12: @@ -3953,7 +3935,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.16.7 + '@babel/helper-plugin-utils': 7.14.5 /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.17.8: resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} @@ -3961,7 +3943,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.16.7 + '@babel/helper-plugin-utils': 7.14.5 /@babel/plugin-syntax-private-property-in-object/7.14.5_@babel+core@7.16.0: resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} @@ -6198,12 +6180,6 @@ packages: core-js-pure: 3.19.1 regenerator-runtime: 0.13.9 - /@babel/runtime/7.0.0: - resolution: {integrity: sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==} - dependencies: - regenerator-runtime: 0.12.1 - dev: true - /@babel/runtime/7.15.4: resolution: {integrity: sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==} engines: {node: '>=6.9.0'} @@ -6250,7 +6226,7 @@ packages: '@babel/helper-split-export-declaration': 7.16.0 '@babel/parser': 7.16.4 '@babel/types': 7.16.0 - debug: 4.3.2 + debug: 4.3.3 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6692,7 +6668,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.2 + debug: 4.3.3 espree: 9.0.0 globals: 13.12.0 ignore: 4.0.6 @@ -6772,7 +6748,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.2 + debug: 4.3.3 minimatch: 3.0.4 transitivePeerDependencies: - supports-color @@ -8808,27 +8784,6 @@ packages: lodash.merge: 4.6.2 postcss: 5.2.18 - /@samverschueren/stream-to-observable/0.3.1_rxjs@6.6.7: - resolution: {integrity: sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==} - engines: {node: '>=6'} - peerDependencies: - rxjs: '*' - zen-observable: '*' - peerDependenciesMeta: - rxjs: - optional: true - zen-observable: - optional: true - dependencies: - any-observable: 0.3.0 - rxjs: 6.6.7 - dev: true - - /@sindresorhus/is/0.14.0: - resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} - engines: {node: '>=6'} - dev: true - /@sindresorhus/is/4.6.0: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -11854,13 +11809,6 @@ packages: - supports-color dev: true - /@szmarczak/http-timer/1.1.2: - resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} - engines: {node: '>=6'} - dependencies: - defer-to-connect: 1.1.3 - dev: true - /@szmarczak/http-timer/4.0.6: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} @@ -13779,14 +13727,6 @@ packages: react: 17.0.2 dev: true - /@wordpress/a11y/2.15.3: - resolution: {integrity: sha512-uoCznHY3/TaNWeXutLI6juC198ykaBwZ34P51PNHHQqi3WzVoBhFx6AnAR/9Uupl3tZcekefpkVHy7AJHMAPIA==} - dependencies: - '@babel/runtime': 7.17.7 - '@wordpress/dom-ready': 2.13.2 - '@wordpress/i18n': 3.20.0 - dev: false - /@wordpress/a11y/3.4.1: resolution: {integrity: sha512-SjeLO8x/Y/QAcKBrvyJiu8KVAPckRLNwuFfgX7zCGM8vBfg+Depj94Hp55ARLjq0oXHg7EWKxSdzNkvmTz8AIA==} engines: {node: '>=12'} @@ -13796,6 +13736,15 @@ packages: '@wordpress/i18n': 4.4.1 dev: false + /@wordpress/a11y/3.5.0: + resolution: {integrity: sha512-pJyDexol4yFfUNs6BAW1IKftdxZBsxvNRpzmYcwXiFA+1jSnMJFehp0nu47skdzxiHS6CKyLqBks17J+a/GqGA==} + engines: {node: '>=12'} + dependencies: + '@babel/runtime': 7.17.7 + '@wordpress/dom-ready': 3.5.0 + '@wordpress/i18n': 4.5.0 + dev: false + /@wordpress/api-fetch/5.2.6: resolution: {integrity: sha512-AG8KdCHwtYJWR38AAU7nEI+UbumUSqSBthQj3rShLUVyFbYGkQdpwXJJG6vFj7FjIp41zljiyj3K1Fh3cqdaAw==} engines: {node: '>=12'} @@ -13865,17 +13814,6 @@ packages: dependencies: '@babel/core': 7.17.8 - /@wordpress/babel-plugin-makepot/2.1.3_@babel+core@7.17.8: - resolution: {integrity: sha512-8ijU4bYUmJuXPnHS47X9Y5OrESLmgx3VVGb+9tNO5hyPoXnZj+ELw9+SB4fJtg0Ur1MDNKRLz4ruJS4Y0tRnNQ==} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.17.8 - '@babel/runtime': 7.17.7 - gettext-parser: 1.4.0 - lodash: 4.17.21 - dev: true - /@wordpress/babel-preset-default/3.0.2: resolution: {integrity: sha512-bsa4piS4GU02isj2XJNUgSEC7MpzdYNy9wOFySrp8G6IHAvwrlwcPEXJf5EuwE8ZqTMmFAzPyKOHFEAx/j+J1A==} engines: {node: '>=8'} @@ -13953,6 +13891,10 @@ packages: /@wordpress/base-styles/3.6.0: resolution: {integrity: sha512-6/vXAmc9FSX7Y17UjKgUJoVU++Pv1U1G8uMx7iClRUaLetc7/jj2DD9PTyX/cdJjHr32e3yXuLVT9wfEbo6SEg==} + /@wordpress/base-styles/4.3.0: + resolution: {integrity: sha512-e9Z+txhEQ3zyAHkzzsuYg1ADFhKArz1eGU3ayqCNtCdakrgNjI6Q/sPODI26LlwTmjJPBIJ5wSCBrsDjMhdWqA==} + dev: false + /@wordpress/blob/3.4.1: resolution: {integrity: sha512-rGm7nXaxnsXStIu9v9IjbUOKtE9UzkvgYiJMX5SyVyzAGLOo2Aq759+JNRDLRR0RDkS6igH/G7qBXS6xSgLFgA==} engines: {node: '>=12'} @@ -13982,7 +13924,7 @@ packages: '@wordpress/deprecated': 3.4.1 '@wordpress/dom': 3.4.1 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 3.4.1 + '@wordpress/hooks': 3.5.0 '@wordpress/html-entities': 3.4.1 '@wordpress/i18n': 4.4.1 '@wordpress/is-shallow-equal': 4.4.1 @@ -14011,7 +13953,7 @@ packages: '@wordpress/deprecated': 3.4.1 '@wordpress/dom': 3.4.1 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 3.4.1 + '@wordpress/hooks': 3.5.0 '@wordpress/html-entities': 3.4.1 '@wordpress/i18n': 4.4.1 '@wordpress/is-shallow-equal': 4.4.1 @@ -14061,7 +14003,7 @@ packages: '@wordpress/dom': 3.4.1 '@wordpress/element': 4.2.1 '@wordpress/escape-html': 2.4.1 - '@wordpress/hooks': 3.4.1 + '@wordpress/hooks': 3.5.0 '@wordpress/i18n': 4.4.1 '@wordpress/icons': 8.0.1 '@wordpress/is-shallow-equal': 4.4.1 @@ -14114,7 +14056,7 @@ packages: '@wordpress/dom': 3.4.1 '@wordpress/element': 4.2.1 '@wordpress/escape-html': 2.4.1 - '@wordpress/hooks': 3.4.1 + '@wordpress/hooks': 3.5.0 '@wordpress/i18n': 4.4.1 '@wordpress/icons': 8.0.1 '@wordpress/is-shallow-equal': 4.4.1 @@ -14363,13 +14305,7 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.17.7 - '@wordpress/hooks': 3.4.1 - dev: false - - /@wordpress/dom-ready/2.13.2: - resolution: {integrity: sha512-COH7n2uZfBq4FtluSbl37N3nCEcdMXzV42ETCWKUcumiP1Zd3qnkfQKcsxTaHWY8aVt/358RvJ7ghWe3xAd+fg==} - dependencies: - '@babel/runtime': 7.17.7 + '@wordpress/hooks': 3.5.0 dev: false /@wordpress/dom-ready/3.4.1: @@ -14379,6 +14315,13 @@ packages: '@babel/runtime': 7.17.7 dev: false + /@wordpress/dom-ready/3.5.0: + resolution: {integrity: sha512-xhxZx3qH0UoWI3AMvZpB7NnKkHR5m5ifrBlinXM3+kSPQ8bIUkuOi2cFYdCnglPi0a+dd7OahWKFzXwDvgjO1w==} + engines: {node: '>=12'} + dependencies: + '@babel/runtime': 7.17.7 + dev: false + /@wordpress/dom/3.4.1: resolution: {integrity: sha512-fySHKew0GEIXC4HsIlHyszivV9qAQZcD+oHWn3N0MjaUYKvBRfh6jeBOQwbeBGR/32U+TX8Bqa/qAmDb8bMsyA==} engines: {node: '>=12'} @@ -14490,6 +14433,19 @@ packages: react: 17.0.2 react-dom: 17.0.2_react@17.0.2 + /@wordpress/element/4.3.0: + resolution: {integrity: sha512-QN5qsNN6kzbHgrCL9CG2877iOu01KMEwls1K3iKk43EQ8hr/D/Ms/h5TqfOgF6oIGUR/QUlbeZQJs4zdvEnFOg==} + engines: {node: '>=12'} + dependencies: + '@babel/runtime': 7.17.7 + '@types/react': 17.0.40 + '@types/react-dom': 17.0.13 + '@wordpress/escape-html': 2.5.0 + lodash: 4.17.21 + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + dev: false + /@wordpress/escape-html/1.12.2: resolution: {integrity: sha512-FabgSwznhdaUwe6hr1CsGpgxQbzqEoGevv73WIL1B9GvlZ6csRWodgHfWh4P6fYqpzxFL4WYB8wPJ1PdO32XFA==} dependencies: @@ -14509,6 +14465,13 @@ packages: dependencies: '@babel/runtime': 7.17.7 + /@wordpress/escape-html/2.5.0: + resolution: {integrity: sha512-WV4jI6uBPZNxxOQdftiOsx1WgimkjxnwCfx6T+K7Ltfnm78Q5q2P5R98twGOqSVI/rPqtZubv9e7oMDbpp4H2w==} + engines: {node: '>=12'} + dependencies: + '@babel/runtime': 7.17.7 + dev: false + /@wordpress/eslint-plugin/11.0.1_2205da2c9bef219d53091cb9dbc5524c: resolution: {integrity: sha512-HDKwKjOmCaWdyJEtWKRAd0xK/NAXL/ykUP/I8l+zCvzvCXbS1UuixWN09RRzl09tv17JUtPiEqehDilkWRCBZg==} engines: {node: '>=12', npm: '>=6.9'} @@ -14716,6 +14679,13 @@ packages: dependencies: '@babel/runtime': 7.17.7 + /@wordpress/hooks/3.5.0: + resolution: {integrity: sha512-6Ko+1rWLq75s2LeZah6e0sJC5lC2nL1M+DDLlP/EZ+YCGZlIKoCvkhVBuCdI2wgIHRPXU56OqLvw85rUjsfDJw==} + engines: {node: '>=12'} + dependencies: + '@babel/runtime': 7.17.7 + dev: false + /@wordpress/html-entities/3.4.1: resolution: {integrity: sha512-wSuwgONTefnhCB9B7mKS+e8islHuCkprfDc+FhqVAa6r5RbVBGvaHUJs8embgdtww7MwBRMnskNf/buQ8Jr02A==} engines: {node: '>=12'} @@ -14749,13 +14719,18 @@ packages: sprintf-js: 1.1.2 tannin: 1.2.0 - /@wordpress/icons/6.3.0: - resolution: {integrity: sha512-Vliw7QsFuTsrA05GZov4i3PQiLQOGO97PR2keUeY53fVZdeoJKv/nfDqOZxZCIts5jR2Mfje6P6hc/KlurxsKg==} + /@wordpress/i18n/4.5.0: + resolution: {integrity: sha512-BhOHsgnbWeUWT+23P/vksen0MiZ+OyhemZkKUHtQnelRKY4FnFEYvjp5q9v/O7DY3J0Hqc+Ss4wLNqajRQmMIw==} engines: {node: '>=12'} + hasBin: true dependencies: '@babel/runtime': 7.17.7 - '@wordpress/element': 4.2.1 - '@wordpress/primitives': 3.2.1 + '@wordpress/hooks': 3.5.0 + gettext-parser: 1.4.0 + lodash: 4.17.21 + memize: 1.1.0 + sprintf-js: 1.1.2 + tannin: 1.2.0 dev: false /@wordpress/icons/8.0.1: @@ -14767,6 +14742,15 @@ packages: '@wordpress/primitives': 3.2.1 dev: false + /@wordpress/icons/8.1.0: + resolution: {integrity: sha512-fNq0Mnzzf03uxIwKqQeU/G48wElyypwkhcBZWYQRpmwLZrOR231dxUeK9mzPOSGlYbgM+YKK+k3lzysGmrJU0A==} + engines: {node: '>=12'} + dependencies: + '@babel/runtime': 7.17.7 + '@wordpress/element': 4.3.0 + '@wordpress/primitives': 3.3.0 + dev: false + /@wordpress/is-shallow-equal/4.4.1: resolution: {integrity: sha512-NlcqqrukKe4zT5fCs3O5FVYwqmHhtqM//KqWs7xfIaoz9B07oKZQNZqOrU72mgz7mgRliQumTQHzFM76RO0hZQ==} engines: {node: '>=12'} @@ -14950,8 +14934,8 @@ packages: '@babel/runtime': 7.17.7 '@wordpress/compose': 5.2.1_react@17.0.2 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 3.4.1 - '@wordpress/icons': 8.0.1 + '@wordpress/hooks': 3.5.0 + '@wordpress/icons': 8.1.0 lodash: 4.17.21 memize: 1.1.0 react: 17.0.2 @@ -15004,6 +14988,15 @@ packages: classnames: 2.3.1 dev: false + /@wordpress/primitives/3.3.0: + resolution: {integrity: sha512-iwlFGSaI2RnQF0SxsWJ3KaM0LPdUosI5mb9879JXOh/vAFVObrQdyk5Fv+++vGUzDfxRnxAH68UpJi7nOzcRRA==} + engines: {node: '>=12'} + dependencies: + '@babel/runtime': 7.17.7 + '@wordpress/element': 4.3.0 + classnames: 2.3.1 + dev: false + /@wordpress/priority-queue/2.4.1: resolution: {integrity: sha512-5+pyUvQCQTTkoiccnO5G6AUDxzCKdAiDh3oLbl+qLz3j56iGuLoKWR6L5ySj+knaYIZb4g8expFsbvf2+RcVtw==} engines: {node: '>=12'} @@ -15644,11 +15637,6 @@ packages: engines: {node: '>=12'} dev: true - /ansi-styles/0.2.0: - resolution: {integrity: sha1-NZq0sV3NZLptdHNLcsNjYKmvLBk=} - engines: {node: '>=0.8.0'} - dev: true - /ansi-styles/2.2.1: resolution: {integrity: sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=} engines: {node: '>=0.10.0'} @@ -15690,11 +15678,6 @@ packages: engines: {node: '>=12.13'} dev: false - /any-observable/0.3.0: - resolution: {integrity: sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==} - engines: {node: '>=6'} - dev: true - /anymatch/2.0.0: resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} dependencies: @@ -17101,20 +17084,6 @@ packages: widest-line: 2.0.1 dev: true - /boxen/4.2.0: - resolution: {integrity: sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==} - engines: {node: '>=8'} - dependencies: - ansi-align: 3.0.1 - camelcase: 5.3.1 - chalk: 3.0.0 - cli-boxes: 2.2.1 - string-width: 4.2.3 - term-size: 2.2.1 - type-fest: 0.8.1 - widest-line: 3.1.0 - dev: true - /boxen/5.1.2: resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} engines: {node: '>=10'} @@ -17451,19 +17420,6 @@ packages: engines: {node: '>=10.6.0'} dev: false - /cacheable-request/6.1.0: - resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} - engines: {node: '>=8'} - dependencies: - clone-response: 1.0.2 - get-stream: 5.2.0 - http-cache-semantics: 4.1.0 - keyv: 3.1.0 - lowercase-keys: 2.0.0 - normalize-url: 4.5.1 - responselike: 1.0.2 - dev: true - /cacheable-request/7.0.2: resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==} engines: {node: '>=8'} @@ -17636,14 +17592,6 @@ packages: type-detect: 4.0.8 dev: true - /chalk/0.2.1: - resolution: {integrity: sha1-dhPhV1FFshOGSD9/SFql/6jL0Qw=} - engines: {node: '>=0.8.0'} - dependencies: - ansi-styles: 0.2.0 - has-color: 0.1.7 - dev: true - /chalk/1.1.3: resolution: {integrity: sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=} engines: {node: '>=0.10.0'} @@ -17684,11 +17632,6 @@ packages: ansi-styles: 4.3.0 supports-color: 7.2.0 - /chalk/5.0.1: - resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: true - /change-case/2.3.1: resolution: {integrity: sha1-LE/ePwY7tB0AzWjg1aCdthy+iU8=} dependencies: @@ -17979,14 +17922,6 @@ packages: colors: 1.4.0 dev: true - /cli-truncate/0.2.1: - resolution: {integrity: sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=} - engines: {node: '>=0.10.0'} - dependencies: - slice-ansi: 0.0.4 - string-width: 1.0.2 - dev: true - /cli-truncate/2.1.0: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} @@ -18083,6 +18018,7 @@ packages: resolution: {integrity: sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=} dependencies: mimic-response: 1.0.1 + dev: false /clone-stats/1.0.0: resolution: {integrity: sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=} @@ -18318,10 +18254,6 @@ packages: resolution: {integrity: sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=} dev: true - /compare-versions/3.6.0: - resolution: {integrity: sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==} - dev: true - /component-emitter/1.3.0: resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} @@ -18404,37 +18336,11 @@ packages: xdg-basedir: 3.0.0 dev: true - /configstore/5.0.1: - resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} - engines: {node: '>=8'} - dependencies: - dot-prop: 5.3.0 - graceful-fs: 4.2.9 - make-dir: 3.1.0 - unique-string: 2.0.0 - write-file-atomic: 3.0.3 - xdg-basedir: 4.0.0 - dev: true - /connect-history-api-fallback/1.6.0: resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==} engines: {node: '>=0.8'} dev: true - /connect-livereload/0.6.1: - resolution: {integrity: sha512-3R0kMOdL7CjJpU66fzAkCe6HNtd3AavCS4m+uW4KtJjrdGPT0SQEZieAYd+cm+lJoBznNQ4lqipYWkhBMgk00g==} - dev: true - - /connect/3.7.0: - resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} - engines: {node: '>= 0.10.0'} - dependencies: - debug: 2.6.9 - finalhandler: 1.1.2 - parseurl: 1.3.3 - utils-merge: 1.0.1 - dev: true - /console-browserify/1.2.0: resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} dev: true @@ -18759,11 +18665,6 @@ packages: engines: {node: '>=4'} dev: true - /crypto-random-string/2.0.0: - resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} - engines: {node: '>=8'} - dev: true - /css-color-function/1.3.3: resolution: {integrity: sha1-jtJMLAIFBzM5+voAS8jBQfzLKC4=} dependencies: @@ -19306,10 +19207,6 @@ packages: whatwg-url: 8.7.0 dev: true - /date-fns/1.30.1: - resolution: {integrity: sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==} - dev: true - /date-fns/2.28.0: resolution: {integrity: sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==} engines: {node: '>=0.11'} @@ -19322,8 +19219,8 @@ packages: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} dev: true - /deasync/0.1.21: - resolution: {integrity: sha512-kUmM8Y+PZpMpQ+B4AuOW9k2Pfx/mSupJtxOsLzmnHY2WqZUYRFccFn2RhzPAqt3Xb+sorK/badW2D4zNzqZz5w==} + /deasync/0.1.26: + resolution: {integrity: sha512-YKw0BmJSWxkjtQsbgn6Q9CHSWB7DKMen8vKrgyC006zy0UZ6nWyGidB0IzZgqkVRkOglAeUaFtiRTeLyel72bg==} engines: {node: '>=0.11.0'} requiresBuild: true dependencies: @@ -19388,7 +19285,7 @@ packages: ms: 2.1.2 supports-color: 8.1.1 - /debug/4.3.3_supports-color@9.2.1: + /debug/4.3.3_supports-color@9.2.2: resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} engines: {node: '>=6.0'} peerDependencies: @@ -19398,7 +19295,7 @@ packages: optional: true dependencies: ms: 2.1.2 - supports-color: 9.2.1 + supports-color: 9.2.2 dev: true /debuglog/1.0.1: @@ -19425,13 +19322,6 @@ packages: resolution: {integrity: sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=} engines: {node: '>=0.10'} - /decompress-response/3.3.0: - resolution: {integrity: sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=} - engines: {node: '>=4'} - dependencies: - mimic-response: 1.0.1 - dev: true - /decompress-response/6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -19505,10 +19395,6 @@ packages: dependencies: clone: 1.0.4 - /defer-to-connect/1.1.3: - resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} - dev: true - /defer-to-connect/2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} @@ -19544,18 +19430,6 @@ packages: is-descriptor: 1.0.2 isobject: 3.0.1 - /del/3.0.0: - resolution: {integrity: sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=} - engines: {node: '>=4'} - dependencies: - globby: 6.1.0 - is-path-cwd: 1.0.0 - is-path-in-cwd: 1.0.1 - p-map: 1.2.0 - pify: 3.0.0 - rimraf: 2.7.1 - dev: true - /del/4.1.1: resolution: {integrity: sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==} engines: {node: '>=6'} @@ -19569,20 +19443,6 @@ packages: rimraf: 2.7.1 dev: true - /del/5.1.0: - resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} - engines: {node: '>=8'} - dependencies: - globby: 10.0.1 - graceful-fs: 4.2.9 - is-glob: 4.0.3 - is-path-cwd: 2.2.0 - is-path-inside: 3.0.3 - p-map: 3.0.0 - rimraf: 3.0.2 - slash: 3.0.0 - dev: true - /del/6.0.0: resolution: {integrity: sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==} engines: {node: '>=10'} @@ -19764,62 +19624,6 @@ packages: buffer-indexof: 1.1.1 dev: true - /docsify-cli/4.4.4: - resolution: {integrity: sha512-NAZgg6b0BsDuq/Pe+P19Qb2J1d+ZVbS0eGkeCNxyu4F9/CQSsRqZqAvPJ9/0I+BCHn4sgftA2jluqhQVzKzrSA==} - engines: {node: '>= 10', npm: '>= 6'} - hasBin: true - dependencies: - chalk: 2.4.2 - connect: 3.7.0 - connect-history-api-fallback: 1.6.0 - connect-livereload: 0.6.1 - cp-file: 7.0.0 - docsify: 4.12.2 - docsify-server-renderer: 4.12.2 - enquirer: 2.3.6 - fs-extra: 8.1.0 - get-port: 5.1.1 - livereload: 0.9.3 - lru-cache: 5.1.1 - open: 6.4.0 - serve-static: 1.14.1 - update-notifier: 4.1.3 - yargonaut: 1.1.4 - yargs: 15.4.1 - transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - utf-8-validate - dev: true - - /docsify-server-renderer/4.12.2: - resolution: {integrity: sha512-/sCq0U0iGvc8mNN6VC5SeodiHUsA98rMsMFYXtQbWsS/jWArkSee8ATlH5KzGDJ/zjf9QOFrkjoanHCNaFWiPQ==} - dependencies: - debug: 4.3.3 - docsify: 4.12.2 - dompurify: 2.3.6 - node-fetch: 2.6.7 - resolve-pathname: 3.0.0 - transitivePeerDependencies: - - encoding - - supports-color - dev: true - - /docsify/4.12.2: - resolution: {integrity: sha512-hpRez5upcvkYigT2zD8P5kH5t9HpSWL8yn/ZU/g04/WfAfxVNW6CPUVOOF1EsQUDxTRuyNTFOb6uUv+tPij3tg==} - requiresBuild: true - dependencies: - dompurify: 2.3.6 - marked: 1.2.9 - medium-zoom: 1.0.6 - opencollective-postinstall: 2.0.3 - prismjs: 1.27.0 - strip-indent: 3.0.0 - tinydate: 1.3.0 - tweezer.js: 1.5.0 - dev: true - /doctrine/2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -19911,6 +19715,7 @@ packages: /dompurify/2.3.6: resolution: {integrity: sha512-OFP2u/3T1R5CEgWCEONuJ1a5+MFKnOYpkywpUSxv/dj1LeBT1erK+JwM7zK0ROy2BRhqVCf0LRw/kHqKuMkVGg==} + dev: false /domutils/1.7.0: resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} @@ -20062,11 +19867,6 @@ packages: /electron-to-chromium/1.4.88: resolution: {integrity: sha512-oA7mzccefkvTNi9u7DXmT0LqvhnOiN2BhSrKerta7HeUC1cLoIwtbf2wL+Ah2ozh5KQd3/1njrGrwDBXx6d14Q==} - /elegant-spinner/1.0.1: - resolution: {integrity: sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=} - engines: {node: '>=0.10.0'} - dev: true - /element-resize-detector/1.2.4: resolution: {integrity: sha512-Fl5Ftk6WwXE0wqCgNoseKWndjzZlDCwuPTcoVZfCP9R3EHQF8qUtr3YUPNETegRBOKqQKPW3n4kiIWngGi8tKg==} dependencies: @@ -20454,11 +20254,6 @@ packages: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} - /escape-goat/2.1.1: - resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==} - engines: {node: '>=8'} - dev: true - /escape-html/1.0.3: resolution: {integrity: sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=} dev: true @@ -21989,21 +21784,6 @@ packages: signal-exit: 3.0.7 strip-eof: 1.0.0 - /execa/2.1.0: - resolution: {integrity: sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==} - engines: {node: ^8.12.0 || >=9.7.0} - dependencies: - cross-spawn: 7.0.3 - get-stream: 5.2.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 3.1.0 - onetime: 5.1.2 - p-finally: 2.0.1 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - dev: true - /execa/3.4.0: resolution: {integrity: sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==} engines: {node: ^8.12.0 || >=9.7.0} @@ -22362,11 +22142,6 @@ packages: resolution: {integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==} dev: true - /figlet/1.5.2: - resolution: {integrity: sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==} - engines: {node: '>= 0.4.0'} - dev: true - /figures/1.7.0: resolution: {integrity: sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=} engines: {node: '>=0.10.0'} @@ -22576,13 +22351,6 @@ packages: path-exists: 4.0.0 dev: true - /find-versions/3.2.0: - resolution: {integrity: sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==} - engines: {node: '>=6'} - dependencies: - semver-regex: 2.0.0 - dev: true - /find-yarn-workspace-root/2.0.0: resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} dependencies: @@ -22623,16 +22391,6 @@ packages: resolve-dir: 1.0.1 dev: true - /findup-sync/4.0.0: - resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} - engines: {node: '>= 8'} - dependencies: - detect-file: 1.0.0 - is-glob: 4.0.3 - micromatch: 4.0.4 - resolve-dir: 1.0.1 - dev: true - /findup/0.1.5: resolution: {integrity: sha1-itkpozk7rGJ5V6fl3kYjsGsOLOs=} engines: {node: '>=0.6'} @@ -22717,11 +22475,6 @@ packages: readable-stream: 2.3.7 dev: true - /fn-name/2.0.1: - resolution: {integrity: sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=} - engines: {node: '>=0.10.0'} - dev: true - /follow-redirects/1.14.5: resolution: {integrity: sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==} engines: {node: '>=4.0'} @@ -23129,17 +22882,6 @@ packages: engines: {node: '>=6'} dev: true - /g-status/2.0.2: - resolution: {integrity: sha512-kQoE9qH+T1AHKgSSD0Hkv98bobE90ILQcXAF4wvGgsr7uFqNvwmh8j+Lq3l0RVt3E3HjSbv2B9biEGcEtpHLCA==} - engines: {node: '>=6'} - dependencies: - arrify: 1.0.1 - matcher: 1.1.1 - simple-git: 1.132.0 - transitivePeerDependencies: - - supports-color - dev: true - /gauge/3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} @@ -23206,19 +22948,10 @@ packages: has: 1.0.3 has-symbols: 1.0.2 - /get-own-enumerable-property-symbols/3.0.2: - resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} - dev: true - /get-package-type/0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} - /get-port/5.1.1: - resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} - engines: {node: '>=8'} - dev: true - /get-stdin/4.0.1: resolution: {integrity: sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=} engines: {node: '>=0.10.0'} @@ -23288,14 +23021,6 @@ packages: encoding: 0.1.13 safe-buffer: 5.2.1 - /gettext-parser/3.1.1: - resolution: {integrity: sha512-vNhWcqXEtZPs5Ft1ReA34g7ByWotpcOIeJvXVy2jF3/G2U9v6W0wG4Z4hXzcU8R//jArqkgHcVCGgGqa4vxVlQ==} - dependencies: - encoding: 0.1.13 - readable-stream: 3.6.0 - safe-buffer: 5.2.1 - dev: true - /github-label-sync/2.0.2: resolution: {integrity: sha512-xDxlGG6s9LVfMNQexatne0bMUrwyYyTma9cC04b82zbEMFoy8rxSlag4eUYYF++ThMxvJp577Wk+uAv0mjRsNg==} engines: {node: '>=12'} @@ -23429,13 +23154,6 @@ packages: ini: 1.3.8 dev: true - /global-dirs/2.1.0: - resolution: {integrity: sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==} - engines: {node: '>=8'} - dependencies: - ini: 1.3.7 - dev: true - /global-modules/0.2.3: resolution: {integrity: sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=} engines: {node: '>=0.10.0'} @@ -23664,23 +23382,6 @@ packages: url-parse-lax: 1.0.0 dev: true - /got/9.6.0: - resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} - engines: {node: '>=8.6'} - dependencies: - '@sindresorhus/is': 0.14.0 - '@szmarczak/http-timer': 1.1.2 - cacheable-request: 6.1.0 - decompress-response: 3.3.0 - duplexer3: 0.1.4 - get-stream: 4.1.0 - lowercase-keys: 1.0.1 - mimic-response: 1.0.1 - p-cancelable: 1.1.0 - to-readable-stream: 1.0.0 - url-parse-lax: 3.0.0 - dev: true - /graceful-fs/4.2.8: resolution: {integrity: sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==} @@ -23730,17 +23431,6 @@ packages: /growly/1.3.0: resolution: {integrity: sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=} - /grunt-checktextdomain/1.0.1_grunt@1.4.1: - resolution: {integrity: sha1-slTQHh3pEwBdTbHFMD2QI7mD4Zs=} - engines: {node: '>= 0.8.0'} - peerDependencies: - grunt: '>=0.4.1' - dependencies: - chalk: 0.2.1 - grunt: 1.4.1 - text-table: 0.2.0 - dev: true - /grunt-cli/1.3.2: resolution: {integrity: sha512-8OHDiZZkcptxVXtMfDxJvmN7MVJNE8L/yIcPb4HB7TlyFD1kDvjHrb62uhySsU14wJx9ORMnTuhRMQ40lH/orQ==} engines: {node: '>=4'} @@ -23753,18 +23443,6 @@ packages: v8flags: 3.1.3 dev: true - /grunt-cli/1.4.3: - resolution: {integrity: sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==} - engines: {node: '>=10'} - hasBin: true - dependencies: - grunt-known-options: 2.0.0 - interpret: 1.1.0 - liftup: 3.0.1 - nopt: 4.0.3 - v8flags: 3.2.0 - dev: true - /grunt-contrib-clean/2.0.0_grunt@1.3.0: resolution: {integrity: sha512-g5ZD3ORk6gMa5ugZosLDQl3dZO7cI3R14U75hTM+dVLVxdMNJCPVmwf9OUt4v4eWgpKKWWoVK9DZc1amJp4nQw==} engines: {node: '>=6'} @@ -23829,11 +23507,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /grunt-known-options/2.0.0: - resolution: {integrity: sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==} - engines: {node: '>=0.10.0'} - dev: true - /grunt-legacy-log-utils/2.1.0: resolution: {integrity: sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==} engines: {node: '>=10'} @@ -23928,14 +23601,6 @@ packages: stylelint: 13.8.0 dev: true - /grunt-wp-i18n/1.0.3: - resolution: {integrity: sha512-CJNbEKeBeOSAPeaJ9B8iCgSwtaG63UR9/uT46a4OsIqnFhOJpeAi138JTlvjfIbnDVoBrzvdrKJe1svveLjUtA==} - engines: {node: '>=0.12.0'} - dependencies: - grunt: 1.4.1 - node-wp-i18n: 1.2.6 - dev: true - /grunt/1.3.0: resolution: {integrity: sha512-6ILlMXv11/4cxuhSMfSU+SfvbxrPuqZrAtLN64+tZpQ3DAKfSQPQHRbTjSbdtxfyQhGZPtN0bDZJ/LdCM5WXXA==} engines: {node: '>=8'} @@ -23958,28 +23623,6 @@ packages: rimraf: 3.0.2 dev: true - /grunt/1.4.1: - resolution: {integrity: sha512-ZXIYXTsAVrA7sM+jZxjQdrBOAg7DyMUplOMhTaspMRExei+fD0BTwdWXnn0W5SXqhb/Q/nlkzXclSi3IH55PIA==} - engines: {node: '>=8'} - hasBin: true - dependencies: - dateformat: 3.0.3 - eventemitter2: 0.4.14 - exit: 0.1.2 - findup-sync: 0.3.0 - glob: 7.1.7 - grunt-cli: 1.4.3 - grunt-known-options: 2.0.0 - grunt-legacy-log: 3.0.0 - grunt-legacy-util: 2.0.1 - iconv-lite: 0.4.24 - js-yaml: 3.14.1 - minimatch: 3.0.4 - mkdirp: 1.0.4 - nopt: 3.0.6 - rimraf: 3.0.2 - dev: true - /gruntify-eslint/5.0.0_grunt@1.3.0: resolution: {integrity: sha512-pa2sXHK9+U4dCGdGSIMkpJARNwRStdLBsddNxmSHSSWROUdhWMrXvFWm6pj48zJhyV3Qy068VIuF1seYIvc0cw==} engines: {node: '>=0.10.0'} @@ -24054,11 +23697,6 @@ packages: /has-bigints/1.0.1: resolution: {integrity: sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==} - /has-color/0.1.7: - resolution: {integrity: sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=} - engines: {node: '>=0.10.0'} - dev: true - /has-flag/1.0.0: resolution: {integrity: sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=} engines: {node: '>=0.10.0'} @@ -24119,11 +23757,6 @@ packages: is-number: 3.0.0 kind-of: 4.0.0 - /has-yarn/2.1.0: - resolution: {integrity: sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==} - engines: {node: '>=8'} - dev: true - /has/1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} @@ -24616,42 +24249,6 @@ packages: ms: 2.1.3 dev: true - /husky/1.3.1: - resolution: {integrity: sha512-86U6sVVVf4b5NYSZ0yvv88dRgBSSXXmHaiq5pP4KDj5JVzdwKgBjEtUPOm8hcoytezFwbU+7gotXNhpHdystlg==} - engines: {node: '>=6'} - hasBin: true - requiresBuild: true - dependencies: - cosmiconfig: 5.2.1 - execa: 1.0.0 - find-up: 3.0.0 - get-stdin: 6.0.0 - is-ci: 2.0.0 - pkg-dir: 3.0.0 - please-upgrade-node: 3.2.0 - read-pkg: 4.0.1 - run-node: 1.0.0 - slash: 2.0.0 - dev: true - - /husky/4.3.0: - resolution: {integrity: sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA==} - engines: {node: '>=10'} - hasBin: true - requiresBuild: true - dependencies: - chalk: 4.1.2 - ci-info: 2.0.0 - compare-versions: 3.6.0 - cosmiconfig: 7.0.1 - find-versions: 3.2.0 - opencollective-postinstall: 2.0.3 - pkg-dir: 4.2.0 - please-upgrade-node: 3.2.0 - slash: 3.0.0 - which-pm-runs: 1.0.0 - dev: true - /husky/7.0.4: resolution: {integrity: sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==} engines: {node: '>=12'} @@ -24839,11 +24436,6 @@ packages: resolution: {integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o=} engines: {node: '>=0.8.19'} - /indent-string/3.2.0: - resolution: {integrity: sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=} - engines: {node: '>=4'} - dev: true - /indent-string/4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -24872,10 +24464,6 @@ packages: /inherits/2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - /ini/1.3.7: - resolution: {integrity: sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==} - dev: true - /ini/1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -24916,7 +24504,7 @@ packages: react-devtools-core: 4.22.0 react-reconciler: 0.26.2_react@17.0.2 scheduler: 0.20.2 - signal-exit: 3.0.5 + signal-exit: 3.0.7 slice-ansi: 3.0.0 stack-utils: 2.0.5 string-width: 4.2.3 @@ -25323,14 +24911,6 @@ packages: is-path-inside: 1.0.1 dev: true - /is-installed-globally/0.3.2: - resolution: {integrity: sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==} - engines: {node: '>=8'} - dependencies: - global-dirs: 2.1.0 - is-path-inside: 3.0.3 - dev: true - /is-interactive/1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -25370,11 +24950,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /is-npm/4.0.0: - resolution: {integrity: sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==} - engines: {node: '>=8'} - dev: true - /is-number-object/1.0.6: resolution: {integrity: sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==} engines: {node: '>= 0.4'} @@ -25404,30 +24979,11 @@ packages: resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} dev: true - /is-observable/1.1.0: - resolution: {integrity: sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==} - engines: {node: '>=4'} - dependencies: - symbol-observable: 1.2.0 - dev: true - - /is-path-cwd/1.0.0: - resolution: {integrity: sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=} - engines: {node: '>=0.10.0'} - dev: true - /is-path-cwd/2.2.0: resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} engines: {node: '>=6'} dev: true - /is-path-in-cwd/1.0.1: - resolution: {integrity: sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==} - engines: {node: '>=0.10.0'} - dependencies: - is-path-inside: 1.0.1 - dev: true - /is-path-in-cwd/2.1.0: resolution: {integrity: sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==} engines: {node: '>=6'} @@ -25488,10 +25044,6 @@ packages: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} dev: true - /is-promise/2.2.2: - resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} - dev: true - /is-promise/4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} dev: false @@ -25514,11 +25066,6 @@ packages: call-bind: 1.0.2 has-tostringtag: 1.0.0 - /is-regexp/1.0.0: - resolution: {integrity: sha1-/S2INUXEa6xaYz57mgnof6LLUGk=} - engines: {node: '>=0.10.0'} - dev: true - /is-regexp/2.1.0: resolution: {integrity: sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==} engines: {node: '>=6'} @@ -25643,10 +25190,6 @@ packages: dependencies: is-docker: 2.2.1 - /is-yarn-global/0.3.0: - resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==} - dev: true - /is/3.3.0: resolution: {integrity: sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==} dev: false @@ -25833,7 +25376,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.3.2 + debug: 4.3.3 istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -28133,10 +27676,6 @@ packages: engines: {node: '>=4'} hasBin: true - /json-buffer/3.0.0: - resolution: {integrity: sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=} - dev: true - /json-buffer/3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: false @@ -28242,12 +27781,6 @@ packages: resolution: {integrity: sha512-X00TokkRIDotUIf3EV4xUm6ELc/IkqhS/vPSHdWnsM5y0HoNMfEqrazizI7g78lpHvnRSRt/PFfKtRqJCOGIuQ==} dev: true - /keyv/3.1.0: - resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} - dependencies: - json-buffer: 3.0.0 - dev: true - /keyv/4.1.1: resolution: {integrity: sha512-tGv1yP6snQVDSM4X6yxrv2zzq/EvpW+oYiUz6aueW1u9CtS8RzUQYxxmFwgZlO2jSgCxQbchhxaqXXp2hnKGpQ==} dependencies: @@ -28321,13 +27854,6 @@ packages: package-json: 4.0.1 dev: true - /latest-version/5.1.0: - resolution: {integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==} - engines: {node: '>=8'} - dependencies: - package-json: 6.5.0 - dev: true - /lazy-cache/0.2.7: resolution: {integrity: sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=} engines: {node: '>=0.10.0'} @@ -28427,20 +27953,6 @@ packages: resolve: 1.20.0 dev: true - /liftup/3.0.1: - resolution: {integrity: sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==} - engines: {node: '>=10'} - dependencies: - extend: 3.0.2 - findup-sync: 4.0.0 - fined: 1.2.0 - flagged-respawn: 1.0.1 - is-plain-object: 2.0.4 - object.map: 1.0.1 - rechoir: 0.7.1 - resolve: 1.20.0 - dev: true - /lilconfig/2.0.4: resolution: {integrity: sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==} engines: {node: '>=10'} @@ -28463,7 +27975,7 @@ packages: cli-truncate: 3.1.0 colorette: 2.0.16 commander: 8.3.0 - debug: 4.3.3_supports-color@9.2.1 + debug: 4.3.3_supports-color@9.2.2 execa: 5.1.1 lilconfig: 2.0.4 listr2: 4.0.5 @@ -28472,123 +27984,17 @@ packages: object-inspect: 1.12.0 pidtree: 0.5.0 string-argv: 0.3.1 - supports-color: 9.2.1 + supports-color: 9.2.2 yaml: 1.10.2 transitivePeerDependencies: - enquirer dev: true - /lint-staged/8.1.5: - resolution: {integrity: sha512-e5ZavfnSLcBJE1BTzRTqw6ly8OkqVyO3GL2M6teSmTBYQ/2BuueD5GIt2RPsP31u/vjKdexUyDCxSyK75q4BDA==} - hasBin: true - dependencies: - chalk: 2.4.2 - commander: 2.20.3 - cosmiconfig: 5.2.1 - debug: 3.2.7 - dedent: 0.7.0 - del: 3.0.0 - execa: 1.0.0 - find-parent-dir: 0.3.1 - g-status: 2.0.2 - is-glob: 4.0.3 - is-windows: 1.0.2 - listr: 0.14.3 - listr-update-renderer: 0.5.0_listr@0.14.3 - lodash: 4.17.21 - log-symbols: 2.2.0 - micromatch: 3.1.10 - npm-which: 3.0.1 - p-map: 1.2.0 - path-is-inside: 1.0.2 - pify: 3.0.0 - please-upgrade-node: 3.2.0 - staged-git-files: 1.1.2 - string-argv: 0.0.2 - stringify-object: 3.3.0 - yup: 0.26.10 - transitivePeerDependencies: - - supports-color - - zen-observable - dev: true - - /lint-staged/9.5.0: - resolution: {integrity: sha512-nawMob9cb/G1J98nb8v3VC/E8rcX1rryUYXVZ69aT9kde6YWX+uvNOEHY5yf2gcWcTJGiD0kqXmCnS3oD75GIA==} - hasBin: true - dependencies: - chalk: 2.4.2 - commander: 2.20.3 - cosmiconfig: 5.2.1 - debug: 4.3.3 - dedent: 0.7.0 - del: 5.1.0 - execa: 2.1.0 - listr: 0.14.3 - log-symbols: 3.0.0 - micromatch: 4.0.4 - normalize-path: 3.0.0 - please-upgrade-node: 3.2.0 - string-argv: 0.3.1 - stringify-object: 3.3.0 - transitivePeerDependencies: - - supports-color - - zen-observable - dev: true - /liquid-json/0.3.1: resolution: {integrity: sha1-kVWhgTbYprJhXl8W+aJEira1Duo=} engines: {node: '>=4'} dev: false - /listr-silent-renderer/1.1.1: - resolution: {integrity: sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=} - engines: {node: '>=4'} - dev: true - - /listr-update-renderer/0.5.0_listr@0.14.3: - resolution: {integrity: sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==} - engines: {node: '>=6'} - peerDependencies: - listr: ^0.14.2 - dependencies: - chalk: 1.1.3 - cli-truncate: 0.2.1 - elegant-spinner: 1.0.1 - figures: 1.7.0 - indent-string: 3.2.0 - listr: 0.14.3 - log-symbols: 1.0.2 - log-update: 2.3.0 - strip-ansi: 3.0.1 - dev: true - - /listr-verbose-renderer/0.5.0: - resolution: {integrity: sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==} - engines: {node: '>=4'} - dependencies: - chalk: 2.4.2 - cli-cursor: 2.1.0 - date-fns: 1.30.1 - figures: 2.0.0 - dev: true - - /listr/0.14.3: - resolution: {integrity: sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==} - engines: {node: '>=6'} - dependencies: - '@samverschueren/stream-to-observable': 0.3.1_rxjs@6.6.7 - is-observable: 1.1.0 - is-promise: 2.2.2 - is-stream: 1.1.0 - listr-silent-renderer: 1.1.1 - listr-update-renderer: 0.5.0_listr@0.14.3 - listr-verbose-renderer: 0.5.0 - p-map: 2.1.0 - rxjs: 6.6.7 - transitivePeerDependencies: - - zen-observable - dev: true - /listr2/4.0.5: resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} engines: {node: '>=12'} @@ -28612,24 +28018,6 @@ packages: resolution: {integrity: sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==} dev: true - /livereload-js/3.3.3: - resolution: {integrity: sha512-a7Jipme3XIBIryJluWP5LQrEAvhobDPyScBe+q+MYwxBiMT2Ck7msy4tAdF8TAa33FMdJqX4guP81Yhiu6BkmQ==} - dev: true - - /livereload/0.9.3: - resolution: {integrity: sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==} - engines: {node: '>=8.0.0'} - hasBin: true - dependencies: - chokidar: 3.5.3 - livereload-js: 3.3.3 - opts: 2.0.2 - ws: 7.5.5 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: true - /load-json-file/1.1.0: resolution: {integrity: sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=} engines: {node: '>=0.10.0'} @@ -28884,20 +28272,6 @@ packages: /lodash/4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - /log-symbols/1.0.2: - resolution: {integrity: sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=} - engines: {node: '>=0.10.0'} - dependencies: - chalk: 1.1.3 - dev: true - - /log-symbols/2.2.0: - resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} - engines: {node: '>=4'} - dependencies: - chalk: 2.4.2 - dev: true - /log-symbols/3.0.0: resolution: {integrity: sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==} engines: {node: '>=8'} @@ -28913,15 +28287,6 @@ packages: is-unicode-supported: 0.1.0 dev: true - /log-update/2.3.0: - resolution: {integrity: sha1-iDKP19HOeTiykoN0bwsbwSayRwg=} - engines: {node: '>=4'} - dependencies: - ansi-escapes: 3.2.0 - cli-cursor: 2.1.0 - wrap-ansi: 3.0.1 - dev: true - /log-update/4.0.0: resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} engines: {node: '>=10'} @@ -28971,6 +28336,7 @@ packages: /lowercase-keys/2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} + dev: false /lowlight/1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} @@ -29201,19 +28567,6 @@ packages: hasBin: true dev: true - /marked/1.2.9: - resolution: {integrity: sha512-H8lIX2SvyitGX+TRdtS06m1jHMijKN/XjfH6Ooii9fvxMlh8QdqBfBDkGUpMWH2kQNrtixjzYUa3SH8ROTgRRw==} - engines: {node: '>= 8.16.2'} - hasBin: true - dev: true - - /matcher/1.1.1: - resolution: {integrity: sha512-+BmqxWIubKTRKNWx/ahnCkk3mG8m7OturVlqq6HiojGJTd5hVYbgZm6WzcYPCoB+KBT4Vd6R7WSRG2OADNaCjg==} - engines: {node: '>=4'} - dependencies: - escape-string-regexp: 1.0.5 - dev: true - /mathml-tag-names/2.1.3: resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} dev: true @@ -29313,10 +28666,6 @@ packages: engines: {node: '>= 0.6'} dev: true - /medium-zoom/1.0.6: - resolution: {integrity: sha512-UdiUWfvz9fZMg1pzf4dcuqA0W079o0mpqbTnOz5ip4VGYX96QjmbM+OgOU/0uOzAytxC0Ny4z+VcYQnhdifimg==} - dev: true - /mem-fs-editor/9.4.0_mem-fs@2.2.1: resolution: {integrity: sha512-HSSOLSVRrsDdui9I6i96dDtG+oAez/4EB2g4cjSrNhgNQ3M+L57/+22NuPdORSoxvOHjIg/xeOE+C0wwF91D2g==} engines: {node: '>=12.10.0'} @@ -29593,6 +28942,7 @@ packages: /mimic-response/1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} + dev: false /mimic-response/3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} @@ -29940,6 +29290,7 @@ packages: /nan/2.15.0: resolution: {integrity: sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==} + requiresBuild: true optional: true /nanoid/3.1.30: @@ -30194,19 +29545,6 @@ packages: engines: {node: '>=6'} dev: true - /node-wp-i18n/1.2.6: - resolution: {integrity: sha512-aLutjDB1rMJ3FNlNcs/XjmaejED1/y30uLYQrmkXpeUj1NH/SA6pI94CUz3iI7fbQd63lTGg0YNvOQAT8cWdIw==} - hasBin: true - dependencies: - bluebird: 3.7.2 - gettext-parser: 3.1.1 - glob: 7.2.0 - lodash: 4.17.21 - minimist: 1.2.5 - mkdirp: 1.0.4 - tmp: 0.2.1 - dev: true - /node.extend/2.0.2: resolution: {integrity: sha512-pDT4Dchl94/+kkgdwyS2PauDFjZG0Hk0IcHIB+LkW27HLDtdoeMxHTxZh39DYbPP8UflWXWj9JcdDozF+YDOpQ==} engines: {node: '>=0.4.0'} @@ -30288,11 +29626,6 @@ packages: resolution: {integrity: sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==} engines: {node: '>=6'} - /normalize-url/4.5.1: - resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} - engines: {node: '>=8'} - dev: true - /normalize-url/6.1.0: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} @@ -30358,14 +29691,6 @@ packages: npm-normalize-package-bin: 1.0.1 dev: true - /npm-path/2.0.4: - resolution: {integrity: sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw==} - engines: {node: '>=0.8'} - hasBin: true - dependencies: - which: 1.3.1 - dev: true - /npm-pick-manifest/6.1.1: resolution: {integrity: sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA==} dependencies: @@ -30411,29 +29736,12 @@ packages: dependencies: path-key: 2.0.1 - /npm-run-path/3.1.0: - resolution: {integrity: sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==} - engines: {node: '>=8'} - dependencies: - path-key: 3.1.1 - dev: true - /npm-run-path/4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} dependencies: path-key: 3.1.1 - /npm-which/3.0.1: - resolution: {integrity: sha1-kiXybsOihcIJyuZ8OxGmtKtxQKo=} - engines: {node: '>=4.2.0'} - hasBin: true - dependencies: - commander: 2.20.3 - npm-path: 2.0.4 - which: 1.3.1 - dev: true - /npmlog/5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} dependencies: @@ -30702,13 +30010,6 @@ packages: dependencies: mimic-fn: 2.1.0 - /open/6.4.0: - resolution: {integrity: sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==} - engines: {node: '>=8'} - dependencies: - is-wsl: 1.1.0 - dev: true - /open/7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} @@ -30726,11 +30027,6 @@ packages: is-wsl: 2.2.0 dev: true - /opencollective-postinstall/2.0.3: - resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} - hasBin: true - dev: true - /opener/1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true @@ -30766,10 +30062,6 @@ packages: word-wrap: 1.2.3 dev: true - /opts/2.0.2: - resolution: {integrity: sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==} - dev: true - /ora/5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -30822,11 +30114,6 @@ packages: p-map: 2.1.0 dev: true - /p-cancelable/1.1.0: - resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} - engines: {node: '>=6'} - dev: true - /p-cancelable/2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -30914,11 +30201,6 @@ packages: p-limit: 3.1.0 dev: true - /p-map/1.2.0: - resolution: {integrity: sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==} - engines: {node: '>=4'} - dev: true - /p-map/2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} @@ -30991,16 +30273,6 @@ packages: semver: 5.7.1 dev: true - /package-json/6.5.0: - resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} - engines: {node: '>=8'} - dependencies: - got: 9.6.0 - registry-auth-token: 4.2.1 - registry-url: 5.1.0 - semver: 6.3.0 - dev: true - /pacote/12.0.3: resolution: {integrity: sha512-CdYEl03JDrRO3x18uHjBYA9TyoW8gy+ThVcypcDkxPtKlw76e4ejhYB6i9lJ+/cebbjpqPW/CijjqxwDTts8Ow==} engines: {node: ^12.13.0 || ^14.15.0 || >=16} @@ -31064,11 +30336,6 @@ packages: dependencies: callsites: 3.1.0 - /parent-require/1.0.0: - resolution: {integrity: sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc=} - engines: {node: '>= 0.4.0'} - dev: true - /parse-asn1/5.1.6: resolution: {integrity: sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==} dependencies: @@ -31405,12 +30672,6 @@ packages: find-up: 5.0.0 dev: true - /please-upgrade-node/3.2.0: - resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} - dependencies: - semver-compare: 1.0.0 - dev: true - /plur/4.0.0: resolution: {integrity: sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==} engines: {node: '>=10'} @@ -32733,11 +31994,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /prepend-http/2.0.0: - resolution: {integrity: sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=} - engines: {node: '>=4'} - dev: true - /prettier-linter-helpers/1.0.0: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} @@ -32929,10 +32185,6 @@ packages: object-assign: 4.1.1 react-is: 16.13.1 - /property-expr/1.5.1: - resolution: {integrity: sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g==} - dev: true - /property-information/5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} dependencies: @@ -33001,13 +32253,6 @@ packages: resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} engines: {node: '>=6'} - /pupa/2.1.1: - resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==} - engines: {node: '>=8'} - dependencies: - escape-goat: 2.1.1 - dev: true - /puppeteer-core/1.12.2: resolution: {integrity: sha512-M+atMV5e+MwJdR+OwQVZ1xqAIwh3Ou4nUxNuf334GwpcLG+LDj5BwIph4J9y8YAViByRtWGL+uF8qX2Ggzb+Fg==} engines: {node: '>=6.4.0'} @@ -34143,15 +33388,6 @@ packages: normalize-package-data: 2.5.0 path-type: 3.0.0 - /read-pkg/4.0.1: - resolution: {integrity: sha1-ljYlN48+HE1IyFhytabsfV0JMjc=} - engines: {node: '>=6'} - dependencies: - normalize-package-data: 2.5.0 - parse-json: 4.0.0 - pify: 3.0.0 - dev: true - /read-pkg/5.2.0: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} @@ -34391,10 +33627,6 @@ packages: resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} dev: true - /regenerator-runtime/0.12.1: - resolution: {integrity: sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==} - dev: true - /regenerator-runtime/0.13.9: resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} @@ -34463,13 +33695,6 @@ packages: safe-buffer: 5.2.1 dev: true - /registry-auth-token/4.2.1: - resolution: {integrity: sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==} - engines: {node: '>=6.0.0'} - dependencies: - rc: 1.2.8 - dev: true - /registry-url/3.1.0: resolution: {integrity: sha1-PU74cPc93h138M+aOBQyRE4XSUI=} engines: {node: '>=0.10.0'} @@ -34477,13 +33702,6 @@ packages: rc: 1.2.8 dev: true - /registry-url/5.1.0: - resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==} - engines: {node: '>=8'} - dependencies: - rc: 1.2.8 - dev: true - /regjsgen/0.5.2: resolution: {integrity: sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==} @@ -34802,6 +34020,7 @@ packages: /resolve-pathname/3.0.0: resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} + dev: false /resolve-url/0.2.1: resolution: {integrity: sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=} @@ -34827,12 +34046,6 @@ packages: is-core-module: 2.8.0 path-parse: 1.0.7 - /responselike/1.0.2: - resolution: {integrity: sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=} - dependencies: - lowercase-keys: 1.0.1 - dev: true - /responselike/2.0.0: resolution: {integrity: sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==} dependencies: @@ -35017,12 +34230,6 @@ packages: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} - /run-node/1.0.0: - resolution: {integrity: sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==} - engines: {node: '>=4'} - hasBin: true - dev: true - /run-parallel/1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -35322,10 +34529,6 @@ packages: node-forge: 0.10.0 dev: true - /semver-compare/1.0.0: - resolution: {integrity: sha1-De4hahyUGrN+nvsXiPavxf9VN/w=} - dev: true - /semver-diff/2.1.0: resolution: {integrity: sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=} engines: {node: '>=0.10.0'} @@ -35333,18 +34536,6 @@ packages: semver: 5.7.1 dev: true - /semver-diff/3.1.1: - resolution: {integrity: sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==} - engines: {node: '>=8'} - dependencies: - semver: 6.3.0 - dev: true - - /semver-regex/2.0.0: - resolution: {integrity: sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==} - engines: {node: '>=6'} - dev: true - /semver/5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} hasBin: true @@ -35572,14 +34763,6 @@ packages: /signal-exit/3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - /simple-git/1.132.0: - resolution: {integrity: sha512-xauHm1YqCTom1sC9eOjfq3/9RKiUA9iPnxBbrY2DdL8l4ADMu0jjM5l5lphQP5YWNqAL2aXC/OeuQ76vHtW5fg==} - dependencies: - debug: 4.3.3 - transitivePeerDependencies: - - supports-color - dev: true - /simple-html-tokenizer/0.5.11: resolution: {integrity: sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==} dev: false @@ -35605,11 +34788,6 @@ packages: engines: {node: '>=12'} dev: true - /slice-ansi/0.0.4: - resolution: {integrity: sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=} - engines: {node: '>=0.10.0'} - dev: true - /slice-ansi/2.1.0: resolution: {integrity: sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==} engines: {node: '>=6'} @@ -35924,11 +35102,6 @@ packages: resolution: {integrity: sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==} dev: true - /staged-git-files/1.1.2: - resolution: {integrity: sha512-0Eyrk6uXW6tg9PYkhi/V/J4zHp33aNyi2hOCmhFLqLTIhbgqWn5jlSzI+IU0VqrZq6+DbHcabQl/WP6P3BG0QA==} - hasBin: true - dev: true - /state-toggle/1.0.3: resolution: {integrity: sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==} dev: true @@ -35986,11 +35159,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /string-argv/0.0.2: - resolution: {integrity: sha1-2sMECGkMIfPDYwo/86BYd73L1zY=} - engines: {node: '>=0.6.19'} - dev: true - /string-argv/0.3.1: resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} engines: {node: '>=0.6.19'} @@ -36133,15 +35301,6 @@ packages: dependencies: safe-buffer: 5.2.1 - /stringify-object/3.3.0: - resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} - engines: {node: '>=4'} - dependencies: - get-own-enumerable-property-symbols: 3.0.2 - is-obj: 1.0.1 - is-regexp: 1.0.0 - dev: true - /strip-ansi/2.0.1: resolution: {integrity: sha1-32LBqpTtLxFOHQ8h/R1QSCt5pg4=} engines: {node: '>=0.10.0'} @@ -36731,8 +35890,8 @@ packages: dependencies: has-flag: 4.0.0 - /supports-color/9.2.1: - resolution: {integrity: sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ==} + /supports-color/9.2.2: + resolution: {integrity: sha512-XC6g/Kgux+rJXmwokjm9ECpD6k/smUoS5LKlUCcsYr4IY3rW0XyAympon2RmxGrlnZURMpg5T18gWDP9CsHXFA==} engines: {node: '>=12'} dev: true @@ -36792,11 +35951,6 @@ packages: upper-case: 1.1.3 dev: true - /symbol-observable/1.2.0: - resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==} - engines: {node: '>=0.10.0'} - dev: true - /symbol-tree/3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -36931,11 +36085,6 @@ packages: execa: 0.7.0 dev: true - /term-size/2.2.1: - resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} - engines: {node: '>=8'} - dev: true - /terminal-link/2.1.1: resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} engines: {node: '>=8'} @@ -37084,7 +36233,7 @@ packages: serialize-javascript: 6.0.0 source-map: 0.6.1 terser: 5.10.0_acorn@8.7.0 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@3.3.12 transitivePeerDependencies: - acorn dev: true @@ -37335,11 +36484,6 @@ packages: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} dev: false - /tinydate/1.3.0: - resolution: {integrity: sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==} - engines: {node: '>=4'} - dev: true - /title-case/1.1.2: resolution: {integrity: sha1-+uSmrlRr+iLQg6DuqRCkDRLtT1o=} dependencies: @@ -37389,11 +36533,6 @@ packages: dependencies: kind-of: 3.2.2 - /to-readable-stream/1.0.0: - resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} - engines: {node: '>=6'} - dev: true - /to-regex-range/2.1.1: resolution: {integrity: sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=} engines: {node: '>=0.10.0'} @@ -37425,10 +36564,6 @@ packages: engines: {node: '>=0.6'} dev: true - /toposort/2.0.2: - resolution: {integrity: sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=} - dev: true - /tough-cookie/2.5.0: resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} engines: {node: '>=0.8'} @@ -37806,10 +36941,6 @@ packages: /tweetnacl/0.14.5: resolution: {integrity: sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=} - /tweezer.js/1.5.0: - resolution: {integrity: sha512-aSiJz7rGWNAQq7hjMK9ZYDuEawXupcCWgl3woQQSoDP2Oh8O4srWb/uO1PzzHIsrPEOqrjJ2sUb9FERfzuBabQ==} - dev: true - /type-check/0.3.2: resolution: {integrity: sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=} engines: {node: '>= 0.8.0'} @@ -38045,13 +37176,6 @@ packages: crypto-random-string: 1.0.0 dev: true - /unique-string/2.0.0: - resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} - engines: {node: '>=8'} - dependencies: - crypto-random-string: 2.0.0 - dev: true - /unist-builder/2.0.3: resolution: {integrity: sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==} dev: true @@ -38191,25 +37315,6 @@ packages: xdg-basedir: 3.0.0 dev: true - /update-notifier/4.1.3: - resolution: {integrity: sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==} - engines: {node: '>=8'} - dependencies: - boxen: 4.2.0 - chalk: 3.0.0 - configstore: 5.0.1 - has-yarn: 2.1.0 - import-lazy: 2.1.0 - is-ci: 2.0.0 - is-installed-globally: 0.3.2 - is-npm: 4.0.0 - is-yarn-global: 0.3.0 - latest-version: 5.1.0 - pupa: 2.1.1 - semver-diff: 3.1.1 - xdg-basedir: 4.0.0 - dev: true - /upper-case-first/1.1.2: resolution: {integrity: sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU=} dependencies: @@ -38290,13 +37395,6 @@ packages: prepend-http: 1.0.4 dev: true - /url-parse-lax/3.0.0: - resolution: {integrity: sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=} - engines: {node: '>=4'} - dependencies: - prepend-http: 2.0.0 - dev: true - /url/0.10.3: resolution: {integrity: sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=} dependencies: @@ -38481,13 +37579,6 @@ packages: homedir-polyfill: 1.0.3 dev: true - /v8flags/3.2.0: - resolution: {integrity: sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==} - engines: {node: '>= 0.10'} - dependencies: - homedir-polyfill: 1.0.3 - dev: true - /validate-npm-package-license/3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -39384,10 +38475,6 @@ packages: /which-module/2.0.0: resolution: {integrity: sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=} - /which-pm-runs/1.0.0: - resolution: {integrity: sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=} - dev: true - /which-pm/2.0.0: resolution: {integrity: sha512-Lhs9Pmyph0p5n5Z3mVnN0yWcbQYUAD7rbQUiMsQxOJ3T57k7RFe35SUwWMf7dsbDZks1uOmw4AecB/JMDj3v/w==} engines: {node: '>=8.15'} @@ -39502,14 +38589,6 @@ packages: strip-ansi: 3.0.1 dev: true - /wrap-ansi/3.0.1: - resolution: {integrity: sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=} - engines: {node: '>=4'} - dependencies: - string-width: 2.1.1 - strip-ansi: 4.0.0 - dev: true - /wrap-ansi/5.1.0: resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} engines: {node: '>=6'} @@ -39623,11 +38702,6 @@ packages: engines: {node: '>=4'} dev: true - /xdg-basedir/4.0.0: - resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} - engines: {node: '>=8'} - dev: true - /xml-name-validator/3.0.0: resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} @@ -39685,14 +38759,6 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - /yargonaut/1.1.4: - resolution: {integrity: sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==} - dependencies: - chalk: 1.1.3 - figlet: 1.5.2 - parent-require: 1.0.0 - dev: true - /yargs-parser/13.1.2: resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==} dependencies: @@ -39933,17 +38999,6 @@ packages: wrap-ansi: 2.1.0 dev: true - /yup/0.26.10: - resolution: {integrity: sha512-keuNEbNSnsOTOuGCt3UJW69jDE3O4P+UHAakO7vSeFMnjaitcmlbij/a3oNb9g1Y1KvSKH/7O1R2PQ4m4TRylw==} - dependencies: - '@babel/runtime': 7.0.0 - fn-name: 2.0.1 - lodash: 4.17.21 - property-expr: 1.5.1 - synchronous-promise: 2.0.15 - toposort: 2.0.2 - dev: true - /zwitch/1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} dev: true diff --git a/tools/monorepo/check-changelogger-use.php b/tools/monorepo/check-changelogger-use.php new file mode 100644 index 00000000000..fc9a0118c62 --- /dev/null +++ b/tools/monorepo/check-changelogger-use.php @@ -0,0 +1,230 @@ +|--path=] +Checks that a monorepo commit contains a Changelogger change entry for each +project touched. + --debug, -v Display verbose output. + --list Just list projects, no explanatory output. + --path=, Project path to check for changed files. + -p + Base git ref to compare for changed files. + Head git ref to compare for changed files. +EOH; + exit( 1 ); +} + +// Options followed by a single colon have a required value. +$short_options = 'vhp:'; +$long_options = array( + 'debug', + 'list', + 'help', + 'path:', +); +$options = getopt( $short_options, $long_options, $remain_index ); +$arg_count = count( $argv ) - $remain_index; + +if ( isset( $options['h'] ) || isset( $options['help'] ) ) { + usage(); +} + +$list = isset( $options['l'] ) || isset( $options['list'] ); +$verbose = isset( $options['v'] ) || isset( $options['debug'] ); +$path = false; +if ( isset( $options['p'] ) || isset( $options['path'] ) ) { + $path = isset( $options['path'] ) ? $options['path'] : $options['p']; +} + +if ( $arg_count > 2 ) { + fprintf( STDERR, "\e[1;31mToo many arguments.\e[0m\n" ); + usage(); +} + +if ( $arg_count < 2 ) { + fprintf( STDERR, "\e[1;31mBase and head refs are required.\e[0m\n" ); + usage(); +} + +$base = $argv[ count( $argv ) - 2 ]; +$head = $argv[ count( $argv ) - 1 ]; + +if ( $verbose ) { + /** + * Output debug info. + * + * @param array ...$args Arguments to printf. A newline is automatically appended. + */ + function debug( ...$args ) { + if ( getenv( 'CI' ) ) { + $args[0] = "\e[34m${args[0]}\e[0m\n"; + } else { + $args[0] = "\e[1;30m${args[0]}\e[0m\n"; + } + fprintf( STDERR, ...$args ); + } +} else { + /** + * Do not output debug info. + */ + function debug() { + } +} + +$base_path = dirname( dirname( __DIR__ ) ); + +// Read workspace.json file to find potential composer files. +try { + $workspace = json_decode( file_get_contents( $base_path . '/workspace.json' ), true, 10, JSON_THROW_ON_ERROR ); +} catch ( Exception $e ) { + $workspace = false; +} +if ( ! $workspace || ! is_array( $workspace['projects'] ) ) { + debug( 'Unable to parse workspace file' ); + exit( 1 ); +} + +$composer_projects = array(); +foreach( $workspace['projects'] as $project => $directory ) { + if ( $path && $directory !== $path ) { + continue; + } + if ( file_exists( $base_path . '/' . $directory . '/composer.json' ) ) { + $composer_projects[] = $directory; + } +} + +if ( $path && ! count( $composer_projects ) ) { + debug( sprintf( 'The provided project path, %s, did not contain a composer file.', $path ) ); + exit( 1 ); +} + +// Find projects that use changelogger, and read the relevant config. +$changelogger_projects = array(); +foreach ( $composer_projects as $project_path ) { + try { + $data = json_decode( file_get_contents( $base_path . '/' . $project_path . '/composer.json' ), true, 512, JSON_THROW_ON_ERROR ); + if ( + ! isset( $data['require']['automattic/jetpack-changelogger'] ) && + ! isset( $data['require-dev']['automattic/jetpack-changelogger'] ) + ) { + continue; + } + } catch ( Exception $e ) { + continue; + } + $data = isset( $data['extra']['changelogger'] ) ? $data['extra']['changelogger'] : array(); + $data += array( + 'changelog' => $project_path . '/CHANGELOG.md', + 'changes-dir' => $project_path . '/changelog', + ); + $changelogger_projects[ $project_path ] = $data; +} + +// Process the diff. +debug( 'Checking diff from %s...%s.', $base, $head ); +$pipes = null; +$p = proc_open( + sprintf( 'git -c core.quotepath=off diff --no-renames --name-only %s...%s', escapeshellarg( $base ), escapeshellarg( $head ) ), + array( array( 'pipe', 'r' ), array( 'pipe', 'w' ), STDERR ), + $pipes +); +if ( ! $p ) { + exit( 1 ); +} +fclose( $pipes[0] ); + +$ok_projects = array(); +$touched_projects = array(); +// phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition +while ( ( $line = fgets( $pipes[1] ) ) ) { + $line = trim( $line ); + + $project_match = false; + foreach( $composer_projects as $path ) { + if ( substr( $line, 0, strlen( $path ) + 1 ) === $path . '/' ) { + $project_match = $path; + break; + } + } + + if ( false === $project_match ) { + debug( 'Ignoring non-project file %s.', $line ); + continue; + } + + if ( ! isset( $changelogger_projects[ $project_match ] ) ) { + debug( 'Ignoring file %s, project %s does not use changelogger.', $line, $project_match ); + continue; + } + if ( basename( $line ) === $changelogger_projects[ $project_match ]['changelog'] ) { + debug( 'Ignoring changelog file %s.', $line ); + continue; + } + if ( dirname( $line ) === $changelogger_projects[ $project_match ]['changes-dir'] ) { + if ( '.' === basename( $line )[0] ) { + debug( 'Ignoring changes dir dotfile %s.', $line ); + } else { + debug( 'PR touches file %s, marking %s as having a change file.', $line, $project_match ); + $ok_projects[ $project_match ] = true; + } + continue; + } + + debug( 'PR touches file %s, marking %s as touched.', $line, $project_match ); + if ( ! isset( $touched_projects[ $project_match ] ) ) { + $touched_projects[ $project_match ][] = $line; + } +} + +fclose( $pipes[1] ); +$status = proc_close( $p ); +if ( $status ) { + exit( $status ); +} + +// Output. +ksort( $touched_projects ); +$exit = 0; +foreach ( $touched_projects as $slug => $files ) { + if ( empty( $ok_projects[ $slug ] ) ) { + if ( $list ) { + echo "$slug\n"; + } elseif ( getenv( 'CI' ) ) { + printf( "---\n" ); // Bracket message containing newlines for better visibility in GH's logs. + printf( + "::error::Project %s is being changed, but no change file in %s is touched!%%0A%%0AUse `pnpm nx affected --target=changelog` to add a change file.\n", + $slug, + "$slug/{$changelogger_projects[ $slug ]['changes-dir']}/", + $slug + ); + printf( "---\n" ); + $exit = 1; + } else { + printf( + "\e[1;31mProject %s is being changed, but no change file in %s is touched!\e[0m\n", + $slug, + "$slug/{$changelogger_projects[ $slug ]['changes-dir']}/" + ); + $exit = 1; + } + } +} +if ( $exit && ! getenv( 'CI' ) && ! $list ) { + printf( "\e[32mUse `pnpm nx affected --target=changelog` to add a change file for each project.\e[0m\n" ); +} + +exit( $exit );