From 4b7643cd6f53d0ce36e6d87bd8523fa14c2b0dc9 Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Fri, 9 Jun 2023 13:35:24 -0700 Subject: [PATCH 01/28] Describe how untrashing should work in relation to orders. --- .../class-wc-order-data-store-cpt-test.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php index 4fb6bd7132c..1db49993c71 100644 --- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php +++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php @@ -275,4 +275,22 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case { } } + /** + * Test the untrashing an order works as expected when done in an agnostic way (ie, not depending directly on + * functions such as `wp_untrash_post()`. + * + * @return void + */ + public function test_untrash(): void { + $order = WC_Helper_Order::create_order(); + $order_id = $order->get_id(); + $original_status = $order->get_status(); + + $order->delete(); + $this->assertEquals( 'trash', $order->get_status(), 'The order was successfully trashed.' ); + + $order = wc_get_order( $order_id ); + $this->assertTrue( $order->untrash(), 'The order was restored from the trash.' ); + $this->assertEquals( $original_status, $order->get_status(), 'The original order status is restored following untrash.' ); + } } From d30754b1cfae9b120145dc566c1100ad909a27ab Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Fri, 9 Jun 2023 13:36:19 -0700 Subject: [PATCH 02/28] Changelog entry. --- plugins/woocommerce/changelog/add-order-model-trash-untrash | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-order-model-trash-untrash diff --git a/plugins/woocommerce/changelog/add-order-model-trash-untrash b/plugins/woocommerce/changelog/add-order-model-trash-untrash new file mode 100644 index 00000000000..0955b34092f --- /dev/null +++ b/plugins/woocommerce/changelog/add-order-model-trash-untrash @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Provide a data-store agnostic way of untrashing orders. From 7f8747f478c8e305d1c6e211b9066cb9f8282ece Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Fri, 9 Jun 2023 13:36:31 -0700 Subject: [PATCH 03/28] Add methods to allow x-datastore untrashing. --- plugins/woocommerce/includes/class-wc-order.php | 11 +++++++++++ .../class-wc-order-data-store-cpt.php | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/plugins/woocommerce/includes/class-wc-order.php b/plugins/woocommerce/includes/class-wc-order.php index f29711ae372..127c9c98d50 100644 --- a/plugins/woocommerce/includes/class-wc-order.php +++ b/plugins/woocommerce/includes/class-wc-order.php @@ -2286,4 +2286,15 @@ class WC_Order extends WC_Abstract_Order { public function is_created_via( $modus ) { return apply_filters( 'woocommerce_order_is_created_via', $modus === $this->get_created_via(), $this, $modus ); } + + /** + * Attempts to restore the specified order back to its original status (after having been trashed). + * + * @param WC_Order $order The order to be untrashed. + * + * @return bool If the operation was successful. + */ + public function untrash(): bool { + return $this->data_store->untrash_order( $this ); + } } diff --git a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php index ab18e688128..17dc35b2df2 100644 --- a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php +++ b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php @@ -1181,4 +1181,20 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement ); WC_Order::prime_raw_meta_data_cache( $raw_meta_data_collection, 'orders' ); } + + /** + * Attempts to restore the specified order back to its original status (after having been trashed). + * + * @param WC_Order $order The order to be untrashed. + * + * @return bool If the operation was successful. + */ + public function untrash_order( WC_Order $order ): bool { + if ( ! wp_untrash_post( $order->get_id() ) ) { + return false; + } + + $order->set_status( get_post_field( 'post_status', $order->get_id() ) ); + return (bool) $order->save(); + } } From 530a7f08d0bdae07a039d803a8130cfd1bdd1346 Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Fri, 9 Jun 2023 13:52:21 -0700 Subject: [PATCH 04/28] Always return a bool, in case the data store lacks the `untrash_order` method. --- plugins/woocommerce/includes/class-wc-order.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/class-wc-order.php b/plugins/woocommerce/includes/class-wc-order.php index 127c9c98d50..2f8261bed02 100644 --- a/plugins/woocommerce/includes/class-wc-order.php +++ b/plugins/woocommerce/includes/class-wc-order.php @@ -2295,6 +2295,6 @@ class WC_Order extends WC_Abstract_Order { * @return bool If the operation was successful. */ public function untrash(): bool { - return $this->data_store->untrash_order( $this ); + return (bool) $this->data_store->untrash_order( $this ); } } From 252d3bc32e223d8ce6f9a5bd815c2e665e898209 Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Fri, 9 Jun 2023 14:07:41 -0700 Subject: [PATCH 05/28] Remove extraneous `@param` tag. --- plugins/woocommerce/includes/class-wc-order.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/woocommerce/includes/class-wc-order.php b/plugins/woocommerce/includes/class-wc-order.php index 2f8261bed02..6a1874bcee9 100644 --- a/plugins/woocommerce/includes/class-wc-order.php +++ b/plugins/woocommerce/includes/class-wc-order.php @@ -2290,8 +2290,6 @@ class WC_Order extends WC_Abstract_Order { /** * Attempts to restore the specified order back to its original status (after having been trashed). * - * @param WC_Order $order The order to be untrashed. - * * @return bool If the operation was successful. */ public function untrash(): bool { From f807d5d65e3ba3b647222f1a057fb6cbccf0a097 Mon Sep 17 00:00:00 2001 From: Rodel Calasagsag Date: Sat, 10 Jun 2023 06:16:12 +0800 Subject: [PATCH 06/28] Add step to wait for overlay to disappear. --- .../products/add-variable-product/update-variations.spec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/add-variable-product/update-variations.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/add-variable-product/update-variations.spec.js index 528796ce9b9..5e8af119e81 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/add-variable-product/update-variations.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/add-variable-product/update-variations.spec.js @@ -541,6 +541,10 @@ test.describe( 'Update variations', () => { await page.locator( 'a[href="#variable_product_options"]' ).click(); } ); + await test.step( 'Wait for block overlay to disappear.', async () => { + await expect( page.locator( '.blockOverlay' ) ).not.toBeVisible(); + } ); + await test.step( 'Select variation defaults', async () => { for ( const attribute of defaultVariation ) { const defaultAttributeMenu = page.locator( 'select', { From 77024a8da7b2923105297fadbcf7aee2bac7cb6d Mon Sep 17 00:00:00 2001 From: Rodel Calasagsag Date: Sat, 10 Jun 2023 06:17:07 +0800 Subject: [PATCH 07/28] Add changelog --- plugins/woocommerce/changelog/e2e-fix-variation-defaults | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/e2e-fix-variation-defaults diff --git a/plugins/woocommerce/changelog/e2e-fix-variation-defaults b/plugins/woocommerce/changelog/e2e-fix-variation-defaults new file mode 100644 index 00000000000..17ae579f110 --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-fix-variation-defaults @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix flakiness in `can set variation defaults` test. \ No newline at end of file From 18f2de2bc41fd0e6a8dc9779300213fdd377f40e Mon Sep 17 00:00:00 2001 From: Nathan Silveira Date: Mon, 12 Jun 2023 12:58:26 -0300 Subject: [PATCH 08/28] Always show pricing group fields, disable if not available for a product type (#38531) * Always show General product tab * Add info messages for variable and grouped products * Add logic to disable/enable fields and labels based on product type * Tweak CSS and fix links * Add tracks events for links * Add filters to pricing disabled message * Tweak logic to only enable fields on certain product types instead of disabling in the opposite ones Add fallback message when pricing is disabled but it's not Variable or Grouped product * Add docblocks * Fix more lint issues * Fix last lint issues * Update selector in e2e test * Refactor PHP echo * Attach to #woocommerce-product-data instead of attaching to body --- .../product-tracking/shared.ts | 22 ++++++ .../changelog/add-always-show-pricinggroup | 4 ++ .../woocommerce/client/legacy/css/admin.scss | 10 +++ .../legacy/js/admin/meta-boxes-product.js | 64 +++++++++++++++++- .../class-wc-meta-box-product-data.php | 2 +- .../views/html-product-data-general.php | 67 ++++++++++++++++--- .../includes/admin/wc-meta-box-functions.php | 5 +- .../update-variations.spec.js | 14 ++-- 8 files changed, 169 insertions(+), 19 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-always-show-pricinggroup diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts index eaab075e38f..69ebe63acf1 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts @@ -541,6 +541,27 @@ const attachProductAttributesTracks = () => { } ); }; +const attachGeneralTabTracks = () => { + document + .querySelector( + '#general_product_data .woocommerce-message .variations-tab-navigation-link' + ) + ?.addEventListener( 'click', () => { + recordEvent( 'disabled_general_tab', { + action: 'go_to_variations', + } ); + } ); + document + .querySelector( + '#general_product_data .woocommerce-message .linked-products-navigation-link' + ) + ?.addEventListener( 'click', () => { + recordEvent( 'disabled_general_tab', { + action: 'go_to_linked_products', + } ); + } ); +}; + /** * Attaches product variations tracks. */ @@ -731,6 +752,7 @@ export const initProductScreenTracks = () => { attachProductVariationsTracks(); attachProductTabsTracks(); attachProductInventoryTabTracks(); + attachGeneralTabTracks(); }; export function addExitPageListener( pageId: string ) { diff --git a/plugins/woocommerce/changelog/add-always-show-pricinggroup b/plugins/woocommerce/changelog/add-always-show-pricinggroup new file mode 100644 index 00000000000..2461dbe0979 --- /dev/null +++ b/plugins/woocommerce/changelog/add-always-show-pricinggroup @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Always show pricing group fields, disable if not available for a product type diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss index 6d5a19cc9b1..69134f55784 100644 --- a/plugins/woocommerce/client/legacy/css/admin.scss +++ b/plugins/woocommerce/client/legacy/css/admin.scss @@ -959,6 +959,7 @@ #variable_product_options #message, #inventory_product_data .notice, +#general_product_data .notice, #variable_product_options .notice { display: flex; margin: 10px; @@ -5363,6 +5364,7 @@ img.help_tip { padding: 0; margin: 0 0 0 -150px; + .req { font-weight: 700; font-style: normal; @@ -5379,6 +5381,14 @@ img.help_tip { display: inline; } + label, + input { + &.disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + input:not([type="checkbox"]):not([type="radio"]) + .description { display: block; clear: both; diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js index 10026b521e0..55846dff0f7 100644 --- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js +++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js @@ -139,6 +139,7 @@ jQuery( function ( $ ) { } show_and_hide_panels(); + disable_or_enable_fields(); change_product_type_tip( get_product_tip_content( select_val ) ); $( 'ul.wc-tabs li:visible' ).eq( 0 ).find( 'a' ).trigger( 'click' ); @@ -261,6 +262,39 @@ jQuery( function ( $ ) { } ); } + function disable_or_enable_fields() { + var product_type = $( 'select#product-type' ).val(); + var hasDisabledFields = true; + $( `.enable_if_simple` ).each( function () { + $( this ).addClass( 'disabled' ); + if ( $( this ).is( 'input' ) ) { + $( this ).prop( 'disabled', true ); + } + } ); + $( `.enable_if_external` ).each( function () { + $( this ).addClass( 'disabled' ); + if ( $( this ).is( 'input' ) ) { + $( this ).prop( 'disabled', true ); + } + } ); + $( `.enable_if_${ product_type }` ).each( function () { + hasDisabledFields = false; + $( this ).removeClass( 'disabled' ); + if ( $( this ).is( 'input' ) ) { + $( this ).prop( 'disabled', false ); + } + } ); + + if ( + hasDisabledFields && + ! $( '#general_product_data .woocommerce-message' ).is( ':visible' ) + ) { + $( `.pricing_disabled_fallback_message` ).show(); + } else { + $( `.pricing_disabled_fallback_message` ).hide(); + } + } + // Sale price schedule. $( '.sale_price_dates_fields' ).each( function () { var $these_sale_dates = $( this ); @@ -891,7 +925,7 @@ jQuery( function ( $ ) { } ); // Go to attributes tab when clicking on link in variations message - $( document.body ).on( + $( '#woocommerce-product-data' ).on( 'click', '#variable_product_options .add-attributes-message a[href="#product_attributes"]', function () { @@ -902,6 +936,34 @@ jQuery( function ( $ ) { } ); + // Go to variations tab when clicking on link in the general tab message + $( '#woocommerce-product-data' ).on( + 'click', + '#general_product_data .woocommerce-message a[href="#variable_product_options"]', + function () { + $( + '#woocommerce-product-data .variations_tab a[href="#variable_product_options"]' + ).trigger( 'click' ); + return false; + } + ); + + // Go to linked products tab when clicking on link in the general tab message + $( '#woocommerce-product-data' ).on( + 'click', + '#general_product_data .woocommerce-message a[href="#linked_product_data"]', + function () { + $( + '#woocommerce-product-data .linked_product_tab a[href="#linked_product_data"]' + ).trigger( 'click' ); + return false; + } + ); + + + + + // Uploading files. var downloadable_file_frame; var file_path_field; diff --git a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php index 67312222375..3a7e4d7a282 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php @@ -89,7 +89,7 @@ class WC_Meta_Box_Product_Data { 'general' => array( 'label' => __( 'General', 'woocommerce' ), 'target' => 'general_product_data', - 'class' => array( 'hide_if_grouped' ), + 'class' => array(), 'priority' => 10, ), 'inventory' => array( diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-general.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-general.php index cff71f755d8..7b1741e9c96 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-general.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-general.php @@ -36,14 +36,61 @@ defined( 'ABSPATH' ) || exit; ?> - ); diff --git a/plugins/woocommerce-admin/client/core-profiler/index.tsx b/plugins/woocommerce-admin/client/core-profiler/index.tsx index 0caca218d6b..9e047cbecd7 100644 --- a/plugins/woocommerce-admin/client/core-profiler/index.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/index.tsx @@ -106,6 +106,14 @@ export type PluginsInstallationRequestedEvent = { }; }; +export type PluginsLearnMoreLinkClicked = { + type: 'PLUGINS_LEARN_MORE_LINK_CLICKED'; + payload: { + plugin: string; + learnMoreLink: string; + }; +}; + // TODO: add types as we develop the pages export type OnboardingProfile = { business_choice: BusinessChoice; @@ -1107,6 +1115,14 @@ export const coreProfilerStateMachineDefinition = createMachine( { ], target: 'pluginsSkipped', }, + PLUGINS_LEARN_MORE_LINK_CLICKED: { + actions: [ + { + type: 'recordTracksPluginsLearnMoreLinkClicked', + step: 'plugins', + }, + ], + }, PLUGINS_INSTALLATION_REQUESTED: { target: 'installPlugins', actions: [ 'assignPluginsSelected' ], @@ -1266,6 +1282,7 @@ export const coreProfilerStateMachineDefinition = createMachine( { data: ( context ) => { return { selectedPlugins: context.pluginsSelected, + pluginsAvailable: context.pluginsAvailable, }; }, }, diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/Plugins.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/Plugins.tsx index 5c8770b25ad..20ebf5f273f 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/Plugins.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/Plugins.tsx @@ -11,7 +11,10 @@ import { useState } from 'react'; /** * Internal dependencies */ -import { CoreProfilerStateMachineContext } from '../index'; +import { + CoreProfilerStateMachineContext, + PluginsLearnMoreLinkClicked, +} from '../index'; import { PluginsInstallationRequestedEvent, PluginsPageSkippedEvent } from '..'; import { Heading } from '../components/heading/heading'; import { Navigation } from '../components/navigation/navigation'; @@ -36,13 +39,16 @@ export const Plugins = ( { }: { context: CoreProfilerStateMachineContext; sendEvent: ( - payload: PluginsInstallationRequestedEvent | PluginsPageSkippedEvent + payload: + | PluginsInstallationRequestedEvent + | PluginsPageSkippedEvent + | PluginsLearnMoreLinkClicked ) => void; navigationProgress: number; } ) => { const [ selectedPlugins, setSelectedPlugins ] = useState< ExtensionList[ 'plugins' ] - >( context.pluginsAvailable.filter( ( plugin ) => ! plugin.is_installed ) ); + >( context.pluginsAvailable.filter( ( plugin ) => ! plugin.is_activated ) ); const setSelectedPlugin = ( plugin: Extension ) => { setSelectedPlugins( @@ -136,10 +142,29 @@ export const Plugins = ( { ) }
{ context.pluginsAvailable.map( ( plugin ) => { + const learnMoreLink = plugin.learn_more_link ? ( + { + sendEvent( { + type: 'PLUGINS_LEARN_MORE_LINK_CLICKED', + payload: { + plugin: plugin.key, + learnMoreLink: + plugin.learn_more_link ?? '', + }, + } ); + } } + href={ plugin.learn_more_link } + target="_blank" + type="external" + > + { __( 'Learn More', 'woocommerce' ) } + + ) : null; return ( { setSelectedPlugin( plugin ); } } @@ -156,8 +181,9 @@ export const Plugins = ( { /> ) : null } - title={ plugin.name } + title={ plugin.label } description={ plugin.description } + learnMoreLink={ learnMoreLink } /> ); } ) } diff --git a/plugins/woocommerce-admin/client/core-profiler/services/installAndActivatePlugins.ts b/plugins/woocommerce-admin/client/core-profiler/services/installAndActivatePlugins.ts index 1415b78e32b..1739ecd0c94 100644 --- a/plugins/woocommerce-admin/client/core-profiler/services/installAndActivatePlugins.ts +++ b/plugins/woocommerce-admin/client/core-profiler/services/installAndActivatePlugins.ts @@ -1,7 +1,12 @@ /** * External dependencies */ -import { PLUGINS_STORE_NAME, PluginNames } from '@woocommerce/data'; +import { + ExtensionList, + ONBOARDING_STORE_NAME, + PLUGINS_STORE_NAME, + PluginNames, +} from '@woocommerce/data'; import { dispatch } from '@wordpress/data'; import { assign, @@ -68,6 +73,7 @@ const createPluginInstalledAndActivatedEvent = ( export type PluginInstallerMachineContext = { selectedPlugins: PluginNames[]; + pluginsAvailable: ExtensionList[ 'plugins' ] | []; pluginsInstallationQueue: PluginNames[]; installedPlugins: InstalledPlugin[]; startTime: number; @@ -93,6 +99,7 @@ export const pluginInstallerMachine = createMachine( initial: 'installing', context: { selectedPlugins: [] as PluginNames[], + pluginsAvailable: [] as ExtensionList[ 'plugins' ] | [], pluginsInstallationQueue: [] as PluginNames[], installedPlugins: [] as InstalledPlugin[], startTime: 0, @@ -160,7 +167,7 @@ export const pluginInstallerMachine = createMachine( invoke: { src: 'queueRemainingPluginsAsync', onDone: { - target: 'finished', + target: 'reportSuccess', }, }, }, @@ -188,7 +195,21 @@ export const pluginInstallerMachine = createMachine( } ), assignPluginsInstallationQueue: assign( { pluginsInstallationQueue: ( ctx ) => { - return ctx.selectedPlugins; + // Sort the plugins by install_priority so that the smaller plugins are installed first + // install_priority is set by plugin's size + // Lower install_prioirty means the plugin is smaller + return ctx.selectedPlugins.slice().sort( ( a, b ) => { + const aIndex = ctx.pluginsAvailable.find( + ( plugin ) => plugin.key === a + ); + const bIndex = ctx.pluginsAvailable.find( + ( plugin ) => plugin.key === b + ); + return ( + ( aIndex?.install_priority ?? 99 ) - + ( bIndex?.install_priority ?? 99 ) + ); + } ); }, } ), assignStartTime: assign( { @@ -262,9 +283,10 @@ export const pluginInstallerMachine = createMachine( ); }, queueRemainingPluginsAsync: ( ctx ) => { - return dispatch( PLUGINS_STORE_NAME ).installPlugins( - ctx.pluginsInstallationQueue, - true + return dispatch( + ONBOARDING_STORE_NAME + ).installAndActivatePluginsAsync( + ctx.pluginsInstallationQueue ); }, }, diff --git a/plugins/woocommerce-admin/client/core-profiler/services/test/installAndActivatePlugins.test.ts b/plugins/woocommerce-admin/client/core-profiler/services/test/installAndActivatePlugins.test.ts index b1e7e733502..031129984da 100644 --- a/plugins/woocommerce-admin/client/core-profiler/services/test/installAndActivatePlugins.test.ts +++ b/plugins/woocommerce-admin/client/core-profiler/services/test/installAndActivatePlugins.test.ts @@ -48,6 +48,7 @@ describe( 'pluginInstallerMachine', () => { .withContext( { ...defaultContext, selectedPlugins: [ 'woocommerce-payments' ], + pluginsAvailable: [], } ); dispatchInstallPluginMock.mockImplementationOnce( ( context ) => { @@ -88,3 +89,10 @@ describe( 'pluginInstallerMachine', () => { } ); } ); } ); + +// TODO: write more tests, I ran out of time and it's friday night +// we need tests for: +// 1. when given multiple plugins it should call the installPlugin service multiple times with the right plugins +// 2. when given multiple plugins and a mocked delay using the config, we can mock the installs to take longer than the timeout and then some plugins should not finish installing, then it should add the remaining to async queue +// 3. when a plugin gives an error it should report the error to the parents. we can check this by mocking 'updateParentWithInstallationErrors' +// 4. it should update parent with the plugin installation progress, we can check this by mocking the action 'updateParentWithPluginProgress' diff --git a/plugins/woocommerce-admin/client/core-profiler/style.scss b/plugins/woocommerce-admin/client/core-profiler/style.scss index f3de578b0d8..aa1ebaa7b0b 100644 --- a/plugins/woocommerce-admin/client/core-profiler/style.scss +++ b/plugins/woocommerce-admin/client/core-profiler/style.scss @@ -398,8 +398,12 @@ max-width: 615px; margin: 58px 0 0 0; } + .woocommerce-profiler-heading__title { + color: $gray-900; + padding: 0; + } .woocommerce-profiler-heading__subtitle { - margin: 0 0 48px 0 !important; + margin: 12px 0 48px 0 !important; @include breakpoint( '<782px' ) { margin-top: 12px !important; } @@ -418,6 +422,7 @@ margin-top: 28px; text-align: center; display: block; + font-size: 14px; } .plugin-error { diff --git a/plugins/woocommerce/assets/images/core-profiler/logo-google.svg b/plugins/woocommerce/assets/images/core-profiler/logo-google.svg new file mode 100644 index 00000000000..f93422532c8 --- /dev/null +++ b/plugins/woocommerce/assets/images/core-profiler/logo-google.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/core-profiler/logo-jetpack.svg b/plugins/woocommerce/assets/images/core-profiler/logo-jetpack.svg new file mode 100644 index 00000000000..0c0a10a368d --- /dev/null +++ b/plugins/woocommerce/assets/images/core-profiler/logo-jetpack.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/core-profiler/logo-mailpoet.svg b/plugins/woocommerce/assets/images/core-profiler/logo-mailpoet.svg new file mode 100644 index 00000000000..e4d303a3a3d --- /dev/null +++ b/plugins/woocommerce/assets/images/core-profiler/logo-mailpoet.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/core-profiler/logo-pinterest.svg b/plugins/woocommerce/assets/images/core-profiler/logo-pinterest.svg new file mode 100644 index 00000000000..392dd3a6b55 --- /dev/null +++ b/plugins/woocommerce/assets/images/core-profiler/logo-pinterest.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/core-profiler/logo-tiktok.svg b/plugins/woocommerce/assets/images/core-profiler/logo-tiktok.svg new file mode 100644 index 00000000000..16840d49ab2 --- /dev/null +++ b/plugins/woocommerce/assets/images/core-profiler/logo-tiktok.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/assets/images/core-profiler/logo-woo.svg b/plugins/woocommerce/assets/images/core-profiler/logo-woo.svg new file mode 100644 index 00000000000..b7bc7d14aa7 --- /dev/null +++ b/plugins/woocommerce/assets/images/core-profiler/logo-woo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/plugins/woocommerce/changelog/update-38436-additional-changes-for-plugins-page b/plugins/woocommerce/changelog/update-38436-additional-changes-for-plugins-page new file mode 100644 index 00000000000..ab669e0b465 --- /dev/null +++ b/plugins/woocommerce/changelog/update-38436-additional-changes-for-plugins-page @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Additional changes for the core profiler plugins page diff --git a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php index d49c3547731..1977ada79d1 100644 --- a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php +++ b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php @@ -816,6 +816,7 @@ class DefaultFreeExtensions { * * - Updated description for the core-profiler. * - Adds learn_more_link and label. + * - Adds install_priority, which is used to sort the plugins. The value is determined by the plugin size. Lower = smaller. * * @param array $plugins Array of plugins. * @@ -824,47 +825,72 @@ class DefaultFreeExtensions { public static function with_core_profiler_fields( array $plugins ) { $_plugins = array( 'woocommerce-payments' => array( - 'label' => __( 'Get paid with WooCommerce Payments', 'woocommerce' ), - 'description' => __( 'Accept credit cards and other popular payment methods smoothly.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/products/woocommerce-payments', + 'label' => __( 'Get paid with WooCommerce Payments', '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', + 'install_priority' => 5, ), 'woocommerce-services:shipping' => array( - 'label' => __( 'Print shipping labels with WooCommerce Shipping', 'woocommerce' ), - 'description' => __( 'Print USPS and DHL labels directly from your dashboard and save on shipping.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/woocommerce-shipping', + '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', + 'install_priority' => 3, ), 'jetpack' => array( - 'label' => __( 'Enhance security with Jetpack', 'woocommerce' ), - 'description' => __( 'Get auto real-time backups, malware scans, and spam protection.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/products/jetpack', + 'label' => __( 'Enhance security with Jetpack', 'woocommerce' ), + 'image_url' => plugins_url( '/assets/images/core-profiler/logo-jetpack.svg', WC_PLUGIN_FILE ), + 'description' => __( 'Get auto real-time backups, malware scans, and spam protection.', 'woocommerce' ), + 'learn_more_link' => 'https://woocommerce.com/products/jetpack', + 'install_priority' => 8, ), 'pinterest-for-woocommerce' => array( - 'label' => __( 'Showcase your products with Pinterest', 'woocommerce' ), - 'description' => __( 'Get your products in front of a highly engaged audience.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/products/pinterest-for-woocommerce', + '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', + 'install_priority' => 2, ), 'mailpoet' => array( - 'label' => __( 'Reach your customers with MailPoet', 'woocommerce' ), - 'description' => __( 'Send purchase follow-up emails, newsletters, and promotional campaigns.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/products/mailpoet', + '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', + 'install_priority' => 7, ), 'tiktok-for-business' => array( - 'label' => __( 'Create ad campaigns with TikTok', 'woocommerce' ), - 'description' => __( 'Create advertising campaigns and reach one billion global users.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/products/tiktok-for-woocommerce', + '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', + 'install_priority' => 1, ), 'google-listings-and-ads' => array( - 'label' => __( 'Drive sales with Google Listings & Ads', 'woocommerce' ), - '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', + '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', + 'install_priority' => 6, ), 'woocommerce-services:tax' => array( - 'label' => __( 'Get automated tax rates with WooCommerce Tax', 'woocommerce' ), - 'description' => __( 'Automatically calculate how much sales tax should be collected – by city, country, or state.', 'woocommerce' ), - 'learn_more_link' => 'https://woocommerce.com/products/tax', + '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', + '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'] = [ + array( + 'type' => 'base_location_country', + 'value' => 'US', + 'operation' => '=', + ), + ]; + $remove_plugins_activated_rule = function( $is_visible ) { $is_visible = array_filter( array_map( From 604132391ab0ba71b64ee404cff4023398a7829a Mon Sep 17 00:00:00 2001 From: Moon Date: Tue, 13 Jun 2023 22:10:43 -0700 Subject: [PATCH 23/28] Visual changes for Intro, guided setup, and skipped guided setup pages (#38709) * Change Skip this setup to Skip this step * Visual changes on intro, user profile, and skip guided page * Always check tracking agreement * Change border to 2px * Change Give yoru store a name font-size to 13 * Add Changelog * Update test to reflect the text change * Remove !important * Update test snapshot * Update test snapshot * Remove test that is no longer necessary --- .../components/heading/style.scss | 8 +- .../client/core-profiler/pages/IntroOptIn.tsx | 6 +- .../core-profiler/pages/UserProfile.tsx | 2 +- .../pages/tests/intro-opt-in.test.tsx | 14 ---- .../pages/tests/user-profile.test.tsx | 2 +- .../client/core-profiler/style.scss | 73 +++++++++++-------- .../core-profiler-machine.test.tsx.snap | 4 +- ...te-38626-visual-changes-for-profiler-pages | 4 + 8 files changed, 54 insertions(+), 59 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-38626-visual-changes-for-profiler-pages diff --git a/plugins/woocommerce-admin/client/core-profiler/components/heading/style.scss b/plugins/woocommerce-admin/client/core-profiler/components/heading/style.scss index 2a06768136c..1a41c6a5544 100644 --- a/plugins/woocommerce-admin/client/core-profiler/components/heading/style.scss +++ b/plugins/woocommerce-admin/client/core-profiler/components/heading/style.scss @@ -10,10 +10,10 @@ .woocommerce-profiler-heading__title { font-style: normal; font-weight: 500; - font-size: 32px; + font-size: 40px; line-height: 40px; text-align: center; - color: #000; + color: $gray-900; margin-bottom: 12px; padding-top: 0; @@ -29,7 +29,7 @@ font-size: 16px; line-height: 24px; text-align: center; - color: $gray-700; + color: $gray-800; @include breakpoint( '<782px' ) { color: $gray-800; @@ -50,8 +50,6 @@ margin: 52px 0 40px; .woocommerce-profiler-heading__title { - font-size: 32px; - line-height: 40px; text-align: left; } .woocommerce-profiler-heading__subtitle { diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/IntroOptIn.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/IntroOptIn.tsx index 0a0db6834d7..5ba817ad513 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/IntroOptIn.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/IntroOptIn.tsx @@ -17,15 +17,13 @@ import { Navigation } from '../components/navigation/navigation'; export const IntroOptIn = ( { sendEvent, navigationProgress, - context, }: { sendEvent: ( event: IntroOptInEvent ) => void; navigationProgress: number; context: CoreProfilerStateMachineContext; } ) => { - const [ iOptInDataSharing, setIsOptInDataSharing ] = useState< boolean >( - context.optInDataSharing - ); + const [ iOptInDataSharing, setIsOptInDataSharing ] = + useState< boolean >( true ); return (
sendEvent( { type: 'USER_PROFILE_SKIPPED', diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/tests/intro-opt-in.test.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/tests/intro-opt-in.test.tsx index f0aa2d00eb2..fa469916c7c 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/tests/intro-opt-in.test.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/tests/intro-opt-in.test.tsx @@ -48,20 +48,6 @@ describe( 'IntroOptIn', () => { expect( screen.getByRole( 'checkbox' ) ).toBeChecked(); } ); - it( 'should checkbox be unchecked when optInDataSharing is false', () => { - const newProps = { - ...props, - context: { - optInDataSharing: false, - }, - }; - render( - // @ts-ignore - - ); - expect( screen.getByRole( 'checkbox' ) ).not.toBeChecked(); - } ); - it( 'should toggle checkbox when checkbox is clicked', () => { render( // @ts-ignore diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/tests/user-profile.test.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/tests/user-profile.test.tsx index 6c60b07a270..b7382b672aa 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/tests/user-profile.test.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/tests/user-profile.test.tsx @@ -114,7 +114,7 @@ describe( 'UserProfile', () => { ); screen .getByRole( 'button', { - name: /Skip this setup/i, + name: /Skip this step/i, } ) .click(); expect( props.sendEvent ).toHaveBeenCalledWith( { diff --git a/plugins/woocommerce-admin/client/core-profiler/style.scss b/plugins/woocommerce-admin/client/core-profiler/style.scss index aa1ebaa7b0b..26d051e5ca4 100644 --- a/plugins/woocommerce-admin/client/core-profiler/style.scss +++ b/plugins/woocommerce-admin/client/core-profiler/style.scss @@ -44,7 +44,7 @@ padding: 10px 16px; height: 48px; font-size: 14px; - font-weight: 500; + font-weight: normal; } .woocommerce-select-control__option { @@ -115,7 +115,7 @@ } .woocommerce-profiler-intro-opt-in__content { - padding-top: 81px; + padding-top: 110px; flex: 1; @include breakpoint( '<782px' ) { @@ -127,7 +127,7 @@ } .woocommerce-profiler-welcome-image { - margin-bottom: 48px; + margin-bottom: 64px; width: 266px; height: 172px; background: url(./assets/images/welcome-desktop.svg) no-repeat center @@ -145,10 +145,12 @@ .woocommerce-profiler-setup-store__button { padding: 10px 16px; width: 200px; - height: 54px; + height: 48px; display: flex; justify-content: center; align-items: center; + font-size: 14px; + font-weight: normal; @include breakpoint( '<782px' ) { width: 100%; @@ -171,6 +173,7 @@ outline: 2px solid transparent; width: 16px; height: 16px; + border-radius: 2px; } .components-checkbox-control__input-container { @@ -248,43 +251,47 @@ top: 40px !important; } - .woocommerce-profiler-business-location { +} + + +.woocommerce-profiler-business-location { + display: flex; + flex-direction: column; + .woocommerce-profiler-business-location__content { + width: 100%; display: flex; - flex-direction: column; - .woocommerce-profiler-business-location__content { - max-width: 550px; + align-self: center; + & > div { width: 100%; - display: flex; - align-self: center; + } - & > div { - width: 100%; - } + .woocommerce-profiler-heading { + max-width: 570px; + } - .components-base-control__field { - height: 40px; - } + .components-base-control__field { + height: 40px; + } - .woocommerce-select-control__control-icon { + .woocommerce-select-control__control-icon { + display: none; + } + + .woocommerce-select-control__control.is-active { + .components-base-control__label { display: none; } + } - .woocommerce-select-control__control.is-active { - .components-base-control__label { - display: none; - } - } + .woocommerce-select-control.is-searchable + .woocommerce-select-control__control-input { + margin: 0; + padding: 0; + } - .woocommerce-select-control.is-searchable - .woocommerce-select-control__control-input { - margin: 0; - padding: 0; - } - - .woocommerce-select-control.is-searchable - .components-base-control__label { - left: 13px; - } + .woocommerce-select-control.is-searchable + .components-base-control__label { + left: 13px; } } } @@ -423,6 +430,7 @@ text-align: center; display: block; font-size: 14px; + font-weight: normal; } .plugin-error { @@ -511,6 +519,7 @@ border-color: #bbb; border-radius: 2px; border-width: 1px; + font-size: 13px; } .woocommerce-profiler-select-control__industry { diff --git a/plugins/woocommerce-admin/client/core-profiler/test/__snapshots__/core-profiler-machine.test.tsx.snap b/plugins/woocommerce-admin/client/core-profiler/test/__snapshots__/core-profiler-machine.test.tsx.snap index e4157b5ea68..aa186640398 100644 --- a/plugins/woocommerce-admin/client/core-profiler/test/__snapshots__/core-profiler-machine.test.tsx.snap +++ b/plugins/woocommerce-admin/client/core-profiler/test/__snapshots__/core-profiler-machine.test.tsx.snap @@ -3086,7 +3086,7 @@ Object { class="components-button woocommerce-profiler-navigation-skip-link is-link" type="button" > - Skip this setup + Skip this step
@@ -3267,7 +3267,7 @@ Object { class="components-button woocommerce-profiler-navigation-skip-link is-link" type="button" > - Skip this setup + Skip this step diff --git a/plugins/woocommerce/changelog/update-38626-visual-changes-for-profiler-pages b/plugins/woocommerce/changelog/update-38626-visual-changes-for-profiler-pages new file mode 100644 index 00000000000..0ab6663da67 --- /dev/null +++ b/plugins/woocommerce/changelog/update-38626-visual-changes-for-profiler-pages @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Visual changes for the core profiler pages -- intro, guided setup, and skipped guided setup pages From 0c2508a1f1b356e34c109b01a704f2ec279ec998 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Tue, 13 Jun 2023 16:18:10 +0530 Subject: [PATCH 24/28] Add re-migrate support to HPOS CLI. --- plugins/woocommerce/changelog/fix-38660 | 4 ++ .../Migrations/CustomOrderTable/CLIRunner.php | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-38660 diff --git a/plugins/woocommerce/changelog/fix-38660 b/plugins/woocommerce/changelog/fix-38660 new file mode 100644 index 00000000000..aa634dc5c0c --- /dev/null +++ b/plugins/woocommerce/changelog/fix-38660 @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add re-migrate support to HPOS CLI. diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php index 87bc72b7ef7..d0f2843f6f1 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php @@ -304,6 +304,11 @@ class CLIRunner { * --- * default: Output of function `wc_get_order_types( 'cot-migration' )` * + * [--re-migrate] + * : Attempt to re-migrate orders that failed verification. You should only use this option when you have never run the site with HPOS as authoritative source of order data yet, otherwise, you risk stale data overwriting the more recent data. + * This option can only be enabled when --verbose flag is also set. + * default: false + * * ## EXAMPLES * * # Verify migrated order data, 500 orders at a time. @@ -327,6 +332,7 @@ class CLIRunner { 'end-at' => - 1, 'verbose' => false, 'order-types' => '', + 're-migrate' => false, ) ); @@ -340,6 +346,7 @@ class CLIRunner { $batch_size = ( (int) $assoc_args['batch-size'] ) === 0 ? 500 : (int) $assoc_args['batch-size']; $verbose = (bool) $assoc_args['verbose']; $order_types = wc_get_order_types( 'cot-migration' ); + $remigrate = (bool) $assoc_args['re-migrate']; if ( ! empty( $assoc_args['order-types'] ) ) { $passed_order_types = array_map( 'trim', explode( ',', $assoc_args['order-types'] ) ); $order_types = array_intersect( $order_types, $passed_order_types ); @@ -415,6 +422,36 @@ class CLIRunner { $errors ) ); + if ( $remigrate ) { + WP_CLI::warning( + sprintf( + __( 'Attempting to remigrate...', 'woocommerce') + ) + ); + $failed_ids = array_keys( $failed_ids_in_current_batch ); + $this->synchronizer->process_batch( $failed_ids ); + $errors_in_remigrate_batch = $this->post_to_cot_migrator->verify_migrated_orders( $failed_ids ); + $errors_in_remigrate_batch = $this->verify_meta_data( $failed_ids, $errors_in_remigrate_batch ); + if ( count( $errors_in_remigrate_batch ) > 0 ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- This is a CLI command and debugging code is intended. + $formatted_errors = print_r( $errors_in_remigrate_batch, true ); + WP_CLI::warning( + sprintf( + /* Translators: %1$d is number of errors and %2$s is the formatted array of order IDs. */ + _n( + '%1$d error found: %2$s when re-migrating order. Please review the error above.', + '%1$d errors found: %2$s when re-migrating orders. Please review the errors above.', + count( $errors_in_remigrate_batch ), + 'woocommerce' + ), + count( $errors_in_remigrate_batch ), + $formatted_errors + ) + ); + } else { + WP_CLI::warning( 'Re-migration successful.', 'woocommerce' ); + } + } } $progress->tick(); From 60e168c6caede103036b222f5987fff885f1800b Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Tue, 13 Jun 2023 16:30:30 +0530 Subject: [PATCH 25/28] Doc fixes. --- .../src/Database/Migrations/CustomOrderTable/CLIRunner.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php index d0f2843f6f1..f2d30be63b0 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php @@ -305,7 +305,7 @@ class CLIRunner { * default: Output of function `wc_get_order_types( 'cot-migration' )` * * [--re-migrate] - * : Attempt to re-migrate orders that failed verification. You should only use this option when you have never run the site with HPOS as authoritative source of order data yet, otherwise, you risk stale data overwriting the more recent data. + * : Attempt to re-migrate orders that failed verification. You should only use this option when you have never run the site with HPOS as authoritative source of order data yet, or you have manually checked the reported errors, otherwise, you risk stale data overwriting the more recent data. * This option can only be enabled when --verbose flag is also set. * default: false * @@ -425,7 +425,7 @@ class CLIRunner { if ( $remigrate ) { WP_CLI::warning( sprintf( - __( 'Attempting to remigrate...', 'woocommerce') + __( 'Attempting to remigrate...', 'woocommerce' ) ) ); $failed_ids = array_keys( $failed_ids_in_current_batch ); From 5f4d7db463e82687d6d85c44f0f1ea89a64c4df6 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Wed, 14 Jun 2023 15:03:27 +0530 Subject: [PATCH 26/28] Remove `modified` check when migrating existing orders for consistency. We were checking whether an order is modified before migrating them. While on surface this looks good, the logic itself has following flaws: 1. We were only checking the columns in the core posts table, but none of the meta fields. 2. Our comparison logic was written in SQL, but given that we have a much better comparison logic already written in PHP, it does not makes sense to maintain two different versions. 3. We are alrady checking for modified records in the batch processor itself, so another check this close to migration logic is not really needed. --- .../Migrations/MetaToCustomTableMigrator.php | 35 ++----------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php index 6f932e842df..ae6c67bdaf3 100644 --- a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php @@ -243,13 +243,7 @@ abstract class MetaToCustomTableMigrator extends TableMigrator { $to_insert = array_diff_key( $data['data'], $existing_records ); $this->process_insert_batch( $to_insert ); - $existing_records = array_filter( - $existing_records, - function( $record_data ) { - return '1' === $record_data->modified; - } - ); - $to_update = array_intersect_key( $data['data'], $existing_records ); + $to_update = array_intersect_key( $data['data'], $existing_records ); $this->process_update_batch( $to_update, $existing_records ); } @@ -357,38 +351,13 @@ abstract class MetaToCustomTableMigrator extends TableMigrator { $entity_id_placeholder = implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) ); - // Additional SQL to check if the row needs update according to the column mapping. - // The IFNULL and CHAR(0) "hack" is needed because NULLs can't be directly compared in SQL. - $modified_selector = array(); - $core_column_mapping = array_filter( - $this->core_column_mapping, - function( $mapping ) { - return ! isset( $mapping['select_clause'] ); - } - ); - foreach ( $core_column_mapping as $column_name => $mapping ) { - if ( $column_name === $source_primary_key_column ) { - continue; - } - $modified_selector[] = - "IFNULL(source.$column_name,CHAR(0)) != IFNULL(destination.{$mapping['destination']},CHAR(0))" - . ( 'string' === $mapping['type'] ? ' COLLATE ' . $wpdb->collate : '' ); - } - - if ( empty( $modified_selector ) ) { - $modified_selector = ', 1 AS modified'; - } else { - $modified_selector = trim( implode( ' OR ', $modified_selector ) ); - $modified_selector = ", if( $modified_selector, 1, 0 ) AS modified"; - } - $additional_where = $this->get_additional_where_clause_for_get_data_to_insert_or_update( $entity_ids ); $already_migrated_entity_ids = $this->db_get_results( $wpdb->prepare( // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- All columns and table names are hardcoded. " -SELECT source.`$source_primary_key_column` as source_id, destination.`$destination_primary_key_column` as destination_id $modified_selector +SELECT source.`$source_primary_key_column` as source_id, destination.`$destination_primary_key_column` as destination_id FROM `$destination_table` destination JOIN `$source_table` source ON source.`$source_destination_join_column` = destination.`$destination_source_join_column` WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) $additional_where From b006eee3ae57ca5f7958d1a5daab3eac59e5b949 Mon Sep 17 00:00:00 2001 From: Nathan Silveira Date: Wed, 14 Jun 2023 09:25:37 -0300 Subject: [PATCH 27/28] Add `wcadmin_settings_change` tracks event when adding/removing entries in shipping (#38465) * Add support for new properties "added" and "deleted" in settings tracking * Add calls to actions when adding and deleting shipping zones and classes * Add changelog * Cover an edge case where the zone is created through adding a method and add more comments * Fix lint issues * Add inline comment explaining if statement Refactor WC_Settings_Tracking to work with a single action with an additional parameter --- .../changelog/update-add-delete-shipping | 5 + .../woocommerce/includes/class-wc-ajax.php | 122 ++++++++++++++---- .../events/class-wc-settings-tracking.php | 39 +++++- 3 files changed, 139 insertions(+), 27 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-add-delete-shipping diff --git a/plugins/woocommerce/changelog/update-add-delete-shipping b/plugins/woocommerce/changelog/update-add-delete-shipping new file mode 100644 index 00000000000..af3f692ea97 --- /dev/null +++ b/plugins/woocommerce/changelog/update-add-delete-shipping @@ -0,0 +1,5 @@ +Significance: patch +Type: enhancement +Comment: Add wcadmin_settings_change tracks event when adding/removing entries in shipping + + diff --git a/plugins/woocommerce/includes/class-wc-ajax.php b/plugins/woocommerce/includes/class-wc-ajax.php index 7df471ec108..6d66ab1224f 100644 --- a/plugins/woocommerce/includes/class-wc-ajax.php +++ b/plugins/woocommerce/includes/class-wc-ajax.php @@ -2995,6 +2995,18 @@ class WC_AJAX { // That's fine, it's not in the database anyways. NEXT! continue; } + /** + * Notify that a non-option setting has been deleted. + * + * @since 7.8.0 + */ + do_action( + 'woocommerce_update_non_option_setting', + array( + 'id' => 'shipping_zone', + 'action' => 'delete', + ) + ); WC_Shipping_Zones::delete_zone( $zone_id ); continue; } @@ -3024,19 +3036,18 @@ class WC_AJAX { ); $zone->set_zone_order( $zone_data['zone_order'] ); } - - global $current_tab; - $current_tab = 'shipping'; - /** - * Completes the saving process for options. - * - * @since 7.8.0 - */ - do_action( 'woocommerce_update_options' ); $zone->save(); } } + global $current_tab; + $current_tab = 'shipping'; + /** + * Completes the saving process for options. + * + * @since 7.8.0 + */ + do_action( 'woocommerce_update_options' ); wp_send_json_success( array( 'zones' => WC_Shipping_Zones::get_zones( 'json' ), @@ -3066,15 +3077,31 @@ class WC_AJAX { $zone_id = wc_clean( wp_unslash( $_POST['zone_id'] ) ); $zone = new WC_Shipping_Zone( $zone_id ); + // A shipping zone can be created here if the user is adding a method without first saving the shipping zone. + if ( '' === $zone_id ) { + /** + * Notified that a non-option setting has been added. + * + * @since 7.8.0 + */ + do_action( + 'woocommerce_update_non_option_setting', + array( + 'id' => 'shipping_zone', + 'action' => 'add', + ) + ); + } /** - * Notify that a non-option setting has been updated. + * Notify that a non-option setting has been added. * * @since 7.8.0 */ do_action( 'woocommerce_update_non_option_setting', array( - 'id' => 'zone_method', + 'id' => 'zone_method', + 'action' => 'add', ) ); $instance_id = $zone->add_shipping_method( wc_clean( wp_unslash( $_POST['method_id'] ) ) ); @@ -3178,11 +3205,26 @@ class WC_AJAX { $zone_id = wc_clean( wp_unslash( $_POST['zone_id'] ) ); $zone = new WC_Shipping_Zone( $zone_id ); + // A shipping zone can be created here if the user is adding a method without first saving the shipping zone. + if ( '' === $zone_id ) { + /** + * Notifies that a non-option setting has been added. + * + * @since 7.8.0 + */ + do_action( + 'woocommerce_update_non_option_setting', + array( + 'id' => 'shipping_zone', + 'action' => 'add', + ) + ); + } $changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( isset( $changes['zone_name'] ) ) { /** - * Completes the saving process for options. + * Notifies that a non-option setting has been updated. * * @since 7.8.0 */ @@ -3192,7 +3234,7 @@ class WC_AJAX { if ( isset( $changes['zone_locations'] ) ) { /** - * Completes the saving process for options. + * Notifies that a non-option setting has been updated. * * @since 7.8.0 */ @@ -3218,7 +3260,7 @@ class WC_AJAX { if ( isset( $changes['zone_postcodes'] ) ) { /** - * Completes the saving process for options. + * Notifies that a non-option setting has been updated. * * @since 7.8.0 */ @@ -3231,12 +3273,6 @@ class WC_AJAX { } if ( isset( $changes['methods'] ) ) { - /** - * Completes the saving process for options. - * - * @since 7.8.0 - */ - do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'zone_methods' ) ); foreach ( $changes['methods'] as $instance_id => $data ) { $method_id = $wpdb->get_var( $wpdb->prepare( "SELECT method_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d", $instance_id ) ); @@ -3245,6 +3281,18 @@ class WC_AJAX { $option_key = $shipping_method->get_instance_option_key(); if ( $wpdb->delete( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'instance_id' => $instance_id ) ) ) { delete_option( $option_key ); + /** + * Notifies that a non-option setting has been deleted. + * + * @since 7.8.0 + */ + do_action( + 'woocommerce_update_non_option_setting', + array( + 'id' => 'zone_method', + 'action' => 'delete', + ) + ); do_action( 'woocommerce_shipping_zone_method_deleted', $instance_id, $method_id, $zone_id ); } continue; @@ -3260,7 +3308,7 @@ class WC_AJAX { if ( isset( $method_data['method_order'] ) ) { /** - * Completes the saving process for options. + * Notifies that a non-option setting has been updated. * * @since 7.8.0 */ @@ -3270,7 +3318,7 @@ class WC_AJAX { if ( isset( $method_data['enabled'] ) ) { /** - * Completes the saving process for options. + * Notifies that a non-option setting has been updated. * * @since 7.8.0 */ @@ -3385,6 +3433,18 @@ class WC_AJAX { // That's fine, it's not in the database anyways. NEXT! continue; } + /** + * Notifies that a non-option setting has been deleted. + * + * @since 7.8.0 + */ + do_action( + 'woocommerce_update_non_option_setting', + array( + 'id' => 'shipping_class', + 'action' => 'delete', + ) + ); wp_delete_term( $term_id, 'product_shipping_class' ); continue; } @@ -3426,9 +3486,27 @@ class WC_AJAX { if ( empty( $update_args['name'] ) ) { continue; } + /** + * Notifies that a non-option setting has been added. + * + * @since 7.8.0 + */ + do_action( + 'woocommerce_update_non_option_setting', + array( + 'id' => 'shipping_class', + 'action' => 'add', + ) + ); $inserted_term = wp_insert_term( $update_args['name'], 'product_shipping_class', $update_args ); $term_id = is_wp_error( $inserted_term ) ? 0 : $inserted_term['term_id']; } else { + /** + * Notifies that a non-option setting has been updated. + * + * @since 7.8.0 + */ + do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'shipping_class' ) ); wp_update_term( $term_id, 'product_shipping_class', $update_args ); } diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-settings-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-settings-tracking.php index d916ad0488e..7f1dde896cc 100644 --- a/plugins/woocommerce/includes/tracks/events/class-wc-settings-tracking.php +++ b/plugins/woocommerce/includes/tracks/events/class-wc-settings-tracking.php @@ -43,6 +43,21 @@ class WC_Settings_Tracking { */ protected $modified_options = array(); + /** + * List of options that have been deleted. + * + * @var array + */ + protected $deleted_options = array(); + + /** + * List of options that have been added. + * + * @var array + */ + protected $added_options = array(); + + /** * Toggled options. * @@ -74,7 +89,11 @@ class WC_Settings_Tracking { if ( ! in_array( $option['id'], $this->allowed_options, true ) ) { $this->allowed_options[] = $option['id']; } - if ( ! in_array( $option['id'], $this->updated_options, true ) ) { + if ( 'add' === $option['action'] ) { + $this->added_options[] = $option['id']; + } elseif ( 'delete' === $option['action'] ) { + $this->deleted_options[] = $option['id']; + } elseif ( ! in_array( $option['id'], $this->updated_options, true ) ) { $this->updated_options[] = $option['id']; } } @@ -143,13 +162,23 @@ class WC_Settings_Tracking { public function send_settings_change_event() { global $current_tab, $current_section; - if ( empty( $this->updated_options ) ) { + if ( empty( $this->updated_options ) && empty( $this->deleted_options ) && empty( $this->added_options ) ) { return; } - $properties = array( - 'settings' => implode( ',', $this->updated_options ), - ); + $properties = array(); + + if ( ! empty( $this->updated_options ) ) { + $properties['settings'] = implode( ',', $this->updated_options ); + } + + if ( ! empty( $this->deleted_options ) ) { + $properties['deleted'] = implode( ',', $this->deleted_options ); + } + + if ( ! empty( $this->added_options ) ) { + $properties['added'] = implode( ',', $this->added_options ); + } foreach ( $this->toggled_options as $state => $options ) { if ( ! empty( $options ) ) { From e4f3273fb54b8580af08b3d88711b5dbd1f55b88 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Wed, 14 Jun 2023 19:17:22 +0530 Subject: [PATCH 28/28] Add meta boxes for custom taxonomies in order edit screens (#38676) --- plugins/woocommerce/changelog/fix-38560 | 4 + .../src/Internal/Admin/Orders/Edit.php | 27 ++++ .../Orders/MetaBoxes/TaxonomiesMetaBox.php | 147 ++++++++++++++++++ .../Orders/OrdersTableDataStore.php | 78 ++++++++++ .../OrderAdminServiceProvider.php | 4 + .../class-wc-abstract-order-test.php | 31 +++- 6 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-38560 create mode 100644 plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/TaxonomiesMetaBox.php diff --git a/plugins/woocommerce/changelog/fix-38560 b/plugins/woocommerce/changelog/fix-38560 new file mode 100644 index 00000000000..57c57921ada --- /dev/null +++ b/plugins/woocommerce/changelog/fix-38560 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add support for taxonomy meta boxes in HPOS order edit screen. diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php b/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php index 9ae7e049508..f08f8acec36 100644 --- a/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php +++ b/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php @@ -6,6 +6,7 @@ namespace Automattic\WooCommerce\Internal\Admin\Orders; use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox; +use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox; /** * Class Edit. @@ -26,6 +27,13 @@ class Edit { */ private $custom_meta_box; + /** + * Instance of the TaxonomiesMetaBox class. Used to render meta box for taxonomies. + * + * @var TaxonomiesMetaBox + */ + private $taxonomies_meta_box; + /** * Instance of WC_Order to be used in metaboxes. * @@ -110,10 +118,16 @@ class Edit { if ( ! isset( $this->custom_meta_box ) ) { $this->custom_meta_box = wc_get_container()->get( CustomMetaBox::class ); } + + if ( ! isset( $this->taxonomies_meta_box ) ) { + $this->taxonomies_meta_box = wc_get_container()->get( TaxonomiesMetaBox::class ); + } + $this->add_save_meta_boxes(); $this->handle_order_update(); $this->add_order_meta_boxes( $this->screen_id, __( 'Order', 'woocommerce' ) ); $this->add_order_specific_meta_box(); + $this->add_order_taxonomies_meta_box(); /** * From wp-admin/includes/meta-boxes.php. @@ -159,6 +173,15 @@ class Edit { ); } + /** + * Render custom meta box. + * + * @return void + */ + private function add_order_taxonomies_meta_box() { + $this->taxonomies_meta_box->add_taxonomies_meta_boxes( $this->screen_id, $this->order->get_type() ); + } + /** * Takes care of updating order data. Fires action that metaboxes can hook to for order data updating. * @@ -176,6 +199,10 @@ class Edit { check_admin_referer( $this->get_order_edit_nonce_action() ); + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized later on by taxonomies_meta_box object. + $taxonomy_input = isset( $_POST['tax_input'] ) ? wp_unslash( $_POST['tax_input'] ) : null; + $this->taxonomies_meta_box->save_taxonomies( $this->order, $taxonomy_input ); + /** * Save meta for shop order. * diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/TaxonomiesMetaBox.php b/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/TaxonomiesMetaBox.php new file mode 100644 index 00000000000..f932371aad2 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/TaxonomiesMetaBox.php @@ -0,0 +1,147 @@ +orders_table_data_store = $orders_table_data_store; + } + + /** + * Registers meta boxes to be rendered in order edit screen for taxonomies. + * + * Note: This is re-implementation of part of WP core's `register_and_do_post_meta_boxes` function. Since the code block that add meta box for taxonomies is not filterable, we have to re-implement it. + * + * @param string $screen_id Screen ID. + * @param string $order_type Order type to register meta boxes for. + * + * @return void + */ + public function add_taxonomies_meta_boxes( string $screen_id, string $order_type ) { + include_once ABSPATH . 'wp-admin/includes/meta-boxes.php'; + $taxonomies = get_object_taxonomies( $order_type ); + // All taxonomies. + foreach ( $taxonomies as $tax_name ) { + $taxonomy = get_taxonomy( $tax_name ); + if ( ! $taxonomy->show_ui || false === $taxonomy->meta_box_cb ) { + continue; + } + + if ( 'post_categories_meta_box' === $taxonomy->meta_box_cb ) { + $taxonomy->meta_box_cb = array( $this, 'order_categories_meta_box' ); + } + + if ( 'post_tags_meta_box' === $taxonomy->meta_box_cb ) { + $taxonomy->meta_box_cb = array( $this, 'order_tags_meta_box' ); + } + + $label = $taxonomy->labels->name; + + if ( ! is_taxonomy_hierarchical( $tax_name ) ) { + $tax_meta_box_id = 'tagsdiv-' . $tax_name; + } else { + $tax_meta_box_id = $tax_name . 'div'; + } + + add_meta_box( + $tax_meta_box_id, + $label, + $taxonomy->meta_box_cb, + $screen_id, + 'side', + 'core', + array( + 'taxonomy' => $tax_name, + '__back_compat_meta_box' => true, + ) + ); + } + } + + /** + * Save handler for taxonomy data. + * + * @param \WC_Abstract_Order $order Order object. + * @param array|null $taxonomy_input Taxonomy input passed from input. + */ + public function save_taxonomies( \WC_Abstract_Order $order, $taxonomy_input ) { + if ( ! isset( $taxonomy_input ) ) { + return; + } + + $sanitized_tax_input = $this->sanitize_tax_input( $taxonomy_input ); + + $sanitized_tax_input = $this->orders_table_data_store->init_default_taxonomies( $order, $sanitized_tax_input ); + $this->orders_table_data_store->set_custom_taxonomies( $order, $sanitized_tax_input ); + } + + /** + * Sanitize taxonomy input by calling sanitize callbacks for each registered taxonomy. + * + * @param array|null $taxonomy_data Nonce verified taxonomy input. + * + * @return array Sanitized taxonomy input. + */ + private function sanitize_tax_input( $taxonomy_data ) : array { + $sanitized_tax_input = array(); + if ( ! is_array( $taxonomy_data ) ) { + return $sanitized_tax_input; + } + + // Convert taxonomy input to term IDs, to avoid ambiguity. + foreach ( $taxonomy_data as $taxonomy => $terms ) { + $tax_object = get_taxonomy( $taxonomy ); + if ( $tax_object && isset( $tax_object->meta_box_sanitize_cb ) ) { + $sanitized_tax_input[ $taxonomy ] = call_user_func_array( $tax_object->meta_box_sanitize_cb, array( $taxonomy, $terms ) ); + } + } + + return $sanitized_tax_input; + } + + /** + * Add the categories meta box to the order screen. This is just a wrapper around the post_categories_meta_box. + * + * @param \WC_Abstract_Order $order Order object. + * @param array $box Meta box args. + * + * @return void + */ + public function order_categories_meta_box( $order, $box ) { + $post = get_post( $order->get_id() ); + post_categories_meta_box( $post, $box ); + } + + /** + * Add the tags meta box to the order screen. This is just a wrapper around the post_tags_meta_box. + * + * @param \WC_Abstract_Order $order Order object. + * @param array $box Meta box args. + * + * @return void + */ + public function order_tags_meta_box( $order, $box ) { + $post = get_post( $order->get_id() ); + post_tags_meta_box( $post, $box ); + } +} diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 2d25de0d9b3..6e7adfdfc66 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -1641,6 +1641,84 @@ FROM $order_meta_table $changes = $order->get_changes(); $this->update_address_index_meta( $order, $changes ); + $default_taxonomies = $this->init_default_taxonomies( $order, array() ); + $this->set_custom_taxonomies( $order, $default_taxonomies ); + } + + /** + * Set default taxonomies for the order. + * + * Note: This is re-implementation of part of WP core's `wp_insert_post` function. Since the code block that set default taxonomies is not filterable, we have to re-implement it. + * + * @param \WC_Abstract_Order $order Order object. + * @param array $sanitized_tax_input Sanitized taxonomy input. + * + * @return array Sanitized tax input with default taxonomies. + */ + public function init_default_taxonomies( \WC_Abstract_Order $order, array $sanitized_tax_input ) { + if ( 'auto-draft' === $order->get_status() ) { + return $sanitized_tax_input; + } + + foreach ( get_object_taxonomies( $order->get_type(), 'object' ) as $taxonomy => $tax_object ) { + if ( empty( $tax_object->default_term ) ) { + return $sanitized_tax_input; + } + + // Filter out empty terms. + if ( isset( $sanitized_tax_input[ $taxonomy ] ) && is_array( $sanitized_tax_input[ $taxonomy ] ) ) { + $sanitized_tax_input[ $taxonomy ] = array_filter( $sanitized_tax_input[ $taxonomy ] ); + } + + // Passed custom taxonomy list overwrites the existing list if not empty. + $terms = wp_get_object_terms( $order->get_id(), $taxonomy, array( 'fields' => 'ids' ) ); + if ( ! empty( $terms ) && empty( $sanitized_tax_input[ $taxonomy ] ) ) { + $sanitized_tax_input[ $taxonomy ] = $terms; + } + + if ( empty( $sanitized_tax_input[ $taxonomy ] ) ) { + $default_term_id = get_option( 'default_term_' . $taxonomy ); + if ( ! empty( $default_term_id ) ) { + $sanitized_tax_input[ $taxonomy ] = array( (int) $default_term_id ); + } + } + } + return $sanitized_tax_input; + } + + /** + * Set custom taxonomies for the order. + * + * Note: This is re-implementation of part of WP core's `wp_insert_post` function. Since the code block that set custom taxonomies is not filterable, we have to re-implement it. + * + * @param \WC_Abstract_Order $order Order object. + * @param array $sanitized_tax_input Sanitized taxonomy input. + * + * @return void + */ + public function set_custom_taxonomies( \WC_Abstract_Order $order, array $sanitized_tax_input ) { + if ( empty( $sanitized_tax_input ) ) { + return; + } + + foreach ( $sanitized_tax_input as $taxonomy => $tags ) { + $taxonomy_obj = get_taxonomy( $taxonomy ); + + if ( ! $taxonomy_obj ) { + /* translators: %s: Taxonomy name. */ + _doing_it_wrong( __FUNCTION__, esc_html( sprintf( __( 'Invalid taxonomy: %s.', 'woocommerce' ), $taxonomy ) ), '7.9.0' ); + continue; + } + + // array = hierarchical, string = non-hierarchical. + if ( is_array( $tags ) ) { + $tags = array_filter( $tags ); + } + + if ( current_user_can( $taxonomy_obj->cap->assign_terms ) ) { + wp_set_post_terms( $order->get_id(), $tags, $taxonomy ); + } + } } /** diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php index b78acdaf918..e916f54561d 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php @@ -9,7 +9,9 @@ use Automattic\WooCommerce\Internal\Admin\Orders\COTRedirectionController; use Automattic\WooCommerce\Internal\Admin\Orders\Edit; use Automattic\WooCommerce\Internal\Admin\Orders\EditLock; use Automattic\WooCommerce\Internal\Admin\Orders\ListTable; +use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox; use Automattic\WooCommerce\Internal\Admin\Orders\PageController; +use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; /** @@ -28,6 +30,7 @@ class OrderAdminServiceProvider extends AbstractServiceProvider { Edit::class, ListTable::class, EditLock::class, + TaxonomiesMetaBox::class, ); /** @@ -41,5 +44,6 @@ class OrderAdminServiceProvider extends AbstractServiceProvider { $this->share( Edit::class )->addArgument( PageController::class ); $this->share( ListTable::class )->addArgument( PageController::class ); $this->share( EditLock::class ); + $this->share( TaxonomiesMetaBox::class )->addArgument( OrdersTableDataStore::class ); } } diff --git a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php index b164ec95a5f..f966aa2e4ca 100644 --- a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php +++ b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php @@ -190,7 +190,7 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case { */ public function test_apply_coupon_across_status() { $coupon_code = 'coupon_test_count_across_status'; - $coupon = WC_Helper_Coupon::create_coupon( $coupon_code ); + $coupon = WC_Helper_Coupon::create_coupon( $coupon_code ); $this->assertEquals( 0, $coupon->get_usage_count() ); $order = WC_Helper_Order::create_order(); @@ -253,8 +253,8 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case { */ public function test_apply_coupon_stores_meta_data() { $coupon_code = 'coupon_test_meta_data'; - $coupon = WC_Helper_Coupon::create_coupon( $coupon_code ); - $order = WC_Helper_Order::create_order(); + $coupon = WC_Helper_Coupon::create_coupon( $coupon_code ); + $order = WC_Helper_Order::create_order(); $order->set_status( 'processing' ); $order->save(); $order->apply_coupon( $coupon_code ); @@ -324,4 +324,29 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case { $order = wc_get_order( $order->get_id() ); $this->assertInstanceOf( Automattic\WooCommerce\Admin\Overrides\Order::class, $order ); } + + /** + * @testDox When a taxonomy with a default term is set on the order, it's inserted when a new order is created. + */ + public function test_default_term_for_custom_taxonomy() { + $custom_taxonomy = register_taxonomy( + 'custom_taxonomy', + 'shop_order', + array( + 'default_term' => 'new_term', + ), + ); + + // Set user who has access to create term. + $current_user_id = get_current_user_id(); + $user = new WP_User( wp_create_user( 'test', '' ) ); + $user->set_role( 'administrator' ); + wp_set_current_user( $user->ID ); + + $order = wc_create_order(); + + wp_set_current_user( $current_user_id ); + $order_terms = wp_list_pluck( wp_get_object_terms( $order->get_id(), $custom_taxonomy->name ), 'name' ); + $this->assertContains( 'new_term', $order_terms ); + } }