Merge branch 'trunk' into fix/wp-l2-retrieval

This commit is contained in:
rodelgc 2023-04-15 01:53:03 +08:00
commit 9897c529d4
89 changed files with 1959 additions and 402 deletions

View File

@ -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

View File

@ -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**

View File

@ -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",

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update select tree control dropdown menu for custom slot fill support for display within Modals

View File

@ -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( () => {

View File

@ -1 +1,2 @@
export * from './select-tree';
export * from './select-tree-menu';

View File

@ -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 (
<div
ref={ selectControlMenuRef }
className="woocommerce-experimental-select-tree-control__menu"
>
<div>
<Popover
// @ts-expect-error this prop does exist, see: https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/popover/index.tsx#L180.
__unstableSlotName="woocommerce-select-tree-control-menu"
focusOnMount={ false }
className={ classnames(
'woocommerce-experimental-select-tree-control__popover-menu',
className,
{
'is-open': isOpen,
'has-results': items.length > 0,
}
) }
position={ position }
animate={ false }
onFocusOutside={ () => {
onClose();
} }
>
{ isOpen && (
<div>
{ isLoading ? (
<div
style={ {
width: boundingRect?.width,
} }
>
<Spinner />
</div>
) : (
<Tree
{ ...props }
id={ `${ props.id }-menu` }
ref={ ref }
items={ items }
onTreeBlur={ onClose }
shouldItemBeExpanded={
shouldItemBeExpanded
}
shouldShowCreateButton={
shouldShowCreateButton
}
style={ {
width: boundingRect?.width,
} }
/>
) }
</div>
) }
</Popover>
</div>
</div>
);
/* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
};
export const SelectTreeMenuSlot: React.FC = () =>
createPortal(
<div aria-live="off">
{ /* @ts-expect-error name does exist on PopoverSlot see: https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/popover/index.tsx#L555 */ }
<Popover.Slot name="woocommerce-select-tree-control-menu" />
</div>,
document.body
);

View File

@ -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 (
<Dropdown
<div
className="woocommerce-experimental-select-tree-control__dropdown"
contentClassName="woocommerce-experimental-select-tree-control__dropdown-content"
focusOnMount={ false }
renderContent={ ( { onClose } ) =>
isLoading ? (
<div
style={ {
width: comboBoxWidth,
} }
>
<Spinner />
</div>
) : (
<Tree
{ ...props }
id={ `${ props.id }-menu` }
ref={ ref }
items={ linkedTree }
onTreeBlur={ onClose }
shouldItemBeExpanded={ shouldItemBeExpanded }
shouldShowCreateButton={ shouldShowCreateButton }
style={ {
width: comboBoxWidth,
} }
/>
)
}
renderToggle={ ( { isOpen, onToggle, onClose } ) => (
<div
className={ classNames(
'woocommerce-experimental-select-control',
{
'is-focused': isFocused,
}
) }
tabIndex={ -1 }
>
<div
className={ classNames(
'woocommerce-experimental-select-control',
{
'is-focused': isFocused,
}
) }
>
<label
htmlFor={ `${ props.id }-input` }
id={ `${ props.id }-label` }
className="woocommerce-experimental-select-control__label"
>
<label
htmlFor={ `${ props.id }-input` }
id={ `${ props.id }-label` }
className="woocommerce-experimental-select-control__label"
>
{ props.label }
</label>
<ComboBox
comboBoxProps={ {
className:
'woocommerce-experimental-select-control__combo-box-wrapper',
ref: comboBoxRef,
role: 'combobox',
'aria-expanded': isOpen,
'aria-haspopup': 'tree',
'aria-labelledby': `${ props.id }-label`,
'aria-owns': `${ props.id }-menu`,
{ props.label }
</label>
<ComboBox
comboBoxProps={ {
className:
'woocommerce-experimental-select-control__combo-box-wrapper',
role: 'combobox',
'aria-expanded': isOpen,
'aria-haspopup': 'tree',
'aria-labelledby': `${ props.id }-label`,
'aria-owns': `${ props.id }-menu`,
} }
inputProps={ {
className:
'woocommerce-experimental-select-control__input',
id: `${ props.id }-input`,
'aria-autocomplete': 'list',
'aria-controls': `${ props.id }-menu`,
autoComplete: 'off',
onFocus: () => {
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 }
>
<SelectedItems
items={ ( props.selected as Item[] ) || [] }
getItemLabel={ ( item ) => 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 }
>
<SelectedItems
items={ ( props.selected as Item[] ) || [] }
getItemLabel={ ( item ) => item?.label || '' }
getItemValue={ ( item ) => item?.value || '' }
onRemove={ ( item ) => {
if (
! Array.isArray( item ) &&
props.onRemove
) {
props.onRemove( item );
onClose();
}
} }
getSelectedItemProps={ () => ( {} ) }
/>
</ComboBox>
</div>
) }
/>
getSelectedItemProps={ () => ( {} ) }
/>
</ComboBox>
</div>
<SelectTreeMenu
{ ...props }
id={ `${ props.id }-menu` }
className={ menuInstanceId.toString() }
ref={ ref }
isOpen={ isOpen }
items={ linkedTree }
shouldShowCreateButton={ shouldShowCreateButton }
onClose={ () => setIsOpen( false ) }
/>
</div>
);
};

View File

@ -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 (
<SlotFillProvider>
Selected: { JSON.stringify( selected ) }
<Button onClick={ () => setOpen( true ) }>
Show Dropdown in Modal
</Button>
{ isOpen && (
<Modal
title="Dropdown Modal"
onRequestClose={ () => setOpen( false ) }
>
<SelectTree
id="multiple-select-tree"
label="Multiple Select Tree"
multiple
items={ items }
selected={ selected }
shouldNotRecursivelySelect
shouldShowCreateButton={ ( typedValue ) =>
! 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 );
} }
/>
</Modal>
) }
<SelectTreeMenuSlot />
</SlotFillProvider>
);
};
export default {
title: 'WooCommerce Admin/experimental/SelectTreeControl',
component: SelectTree,

View File

@ -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.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix invalid return callback ref warning

View File

@ -23,6 +23,8 @@ jest.mock( 'react-visibility-sensor', () =>
} )
);
window.open = jest.fn();
describe( 'InboxNoteCard', () => {
const note = {
id: 1,

View File

@ -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(
<TestComp callback={ jest.fn() } />
);
const span = getByText( 'Some Text' );
jest.spyOn( span, 'removeEventListener' ).mockImplementation(
listener
);
unmount();
expect( listener ).toHaveBeenCalledTimes( 1 );
} );
} );

