From 9b042534d04544abfd7256f842b85fc348f8af8d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 20:33:45 +1200 Subject: [PATCH 01/91] Prepare Packages for Release (#47208) Automated change: Prep @woocommerce/dependency-extraction-webpack-plugin for release. Co-authored-by: psealock --- .../js/dependency-extraction-webpack-plugin/CHANGELOG.md | 8 ++++++-- .../changelog/47099-add-price-format-package-dewp | 4 ---- .../js/dependency-extraction-webpack-plugin/package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 packages/js/dependency-extraction-webpack-plugin/changelog/47099-add-price-format-package-dewp diff --git a/packages/js/dependency-extraction-webpack-plugin/CHANGELOG.md b/packages/js/dependency-extraction-webpack-plugin/CHANGELOG.md index 0c1776d6b46b..8b23290cad8c 100644 --- a/packages/js/dependency-extraction-webpack-plugin/CHANGELOG.md +++ b/packages/js/dependency-extraction-webpack-plugin/CHANGELOG.md @@ -2,12 +2,16 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.1](https://www.npmjs.com/package/@woocommerce/dependency-extraction-webpack-plugin/v/3.0.1) - 2024-05-07 + +- Patch - Add @woocommerce/price-format package. [#47099] + ## [3.0.0](https://www.npmjs.com/package/@woocommerce/dependency-extraction-webpack-plugin/v/3.0.0) - 2024-04-23 - Patch - Fix a bug where the assets folder was not distributed with @woocommerce/dependency-extraction-webpack-plugin [#46755] -- Major [ **BREAKING CHANGE** ] - Bump node to version 20 [#46702] -- Minor - Bump node version. [#45148] - Patch - bump php version in packages/js/*/composer.json [#42020] +- - Bump node to version 20 [#46702] +- Minor - Bump node version. [#45148] ## [2.3.0](https://www.npmjs.com/package/@woocommerce/dependency-extraction-webpack-plugin/v/2.3.0) - 2023-10-27 diff --git a/packages/js/dependency-extraction-webpack-plugin/changelog/47099-add-price-format-package-dewp b/packages/js/dependency-extraction-webpack-plugin/changelog/47099-add-price-format-package-dewp deleted file mode 100644 index 3e9b04d0a5c8..000000000000 --- a/packages/js/dependency-extraction-webpack-plugin/changelog/47099-add-price-format-package-dewp +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: add - -Add @woocommerce/price-format package. \ No newline at end of file diff --git a/packages/js/dependency-extraction-webpack-plugin/package.json b/packages/js/dependency-extraction-webpack-plugin/package.json index f9f515020b6b..2b2a5299a51a 100644 --- a/packages/js/dependency-extraction-webpack-plugin/package.json +++ b/packages/js/dependency-extraction-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/dependency-extraction-webpack-plugin", - "version": "3.0.0", + "version": "3.0.1", "description": "WooCommerce Dependency Extraction Webpack Plugin", "author": "Automattic", "license": "GPL-2.0-or-later", From f040e3acf7df9420a09d37b84358ac7d2e03b8a3 Mon Sep 17 00:00:00 2001 From: Tom Cafferkey Date: Tue, 7 May 2024 09:46:22 +0100 Subject: [PATCH 02/91] Customer Account: Don't hook block unless filter is available. (#47171) * Check WP version before adding Customer Account hooked block due to usage of hooked_block_{} filter * Changelog * Replace global with get_bloginfo('version') for getting WP version --- .../fix-customer-account-hooked-block-attributes | 4 ++++ .../src/Blocks/BlockTypes/CustomerAccount.php | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-customer-account-hooked-block-attributes diff --git a/plugins/woocommerce/changelog/fix-customer-account-hooked-block-attributes b/plugins/woocommerce/changelog/fix-customer-account-hooked-block-attributes new file mode 100644 index 000000000000..8b78e857eab2 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-customer-account-hooked-block-attributes @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Don't automatically insert hooked customer account block into header for sites running less than WP 6.5 diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php b/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php index bf5732e5ec8d..1ff697d07a74 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php @@ -41,8 +41,15 @@ class CustomerAccount extends AbstractBlock { */ protected function initialize() { parent::initialize(); - add_filter( 'hooked_block_types', array( $this, 'register_hooked_block' ), 9, 4 ); - add_filter( 'hooked_block_woocommerce/customer-account', array( $this, 'modify_hooked_block_attributes' ), 10, 5 ); + /** + * The hooked_block_{$hooked_block_type} filter was added in WordPress 6.5. + * We are the only code adding the filter 'hooked_block_woocommerce/customer-account'. + * Using has_filter() for a compatibility check won't work because add_filter() is used in the same file. + */ + if ( version_compare( get_bloginfo( 'version' ), '6.5', '>=' ) ) { + add_filter( 'hooked_block_woocommerce/customer-account', array( $this, 'modify_hooked_block_attributes' ), 10, 5 ); + add_filter( 'hooked_block_types', array( $this, 'register_hooked_block' ), 9, 4 ); + } } /** From 1671523cc0389f5b2a17b9bb4a2eed7f224c8208 Mon Sep 17 00:00:00 2001 From: Jonathan Lane Date: Tue, 7 May 2024 04:40:12 -0700 Subject: [PATCH 03/91] Remove upload plugin test from Slack reporting on daily (#47067) Co-authored-by: Jon Lane --- .../scripts/create-blocks-plugin-tests.js | 14 ++------------ .../changelog/e2e-update-daily-slack-pings | 4 ++++ 2 files changed, 6 insertions(+), 12 deletions(-) create mode 100644 plugins/woocommerce/changelog/e2e-update-daily-slack-pings diff --git a/.github/actions/tests/slack-summary-daily/scripts/create-blocks-plugin-tests.js b/.github/actions/tests/slack-summary-daily/scripts/create-blocks-plugin-tests.js index e2e99712e32b..fc665c0472d7 100644 --- a/.github/actions/tests/slack-summary-daily/scripts/create-blocks-plugin-tests.js +++ b/.github/actions/tests/slack-summary-daily/scripts/create-blocks-plugin-tests.js @@ -1,9 +1,8 @@ module.exports = ( { core } ) => { - const { UPLOAD_RESULT, E2E_RESULT, PLUGIN_NAME, PLUGIN_SLUG } = process.env; + const { E2E_RESULT, PLUGIN_NAME, PLUGIN_SLUG } = process.env; const { selectEmoji } = require( './utils' ); const fs = require( 'fs' ); - const emoji_UPLOAD = selectEmoji( UPLOAD_RESULT ); const emoji_E2E = selectEmoji( E2E_RESULT ); const reportURL = `https://woocommerce.github.io/woocommerce-test-reports/daily/${ PLUGIN_SLUG }/e2e`; @@ -12,18 +11,9 @@ module.exports = ( { core } ) => { type: 'section', text: { type: 'mrkdwn', - text: `<${ reportURL }|*${ PLUGIN_NAME }*>`, + text: `<${ reportURL }|*${ PLUGIN_NAME }*>: E2E tests ${ emoji_E2E }`, }, }, - { - type: 'context', - elements: [ - { - type: 'mrkdwn', - text: `"Upload plugin" test ${ emoji_UPLOAD }\tOther E2E tests ${ emoji_E2E }`, - }, - ], - }, { type: 'divider', }, diff --git a/plugins/woocommerce/changelog/e2e-update-daily-slack-pings b/plugins/woocommerce/changelog/e2e-update-daily-slack-pings new file mode 100644 index 000000000000..dc9cf2103f7d --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-update-daily-slack-pings @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Remove upload plugin test from daily reporting From 40486a0151669211a64f85e1bf1cda4b5c753d13 Mon Sep 17 00:00:00 2001 From: Maikel Perez Date: Tue, 7 May 2024 08:35:25 -0400 Subject: [PATCH 04/91] Remove duplicate code lines in css (#47122) * Remove duplicate css code from any woo scss file * Add changelog files --- packages/js/internal-style-build/abstracts/_variables.scss | 2 -- .../js/internal-style-build/changelog/enhancement-46501 | 4 ++++ plugins/woocommerce-admin/client/stylesheets/_index.scss | 6 ++---- plugins/woocommerce/changelog/enhancement-46501 | 4 ++++ 4 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 packages/js/internal-style-build/changelog/enhancement-46501 create mode 100644 plugins/woocommerce/changelog/enhancement-46501 diff --git a/packages/js/internal-style-build/abstracts/_variables.scss b/packages/js/internal-style-build/abstracts/_variables.scss index 812b09a7464b..da75f25635e8 100644 --- a/packages/js/internal-style-build/abstracts/_variables.scss +++ b/packages/js/internal-style-build/abstracts/_variables.scss @@ -9,8 +9,6 @@ @import '@wordpress/base-styles/z-index.scss'; @import '@wordpress/base-styles/default-custom-properties.scss'; -@include wordpress-admin-schemes; - $fallback-gutter: 24px; $fallback-gutter-large: 40px; $gutter: var(--main-gap); diff --git a/packages/js/internal-style-build/changelog/enhancement-46501 b/packages/js/internal-style-build/changelog/enhancement-46501 new file mode 100644 index 000000000000..8ad44e6ac3d7 --- /dev/null +++ b/packages/js/internal-style-build/changelog/enhancement-46501 @@ -0,0 +1,4 @@ +Significance: minor +Type: performance + +Remove duplicate css code from any woo scss file diff --git a/plugins/woocommerce-admin/client/stylesheets/_index.scss b/plugins/woocommerce-admin/client/stylesheets/_index.scss index 27111a8663d7..efe838e6fc2b 100644 --- a/plugins/woocommerce-admin/client/stylesheets/_index.scss +++ b/plugins/woocommerce-admin/client/stylesheets/_index.scss @@ -1,3 +1,5 @@ +@include wordpress-admin-schemes; + // Import our wp-admin reset. @import "./shared/_reset.scss"; @@ -27,7 +29,3 @@ // Import the embed-specific styles. @import "./shared/_embed.scss"; - -:root { - @include admin-scheme( #007cba ); -} diff --git a/plugins/woocommerce/changelog/enhancement-46501 b/plugins/woocommerce/changelog/enhancement-46501 new file mode 100644 index 000000000000..8ad44e6ac3d7 --- /dev/null +++ b/plugins/woocommerce/changelog/enhancement-46501 @@ -0,0 +1,4 @@ +Significance: minor +Type: performance + +Remove duplicate css code from any woo scss file From f401a98301c167dd3f90cf67efa0802bc2f97cee Mon Sep 17 00:00:00 2001 From: Justin Palmer <228780+layoutd@users.noreply.github.com> Date: Tue, 7 May 2024 15:54:32 +0200 Subject: [PATCH 05/91] Stamp the order attribution HTML element (only once) on a wider set of checkout form actions (#46834) * Inject the fields (only once) on a large set of checkout form actions * Changelog * Better doc format * Add _once to the new method to clarify functionality * Filter to modify the default list of actions --- .../tweak-more-robust-oa-field-injection | 4 ++ .../Orders/OrderAttributionController.php | 50 +++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce/changelog/tweak-more-robust-oa-field-injection diff --git a/plugins/woocommerce/changelog/tweak-more-robust-oa-field-injection b/plugins/woocommerce/changelog/tweak-more-robust-oa-field-injection new file mode 100644 index 000000000000..1c9e5a0b79bf --- /dev/null +++ b/plugins/woocommerce/changelog/tweak-more-robust-oa-field-injection @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Inject order attribution checkout fields (only once) on a wider set of checkout form actions. diff --git a/plugins/woocommerce/src/Internal/Orders/OrderAttributionController.php b/plugins/woocommerce/src/Internal/Orders/OrderAttributionController.php index c359696fe1f3..9aa0f137e7ac 100644 --- a/plugins/woocommerce/src/Internal/Orders/OrderAttributionController.php +++ b/plugins/woocommerce/src/Internal/Orders/OrderAttributionController.php @@ -58,6 +58,13 @@ class OrderAttributionController implements RegisterHooksInterface { */ private $proxy; + /** + * Whether the `stamp_checkout_html_element` method has been called. + * + * @var bool + */ + private static $is_stamp_checkout_html_called = false; + /** * Initialization method. * @@ -111,7 +118,27 @@ function() { } ); - add_action( 'woocommerce_checkout_after_customer_details', array( $this, 'stamp_html_element' ) ); + /** + * Filter set of actions used to stamp the unique checkout order attribution HTML container element. + * + * @since 9.0.0 + * + * @param array $stamp_checkout_html_actions The set of actions used to stamp the unique checkout order attribution HTML container element. + */ + $stamp_checkout_html_actions = apply_filters( + 'wc_order_attribution_stamp_checkout_html_actions', + array( + 'woocommerce_checkout_billing', + 'woocommerce_after_checkout_billing_form', + 'woocommerce_checkout_shipping', + 'woocommerce_after_order_notes', + 'woocommerce_checkout_after_customer_details', + ) + ); + foreach ( $stamp_checkout_html_actions as $action ) { + add_action( $action, array( $this, 'stamp_checkout_html_element_once' ) ); + } + add_action( 'woocommerce_register_form', array( $this, 'stamp_html_element' ) ); // Update order based on submitted fields. @@ -339,8 +366,25 @@ private function output_origin_column( WC_Order $order ) { } /** - * Add `` element that contributes the order attribution values to the enclosing form. - * Used for checkout & customer register forms. + * Handles the `` element for checkout forms, ensuring that the field is only output once. + * + * @since 9.0.0 + * + * @return void + */ + public function stamp_checkout_html_element_once() { + if ( self::$is_stamp_checkout_html_called ) { + return; + } + $this->stamp_html_element(); + self::$is_stamp_checkout_html_called = true; + } + + /** + * Output `` element that contributes the order attribution values to the enclosing form. + * Used customer register forms, and for checkout forms through `stamp_checkout_html_element()`. + * + * @return void */ public function stamp_html_element() { printf( '' ); From b107cff5190d10cfb435dd39dd8c54fe9ee8c80b Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Tue, 7 May 2024 11:43:34 -0400 Subject: [PATCH 06/91] Product Editor API: Modal block editor sidebar extensibility (description field) (#46597) * iframe editor PluginSidebar * iframe editor PluginArea and ComplementaryArea * iframe editor PinnedItems * Try ComplementaryArea slot * Update scope name * Only render ComplementaryArea.Slot when right sidebar is open * Register interface store in sub registry * Move block inspector to complementary area * Remove hardcoded inspector toggle button from header * Remove debug statement * Remove unused isSidebarOpened context * Set settings sidebar to be active by default * Don't allow unpinning of settings sidebar * Organize sidebar components under folder * Add plugin more menu item * Remove width for sidebar * Update PluginArea scope name * Pull out small viewport support for pinned items (incomplete) * Move sidebar complementary area scope to a const * Update sidebar complementary area scope name * Update PluginSidebar export name * Provide comments to clarify sidebar props settings * Add comment about why this icon was copied * Remove unnecessary wrapper div around complementary area * Changelog --- ...product-editor-description-editor-skeleton | 4 ++ .../iframe-editor/RegisterStores.tsx | 20 ++++++++++ .../src/components/iframe-editor/constants.ts | 2 + .../src/components/iframe-editor/context.ts | 4 -- .../header-toolbar/header-toolbar.tsx | 11 +++--- .../header-toolbar/more-menu/more-menu.tsx | 22 +++++++++-- .../show-block-inspector-panel.tsx | 38 ------------------- .../iframe-editor/iframe-editor.scss | 1 - .../iframe-editor/iframe-editor.tsx | 35 ++++++++++++----- .../src/components/iframe-editor/index.ts | 1 + .../sidebar/plugin-sidebar/index.js | 1 + .../sidebar/plugin-sidebar/plugin-sidebar.tsx | 36 ++++++++++++++++++ .../sidebar/settings-sidebar/drawer-left.tsx | 24 ++++++++++++ .../settings-sidebar}/drawer-right.tsx | 0 .../sidebar/settings-sidebar/index.js | 1 + .../settings-sidebar/settings-sidebar.tsx | 36 ++++++++++++++++++ .../js/product-editor/src/components/index.ts | 2 + 17 files changed, 177 insertions(+), 61 deletions(-) create mode 100644 packages/js/product-editor/changelog/update-product-editor-description-editor-skeleton create mode 100644 packages/js/product-editor/src/components/iframe-editor/RegisterStores.tsx create mode 100644 packages/js/product-editor/src/components/iframe-editor/constants.ts delete mode 100644 packages/js/product-editor/src/components/iframe-editor/header-toolbar/show-block-inspector-panel.tsx create mode 100644 packages/js/product-editor/src/components/iframe-editor/sidebar/plugin-sidebar/index.js create mode 100644 packages/js/product-editor/src/components/iframe-editor/sidebar/plugin-sidebar/plugin-sidebar.tsx create mode 100644 packages/js/product-editor/src/components/iframe-editor/sidebar/settings-sidebar/drawer-left.tsx rename packages/js/product-editor/src/components/iframe-editor/{header-toolbar => sidebar/settings-sidebar}/drawer-right.tsx (100%) create mode 100644 packages/js/product-editor/src/components/iframe-editor/sidebar/settings-sidebar/index.js create mode 100644 packages/js/product-editor/src/components/iframe-editor/sidebar/settings-sidebar/settings-sidebar.tsx diff --git a/packages/js/product-editor/changelog/update-product-editor-description-editor-skeleton b/packages/js/product-editor/changelog/update-product-editor-description-editor-skeleton new file mode 100644 index 000000000000..86528811021d --- /dev/null +++ b/packages/js/product-editor/changelog/update-product-editor-description-editor-skeleton @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Added support for extension sidebars in modal block editor (description field). diff --git a/packages/js/product-editor/src/components/iframe-editor/RegisterStores.tsx b/packages/js/product-editor/src/components/iframe-editor/RegisterStores.tsx new file mode 100644 index 000000000000..f30f5089a51d --- /dev/null +++ b/packages/js/product-editor/src/components/iframe-editor/RegisterStores.tsx @@ -0,0 +1,20 @@ +/** + * External dependencies + */ +import { useRegistry } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; +import { + store as interfaceStore, + // @ts-expect-error No types for this exist yet. +} from '@wordpress/interface'; + +export const RegisterStores = () => { + const registry = useRegistry(); + + useEffect( () => { + // @ts-expect-error No types for this exist yet. + registry.register( interfaceStore ); + }, [ registry ] ); + + return null; +}; diff --git a/packages/js/product-editor/src/components/iframe-editor/constants.ts b/packages/js/product-editor/src/components/iframe-editor/constants.ts new file mode 100644 index 000000000000..24dd59e95207 --- /dev/null +++ b/packages/js/product-editor/src/components/iframe-editor/constants.ts @@ -0,0 +1,2 @@ +export const SIDEBAR_COMPLEMENTARY_AREA_SCOPE = + 'woocommerce/product-editor/modal-block-editor/sidebar'; diff --git a/packages/js/product-editor/src/components/iframe-editor/context.ts b/packages/js/product-editor/src/components/iframe-editor/context.ts index ce5a83e2ef72..7703387d9890 100644 --- a/packages/js/product-editor/src/components/iframe-editor/context.ts +++ b/packages/js/product-editor/src/components/iframe-editor/context.ts @@ -8,11 +8,9 @@ type EditorContextType = { hasUndo: boolean; isDocumentOverviewOpened: boolean; isInserterOpened: boolean; - isSidebarOpened: boolean; redo: () => void; setIsDocumentOverviewOpened: ( value: boolean ) => void; setIsInserterOpened: ( value: boolean ) => void; - setIsSidebarOpened: ( value: boolean ) => void; undo: () => void; }; @@ -21,10 +19,8 @@ export const EditorContext = createContext< EditorContextType >( { hasUndo: false, isDocumentOverviewOpened: false, isInserterOpened: false, - isSidebarOpened: true, redo: () => {}, setIsDocumentOverviewOpened: () => {}, setIsInserterOpened: () => {}, - setIsSidebarOpened: () => {}, undo: () => {}, } ); diff --git a/packages/js/product-editor/src/components/iframe-editor/header-toolbar/header-toolbar.tsx b/packages/js/product-editor/src/components/iframe-editor/header-toolbar/header-toolbar.tsx index 81946b315111..628f32f167b8 100644 --- a/packages/js/product-editor/src/components/iframe-editor/header-toolbar/header-toolbar.tsx +++ b/packages/js/product-editor/src/components/iframe-editor/header-toolbar/header-toolbar.tsx @@ -36,6 +36,10 @@ import { ToolSelector, BlockToolbar, } from '@wordpress/block-editor'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { PinnedItems } from '@wordpress/interface'; /** * Internal dependencies @@ -44,9 +48,9 @@ import { EditorContext } from '../context'; import EditorHistoryRedo from './editor-history-redo'; import EditorHistoryUndo from './editor-history-undo'; import { DocumentOverview } from './document-overview'; -import { ShowBlockInspectorPanel } from './show-block-inspector-panel'; import { MoreMenu } from './more-menu'; import { getGutenbergVersion } from '../../../utils/get-gutenberg-version'; +import { SIDEBAR_COMPLEMENTARY_AREA_SCOPE } from '../constants'; type HeaderToolbarProps = { onSave?: () => void; @@ -217,10 +221,7 @@ export function HeaderToolbar( { onClick={ onSave } text={ __( 'Done', 'woocommerce' ) } /> - + diff --git a/packages/js/product-editor/src/components/iframe-editor/header-toolbar/more-menu/more-menu.tsx b/packages/js/product-editor/src/components/iframe-editor/header-toolbar/more-menu/more-menu.tsx index aba88e3e2cb6..a860c5de1294 100644 --- a/packages/js/product-editor/src/components/iframe-editor/header-toolbar/more-menu/more-menu.tsx +++ b/packages/js/product-editor/src/components/iframe-editor/header-toolbar/more-menu/more-menu.tsx @@ -1,12 +1,16 @@ /** * External dependencies */ +import { MenuGroup } from '@wordpress/components'; import { createElement, Fragment } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; import { isWpVersion } from '@woocommerce/settings'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore No types for this exist yet. // eslint-disable-next-line @woocommerce/dependency-group -import { MoreMenuDropdown } from '@wordpress/interface'; +import { + ActionItem, + MoreMenuDropdown, + // @ts-expect-error No types for this exist yet. +} from '@wordpress/interface'; /** * Internal dependencies @@ -14,15 +18,25 @@ import { MoreMenuDropdown } from '@wordpress/interface'; import { ToolsMenuGroup } from './tools-menu-group'; import { WritingMenu } from '../writing-menu'; import { getGutenbergVersion } from '../../../../utils/get-gutenberg-version'; +import { SIDEBAR_COMPLEMENTARY_AREA_SCOPE } from '../../constants'; export const MoreMenu = () => { const renderBlockToolbar = isWpVersion( '6.5', '>=' ) || getGutenbergVersion() > 17.3; + return ( - { () => ( + { ( { onClose }: { onClose: () => void } ) => ( <> { renderBlockToolbar && } + + + ) } diff --git a/packages/js/product-editor/src/components/iframe-editor/header-toolbar/show-block-inspector-panel.tsx b/packages/js/product-editor/src/components/iframe-editor/header-toolbar/show-block-inspector-panel.tsx deleted file mode 100644 index b1e90168b2b8..000000000000 --- a/packages/js/product-editor/src/components/iframe-editor/header-toolbar/show-block-inspector-panel.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/** - * External dependencies - */ -import { Button } from '@wordpress/components'; -import { createElement, forwardRef, useContext } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { Ref } from 'react'; - -/** - * Internal dependencies - */ -import { EditorContext } from '../context'; -import drawerRight from './drawer-right'; - -export const ShowBlockInspectorPanel = forwardRef( - function ForwardedRefSidebarOpened( - props: { [ key: string ]: unknown }, - ref: Ref< HTMLButtonElement > - ) { - const { isSidebarOpened, setIsSidebarOpened } = - useContext( EditorContext ); - - function handleClick() { - setIsSidebarOpened( ! isSidebarOpened ); - } - - return ( - + + + ); +}; diff --git a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.scss b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.scss index 71832d9495be..fed3122e5f05 100644 --- a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.scss +++ b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.scss @@ -53,13 +53,54 @@ display: grid; grid-template-columns: 40% 55% 5%; border-bottom: 1px solid $gray-300; - } - &__table-row { + padding: $gap-large 0; td:not(:last-child) { margin-right: $gap; } + .components-form-token-field { + position: relative; + } + + .components-form-token-field__help { + position: absolute; + top: 32px; + z-index: 0; + } + + .components-form-token-field__input-container { + position: absolute; + top: 0; + min-height: 34px; + z-index: 1; + + > .components-flex { + min-height: 34px; + } + + &.is-disabled { + border-color: $gray-600; + } + + &.is-active { + z-index: 10; + } + + &:not(.is-disabled) { + background-color: white; + } + } + + .components-form-token-field__input { + margin: 1px 0; + } + + .components-form-token-field__suggestion { + min-height: 36px; + padding: 10px 12px; + } + @include breakpoint( '<782px' ) { position: relative; grid-template-columns: 1fr; diff --git a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx index 92fb26f11a8d..b75d0542c332 100644 --- a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx +++ b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx @@ -3,14 +3,12 @@ */ import { __ } from '@wordpress/i18n'; import { createElement, Fragment, useEffect } from '@wordpress/element'; -import { resolveSelect, useSelect, useDispatch } from '@wordpress/data'; -import { closeSmall } from '@wordpress/icons'; +import { useSelect, useDispatch } from '@wordpress/data'; import { Form, __experimentalSelectControlMenuSlot as SelectControlMenuSlot, } from '@woocommerce/components'; import { - EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME, EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME, type ProductAttributesActions, type WPDataActions, @@ -23,15 +21,10 @@ import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies */ -import { - AttributeTermInputField, - CustomAttributeTermInputField, -} from '../attribute-term-input-field'; import { TRACKS_SOURCE } from '../../constants'; -import type { AttributeInputFieldItemProps } from '../attribute-input-field/types'; +import { AttributeTableRow } from './attribute-table-row'; import type { EnhancedProductAttribute } from '../../hooks/use-product-attributes'; -import AttributesComboboxControl from '../attribute-combobox-field'; -import { AttributesComboboxControlItem } from '../attribute-combobox-field/types'; +import type { AttributesComboboxControlItem } from '../attribute-combobox-field/types'; type NewAttributeModalProps = { title?: string; @@ -170,7 +163,7 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { values: AttributeForm, setValue: ( name: string, - value: AttributeForm[ keyof AttributeForm ] + value: AttributeForm[ keyof AttributeForm ] | null ) => void ) => { onRemoveItem(); @@ -180,24 +173,10 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { values.attributes.filter( ( val, i ) => i !== index ) ); } else { - setValue( `attributes[${ index }]`, [ null ] ); + setValue( `attributes[${ index }]`, null ); } }; - const focusValueField = ( index: number ) => { - setTimeout( () => { - const valueInputField: HTMLInputElement | null = - document.querySelector( - '.woocommerce-new-attribute-modal__table-row-' + - index + - ' .woocommerce-new-attribute-modal__table-attribute-value-column .woocommerce-experimental-select-control__input' - ); - if ( valueInputField ) { - valueInputField.focus(); - } - }, 0 ); - }; - useEffect( function focusFirstAttributeField() { const firstAttributeFieldLabel = document.querySelector< HTMLLabelElement >( @@ -214,21 +193,26 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { name: defaultSearch, } as EnhancedProductAttribute; - const sortCriteria = { order_by: 'name' }; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const { attributes, isLoading } = useSelect( ( select: WCDataSelector ) => { - const { getProductAttributes, hasFinishedResolution } = select( - EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME - ); - return { - isLoading: ! hasFinishedResolution( 'getProductAttributes', [ - sortCriteria, - ] ), - attributes: getProductAttributes( sortCriteria ), - }; - } ); + const attributeSortCriteria = { order_by: 'name' }; + + const { attributes, isLoadingAttributes } = useSelect( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ( select: WCDataSelector ) => { + const { + getProductAttributes: getAttributes, + hasFinishedResolution: hasLoadedAttributes, + } = select( EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME ); + + return { + isLoadingAttributes: ! hasLoadedAttributes( + 'getProductAttributes', + [ attributeSortCriteria ] + ), + attributes: getAttributes( attributeSortCriteria ), + }; + } + ); const { createErrorNotice } = useDispatch( 'core/notices' ); const { createProductAttribute } = useDispatch( @@ -251,80 +235,33 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { setValue: ( name: string, value: any ) => void; } ) => { /** - * Set the attribute values in the form state, - * and populate the terms if termsAutoSelection is enabled. + * Select the attribute in the form field. + * If the attribute does not exist, create it. + * ToDo: Improve Id. Adding a attribute with id -99 + * does not seem a good idea. * - * @param {AttributeInputFieldItemProps} attribute - The attribute value. - * @param {number} index - The index of the attribute in the form state. + * @param {AttributesComboboxControlItem} nextAttribute - The attribute to select. + * @param { number } index - The index of the attribute in the form field. + * @return { void } */ - function setAttributeValues( - attribute: AttributeInputFieldItemProps, - index: number, - populateTerms = true - ) { - /* - * When the attribute exists, - * set the attribute in the form state and populate the terms. - */ - if ( termsAutoSelection && populateTerms ) { - const selectedAttribute = - attribute as EnhancedProductAttribute; - - setValue( 'attributes[' + index + ']', { - ...selectedAttribute, - } ); - - return resolveSelect( - EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME - ) - .getProductAttributeTerms< - ProductAttributeTerm[] - >( { - // Send search parameter as empty to avoid a second - // request when focusing the attribute-term-input-field - // which perform the same request to get all the terms - search: '', - attribute_id: attribute.id, - } ) - .then( ( terms ) => { - if ( termsAutoSelection === 'all' ) { - selectedAttribute.terms = terms; - } else if ( terms.length > 0 ) { - selectedAttribute.terms = [ - terms[ 0 ], - ]; - } - setValue( 'attributes[' + index + ']', { - ...selectedAttribute, - } ); - focusValueField( index ); - } ); - } - - /* - * When the attribute does not exist, - * set the attribute in the form state without terms. - */ - setValue( 'attributes[' + index + ']', attribute ); - focusValueField( index ); - } - - function selectAttributeHandler( + function selectAttribute( nextAttribute: AttributesComboboxControlItem, index: number - ) { + ): void { recordEvent( 'product_attribute_add_custom_attribute', { source: TRACKS_SOURCE, } ); const attributeExists = nextAttribute.id !== -99; + const fieldName = `attributes[${ index }]`; + /* * When the attribute exists, * set the attribute values. */ if ( attributeExists ) { - return setAttributeValues( nextAttribute, index ); + return setValue( fieldName, nextAttribute ); } /* @@ -333,15 +270,11 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { * only set the attribute values. */ if ( ! createNewAttributesAsGlobal ) { - return setAttributeValues( - { - id: 0, - name: nextAttribute.name, - slug: nextAttribute.name, - }, - index, - false - ); + return setValue( fieldName, { + id: 0, + name: nextAttribute.name, + slug: nextAttribute.name, + } ); } /* @@ -355,11 +288,11 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { generate_slug: true, }, { - optimisticQueryUpdate: sortCriteria, + optimisticQueryUpdate: attributeSortCriteria, } ) .then( ( newAttribute ) => { - setAttributeValues( newAttribute, index ); + setValue( fieldName, newAttribute ); } ) .catch( ( error ) => { let message = __( @@ -379,19 +312,45 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { } ); } + /** + * Set the attribute terms in the form field. + * + * @param {ProductAttributeTerm[] | string[]} terms - The terms to set. + * @param {number} index - The index of the attribute in the form field. + * @param {EnhancedProductAttribute} attribute - The attribute to set the terms. + */ + function selectTerms( + terms: ProductAttributeTerm[] | string[], + index: number, + attribute?: EnhancedProductAttribute + ) { + /* + * By convention, it's a global attribute if the attribute ID is 0. + * For global attributes, the field name suffix + * to set the attribute terms is 'options', + * for local attributes, the field name suffix is 'terms'. + */ + const attributeTermPropName = + attribute?.id === 0 ? 'options' : 'terms'; + + const fieldName = `attributes[${ index }].${ attributeTermPropName }`; + + setValue( fieldName, terms ); + } + /* * Get the attribute ids that are already selected * by other form fields. */ - const attributeBelongTo = values.attributes.map( ( attr ) => - attr ? attr.id : null - ); + const attributeBelongTo = values.attributes + .map( ( attr ) => ( attr ? attr.id : null ) ) + .filter( ( id ) => typeof id === 'number' ); /* * Compute the available attributes to show in the attribute input field, * filtering out the ignored attributes, * marking the disabled ones, - * and setting the takenBy property. + * and setting the `takenBy` property. */ const availableAttributes = attributes ?.filter( @@ -404,7 +363,7 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { attribute.id ), takenBy: attributeBelongTo.indexOf( attribute.id ), - } ) ); + } ) ) as AttributesComboboxControlItem[]; return ( = ( { { values.attributes.map( ( attribute, index ) => ( - - - - ( attr.takenBy && - attr.takenBy < - 0 ) || - attr.takenBy === - index - ) } - isLoading={ - isLoading - } - onChange={ ( - nextAttribute - ) => - selectAttributeHandler( - nextAttribute, - index - ) - } - disabledAttributeMessage={ - disabledAttributeMessage - } - /> - - - { ! attribute || - attribute.id !== 0 ? ( - - 0 - ? '' - : termPlaceholder - } - disabled={ - attribute - ? ! attribute.id - : true - } - attributeId={ - attribute - ? attribute.id - : undefined - } - value={ - attribute === - null || - attribute === - undefined - ? [] - : attribute.terms - } - label={ - valueLabel - } - onChange={ ( - val - ) => - setValue( - 'attributes[' + - index + - '].terms', - val - ) - } - /> - ) : ( - - 0 - ? '' - : termPlaceholder - } - disabled={ - ! attribute.name - } - value={ - attribute.options - } - label={ - valueLabel - } - onChange={ ( - val - ) => - setValue( - 'attributes[' + - index + - '].options', - val - ) - } - /> - ) } - - - - - + index={ index } + attribute={ attribute } + attributePlaceholder={ + attributePlaceholder + } + disabledAttributeMessage={ + disabledAttributeMessage + } + isLoadingAttributes={ + isLoadingAttributes + } + attributes={ + availableAttributes + } + onAttributeSelect={ + selectAttribute + } + termPlaceholder={ + termPlaceholder + } + removeLabel={ removeLabel } + onTermsSelect={ + selectTerms + } + onRemove={ ( + removedIndex + ) => + onRemove( + removedIndex, + values, + setValue + ) + } + termsAutoSelection={ + termsAutoSelection + } + /> ) ) } diff --git a/packages/js/product-editor/src/components/attribute-control/test/new-attribute-modal.spec.tsx b/packages/js/product-editor/src/components/attribute-control/test/new-attribute-modal.spec.tsx deleted file mode 100644 index b80a919955c0..000000000000 --- a/packages/js/product-editor/src/components/attribute-control/test/new-attribute-modal.spec.tsx +++ /dev/null @@ -1,198 +0,0 @@ -/** - * External dependencies - */ -import { render } from '@testing-library/react'; -import { ProductAttributeTerm } from '@woocommerce/data'; -import { createElement } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { NewAttributeModal } from '../new-attribute-modal'; - -let attributeTermOnChange: ( val: ProductAttributeTerm[] ) => void; -jest.mock( '../../attribute-term-input-field', () => ( { - AttributeTermInputField: ( { - onChange, - disabled, - }: { - onChange: ( value: ProductAttributeTerm[] ) => void; - disabled: boolean; - } ) => { - attributeTermOnChange = onChange; - return ( -
- attribute_term_input_field: disabled:{ disabled.toString() } -
- ); - }, -} ) ); - -const attributeTermList: ProductAttributeTerm[] = [ - { - id: 23, - name: 'XXS', - slug: 'xxs', - description: '', - menu_order: 1, - count: 1, - }, - { - id: 22, - name: 'XS', - slug: 'xs', - description: '', - menu_order: 2, - count: 1, - }, - { - id: 17, - name: 'S', - slug: 's', - description: '', - menu_order: 3, - count: 1, - }, - { - id: 18, - name: 'M', - slug: 'm', - description: '', - menu_order: 4, - count: 1, - }, - { - id: 19, - name: 'L', - slug: 'l', - description: '', - menu_order: 5, - count: 1, - }, -]; - -describe( 'NewAttributeModal', () => { - beforeEach( () => { - jest.clearAllMocks(); - } ); - - it( 'should render at-least one row with the attribute dropdown fields', () => { - const { queryAllByText } = render( - {} } - onAdd={ () => {} } - selectedAttributeIds={ [] } - /> - ); - expect( - queryAllByText( 'attribute_term_input_field: disabled:true' ).length - ).toEqual( 1 ); - } ); - - it( 'should allow us to add multiple new rows with the attribute fields', () => { - const { queryAllByText, queryByRole } = render( - {} } - onAdd={ () => {} } - selectedAttributeIds={ [] } - /> - ); - queryByRole( 'button', { name: 'Add another attribute' } )?.click(); - - expect( - queryAllByText( 'attribute_term_input_field: disabled:true' ).length - ).toEqual( 2 ); - queryByRole( 'button', { name: 'Add another attribute' } )?.click(); - - expect( - queryAllByText( 'attribute_term_input_field: disabled:true' ).length - ).toEqual( 3 ); - } ); - - it( 'should allow us to remove the added fields', () => { - const { queryAllByText, queryByRole, queryAllByLabelText } = render( - {} } - onAdd={ () => {} } - selectedAttributeIds={ [] } - /> - ); - - queryByRole( 'button', { name: 'Add another attribute' } )?.click(); - queryByRole( 'button', { name: 'Add another attribute' } )?.click(); - - expect( - queryAllByText( 'attribute_term_input_field: disabled:true' ).length - ).toEqual( 3 ); - - const removeButtons = queryAllByLabelText( 'Remove attribute' ); - - removeButtons[ 0 ].click(); - removeButtons[ 1 ].click(); - - expect( - queryAllByText( 'attribute_term_input_field: disabled:true' ).length - ).toEqual( 1 ); - } ); - - it( 'should not allow us to remove all the rows', () => { - const { queryAllByText, queryAllByLabelText } = render( - {} } - onAdd={ () => {} } - selectedAttributeIds={ [] } - /> - ); - - const removeButtons = queryAllByLabelText( 'Remove attribute' ); - - removeButtons[ 0 ].click(); - - expect( - queryAllByText( 'attribute_term_input_field: disabled:true' ).length - ).toEqual( 1 ); - } ); - - describe( 'onAdd', () => { - it( 'should not return empty attribute rows', () => { - const onAddMock = jest.fn(); - const { queryAllByText, queryByLabelText, queryByRole } = render( - {} } - onAdd={ onAddMock } - selectedAttributeIds={ [] } - /> - ); - - const addAnotherButton = queryByLabelText( - 'Add another attribute' - ); - addAnotherButton?.click(); - addAnotherButton?.click(); - - expect( - queryAllByText( 'attribute_term_input_field: disabled:true' ) - .length - ).toEqual( 3 ); - queryByRole( 'button', { name: 'Add attributes' } )?.click(); - expect( onAddMock ).toHaveBeenCalledWith( [] ); - } ); - - it( 'should add attribute with array of terms', () => { - const onAddMock = jest.fn(); - const { queryByRole } = render( - {} } - onAdd={ onAddMock } - selectedAttributeIds={ [] } - /> - ); - - attributeTermOnChange( [ - attributeTermList[ 0 ], - attributeTermList[ 1 ], - ] ); - queryByRole( 'button', { name: 'Add attributes' } )?.click(); - } ); - } ); -} ); diff --git a/packages/js/product-editor/src/components/attribute-control/types.ts b/packages/js/product-editor/src/components/attribute-control/types.ts index 79c2fc184270..6bd77f85669e 100644 --- a/packages/js/product-editor/src/components/attribute-control/types.ts +++ b/packages/js/product-editor/src/components/attribute-control/types.ts @@ -1,12 +1,16 @@ /** * External dependencies */ -import { ProductProductAttribute } from '@woocommerce/data'; +import { + ProductAttributeTerm, + ProductProductAttribute, +} from '@woocommerce/data'; /** * Internal dependencies */ import { EnhancedProductAttribute } from '../../hooks/use-product-attributes'; +import { AttributesComboboxControlItem } from '../attribute-combobox-field/types'; export type AttributeControlEmptyStateProps = { addAttribute: ( search?: string ) => void; @@ -49,3 +53,33 @@ export type AttributeControlProps = { disabledAttributeMessage?: string; }; }; + +export type AttributeTableRowProps = { + index: number; + attribute: EnhancedProductAttribute | null; + attributePlaceholder: string; + disabledAttributeMessage: string; + + isLoadingAttributes: boolean; + attributes: AttributesComboboxControlItem[]; + + termPlaceholder: string; + termLabel?: string; + termsAutoSelection?: 'first' | 'all'; + + clearButtonDisabled?: boolean; + removeLabel: string; + + onAttributeSelect: ( + attribute: AttributesComboboxControlItem, + index: number + ) => void; + + onTermsSelect: ( + terms: ProductAttributeTerm[] | string[], + index: number, + attribute: EnhancedProductAttribute + ) => void; + + onRemove: ( index: number ) => void; +}; From 821d764b04531c94ba99f11af3380520d70773fd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 10 May 2024 17:13:50 +0200 Subject: [PATCH 52/91] Delete changelog files based on PR 47205 (#47367) Delete changelog files for 47205 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/restrict_paypal_standard | 4 ---- plugins/woocommerce/changelog/restrict_paypal_standard-filter | 4 ---- 2 files changed, 8 deletions(-) delete mode 100644 plugins/woocommerce/changelog/restrict_paypal_standard delete mode 100644 plugins/woocommerce/changelog/restrict_paypal_standard-filter diff --git a/plugins/woocommerce/changelog/restrict_paypal_standard b/plugins/woocommerce/changelog/restrict_paypal_standard deleted file mode 100644 index e4b6ffe66d13..000000000000 --- a/plugins/woocommerce/changelog/restrict_paypal_standard +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Only load the PayPal Standard payment method on stores that have connected their account or have existing PayPal orders. diff --git a/plugins/woocommerce/changelog/restrict_paypal_standard-filter b/plugins/woocommerce/changelog/restrict_paypal_standard-filter deleted file mode 100644 index ccc2f6bb9186..000000000000 --- a/plugins/woocommerce/changelog/restrict_paypal_standard-filter +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Deprecate the woocommerce_should_load_paypal_standard filter used to bypass loading PayPal Standard. From 23773e19f0b4b7b6ac357037c04b51367bad79c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cem=20=C3=9Cnalan?= Date: Fri, 10 May 2024 23:00:05 +0300 Subject: [PATCH 53/91] Freemium: update marketplace product card label (#45982) * Freemium: update marketplace product card label * Add changefile(s) from automation for the following project(s): woocommerce * Add changefile(s) from automation for the following project(s): woocommerce --------- Co-authored-by: github-actions --- .../product-card/product-card-footer.tsx | 35 ++++++++++++++----- .../product-list-content.tsx | 1 + .../components/product-list/types.ts | 2 ++ .../client/marketplace/utils/functions.tsx | 1 + ...-update-in-app-marketplace-new-price-label | 4 +++ 5 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 plugins/woocommerce/changelog/45982-update-in-app-marketplace-new-price-label diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card-footer.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card-footer.tsx index 38dcdd0f0b1a..c3a337464b78 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card-footer.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/product-card/product-card-footer.tsx @@ -67,6 +67,30 @@ function ProductCardFooter( props: { product: Product } ) { // We hardcode this for now while we only display prices in USD. const currencySymbol = '$'; + function getPriceLabel(): string { + if ( product.price === 0 ) { + return __( 'Free download', 'woocommerce' ); + } + + if ( product.freemium_type === 'primary' ) { + return __( 'Free plan available', 'woocommerce' ); + } + + return currencySymbol + product.price; + } + + function getBillingText(): string { + if ( product.freemium_type === 'primary' ) { + return ''; + } + + if ( product.price !== 0 ) { + return __( ' annually', 'woocommerce' ); + } + + return ''; + } + if ( shouldShowAddToStore( product ) ) { return ( <> @@ -83,17 +107,10 @@ function ProductCardFooter( props: { product: Product } ) { <>
- { - // '0' is a free product - product.price === 0 - ? __( 'Free download', 'woocommerce' ) - : currencySymbol + product.price - } + { getPriceLabel() } - { product.price === 0 - ? '' - : __( ' annually', 'woocommerce' ) } + { getBillingText() }
diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.tsx index 9111923f3e86..79bf0e059959 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.tsx @@ -72,6 +72,7 @@ export default function ProductListContent( props: { title: product.title, image: product.image, type: product.type, + freemium_type: product.freemium_type, icon: product.icon, label: product.label, primary_color: product.primary_color, diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts b/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts index 1626e469df2c..1080acf7468b 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts +++ b/plugins/woocommerce-admin/client/marketplace/components/product-list/types.ts @@ -6,6 +6,7 @@ export type SearchAPIProductType = { title: string; image: string; type: ProductType; + freemium_type: 'unset' | 'primary'; excerpt: string; link: string; demo_url: string; @@ -29,6 +30,7 @@ export interface Product { title: string; image: string; type: ProductType; + freemium_type?: 'unset' | 'primary'; description: string; vendorName: string; vendorUrl: string; diff --git a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx index 7378d3264096..98d36d41bd83 100644 --- a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx +++ b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx @@ -129,6 +129,7 @@ async function fetchSearchResults( title: product.title, image: product.image, type: product.type, + freemium_type: product.freemium_type, description: product.excerpt, vendorName: product.vendor_name, vendorUrl: product.vendor_url, diff --git a/plugins/woocommerce/changelog/45982-update-in-app-marketplace-new-price-label b/plugins/woocommerce/changelog/45982-update-in-app-marketplace-new-price-label new file mode 100644 index 000000000000..466c1002c216 --- /dev/null +++ b/plugins/woocommerce/changelog/45982-update-in-app-marketplace-new-price-label @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Show the label for freemium products in the in-app marketpalce \ No newline at end of file From 1ce0f14de268d9678c785eb6a9b83a1934da0de4 Mon Sep 17 00:00:00 2001 From: nigeljamesstevenson <105309450+nigeljamesstevenson@users.noreply.github.com> Date: Sat, 11 May 2024 21:14:06 +0100 Subject: [PATCH 54/91] Update/workflow for rest api slack notifications (#47278) * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-release-highlight-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update README.md * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * update readme.md * workflow updates * Add script to determine milestone date * udpate workflow * udpate workflow * Update to ensure variable is added to * Update release highlight workflow * Update workflows to ensure repo is checked out before referencing script --- .../scripts/determine_milestone_date.sh | 83 +++++++++++++++++++ ...test-assistant-api-rest-change-tracker.yml | 56 ++----------- ...st-assistant-release-highlight-tracker.yml | 56 ++----------- 3 files changed, 97 insertions(+), 98 deletions(-) create mode 100644 .github/workflows/scripts/determine_milestone_date.sh diff --git a/.github/workflows/scripts/determine_milestone_date.sh b/.github/workflows/scripts/determine_milestone_date.sh new file mode 100644 index 000000000000..4302095a5610 --- /dev/null +++ b/.github/workflows/scripts/determine_milestone_date.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# Assuming all necessary environment variables like GITHUB_EVENT_PATH are correctly set + +# Function to calculate the second Tuesday of the given month and year +# as the release day is always the 2nd Tuesday +calculate_second_tuesday() { + year=$1 + month=$2 + + first_of_month="$year-$month-01" + day_of_week=$(date -d "$first_of_month" "+%u") + offset_to_first_tuesday=$(( (9 - day_of_week) % 7 )) + first_tuesday=$(date -d "$first_of_month +$offset_to_first_tuesday days" "+%Y-%m-%d") + # Calculate the second Tuesday by adding 7 days + second_tuesday=$(date -d "$first_tuesday +7 days" "+%Y-%m-%d") + + echo $second_tuesday +} + +# Set the initial values for version calculation +initial_version_major=8 # Major version start +initial_version_minor=8 # Minor version start +initial_year=2024 +initial_month=4 # April, for the 8.8.0 release + +# Assuming the script is run in or after 2024 +current_year=$(date +%Y) # Get the current year + +# Calculate the number of versions to generate based on the current year +additional_versions=$(( (current_year - 2024 + 1) * 12 )) + +# Versions to calculate +versions_to_calculate=$additional_versions + +# Declare the associative array outside the loop +declare -A MILESTONE_DATES + +for (( i=0; i> $GITHUB_ENV + +# Print the array in the desired format for display purposes +echo "MILESTONE_DATES=(" +for version in "${!MILESTONE_DATES[@]}"; do +echo " [\"$version\"]=\"${MILESTONE_DATES[$version]}\"" +done +echo ")" diff --git a/.github/workflows/test-assistant-api-rest-change-tracker.yml b/.github/workflows/test-assistant-api-rest-change-tracker.yml index 2c2e95c7033c..6ead49601569 100644 --- a/.github/workflows/test-assistant-api-rest-change-tracker.yml +++ b/.github/workflows/test-assistant-api-rest-change-tracker.yml @@ -15,6 +15,9 @@ jobs: run: sleep 2m shell: bash + - name: Checkout repository + uses: actions/checkout@v2 + - name: Calculate test date id: calculate_date run: | @@ -40,55 +43,10 @@ jobs: - name: Determine Milestone Date id: get_milestone_date - run: | - #!/bin/bash - - MILESTONE_TITLE="${{ github.event.pull_request.milestone.title }}" - MILESTONE_DATE="Undefined" - - # Mapping of milestone titles to release dates - declare -A MILESTONE_DATES - MILESTONE_DATES=( - ["8.0.0"]="2023-08-08" - ["8.1.0"]="2023-09-12" - ["8.2.0"]="2023-10-10" - ["8.3.0"]="2023-11-14" - ["8.4.0"]="2023-12-12" - ["8.5.0"]="2024-01-09" - ["8.6.0"]="2024-02-13" - ["8.7.0"]="2024-03-12" - ["8.8.0"]="2024-04-09" - ["8.9.0"]="2024-05-14" - ["9.0.0"]="2024-06-11" - ["9.1.0"]="2024-07-09" - ["9.2.0"]="2024-08-13" - ["9.3.0"]="2024-09-10" - ["9.4.0"]="2024-10-08" - ["9.5.0"]="2024-11-12" - ["9.6.0"]="2024-12-10" - ["9.7.0"]="2025-01-14" - ["9.8.0"]="2025-02-11" - ["9.9.0"]="2025-03-11" - ["10.0.0"]="2025-04-08" - ["10.1.0"]="2025-05-13" - ["10.2.0"]="2025-06-10" - ["10.3.0"]="2025-07-08" - ["10.4.0"]="2025-08-12" - ["10.5.0"]="2025-09-09" - ["10.6.0"]="2025-10-14" - ["10.7.0"]="2025-11-11" - ["10.8.0"]="2025-12-09" - ["10.9.0"]="2026-01-13" - ["11.0.0"]="2026-02-10" - ) - - # Check if the milestone title exists in our predefined list and get the date - if [[ -v "MILESTONE_DATES[${MILESTONE_TITLE}]" ]]; then - MILESTONE_DATE=${MILESTONE_DATES[${MILESTONE_TITLE}]} - fi - - # Export for later steps - echo "MILESTONE_DATE=${MILESTONE_DATE}" >> $GITHUB_ENV + working-directory: .github/workflows/scripts + run: bash determine_milestone_date.sh + env: + GITHUB_EVENT_PATH_PULL_REQUEST_MILESTONE_TITLE: ${{ github.event.pull_request.milestone.title }} # Notify Slack Step - name: Notify Slack diff --git a/.github/workflows/test-assistant-release-highlight-tracker.yml b/.github/workflows/test-assistant-release-highlight-tracker.yml index 5ac53d4b0a2d..1e9e38e71a65 100644 --- a/.github/workflows/test-assistant-release-highlight-tracker.yml +++ b/.github/workflows/test-assistant-release-highlight-tracker.yml @@ -15,6 +15,9 @@ jobs: run: sleep 2m shell: bash + - name: Checkout repository + uses: actions/checkout@v2 + - name: Calculate test date id: calculate_date run: | @@ -40,55 +43,10 @@ jobs: - name: Determine Milestone Date id: get_milestone_date - run: | - #!/bin/bash - - MILESTONE_TITLE="${{ github.event.pull_request.milestone.title }}" - MILESTONE_DATE="Undefined" - - # Mapping of milestone titles to release dates - declare -A MILESTONE_DATES - MILESTONE_DATES=( - ["8.0.0"]="2023-08-08" - ["8.1.0"]="2023-09-12" - ["8.2.0"]="2023-10-10" - ["8.3.0"]="2023-11-14" - ["8.4.0"]="2023-12-12" - ["8.5.0"]="2024-01-09" - ["8.6.0"]="2024-02-13" - ["8.7.0"]="2024-03-12" - ["8.8.0"]="2024-04-09" - ["8.9.0"]="2024-05-14" - ["9.0.0"]="2024-06-11" - ["9.1.0"]="2024-07-09" - ["9.2.0"]="2024-08-13" - ["9.3.0"]="2024-09-10" - ["9.4.0"]="2024-10-08" - ["9.5.0"]="2024-11-12" - ["9.6.0"]="2024-12-10" - ["9.7.0"]="2025-01-14" - ["9.8.0"]="2025-02-11" - ["9.9.0"]="2025-03-11" - ["10.0.0"]="2025-04-08" - ["10.1.0"]="2025-05-13" - ["10.2.0"]="2025-06-10" - ["10.3.0"]="2025-07-08" - ["10.4.0"]="2025-08-12" - ["10.5.0"]="2025-09-09" - ["10.6.0"]="2025-10-14" - ["10.7.0"]="2025-11-11" - ["10.8.0"]="2025-12-09" - ["10.9.0"]="2026-01-13" - ["11.0.0"]="2026-02-10" - ) - - # Check if the milestone title exists in our predefined list and get the date - if [[ -v "MILESTONE_DATES[${MILESTONE_TITLE}]" ]]; then - MILESTONE_DATE=${MILESTONE_DATES[${MILESTONE_TITLE}]} - fi - - # Export for later steps - echo "MILESTONE_DATE=${MILESTONE_DATE}" >> $GITHUB_ENV + working-directory: .github/workflows/scripts + run: bash determine_milestone_date.sh + env: + GITHUB_EVENT_PATH_PULL_REQUEST_MILESTONE_TITLE: ${{ github.event.pull_request.milestone.title }} # Notify Slack Step - name: Notify Slack From 0cd8740a85965bbebfc8ab3f709649e4609f3e6b Mon Sep 17 00:00:00 2001 From: nigeljamesstevenson <105309450+nigeljamesstevenson@users.noreply.github.com> Date: Sat, 11 May 2024 22:15:53 +0100 Subject: [PATCH 55/91] Ensure the cherry pick operation only executes against `trunk` (#47312) * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-release-highlight-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update README.md * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update test-assistant-api-rest-change-tracker.yml * Update cherry pick workflow to only execute against trunk * Update workflow to add safety checks before accessing nested properties * Update workflow to add safety checks before accessing nested properties * log outputs for debugging * log outputs for debugging * log outputs for debugging * log outputs for debugging * log outputs for debugging * log outputs for debugging * log outputs for debugging * sync files pre PR --- .github/workflows/cherry-pick.yml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml index c9519aeb2777..3fda61def9d0 100644 --- a/.github/workflows/cherry-pick.yml +++ b/.github/workflows/cherry-pick.yml @@ -39,18 +39,34 @@ jobs: outputs: run: ${{ steps.check.outputs.run }} steps: + - name: Fetch Pull Request Details + if: github.event.issue.pull_request + id: fetch_pr_details + uses: actions/github-script@v6 + with: + script: | + const issue = context.payload.issue; + const pullRequestUrl = issue.pull_request.url; + const prDetails = await github.request(pullRequestUrl); + core.setOutput('base_ref', prDetails.data.base.ref); + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: check id: check uses: actions/github-script@v6 with: script: | + const baseRef = process.env.BASE_REF; + console.log("baseRef:", baseRef); + let run = false; const isManualTrigger = context.payload.inputs && context.payload.inputs.release_branch && context.payload.inputs.release_branch != null; - const isMergedMilestonedIssue = context.payload.issue && context.payload.issue.pull_request != null && context.payload.issue.pull_request.merged_at != null && context.payload.issue.milestone != null; + const isMergedMilestonedIssue = context.payload.issue && context.payload.issue.pull_request != null && context.payload.issue.pull_request.merged_at != null && context.payload.issue.milestone != null && baseRef == 'trunk'; - const isMergedMilestonedPR = context.payload.pull_request && context.payload.pull_request != null && context.payload.pull_request.merged == true && context.payload.pull_request.milestone != null; + const isMergedMilestonedPR = context.payload.pull_request && context.payload.pull_request != null && context.payload.pull_request.merged == true && context.payload.pull_request.milestone != null && baseRef == 'trunk'; const isBot = context.payload.pull_request && ( context.payload.pull_request.user.login == 'github-actions[bot]' || context.payload.pull_request.user.type == 'Bot' ); @@ -59,6 +75,9 @@ jobs: } else { core.setOutput( 'run', 'false' ); } + env: + BASE_REF: ${{ steps.fetch_pr_details.outputs.base_ref }} + prep: name: Prep inputs runs-on: ubuntu-20.04 @@ -363,4 +382,4 @@ jobs: Release lead please review: ${{ steps.cherry-pick-pr.outputs.cherry-pick-pr }} - ${{ steps.deletion-pr.outputs.deletion-pr }} + ${{ steps.deletion-pr.outputs.deletion-pr }} \ No newline at end of file From 2898efa648ada5e5b94868eec5c87037529c4fa0 Mon Sep 17 00:00:00 2001 From: RJ <27843274+rjchow@users.noreply.github.com> Date: Mon, 13 May 2024 09:52:49 +1000 Subject: [PATCH 56/91] add: lys badge tracks (#46509) --- .../client/launch-your-store/status/index.tsx | 16 ++++++++ .../changelog/add-lys-badge-tracks | 4 ++ .../src/Admin/Features/LaunchYourStore.php | 38 ++++++++++++------- 3 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-lys-badge-tracks diff --git a/plugins/woocommerce-admin/client/launch-your-store/status/index.tsx b/plugins/woocommerce-admin/client/launch-your-store/status/index.tsx index b01ea94ded49..2e333b28506b 100644 --- a/plugins/woocommerce-admin/client/launch-your-store/status/index.tsx +++ b/plugins/woocommerce-admin/client/launch-your-store/status/index.tsx @@ -6,6 +6,7 @@ import { Icon, moreVertical, edit, cog } from '@wordpress/icons'; import { Dropdown, Button, MenuGroup, MenuItem } from '@wordpress/components'; import { getAdminLink, getSetting } from '@woocommerce/settings'; import classnames from 'classnames'; +import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies @@ -66,6 +67,16 @@ export const LaunchYourStoreStatus = ( { <> { + recordEvent( + 'launch_your_store_badge_menu_manage_site_visibility_click', + { + site_visibility: isComingSoon + ? 'coming_soon' + : 'live', + } + ); + } } // @ts-expect-error Prop gets passed down to underlying button https://developer.wordpress.org/block-editor/reference-guides/components/menu-item/#props href={ getAdminLink( 'admin.php?page=wc-settings&tab=site-visibility' @@ -80,6 +91,11 @@ export const LaunchYourStoreStatus = ( { { isComingSoon && getSetting( 'currentThemeIsFSETheme' ) && ( { + recordEvent( + 'launch_your_store_badge_menu_customize_coming_soon_click' + ); + } } // @ts-expect-error Prop gets passed down to underlying button https://developer.wordpress.org/block-editor/reference-guides/components/menu-item/#props href={ COMING_SOON_PAGE_EDITOR_LINK diff --git a/plugins/woocommerce/changelog/add-lys-badge-tracks b/plugins/woocommerce/changelog/add-lys-badge-tracks new file mode 100644 index 000000000000..69a5410e1415 --- /dev/null +++ b/plugins/woocommerce/changelog/add-lys-badge-tracks @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add tracks events for the LYS badge diff --git a/plugins/woocommerce/src/Admin/Features/LaunchYourStore.php b/plugins/woocommerce/src/Admin/Features/LaunchYourStore.php index 80865f089d54..b6528fcab0a0 100644 --- a/plugins/woocommerce/src/Admin/Features/LaunchYourStore.php +++ b/plugins/woocommerce/src/Admin/Features/LaunchYourStore.php @@ -28,30 +28,42 @@ public function __construct() { * @return void */ public function save_site_visibility_options() { - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'woocommerce-settings' ) ) { + $nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : ''; + if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'woocommerce-settings' ) ) { return; } + // options to allowed update and their allowed values. $options = array( 'woocommerce_coming_soon' => array( 'yes', 'no' ), 'woocommerce_store_pages_only' => array( 'yes', 'no' ), 'woocommerce_private_link' => array( 'yes', 'no' ), ); - $at_least_one_saved = false; - foreach ( $options as $name => $option ) { - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - if ( isset( $_POST[ $name ] ) && in_array( $_POST[ $name ], $option, true ) ) { - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - update_option( $name, wp_unslash( $_POST[ $name ] ) ); - $at_least_one_saved = true; - } - } + $event_data = array(); + + foreach ( $options as $name => $allowed_values ) { + $current_value = get_option( $name, 'not set' ); + $new_value = $current_value; - if ( $at_least_one_saved ) { - wc_admin_record_tracks_event( 'site_visibility_saved' ); + if ( isset( $_POST[ $name ] ) ) { + $input_value = sanitize_text_field( wp_unslash( $_POST[ $name ] ) ); + + // no-op if input value is invalid. + if ( in_array( $input_value, $allowed_values, true ) ) { + update_option( $name, $input_value ); + $new_value = $input_value; + + // log the transition if there is one. + if ( $current_value !== $new_value ) { + $enabled_or_disabled = 'yes' === $new_value ? 'enabled' : 'disabled'; + $event_data[ $name . '_toggled' ] = $enabled_or_disabled; + } + } + } + $event_data[ $name ] = $new_value; } + wc_admin_record_tracks_event( 'site_visibility_saved', $event_data ); } /** From 6cf09f30a8a3f79e8af0e6f726dd232b0a3fca0f Mon Sep 17 00:00:00 2001 From: Moon Date: Mon, 13 May 2024 17:15:47 +1200 Subject: [PATCH 57/91] LYS - Use flow layout for the coming soon template (#47335) * Use flow layout * Tmp fix - fix broken test * Add changefile(s) from automation for the following project(s): woocommerce * Use flex layout --------- Co-authored-by: github-actions --- .../changelog/47335-update-47193-fix-windowed-text-in-tt4 | 4 ++++ plugins/woocommerce/patterns/coming-soon-store-only.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/47335-update-47193-fix-windowed-text-in-tt4 diff --git a/plugins/woocommerce/changelog/47335-update-47193-fix-windowed-text-in-tt4 b/plugins/woocommerce/changelog/47335-update-47193-fix-windowed-text-in-tt4 new file mode 100644 index 000000000000..7c75a27815b1 --- /dev/null +++ b/plugins/woocommerce/changelog/47335-update-47193-fix-windowed-text-in-tt4 @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +LYS - Use flow layout for the coming soon template \ No newline at end of file diff --git a/plugins/woocommerce/patterns/coming-soon-store-only.php b/plugins/woocommerce/patterns/coming-soon-store-only.php index c98d786063f9..addd3aeec7f1 100644 --- a/plugins/woocommerce/patterns/coming-soon-store-only.php +++ b/plugins/woocommerce/patterns/coming-soon-store-only.php @@ -19,7 +19,7 @@ } ?> - +
From c4ca5f87d9da538de529ddf256da4eb0d55613e5 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Mon, 13 May 2024 13:38:54 +0800 Subject: [PATCH 58/91] Reenable global styles for coming soon entire site template (#47388) * Reenable global styles for coming soon pages * Changelog * Update fix-lys-reenable-global-styles --- .../changelog/fix-lys-reenable-global-styles | 4 +++ .../ComingSoon/ComingSoonRequestHandler.php | 29 +------------------ 2 files changed, 5 insertions(+), 28 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-lys-reenable-global-styles diff --git a/plugins/woocommerce/changelog/fix-lys-reenable-global-styles b/plugins/woocommerce/changelog/fix-lys-reenable-global-styles new file mode 100644 index 000000000000..b333b8dd96c8 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-lys-reenable-global-styles @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Reenable global styles in coming soon entire site template diff --git a/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php b/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php index cfd4642dcf08..f0ace0e7420f 100644 --- a/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php +++ b/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php @@ -26,35 +26,8 @@ class ComingSoonRequestHandler { final public function init( ComingSoonHelper $coming_soon_helper ) { $this->coming_soon_helper = $coming_soon_helper; add_filter( 'template_include', array( $this, 'handle_template_include' ) ); - add_action( 'wp_enqueue_scripts', array( $this, 'deregister_unnecessary_styles' ), 100 ); } - /** - * Deregisters unnecessary styles for the coming soon page. - * - * @return void - */ - public function deregister_unnecessary_styles() { - global $wp; - - if ( ! $this->should_show_coming_soon( $wp ) ) { - return; - } - - if ( $this->coming_soon_helper->is_site_coming_soon() ) { - global $wp_styles; - - foreach ( $wp_styles->registered as $handle => $registered_style ) { - // Deregister all styles except for block styles. - if ( - strpos( $handle, 'wp-block' ) !== 0 && - strpos( $handle, 'core-block' ) !== 0 - ) { - wp_deregister_style( $handle ); - } - } - } - } /** * Replaces the page template with a 'coming soon' when the site is in coming soon mode. @@ -75,7 +48,7 @@ public function handle_template_include( $template ) { nocache_headers(); add_theme_support( 'block-templates' ); - wp_dequeue_style( 'global-styles' ); + $coming_soon_template = get_query_template( 'coming-soon' ); if ( ! wc_current_theme_is_fse_theme() && $this->coming_soon_helper->is_store_coming_soon() ) { From db804f4193559aa5a4379ce9a6c501a4ff3a3675 Mon Sep 17 00:00:00 2001 From: Prahesa Kusuma Setia Date: Mon, 13 May 2024 13:45:43 +0700 Subject: [PATCH 59/91] Tracks event for WooCommerce.com connect notice (#47003) * add tracks event in the wc settings connect notice * add clicked and dismissed tracks event in the marketplace connect notice * add shown tracks event in the marketplace connect notice * add changelog * fix linter error * Add Tracks event when connect-notice in plugin is shown and clicked * Add docblock for maybe_enqueue_scripts_for_connect_notice_in_plugins --------- Co-authored-by: Akeda Bagus --- .../connect-notice/connect-notice.tsx | 26 ++++++++++++++++--- .../marketplace/components/notice/notice.tsx | 12 ++++++++- .../woo-connect-notice/index.js | 14 ++++++++++ .../woo-plugin-update-connect-notice/index.js | 20 ++++++++++++++ plugins/woocommerce-admin/webpack.config.js | 1 + .../47003-add-tracks-woo-connect-notice | 4 +++ .../admin/helper/class-wc-helper-updater.php | 5 ++-- .../woocommerce/src/Admin/PluginsHelper.php | 18 +++++++++++-- 8 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 plugins/woocommerce-admin/client/wp-admin-scripts/woo-plugin-update-connect-notice/index.js create mode 100644 plugins/woocommerce/changelog/47003-add-tracks-woo-connect-notice diff --git a/plugins/woocommerce-admin/client/marketplace/components/connect-notice/connect-notice.tsx b/plugins/woocommerce-admin/client/marketplace/components/connect-notice/connect-notice.tsx index 91cdb18d727c..179c646681a2 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/connect-notice/connect-notice.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/connect-notice/connect-notice.tsx @@ -3,6 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; +import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies @@ -58,17 +59,34 @@ export default function ConnectNotice(): JSX.Element | null { ) ); + const handleClick = () => { + recordEvent( 'woo_connect_notice_in_marketplace_clicked' ); + return true; + }; + + const handleClose = () => { + localStorage.setItem( localStorageKey, new Date().toString() ); + recordEvent( 'woo_connect_notice_in_marketplace_dismissed' ); + }; + + const handleLoad = () => { + recordEvent( 'woo_connect_notice_in_marketplace_shown' ); + }; + return ( { - localStorage.setItem( localStorageKey, new Date().toString() ); - } } + onClose={ handleClose } + onLoad={ handleLoad } > - diff --git a/plugins/woocommerce-admin/client/marketplace/components/notice/notice.tsx b/plugins/woocommerce-admin/client/marketplace/components/notice/notice.tsx index fc3165ff0fe5..da78c7e01163 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/notice/notice.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/notice/notice.tsx @@ -2,7 +2,7 @@ * External dependencies */ import classNames from 'classnames'; -import { useState } from '@wordpress/element'; +import { useEffect, useState } from '@wordpress/element'; import { Icon, check, closeSmall, info, percent } from '@wordpress/icons'; // See: https://wordpress.github.io/gutenberg/?path=/docs/icons-icon--docs /** @@ -19,6 +19,7 @@ export interface NoticeProps { isDismissible: boolean; variant: string; onClose?: () => void; + onLoad?: () => void; } type IconKey = keyof typeof iconMap; @@ -39,6 +40,7 @@ export default function Notice( props: NoticeProps ): JSX.Element | null { isDismissible = true, variant = 'info', onClose, + onLoad, } = props; const [ isVisible, setIsVisible ] = useState( localStorage.getItem( `wc-marketplaceNoticeClosed-${ id }` ) !== 'true' @@ -52,6 +54,14 @@ export default function Notice( props: NoticeProps ): JSX.Element | null { } }; + useEffect( () => { + if ( isVisible && typeof onLoad === 'function' ) { + onLoad(); + } + // only run once + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ isVisible ] ); + if ( ! isVisible ) return null; const classes = classNames( diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/woo-connect-notice/index.js b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-connect-notice/index.js index 34c05454eda9..3f210426bf3e 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/woo-connect-notice/index.js +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-connect-notice/index.js @@ -1,6 +1,12 @@ +/** + * External dependencies + */ +import { recordEvent } from '@woocommerce/tracks'; + window.jQuery( document ).ready( function () { // hide the notice when the customer clicks the dismiss button up until 1 month, then it will be shown again. const wooConnectNoticeSelector = '.woo-connect-notice'; + const wooConnectNoticeLinkSelector = '#woo-connect-notice-url'; const localStorageKey = 'woo-connect-notice-settings-dismissed'; window @@ -10,8 +16,14 @@ window.jQuery( document ).ready( function () { localStorageKey, new Date().toString() ); + recordEvent( 'woo_connect_notice_in_settings_dismissed' ); } ); + window.jQuery( wooConnectNoticeLinkSelector ).on( 'click', function () { + recordEvent( 'woo_connect_notice_in_settings_clicked' ); + return true; + } ); + let shouldHideNotice = false; const savedDismissedDate = window.localStorage.getItem( localStorageKey ); @@ -28,5 +40,7 @@ window.jQuery( document ).ready( function () { if ( shouldHideNotice ) { window.jQuery( wooConnectNoticeSelector ).remove(); + } else { + recordEvent( 'woo_connect_notice_in_settings_shown' ); } } ); diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/woo-plugin-update-connect-notice/index.js b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-plugin-update-connect-notice/index.js new file mode 100644 index 000000000000..23977a34254f --- /dev/null +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-plugin-update-connect-notice/index.js @@ -0,0 +1,20 @@ +/** + * External dependencies + */ +import domReady from '@wordpress/dom-ready'; +import { recordEvent } from '@woocommerce/tracks'; + +domReady( () => { + const connectYourStoreLinks = document.querySelectorAll( + '.woocommerce-connect-your-store' + ); + + if ( connectYourStoreLinks.length > 0 ) { + recordEvent( 'woo_connect_notice_in_plugins_shown' ); + connectYourStoreLinks.forEach( ( link ) => { + link.addEventListener( 'click', function () { + recordEvent( 'woo_connect_notice_in_plugins_clicked' ); + } ); + } ); + } +} ); diff --git a/plugins/woocommerce-admin/webpack.config.js b/plugins/woocommerce-admin/webpack.config.js index 739d1868c3e2..01b53eeb3685 100644 --- a/plugins/woocommerce-admin/webpack.config.js +++ b/plugins/woocommerce-admin/webpack.config.js @@ -74,6 +74,7 @@ const wpAdminScripts = [ 'command-palette', 'command-palette-analytics', 'woo-connect-notice', + 'woo-plugin-update-connect-notice', ]; const getEntryPoints = () => { const entryPoints = { diff --git a/plugins/woocommerce/changelog/47003-add-tracks-woo-connect-notice b/plugins/woocommerce/changelog/47003-add-tracks-woo-connect-notice new file mode 100644 index 000000000000..70653db7652f --- /dev/null +++ b/plugins/woocommerce/changelog/47003-add-tracks-woo-connect-notice @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Send tracks event for woocommerce.com connect notices diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php index 4cd4310a3e81..8258a4af2c70 100644 --- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php +++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php @@ -184,10 +184,11 @@ public static function add_connect_woocom_plugin_message() { printf( wp_kses( /* translators: 1: Woo Update Manager plugin install URL */ - __( ' Connect your store to woocommerce.com to update.', 'woocommerce' ), + __( ' Connect your store to woocommerce.com to update.', 'woocommerce' ), array( 'a' => array( - 'href' => array(), + 'href' => array(), + 'class' => array(), ), ) ), diff --git a/plugins/woocommerce/src/Admin/PluginsHelper.php b/plugins/woocommerce/src/Admin/PluginsHelper.php index ce7b0ee844d4..ab24dc5f87e6 100644 --- a/plugins/woocommerce/src/Admin/PluginsHelper.php +++ b/plugins/woocommerce/src/Admin/PluginsHelper.php @@ -40,6 +40,7 @@ public static function init() { add_action( 'woocommerce_plugins_activate_callback', array( __CLASS__, 'activate_plugins' ), 10, 2 ); add_action( 'admin_notices', array( __CLASS__, 'maybe_show_connect_notice_in_plugin_list' ) ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_scripts_for_connect_notice' ) ); + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_scripts_for_connect_notice_in_plugins' ) ); } /** @@ -571,7 +572,7 @@ public static function maybe_show_connect_notice_in_plugin_list() { $notice_string .= sprintf( /* translators: %s: Connect page URL */ - __( 'Connect your store to WooCommerce.com to get updates and streamlined support for your subscriptions.', 'woocommerce' ), + __( 'Connect your store to WooCommerce.com to get updates and streamlined support for your subscriptions.', 'woocommerce' ), esc_url( $connect_page_url ) ); @@ -581,7 +582,7 @@ public static function maybe_show_connect_notice_in_plugin_list() { } /** - * Enqueue scripts for connect notice. + * Enqueue scripts for connect notice in WooCommerce settings page. * * @return void */ @@ -600,4 +601,17 @@ public static function maybe_enqueue_scripts_for_connect_notice() { wp_enqueue_script( 'woo-connect-notice' ); } + /** + * Enqueue scripts for connect notice in plugin list page. + * + * @return void + */ + public static function maybe_enqueue_scripts_for_connect_notice_in_plugins() { + if ( 'plugins' !== get_current_screen()->id ) { + return; + } + + WCAdminAssets::register_script( 'wp-admin-scripts', 'woo-plugin-update-connect-notice' ); + wp_enqueue_script( 'woo-plugin-update-connect-notice' ); + } } From b28adc17036e33c17c1f4b70e37f3c3c9f37e6e9 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Mon, 13 May 2024 15:51:42 +0800 Subject: [PATCH 60/91] =?UTF-8?q?Use=20a=20real=20em=20dash=20character=20?= =?UTF-8?q?(=E2=80=94)=20in=20the=20coming=20soon=20site=20template=20(#47?= =?UTF-8?q?394)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use a real em dash character (—) in the coming soon template * Add changelog --- plugins/woocommerce/changelog/update-use-real-en-dash | 4 ++++ plugins/woocommerce/patterns/coming-soon-entire-site.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/update-use-real-en-dash diff --git a/plugins/woocommerce/changelog/update-use-real-en-dash b/plugins/woocommerce/changelog/update-use-real-en-dash new file mode 100644 index 000000000000..c73e8f8ca27d --- /dev/null +++ b/plugins/woocommerce/changelog/update-use-real-en-dash @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Use a real em dash character (—) in the coming soon template diff --git a/plugins/woocommerce/patterns/coming-soon-entire-site.php b/plugins/woocommerce/patterns/coming-soon-entire-site.php index 2f33bbb92d00..8bdfc4f77dd4 100644 --- a/plugins/woocommerce/patterns/coming-soon-entire-site.php +++ b/plugins/woocommerce/patterns/coming-soon-entire-site.php @@ -39,7 +39,7 @@
-

Pardon our dust! We're working on something amazing -- check back soon!

+

Pardon our dust! We're working on something amazing — check back soon!

From 49c379c5a7de603a6cc0005981e67ecf72c5a4ae Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Mon, 13 May 2024 05:20:07 -0300 Subject: [PATCH 61/91] Fix typo in the Exposing your data in the Store API documentation (#43488) * Fix basic usage code snippet example in extend-rest-api-add-data.md * Add changefile(s) from automation for the following project(s): woocommerce-blocks --------- Co-authored-by: github-actions --- .../extensibility/rest-api/extend-rest-api-add-data.md | 4 ++-- plugins/woocommerce/changelog/43488-patch-1 | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/43488-patch-1 diff --git a/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/rest-api/extend-rest-api-add-data.md b/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/rest-api/extend-rest-api-add-data.md index ef9abeb3577d..44a8eade94c4 100644 --- a/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/rest-api/extend-rest-api-add-data.md +++ b/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/rest-api/extend-rest-api-add-data.md @@ -55,10 +55,10 @@ function my_schema_callback() { return [ 'custom-key' => [ 'description' => __( 'My custom data', 'plugin-namespace' ), - 'type' => 'string' + 'type' => 'string', 'readonly' => true, ] - ] + ]; } ``` diff --git a/plugins/woocommerce/changelog/43488-patch-1 b/plugins/woocommerce/changelog/43488-patch-1 new file mode 100644 index 000000000000..06b859056ca0 --- /dev/null +++ b/plugins/woocommerce/changelog/43488-patch-1 @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +This PR fixes a minor typo in the Exposing your data in the Store API doc. \ No newline at end of file From f61bdd51d5b7d2432d42defa934e05b64e2015db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9stor=20Soriano?= Date: Mon, 13 May 2024 11:28:09 +0200 Subject: [PATCH 62/91] Fix fatal error when trying to download log files (#47398) * Fix inverted logif for readable file check in FileExporter::emit_file * Fix: instance of WP_Error being passed to PageController::handle_list_table_bulk_actions --- plugins/woocommerce/changelog/pr-47398 | 4 ++++ .../Internal/Admin/Logging/FileV2/FileExporter.php | 8 ++++---- .../src/Internal/Admin/Logging/PageController.php | 14 +++++++------- 3 files changed, 15 insertions(+), 11 deletions(-) create mode 100644 plugins/woocommerce/changelog/pr-47398 diff --git a/plugins/woocommerce/changelog/pr-47398 b/plugins/woocommerce/changelog/pr-47398 new file mode 100644 index 000000000000..f320665237a2 --- /dev/null +++ b/plugins/woocommerce/changelog/pr-47398 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix fatal error when trying to download log files diff --git a/plugins/woocommerce/src/Internal/Admin/Logging/FileV2/FileExporter.php b/plugins/woocommerce/src/Internal/Admin/Logging/FileV2/FileExporter.php index 92b0045e8709..024639fc3546 100644 --- a/plugins/woocommerce/src/Internal/Admin/Logging/FileV2/FileExporter.php +++ b/plugins/woocommerce/src/Internal/Admin/Logging/FileV2/FileExporter.php @@ -52,7 +52,7 @@ public function __construct( string $path, string $alternate_filename = '' ) { public function emit_file() { try { $filesystem = FilesystemUtil::get_wp_filesystem(); - $is_readable = ! $filesystem->is_file( $this->path ) || ! $filesystem->is_readable( $this->path ); + $is_readable = $filesystem->is_file( $this->path ) && $filesystem->is_readable( $this->path ); } catch ( Exception $exception ) { $is_readable = false; } @@ -106,11 +106,11 @@ private function send_headers(): void { * @return void */ private function send_contents(): void { - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- No suitable alternative. + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- No suitable alternative. $stream = fopen( $this->path, 'rb' ); while ( is_resource( $stream ) && ! feof( $stream ) ) { - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fread -- No suitable alternative. + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- No suitable alternative. $chunk = fread( $stream, self::CHUNK_SIZE ); if ( is_string( $chunk ) ) { @@ -119,7 +119,7 @@ private function send_contents(): void { } } - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative. + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- No suitable alternative. fclose( $stream ); } diff --git a/plugins/woocommerce/src/Internal/Admin/Logging/PageController.php b/plugins/woocommerce/src/Internal/Admin/Logging/PageController.php index ca7b2b48b2e2..0162c1ec44bc 100644 --- a/plugins/woocommerce/src/Internal/Admin/Logging/PageController.php +++ b/plugins/woocommerce/src/Internal/Admin/Logging/PageController.php @@ -104,7 +104,7 @@ private function notices() { if ( ! $this->settings->logging_is_enabled() ) { add_action( 'admin_notices', - function() { + function () { ?>

@@ -395,7 +395,7 @@ private function render_single_file_view(): void { if ( is_string( $line ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- format_line does the escaping. echo $this->format_line( $line, $line_number ); - $line_number ++; + ++$line_number; } ?> @@ -464,7 +464,7 @@ public function get_query_params( array $param_keys = array() ): array { array( 'file_id' => array( 'filter' => FILTER_CALLBACK, - 'options' => function( $file_id ) { + 'options' => function ( $file_id ) { return sanitize_file_name( wp_unslash( $file_id ) ); }, ), @@ -484,13 +484,13 @@ public function get_query_params( array $param_keys = array() ): array { ), 'search' => array( 'filter' => FILTER_CALLBACK, - 'options' => function( $search ) { + 'options' => function ( $search ) { return esc_html( wp_unslash( $search ) ); }, ), 'source' => array( 'filter' => FILTER_CALLBACK, - 'options' => function( $source ) { + 'options' => function ( $source ) { return File::sanitize_source( wp_unslash( $source ) ); }, ), @@ -624,7 +624,7 @@ private function handle_list_table_bulk_actions( string $view ): void { } if ( is_wp_error( $export_error ) ) { - wp_die( wp_kses_post( $export_error ) ); + wp_die( wp_kses_post( $export_error->get_error_message() ) ); } break; case 'delete': @@ -654,7 +654,7 @@ private function handle_list_table_bulk_actions( string $view ): void { if ( is_numeric( $deleted ) ) { add_action( 'admin_notices', - function() use ( $deleted ) { + function () use ( $deleted ) { ?>

From b164d193282f7edbc4bc5f493f7e7ed6459cec34 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 12:09:56 +0200 Subject: [PATCH 63/91] Delete changelog files based on PR 47398 (#47405) Delete changelog files for 47398 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/pr-47398 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/pr-47398 diff --git a/plugins/woocommerce/changelog/pr-47398 b/plugins/woocommerce/changelog/pr-47398 deleted file mode 100644 index f320665237a2..000000000000 --- a/plugins/woocommerce/changelog/pr-47398 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix fatal error when trying to download log files From 7796c13b65e083384071d40f692d9789e5833380 Mon Sep 17 00:00:00 2001 From: Justin Palmer <228780+layoutd@users.noreply.github.com> Date: Mon, 13 May 2024 12:24:32 +0200 Subject: [PATCH 64/91] Make method getAttributionData globally accessible (#46965) * Make method to getAttributionData externally accessible * Include checks for sjbs availability * Failsafe for the custom html element if sbjs isn't available * Changelog --- .../tweak-surface-order-attribution-data-js | 4 +++ .../legacy/js/frontend/order-attribution.js | 34 ++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 plugins/woocommerce/changelog/tweak-surface-order-attribution-data-js diff --git a/plugins/woocommerce/changelog/tweak-surface-order-attribution-data-js b/plugins/woocommerce/changelog/tweak-surface-order-attribution-data-js new file mode 100644 index 000000000000..33d48a31d603 --- /dev/null +++ b/plugins/woocommerce/changelog/tweak-surface-order-attribution-data-js @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Make order attribution data globally accessible client side. diff --git a/plugins/woocommerce/client/legacy/js/frontend/order-attribution.js b/plugins/woocommerce/client/legacy/js/frontend/order-attribution.js index 79411e928e1d..9dad4dc842cb 100644 --- a/plugins/woocommerce/client/legacy/js/frontend/order-attribution.js +++ b/plugins/woocommerce/client/legacy/js/frontend/order-attribution.js @@ -15,14 +15,15 @@ /** * Get the order attribution data. * - * Returns object full of `null`s if tracking is disabled. + * Returns object full of `null`s if tracking is disabled or if sourcebuster.js is blocked. * * @returns {Object} Schema compatible object. */ - function getData() { - const accessor = params.allowTracking ? propertyAccessor : returnNull; + wc_order_attribution.getAttributionData = function() { + const accessor = params.allowTracking && isSbjsAvailable() ? propertyAccessor : returnNull; + const getter = isSbjsAvailable() ? sbjs.get : {}; const entries = Object.entries( wc_order_attribution.fields ) - .map( ( [ key, property ] ) => [ key, accessor( sbjs.get, property ) ] ); + .map( ( [ key, property ] ) => [ key, accessor( getter, property ) ] ); return Object.fromEntries( entries ); } @@ -55,6 +56,15 @@ } } + /** + * Determin whether sourcebuster.js is available. + * + * @returns {boolean} Whether sourcebuster.js is available. + */ + function isSbjsAvailable() { + return typeof sbjs !== 'undefined'; + } + /** * Initialize sourcebuster & set data, or clear cookies & data. * @@ -65,7 +75,7 @@ if ( ! allow ) { // Reset cookies, and clear form data. removeTrackingCookies(); - } else if ( typeof sbjs === 'undefined' ) { + } else if ( ! isSbjsAvailable() ) { return; // Do nothing, as sourcebuster.js is not loaded. } else { // If not done yet, initialize sourcebuster.js which populates `sbjs.get` object. @@ -75,7 +85,7 @@ timezone_offset: '0', // utc } ); } - const values = getData(); + const values = wc_order_attribution.getAttributionData(); updateFormValues( values ); updateCheckoutBlockData( values ); } @@ -117,7 +127,7 @@ // Update checkout block data once more if the checkout store was loaded after this script. const unsubscribe = window.wp.data.subscribe( function () { unsubscribe(); - updateCheckoutBlockData( getData() ); + updateCheckoutBlockData( wc_order_attribution.getAttributionData() ); }, CHECKOUT_STORE_KEY ); } }; @@ -142,10 +152,10 @@ this._fieldNames = Object.keys( wc_order_attribution.fields ); // Allow values to be lazily set before CE upgrade. if ( this.hasOwnProperty( '_values' ) ) { - let values = this.values; - // Restore the setter. - delete this.values; - this.values = values || {}; + let values = this.values; + // Restore the setter. + delete this.values; + this.values = values || {}; } } /** @@ -157,7 +167,7 @@ connectedCallback() { let inputs = ''; for( const fieldName of this._fieldNames ) { - const value = stringifyFalsyInputValue( this.values[ fieldName ] ); + const value = stringifyFalsyInputValue( ( this.values && this.values[ fieldName ] ) || '' ); inputs += ``; } this.innerHTML = inputs; From 731312764972ca1714c52f0dfff46912bc1b4ccb Mon Sep 17 00:00:00 2001 From: Patricia Hillebrandt Date: Mon, 13 May 2024 12:28:20 +0200 Subject: [PATCH 65/91] CYS > Add a feature flag for full composability and the Pattern Toolkit (#47392) * Add a feature flag for full composability and the Pattern Toolkit projects. * Add changefile(s) from automation for the following project(s): woocommerce --------- Co-authored-by: github-actions --- .../changelog/47392-add-ptk-full-composability-feature-flag | 4 ++++ plugins/woocommerce/client/admin/config/core.json | 1 + plugins/woocommerce/client/admin/config/development.json | 1 + 3 files changed, 6 insertions(+) create mode 100644 plugins/woocommerce/changelog/47392-add-ptk-full-composability-feature-flag diff --git a/plugins/woocommerce/changelog/47392-add-ptk-full-composability-feature-flag b/plugins/woocommerce/changelog/47392-add-ptk-full-composability-feature-flag new file mode 100644 index 000000000000..e260cc0264ca --- /dev/null +++ b/plugins/woocommerce/changelog/47392-add-ptk-full-composability-feature-flag @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Add a feature flag for full composability and the Pattern Toolkit \ No newline at end of file diff --git a/plugins/woocommerce/client/admin/config/core.json b/plugins/woocommerce/client/admin/config/core.json index 3a2a476a3bc5..f4e182c20a51 100644 --- a/plugins/woocommerce/client/admin/config/core.json +++ b/plugins/woocommerce/client/admin/config/core.json @@ -19,6 +19,7 @@ "new-product-management-experience": false, "onboarding": true, "onboarding-tasks": true, + "pattern-toolkit-full-composability": false, "product-variation-management": true, "product-virtual-downloadable": true, "product-external-affiliate": true, diff --git a/plugins/woocommerce/client/admin/config/development.json b/plugins/woocommerce/client/admin/config/development.json index a068041c4064..95372fdbc312 100644 --- a/plugins/woocommerce/client/admin/config/development.json +++ b/plugins/woocommerce/client/admin/config/development.json @@ -19,6 +19,7 @@ "new-product-management-experience": false, "onboarding": true, "onboarding-tasks": true, + "pattern-toolkit-full-composability": false, "payment-gateway-suggestions": true, "product-variation-management": true, "product-virtual-downloadable": true, From 69daa6187732f381182a59eecefa41cad1ac6f9a Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 13 May 2024 13:23:19 +0200 Subject: [PATCH 66/91] Add aria-label to My Account Navigation Item (#43696) Co-authored-by: Niels Lange Co-authored-by: github-actions --- plugins/woocommerce/changelog/43696-patch-3 | 4 ++++ plugins/woocommerce/templates/myaccount/navigation.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/43696-patch-3 diff --git a/plugins/woocommerce/changelog/43696-patch-3 b/plugins/woocommerce/changelog/43696-patch-3 new file mode 100644 index 000000000000..49f6bd0e830c --- /dev/null +++ b/plugins/woocommerce/changelog/43696-patch-3 @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Add aria-label to account page \ No newline at end of file diff --git a/plugins/woocommerce/templates/myaccount/navigation.php b/plugins/woocommerce/templates/myaccount/navigation.php index adc867779318..54373433691a 100644 --- a/plugins/woocommerce/templates/myaccount/navigation.php +++ b/plugins/woocommerce/templates/myaccount/navigation.php @@ -12,7 +12,7 @@ * * @see https://woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 2.6.0 + * @version 9.0.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -22,7 +22,7 @@ do_action( 'woocommerce_before_account_navigation' ); ?> -

diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.block_theme.spec.ts index 3a34b3c063c9..370faff08d2c 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.block_theme.spec.ts @@ -24,6 +24,17 @@ const test = base.extend< { test.describe( 'Product Filter: Price Filter Block', () => { test.describe( 'frontend', () => { + test( 'clear button is not shown on initial page load', async ( { + page, + defaultBlockPost, + } ) => { + await page.goto( defaultBlockPost.link ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeHidden(); + } ); + test( 'With price filters applied it shows the correct price', async ( { page, defaultBlockPost, @@ -62,6 +73,81 @@ test.describe( 'Product Filter: Price Filter Block', () => { await expect( maxPriceThumb ).toHaveValue( '67' ); } ); + test( 'clear button appears after a filter is applied', async ( { + page, + defaultBlockPost, + } ) => { + await page.goto( + `${ defaultBlockPost.link }?min_price=20&max_price=67` + ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeVisible(); + } ); + + test( 'clear button hides after deselecting all filters', async ( { + page, + defaultBlockPost, + } ) => { + await page.goto( + `${ defaultBlockPost.link }?min_price=20&max_price=67` + ); + + const defaultRange = await page + .locator( '.wp-block-woocommerce-product-filter-price' ) + .getAttribute( 'data-wc-context' ); + const defaultMinRange = JSON.parse( defaultRange ).minRange; + const defaultMaxRange = JSON.parse( defaultRange ).maxRange; + + // Min price input field + const leftInputContainer = page.locator( + '.wp-block-woocommerce-product-filter-price-content-left-input' + ); + const minPriceInput = leftInputContainer.locator( '.min' ); + await minPriceInput.fill( String( defaultMinRange ) ); + await minPriceInput.blur(); + + // Max price input field + const rightInputContainer = page.locator( + '.wp-block-woocommerce-product-filter-price-content-right-input' + ); + const maxPriceInput = rightInputContainer.locator( '.max' ); + await maxPriceInput.fill( String( defaultMaxRange ) ); + await maxPriceInput.blur(); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeHidden(); + } ); + + test( 'filters are cleared after clear button is clicked', async ( { + page, + defaultBlockPost, + } ) => { + await page.goto( + `${ defaultBlockPost.link }?min_price=20&max_price=67` + ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await button.click(); + + await page.waitForURL( defaultBlockPost.link ); + + const defaultRangePrice = await page + .locator( '.wp-block-woocommerce-product-filter-price' ) + .getAttribute( 'data-wc-context' ); + + const defaultMinRange = JSON.parse( defaultRangePrice ).minRange; + const defaultMaxRange = JSON.parse( defaultRangePrice ).maxRange; + const defaultMinPrice = JSON.parse( defaultRangePrice ).minPrice; + const defaultMaxPrice = JSON.parse( defaultRangePrice ).maxPrice; + + expect( defaultMinRange ).toEqual( defaultMinPrice ); + expect( defaultMaxRange ).toEqual( defaultMaxPrice ); + } ); + test( 'Changes in the price input field triggers price slider updates', async ( { page, defaultBlockPost, diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.handlebars b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.handlebars index e6dd22ba303c..2f24a86cfbb5 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.handlebars +++ b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.handlebars @@ -15,6 +15,14 @@

Filter by Price

+ + +
+ +
+ + + {{#> wp-block blockName='woocommerce/product-filter-price' attributes=attributes }} {{/ wp-block }}
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.block_theme.spec.ts index 14e25b38d7c7..2add3e718848 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.block_theme.spec.ts @@ -24,6 +24,63 @@ const test = base.extend< { test.describe( 'Product Filter: Rating Filter Block', () => { test.describe( 'frontend', () => { + test( 'clear button is not shown on initial page load', async ( { + page, + defaultBlockPost, + } ) => { + await page.goto( defaultBlockPost.link ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeHidden(); + } ); + + test( 'clear button appears after a filter is applied', async ( { + page, + defaultBlockPost, + } ) => { + await page.goto( `${ defaultBlockPost.link }?rating_filter=1` ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeVisible(); + } ); + + test( 'clear button hides after deselecting all filters', async ( { + page, + defaultBlockPost, + } ) => { + await page.goto( `${ defaultBlockPost.link }?rating_filter=1` ); + + const ratingCheckboxes = page.getByLabel( + /Checkbox: Rated \d out of 5/ + ); + + await ratingCheckboxes.nth( 0 ).uncheck(); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeHidden(); + } ); + + test( 'filters are cleared after clear button is clicked', async ( { + page, + defaultBlockPost, + } ) => { + await page.goto( `${ defaultBlockPost.link }?rating_filter=1` ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await button.click(); + + const ratingCheckbox = page.getByLabel( + /Checkbox: Rated 1 out of 5/ + ); + + await expect( ratingCheckbox ).toBeVisible(); + await expect( ratingCheckbox ).not.toBeChecked(); + } ); + test( 'Renders a checkbox list with the available ratings', async ( { page, defaultBlockPost, diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.handlebars b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.handlebars index c769d26b9fd1..09a786cb23b8 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.handlebars +++ b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.handlebars @@ -15,6 +15,14 @@

Filter by Rating

+ + +
+ +
+ + + {{#> wp-block blockName='woocommerce/product-filter-rating' attributes=attributes }} {{/ wp-block }}
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.block_theme.spec.ts index f5ab3553abe4..7d4569c080b9 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.block_theme.spec.ts @@ -40,6 +40,17 @@ const test = base.extend< { test.describe( 'Product Filter: Stock Status Block', () => { test.describe( 'With default display style', () => { + test( 'clear button is not shown on initial page load', async ( { + page, + defaultBlockPost, + } ) => { + await page.goto( defaultBlockPost.link ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeHidden(); + } ); + test( 'renders a checkbox list with the available stock statuses', async ( { page, defaultBlockPost, @@ -71,9 +82,72 @@ test.describe( 'Product Filter: Stock Status Block', () => { await expect( products ).toHaveCount( 1 ); } ); + + test( 'clear button appears after a filter is applied', async ( { + page, + defaultBlockPost, + } ) => { + await page.goto( defaultBlockPost.link ); + + const outOfStockCheckbox = page.getByText( 'Out of stock' ); + await outOfStockCheckbox.click(); + + // wait for navigation + await page.waitForURL( /.*filter_stock_status=outofstock.*/ ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeVisible(); + } ); + + test( 'clear button hides after deselecting all filters', async ( { + page, + defaultBlockPost, + } ) => { + await page.goto( + `${ defaultBlockPost.link }?filter_stock_status=outofstock` + ); + + const outOfStockCheckbox = page.getByText( 'Out of stock' ); + await outOfStockCheckbox.click(); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeHidden(); + } ); + + test( 'filters are cleared after clear button is clicked', async ( { + page, + defaultBlockPost, + } ) => { + await page.goto( + `${ defaultBlockPost.link }?filter_stock_status=outofstock` + ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await button.click(); + + const outOfStockCheckbox = page.getByText( 'Out of stock' ); + + await expect( outOfStockCheckbox ).toBeVisible(); + + await expect( outOfStockCheckbox ).not.toBeChecked(); + } ); } ); test.describe( 'With dropdown display style', () => { + test( 'clear button is not shown on initial page load', async ( { + page, + defaultBlockPost, + } ) => { + await page.goto( defaultBlockPost.link ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeHidden(); + } ); + test( 'a dropdown is displayed with the available stock statuses', async ( { page, dropdownBlockPost, @@ -90,5 +164,75 @@ test.describe( 'Product Filter: Stock Status Block', () => { await expect( page.getByText( 'In stock' ) ).toBeVisible(); await expect( page.getByText( 'Out of stock' ) ).toBeVisible(); } ); + + test( 'clear button appears after a filter is applied', async ( { + page, + dropdownBlockPost, + } ) => { + await page.goto( dropdownBlockPost.link ); + + const dropdownLocator = page.locator( + '.wc-interactivity-dropdown' + ); + + await dropdownLocator.click(); + + await page.getByText( 'In stock' ).click(); + + // wait for navigation + await page.waitForURL( /.*filter_stock_status=instock.*/ ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await expect( button ).toBeVisible(); + } ); + + test( 'clear button hides after deselecting all filters', async ( { + page, + dropdownBlockPost, + } ) => { + await page.goto( + `${ dropdownBlockPost.link }?filter_stock_status=instock` + ); + + const dropdownLocator = page.locator( + '.wc-interactivity-dropdown' + ); + + await dropdownLocator.click(); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + const removeFilter = page.locator( + '.wc-interactivity-dropdown__badge-remove' + ); + + await removeFilter.click(); + + await expect( button ).toBeHidden(); + } ); + + test( 'filters are cleared after clear button is clicked', async ( { + page, + dropdownBlockPost, + } ) => { + await page.goto( + `${ dropdownBlockPost.link }?filter_stock_status=instock` + ); + + const button = page.getByRole( 'button', { name: 'Clear' } ); + + await button.click(); + + const placeholder = page.locator( + '.wc-interactivity-dropdown__placeholder' + ); + + await expect( placeholder ).toBeVisible(); + + const placeholderText = await placeholder.textContent(); + + expect( placeholderText ).toEqual( 'Select stock statuses' ); + } ); } ); } ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.handlebars b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.handlebars index 7ac88f53a940..62e2735de376 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.handlebars +++ b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.handlebars @@ -15,8 +15,16 @@

Filter by Stock Status

+ + +
+ +
+ + + {{#> wp-block blockName='woocommerce/product-filter-stock-status' attributes=attributes }} {{/ wp-block }} - +
diff --git a/plugins/woocommerce/changelog/47101-product-filters-clear-button b/plugins/woocommerce/changelog/47101-product-filters-clear-button new file mode 100644 index 000000000000..be2ce4e105e8 --- /dev/null +++ b/plugins/woocommerce/changelog/47101-product-filters-clear-button @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement +Comment: Add clear filter button block + diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php index 7fac543fd02e..0b24b3b07d79 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php @@ -53,6 +53,25 @@ protected function enqueue_data( array $attributes = [] ) { $this->asset_data_registry->add( 'isWidgetEditor', 'widgets.php' === $pagenow || 'customize.php' === $pagenow ); } + /** + * Check array for checked item. + * + * @param array $items Items to check. + */ + private function hasSelectedFilter( $items ) { + foreach ( $items as $key => $value ) { + if ( 'checked' === $key && true === $value ) { + return true; + } + + if ( is_array( $value ) && $this->hasSelectedFilter( $value ) ) { + return true; + } + } + + return false; + } + /** * Render the block. * @@ -66,8 +85,38 @@ protected function render( $attributes, $content, $block ) { return $content; } + $tags = new WP_HTML_Tag_Processor( $content ); + $has_selected_filter = false; + + while ( $tags->next_tag( 'div' ) ) { + $items = $tags->get_attribute( 'data-wc-context' ) ? json_decode( $tags->get_attribute( 'data-wc-context' ), true ) : null; + + // For checked box filters. + if ( $items && array_key_exists( 'items', $items ) ) { + $has_selected_filter = $this->hasSelectedFilter( $items['items'] ); + break; + } + + // For price range filter. + if ( $items && array_key_exists( 'minPrice', $items ) ) { + if ( $items['minPrice'] > $items['minRange'] || $items['maxPrice'] < $items['maxRange'] ) { + $has_selected_filter = true; + break; + } + } + + // For dropdown filters. + if ( $items && array_key_exists( 'selectedItems', $items ) ) { + if ( count( $items['selectedItems'] ) > 0 ) { + $has_selected_filter = true; + break; + } + } + } + $attributes_data = array( 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ), + 'data-wc-context' => wp_json_encode( array( 'hasSelectedFilter' => $has_selected_filter ) ), 'class' => 'wc-block-product-filters', ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterClearButton.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterClearButton.php new file mode 100644 index 000000000000..e1288926e7f2 --- /dev/null +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterClearButton.php @@ -0,0 +1,55 @@ + '!context.hasSelectedFilter', + ) + ); + + $p = new \WP_HTML_Tag_Processor( $content ); + + if ( $p->next_tag( array( 'class_name' => 'wp-block-button__link' ) ) ) { + $p->set_attribute( 'data-wc-on--click', 'actions.clear' ); + + $style = $p->get_attribute( 'style' ); + $p->set_attribute( 'style', 'outline:none;' . $style ); + + $content = $p->get_updated_html(); + } + + $content = str_replace( array( '' ), array( '' ), $content ); + + return sprintf( + '', + $wrapper_attributes, + $content + ); + } +} diff --git a/plugins/woocommerce/src/Blocks/BlockTypesController.php b/plugins/woocommerce/src/Blocks/BlockTypesController.php index 2a07a6b27435..713c0c4a2f4d 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypesController.php +++ b/plugins/woocommerce/src/Blocks/BlockTypesController.php @@ -304,6 +304,7 @@ protected function get_block_types() { $block_types[] = 'ProductFilterAttribute'; $block_types[] = 'ProductFilterRating'; $block_types[] = 'ProductFilterActive'; + $block_types[] = 'ProductFilterClearButton'; } /** From b8aabe9005f0939be39bab27646868eecab8bac0 Mon Sep 17 00:00:00 2001 From: Christopher Allford <6451942+ObliviousHarmony@users.noreply.github.com> Date: Mon, 13 May 2024 16:00:24 -0700 Subject: [PATCH 88/91] Fixed Taxonomy Term Limit On Product Collection Filters (#47155) In the interest of avoiding pagination this refactor swaps the current term loading with one that doesn't require a cache of all terms. We've removed the need for the cache by storing the ID and term name together in the token/suggestion list and using a display transformation to hide any unnecessary information. --- .../taxonomy-controls/taxonomy-item.tsx | 368 ++++++++---------- ...6850-collection-taxonomy-filter-pagination | 4 + 2 files changed, 163 insertions(+), 209 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-46850-collection-taxonomy-filter-pagination diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/taxonomy-controls/taxonomy-item.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/taxonomy-controls/taxonomy-item.tsx index f452a918da80..99504a0d5fe8 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/taxonomy-controls/taxonomy-item.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/taxonomy-controls/taxonomy-item.tsx @@ -1,17 +1,17 @@ /** * External dependencies */ -import { useEntityRecords } from '@wordpress/core-data'; -import { Taxonomy } from '@wordpress/core-data/src/entity-types'; -import { useState, useMemo, useRef } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; import { useDebounce } from '@wordpress/compose'; import { FormTokenField } from '@wordpress/components'; import { decodeEntities } from '@wordpress/html-entities'; +import type { Taxonomy } from '@wordpress/core-data/src/entity-types'; type Term = { id: number; name: string; - slug: string; }; interface TaxonomyItemProps { @@ -20,253 +20,203 @@ interface TaxonomyItemProps { onChange: ( termIds: number[] ) => void; } -// A constant empty array that is reused throughout the component. -const EMPTY_ARRAY: [] = []; - -// Base arguments for querying terms. -const BASE_QUERY_ARGS = { +/** + * The default arguments to use when querying terms. + */ +const DEFAULT_QUERY_ARGS = { + _fields: 'id,name', order: 'asc', - _fields: 'id,name,slug', + orderby: 'name', context: 'view', }; -// Function to get the term id based on user input in the `FormTokenField`. -const getTermIdByTermValue = ( - searchTerm: Term | string, - termNameToIdMap: Map< string, number > -): number | undefined => { - const termId = ( searchTerm as Term )?.id; - if ( termId ) { - return termId; - } - - return ( - termNameToIdMap.get( searchTerm as string ) || - termNameToIdMap.get( ( searchTerm as string ).toLocaleLowerCase() ) - ); -}; - /** - * Creates a map that keeps track of the count of each term name in the provided list of terms. + * Given a term this will return a token to use in the FormTokenField. Since the + * field only allows for string values we need to make sure that the name + * has all of the information needed to identify the term object. We do + * this by encoding the term ID in the name. * - * @param {Term[]} allTerms - Array of all term objects. - * @return {Map} A map with term names as keys and their counts as values. + * @param {Term} term The term to build a token for. + * @return {string} The token for the term. */ -const createNameCountMap = ( allTerms: Term[] ): Map< string, number > => { - return allTerms.reduce( - ( accumulator: Map< string, number >, term: Term ) => { - const termName = term.name; - if ( accumulator.has( termName ) ) { - accumulator.set( - termName, - ( accumulator.get( termName ) as number ) + 1 - ); - } else { - accumulator.set( termName, 1 ); - } - return accumulator; - }, - new Map< string, number >() - ); +const getTokenForTerm = ( term: Term ): string => { + // Make sure that the ID is AFTER the name so that the matching + // in FormTokenField works and the suggestions are rendered. + return `${ term.name } (#${ term.id })`; }; /** - * Generates a unique name for the term. If there are multiple terms with the same name, - * appends the term's slug to the name to distinguish them. + * Parses a token generated by `getTokenForTerm` into a term object. * - * @param {string} termName - Name of the term. - * @param {string} termSlug - Slug of the term. - * @param {Map} nameCountMap - A map storing count of each term name. - * @return {string} A unique name for the term. + * @param {string} token The token to parse. + * @return {Term|false} The term if one could be parsed and false if not. */ -const generateUniqueName = ( - termName: string, - termSlug: string, - nameCountMap: Map< string, number > -): string => { - return nameCountMap.get( termName ) === 1 - ? termName - : `${ termName } - ${ termSlug }`; +const getTermFromToken = ( token: string ): Term | false => { + const matches = token.match( /^(?:(.+) )?\(#(\d+)\)$/ ); + if ( ! matches ) { + return false; + } + + return { + name: matches[ 1 ] ?? '', + id: parseInt( matches[ 2 ], 10 ), + }; }; -/** - * This function generates and returns two mapping structures (Maps) for terms: - * 1. termIdToNameMap: Map with term IDs as keys and their corresponding term names as values. - * 2. termNameToIdMap: Map where the keys are term names and their corresponding values are the term IDs. - * - * The primary purpose of these Maps is to facilitate quick lookups in either direction (ID to name, or name to ID). - * - * In the case of duplicate term names, to ensure uniqueness, the term's slug is appended to the name. - * This ensures that when the terms are displayed in the `FormTokenField`, each term name remains unique. - * - * An illustrative example of how termIdToNameMap might look is as follows: - * { - * "19": "Accessories", - * "37": "category1 - category1", - * "38": "category1 - category1-clothing", - * "39": "category1 - category1-clothing-2", - * "16": "Clothing", - * "21": "Decor" - * } - * In the example above, "category1" is a duplicated term name, so the term's slug is appended for distinction. - * - * termNameToIdMap is the inverse of termIdToNameMap, mapping term names back to their respective IDs. - */ -const useTermMaps = ( - taxonomy: Taxonomy -): { - termIdToNameMap: Map< number, string >; - termNameToIdMap: Map< string, number >; - isResolving: boolean; -} => { - // Fetch all terms for the given taxonomy. - const { records: allTerms, isResolving: isResolvingAllTerms } = - useEntityRecords< Term[] >( 'taxonomy', taxonomy.slug, { - ...BASE_QUERY_ARGS, - } ); +const TaxonomyItem = ( { taxonomy, termIds, onChange }: TaxonomyItemProps ) => { + // We need to get the existing terms so that we can store the term object + // in the map that we used to render the term name in the FormTokenField. + const { existingTerms, isLoadingExistingTerms } = useSelect( + ( select ) => { + // There's no need to load any existing terms when there are no terms set. + if ( ! termIds || ! termIds.length ) { + return { existingTerms: [], isLoadingExistingTerms: false }; + } - // Memoize the result to avoid re-renders. - return useMemo( () => { - const termIdToNameMap = new Map< number, string >(); - const termNameToIdMap = new Map< string, number >(); + // @ts-expect-error hasFinishedResolution is untyped. + const { getEntityRecords, hasFinishedResolution } = + select( 'core' ); + + const selectorArgs: [ string, string, Record< string, unknown > ] = + [ + 'taxonomy', + taxonomy.slug, + { + ...DEFAULT_QUERY_ARGS, + include: termIds, + }, + ]; - if ( ! allTerms ) return { - termIdToNameMap, - termNameToIdMap, - isResolving: isResolvingAllTerms, + existingTerms: getEntityRecords( ...selectorArgs ) as Term[], + isLoadingExistingTerms: ! hasFinishedResolution( + 'getEntityRecords', + selectorArgs + ), }; + }, + [ taxonomy, termIds ] + ); - // Count the number of times a term name appears. - const nameCountMap = createNameCountMap( allTerms ); + // A search query will enable us to populate the FormTokenField's suggestion + // list based on the user supplied search string. + const [ searchQuery, setSearchQuery ] = useState( '' ); + const { searchTerms } = useSelect( + ( select ) => { + // The FormTokenField requires at least two characters to start showing + // the suggestions. Let's not waste a web request since it won't do + // anything useful. + if ( searchQuery.length <= 1 ) { + return { searchTerms: [] }; + } - // Create the map with term ids as keys and term names as values. - for ( const term of allTerms ) { - const termId = term.id; - const termName = term.name; - const name = generateUniqueName( - termName, - term.slug, - nameCountMap - ); - termIdToNameMap.set( termId, name ); - termNameToIdMap.set( name, termId ); - // Add lower case version of the term name to the map as well - // Because the search is case insensitive in FormTokenField. - termNameToIdMap.set( name.toLocaleLowerCase(), termId ); + const { getEntityRecords } = select( 'core' ); + return { + searchTerms: getEntityRecords( 'taxonomy', taxonomy.slug, { + ...DEFAULT_QUERY_ARGS, + exclude: termIds, + search: searchQuery, + } ) as Term[], + }; + }, + [ taxonomy, termIds, searchQuery ] + ); + const handleSearch = useDebounce( setSearchQuery, 250 ); + + // Transform the terms for the FormTokenField control and + // keep track of any duplicate term names for later. + const allTermNames = new Set< string >(); + const duplicateNames = new Set< string >(); + const createTokenForTerm = ( term: Term ) => { + if ( allTermNames.has( term.name ) ) { + duplicateNames.add( term.name ); } - return { - termIdToNameMap, - termNameToIdMap, - isResolving: isResolvingAllTerms, - }; - }, [ allTerms, isResolvingAllTerms ] ); -}; - -const TaxonomyItem = ( { taxonomy, termIds, onChange }: TaxonomyItemProps ) => { - const [ search, setSearch ] = useState< string | undefined >( undefined ); - const suggestionsRef = useRef< string[] >( EMPTY_ARRAY ); - const currentValueRef = useRef< - { - id: number; - value: string; - }[] - >( EMPTY_ARRAY ); - - // Search is debounced to limit the number of API calls as the user types - const debouncedSearch = useDebounce( setSearch, 250 ); + allTermNames.add( term.name ); - const { - termIdToNameMap, - termNameToIdMap, - isResolving: isResolvingTermMaps, - } = useTermMaps( taxonomy ); + return getTokenForTerm( term ); + }; - // Fetch the terms based on the search query. - const { records: searchResults, hasResolved: searchHasResolved } = - useEntityRecords( - 'taxonomy', - taxonomy.slug, - { - ...BASE_QUERY_ARGS, - search, - orderby: 'name', - exclude: termIds, - per_page: 20, - }, - { - enabled: search !== undefined, + const existingTokens = existingTerms + ? existingTerms.map( createTokenForTerm ) + : []; + const suggestionTokens = searchTerms + ? searchTerms.map( createTokenForTerm ) + : []; + + // Since the FormTokenField has the term ID encoded in the token + // we need to pull out the ID in order to update the term IDs. + const handleChangeTermIDs = ( tokens: string[] ) => { + const newTermIds: number[] = []; + tokens.forEach( ( token ) => { + const term = getTermFromToken( token ); + if ( ! term ) { + return; } - ); - suggestionsRef.current = useMemo( () => { - if ( ! searchHasResolved ) return suggestionsRef.current; + newTermIds.push( term.id ); + } ); - const newSuggestions = searchResults.map( - ( searchResult: Term ) => - termIdToNameMap.get( searchResult.id ) || searchResult.name - ); - return newSuggestions; - }, [ searchHasResolved, searchResults, termIdToNameMap ] ); + onChange( newTermIds ); + }; - // Fetch the existing terms & set the current value. - const { records: existingTerms, hasResolved: hasExistingTermsResolved } = - useEntityRecords< Term >( - 'taxonomy', - taxonomy.slug, - { - ...BASE_QUERY_ARGS, - include: termIds, + // It's possible that a term may have been deleted but still + // be present in the termIds array. In that case we will + // display the ID and an indication it was deleted. + if ( existingTerms && termIds.length !== existingTerms.length ) { + // Use a map to make checking for the terms faster. + const termMap = existingTerms.reduce( + ( acc: Record< string, Term >, term: Term ) => { + acc[ term.id ] = term; + return acc; }, - { - enabled: termIds?.length > 0, - } + {} ); - currentValueRef.current = useMemo( () => { - if ( hasExistingTermsResolved === false ) { - return currentValueRef.current; - } - - if ( ! existingTerms || ! termIds.length ) return EMPTY_ARRAY; - - return existingTerms.map( ( { id, name }: Term ) => ( { - id, - value: termIdToNameMap.get( id ) || name, - } ) ); - }, [ existingTerms, hasExistingTermsResolved, termIdToNameMap, termIds ] ); + // Deleted terms will be displayed as just the ID with no name. + termIds.forEach( ( termId ) => { + if ( ! termMap[ termId ] ) { + existingTokens.push( `(#${ termId })` ); + } + } ); + } - // Update the selected terms when the user selects a suggestion. - const onTermsChange = ( newTermValues: FormTokenField.Value[] ) => { - const newTermIds = []; - for ( const termValue of newTermValues ) { - const termId = getTermIdByTermValue( - termValue as string | Term, - termNameToIdMap - ); - if ( termId ) { - newTermIds.push( termId ); + // Since our tokens include some encoding we need to perform some transformations + // before they can be displayed in the input and in the suggestion list. + const displayTermName = ( display: string ) => { + const term = getTermFromToken( display ); + if ( term ) { + // Terms that are missing will be identified as such. + if ( ! term.name ) { + display = `(#${ term.id } ${ __( 'Missing', 'woocommerce' ) })`; + } + // Terms with names that are non-unique will have the ID appended. + else if ( duplicateNames.has( term.name ) ) { + display = `${ term.name } (#${ term.id })`; + } + // Terms that fit neither criteria just display the name. + else { + display = term.name; } } - onChange( newTermIds ); - }; - const decodeHTMLEntities = ( value: string ) => { - return decodeEntities( value ) || ''; + // Both the API and React will encode any HTML entities in the term name. + // We need to decode them before they are rendered to undo the + // API's encoding so that React can display them properly. + return decodeEntities( display ) || ''; }; return (
); diff --git a/plugins/woocommerce/changelog/fix-46850-collection-taxonomy-filter-pagination b/plugins/woocommerce/changelog/fix-46850-collection-taxonomy-filter-pagination new file mode 100644 index 000000000000..0b22d0b706ae --- /dev/null +++ b/plugins/woocommerce/changelog/fix-46850-collection-taxonomy-filter-pagination @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Allow category and tag taxonomy filters on the Product Collection block to see more than the first 10. From d19e90105a632e531d827213b9f65508e9bc8495 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 May 2024 00:24:24 +0100 Subject: [PATCH 89/91] Delete changelog files based on PR 47003 (#47439) Delete changelog files for 47003 Co-authored-by: WooCommerce Bot --- .../woocommerce/changelog/47003-add-tracks-woo-connect-notice | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/47003-add-tracks-woo-connect-notice diff --git a/plugins/woocommerce/changelog/47003-add-tracks-woo-connect-notice b/plugins/woocommerce/changelog/47003-add-tracks-woo-connect-notice deleted file mode 100644 index 70653db7652f..000000000000 --- a/plugins/woocommerce/changelog/47003-add-tracks-woo-connect-notice +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Send tracks event for woocommerce.com connect notices From 208e5633baa14c7881c4792e6f14ff038d15dd09 Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 14 May 2024 11:41:35 +1200 Subject: [PATCH 90/91] [Launch Your Store] Add e2e tests (#47239) * LYS spec * get homescreen check working * move over to shopper * basic test * get first test working * Store only coming soon mode - logged out * add logged in ttests * linter * Add changefile(s) from automation for the following project(s): woocommerce * Remove skip from tests --------- Co-authored-by: github-actions --- .../changelog/47239-add-LYS-e2e-tests | 4 + .../tests/e2e-pw/bin/test-helper-apis.php | 10 ++- .../tests/merchant/launch-your-store.spec.js | 75 ++++++++++++++++++ .../tests/shopper/launch-your-store.spec.js | 77 +++++++++++++++++++ .../woocommerce/tests/e2e-pw/utils/options.js | 15 +++- 5 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 plugins/woocommerce/changelog/47239-add-LYS-e2e-tests create mode 100644 plugins/woocommerce/tests/e2e-pw/tests/merchant/launch-your-store.spec.js create mode 100644 plugins/woocommerce/tests/e2e-pw/tests/shopper/launch-your-store.spec.js diff --git a/plugins/woocommerce/changelog/47239-add-LYS-e2e-tests b/plugins/woocommerce/changelog/47239-add-LYS-e2e-tests new file mode 100644 index 000000000000..a1e731ecaaf7 --- /dev/null +++ b/plugins/woocommerce/changelog/47239-add-LYS-e2e-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: add +Comment: Add e2e test to unreleased feature + diff --git a/plugins/woocommerce/tests/e2e-pw/bin/test-helper-apis.php b/plugins/woocommerce/tests/e2e-pw/bin/test-helper-apis.php index d3e3c582df7a..edbc0aba17e7 100644 --- a/plugins/woocommerce/tests/e2e-pw/bin/test-helper-apis.php +++ b/plugins/woocommerce/tests/e2e-pw/bin/test-helper-apis.php @@ -87,11 +87,17 @@ function api_update_option( WP_REST_Request $request ) { $option_name = sanitize_text_field( $request['option_name'] ); $option_value = sanitize_text_field( $request['option_value'] ); + $existing_value = get_option( $option_name ); + + if ( $existing_value === $option_value ) { + return new WP_REST_Response( 'Option ' . $option_name . ' already set to: ' . $option_value, 200 ); + } + if ( update_option( $option_name, $option_value ) ) { - return new WP_REST_Response( 'Option updated', 200 ); + return new WP_REST_Response( 'Update option SUCCESS: ' . $option_name . ' => ' . $option_value, 200 ); } - return new WP_REST_Response( 'Invalid request body', 400 ); + return new WP_REST_Response( 'Update option FAILED: ' . $option_name . ' => ' . $option_value, 400 ); } /** diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/launch-your-store.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/launch-your-store.spec.js new file mode 100644 index 000000000000..06a6f9d4a683 --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/launch-your-store.spec.js @@ -0,0 +1,75 @@ +const { test, expect, request } = require( '@playwright/test' ); +const { setOption } = require( '../../utils/options' ); + +test.describe( 'Launch Your Store front end - logged in', () => { + test.use( { storageState: process.env.ADMINSTATE } ); + + test.afterAll( async ( { baseURL } ) => { + try { + await setOption( + request, + baseURL, + 'woocommerce_coming_soon', + 'no' + ); + } catch ( error ) { + console.log( error ); + } + } ); + + test( 'Entire site coming soon mode', async ( { page, baseURL } ) => { + try { + await setOption( + request, + baseURL, + 'woocommerce_coming_soon', + 'yes' + ); + + await setOption( + request, + baseURL, + 'woocommerce_store_pages_only', + 'no' + ); + } catch ( error ) { + console.log( error ); + } + + await page.goto( baseURL ); + + await expect( + page.getByText( + 'This page is in "Coming soon" mode and is only visible to you and those who have permission. To make it public to everyone, change visibility settings' + ) + ).toBeVisible(); + } ); + + test( 'Store only coming soon mode', async ( { page, baseURL } ) => { + try { + await setOption( + request, + baseURL, + 'woocommerce_coming_soon', + 'yes' + ); + + await setOption( + request, + baseURL, + 'woocommerce_store_pages_only', + 'yes' + ); + } catch ( error ) { + console.log( error ); + } + + await page.goto( baseURL + '/shop/' ); + + await expect( + page.getByText( + 'This page is in "Coming soon" mode and is only visible to you and those who have permission. To make it public to everyone, change visibility settings' + ) + ).toBeVisible(); + } ); +} ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/launch-your-store.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/launch-your-store.spec.js new file mode 100644 index 000000000000..c86c23744f24 --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/launch-your-store.spec.js @@ -0,0 +1,77 @@ +const { test, expect, request } = require( '@playwright/test' ); +const { setOption } = require( '../../utils/options' ); + +test.describe( 'Launch Your Store front end - logged out', () => { + test.afterAll( async ( { baseURL } ) => { + try { + await setOption( + request, + baseURL, + 'woocommerce_coming_soon', + 'no' + ); + } catch ( error ) { + console.log( error ); + } + } ); + + test( 'Entire site coming soon mode', async ( { page, baseURL } ) => { + try { + await setOption( + request, + baseURL, + 'woocommerce_coming_soon', + 'yes' + ); + + await setOption( + request, + baseURL, + 'woocommerce_store_pages_only', + 'no' + ); + } catch ( error ) { + console.log( error ); + } + + await page.goto( baseURL ); + + await expect( + page.getByText( + 'Pardon our dust! We’re working on something amazing — check back soon!' + ) + ).toBeVisible(); + } ); + + test( 'Store only coming soon mode', async ( { page, baseURL } ) => { + try { + await setOption( + request, + baseURL, + 'woocommerce_coming_soon', + 'yes' + ); + + await setOption( + request, + baseURL, + 'woocommerce_store_pages_only', + 'yes' + ); + } catch ( error ) { + console.log( error ); + } + + await page.goto( baseURL + '/shop/' ); + + await expect( + page.getByText( 'Great things are on the horizon' ) + ).toBeVisible(); + + await expect( + page.getByText( + 'Something big is brewing! Our store is in the works and will be launching soon!' + ) + ).toBeVisible(); + } ); +} ); diff --git a/plugins/woocommerce/tests/e2e-pw/utils/options.js b/plugins/woocommerce/tests/e2e-pw/utils/options.js index 1254c94ec476..d5acf1ab1607 100644 --- a/plugins/woocommerce/tests/e2e-pw/utils/options.js +++ b/plugins/woocommerce/tests/e2e-pw/utils/options.js @@ -1,3 +1,6 @@ +/** + * Internal dependencies + */ import { encodeCredentials } from './plugin-utils'; export const setOption = async ( @@ -17,8 +20,12 @@ export const setOption = async ( }, } ); - await apiContext.post( '/wp-json/e2e-options/update', { - failOnStatusCode: true, - data: { option_name: optionName, option_value: optionValue }, - } ); + return await apiContext + .post( '/wp-json/e2e-options/update', { + failOnStatusCode: true, + data: { option_name: optionName, option_value: optionValue }, + } ) + .then( ( response ) => { + return response.json(); + } ); }; From d66c5b8efec6a4f53246c2d91344454513e93b4c Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Tue, 14 May 2024 10:32:51 +0800 Subject: [PATCH 91/91] Add UTM tags to all product links in core profiler Free features step (#47397) * Add UTM tags to all product links for core profiler default free extensions * Add changelog * Update mailpoet link --- .../update-obw-free-extensions-links | 4 +++ .../DefaultFreeExtensions.php | 26 +++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-obw-free-extensions-links diff --git a/plugins/woocommerce/changelog/update-obw-free-extensions-links b/plugins/woocommerce/changelog/update-obw-free-extensions-links new file mode 100644 index 000000000000..7de872d61b26 --- /dev/null +++ b/plugins/woocommerce/changelog/update-obw-free-extensions-links @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Add UTM tags to all product links in core profiler Free features step diff --git a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php index ef4d2bcfc428..35c109c8d850 100644 --- a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php +++ b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php @@ -846,80 +846,80 @@ public static function with_core_profiler_fields( array $plugins ) { 'label' => __( 'Get paid with WooPayments', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ), 'description' => __( "Securely accept payments and manage payment activity straight from your store's dashboard", 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/products/woocommerce-payments', + 'learn_more_link' => 'https://woocommerce.com/products/woocommerce-payments?utm_source=storeprofiler&utm_medium=product&utm_campaign=freefeatures', 'install_priority' => 5, ), 'woocommerce-services:shipping' => array( 'label' => __( 'Print shipping labels with WooCommerce Shipping', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ), 'description' => __( 'Print USPS and DHL labels directly from your dashboard and save on shipping.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/woocommerce-shipping', + 'learn_more_link' => 'https://woocommerce.com/woocommerce-shipping?utm_source=storeprofiler&utm_medium=product&utm_campaign=freefeatures', 'install_priority' => 3, ), 'jetpack' => array( 'label' => __( 'Boost content creation with Jetpack AI Assistant', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-jetpack.svg', WC_PLUGIN_FILE ), 'description' => __( 'Save time on content creation — unlock high-quality blog posts and pages using AI.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/products/jetpack', + 'learn_more_link' => 'https://woocommerce.com/products/jetpack?utm_source=storeprofiler&utm_medium=product&utm_campaign=freefeatures', 'install_priority' => 8, ), 'pinterest-for-woocommerce' => array( 'label' => __( 'Showcase your products with Pinterest', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-pinterest.svg', WC_PLUGIN_FILE ), 'description' => __( 'Get your products in front of a highly engaged audience.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/products/pinterest-for-woocommerce', + 'learn_more_link' => 'https://woocommerce.com/products/pinterest-for-woocommerce?utm_source=storeprofiler&utm_medium=product&utm_campaign=freefeatures', 'install_priority' => 2, ), 'mailpoet' => array( 'label' => __( 'Reach your customers with MailPoet', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-mailpoet.svg', WC_PLUGIN_FILE ), 'description' => __( 'Send purchase follow-up emails, newsletters, and promotional campaigns.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/products/mailpoet', + 'learn_more_link' => 'https://woocommerce.com/products/mailpoet?utm_source=storeprofiler&utm_medium=product&utm_campaign=freefeatures', 'install_priority' => 7, ), 'tiktok-for-business' => array( 'label' => __( 'Create ad campaigns with TikTok', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-tiktok.svg', WC_PLUGIN_FILE ), 'description' => __( 'Create advertising campaigns and reach one billion global users.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/products/tiktok-for-woocommerce', + 'learn_more_link' => 'https://woocommerce.com/products/tiktok-for-woocommerce?utm_source=storeprofiler&utm_medium=product&utm_campaign=freefeatures', 'install_priority' => 1, ), 'google-listings-and-ads' => array( 'label' => __( 'Drive sales with Google Listings & Ads', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-google.svg', WC_PLUGIN_FILE ), 'description' => __( 'Reach millions of active shoppers across Google with free product listings and ads.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/products/google-listings-and-ads', + 'learn_more_link' => 'https://woocommerce.com/products/google-listings-and-ads?utm_source=storeprofiler&utm_medium=product&utm_campaign=freefeatures', 'install_priority' => 6, ), 'woocommerce-services:tax' => array( 'label' => __( 'Get automated tax rates with WooCommerce Tax', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ), 'description' => __( 'Automatically calculate how much sales tax should be collected – by city, country, or state.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/products/tax', + 'learn_more_link' => 'https://woocommerce.com/products/tax?utm_source=storeprofiler&utm_medium=product&utm_campaign=freefeatures', 'install_priority' => 4, ), ); // Copy shipping for the core-profiler and remove is_visible conditions, except for the country restriction. - $_plugins['woocommerce-services:shipping']['is_visible'] = [ + $_plugins['woocommerce-services:shipping']['is_visible'] = array( array( 'type' => 'base_location_country', 'value' => 'US', 'operation' => '=', ), - ]; + ); - $remove_plugins_activated_rule = function( $is_visible ) { + $remove_plugins_activated_rule = function ( $is_visible ) { $is_visible = array_filter( array_map( - function( $rule ) { + function ( $rule ) { if ( is_object( $rule ) || ! isset( $rule['operand'] ) ) { return $rule; } return array_filter( $rule['operand'], - function( $operand ) { + function ( $operand ) { return 'plugins_activated' !== $operand['type']; } );