diff --git a/.github/workflows/pr-highlight-changes.yml b/.github/workflows/pr-highlight-changes.yml
index ec9d0c28393..b1ecc977f14 100644
--- a/.github/workflows/pr-highlight-changes.yml
+++ b/.github/workflows/pr-highlight-changes.yml
@@ -22,7 +22,7 @@ jobs:
run: |
npm install -g pnpm@7
npm -g i @wordpress/env@5.1.0
- pnpm install --filter monorepo-utils --filter code-analyzer --filter cli-core
+ pnpm install --filter code-analyzer --filter cli-core
- name: Run analyzer
id: run
working-directory: tools/code-analyzer
diff --git a/changelog.txt b/changelog.txt
index 30d4a66ac9e..a45486b63ae 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,5 +1,129 @@
== Changelog ==
+= 7.6.0 2023-04-13 =
+
+**WooCommerce**
+
+* Fix - Fix incorrect usage of dispatch, useSelect, and setState calls in homescreen along with settings and onboarding package [#37641](https://github.com/woocommerce/woocommerce/pull/37641)
+* Fix - Do not attempt to cache order during order creation (HPOS). [#37569](https://github.com/woocommerce/woocommerce/pull/37569)
+* Fix - Add default value when calling get_option for woocommerce_task_list_tracked_completed_tasks. [#37397](https://github.com/woocommerce/woocommerce/pull/37397)
+* Fix - When order meta data is saved via HPOS, it should be backfilled to the CPT data store. [#36593](https://github.com/woocommerce/woocommerce/pull/36593)
+* Fix - Overwrite clone method to prevent duplicate data when saving a clone. [#37313](https://github.com/woocommerce/woocommerce/pull/37313)
+* Fix - Add default button padding to TT2 stylesheet to fix some visual issues in WP 5.9 and 6.0 [#37018](https://github.com/woocommerce/woocommerce/pull/37018)
+* Fix - Added skydropx slug back to shipping partners list so that it can be installed through the shipping task [#37286](https://github.com/woocommerce/woocommerce/pull/37286)
+* Fix - Add HPOS compat for admin report functions. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
+* Fix - Add HPOS compat for wc-user-functions.php. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
+* Fix - Add support for null inputs to pnpm wc_add_number_precision [#36891](https://github.com/woocommerce/woocommerce/pull/36891)
+* Fix - Add support for `after`, `before`, `modified_after` and `modified_before` params in local timezone. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
+* Fix - Add validation when saving attributes and variations [#37046](https://github.com/woocommerce/woocommerce/pull/37046)
+* Fix - Also delete when order type is placehoder, since it was created by HPOS. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
+* Fix - Corrects a class reference in the ProductDownloadsServiceProvider. [#37052](https://github.com/woocommerce/woocommerce/pull/37052)
+* Fix - Corrects a variable name in Reports\Stock\Stats. It was missed during the last name change. [#37057](https://github.com/woocommerce/woocommerce/pull/37057)
+* Fix - Corrects class namespaces in Onboarding. It was missed during last restructuring. [#37056](https://github.com/woocommerce/woocommerce/pull/37056)
+* Fix - Corrects imported classes. Class names should not begin with a backslash. [#37058](https://github.com/woocommerce/woocommerce/pull/37058)
+* Fix - Ensure product importer imports all lines in a CSV file. [#36839](https://github.com/woocommerce/woocommerce/pull/36839)
+* Fix - Fetch order first to refresh cache before returning prop. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
+* Fix - Fix 0 rendered on short-circuit evaluation. [#37104](https://github.com/woocommerce/woocommerce/pull/37104)
+* Fix - Fix ArrayUtil::get_value_or_default method not behaving as documented for null array values [#37053](https://github.com/woocommerce/woocommerce/pull/37053)
+* Fix - Fix blank screen is displayed during OBW when using WP5.9 [#36903](https://github.com/woocommerce/woocommerce/pull/36903)
+* Fix - Fix duplicated global attribute [#37109](https://github.com/woocommerce/woocommerce/pull/37109)
+* Fix - fixed bug where jetpack connection owner field was assumed to be username when its actually display name [#37170](https://github.com/woocommerce/woocommerce/pull/37170)
+* Fix - Fixed payments recommendations pane in WooCommerce Payment Settings using the wrong image prop [#37259](https://github.com/woocommerce/woocommerce/pull/37259)
+* Fix - Fixes filtering by attributes in the Analytics Orders and Variations reports. [#37223](https://github.com/woocommerce/woocommerce/pull/37223)
+* Fix - Fix incorrect VAT exempt behaviour on shop page when prices are exclusive of tax. [#33991](https://github.com/woocommerce/woocommerce/pull/33991)
+* Fix - Fix React rendering falsy value in marketing page. [#37227](https://github.com/woocommerce/woocommerce/pull/37227)
+* Fix - Fix the inability to apply a coupon whose code is "0" [#36924](https://github.com/woocommerce/woocommerce/pull/36924)
+* Fix - fix typo in variable name [#36759](https://github.com/woocommerce/woocommerce/pull/36759)
+* Fix - Fix unit test snapshots due to a dependency version change [#36435](https://github.com/woocommerce/woocommerce/pull/36435)
+* Fix - Fix variations exported as draft being imported as draft (and thus remaining invisible) [#36933](https://github.com/woocommerce/woocommerce/pull/36933)
+* Fix - Fix WP data resolution (`invalidateResolution`) not working with WP 5.9 in marketing page. [#37198](https://github.com/woocommerce/woocommerce/pull/37198)
+* Fix - Handle date arguments in OrderTableQuery correctly by adjusting their timezones before running. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
+* Fix - Load same stylesheets in the Site Editor as in the frontend [#36911](https://github.com/woocommerce/woocommerce/pull/36911)
+* Fix - Loco Translate and wp-cli compatibility for woocommerce-admin translation files [#36739](https://github.com/woocommerce/woocommerce/pull/36739)
+* Fix - Override react version to 17.0.2 [#37087](https://github.com/woocommerce/woocommerce/pull/37087)
+* Fix - Prevent possible warning arising from use of woocommerce_wp_* family of functions. [#37026](https://github.com/woocommerce/woocommerce/pull/37026)
+* Fix - Record values for toggled checkboxes/features in settings [#37242](https://github.com/woocommerce/woocommerce/pull/37242)
+* Fix - Restore the sort order when orders are cached. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
+* Fix - Treat order as seperate resource when validating for webhook since it's not necessarily a CPT anymore. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
+* Fix - Update Customers report with latest user data after editing user. [#37237](https://github.com/woocommerce/woocommerce/pull/37237)
+* Add - Add "Create a new campaign" modal in Campaigns card in Multichannel Marketing page. [#37044](https://github.com/woocommerce/woocommerce/pull/37044)
+* Add - Add a cache for orders, to use when custom order tables are enabled [#35014](https://github.com/woocommerce/woocommerce/pull/35014)
+* Add - Add an encoding selector to the product importer [#36819](https://github.com/woocommerce/woocommerce/pull/36819)
+* Add - Add Campaigns card into Multichannel Marketing page. [#36735](https://github.com/woocommerce/woocommerce/pull/36735)
+* Add - Added images support for the payment recommendations transaction processors [#37230](https://github.com/woocommerce/woocommerce/pull/37230)
+* Add - Added `woocommerce_widget_layered_nav_filters_start/end` hooks around layered nav filters widget [#36705](https://github.com/woocommerce/woocommerce/pull/36705)
+* Add - Add introduction banner to multichannel marketing page. [#37110](https://github.com/woocommerce/woocommerce/pull/37110)
+* Add - Add marketplace suggestions and multichannel marketing information to WC Tracker. [#37017](https://github.com/woocommerce/woocommerce/pull/37017)
+* Add - Add new feature flag for the product edit blocks experience [#37137](https://github.com/woocommerce/woocommerce/pull/37137)
+* Add - Add productBlockEditorSettings script to be used for the Product Block Editor. [#37123](https://github.com/woocommerce/woocommerce/pull/37123)
+* Add - Add support for new countries in WCPay [#36906](https://github.com/woocommerce/woocommerce/pull/36906)
+* Add - Add wp-json/wc-admin/shipping-partner-suggestions API endpoint [#37155](https://github.com/woocommerce/woocommerce/pull/37155)
+* Add - Allow sorting by menu_order in products widget. [#37002](https://github.com/woocommerce/woocommerce/pull/37002)
+* Add - Create editor skeleton on add/edit product pages [#37023](https://github.com/woocommerce/woocommerce/pull/37023)
+* Add - Creating product entity in auto-draft status, and adding support for retrieving preexisting products. [#37064](https://github.com/woocommerce/woocommerce/pull/37064)
+* Add - Fixed image array in edit context for product/variations endpoint. [#28498](https://github.com/woocommerce/woocommerce/pull/28498)
+* Add - Initial e2e tests for new product editor. [#36902](https://github.com/woocommerce/woocommerce/pull/36902)
+* Add - Log to order notes when coupons are removed or applied. [#30642](https://github.com/woocommerce/woocommerce/pull/30642)
+* Add - Make WC_Order::get_tax_location accessible publicly through a wrapper function. [#36953](https://github.com/woocommerce/woocommerce/pull/36953)
+* Add - Update product post rest config when block editor feature is enabled. [#37206](https://github.com/woocommerce/woocommerce/pull/37206)
+* Update - Update WooCommerce Blocks to 9.8.4 [#37492](https://github.com/woocommerce/woocommerce/pull/37492)
+* Update - Update WooCommerce Blocks to 9.8.3 [#37477](https://github.com/woocommerce/woocommerce/pull/37477)
+* Update - Update WooCommerce Blocks to 9.8.2 [#37373](https://github.com/woocommerce/woocommerce/pull/37373)
+* Update - Add tabs and sections placeholders in product blocks template [#37174](https://github.com/woocommerce/woocommerce/pull/37174)
+* Update - Change the default date used on Revenue and Orders report to 'date_paid' and create spotlight on both reports [#36653](https://github.com/woocommerce/woocommerce/pull/36653)
+* Update - Change Variations form shown in Variations tab when there are no variations created [#36957](https://github.com/woocommerce/woocommerce/pull/36957)
+* Update - Moving currencyContext to relevant package, and updating all references. [#36959](https://github.com/woocommerce/woocommerce/pull/36959)
+* Update - Moving some components out of core and into product-editor package. [#36945](https://github.com/woocommerce/woocommerce/pull/36945)
+* Update - Moving use-product-helper and related product hooks to product editor package. [#37006](https://github.com/woocommerce/woocommerce/pull/37006)
+* Update - Refresh data source poller transients on wc_admin_daily [#37027](https://github.com/woocommerce/woocommerce/pull/37027)
+* Update - Remove accordion from "Other payment providers" in payment task [#37205](https://github.com/woocommerce/woocommerce/pull/37205)
+* Update - Remove Cart2Cart option from add product task [#37285](https://github.com/woocommerce/woocommerce/pull/37285)
+* Update - Show link to store settings when stock management is disabled. [#37140](https://github.com/woocommerce/woocommerce/pull/37140)
+* Update - Update create-wc-extension script within woocommerce-admin. [#36917](https://github.com/woocommerce/woocommerce/pull/36917)
+* Update - Update imports of product slot fills to new @woocommerce/product-editor library [#36830](https://github.com/woocommerce/woocommerce/pull/36830)
+* Update - Update obw payment gateways [#37233](https://github.com/woocommerce/woocommerce/pull/37233)
+* Update - Update playwright api-core-tests to associate orders with real products to prevent extension issues for those that validate product ids [#37243](https://github.com/woocommerce/woocommerce/pull/37243)
+* Update - Update playwright api-core-tests to handle cases where extensions add to shipping methods [#37239](https://github.com/woocommerce/woocommerce/pull/37239)
+* Update - Update product template by adding the list price and sale price blocks. [#37211](https://github.com/woocommerce/woocommerce/pull/37211)
+* Update - Updates automated release testing workflow to use Playwright [#36598](https://github.com/woocommerce/woocommerce/pull/36598)
+* Update - Update template of product type to include product name block. [#37132](https://github.com/woocommerce/woocommerce/pull/37132)
+* Update - Update the date modified field for an order when a refund for it is successfully processed. [#37047](https://github.com/woocommerce/woocommerce/pull/37047)
+* Update - Update WooCommerce BLocks to 9.8.0 [#37210](https://github.com/woocommerce/woocommerce/pull/37210)
+* Update - Update WooCommerce Blocks to 9.8.1 [#37238](https://github.com/woocommerce/woocommerce/pull/37238)
+* Update - Updating rest namespace for product posttype to version 3. [#37028](https://github.com/woocommerce/woocommerce/pull/37028)
+* Update - Use the currently activated theme color for completed tasks strikethough [#37001](https://github.com/woocommerce/woocommerce/pull/37001)
+* Dev - Add @woocommerce/admin-layout package. [#37094](https://github.com/woocommerce/woocommerce/pull/37094)
+* Dev - Add CES data store to @woocommerce/customer-effort-score [#37252](https://github.com/woocommerce/woocommerce/pull/37252)
+* Dev - Add existing global attribute layout #36944 [#36944](https://github.com/woocommerce/woocommerce/pull/36944)
+* Dev - Add missing woocommerce_run_on_woocommerce_admin_updated hook for the scheduled action registered in RemoteInboxNotificationsEngine [#36768](https://github.com/woocommerce/woocommerce/pull/36768)
+* Dev - add wpLogin import to wc-baseline-load.js [#36940](https://github.com/woocommerce/woocommerce/pull/36940)
+* Dev - Convert "Allow backorders?" into radio buttons [#37282](https://github.com/woocommerce/woocommerce/pull/37282)
+* Dev - Fix lint issues [#36988](https://github.com/woocommerce/woocommerce/pull/36988)
+* Dev - Fix the value of `UPDATE_WC` environment variable in the daily k6 performance tests. [#37049](https://github.com/woocommerce/woocommerce/pull/37049)
+* Dev - Move CES components and utilities to @woocommerce/customer-effort-score [#37112](https://github.com/woocommerce/woocommerce/pull/37112)
+* Dev - Move hook to confirm unsaved form changes to navigation package [#36752](https://github.com/woocommerce/woocommerce/pull/36752)
+* Dev - Move product utils into product editor package [#36730](https://github.com/woocommerce/woocommerce/pull/36730)
+* Dev - Set up React Fast Refresh in woocommerce-admin [#37165](https://github.com/woocommerce/woocommerce/pull/37165)
+* Dev - Show "Stock status" as a collection of radio buttons [#37278](https://github.com/woocommerce/woocommerce/pull/37278)
+* Dev - Show a message for variable products [#37185](https://github.com/woocommerce/woocommerce/pull/37185)
+* Dev - Support E2E testing of draft releases. [#36997](https://github.com/woocommerce/woocommerce/pull/36997)
+* Dev - Sync @wordpress package versions via syncpack. [#37034](https://github.com/woocommerce/woocommerce/pull/37034)
+* Tweak - Add productId dependency when getting the product by id in ProductPage [#37152](https://github.com/woocommerce/woocommerce/pull/37152)
+* Tweak - Add tracking for local pickup method in Checkout [#36847](https://github.com/woocommerce/woocommerce/pull/36847)
+* Tweak - Add Tracks events for product inventory tab interactions. [#37202](https://github.com/woocommerce/woocommerce/pull/37202)
+* Tweak - Change Avalara CTA copy in tax task to Download [#37224](https://github.com/woocommerce/woocommerce/pull/37224)
+* Tweak - Make sure 'safe_text' settings are rendered as 'text' inputs for compatibility. [#37154](https://github.com/woocommerce/woocommerce/pull/37154)
+* Tweak - Prevent 'woocommerce_ajax_order_items_removed' from generating PHP warnings. [#37178](https://github.com/woocommerce/woocommerce/pull/37178)
+* Tweak - Rename "Manage stock?" label to "Stock management". [#37135](https://github.com/woocommerce/woocommerce/pull/37135)
+* Tweak - Trigger event `woocommerce_attributes_saved` following successful product meta box ajax update. [#36943](https://github.com/woocommerce/woocommerce/pull/36943)
+* Tweak - Visual tweaks for shipping partner banners [#37229](https://github.com/woocommerce/woocommerce/pull/37229)
+* Performance - Bypass Action Scheduler for customer updates. [#37265](https://github.com/woocommerce/woocommerce/pull/37265)
+* Performance - Switch wc_product_attributes_lookup table management to use truncate and dbDelta over drop table [#36872](https://github.com/woocommerce/woocommerce/pull/36872)
+* Enhancement - Add 'display_context' argument to wc_get_price_to_display(). [#25080](https://github.com/woocommerce/woocommerce/pull/25080)
+* Enhancement - Added woocommerce_reduce_order_item_stock action hook [#34721](https://github.com/woocommerce/woocommerce/pull/34721)
+* Enhancement - Add the support for the C&C Blocks in declaring compatibility feature [#36426](https://github.com/woocommerce/woocommerce/pull/36426)
+
+
= 7.5.1 2023-03-21 =
**WooCommerce**
diff --git a/package.json b/package.json
index 5f912b02622..173ab1f949f 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
"test": "pnpm exec turbo run turbo:test",
"clean": "pnpm store prune && git clean -fx **/node_modules && pnpm i",
"preinstall": "npx only-allow pnpm",
- "postinstall": "pnpm git:update-hooks && pnpm run --filter='./tools/monorepo-utils' build",
+ "postinstall": "pnpm git:update-hooks",
"git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && husky install",
"create-extension": "node ./tools/create-extension/index.js",
"cherry-pick": "node ./tools/cherry-pick/bin/run",
diff --git a/packages/js/components/changelog/update-select_tree_dropdown b/packages/js/components/changelog/update-select_tree_dropdown
new file mode 100644
index 00000000000..5c9d5b64dc6
--- /dev/null
+++ b/packages/js/components/changelog/update-select_tree_dropdown
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Update select tree control dropdown menu for custom slot fill support for display within Modals
diff --git a/packages/js/components/src/experimental-select-control/menu.tsx b/packages/js/components/src/experimental-select-control/menu.tsx
index 691b038af68..2587e8422f2 100644
--- a/packages/js/components/src/experimental-select-control/menu.tsx
+++ b/packages/js/components/src/experimental-select-control/menu.tsx
@@ -10,6 +10,7 @@ import {
useState,
createPortal,
Children,
+ useLayoutEffect,
} from '@wordpress/element';
/**
@@ -37,13 +38,19 @@ export const Menu = ( {
const [ boundingRect, setBoundingRect ] = useState< DOMRect >();
const selectControlMenuRef = useRef< HTMLDivElement >( null );
- useEffect( () => {
- if ( selectControlMenuRef.current?.parentElement ) {
+ useLayoutEffect( () => {
+ if (
+ selectControlMenuRef.current?.parentElement &&
+ selectControlMenuRef.current?.parentElement.clientWidth > 0
+ ) {
setBoundingRect(
selectControlMenuRef.current.parentElement.getBoundingClientRect()
);
}
- }, [ selectControlMenuRef.current ] );
+ }, [
+ selectControlMenuRef.current,
+ selectControlMenuRef.current?.clientWidth,
+ ] );
// Scroll the selected item into view when the menu opens.
useEffect( () => {
diff --git a/packages/js/components/src/experimental-select-tree-control/index.ts b/packages/js/components/src/experimental-select-tree-control/index.ts
index 9ea352bc18b..876360ad549 100644
--- a/packages/js/components/src/experimental-select-tree-control/index.ts
+++ b/packages/js/components/src/experimental-select-tree-control/index.ts
@@ -1 +1,2 @@
export * from './select-tree';
+export * from './select-tree-menu';
diff --git a/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx b/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx
new file mode 100644
index 00000000000..294ceafef60
--- /dev/null
+++ b/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx
@@ -0,0 +1,154 @@
+/**
+ * External dependencies
+ */
+import { Popover, Spinner } from '@wordpress/components';
+import classnames from 'classnames';
+import {
+ createElement,
+ useEffect,
+ useRef,
+ createPortal,
+ useLayoutEffect,
+ useState,
+} from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import {
+ LinkedTree,
+ Tree,
+ TreeControlProps,
+} from '../experimental-tree-control';
+
+type MenuProps = {
+ isOpen: boolean;
+ isLoading?: boolean;
+ position?: Popover.Position;
+ scrollIntoViewOnOpen?: boolean;
+ items: LinkedTree[];
+ treeRef?: React.ForwardedRef< HTMLOListElement >;
+ onClose?: () => void;
+} & Omit< TreeControlProps, 'items' >;
+
+export const SelectTreeMenu = ( {
+ isLoading,
+ isOpen,
+ className,
+ position = 'bottom center',
+ scrollIntoViewOnOpen = false,
+ items,
+ treeRef: ref,
+ onClose = () => {},
+ shouldShowCreateButton,
+ ...props
+}: MenuProps ) => {
+ const [ boundingRect, setBoundingRect ] = useState< DOMRect >();
+ const selectControlMenuRef = useRef< HTMLDivElement >( null );
+
+ useLayoutEffect( () => {
+ if (
+ selectControlMenuRef.current?.parentElement &&
+ selectControlMenuRef.current?.parentElement.clientWidth > 0
+ ) {
+ setBoundingRect(
+ selectControlMenuRef.current.parentElement.getBoundingClientRect()
+ );
+ }
+ }, [
+ selectControlMenuRef.current,
+ selectControlMenuRef.current?.clientWidth,
+ ] );
+
+ // Scroll the selected item into view when the menu opens.
+ useEffect( () => {
+ if ( isOpen && scrollIntoViewOnOpen ) {
+ selectControlMenuRef.current?.scrollIntoView();
+ }
+ }, [ isOpen, scrollIntoViewOnOpen ] );
+
+ const shouldItemBeExpanded = ( item: LinkedTree ): boolean => {
+ if ( ! props.createValue || ! item.children?.length ) return false;
+ return item.children.some( ( child ) => {
+ if (
+ new RegExp( props.createValue || '', 'ig' ).test(
+ child.data.label
+ )
+ ) {
+ return true;
+ }
+ return shouldItemBeExpanded( child );
+ } );
+ };
+
+ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
+ /* Disabled because of the onmouseup on the ul element below. */
+ return (
+
+
+
0,
+ }
+ ) }
+ position={ position }
+ animate={ false }
+ onFocusOutside={ () => {
+ onClose();
+ } }
+ >
+ { isOpen && (
+
+ { isLoading ? (
+
+
+
+ ) : (
+
+ ) }
+
+ ) }
+
+
+
+ );
+ /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
+};
+
+export const SelectTreeMenuSlot: React.FC = () =>
+ createPortal(
+
+ { /* @ts-expect-error name does exist on PopoverSlot see: https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/popover/index.tsx#L555 */ }
+
+
,
+ document.body
+ );
diff --git a/packages/js/components/src/experimental-select-tree-control/select-tree.tsx b/packages/js/components/src/experimental-select-tree-control/select-tree.tsx
index 7406ea9d0f7..5c276342cc8 100644
--- a/packages/js/components/src/experimental-select-tree-control/select-tree.tsx
+++ b/packages/js/components/src/experimental-select-tree-control/select-tree.tsx
@@ -2,24 +2,20 @@
/**
* External dependencies
*/
-import { createElement, useRef, useState } from 'react';
+import { createElement, useState } from '@wordpress/element';
import classNames from 'classnames';
import { search } from '@wordpress/icons';
-import { Dropdown, Spinner } from '@wordpress/components';
+import { useInstanceId } from '@wordpress/compose';
/**
* Internal dependencies
*/
import { useLinkedTree } from '../experimental-tree-control/hooks/use-linked-tree';
-import { Tree } from '../experimental-tree-control/tree';
-import {
- Item,
- LinkedTree,
- TreeControlProps,
-} from '../experimental-tree-control/types';
+import { Item, TreeControlProps } from '../experimental-tree-control/types';
import { SelectedItems } from '../experimental-select-control/selected-items';
import { ComboBox } from '../experimental-select-control/combo-box';
import { SuffixIcon } from '../experimental-select-control/suffix-icon';
+import { SelectTreeMenu } from './select-tree-menu';
interface SelectTreeProps extends TreeControlProps {
id: string;
@@ -44,156 +40,115 @@ export const SelectTree = function SelectTree( {
...props
}: SelectTreeProps ) {
const linkedTree = useLinkedTree( items );
+ const menuInstanceId = useInstanceId(
+ SelectTree,
+ 'woocommerce-select-tree-control__menu'
+ );
const [ isFocused, setIsFocused ] = useState( false );
-
- const comboBoxRef = useRef< HTMLDivElement >( null );
-
- // getting the parent's parent div width to set the width of the dropdown
- const comboBoxWidth =
- comboBoxRef.current?.parentElement?.parentElement?.getBoundingClientRect()
- .width;
-
- const shouldItemBeExpanded = ( item: LinkedTree ): boolean => {
- if ( ! props.createValue || ! item.children?.length ) return false;
- return item.children.some( ( child ) => {
- if (
- new RegExp( props.createValue || '', 'ig' ).test(
- child.data.label
- )
- ) {
- return true;
- }
- return shouldItemBeExpanded( child );
- } );
- };
+ const [ isOpen, setIsOpen ] = useState( false );
return (
-
- isLoading ? (
-
-
-
- ) : (
-
- )
- }
- renderToggle={ ( { isOpen, onToggle, onClose } ) => (
-
+
+
-
- { props.label }
-
-
+ {
+ if ( ! isOpen ) {
+ setIsOpen( true );
+ }
+ setIsFocused( true );
+ },
+ onBlur: ( event ) => {
+ // if blurring to an element inside the dropdown, don't close it
+ if (
+ isOpen &&
+ ! document
+ .querySelector( '.' + menuInstanceId )
+ ?.contains( event.relatedTarget )
+ ) {
+ setIsOpen( false );
+ }
+ setIsFocused( false );
+ },
+ onKeyDown: ( event ) => {
+ setIsOpen( true );
+ if ( event.key === 'ArrowDown' ) {
+ event.preventDefault();
+ // focus on the first element from the Popover
+ (
+ document.querySelector(
+ `.${ menuInstanceId } input, .${ menuInstanceId } button`
+ ) as HTMLInputElement | HTMLButtonElement
+ )?.focus();
+ }
+ if ( event.key === 'Tab' ) {
+ setIsOpen( false );
+ }
+ },
+ onChange: ( event ) =>
+ onInputChange &&
+ onInputChange( event.target.value ),
+ placeholder,
+ } }
+ suffix={ suffix }
+ >
+ item?.label || '' }
+ getItemValue={ ( item ) => item?.value || '' }
+ onRemove={ ( item ) => {
+ if ( ! Array.isArray( item ) && props.onRemove ) {
+ props.onRemove( item );
+ setIsOpen( false );
+ }
} }
- inputProps={ {
- className:
- 'woocommerce-experimental-select-control__input',
- id: `${ props.id }-input`,
- 'aria-autocomplete': 'list',
- 'aria-controls': `${ props.id }-menu`,
- autoComplete: 'off',
- onFocus: () => {
- if ( ! isOpen ) {
- onToggle();
- }
- setIsFocused( true );
- },
- onBlur: ( event ) => {
- // if blurring to an element inside the dropdown, don't close it
- if (
- isOpen &&
- ! document
- .querySelector(
- '.woocommerce-experimental-select-control ~ .components-popover'
- )
- ?.contains( event.relatedTarget )
- ) {
- onClose();
- }
- setIsFocused( false );
- },
- onKeyDown: ( event ) => {
- const baseQuery =
- '.woocommerce-experimental-select-tree-control__dropdown > .components-popover';
- if ( event.key === 'ArrowDown' ) {
- event.preventDefault();
- // focus on the first element from the Popover
- (
- document.querySelector(
- `${ baseQuery } input, ${ baseQuery } button`
- ) as
- | HTMLInputElement
- | HTMLButtonElement
- )?.focus();
- }
- if ( event.key === 'Tab' ) {
- onClose();
- }
- },
- onChange: ( event ) =>
- onInputChange &&
- onInputChange( event.target.value ),
- placeholder,
- } }
- suffix={ suffix }
- >
- item?.label || '' }
- getItemValue={ ( item ) => item?.value || '' }
- onRemove={ ( item ) => {
- if (
- ! Array.isArray( item ) &&
- props.onRemove
- ) {
- props.onRemove( item );
- onClose();
- }
- } }
- getSelectedItemProps={ () => ( {} ) }
- />
-
-
- ) }
- />
+ getSelectedItemProps={ () => ( {} ) }
+ />
+
+
+ setIsOpen( false ) }
+ />
+
);
};
diff --git a/packages/js/components/src/experimental-select-tree-control/stories/index.tsx b/packages/js/components/src/experimental-select-tree-control/stories/index.tsx
index 4034c571905..024f1e68e9e 100644
--- a/packages/js/components/src/experimental-select-tree-control/stories/index.tsx
+++ b/packages/js/components/src/experimental-select-tree-control/stories/index.tsx
@@ -1,14 +1,15 @@
/**
* External dependencies
*/
-
-import React, { createElement } from 'react';
+import React, { createElement, useState } from 'react';
+import { Button, Modal, SlotFillProvider } from '@wordpress/components';
/**
* Internal dependencies
*/
import { SelectTree } from '../select-tree';
import { Item } from '../../experimental-tree-control/types';
+import { SelectTreeMenuSlot } from '../select-tree-menu';
const listItems: Item[] = [
{ value: '1', label: 'Technology' },
@@ -95,6 +96,72 @@ export const MultipleSelectTree: React.FC = () => {
);
};
+export const SingleWithinModalUsingBodyDropdownPlacement: React.FC = () => {
+ const [ isOpen, setOpen ] = useState( true );
+ const [ value, setValue ] = useState( '' );
+ const [ selected, setSelected ] = useState< Item[] >( [] );
+
+ const items = filterItems( listItems, value );
+
+ return (
+
+ Selected: { JSON.stringify( selected ) }
+ setOpen( true ) }>
+ Show Dropdown in Modal
+
+ { isOpen && (
+ setOpen( false ) }
+ >
+
+ ! value ||
+ listItems.findIndex(
+ ( item ) => item.label === typedValue
+ ) === -1
+ }
+ createValue={ value }
+ // eslint-disable-next-line no-alert
+ onCreateNew={ () => alert( 'create new called' ) }
+ onInputChange={ ( a ) => setValue( a || '' ) }
+ onSelect={ ( selectedItems ) => {
+ if ( Array.isArray( selectedItems ) ) {
+ setSelected( [
+ ...selected,
+ ...selectedItems,
+ ] );
+ }
+ } }
+ onRemove={ ( removedItems ) => {
+ const newValues = Array.isArray( removedItems )
+ ? selected.filter(
+ ( item ) =>
+ ! removedItems.some(
+ ( { value: removedValue } ) =>
+ item.value === removedValue
+ )
+ )
+ : selected.filter(
+ ( item ) =>
+ item.value !== removedItems.value
+ );
+ setSelected( newValues );
+ } }
+ />
+
+ ) }
+
+
+ );
+};
+
export default {
title: 'WooCommerce Admin/experimental/SelectTreeControl',
component: SelectTree,
diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts
index 99d39bf3fa9..e971ee0d231 100644
--- a/packages/js/components/src/index.ts
+++ b/packages/js/components/src/index.ts
@@ -99,7 +99,10 @@ export {
TreeControl as __experimentalTreeControl,
Item as TreeItemType,
} from './experimental-tree-control';
-export { SelectTree as __experimentalSelectTreeControl } from './experimental-select-tree-control';
+export {
+ SelectTree as __experimentalSelectTreeControl,
+ SelectTreeMenuSlot as __experimentalSelectTreeMenuSlot,
+} from './experimental-select-tree-control';
export { default as TreeSelectControl } from './tree-select-control';
// Exports below can be removed once the @woocommerce/product-editor package is released.
diff --git a/packages/js/experimental/changelog/fix-37654-invalid-return-callback-ref b/packages/js/experimental/changelog/fix-37654-invalid-return-callback-ref
new file mode 100644
index 00000000000..d14e18f1448
--- /dev/null
+++ b/packages/js/experimental/changelog/fix-37654-invalid-return-callback-ref
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix invalid return callback ref warning
diff --git a/packages/js/experimental/src/inbox-note/test/inbox-note.tsx b/packages/js/experimental/src/inbox-note/test/inbox-note.tsx
index f1078577e04..bbe03171804 100644
--- a/packages/js/experimental/src/inbox-note/test/inbox-note.tsx
+++ b/packages/js/experimental/src/inbox-note/test/inbox-note.tsx
@@ -23,6 +23,8 @@ jest.mock( 'react-visibility-sensor', () =>
} )
);
+window.open = jest.fn();
+
describe( 'InboxNoteCard', () => {
const note = {
id: 1,
diff --git a/packages/js/experimental/src/inbox-note/test/use-callback-on-link-click.tsx b/packages/js/experimental/src/inbox-note/test/use-callback-on-link-click.tsx
index a7c4d334565..2efcb221b98 100644
--- a/packages/js/experimental/src/inbox-note/test/use-callback-on-link-click.tsx
+++ b/packages/js/experimental/src/inbox-note/test/use-callback-on-link-click.tsx
@@ -42,4 +42,17 @@ describe( 'useCallbackOnLinkClick hook', () => {
userEvent.click( getByText( 'Button' ) );
expect( callback ).not.toHaveBeenCalled();
} );
+
+ it( 'should remove listener on unmount', () => {
+ const listener = jest.fn();
+ const { getByText, unmount } = render(
+
+ );
+ const span = getByText( 'Some Text' );
+ jest.spyOn( span, 'removeEventListener' ).mockImplementation(
+ listener
+ );
+ unmount();
+ expect( listener ).toHaveBeenCalledTimes( 1 );
+ } );
} );
diff --git a/packages/js/experimental/src/inbox-note/use-callback-on-link-click.ts b/packages/js/experimental/src/inbox-note/use-callback-on-link-click.ts
index e7243aff958..537c08e0467 100644
--- a/packages/js/experimental/src/inbox-note/use-callback-on-link-click.ts
+++ b/packages/js/experimental/src/inbox-note/use-callback-on-link-click.ts
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { useCallback } from '@wordpress/element';
+import { useCallback, useEffect, useRef } from '@wordpress/element';
export function useCallbackOnLinkClick( onClick: ( link: string ) => void ) {
const onNodeClick = useCallback(
@@ -20,17 +20,21 @@ export function useCallbackOnLinkClick( onClick: ( link: string ) => void ) {
[ onClick ]
);
- return useCallback(
- ( node: HTMLElement ) => {
+ const nodeRef = useRef< HTMLElement | null >( null );
+
+ useEffect( () => {
+ const node = nodeRef.current;
+ if ( node ) {
+ node.addEventListener( 'click', onNodeClick );
+ }
+ return () => {
if ( node ) {
- node.addEventListener( 'click', onNodeClick );
+ node.removeEventListener( 'click', onNodeClick );
}
- return () => {
- if ( node ) {
- node.removeEventListener( 'click', onNodeClick );
- }
- };
- },
- [ onNodeClick ]
- );
+ };
+ }, [ onNodeClick ] );
+
+ return useCallback( ( node: HTMLElement ) => {
+ nodeRef.current = node;
+ }, [] );
}
diff --git a/packages/js/product-editor/changelog/add-inventory-advanced-37401 b/packages/js/product-editor/changelog/add-inventory-advanced-37401
new file mode 100644
index 00000000000..9465dcb1bb7
--- /dev/null
+++ b/packages/js/product-editor/changelog/add-inventory-advanced-37401
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Adding inventory email, conditional and checkbox blocks.
diff --git a/packages/js/product-editor/changelog/add-inventory-sku-block-37399 b/packages/js/product-editor/changelog/add-inventory-sku-block-37399
new file mode 100644
index 00000000000..c6141801e2a
--- /dev/null
+++ b/packages/js/product-editor/changelog/add-inventory-sku-block-37399
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Adding sku block to product editor.
diff --git a/packages/js/product-editor/changelog/add-product-link-edit-37610 b/packages/js/product-editor/changelog/add-product-link-edit-37610
new file mode 100644
index 00000000000..ab36038aa75
--- /dev/null
+++ b/packages/js/product-editor/changelog/add-product-link-edit-37610
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Refactoring product link modal and adding link to product block editor.
diff --git a/packages/js/product-editor/changelog/fix-rest-namespace-blocks-37619 b/packages/js/product-editor/changelog/fix-rest-namespace-blocks-37619
new file mode 100644
index 00000000000..a6315f79155
--- /dev/null
+++ b/packages/js/product-editor/changelog/fix-rest-namespace-blocks-37619
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Adding apifetch middleware to override product api endpoint only for the product editor.
diff --git a/packages/js/product-editor/changelog/update-select_tree_dropdown b/packages/js/product-editor/changelog/update-select_tree_dropdown
new file mode 100644
index 00000000000..e759cafaa65
--- /dev/null
+++ b/packages/js/product-editor/changelog/update-select_tree_dropdown
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Fix issue with category parent select control clearing search value when typing.
diff --git a/packages/js/product-editor/package.json b/packages/js/product-editor/package.json
index f508c8d9d77..49b271d990a 100644
--- a/packages/js/product-editor/package.json
+++ b/packages/js/product-editor/package.json
@@ -41,6 +41,7 @@
"@woocommerce/number": "workspace:*",
"@woocommerce/settings": "^1.0.0",
"@woocommerce/tracks": "workspace:^1.3.0",
+ "@wordpress/api-fetch": "wp-6.0",
"@wordpress/block-editor": "^9.8.0",
"@wordpress/blocks": "^12.3.0",
"@wordpress/components": "wp-6.0",
diff --git a/packages/js/product-editor/src/blocks/checkbox/block.json b/packages/js/product-editor/src/blocks/checkbox/block.json
new file mode 100644
index 00000000000..08b0348a07b
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/checkbox/block.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "woocommerce/product-checkbox",
+ "title": "Product checkbox control",
+ "category": "woocommerce",
+ "description": "The product checkbox.",
+ "keywords": [ "products", "checkbox", "input" ],
+ "textdomain": "default",
+ "attributes": {
+ "title": {
+ "type": "string",
+ "__experimentalRole": "content"
+ },
+ "label": {
+ "type": "string"
+ },
+ "property": {
+ "type": "string"
+ },
+ "tooltip": {
+ "type": "string"
+ }
+ },
+ "supports": {
+ "align": false,
+ "html": false,
+ "multiple": true,
+ "reusable": false,
+ "inserter": false,
+ "lock": false,
+ "__experimentalToolbar": false
+ },
+ "editorStyle": "file:./editor.css"
+}
diff --git a/packages/js/product-editor/src/blocks/checkbox/edit.tsx b/packages/js/product-editor/src/blocks/checkbox/edit.tsx
new file mode 100644
index 00000000000..64655ac5d89
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/checkbox/edit.tsx
@@ -0,0 +1,62 @@
+/**
+ * External dependencies
+ */
+import { createElement, createInterpolateElement } from '@wordpress/element';
+import type { BlockAttributes } from '@wordpress/blocks';
+import { CheckboxControl, Tooltip } from '@wordpress/components';
+import { useBlockProps } from '@wordpress/block-editor';
+import { useEntityProp } from '@wordpress/core-data';
+import { Icon, help } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+
+export function Edit( { attributes }: { attributes: BlockAttributes } ) {
+ const blockProps = useBlockProps( {
+ className: 'woocommerce-product-form__checkbox',
+ } );
+ const { property, title, label, tooltip } = attributes;
+ const [ value, setValue ] = useEntityProp< boolean >(
+ 'postType',
+ 'product',
+ property
+ );
+
+ return (
+
+
{ title }
+ `, {
+ label: { label } ,
+ tooltip: (
+ { tooltip } }
+ position="top center"
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore Incorrect types.
+ className={
+ 'woocommerce-product-form__checkbox-tooltip'
+ }
+ delay={ 0 }
+ >
+
+
+
+
+ ),
+ } )
+ : label
+ }
+ checked={ value }
+ onChange={ ( selected ) => setValue( selected ) }
+ />
+
+ );
+}
diff --git a/packages/js/product-editor/src/blocks/checkbox/editor.scss b/packages/js/product-editor/src/blocks/checkbox/editor.scss
new file mode 100644
index 00000000000..472ffb521b5
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/checkbox/editor.scss
@@ -0,0 +1,24 @@
+
+.woocommerce-product-form__checkbox {
+
+ .components-base-control__field {
+ display: flex;
+ }
+
+ .components-checkbox-control__label {
+ display: flex;
+ }
+
+ &-tooltip-icon {
+ margin: -2px 0 0 $gap-small;
+ }
+}
+
+.woocommerce-product-form__checkbox-tooltip {
+ .components-popover__content {
+ width: 200px;
+ min-width: auto;
+ white-space: normal !important;
+ }
+}
+
diff --git a/packages/js/product-editor/src/blocks/checkbox/index.ts b/packages/js/product-editor/src/blocks/checkbox/index.ts
new file mode 100644
index 00000000000..15144b2b28d
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/checkbox/index.ts
@@ -0,0 +1,17 @@
+/**
+ * Internal dependencies
+ */
+import initBlock from '../../utils/init-block';
+import metadata from './block.json';
+import { Edit } from './edit';
+
+const { name } = metadata;
+
+export { metadata, name };
+
+export const settings = {
+ example: {},
+ edit: Edit,
+};
+
+export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/js/product-editor/src/blocks/conditional/block.json b/packages/js/product-editor/src/blocks/conditional/block.json
new file mode 100644
index 00000000000..572a2d90c35
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/conditional/block.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "woocommerce/conditional",
+ "title": "Conditional",
+ "category": "widgets",
+ "description": "Container to only conditionally render inner blocks.",
+ "textdomain": "default",
+ "attributes": {
+ "mustMatch": {
+ "__experimentalRole": "content",
+ "type": "array",
+ "items": {
+ "type": "object"
+ },
+ "default": []
+ }
+ },
+ "supports": {
+ "align": false,
+ "html": false,
+ "multiple": true,
+ "reusable": false,
+ "inserter": false,
+ "lock": false
+ }
+}
diff --git a/packages/js/product-editor/src/blocks/conditional/edit.tsx b/packages/js/product-editor/src/blocks/conditional/edit.tsx
new file mode 100644
index 00000000000..61e15444d4d
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/conditional/edit.tsx
@@ -0,0 +1,52 @@
+/**
+ * External dependencies
+ */
+import type { BlockAttributes } from '@wordpress/blocks';
+import { createElement, useMemo } from '@wordpress/element';
+import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
+import { useSelect } from '@wordpress/data';
+import { DisplayState } from '@woocommerce/components';
+import { Product } from '@woocommerce/data';
+// 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 { useEntityId } from '@wordpress/core-data';
+
+export function Edit( {
+ attributes,
+}: {
+ attributes: BlockAttributes & {
+ mustMatch: Record< string, Array< string > >;
+ };
+} ) {
+ const blockProps = useBlockProps();
+ const { mustMatch } = attributes;
+
+ const productId = useEntityId( 'postType', 'product' );
+ const product: Product = useSelect( ( select ) =>
+ select( 'core' ).getEditedEntityRecord(
+ 'postType',
+ 'product',
+ productId
+ )
+ );
+
+ const displayBlocks = useMemo( () => {
+ for ( const [ prop, values ] of Object.entries( mustMatch ) ) {
+ if ( ! values.includes( product[ prop ] ) ) {
+ return false;
+ }
+ }
+ return true;
+ }, [ mustMatch, product ] );
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/packages/js/product-editor/src/blocks/conditional/index.ts b/packages/js/product-editor/src/blocks/conditional/index.ts
new file mode 100644
index 00000000000..43b3aabdc22
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/conditional/index.ts
@@ -0,0 +1,18 @@
+/**
+ * Internal dependencies
+ */
+import { initBlock } from '../../utils';
+import metadata from './block.json';
+import { Edit } from './edit';
+
+const { name } = metadata;
+
+export { metadata, name };
+
+export const settings = {
+ example: {},
+ edit: Edit,
+};
+
+export const init = () =>
+ initBlock( { name, metadata: metadata as never, settings } );
diff --git a/packages/js/product-editor/src/blocks/inventory-email/block.json b/packages/js/product-editor/src/blocks/inventory-email/block.json
new file mode 100644
index 00000000000..749846acb5a
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/inventory-email/block.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "woocommerce/product-inventory-email",
+ "title": "Stock level threshold",
+ "category": "widgets",
+ "description": "Stock management minimum quantity.",
+ "keywords": [ "products", "inventory", "email", "minimum" ],
+ "textdomain": "default",
+ "attributes": {
+ "name": {
+ "type": "string",
+ "__experimentalRole": "content"
+ }
+ },
+ "supports": {
+ "align": false,
+ "html": false,
+ "multiple": false,
+ "reusable": false,
+ "inserter": false,
+ "lock": false
+ },
+ "editorStyle": "file:./editor.css"
+}
diff --git a/packages/js/product-editor/src/blocks/inventory-email/edit.tsx b/packages/js/product-editor/src/blocks/inventory-email/edit.tsx
new file mode 100644
index 00000000000..44af82442d1
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/inventory-email/edit.tsx
@@ -0,0 +1,85 @@
+/**
+ * External dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+import { Link } from '@woocommerce/components';
+
+import {
+ createElement,
+ Fragment,
+ createInterpolateElement,
+} from '@wordpress/element';
+import { getSetting } from '@woocommerce/settings';
+
+import { useBlockProps } from '@wordpress/block-editor';
+
+import {
+ BaseControl,
+ // @ts-expect-error `__experimentalInputControl` does exist.
+ __experimentalInputControl as InputControl,
+} from '@wordpress/components';
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore No types for this exist yet.
+// eslint-disable-next-line @woocommerce/dependency-group
+import { useEntityProp } from '@wordpress/core-data';
+
+export function Edit() {
+ const blockProps = useBlockProps( {
+ className: 'woocommerce-product-form__inventory-email',
+ } );
+ const notifyLowStockAmount = getSetting( 'notifyLowStockAmount', 2 );
+
+ const [ lowStockAmount, setLowStockAmount ] = useEntityProp(
+ 'postType',
+ 'product',
+ 'low_stock_amount'
+ );
+
+ return (
+ <>
+
+
+
+ store settings.',
+ 'woocommerce'
+ ),
+ {
+ link: (
+
+ ),
+ }
+ ) }
+ >
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/packages/js/product-editor/src/blocks/inventory-email/index.ts b/packages/js/product-editor/src/blocks/inventory-email/index.ts
new file mode 100644
index 00000000000..59d35906a12
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/inventory-email/index.ts
@@ -0,0 +1,22 @@
+/**
+ * Internal dependencies
+ */
+import { initBlock } from '../../utils';
+import metadata from './block.json';
+import { Edit } from './edit';
+
+const { name } = metadata;
+
+export { metadata, name };
+
+export const settings = {
+ example: {},
+ edit: Edit,
+};
+
+export const init = () =>
+ initBlock( {
+ name,
+ metadata: metadata as never,
+ settings,
+ } );
diff --git a/packages/js/product-editor/src/blocks/inventory-email/style.scss b/packages/js/product-editor/src/blocks/inventory-email/style.scss
new file mode 100644
index 00000000000..893e1b68016
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/inventory-email/style.scss
@@ -0,0 +1,3 @@
+.woocommerce-product-form__inventory-email {
+ margin-top: $gap-large;
+}
diff --git a/packages/js/product-editor/src/blocks/inventory-sku/block.json b/packages/js/product-editor/src/blocks/inventory-sku/block.json
new file mode 100644
index 00000000000..335c9de9858
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/inventory-sku/block.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "woocommerce/product-sku",
+ "title": "Product text control",
+ "category": "woocommerce",
+ "description": "The product sku.",
+ "keywords": [ "products", "sku" ],
+ "textdomain": "default",
+ "attributes": {
+ "name": {
+ "type": "string",
+ "__experimentalRole": "content"
+ }
+ },
+ "supports": {
+ "align": false,
+ "html": false,
+ "multiple": true,
+ "reusable": false,
+ "inserter": false,
+ "lock": false,
+ "__experimentalToolbar": false
+ },
+ "editorStyle": "file:./editor.css"
+}
diff --git a/packages/js/product-editor/src/blocks/inventory-sku/edit.tsx b/packages/js/product-editor/src/blocks/inventory-sku/edit.tsx
new file mode 100644
index 00000000000..9c4519a986c
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/inventory-sku/edit.tsx
@@ -0,0 +1,51 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { createElement, createInterpolateElement } from '@wordpress/element';
+import { useBlockProps } from '@wordpress/block-editor';
+
+import {
+ BaseControl,
+ // @ts-expect-error `__experimentalInputControl` does exist.
+ __experimentalInputControl as InputControl,
+} from '@wordpress/components';
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore No types for this exist yet.
+// eslint-disable-next-line @woocommerce/dependency-group
+import { useEntityProp } from '@wordpress/core-data';
+
+/**
+ * Internal dependencies
+ */
+
+export function Edit() {
+ const blockProps = useBlockProps();
+
+ const [ sku, setSku ] = useEntityProp( 'postType', 'product', 'sku' );
+
+ return (
+
+ ', 'woocommerce' ),
+ {
+ description: (
+
+ { __( '(STOCK KEEPING UNIT)', 'woocommerce' ) }
+
+ ),
+ }
+ ) }
+ >
+
+
+
+ );
+}
diff --git a/packages/js/product-editor/src/blocks/inventory-sku/index.ts b/packages/js/product-editor/src/blocks/inventory-sku/index.ts
new file mode 100644
index 00000000000..15144b2b28d
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/inventory-sku/index.ts
@@ -0,0 +1,17 @@
+/**
+ * Internal dependencies
+ */
+import initBlock from '../../utils/init-block';
+import metadata from './block.json';
+import { Edit } from './edit';
+
+const { name } = metadata;
+
+export { metadata, name };
+
+export const settings = {
+ example: {},
+ edit: Edit,
+};
+
+export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/js/product-editor/src/blocks/inventory-sku/style.scss b/packages/js/product-editor/src/blocks/inventory-sku/style.scss
new file mode 100644
index 00000000000..32372ce5b82
--- /dev/null
+++ b/packages/js/product-editor/src/blocks/inventory-sku/style.scss
@@ -0,0 +1,13 @@
+
+.woocommerce-product-form_inventory-sku {
+ .components-base-control__label {
+ display: flex;
+ align-items: center;
+ }
+ .woocommerce-product-form__optional-input {
+ margin-left: $gap-smallest;
+ }
+ .woocommerce-tooltip__button {
+ padding: 0 0 0 $gap-smallest;
+ }
+}
diff --git a/packages/js/product-editor/src/components/details-categories-field/create-category-modal.tsx b/packages/js/product-editor/src/components/details-categories-field/create-category-modal.tsx
index 0b9d2ae3b31..eefec178245 100644
--- a/packages/js/product-editor/src/components/details-categories-field/create-category-modal.tsx
+++ b/packages/js/product-editor/src/components/details-categories-field/create-category-modal.tsx
@@ -32,6 +32,15 @@ type CreateCategoryModalProps = {
onCreate: ( newCategory: ProductCategory ) => void;
};
+function getCategoryItemLabel( item: ProductCategoryNode | null ): string {
+ return item?.name || '';
+}
+function getCategoryItemValue(
+ item: ProductCategoryNode | null
+): string | number {
+ return item?.id || '';
+}
+
export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( {
initialCategoryName,
onCancel,
@@ -109,8 +118,8 @@ export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( {
onRemove={ () => setCategoryParent( null ) }
onInputChange={ debouncedSearch }
getFilteredItems={ getFilteredItems }
- getItemLabel={ ( item ) => item?.name || '' }
- getItemValue={ ( item ) => item?.id || '' }
+ getItemLabel={ getCategoryItemLabel }
+ getItemValue={ getCategoryItemValue }
>
{ ( {
items,
diff --git a/packages/js/product-editor/src/components/details-name-block/edit.tsx b/packages/js/product-editor/src/components/details-name-block/edit.tsx
index ea38e9a0703..a06c2de2d9a 100644
--- a/packages/js/product-editor/src/components/details-name-block/edit.tsx
+++ b/packages/js/product-editor/src/components/details-name-block/edit.tsx
@@ -2,36 +2,179 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
-import { createElement, createInterpolateElement } from '@wordpress/element';
-import { TextControl } from '@woocommerce/components';
+import {
+ createElement,
+ Fragment,
+ createInterpolateElement,
+ useState,
+} from '@wordpress/element';
+
import { useBlockProps } from '@wordpress/block-editor';
+import { cleanForSlug } from '@wordpress/url';
+import { useSelect, useDispatch } from '@wordpress/data';
+import {
+ PRODUCTS_STORE_NAME,
+ WCDataSelector,
+ Product,
+} from '@woocommerce/data';
+import {
+ Button,
+ BaseControl,
+ // @ts-expect-error `__experimentalInputControl` does exist.
+ __experimentalInputControl as InputControl,
+} from '@wordpress/components';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
-import { useEntityProp } from '@wordpress/core-data';
+import { useEntityProp, useEntityId } from '@wordpress/core-data';
+
+/**
+ * Internal dependencies
+ */
+import { AUTO_DRAFT_NAME } from '../../utils';
+import { EditProductLinkModal } from '../edit-product-link-modal';
+
+import { useValidation } from '../../hooks/use-validation';
export function Edit() {
const blockProps = useBlockProps();
- const [ name, setName ] = useEntityProp( 'postType', 'product', 'name' );
+
+ const { editEntityRecord, saveEntityRecord } = useDispatch( 'core' );
+
+ const [ showProductLinkEditModal, setShowProductLinkEditModal ] =
+ useState( false );
+
+ const productId = useEntityId( 'postType', 'product' );
+ const product: Product = useSelect( ( select ) =>
+ select( 'core' ).getEditedEntityRecord(
+ 'postType',
+ 'product',
+ productId
+ )
+ );
+
+ const [ sku, setSku ] = useEntityProp( 'postType', 'product', 'sku' );
+ const [ name, setName ] = useEntityProp< string >(
+ 'postType',
+ 'product',
+ 'name'
+ );
+
+ const { permalinkPrefix, permalinkSuffix } = useSelect(
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ ( select: WCDataSelector ) => {
+ const { getPermalinkParts } = select( PRODUCTS_STORE_NAME );
+ if ( productId ) {
+ const parts = getPermalinkParts( productId );
+ return {
+ permalinkPrefix: parts?.prefix,
+ permalinkSuffix: parts?.suffix,
+ };
+ }
+ return {};
+ }
+ );
+
+ const nameIsValid = useValidation(
+ 'product/name',
+ () => Boolean( name ) && name !== AUTO_DRAFT_NAME
+ );
+
+ const setSkuIfEmpty = () => {
+ if ( sku || ! nameIsValid ) {
+ return;
+ }
+ setSku( cleanForSlug( name ) );
+ };
return (
-
-
', 'woocommerce' ),
- {
- required: (
-
- { __( '(required)', 'woocommerce' ) }
-
- ),
- }
+ <>
+
+
', 'woocommerce' ),
+ {
+ required: (
+
+ { __( '*', 'woocommerce' ) }
+
+ ),
+ }
+ ) }
+ >
+
+
+ { productId &&
+ nameIsValid &&
+ [ 'publish', 'draft' ].includes( product.status ) &&
+ permalinkPrefix && (
+
+ { __( 'Product link', 'woocommerce' ) }
+ :
+
+ { permalinkPrefix }
+ { product.slug || cleanForSlug( name ) }
+ { permalinkSuffix }
+
+
+ setShowProductLinkEditModal( true )
+ }
+ >
+ { __( 'Edit', 'woocommerce' ) }
+
+
+ ) }
+ { showProductLinkEditModal && (
+
setShowProductLinkEditModal( false ) }
+ onSaved={ () => setShowProductLinkEditModal( false ) }
+ saveHandler={ async ( updatedSlug ) => {
+ const { slug, permalink }: Product =
+ await saveEntityRecord( 'postType', 'product', {
+ id: product.id,
+ slug: updatedSlug,
+ } );
+
+ if ( slug && permalink ) {
+ editEntityRecord(
+ 'postType',
+ 'product',
+ product.id,
+ {
+ slug,
+ permalink,
+ }
+ );
+
+ return {
+ slug,
+ permalink,
+ };
+ }
+ } }
+ />
) }
- name={ 'woocommerce-product-name' }
- placeholder={ __( 'e.g. 12 oz Coffee Mug', 'woocommerce' ) }
- onChange={ setName }
- value={ name || '' }
- />
-
+
+ >
);
}
diff --git a/packages/js/product-editor/src/components/details-name-block/style.scss b/packages/js/product-editor/src/components/details-name-block/style.scss
new file mode 100644
index 00000000000..b6e6e66c221
--- /dev/null
+++ b/packages/js/product-editor/src/components/details-name-block/style.scss
@@ -0,0 +1,25 @@
+.product-details-section {
+
+ &__product-link {
+ color: #757575;
+ font-size: 12px;
+ display: block;
+ margin-top: $gap-smaller;
+
+ > a {
+ color: inherit;
+ text-decoration: none;
+ font-weight: 600;
+ }
+
+ .components-button.is-link {
+ font-size: 12px;
+ text-decoration: none;
+ margin-left: $gap-smaller;
+ }
+ }
+}
+
+.woocommerce-product-form__required-input {
+ color: #CC1818;
+}
diff --git a/packages/js/product-editor/src/components/details-name-field/details-name-field.tsx b/packages/js/product-editor/src/components/details-name-field/details-name-field.tsx
index 498c1f016e6..b964a445da3 100644
--- a/packages/js/product-editor/src/components/details-name-field/details-name-field.tsx
+++ b/packages/js/product-editor/src/components/details-name-field/details-name-field.tsx
@@ -22,11 +22,13 @@ import {
*/
import { PRODUCT_DETAILS_SLUG } from '../../constants';
import { EditProductLinkModal } from '../edit-product-link-modal';
+import { useProductHelper } from '../../hooks/use-product-helper';
export const DetailsNameField = ( {} ) => {
+ const { updateProductWithStatus } = useProductHelper();
const [ showProductLinkEditModal, setShowProductLinkEditModal ] =
useState( false );
- const { getInputProps, values, touched, errors, setValue } =
+ const { getInputProps, values, touched, errors, setValue, resetForm } =
useFormContext< Product >();
const { permalinkPrefix, permalinkSuffix } = useSelect(
@@ -102,6 +104,33 @@ export const DetailsNameField = ( {} ) => {
product={ values }
onCancel={ () => setShowProductLinkEditModal( false ) }
onSaved={ () => setShowProductLinkEditModal( false ) }
+ saveHandler={ async ( slug ) => {
+ const updatedProduct = await updateProductWithStatus(
+ values.id,
+ {
+ slug,
+ },
+ values.status,
+ true
+ );
+ if ( updatedProduct && updatedProduct.id ) {
+ // only reset the updated slug and permalink fields.
+ resetForm(
+ {
+ ...values,
+ slug: updatedProduct.slug,
+ permalink: updatedProduct.permalink,
+ },
+ touched,
+ errors
+ );
+
+ return {
+ slug: updatedProduct.slug,
+ permalink: updatedProduct.permalink,
+ };
+ }
+ } }
/>
) }
diff --git a/packages/js/product-editor/src/components/edit-product-link-modal/edit-product-link-modal.tsx b/packages/js/product-editor/src/components/edit-product-link-modal/edit-product-link-modal.tsx
index fab86bce6f5..9f1170e0937 100644
--- a/packages/js/product-editor/src/components/edit-product-link-modal/edit-product-link-modal.tsx
+++ b/packages/js/product-editor/src/components/edit-product-link-modal/edit-product-link-modal.tsx
@@ -11,20 +11,17 @@ import {
import { useDispatch } from '@wordpress/data';
import { cleanForSlug } from '@wordpress/url';
import { Product } from '@woocommerce/data';
-import { useFormContext } from '@woocommerce/components';
import { recordEvent } from '@woocommerce/tracks';
-/**
- * Internal dependencies
- */
-import { useProductHelper } from '../../hooks/use-product-helper';
-
type EditProductLinkModalProps = {
product: Product;
permalinkPrefix: string;
permalinkSuffix: string;
onCancel: () => void;
onSaved: () => void;
+ saveHandler: (
+ slug: string
+ ) => Promise< { slug: string; permalink: string } | undefined >;
};
export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( {
@@ -33,14 +30,13 @@ export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( {
permalinkSuffix,
onCancel,
onSaved,
+ saveHandler,
} ) => {
const { createNotice } = useDispatch( 'core/notices' );
- const { updateProductWithStatus, isUpdatingDraft, isUpdatingPublished } =
- useProductHelper();
+ const [ isSaving, setIsSaving ] = useState< boolean >( false );
const [ slug, setSlug ] = useState(
product.slug || cleanForSlug( product.name )
);
- const { resetForm, touched, errors } = useFormContext< Product >();
const onSave = async () => {
recordEvent( 'product_update_slug', {
@@ -48,35 +44,19 @@ export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( {
product_id: product.id,
product_type: product.type,
} );
- const updatedProduct = await updateProductWithStatus(
- product.id,
- {
- slug,
- },
- product.status,
- true
- );
- if ( updatedProduct && updatedProduct.id ) {
- // only reset the updated slug and permalink fields.
- resetForm(
- {
- ...product,
- slug: updatedProduct.slug,
- permalink: updatedProduct.permalink,
- },
- touched,
- errors
- );
+
+ const { slug: updatedSlug, permalink: updatedPermalink } =
+ ( await saveHandler( slug ) ) ?? {};
+
+ if ( updatedSlug ) {
createNotice(
- updatedProduct.slug === cleanForSlug( slug )
- ? 'success'
- : 'info',
- updatedProduct.slug === cleanForSlug( slug )
+ updatedSlug === cleanForSlug( slug ) ? 'success' : 'info',
+ updatedSlug === cleanForSlug( slug )
? __( 'Product link successfully updated.', 'woocommerce' )
: __(
'Product link already existed, updated to ',
'woocommerce'
- ) + updatedProduct.permalink
+ ) + updatedPermalink
);
} else {
createNotice(
@@ -122,14 +102,12 @@ export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( {
{
- onSave();
+ isBusy={ isSaving }
+ disabled={ isSaving || slug === product.slug }
+ onClick={ async () => {
+ setIsSaving( true );
+ await onSave();
+ setIsSaving( false );
} }
>
{ __( 'Save', 'woocommerce' ) }
diff --git a/packages/js/product-editor/src/components/edit-product-link-modal/test/edit-product-link-modal.test.tsx b/packages/js/product-editor/src/components/edit-product-link-modal/test/edit-product-link-modal.test.tsx
index d134dfddf8b..bb33e69164c 100644
--- a/packages/js/product-editor/src/components/edit-product-link-modal/test/edit-product-link-modal.test.tsx
+++ b/packages/js/product-editor/src/components/edit-product-link-modal/test/edit-product-link-modal.test.tsx
@@ -37,6 +37,12 @@ describe( 'EditProductLinkModal', () => {
}
onCancel={ () => {} }
onSaved={ () => {} }
+ saveHandler={ () =>
+ new Promise( () => ( {
+ slug: 'test-slug',
+ permalink: 'http://test-link',
+ } ) )
+ }
/>
);
expect(
@@ -57,6 +63,12 @@ describe( 'EditProductLinkModal', () => {
}
onCancel={ () => {} }
onSaved={ () => {} }
+ saveHandler={ () =>
+ new Promise( () => ( {
+ slug: 'test-slug',
+ permalink: 'http://test-link',
+ } ) )
+ }
/>
);
userEvent.type(
@@ -82,6 +94,12 @@ describe( 'EditProductLinkModal', () => {
}
onCancel={ () => {} }
onSaved={ () => {} }
+ saveHandler={ () =>
+ new Promise( () => ( {
+ slug: 'test-slug',
+ permalink: 'http://test-link',
+ } ) )
+ }
/>
);
userEvent.type(
diff --git a/packages/js/product-editor/src/components/editor/init-blocks.ts b/packages/js/product-editor/src/components/editor/init-blocks.ts
index ea4d0128b1d..346fecf1720 100644
--- a/packages/js/product-editor/src/components/editor/init-blocks.ts
+++ b/packages/js/product-editor/src/components/editor/init-blocks.ts
@@ -22,6 +22,10 @@ import { init as initPricing } from '../pricing-block';
import { init as initCollapsible } from '../collapsible-block';
import { init as initScheduleSale } from '../../blocks/schedule-sale';
import { init as initTrackInventory } from '../../blocks/track-inventory';
+import { init as initSku } from '../../blocks/inventory-sku';
+import { init as initConditional } from '../../blocks/conditional';
+import { init as initLowStockQty } from '../../blocks/inventory-email';
+import { init as initCheckbox } from '../../blocks/checkbox';
export const initBlocks = () => {
const coreBlocks = __experimentalGetCoreBlocks();
@@ -42,4 +46,8 @@ export const initBlocks = () => {
initCollapsible();
initScheduleSale();
initTrackInventory();
+ initSku();
+ initConditional();
+ initLowStockQty();
+ initCheckbox();
};
diff --git a/packages/js/product-editor/src/components/radio/editor.scss b/packages/js/product-editor/src/components/radio/editor.scss
index 8b1729d3d81..185c801248e 100644
--- a/packages/js/product-editor/src/components/radio/editor.scss
+++ b/packages/js/product-editor/src/components/radio/editor.scss
@@ -13,7 +13,6 @@
font-size: 16px;
font-weight: 600;
color: #1e1e1e;
- margin-bottom: $gap-smaller;
}
&__description {
@@ -24,4 +23,4 @@
.components-base-control__field > .components-v-stack {
gap: $gap;
}
-}
\ No newline at end of file
+}
diff --git a/packages/js/product-editor/src/style.scss b/packages/js/product-editor/src/style.scss
index be7eb118202..825a8d367fe 100644
--- a/packages/js/product-editor/src/style.scss
+++ b/packages/js/product-editor/src/style.scss
@@ -9,9 +9,13 @@
@import 'components/images/editor.scss';
@import 'components/block-editor/style.scss';
@import 'components/radio/editor.scss';
+@import 'blocks/checkbox/editor.scss';
@import 'components/section/editor.scss';
@import 'components/tab/editor.scss';
@import 'components/tabs/style.scss';
@import 'components/details-summary-block/style.scss';
@import 'components/product-mvp-ces-footer/style.scss';
@import 'components/product-mvp-feedback-modal/style.scss';
+@import 'components/details-name-block/style.scss';
+@import 'blocks/inventory-sku/style.scss';
+@import 'blocks/inventory-email/style.scss';
diff --git a/packages/js/product-editor/src/utils/index.ts b/packages/js/product-editor/src/utils/index.ts
index 67a0014effc..31d0ce7f148 100644
--- a/packages/js/product-editor/src/utils/index.ts
+++ b/packages/js/product-editor/src/utils/index.ts
@@ -22,6 +22,7 @@ import { preventLeavingProductForm } from './prevent-leaving-product-form';
export * from './create-ordered-children';
export * from './sort-fills-by-order';
export * from './init-blocks';
+export * from './product-apifetch-middleware';
export {
AUTO_DRAFT_NAME,
diff --git a/packages/js/product-editor/src/utils/product-apifetch-middleware.ts b/packages/js/product-editor/src/utils/product-apifetch-middleware.ts
new file mode 100644
index 00000000000..b33c5d8ca71
--- /dev/null
+++ b/packages/js/product-editor/src/utils/product-apifetch-middleware.ts
@@ -0,0 +1,34 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { getQuery } from '@woocommerce/navigation';
+
+const isProductEditor = () => {
+ const query: { page?: string; path?: string } = getQuery();
+ return (
+ query?.page === 'wc-admin' &&
+ [ '/add-product', '/product/' ].some( ( path ) =>
+ query?.path?.startsWith( path )
+ )
+ );
+};
+
+export const productApiFetchMiddleware = () => {
+ // This is needed to ensure that we use the correct namespace for the entity data store
+ // without disturbing the rest_namespace outside of the product block editor.
+ apiFetch.use( ( options, next ) => {
+ const versionTwoRegex = new RegExp( '^/wp/v2/product' );
+ if (
+ options.path &&
+ versionTwoRegex.test( options?.path ) &&
+ isProductEditor()
+ ) {
+ options.path = options.path.replace(
+ versionTwoRegex,
+ '/wc/v3/products'
+ );
+ }
+ return next( options );
+ } );
+};
diff --git a/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts b/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts
index b0f03086810..823e371869d 100644
--- a/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts
+++ b/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts
@@ -4,7 +4,6 @@
import { AUTO_DRAFT_NAME } from '@woocommerce/product-editor';
import { Product } from '@woocommerce/data';
import { useDispatch, resolveSelect } from '@wordpress/data';
-
import { useEffect, useState } from '@wordpress/element';
export function useProductEntityRecord(
diff --git a/plugins/woocommerce-admin/client/products/product-page.tsx b/plugins/woocommerce-admin/client/products/product-page.tsx
index 6b414978f5a..2f67ab5fadb 100644
--- a/plugins/woocommerce-admin/client/products/product-page.tsx
+++ b/plugins/woocommerce-admin/client/products/product-page.tsx
@@ -4,6 +4,7 @@
import {
__experimentalEditor as Editor,
ProductEditorSettings,
+ productApiFetchMiddleware,
} from '@woocommerce/product-editor';
import { Spinner } from '@wordpress/components';
@@ -20,6 +21,8 @@ import './fills/product-block-editor-fills';
declare const productBlockEditorSettings: ProductEditorSettings;
+productApiFetchMiddleware();
+
export default function ProductPage() {
const { productId } = useParams();
diff --git a/plugins/woocommerce/.wp-env.json b/plugins/woocommerce/.wp-env.json
index be25f189c38..618c4a21fdf 100644
--- a/plugins/woocommerce/.wp-env.json
+++ b/plugins/woocommerce/.wp-env.json
@@ -1,19 +1,24 @@
{
- "phpVersion": "7.4",
- "plugins": [ "." ],
- "config": {
- "JETPACK_AUTOLOAD_DEV": true,
- "WP_DEBUG_LOG": true,
- "WP_DEBUG_DISPLAY": true,
- "ALTERNATE_WP_CRON": true
- },
- "env": {
- "development": {},
- "tests": {
- "port": 8086,
- "config": {
- "ALTERNATE_WP_CRON": false
- }
- }
- }
-}
+ "phpVersion": "7.4",
+ "plugins": [
+ "."
+ ],
+ "config": {
+ "JETPACK_AUTOLOAD_DEV": true,
+ "WP_DEBUG_LOG": true,
+ "WP_DEBUG_DISPLAY": true,
+ "ALTERNATE_WP_CRON": true
+ },
+ "mappings": {
+ "wp-cli.yml": ".\/tests\/wp-cli.yml"
+ },
+ "env": {
+ "development": [],
+ "tests": {
+ "port": 8086,
+ "config": {
+ "ALTERNATE_WP_CRON": false
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/add-e2e-tests-wp-version-support b/plugins/woocommerce/changelog/add-e2e-tests-wp-version-support
new file mode 100644
index 00000000000..0525f22750f
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-e2e-tests-wp-version-support
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Allows the WP, WC & PHP version to be specified in .wp-env.json for e2e and api tests
diff --git a/plugins/woocommerce/changelog/add-inventory-advanced-37401 b/plugins/woocommerce/changelog/add-inventory-advanced-37401
new file mode 100644
index 00000000000..6bbe1d6982d
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-inventory-advanced-37401
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Adding checkbox, conditional and inventory email blocks to product blocks editor.
diff --git a/plugins/woocommerce/changelog/add-inventory-sku-block-37399 b/plugins/woocommerce/changelog/add-inventory-sku-block-37399
new file mode 100644
index 00000000000..07efb4ad5cb
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-inventory-sku-block-37399
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Adding inventory section and sku blocks to product block editor.
diff --git a/plugins/woocommerce/changelog/e2e-dismiss-tour-product-variations b/plugins/woocommerce/changelog/e2e-dismiss-tour-product-variations
new file mode 100644
index 00000000000..ad3f2217be1
--- /dev/null
+++ b/plugins/woocommerce/changelog/e2e-dismiss-tour-product-variations
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Update to the merchant variable product e2e test
diff --git a/plugins/woocommerce/changelog/fix-37200 b/plugins/woocommerce/changelog/fix-37200
new file mode 100644
index 00000000000..ecce3e178be
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-37200
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add a category for product editor blocks
diff --git a/plugins/woocommerce/changelog/fix-37650-delete-meta-data b/plugins/woocommerce/changelog/fix-37650-delete-meta-data
new file mode 100644
index 00000000000..1a27725d278
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-37650-delete-meta-data
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add method delete_meta_data_value to WC_Data objects
diff --git a/plugins/woocommerce/changelog/fix-37657 b/plugins/woocommerce/changelog/fix-37657
new file mode 100644
index 00000000000..43d36063725
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-37657
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Lock the product block editor template root
diff --git a/plugins/woocommerce/changelog/fix-37660 b/plugins/woocommerce/changelog/fix-37660
new file mode 100644
index 00000000000..19479c9b9bf
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-37660
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Use first meta value for HPOS migration when there are duplicates for flat column.
diff --git a/plugins/woocommerce/changelog/fix-empty-attributes-tracking b/plugins/woocommerce/changelog/fix-empty-attributes-tracking
new file mode 100644
index 00000000000..ecbc77d5177
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-empty-attributes-tracking
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Exclude empty attributes from the attribute count when tracking product updates.
diff --git a/plugins/woocommerce/changelog/fix-order_key_migration b/plugins/woocommerce/changelog/fix-order_key_migration
new file mode 100644
index 00000000000..0dd82f9f565
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-order_key_migration
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Remove unique constraint from order_key, since orders can be created with empty order keys, which then conflict with the constraint.
diff --git a/plugins/woocommerce/changelog/fix-rest-namespace-blocks-37619 b/plugins/woocommerce/changelog/fix-rest-namespace-blocks-37619
new file mode 100644
index 00000000000..683a9b7bc9f
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-rest-namespace-blocks-37619
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Removing modification to rest_namespace on post type and replacing with middleware.
diff --git a/plugins/woocommerce/changelog/update-16525-check-php-version-for-extensions b/plugins/woocommerce/changelog/update-16525-check-php-version-for-extensions
new file mode 100644
index 00000000000..9cb44346db3
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-16525-check-php-version-for-extensions
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Support min_php_version and min_wp_version for the free extensions feed
diff --git a/plugins/woocommerce/changelog/update-37690-shipping-task-is-not-marked-as-completed b/plugins/woocommerce/changelog/update-37690-shipping-task-is-not-marked-as-completed
new file mode 100644
index 00000000000..2670a0d49a0
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-37690-shipping-task-is-not-marked-as-completed
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Delete shipping zone count transient on woocommerce_shipping_zone_method_added and woocommerce_after_shipping_zone_object_save
diff --git a/plugins/woocommerce/changelog/update-test-docs-php-8 b/plugins/woocommerce/changelog/update-test-docs-php-8
new file mode 100644
index 00000000000..b8c0c05f43d
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-test-docs-php-8
@@ -0,0 +1,5 @@
+Significance: patch
+Type: update
+Comment: No changelog entry needed, this is an update to developer-facing documentation only.
+
+
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-data.php b/plugins/woocommerce/includes/abstracts/abstract-wc-data.php
index 863db390320..1079c52f9c1 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-data.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-data.php
@@ -506,6 +506,26 @@ abstract class WC_Data {
}
}
+ /**
+ * Delete meta data with a matching value.
+ *
+ * @since 7.7.0
+ * @param string $key Meta key.
+ * @param mixed $value Meta value. Entries will only be removed that match the value.
+ */
+ public function delete_meta_data_value( $key, $value ) {
+ $this->maybe_read_meta_data();
+ $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'key' ), $key, true );
+
+ if ( $array_keys ) {
+ foreach ( $array_keys as $array_key ) {
+ if ( $value === $this->meta_data[ $array_key ]->value ) {
+ $this->meta_data[ $array_key ]->value = null;
+ }
+ }
+ }
+ }
+
/**
* Delete meta data.
*
diff --git a/plugins/woocommerce/includes/class-wc-post-types.php b/plugins/woocommerce/includes/class-wc-post-types.php
index e2c74e2afe7..5670bf77b90 100644
--- a/plugins/woocommerce/includes/class-wc-post-types.php
+++ b/plugins/woocommerce/includes/class-wc-post-types.php
@@ -365,7 +365,6 @@ class WC_Post_Types {
'has_archive' => $has_archive,
'show_in_nav_menus' => true,
'show_in_rest' => true,
- 'rest_namespace' => 'wp/v3',
'template' => array(
array(
'woocommerce/product-tab',
@@ -586,10 +585,97 @@ class WC_Post_Types {
),
array(
array(
- 'woocommerce/product-track-inventory-fields',
- array(),
+ 'woocommerce/product-section',
+ array(
+ 'title' => __( 'Inventory', 'woocommerce' ),
+ 'description' => sprintf(
+ /* translators: %1$s: Inventory settings link opening tag. %2$s: Inventory settings link closing tag.*/
+ __( 'Set up and manage inventory for this product, including status and available quantity. %1$sManage store inventory settings%2$s', 'woocommerce' ),
+ '',
+ ' '
+ ),
+ 'icon' => array(
+ 'src' => ' ',
+ ),
+ ),
+ array(
+ array(
+ 'woocommerce/product-sku',
+ ),
+ array(
+ 'woocommerce/product-track-inventory-fields',
+ ),
+ array(
+ 'woocommerce/collapsible',
+ array(
+ 'toggleText' => __( 'Advanced', 'woocommerce' ),
+ 'initialCollapsed' => true,
+ 'persistRender' => true,
+ ),
+ array(
+ array(
+ 'woocommerce/conditional',
+ array(
+ 'mustMatch' => array(
+ 'manage_stock' => array( true ),
+ ),
+ ),
+ array(
+ array(
+ 'woocommerce/product-radio',
+ array(
+ 'title' => __( 'When out of stock', 'woocommerce' ),
+ 'property' => 'backorders',
+ 'options' => array(
+ array(
+ 'label' => __( 'Allow purchases', 'woocommerce' ),
+ 'value' => 'yes',
+ ),
+ array(
+ 'label' => __(
+ 'Allow purchases, but notify customers',
+ 'woocommerce'
+ ),
+ 'value' => 'notify',
+ ),
+ array(
+ 'label' => __( "Don't allow purchases", 'woocommerce' ),
+ 'value' => 'no',
+ ),
+ ),
+ ),
+ ),
+ array(
+ 'woocommerce/product-inventory-email',
+ ),
+ ),
+ ),
+ array(
+ 'woocommerce/product-checkbox',
+ array(
+ 'title' => __(
+ 'Restrictions',
+ 'woocommerce'
+ ),
+ 'label' => __(
+ 'Limit purchases to 1 item per order',
+ 'woocommerce'
+ ),
+ 'property' => 'sold_individually',
+ 'tooltip' => __(
+ 'When checked, customers will be able to purchase only 1 item in a single order. This is particularly useful for items that have limited quantity, like art or handmade goods.',
+ 'woocommerce'
+ ),
+ ),
+ ),
+
+ ),
+ ),
+
+ ),
),
),
+
),
array(
'woocommerce/product-tab',
@@ -601,7 +687,7 @@ class WC_Post_Types {
array(
'woocommerce/product-section',
array(
- 'title' => __( 'Shipping section', 'woocommerce' ),
+ 'title' => __( 'Shipping', 'woocommerce' ),
'icon' => array(
'src' => ' ',
),
@@ -615,6 +701,7 @@ class WC_Post_Types {
),
),
),
+ 'template_lock' => 'all',
)
)
);
diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php
index 920f2ae1e8b..48d8fecffd6 100644
--- a/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php
+++ b/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php
@@ -162,8 +162,19 @@ class WC_Products_Tracking {
description_value = $( '.block-editor-rich-text__editable' ).text();
}
+ // We can't just check the number of '.woocommerce_attribute' elements because
+ // there might be empty ones, which get stripped out when saved. So, we'll check
+ // whether the name and values have been filled out.
+ var numberOfAttributes = $( '.woocommerce_attribute' ).filter( function () {
+ var attributeElement = $( this );
+ var attributeName = attributeElement.find( 'input.attribute_name' ).val();
+ var attributeValues = attributeElement.find( 'textarea[name^=\"attribute_values\"]' ).val();
+
+ return attributeName !== '' && attributeValues !== '';
+ } ).length;
+
var properties = {
- attributes: $( '.woocommerce_attribute' ).length,
+ attributes: numberOfAttributes,
categories: $( '[name=\"tax_input[product_cat][]\"]:checked' ).length,
cross_sells: $( '#crosssell_ids option' ).length ? 'Yes' : 'No',
description: description_value.trim() !== '' ? 'Yes' : 'No',
diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json
index 827d2042bf8..4c235643577 100644
--- a/plugins/woocommerce/package.json
+++ b/plugins/woocommerce/package.json
@@ -30,16 +30,17 @@
"docker:ssh": "pnpm exec wc-e2e docker:ssh",
"docker:up": "pnpm exec wc-e2e docker:up",
"env:dev": "pnpm wp-env start",
- "env:test": "pnpm run env:dev && ./tests/e2e-pw/bin/test-env-setup.sh",
+ "update-wp-env": "php ./tests/e2e-pw/bin/update-wp-env.php",
+ "env:test": "pnpm run update-wp-env && pnpm run env:dev && ./tests/e2e-pw/bin/test-env-setup.sh",
"test:e2e-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/e2e-pw/playwright.config.js",
"test:api-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js",
"env:start": "pnpm wp-env start && ./tests/e2e-pw/bin/test-env-setup.sh",
"env:restart": "pnpm wp-env destroy && pnpm wp-env start && ./tests/e2e-pw/bin/test-env-setup.sh",
"env:stop": "pnpm wp-env stop",
- "env:test:cot": "pnpm run env:dev && ENABLE_HPOS=1 ./tests/e2e-pw/bin/test-env-setup.sh",
+ "env:test:cot": "pnpm run update-wp-env && pnpm run env:dev && ENABLE_HPOS=1 ./tests/e2e-pw/bin/test-env-setup.sh",
"env:performance-init": "./tests/performance/bin/init-sample-products.sh",
"env:down": "pnpm wp-env stop",
- "env:destroy": "pnpm wp-env destroy",
+ "env:destroy": "pnpm run update-wp-env && pnpm wp-env destroy",
"test:api": "API_TEST_REPORT_DIR=\"$PWD/tests/api\" pnpm exec wc-api-tests test api",
"make:collection": "pnpm exec wc-api-tests make:collection",
"e2e:debug": "pnpm exec wc-e2e test:e2e-debug",
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php
index a6f465673e5..c985e22ac90 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php
@@ -26,6 +26,8 @@ class Shipping extends Task {
// when a new zone is added or an existing one has been changed.
add_action( 'wp_ajax_woocommerce_shipping_zones_save_changes', array( __CLASS__, 'delete_zone_count_transient' ), 9 );
add_action( 'wp_ajax_woocommerce_shipping_zone_methods_save_changes', array( __CLASS__, 'delete_zone_count_transient' ), 9 );
+ add_action( 'woocommerce_shipping_zone_method_added', array( __CLASS__, 'delete_zone_count_transient' ), 9 );
+ add_action( 'woocommerce_after_shipping_zone_object_save', array( __CLASS__, 'delete_zone_count_transient' ), 9 );
}
/**
diff --git a/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/BlockRegistry.php b/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/BlockRegistry.php
index 0271e4cf7ac..473a476982b 100644
--- a/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/BlockRegistry.php
+++ b/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/BlockRegistry.php
@@ -31,19 +31,45 @@ class BlockRegistry {
*
* @param string $path File path.
*/
- public function get_file_path( $path ) {
+ private function get_file_path( $path ) {
return WC_ABSPATH . WCAdminAssets::get_path( 'js' ) . trailingslashit( self::BLOCKS_DIR ) . $path;
}
+ /**
+ * Initialize all blocks.
+ */
+ public function init() {
+ add_filter( 'block_categories_all', array( $this, 'register_categories' ), 10, 2 );
+ $this->register_product_blocks();
+ }
+
/**
* Register all the product blocks.
*/
- public function register_product_blocks() {
+ private function register_product_blocks() {
foreach ( self::PRODUCT_BLOCKS as $block_name ) {
$this->register_block( $block_name );
}
}
+ /**
+ * Register product related block categories.
+ *
+ * @param array[] $block_categories Array of categories for block types.
+ * @param WP_Block_Editor_Context $editor_context The current block editor context.
+ */
+ public function register_categories( $block_categories, $editor_context ) {
+ if ( INIT::EDITOR_CONTEXT_NAME === $editor_context->name ) {
+ $block_categories[] = array(
+ 'slug' => 'woocommerce',
+ 'title' => __( 'WooCommerce', 'woocommerce' ),
+ 'icon' => null,
+ );
+ }
+
+ return $block_categories;
+ }
+
/**
* Get the block name without the "woocommerce/" prefix.
*
@@ -51,7 +77,7 @@ class BlockRegistry {
*
* @return string
*/
- public function remove_block_prefix( $block_name ) {
+ private function remove_block_prefix( $block_name ) {
if ( 0 === strpos( $block_name, 'woocommerce/' ) ) {
return substr_replace( $block_name, '', 0, strlen( 'woocommerce/' ) );
}
@@ -66,7 +92,7 @@ class BlockRegistry {
*
* @return WP_Block_Type|false The registered block type on success, or false on failure.
*/
- public function register_block( $block_name ) {
+ private function register_block( $block_name ) {
$block_name = $this->remove_block_prefix( $block_name );
$block_json_file = $this->get_file_path( $block_name . '/block.json' );
diff --git a/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php b/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php
index b907226e10f..eccdc352acb 100644
--- a/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php
+++ b/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php
@@ -23,6 +23,11 @@ class Init {
*/
const TOGGLE_OPTION_NAME = 'woocommerce_' . self::FEATURE_ID . '_enabled';
+ /**
+ * The context name used to identify the editor.
+ */
+ const EDITOR_CONTEXT_NAME = 'woocommerce/edit-product';
+
/**
* Constructor
*/
@@ -33,9 +38,8 @@ class Init {
}
if ( Features::is_enabled( self::FEATURE_ID ) ) {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
- add_filter( 'woocommerce_register_post_type_product', array( $this, 'add_rest_base_config' ) );
$block_registry = new BlockRegistry();
- $block_registry->register_product_blocks();
+ $block_registry->init();
}
}
@@ -47,7 +51,7 @@ class Init {
return;
}
$post_type_object = get_post_type_object( 'product' );
- $block_editor_context = new WP_Block_Editor_Context( array( 'name' => 'core/edit-post' ) );
+ $block_editor_context = new WP_Block_Editor_Context( array( 'name' => self::EDITOR_CONTEXT_NAME ) );
$editor_settings = array();
if ( ! empty( $post_type_object->template ) ) {
@@ -65,6 +69,11 @@ class Init {
'var productBlockEditorSettings = productBlockEditorSettings || ' . wp_json_encode( $editor_settings ) . ';',
'before'
);
+ wp_add_inline_script(
+ $script_handle,
+ sprintf( 'wp.blocks.setCategories( %s );', wp_json_encode( $editor_settings['blockCategories'] ) ),
+ 'before'
+ );
}
/**
@@ -106,15 +115,4 @@ class Init {
return $link;
}
- /**
- * Updates the product endpoint to use WooCommerce REST API.
- *
- * @param array $post_args Args for the product post type.
- * @return array
- */
- public function add_rest_base_config( $post_args ) {
- $post_args['rest_base'] = 'products';
- $post_args['rest_namespace'] = 'wc/v3';
- return $post_args;
- }
}
diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostMetaToOrderMetaMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostMetaToOrderMetaMigrator.php
index a10720d2b4f..85a8ea5555f 100644
--- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostMetaToOrderMetaMigrator.php
+++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostMetaToOrderMetaMigrator.php
@@ -52,6 +52,7 @@ class PostMetaToOrderMetaMigrator extends MetaToMetaTableMigrator {
'meta' => array(
'table_name' => $wpdb->postmeta,
'entity_id_column' => 'post_id',
+ 'meta_id_column' => 'meta_id',
'meta_key_column' => 'meta_key',
'meta_value_column' => 'meta_value',
),
diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderAddressTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderAddressTableMigrator.php
index a9aba4bf6dd..526bd0a7da0 100644
--- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderAddressTableMigrator.php
+++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderAddressTableMigrator.php
@@ -56,6 +56,7 @@ class PostToOrderAddressTableMigrator extends MetaToCustomTableMigrator {
),
'meta' => array(
'table_name' => $wpdb->postmeta,
+ 'meta_id_column' => 'meta_id',
'meta_key_column' => 'meta_key',
'meta_value_column' => 'meta_value',
'entity_id_column' => 'post_id',
diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderOpTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderOpTableMigrator.php
index 05f22f6d1d0..995540baaf8 100644
--- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderOpTableMigrator.php
+++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderOpTableMigrator.php
@@ -40,6 +40,7 @@ class PostToOrderOpTableMigrator extends MetaToCustomTableMigrator {
),
'meta' => array(
'table_name' => $wpdb->postmeta,
+ 'meta_id_column' => 'meta_id',
'meta_key_column' => 'meta_key',
'meta_value_column' => 'meta_value',
'entity_id_column' => 'post_id',
diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderTableMigrator.php
index e21085c62ba..8635eb36f7b 100644
--- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderTableMigrator.php
+++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderTableMigrator.php
@@ -39,6 +39,7 @@ class PostToOrderTableMigrator extends MetaToCustomTableMigrator {
),
'meta' => array(
'table_name' => $wpdb->postmeta,
+ 'meta_id_column' => 'meta_id',
'meta_key_column' => 'meta_key',
'meta_value_column' => 'meta_value',
'entity_id_column' => 'post_id',
diff --git a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php
index 71b0d3022a4..55c3e9d1973 100644
--- a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php
+++ b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php
@@ -543,7 +543,11 @@ WHERE
private function processs_and_sanitize_meta_data( array &$sanitized_entity_data, array &$error_records, array $meta_data ): void {
foreach ( $meta_data as $datum ) {
$column_schema = $this->meta_column_mapping[ $datum->meta_key ];
- $value = $this->validate_data( $datum->meta_value, $column_schema['type'] );
+ if ( isset( $sanitized_entity_data[ $datum->entity_id ][ $column_schema['destination'] ] ) ) {
+ // We pick only the first meta if there are duplicates for a flat column, to be consistent with WP core behavior in handing duplicate meta which are marked as unique.
+ continue;
+ }
+ $value = $this->validate_data( $datum->meta_value, $column_schema['type'] );
if ( is_wp_error( $value ) ) {
$error_records[ $datum->entity_id ][ $column_schema['destination'] ] = "{$value->get_error_code()}: {$value->get_error_message()}";
} else {
@@ -610,7 +614,7 @@ WHERE
$query = $this->build_verification_query( $source_ids );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $query should already be prepared.
$results = $wpdb->get_results( $query, ARRAY_A );
-
+ $results = $this->fill_source_metadata( $results, $source_ids );
return $this->verify_data( $results );
}
@@ -623,19 +627,13 @@ WHERE
*/
protected function build_verification_query( $source_ids ) {
$source_table = $this->schema_config['source']['entity']['table_name'];
- $meta_table = $this->schema_config['source']['meta']['table_name'];
$destination_table = $this->schema_config['destination']['table_name'];
- $meta_entity_id_column = $this->schema_config['source']['meta']['entity_id_column'];
- $meta_key_column = $this->schema_config['source']['meta']['meta_key_column'];
- $meta_value_column = $this->schema_config['source']['meta']['meta_value_column'];
$destination_source_rel_column = $this->schema_config['destination']['source_rel_column'];
$source_destination_rel_column = $this->schema_config['source']['entity']['destination_rel_column'];
- $source_meta_rel_column = $this->schema_config['source']['entity']['meta_rel_column'];
$source_destination_join_clause = "$destination_table ON $destination_table.$destination_source_rel_column = $source_table.$source_destination_rel_column";
$meta_select_clauses = array();
- $meta_join_clauses = array();
$source_select_clauses = array();
$destination_select_clauses = array();
@@ -646,31 +644,85 @@ WHERE
}
foreach ( $this->meta_column_mapping as $meta_key => $schema ) {
- $meta_table_alias = "meta_source_{$schema['destination']}";
- $meta_select_clauses[] = "$meta_table_alias.$meta_value_column AS $meta_table_alias";
- $meta_join_clauses[] = "
-$meta_table $meta_table_alias ON
- $meta_table_alias.$meta_entity_id_column = $source_table.$source_meta_rel_column AND
- $meta_table_alias.$meta_key_column = '$meta_key'
-";
$destination_select_clauses[] = "$destination_table.{$schema['destination']} as {$destination_table}_{$schema['destination']}";
}
$select_clause = implode( ', ', array_merge( $source_select_clauses, $meta_select_clauses, $destination_select_clauses ) );
- $meta_join_clause = implode( ' LEFT JOIN ', $meta_join_clauses );
-
$where_clause = $this->get_where_clause_for_verification( $source_ids );
return "
SELECT $select_clause
FROM $source_table
LEFT JOIN $source_destination_join_clause
- LEFT JOIN $meta_join_clause
WHERE $where_clause
";
}
+ /**
+ * Fill source metadata for given IDs for verification. This will return filled data in following format:
+ * [
+ * {
+ * $source_table_$source_column: $value,
+ * ...,
+ * $destination_table_$destination_column: $value,
+ * ...
+ * meta_source_{$destination_column_name1}: $meta_value,
+ * ...
+ * },
+ * ...
+ * ]
+ *
+ * @param array $results Entity data from both source and destination table.
+ * @param array $source_ids List of source IDs.
+ *
+ * @return array Filled $results param with source metadata.
+ */
+ private function fill_source_metadata( $results, $source_ids ) {
+ global $wpdb;
+ $meta_table = $this->schema_config['source']['meta']['table_name'];
+ $meta_entity_id_column = $this->schema_config['source']['meta']['entity_id_column'];
+ $meta_key_column = $this->schema_config['source']['meta']['meta_key_column'];
+ $meta_value_column = $this->schema_config['source']['meta']['meta_value_column'];
+ $meta_id_column = $this->schema_config['source']['meta']['meta_id_column'];
+ $meta_columns = array_keys( $this->meta_column_mapping );
+
+ $meta_columns_placeholder = implode( ', ', array_fill( 0, count( $meta_columns ), '%s' ) );
+ $source_ids_placeholder = implode( ', ', array_fill( 0, count( $source_ids ), '%d' ) );
+
+ $query = $wpdb->prepare(
+ // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+ "SELECT $meta_entity_id_column as entity_id, $meta_key_column as meta_key, $meta_value_column as meta_value
+ FROM $meta_table
+ WHERE $meta_entity_id_column IN ($source_ids_placeholder)
+ AND $meta_key_column IN ($meta_columns_placeholder)
+ ORDER BY $meta_id_column ASC",
+ array_merge( $source_ids, $meta_columns )
+ );
+ //phpcs:enable
+
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $meta_data = $wpdb->get_results( $query, ARRAY_A );
+ $source_metadata_rows = array();
+ foreach ( $meta_data as $meta_datum ) {
+ if ( ! isset( $source_metadata_rows[ $meta_datum['entity_id'] ] ) ) {
+ $source_metadata_rows[ $meta_datum['entity_id'] ] = array();
+ }
+ $destination_column = $this->meta_column_mapping[ $meta_datum['meta_key'] ]['destination'];
+ $alias = "meta_source_{$destination_column}";
+ if ( isset( $source_metadata_rows[ $meta_datum['entity_id'] ][ $alias ] ) ) {
+ // Only process first value, duplicate values mapping to flat columns are ignored to be consistent with WP core.
+ continue;
+ }
+ $source_metadata_rows[ $meta_datum['entity_id'] ][ $alias ] = $meta_datum['meta_value'];
+ }
+ foreach ( $results as $index => $result_row ) {
+ $source_id = $result_row[ $this->schema_config['source']['entity']['table_name'] . '_' . $this->schema_config['source']['entity']['primary_key'] ];
+ $results[ $index ] = array_merge( $result_row, ( $source_metadata_rows[ $source_id ] ?? array() ) );
+ }
+ return $results;
+ }
+
/**
* Helper function to generate where clause for fetching data for verification.
*
@@ -777,6 +829,9 @@ WHERE $where_clause
* @return array Processed row.
*/
private function pre_process_row( $row, $schema, $alias, $destination_alias ) {
+ if ( ! isset( $row[ $alias ] ) ) {
+ $row[ $alias ] = $this->get_type_defaults( $schema['type'] );
+ }
if ( in_array( $schema['type'], array( 'int', 'decimal' ), true ) ) {
if ( '' === $row[ $alias ] || null === $row[ $alias ] ) {
$row[ $alias ] = 0; // $wpdb->prepare forces empty values to 0.
@@ -798,10 +853,6 @@ WHERE $where_clause
$row[ $destination_alias ] = null;
}
}
- if ( is_null( $row[ $alias ] ) ) {
- $row[ $alias ] = $this->get_type_defaults( $schema['type'] );
- }
-
return $row;
}
diff --git a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php
index 99055062238..e893afd283e 100644
--- a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php
+++ b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php
@@ -79,17 +79,18 @@ class DefaultFreeExtensions {
public static function get_plugin( $slug ) {
$plugins = array(
'google-listings-and-ads' => [
- 'name' => __( 'Google Listings & Ads', 'woocommerce' ),
- 'description' => sprintf(
+ 'min_php_version' => '7.4',
+ 'name' => __( 'Google Listings & Ads', 'woocommerce' ),
+ 'description' => sprintf(
/* translators: 1: opening product link tag. 2: closing link tag */
__( 'Drive sales with %1$sGoogle Listings and Ads%2$s', 'woocommerce' ),
'',
' '
),
- 'image_url' => plugins_url( '/assets/images/onboarding/google.svg', WC_PLUGIN_FILE ),
- 'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart',
- 'is_built_by_wc' => true,
- 'is_visible' => [
+ 'image_url' => plugins_url( '/assets/images/onboarding/google.svg', WC_PLUGIN_FILE ),
+ 'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart',
+ 'is_built_by_wc' => true,
+ 'is_visible' => [
[
'type' => 'not',
'operand' => [
@@ -125,11 +126,12 @@ class DefaultFreeExtensions {
'is_built_by_wc' => false,
],
'pinterest-for-woocommerce' => [
- 'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ),
- 'description' => __( 'Get your products in front of Pinners searching for ideas and things to buy.', 'woocommerce' ),
- 'image_url' => plugins_url( '/assets/images/onboarding/pinterest.png', WC_PLUGIN_FILE ),
- 'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding',
- 'is_built_by_wc' => true,
+ 'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ),
+ 'description' => __( 'Get your products in front of Pinners searching for ideas and things to buy.', 'woocommerce' ),
+ 'image_url' => plugins_url( '/assets/images/onboarding/pinterest.png', WC_PLUGIN_FILE ),
+ 'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding',
+ 'is_built_by_wc' => true,
+ 'min_php_version' => '7.3',
],
'pinterest-for-woocommerce:alt' => [
'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ),
@@ -349,6 +351,7 @@ class DefaultFreeExtensions {
DefaultPaymentGateways::get_rules_for_cbd( false ),
],
'is_built_by_wc' => true,
+ 'min_wp_version' => '5.9',
],
'woocommerce-services:shipping' => [
'description' => sprintf(
@@ -516,6 +519,7 @@ class DefaultFreeExtensions {
],
],
'is_built_by_wc' => false,
+ 'min_wp_version' => '6.0',
],
'mailpoet' => [
'name' => __( 'MailPoet', 'woocommerce' ),
diff --git a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/EvaluateExtension.php b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/EvaluateExtension.php
index 100dd94a125..2589a7d2847 100644
--- a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/EvaluateExtension.php
+++ b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/EvaluateExtension.php
@@ -21,6 +21,7 @@ class EvaluateExtension {
* @return object The evaluated extension.
*/
public static function evaluate( $extension ) {
+ global $wp_version;
$rule_evaluator = new RuleEvaluator();
if ( isset( $extension->is_visible ) ) {
@@ -30,6 +31,17 @@ class EvaluateExtension {
$extension->is_visible = true;
}
+ // Run PHP and WP version chcecks.
+ if ( true === $extension->is_visible ) {
+ if ( isset( $extension->min_php_version ) && ! version_compare( PHP_VERSION, $extension->min_php_version, '>=' ) ) {
+ $extension->is_visible = false;
+ }
+
+ if ( isset( $extension->min_wp_version ) && ! version_compare( $wp_version, $extension->min_wp_version, '>=' ) ) {
+ $extension->is_visible = false;
+ }
+ }
+
$installed_plugins = PluginsHelper::get_installed_plugin_slugs();
$activated_plugins = PluginsHelper::get_active_plugin_slugs();
$extension->is_installed = in_array( explode( ':', $extension->key )[0], $installed_plugins, true );
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
index 4050de796f1..f9d15c1e53a 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
@@ -847,7 +847,7 @@ WHERE
$wpdb->prepare(
"SELECT {$orders_table}.id FROM {$orders_table}
INNER JOIN {$op_table} ON {$op_table}.order_id = {$orders_table}.id
- WHERE {$op_table}.order_key = %s",
+ WHERE {$op_table}.order_key = %s AND {$op_table}.order_key != ''",
$order_key
)
);
@@ -2412,7 +2412,7 @@ CREATE TABLE $operational_data_table_name (
discount_total_amount decimal(26, 8) NULL,
recorded_sales tinyint(1) NULL,
UNIQUE KEY order_id (order_id),
- UNIQUE KEY order_key (order_key)
+ KEY order_key (order_key)
) $collate;
CREATE TABLE $meta_table (
id bigint(20) unsigned auto_increment primary key,
diff --git a/plugins/woocommerce/tests/README.md b/plugins/woocommerce/tests/README.md
index 6e662606437..7d931f510e9 100644
--- a/plugins/woocommerce/tests/README.md
+++ b/plugins/woocommerce/tests/README.md
@@ -97,32 +97,6 @@ $ tests/bin/install.sh woocommerce_tests_1 root root
Note that `woocommerce_tests` changed to `woocommerce_tests_1` as the `woocommerce_tests` database already exists due to the prior command.
-### Running tests in PHP 8
-
-WooCommerce currently supports PHP versions from 7.0 up to 8.0, and this poses an issue with PHPUnit:
-
-* The latest PHPUnit version that supports PHP 7.0 is 6.5.14
-* The latest PHPUnit version that WordPress (and thus WooCommerce) supports is 7.5.20, but that version doesn't work on PHP 8
-
-To workaround this, the testing strategy used by WooCommerce is as follows:
-
-* We normally use PHPUnit 6.5.14
-* For PHP 8 we use [a custom fork of PHPUnit 7.5.20 with support for PHP 8](https://github.com/woocommerce/phpunit/pull/1). WooCommerce's GitHub Actions CI workflow is configured to use this fork instead of the old version 6 when running in PHP 8.
-
-If you want to run the tests locally under PHP 8 you'll need to temporarily modify `composer.json` to use the custom PHPUnit fork in the same way that the GitHub Actions CI workflow file does. These are the commands that you'll need (run them after a regular `composer install` from within the `plugins/woocommerce` directory):
-
-```shell
-curl -L https://github.com/woocommerce/phpunit/archive/add-compatibility-with-php8-to-phpunit-7.zip -o /tmp/phpunit-7.5-fork.zip
-unzip -d /tmp/phpunit-7.5-fork /tmp/phpunit-7.5-fork.zip
-composer bin phpunit config --unset platform
-composer bin phpunit config repositories.0 '{"type": "path", "url": "/tmp/phpunit-7.5-fork/phpunit-add-compatibility-with-php8-to-phpunit-7", "options": {"symlink": false}}'
-composer bin phpunit require --dev -W phpunit/phpunit:@dev --ignore-platform-reqs
-rm -rf ./vendor/phpunit/
-composer dump-autoload
-```
-
-Just remember that you can't include the modified `composer.json` in any commit!
-
## Writing Tests
There are three different unit test directories:
diff --git a/plugins/woocommerce/tests/e2e-pw/README.md b/plugins/woocommerce/tests/e2e-pw/README.md
index 4cb9d6fb2a8..5df175f010b 100644
--- a/plugins/woocommerce/tests/e2e-pw/README.md
+++ b/plugins/woocommerce/tests/e2e-pw/README.md
@@ -57,13 +57,30 @@ To see all options, run `cd plugins/woocommerce && pnpm playwright test --help`
The default values are:
-- Latest stable WordPress version
+- Latest stable WordPress version
- PHP 7.4
- MariaDB
- URL: `http://localhost:8086/`
- Admin credentials: `admin/password`
-If you want to customize these, check the [Test Variables](#test-variables) section.
+If you want to customize the port or admin credentials, check the [Test Variables](#test-variables) section.
+
+If you would like to customize the `PHP`, `WordPress` or `WooCommerce` versions installed in the environment, you can define `UPDATE_WP_JSON_FILE=1` along with any or all of the following env vars when building the environment.
+- `WP_VERSION`
+ - Acceptable versions are `nightly`, `trunk`, and any version listed on [WordPress Releases] page.
+- `WC_VERSION`
+ - Acceptable versions can be found on the [WooCommerce Releases](https://github.com/woocommerce/woocommerce/releases) page
+- `PHP`
+ - Any PHP version you see it. Please note that WooCommerce requries a minimum of PHP 7.2.
+
+**Example**
+
+The command below will create and environment with WordPress version 6.2, WooCommerce version 7.5.1 and PHP version 8.2 installed.
+
+`UPDATE_WP_JSON_FILE=1 WP_VERSION=6.2 WC_TEST_VERSION=7.5.1 PHP_VERSION=8.2 pnpm run env:test`
+
+If you'd like to run with the default configuation, simply remove the `UPDATE_WP_JSON_FILE`.
+
For more information how to configure the test environment for `wp-env`, please checkout the [documentation](https://github.com/WordPress/gutenberg/tree/trunk/packages/env) documentation.
diff --git a/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh b/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh
index 9f3d7601740..623f4caa128 100755
--- a/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh
+++ b/plugins/woocommerce/tests/e2e-pw/bin/test-env-setup.sh
@@ -4,39 +4,39 @@ ENABLE_HPOS="${ENABLE_HPOS:-0}"
ENABLE_NEW_PRODUCT_EDITOR="${ENABLE_NEW_PRODUCT_EDITOR:-0}"
ENABLE_TRACKING="${ENABLE_TRACKING:-0}"
-wp-env run tests-cli "wp theme install twentynineteen --activate"
+echo -e 'Normalize permissions for wp-content directory \n'
+docker-compose -f $(wp-env install-path)/docker-compose.yml run --rm -u www-data -e HOME=/tmp tests-wordpress sh -c "chmod -c ugo+w /var/www/html/wp-config.php \
+&& chmod -c ugo+w /var/www/html/wp-content \
+&& chmod -c ugo+w /var/www/html/wp-content/themes \
+&& chmod -c ugo+w /var/www/html/wp-content/plugins"
-wp-env run tests-cli "wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate"
-
-wp-env run tests-cli "wp plugin install wp-mail-logging --activate"
-
-wp-env run tests-cli "wp plugin install https://github.com/woocommerce/woocommerce-reset/archive/refs/heads/trunk.zip --activate"
-
-wp-env run tests-cli "wp rewrite structure /%postname%/"
-
-wp-env run tests-cli "wp user create customer customer@woocommercecoree2etestsuite.com \
+docker-compose -f $(wp-env install-path)/docker-compose.yml run --rm -u www-data -e HOME=/tmp tests-cli sh -c "ls \
+&& wp theme install twentynineteen --activate \
+&& wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate \
+&& wp plugin install wp-mail-logging --activate \
+&& wp plugin install https://github.com/woocommerce/woocommerce-reset/archive/refs/heads/trunk.zip --activate \
+&& wp rewrite structure '/%postname%/' --hard \
+&& wp user create customer customer@woocommercecoree2etestsuite.com \
--user_pass=password \
--role=subscriber \
--first_name='Jane' \
--last_name='Smith' \
- --path=/var/www/html \
- --user_registered='2022-01-01 12:23:45'
-"
+ --user_registered='2022-01-01 12:23:45'"
echo -e 'Update Blog Name \n'
-wp-env run tests-cli 'wp option update blogname "WooCommerce Core E2E Test Suite"'
+docker-compose -f $(wp-env install-path)/docker-compose.yml run --rm -u $(id -u) -e HOME=/tmp tests-cli sh -c 'wp option update blogname "WooCommerce Core E2E Test Suite"'
if [ $ENABLE_HPOS == 1 ]; then
echo 'Enable the COT feature'
- wp-env run tests-cli "wp plugin install https://gist.github.com/vedanshujain/564afec8f5e9235a1257994ed39b1449/archive/b031465052fc3e04b17624acbeeb2569ef4d5301.zip --activate"
+ docker-compose -f $(wp-env install-path)/docker-compose.yml run --rm -u www-data -e HOME=/tmp tests-cli sh -c "wp plugin install https://gist.github.com/vedanshujain/564afec8f5e9235a1257994ed39b1449/archive/b031465052fc3e04b17624acbeeb2569ef4d5301.zip --activate"
fi
if [ $ENABLE_NEW_PRODUCT_EDITOR == 1 ]; then
echo 'Enable the new product editor feature'
- wp-env run tests-cli "wp plugin install https://github.com/woocommerce/woocommerce-experimental-enable-new-product-editor/releases/download/0.1.0/woocommerce-experimental-enable-new-product-editor.zip --activate"
+ docker-compose -f $(wp-env install-path)/docker-compose.yml run --rm -u www-data -e HOME=/tmp tests-cli sh -c "wp plugin install https://github.com/woocommerce/woocommerce-experimental-enable-new-product-editor/releases/download/0.1.0/woocommerce-experimental-enable-new-product-editor.zip --activate"
fi
if [ $ENABLE_TRACKING == 1 ]; then
echo 'Enable tracking'
- wp-env run tests-cli "wp option update woocommerce_allow_tracking 'yes'"
+ docker-compose -f $(wp-env install-path)/docker-compose.yml run --rm -u $(id -u) -e HOME=/tmp tests-cli sh -c "wp option update woocommerce_allow_tracking 'yes'"
fi
diff --git a/plugins/woocommerce/tests/e2e-pw/bin/update-wp-env.php b/plugins/woocommerce/tests/e2e-pw/bin/update-wp-env.php
new file mode 100644
index 00000000000..07e5be7c0db
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/bin/update-wp-env.php
@@ -0,0 +1,99 @@
+wp_env_path ) ) {
+ $this->wp_json = json_decode( file_get_contents( $this->wp_env_path ), true );
+ } else {
+ throw new Exception( ".wp_env.json doesn't exist!" );
+ }
+
+ $env = getenv();
+
+ $this->wp_version = isset( $env['WP_VERSION'] ) ? $env['WP_VERSION'] : null;
+ $this->wc_version = isset( $env['WC_TEST_VERSION'] ) ? $env['WC_TEST_VERSION'] : null;
+ $this->php_version = isset( $env['PHP_VERSION'] ) ? $env['PHP_VERSION'] : null;
+ }
+
+ public function set_wp_version(){
+ if ( $this->wp_version ) {
+
+ $version = "WordPress/WordPress#tags/$this->wp_version";
+
+ if ( 'trunk' === $this->wp_version ) {
+ $version = "WordPress/WordPress";
+ }
+
+ if ( 'nightly' === $this->wp_version ) {
+ $version = "https://wordpress.org/nightly-builds/wordpress-latest.zip";
+ }
+
+ echo "Set WP Version to $version \n";
+ $this->wp_json["core"] = $version;
+ }
+ }
+
+ public function revert_wp_version(){
+ unset( $this->wp_json["core"] );
+ }
+
+ public function set_wc_version(){
+ if ( $this->wc_version ) {
+ echo "Set WC Version to $this->wc_version \n";
+ $this->wp_json["plugins"] = [ "https://github.com/woocommerce/woocommerce/releases/download/$this->wc_version/woocommerce.zip" ];
+ }
+ }
+
+ public function revert_wc_version(){
+ $this->wp_json["plugins"] = [ "." ];
+ }
+
+ public function set_php_version(){
+ if ( $this->php_version ) {
+ echo "Set PHP Version to $this->php_version \n";
+ $this->wp_json["phpVersion"] = $this->php_version;
+ }
+ }
+
+ public function revert_php_version(){
+ $this->wp_json["phpVersion"] = "7.4";
+ }
+
+ public function update(){
+ $this->set_wp_version();
+ $this->set_wc_version();
+ $this->set_php_version();
+ file_put_contents( $this->wp_env_path, json_encode( $this->wp_json, JSON_PRETTY_PRINT ) );
+ }
+
+ public function revert(){
+ $this->revert_wp_version();
+ $this->revert_wc_version();
+ $this->revert_php_version();
+ file_put_contents( $this->wp_env_path, json_encode( $this->wp_json, JSON_PRETTY_PRINT ) );
+
+ echo "Reverted .wp-env.json \n";
+ }
+ }
+}
+
+
+$env = getenv();
+$update_wp_json = isset( $env['UPDATE_WP_JSON_FILE'] ) ? $env['UPDATE_WP_JSON_FILE'] : null;
+
+if ( is_null( $update_wp_json ) || $update_wp_json == false ) {
+ $wp_json = new UPDATE_WP_JSON();
+ $wp_json->revert();
+} else {
+ $wp_json = new UPDATE_WP_JSON();
+ $wp_json->update();
+}
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-variable-product.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-variable-product.spec.js
index 446d3e64c87..ad4842cfb60 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-variable-product.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-variable-product.spec.js
@@ -75,36 +75,6 @@ test.describe( 'Add New Variable Product Page', () => {
await resetVariableProductTour( baseURL, browser );
} );
- test( 'shows the variable product tour', async ( { page } ) => {
- await page.goto( 'wp-admin/post-new.php?post_type=product' );
- await page.selectOption( '#product-type', 'variable' );
-
- // because of the way that the tour is dynamically positioned,
- // Playwright can't automatically scroll the button into view,
- // so we will manually scroll the attributes tab into view,
- // which will cause the tour to be scrolled into view as well
- await page
- .locator( '.attribute_tab' )
- .getByRole( 'link', { name: 'Attributes' } )
- .scrollIntoViewIfNeeded();
-
- // the tour only seems to display when not running headless, so just make sure
- if ( await page.locator( '.components-card-header' ).nth(1).isVisible() ) {
- // dismiss the variable product tour
- await page
- .getByRole( 'button', { name: 'Close Tour' } )
- .click();
-
- // wait for the tour's dismissal to be saved
- await page.waitForResponse(
- ( response ) =>
- response.url().includes( '/users/' ) &&
- response.status() === 200
- );
-
- }
- } );
-
test( 'can create product, attributes and variations, edit variations and delete variations', async ( {
page,
} ) => {
@@ -112,6 +82,26 @@ test.describe( 'Add New Variable Product Page', () => {
await page.fill( '#title', variableProductName );
await page.selectOption( '#product-type', 'variable' );
+ await page
+ .locator( '.attribute_tab' )
+ .getByRole( 'link', { name: 'Attributes' } )
+ .scrollIntoViewIfNeeded();
+
+ // the tour only seems to display when not running headless, so just make sure
+ if ( await page.locator( '.woocommerce-tour-kit-step__heading' ).isVisible() ) {
+ // dismiss the variable product tour
+ await page
+ .getByRole( 'button', { name: 'Close Tour' } )
+ .click();
+
+ // wait for the tour's dismissal to be saved
+ await page.waitForResponse(
+ ( response ) =>
+ response.url().includes( '/users/' ) &&
+ response.status() === 200
+ );
+ }
+
await page.click( 'a[href="#product_attributes"]' );
// add 3 attributes
@@ -251,7 +241,28 @@ test.describe( 'Add New Variable Product Page', () => {
} ) => {
await page.goto( productPageURL );
await page.fill( '#title', manualVariableProduct );
- await page.selectOption( '#product-type', 'variable', { force: true } );
+ await page.selectOption( '#product-type', 'variable' );
+
+ await page
+ .locator( '.attribute_tab' )
+ .getByRole( 'link', { name: 'Attributes' } )
+ .scrollIntoViewIfNeeded();
+
+ // the tour only seems to display when not running headless, so just make sure
+ if ( await page.locator( '.woocommerce-tour-kit-step__heading' ).isVisible() ) {
+ // dismiss the variable product tour
+ await page
+ .getByRole( 'button', { name: 'Close Tour' } )
+ .click();
+
+ // wait for the tour's dismissal to be saved
+ await page.waitForResponse(
+ ( response ) =>
+ response.url().includes( '/users/' ) &&
+ response.status() === 200
+ );
+ }
+
await page.click( 'a[href="#product_attributes"]' );
// add 3 attributes
for ( let i = 0; i < 3; i++ ) {
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/crud/data.php b/plugins/woocommerce/tests/legacy/unit-tests/crud/data.php
index e4a4c4ce33b..76a8e85c921 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/crud/data.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/crud/data.php
@@ -325,6 +325,30 @@ class WC_Tests_CRUD_Data extends WC_Unit_Test_Case {
$this->assertEmpty( $object->get_meta( 'test_meta_key' ) );
}
+ /**
+ * Test deleting meta selectively.
+ */
+ public function test_delete_meta_data_value() {
+ $object = $this->create_test_post();
+ $object_id = $object->get_id();
+ add_metadata( 'post', $object_id, 'test_meta_key', 'val1' );
+ add_metadata( 'post', $object_id, 'test_meta_key', 'val2' );
+ add_metadata( 'post', $object_id, 'test_meta_key', array( 'foo', 'bar' ) );
+ $object = new WC_Mock_WC_Data( $object_id );
+
+ $this->assertCount( 3, $object->get_meta( 'test_meta_key', false ) );
+
+ $object->delete_meta_data_value( 'test_meta_key', 'val1' );
+ $this->assertCount( 2, $object->get_meta( 'test_meta_key', false ) );
+
+ $object->delete_meta_data_value( 'test_meta_key', array( 'bar', 'baz' ) );
+ $this->assertCount( 2, $object->get_meta( 'test_meta_key', false ) );
+
+ $object->delete_meta_data_value( 'test_meta_key', array( 'foo', 'bar' ) );
+ $this->assertCount( 1, $object->get_meta( 'test_meta_key', false ) );
+
+ $this->assertEquals( 'val2', $object->get_meta( 'test_meta_key' ) );
+ }
/**
* Test saving metadata (Actually making sure changes are written to DB).
diff --git a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php
index 1670e5444c3..072125136fb 100644
--- a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php
@@ -746,4 +746,48 @@ WHERE order_id = {$order_id} AND meta_key = 'non_unique_key_1' AND meta_value in
$this->assertEmpty( $errors );
}
+
+ /**
+ * @testDox When there are mutli meta values for a supposed unique meta key, the first one is picked.
+ */
+ public function test_first_value_is_picked_when_multi_value() {
+ global $wpdb;
+ $order = wc_get_order( OrderHelper::create_complex_wp_post_order() );
+ $original_order_key = $order->get_order_key();
+
+ $this->assertNotEmpty( $original_order_key );
+
+ // Add a second order key.
+ add_post_meta( $order->get_id(), '_order_key', 'second_order_key_should_be_ignored' );
+
+ $this->sut->migrate_order( $order->get_id() );
+
+ $migrated_order_key = $wpdb->get_var(
+ $wpdb->prepare(
+ "SELECT order_key FROM {$wpdb->prefix}wc_order_operational_data WHERE order_id = %d",
+ $order->get_id()
+ )
+ );
+
+ $this->assertEquals( $original_order_key, $migrated_order_key );
+
+ $errors = $this->sut->verify_migrated_orders( array( $order->get_id() ) );
+ $this->assertEmpty( $errors );
+ }
+
+ /**
+ * @testDox Test migration for multiple null order_key meta value.
+ */
+ public function test_order_key_null_multiple() {
+ $order1 = OrderHelper::create_order();
+ $order2 = OrderHelper::create_order();
+ delete_post_meta( $order1->get_id(), '_order_key' );
+ delete_post_meta( $order2->get_id(), '_order_key' );
+
+ $this->sut->migrate_order( $order1->get_id() );
+ $this->sut->migrate_order( $order2->get_id() );
+
+ $errors = $this->sut->verify_migrated_orders( array( $order1->get_id(), $order2->get_id() ) );
+ $this->assertEmpty( $errors );
+ }
}
diff --git a/plugins/woocommerce/tests/wp-cli.yml b/plugins/woocommerce/tests/wp-cli.yml
new file mode 100644
index 00000000000..3cf7565b3bc
--- /dev/null
+++ b/plugins/woocommerce/tests/wp-cli.yml
@@ -0,0 +1,2 @@
+apache_modules:
+ - mod_rewrite
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 94584ec418c..9d24fa04cd5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1441,6 +1441,7 @@ importers:
'@woocommerce/number': workspace:*
'@woocommerce/settings': ^1.0.0
'@woocommerce/tracks': workspace:^1.3.0
+ '@wordpress/api-fetch': wp-6.0
'@wordpress/block-editor': ^9.8.0
'@wordpress/blocks': ^12.3.0
'@wordpress/browserslist-config': wp-6.0
@@ -1496,6 +1497,7 @@ importers:
'@woocommerce/number': link:../number
'@woocommerce/settings': 1.0.0
'@woocommerce/tracks': link:../tracks
+ '@wordpress/api-fetch': 6.3.1
'@wordpress/block-editor': 9.8.0_mtk4wljkd5jimhszw4p7nnxuzm
'@wordpress/blocks': 12.5.0_react@17.0.2
'@wordpress/components': 19.8.5_eqi5qhcxfphl6j3pngzexvnehi
@@ -9078,7 +9080,7 @@ packages:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.17.8
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.20.2
'@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.17.8
'@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.17.8
'@babel/types': 7.21.3
@@ -9091,7 +9093,7 @@ packages:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.21.3
- '@babel/helper-plugin-utils': 7.19.0
+ '@babel/helper-plugin-utils': 7.20.2
'@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.21.3
'@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.21.3
'@babel/types': 7.21.3
@@ -17215,7 +17217,7 @@ packages:
'@wordpress/style-engine': 0.15.0
'@wordpress/token-list': 2.19.0
'@wordpress/url': 3.29.0
- '@wordpress/warning': 2.28.0
+ '@wordpress/warning': 2.19.0
'@wordpress/wordcount': 3.19.0
change-case: 4.1.2
classnames: 2.3.1
@@ -20624,8 +20626,8 @@ packages:
peerDependencies:
postcss: ^8.1.0
dependencies:
- browserslist: 4.21.4
- caniuse-lite: 1.0.30001418
+ browserslist: 4.20.2
+ caniuse-lite: 1.0.30001352
fraction.js: 4.2.0
normalize-range: 0.1.2
picocolors: 1.0.0
@@ -22182,7 +22184,6 @@ packages:
escalade: 3.1.1
node-releases: 2.0.6
picocolors: 1.0.0
- dev: true
/browserslist/4.20.4:
resolution: {integrity: sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==}
@@ -38056,7 +38057,7 @@ packages:
is-touch-device: 1.0.1
lodash: 4.17.21
moment: 2.29.4
- object.assign: 4.1.2
+ object.assign: 4.1.4
object.values: 1.1.5
prop-types: 15.8.1
raf: 3.4.1
diff --git a/tools/monorepo-utils/bin/run b/tools/monorepo-utils/bin/run
index 4c9974b2f94..f6931219020 100755
--- a/tools/monorepo-utils/bin/run
+++ b/tools/monorepo-utils/bin/run
@@ -1,5 +1,25 @@
#!/usr/bin/env node
+const { existsSync } = require( 'fs' );
+const chalk = require( 'chalk' );
+const path = require( 'path' );
+
+const nodeModulesDirectory = path.join( __dirname, '../', 'node_modules' );
+
+if ( ! existsSync( nodeModulesDirectory ) ) {
+ console.log(
+ chalk.red(
+ 'The @woocommerce/monorepo-utils must be built before running the CLI.'
+ )
+ );
+ console.log(
+ chalk.yellow(
+ 'run `pnpm run build` from the root of the monorepo-utils package. or `pnpm install --filter monorepo-utils` from project root.'
+ )
+ );
+ process.exit( 1 );
+}
+
const { program } = require( '../dist/index' );
program.parse( process.argv );
diff --git a/tools/monorepo-utils/package.json b/tools/monorepo-utils/package.json
index a0d8c5cddc7..a231fd003e8 100644
--- a/tools/monorepo-utils/package.json
+++ b/tools/monorepo-utils/package.json
@@ -27,6 +27,7 @@
"build": "tsc",
"start": "tsc --watch",
"lint": "eslint . --ext .ts",
+ "postinstall": "pnpm run build",
"test": "jest"
},
"engines": {