View File

@ -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;
}, [] );
}

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding inventory email, conditional and checkbox blocks.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding sku block to product editor.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Refactoring product link modal and adding link to product block editor.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Adding apifetch middleware to override product api endpoint only for the product editor.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Fix issue with category parent select control clearing search value when typing.

View File

@ -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",

View File

@ -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"
}

View File

@ -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 (
<div { ...blockProps }>
<h4> { title } </h4>
<CheckboxControl
label={
tooltip
? createInterpolateElement( `<label /> <tooltip />`, {
label: <span>{ label }</span>,
tooltip: (
<Tooltip
text={ <span>{ tooltip }</span> }
position="top center"
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Incorrect types.
className={
'woocommerce-product-form__checkbox-tooltip'
}
delay={ 0 }
>
<span className="woocommerce-product-form__checkbox-tooltip-icon">
<Icon
icon={ help }
size={ 20 }
fill="#949494"
/>
</span>
</Tooltip>
),
} )
: label
}
checked={ value }
onChange={ ( selected ) => setValue( selected ) }
/>
</div>
);
}

View File

@ -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;
}
}

View File

@ -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 } );

View File

@ -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
}
}

View File

@ -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 (
<div { ...blockProps }>
<DisplayState
state={ displayBlocks ? 'visible' : 'visually-hidden' }
>
<InnerBlocks templateLock="all" />
</DisplayState>
</div>
);
}

View File

@ -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 } );

View File

@ -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"
}

View File

@ -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 (
<>
<div { ...blockProps }>
<div className="wp-block-columns">
<div className="wp-block-column">
<BaseControl
id={ 'product_inventory_email' }
label={ __(
'Email me when stock reaches',
'woocommerce'
) }
help={ createInterpolateElement(
__(
'Make sure to enable notifications in <link>store settings.</link>',
'woocommerce'
),
{
link: (
<Link
href={ `${ getSetting(
'adminUrl'
) }admin.php?page=wc-settings&tab=products&section=inventory` }
target="_blank"
type="external"
></Link>
),
}
) }
>
<InputControl
name={ 'woocommerce-product-name' }
placeholder={ sprintf(
// translators: Default quantity to notify merchants of low stock.
__( '%d (store default)', 'woocommerce' ),
notifyLowStockAmount
) }
onChange={ setLowStockAmount }
value={ lowStockAmount }
min={ 0 }
/>
</BaseControl>
</div>
<div className="wp-block-column"></div>
</div>
</div>
</>
);
}

View File

@ -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,
} );

View File

@ -0,0 +1,3 @@
.woocommerce-product-form__inventory-email {
margin-top: $gap-large;
}

View File

@ -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"
}

View File

@ -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 (
<div { ...blockProps }>
<BaseControl
id={ 'product_sku' }
className="woocommerce-product-form_inventory-sku"
label={ createInterpolateElement(
__( 'Sku <description />', 'woocommerce' ),
{
description: (
<span className="woocommerce-product-form__optional-input">
{ __( '(STOCK KEEPING UNIT)', 'woocommerce' ) }
</span>
),
}
) }
>
<InputControl
name={ 'woocommerce-product-sku' }
onChange={ setSku }
value={ sku || '' }
/>
</BaseControl>
</div>
);
}

View File

@ -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 } );

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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 (
<div { ...blockProps }>
<TextControl
label={ createInterpolateElement(
__( 'Name <required />', 'woocommerce' ),
{
required: (
<span className="woocommerce-product-form__optional-input">
{ __( '(required)', 'woocommerce' ) }
</span>
),
}
<>
<div { ...blockProps }>
<BaseControl
id={ 'product_name' }
label={ createInterpolateElement(
__( 'Name <required />', 'woocommerce' ),
{
required: (
<span className="woocommerce-product-form__required-input">
{ __( '*', 'woocommerce' ) }
</span>
),
}
) }
>
<InputControl
name={ 'woocommerce-product-name' }
placeholder={ __(
'e.g. 12 oz Coffee Mug',
'woocommerce'
) }
onChange={ setName }
value={ name || '' }
onBlur={ setSkuIfEmpty }
/>
</BaseControl>
{ productId &&
nameIsValid &&
[ 'publish', 'draft' ].includes( product.status ) &&
permalinkPrefix && (
<span className="woocommerce-product-form__secondary-text product-details-section__product-link">
{ __( 'Product link', 'woocommerce' ) }
:&nbsp;
<a
href={ product.permalink }
target="_blank"
rel="noreferrer"
>
{ permalinkPrefix }
{ product.slug || cleanForSlug( name ) }
{ permalinkSuffix }
</a>
<Button
variant="link"
onClick={ () =>
setShowProductLinkEditModal( true )
}
>
{ __( 'Edit', 'woocommerce' ) }
</Button>
</span>
) }
{ showProductLinkEditModal && (
<EditProductLinkModal
permalinkPrefix={ permalinkPrefix || '' }
permalinkSuffix={ permalinkSuffix || '' }
product={ product }
onCancel={ () => 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 || '' }
/>
</div>
</div>
</>
);
}

View File

@ -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;
}

View File

@ -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,
};
}
} }
/>
) }
</div>

View File

@ -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 > = ( {
</Button>
<Button
isPrimary
isBusy={ isUpdatingDraft || isUpdatingPublished }
disabled={
isUpdatingDraft ||
isUpdatingPublished ||
slug === product.slug
}
onClick={ () => {
onSave();
isBusy={ isSaving }
disabled={ isSaving || slug === product.slug }
onClick={ async () => {
setIsSaving( true );
await onSave();
setIsSaving( false );
} }
>
{ __( 'Save', 'woocommerce' ) }

View File

@ -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(

View File

@ -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();
};

View File

@ -13,7 +13,6 @@
font-size: 16px;
font-weight: 600;
color: #1e1e1e;
margin-bottom: $gap-smaller;
}
&__description {

View File

@ -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';

View File

@ -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,

View File

@ -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 );
} );
};

View File

@ -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(

View File

@ -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();

View File

@ -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
}
}
}
}

View File

@ -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

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding checkbox, conditional and inventory email blocks to product blocks editor.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding inventory section and sku blocks to product block editor.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Update to the merchant variable product e2e test

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add a category for product editor blocks

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add method delete_meta_data_value to WC_Data objects

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Lock the product block editor template root

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Use first meta value for HPOS migration when there are duplicates for flat column.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Exclude empty attributes from the attribute count when tracking product updates.

View File

@ -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.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Removing modification to rest_namespace on post type and replacing with middleware.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Support min_php_version and min_wp_version for the free extensions feed

View File

@ -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

View File

@ -0,0 +1,5 @@
Significance: patch
Type: update
Comment: No changelog entry needed, this is an update to developer-facing documentation only.

View File

@ -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.
*

View File

@ -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' ),
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=products&section=inventory' ) . '" target="_blank" rel="noreferrer">',
'</a>'
),
'icon' => array(
'src' => '<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M2 2H14C14.2761 2 14.5 2.22386 14.5 2.5V9.5H11.5H10C10 10.6046 9.10457 11.5 8 11.5C6.89543 11.5 6 10.6046 6 9.5H4.5H1.5V2.5C1.5 2.22386 1.72386 2 2 2ZM1.5 11V14.5C1.5 14.7761 1.72386 15 2 15H14C14.2761 15 14.5 14.7761 14.5 14.5V11H11.1632C10.6015 12.1825 9.3962 13 8 13C6.6038 13 5.39855 12.1825 4.83682 11H1.5ZM0 9.5V2.5C0 1.39543 0.895431 0.5 2 0.5H14C15.1046 0.5 16 1.39543 16 2.5V9.5V11V14.5C16 15.6046 15.1046 16.5 14 16.5H2C0.895431 16.5 0 15.6046 0 14.5V11V9.5Z" fill="#1E1E1E"/></svg>',
),
),
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' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 7.75C3.5 6.7835 4.2835 6 5.25 6H14.75H15.5V6.75V9H17.25H17.5607L17.7803 9.21967L20.7803 12.2197L21 12.4393V12.75V14.75C21 15.7165 20.2165 16.5 19.25 16.5H19.2377C19.2458 16.5822 19.25 16.6656 19.25 16.75C19.25 18.1307 18.1307 19.25 16.75 19.25C15.3693 19.25 14.25 18.1307 14.25 16.75C14.25 16.6656 14.2542 16.5822 14.2623 16.5H14H10.2377C10.2458 16.5822 10.25 16.6656 10.25 16.75C10.25 18.1307 9.13071 19.25 7.75 19.25C6.36929 19.25 5.25 18.1307 5.25 16.75C5.25 16.6656 5.25418 16.5822 5.26234 16.5H4.25H3.5V15.75V7.75ZM14 15V9.75V9V7.5H5.25C5.11193 7.5 5 7.61193 5 7.75V15H5.96464C6.41837 14.5372 7.05065 14.25 7.75 14.25C8.44935 14.25 9.08163 14.5372 9.53536 15H14ZM18.5354 15H19.25C19.3881 15 19.5 14.8881 19.5 14.75V13.0607L16.9393 10.5H15.5V14.5845C15.8677 14.3717 16.2946 14.25 16.75 14.25C17.4493 14.25 18.0816 14.5372 18.5354 15ZM6.7815 16.5C6.76094 16.5799 6.75 16.6637 6.75 16.75C6.75 17.3023 7.19772 17.75 7.75 17.75C8.30228 17.75 8.75 17.3023 8.75 16.75C8.75 16.6637 8.73906 16.5799 8.7185 16.5C8.60749 16.0687 8.21596 15.75 7.75 15.75C7.28404 15.75 6.89251 16.0687 6.7815 16.5ZM15.7815 16.5C15.7609 16.5799 15.75 16.6637 15.75 16.75C15.75 17.3023 16.1977 17.75 16.75 17.75C17.3023 17.75 17.75 17.3023 17.75 16.75C17.75 16.6637 17.7391 16.5799 17.7185 16.5C17.7144 16.4841 17.7099 16.4683 17.705 16.4526C17.5784 16.0456 17.1987 15.75 16.75 15.75C16.284 15.75 15.8925 16.0687 15.7815 16.5Z" fill="#1E1E1E"/></svg>',
),
@ -615,6 +701,7 @@ class WC_Post_Types {
),
),
),
'template_lock' => 'all',
)
)
);

View File

@ -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',

View File

@ -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",

View File

@ -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 );
}
/**

View File

@ -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' );

View File

@ -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;
}
}

View File

@ -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',
),

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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;
}

View File

@ -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' ),
'<a href="https://woocommerce.com/products/google-listings-and-ads" target="_blank">',
'</a>'
),
'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' ),

View File

@ -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 );

View File

@ -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,

View File

@ -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:

View File

@ -63,7 +63,24 @@ The default values are:
- 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.

View File

@ -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

View File

@ -0,0 +1,99 @@
<?php
// phpcs:ignoreFile
if ( ! class_exists( UPDATE_WP_JSON::class ) ) {
class UPDATE_WP_JSON {
private $wp_env_path = __DIR__ . '/../../../.wp-env.json';
private $wp_json = [];
private $wc_version = null;
private $wp_version = null;
private $php_version = null;
public function __construct() {
if ( file_exists( $this->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();
}

View File

@ -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++ ) {

View File

@ -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).

View File

@ -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 );
}
}

View File

@ -0,0 +1,2 @@
apache_modules:
- mod_rewrite

View File

@ -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

View File

@ -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 );

View File

@ -27,6 +27,7 @@
"build": "tsc",
"start": "tsc --watch",
"lint": "eslint . --ext .ts",
"postinstall": "pnpm run build",
"test": "jest"
},
"engines": {