diff --git a/.github/workflows/pr-highlight-changes.yml b/.github/workflows/pr-highlight-changes.yml index ec9d0c28393..b1ecc977f14 100644 --- a/.github/workflows/pr-highlight-changes.yml +++ b/.github/workflows/pr-highlight-changes.yml @@ -22,7 +22,7 @@ jobs: run: | npm install -g pnpm@7 npm -g i @wordpress/env@5.1.0 - pnpm install --filter monorepo-utils --filter code-analyzer --filter cli-core + pnpm install --filter code-analyzer --filter cli-core - name: Run analyzer id: run working-directory: tools/code-analyzer diff --git a/changelog.txt b/changelog.txt index 30d4a66ac9e..a45486b63ae 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,129 @@ == Changelog == += 7.6.0 2023-04-13 = + +**WooCommerce** + +* Fix - Fix incorrect usage of dispatch, useSelect, and setState calls in homescreen along with settings and onboarding package [#37641](https://github.com/woocommerce/woocommerce/pull/37641) +* Fix - Do not attempt to cache order during order creation (HPOS). [#37569](https://github.com/woocommerce/woocommerce/pull/37569) +* Fix - Add default value when calling get_option for woocommerce_task_list_tracked_completed_tasks. [#37397](https://github.com/woocommerce/woocommerce/pull/37397) +* Fix - When order meta data is saved via HPOS, it should be backfilled to the CPT data store. [#36593](https://github.com/woocommerce/woocommerce/pull/36593) +* Fix - Overwrite clone method to prevent duplicate data when saving a clone. [#37313](https://github.com/woocommerce/woocommerce/pull/37313) +* Fix - Add default button padding to TT2 stylesheet to fix some visual issues in WP 5.9 and 6.0 [#37018](https://github.com/woocommerce/woocommerce/pull/37018) +* Fix - Added skydropx slug back to shipping partners list so that it can be installed through the shipping task [#37286](https://github.com/woocommerce/woocommerce/pull/37286) +* Fix - Add HPOS compat for admin report functions. [#36650](https://github.com/woocommerce/woocommerce/pull/36650) +* Fix - Add HPOS compat for wc-user-functions.php. [#36650](https://github.com/woocommerce/woocommerce/pull/36650) +* Fix - Add support for null inputs to pnpm wc_add_number_precision [#36891](https://github.com/woocommerce/woocommerce/pull/36891) +* Fix - Add support for `after`, `before`, `modified_after` and `modified_before` params in local timezone. [#36650](https://github.com/woocommerce/woocommerce/pull/36650) +* Fix - Add validation when saving attributes and variations [#37046](https://github.com/woocommerce/woocommerce/pull/37046) +* Fix - Also delete when order type is placehoder, since it was created by HPOS. [#36650](https://github.com/woocommerce/woocommerce/pull/36650) +* Fix - Corrects a class reference in the ProductDownloadsServiceProvider. [#37052](https://github.com/woocommerce/woocommerce/pull/37052) +* Fix - Corrects a variable name in Reports\Stock\Stats. It was missed during the last name change. [#37057](https://github.com/woocommerce/woocommerce/pull/37057) +* Fix - Corrects class namespaces in Onboarding. It was missed during last restructuring. [#37056](https://github.com/woocommerce/woocommerce/pull/37056) +* Fix - Corrects imported classes. Class names should not begin with a backslash. [#37058](https://github.com/woocommerce/woocommerce/pull/37058) +* Fix - Ensure product importer imports all lines in a CSV file. [#36839](https://github.com/woocommerce/woocommerce/pull/36839) +* Fix - Fetch order first to refresh cache before returning prop. [#36650](https://github.com/woocommerce/woocommerce/pull/36650) +* Fix - Fix 0 rendered on short-circuit evaluation. [#37104](https://github.com/woocommerce/woocommerce/pull/37104) +* Fix - Fix ArrayUtil::get_value_or_default method not behaving as documented for null array values [#37053](https://github.com/woocommerce/woocommerce/pull/37053) +* Fix - Fix blank screen is displayed during OBW when using WP5.9 [#36903](https://github.com/woocommerce/woocommerce/pull/36903) +* Fix - Fix duplicated global attribute [#37109](https://github.com/woocommerce/woocommerce/pull/37109) +* Fix - fixed bug where jetpack connection owner field was assumed to be username when its actually display name [#37170](https://github.com/woocommerce/woocommerce/pull/37170) +* Fix - Fixed payments recommendations pane in WooCommerce Payment Settings using the wrong image prop [#37259](https://github.com/woocommerce/woocommerce/pull/37259) +* Fix - Fixes filtering by attributes in the Analytics Orders and Variations reports. [#37223](https://github.com/woocommerce/woocommerce/pull/37223) +* Fix - Fix incorrect VAT exempt behaviour on shop page when prices are exclusive of tax. [#33991](https://github.com/woocommerce/woocommerce/pull/33991) +* Fix - Fix React rendering falsy value in marketing page. [#37227](https://github.com/woocommerce/woocommerce/pull/37227) +* Fix - Fix the inability to apply a coupon whose code is "0" [#36924](https://github.com/woocommerce/woocommerce/pull/36924) +* Fix - fix typo in variable name [#36759](https://github.com/woocommerce/woocommerce/pull/36759) +* Fix - Fix unit test snapshots due to a dependency version change [#36435](https://github.com/woocommerce/woocommerce/pull/36435) +* Fix - Fix variations exported as draft being imported as draft (and thus remaining invisible) [#36933](https://github.com/woocommerce/woocommerce/pull/36933) +* Fix - Fix WP data resolution (`invalidateResolution`) not working with WP 5.9 in marketing page. [#37198](https://github.com/woocommerce/woocommerce/pull/37198) +* Fix - Handle date arguments in OrderTableQuery correctly by adjusting their timezones before running. [#36650](https://github.com/woocommerce/woocommerce/pull/36650) +* Fix - Load same stylesheets in the Site Editor as in the frontend [#36911](https://github.com/woocommerce/woocommerce/pull/36911) +* Fix - Loco Translate and wp-cli compatibility for woocommerce-admin translation files [#36739](https://github.com/woocommerce/woocommerce/pull/36739) +* Fix - Override react version to 17.0.2 [#37087](https://github.com/woocommerce/woocommerce/pull/37087) +* Fix - Prevent possible warning arising from use of woocommerce_wp_* family of functions. [#37026](https://github.com/woocommerce/woocommerce/pull/37026) +* Fix - Record values for toggled checkboxes/features in settings [#37242](https://github.com/woocommerce/woocommerce/pull/37242) +* Fix - Restore the sort order when orders are cached. [#36650](https://github.com/woocommerce/woocommerce/pull/36650) +* Fix - Treat order as seperate resource when validating for webhook since it's not necessarily a CPT anymore. [#36650](https://github.com/woocommerce/woocommerce/pull/36650) +* Fix - Update Customers report with latest user data after editing user. [#37237](https://github.com/woocommerce/woocommerce/pull/37237) +* Add - Add "Create a new campaign" modal in Campaigns card in Multichannel Marketing page. [#37044](https://github.com/woocommerce/woocommerce/pull/37044) +* Add - Add a cache for orders, to use when custom order tables are enabled [#35014](https://github.com/woocommerce/woocommerce/pull/35014) +* Add - Add an encoding selector to the product importer [#36819](https://github.com/woocommerce/woocommerce/pull/36819) +* Add - Add Campaigns card into Multichannel Marketing page. [#36735](https://github.com/woocommerce/woocommerce/pull/36735) +* Add - Added images support for the payment recommendations transaction processors [#37230](https://github.com/woocommerce/woocommerce/pull/37230) +* Add - Added `woocommerce_widget_layered_nav_filters_start/end` hooks around layered nav filters widget [#36705](https://github.com/woocommerce/woocommerce/pull/36705) +* Add - Add introduction banner to multichannel marketing page. [#37110](https://github.com/woocommerce/woocommerce/pull/37110) +* Add - Add marketplace suggestions and multichannel marketing information to WC Tracker. [#37017](https://github.com/woocommerce/woocommerce/pull/37017) +* Add - Add new feature flag for the product edit blocks experience [#37137](https://github.com/woocommerce/woocommerce/pull/37137) +* Add - Add productBlockEditorSettings script to be used for the Product Block Editor. [#37123](https://github.com/woocommerce/woocommerce/pull/37123) +* Add - Add support for new countries in WCPay [#36906](https://github.com/woocommerce/woocommerce/pull/36906) +* Add - Add wp-json/wc-admin/shipping-partner-suggestions API endpoint [#37155](https://github.com/woocommerce/woocommerce/pull/37155) +* Add - Allow sorting by menu_order in products widget. [#37002](https://github.com/woocommerce/woocommerce/pull/37002) +* Add - Create editor skeleton on add/edit product pages [#37023](https://github.com/woocommerce/woocommerce/pull/37023) +* Add - Creating product entity in auto-draft status, and adding support for retrieving preexisting products. [#37064](https://github.com/woocommerce/woocommerce/pull/37064) +* Add - Fixed image array in edit context for product/variations endpoint. [#28498](https://github.com/woocommerce/woocommerce/pull/28498) +* Add - Initial e2e tests for new product editor. [#36902](https://github.com/woocommerce/woocommerce/pull/36902) +* Add - Log to order notes when coupons are removed or applied. [#30642](https://github.com/woocommerce/woocommerce/pull/30642) +* Add - Make WC_Order::get_tax_location accessible publicly through a wrapper function. [#36953](https://github.com/woocommerce/woocommerce/pull/36953) +* Add - Update product post rest config when block editor feature is enabled. [#37206](https://github.com/woocommerce/woocommerce/pull/37206) +* Update - Update WooCommerce Blocks to 9.8.4 [#37492](https://github.com/woocommerce/woocommerce/pull/37492) +* Update - Update WooCommerce Blocks to 9.8.3 [#37477](https://github.com/woocommerce/woocommerce/pull/37477) +* Update - Update WooCommerce Blocks to 9.8.2 [#37373](https://github.com/woocommerce/woocommerce/pull/37373) +* Update - Add tabs and sections placeholders in product blocks template [#37174](https://github.com/woocommerce/woocommerce/pull/37174) +* Update - Change the default date used on Revenue and Orders report to 'date_paid' and create spotlight on both reports [#36653](https://github.com/woocommerce/woocommerce/pull/36653) +* Update - Change Variations form shown in Variations tab when there are no variations created [#36957](https://github.com/woocommerce/woocommerce/pull/36957) +* Update - Moving currencyContext to relevant package, and updating all references. [#36959](https://github.com/woocommerce/woocommerce/pull/36959) +* Update - Moving some components out of core and into product-editor package. [#36945](https://github.com/woocommerce/woocommerce/pull/36945) +* Update - Moving use-product-helper and related product hooks to product editor package. [#37006](https://github.com/woocommerce/woocommerce/pull/37006) +* Update - Refresh data source poller transients on wc_admin_daily [#37027](https://github.com/woocommerce/woocommerce/pull/37027) +* Update - Remove accordion from "Other payment providers" in payment task [#37205](https://github.com/woocommerce/woocommerce/pull/37205) +* Update - Remove Cart2Cart option from add product task [#37285](https://github.com/woocommerce/woocommerce/pull/37285) +* Update - Show link to store settings when stock management is disabled. [#37140](https://github.com/woocommerce/woocommerce/pull/37140) +* Update - Update create-wc-extension script within woocommerce-admin. [#36917](https://github.com/woocommerce/woocommerce/pull/36917) +* Update - Update imports of product slot fills to new @woocommerce/product-editor library [#36830](https://github.com/woocommerce/woocommerce/pull/36830) +* Update - Update obw payment gateways [#37233](https://github.com/woocommerce/woocommerce/pull/37233) +* Update - Update playwright api-core-tests to associate orders with real products to prevent extension issues for those that validate product ids [#37243](https://github.com/woocommerce/woocommerce/pull/37243) +* Update - Update playwright api-core-tests to handle cases where extensions add to shipping methods [#37239](https://github.com/woocommerce/woocommerce/pull/37239) +* Update - Update product template by adding the list price and sale price blocks. [#37211](https://github.com/woocommerce/woocommerce/pull/37211) +* Update - Updates automated release testing workflow to use Playwright [#36598](https://github.com/woocommerce/woocommerce/pull/36598) +* Update - Update template of product type to include product name block. [#37132](https://github.com/woocommerce/woocommerce/pull/37132) +* Update - Update the date modified field for an order when a refund for it is successfully processed. [#37047](https://github.com/woocommerce/woocommerce/pull/37047) +* Update - Update WooCommerce BLocks to 9.8.0 [#37210](https://github.com/woocommerce/woocommerce/pull/37210) +* Update - Update WooCommerce Blocks to 9.8.1 [#37238](https://github.com/woocommerce/woocommerce/pull/37238) +* Update - Updating rest namespace for product posttype to version 3. [#37028](https://github.com/woocommerce/woocommerce/pull/37028) +* Update - Use the currently activated theme color for completed tasks strikethough [#37001](https://github.com/woocommerce/woocommerce/pull/37001) +* Dev - Add @woocommerce/admin-layout package. [#37094](https://github.com/woocommerce/woocommerce/pull/37094) +* Dev - Add CES data store to @woocommerce/customer-effort-score [#37252](https://github.com/woocommerce/woocommerce/pull/37252) +* Dev - Add existing global attribute layout #36944 [#36944](https://github.com/woocommerce/woocommerce/pull/36944) +* Dev - Add missing woocommerce_run_on_woocommerce_admin_updated hook for the scheduled action registered in RemoteInboxNotificationsEngine [#36768](https://github.com/woocommerce/woocommerce/pull/36768) +* Dev - add wpLogin import to wc-baseline-load.js [#36940](https://github.com/woocommerce/woocommerce/pull/36940) +* Dev - Convert "Allow backorders?" into radio buttons [#37282](https://github.com/woocommerce/woocommerce/pull/37282) +* Dev - Fix lint issues [#36988](https://github.com/woocommerce/woocommerce/pull/36988) +* Dev - Fix the value of `UPDATE_WC` environment variable in the daily k6 performance tests. [#37049](https://github.com/woocommerce/woocommerce/pull/37049) +* Dev - Move CES components and utilities to @woocommerce/customer-effort-score [#37112](https://github.com/woocommerce/woocommerce/pull/37112) +* Dev - Move hook to confirm unsaved form changes to navigation package [#36752](https://github.com/woocommerce/woocommerce/pull/36752) +* Dev - Move product utils into product editor package [#36730](https://github.com/woocommerce/woocommerce/pull/36730) +* Dev - Set up React Fast Refresh in woocommerce-admin [#37165](https://github.com/woocommerce/woocommerce/pull/37165) +* Dev - Show "Stock status" as a collection of radio buttons [#37278](https://github.com/woocommerce/woocommerce/pull/37278) +* Dev - Show a message for variable products [#37185](https://github.com/woocommerce/woocommerce/pull/37185) +* Dev - Support E2E testing of draft releases. [#36997](https://github.com/woocommerce/woocommerce/pull/36997) +* Dev - Sync @wordpress package versions via syncpack. [#37034](https://github.com/woocommerce/woocommerce/pull/37034) +* Tweak - Add productId dependency when getting the product by id in ProductPage [#37152](https://github.com/woocommerce/woocommerce/pull/37152) +* Tweak - Add tracking for local pickup method in Checkout [#36847](https://github.com/woocommerce/woocommerce/pull/36847) +* Tweak - Add Tracks events for product inventory tab interactions. [#37202](https://github.com/woocommerce/woocommerce/pull/37202) +* Tweak - Change Avalara CTA copy in tax task to Download [#37224](https://github.com/woocommerce/woocommerce/pull/37224) +* Tweak - Make sure 'safe_text' settings are rendered as 'text' inputs for compatibility. [#37154](https://github.com/woocommerce/woocommerce/pull/37154) +* Tweak - Prevent 'woocommerce_ajax_order_items_removed' from generating PHP warnings. [#37178](https://github.com/woocommerce/woocommerce/pull/37178) +* Tweak - Rename "Manage stock?" label to "Stock management". [#37135](https://github.com/woocommerce/woocommerce/pull/37135) +* Tweak - Trigger event `woocommerce_attributes_saved` following successful product meta box ajax update. [#36943](https://github.com/woocommerce/woocommerce/pull/36943) +* Tweak - Visual tweaks for shipping partner banners [#37229](https://github.com/woocommerce/woocommerce/pull/37229) +* Performance - Bypass Action Scheduler for customer updates. [#37265](https://github.com/woocommerce/woocommerce/pull/37265) +* Performance - Switch wc_product_attributes_lookup table management to use truncate and dbDelta over drop table [#36872](https://github.com/woocommerce/woocommerce/pull/36872) +* Enhancement - Add 'display_context' argument to wc_get_price_to_display(). [#25080](https://github.com/woocommerce/woocommerce/pull/25080) +* Enhancement - Added woocommerce_reduce_order_item_stock action hook [#34721](https://github.com/woocommerce/woocommerce/pull/34721) +* Enhancement - Add the support for the C&C Blocks in declaring compatibility feature [#36426](https://github.com/woocommerce/woocommerce/pull/36426) + + = 7.5.1 2023-03-21 = **WooCommerce** diff --git a/package.json b/package.json index 5f912b02622..173ab1f949f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "test": "pnpm exec turbo run turbo:test", "clean": "pnpm store prune && git clean -fx **/node_modules && pnpm i", "preinstall": "npx only-allow pnpm", - "postinstall": "pnpm git:update-hooks && pnpm run --filter='./tools/monorepo-utils' build", + "postinstall": "pnpm git:update-hooks", "git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && husky install", "create-extension": "node ./tools/create-extension/index.js", "cherry-pick": "node ./tools/cherry-pick/bin/run", diff --git a/packages/js/components/changelog/update-select_tree_dropdown b/packages/js/components/changelog/update-select_tree_dropdown new file mode 100644 index 00000000000..5c9d5b64dc6 --- /dev/null +++ b/packages/js/components/changelog/update-select_tree_dropdown @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update select tree control dropdown menu for custom slot fill support for display within Modals diff --git a/packages/js/components/src/experimental-select-control/menu.tsx b/packages/js/components/src/experimental-select-control/menu.tsx index 691b038af68..2587e8422f2 100644 --- a/packages/js/components/src/experimental-select-control/menu.tsx +++ b/packages/js/components/src/experimental-select-control/menu.tsx @@ -10,6 +10,7 @@ import { useState, createPortal, Children, + useLayoutEffect, } from '@wordpress/element'; /** @@ -37,13 +38,19 @@ export const Menu = ( { const [ boundingRect, setBoundingRect ] = useState< DOMRect >(); const selectControlMenuRef = useRef< HTMLDivElement >( null ); - useEffect( () => { - if ( selectControlMenuRef.current?.parentElement ) { + useLayoutEffect( () => { + if ( + selectControlMenuRef.current?.parentElement && + selectControlMenuRef.current?.parentElement.clientWidth > 0 + ) { setBoundingRect( selectControlMenuRef.current.parentElement.getBoundingClientRect() ); } - }, [ selectControlMenuRef.current ] ); + }, [ + selectControlMenuRef.current, + selectControlMenuRef.current?.clientWidth, + ] ); // Scroll the selected item into view when the menu opens. useEffect( () => { diff --git a/packages/js/components/src/experimental-select-tree-control/index.ts b/packages/js/components/src/experimental-select-tree-control/index.ts index 9ea352bc18b..876360ad549 100644 --- a/packages/js/components/src/experimental-select-tree-control/index.ts +++ b/packages/js/components/src/experimental-select-tree-control/index.ts @@ -1 +1,2 @@ export * from './select-tree'; +export * from './select-tree-menu'; diff --git a/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx b/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx new file mode 100644 index 00000000000..294ceafef60 --- /dev/null +++ b/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx @@ -0,0 +1,154 @@ +/** + * External dependencies + */ +import { Popover, Spinner } from '@wordpress/components'; +import classnames from 'classnames'; +import { + createElement, + useEffect, + useRef, + createPortal, + useLayoutEffect, + useState, +} from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { + LinkedTree, + Tree, + TreeControlProps, +} from '../experimental-tree-control'; + +type MenuProps = { + isOpen: boolean; + isLoading?: boolean; + position?: Popover.Position; + scrollIntoViewOnOpen?: boolean; + items: LinkedTree[]; + treeRef?: React.ForwardedRef< HTMLOListElement >; + onClose?: () => void; +} & Omit< TreeControlProps, 'items' >; + +export const SelectTreeMenu = ( { + isLoading, + isOpen, + className, + position = 'bottom center', + scrollIntoViewOnOpen = false, + items, + treeRef: ref, + onClose = () => {}, + shouldShowCreateButton, + ...props +}: MenuProps ) => { + const [ boundingRect, setBoundingRect ] = useState< DOMRect >(); + const selectControlMenuRef = useRef< HTMLDivElement >( null ); + + useLayoutEffect( () => { + if ( + selectControlMenuRef.current?.parentElement && + selectControlMenuRef.current?.parentElement.clientWidth > 0 + ) { + setBoundingRect( + selectControlMenuRef.current.parentElement.getBoundingClientRect() + ); + } + }, [ + selectControlMenuRef.current, + selectControlMenuRef.current?.clientWidth, + ] ); + + // Scroll the selected item into view when the menu opens. + useEffect( () => { + if ( isOpen && scrollIntoViewOnOpen ) { + selectControlMenuRef.current?.scrollIntoView(); + } + }, [ isOpen, scrollIntoViewOnOpen ] ); + + const shouldItemBeExpanded = ( item: LinkedTree ): boolean => { + if ( ! props.createValue || ! item.children?.length ) return false; + return item.children.some( ( child ) => { + if ( + new RegExp( props.createValue || '', 'ig' ).test( + child.data.label + ) + ) { + return true; + } + return shouldItemBeExpanded( child ); + } ); + }; + + /* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ + /* Disabled because of the onmouseup on the ul element below. */ + return ( +
+
+ 0, + } + ) } + position={ position } + animate={ false } + onFocusOutside={ () => { + onClose(); + } } + > + { isOpen && ( +
+ { isLoading ? ( +
+ +
+ ) : ( + + ) } +
+ ) } +
+
+
+ ); + /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ +}; + +export const SelectTreeMenuSlot: React.FC = () => + createPortal( +
+ { /* @ts-expect-error name does exist on PopoverSlot see: https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/popover/index.tsx#L555 */ } + +
, + document.body + ); diff --git a/packages/js/components/src/experimental-select-tree-control/select-tree.tsx b/packages/js/components/src/experimental-select-tree-control/select-tree.tsx index 7406ea9d0f7..5c276342cc8 100644 --- a/packages/js/components/src/experimental-select-tree-control/select-tree.tsx +++ b/packages/js/components/src/experimental-select-tree-control/select-tree.tsx @@ -2,24 +2,20 @@ /** * External dependencies */ -import { createElement, useRef, useState } from 'react'; +import { createElement, useState } from '@wordpress/element'; import classNames from 'classnames'; import { search } from '@wordpress/icons'; -import { Dropdown, Spinner } from '@wordpress/components'; +import { useInstanceId } from '@wordpress/compose'; /** * Internal dependencies */ import { useLinkedTree } from '../experimental-tree-control/hooks/use-linked-tree'; -import { Tree } from '../experimental-tree-control/tree'; -import { - Item, - LinkedTree, - TreeControlProps, -} from '../experimental-tree-control/types'; +import { Item, TreeControlProps } from '../experimental-tree-control/types'; import { SelectedItems } from '../experimental-select-control/selected-items'; import { ComboBox } from '../experimental-select-control/combo-box'; import { SuffixIcon } from '../experimental-select-control/suffix-icon'; +import { SelectTreeMenu } from './select-tree-menu'; interface SelectTreeProps extends TreeControlProps { id: string; @@ -44,156 +40,115 @@ export const SelectTree = function SelectTree( { ...props }: SelectTreeProps ) { const linkedTree = useLinkedTree( items ); + const menuInstanceId = useInstanceId( + SelectTree, + 'woocommerce-select-tree-control__menu' + ); const [ isFocused, setIsFocused ] = useState( false ); - - const comboBoxRef = useRef< HTMLDivElement >( null ); - - // getting the parent's parent div width to set the width of the dropdown - const comboBoxWidth = - comboBoxRef.current?.parentElement?.parentElement?.getBoundingClientRect() - .width; - - const shouldItemBeExpanded = ( item: LinkedTree ): boolean => { - if ( ! props.createValue || ! item.children?.length ) return false; - return item.children.some( ( child ) => { - if ( - new RegExp( props.createValue || '', 'ig' ).test( - child.data.label - ) - ) { - return true; - } - return shouldItemBeExpanded( child ); - } ); - }; + const [ isOpen, setIsOpen ] = useState( false ); return ( - - isLoading ? ( -
- -
- ) : ( - - ) - } - renderToggle={ ( { isOpen, onToggle, onClose } ) => ( -
+
+
- ) } - /> + getSelectedItemProps={ () => ( {} ) } + /> + +
+ setIsOpen( false ) } + /> + ); }; diff --git a/packages/js/components/src/experimental-select-tree-control/stories/index.tsx b/packages/js/components/src/experimental-select-tree-control/stories/index.tsx index 4034c571905..024f1e68e9e 100644 --- a/packages/js/components/src/experimental-select-tree-control/stories/index.tsx +++ b/packages/js/components/src/experimental-select-tree-control/stories/index.tsx @@ -1,14 +1,15 @@ /** * External dependencies */ - -import React, { createElement } from 'react'; +import React, { createElement, useState } from 'react'; +import { Button, Modal, SlotFillProvider } from '@wordpress/components'; /** * Internal dependencies */ import { SelectTree } from '../select-tree'; import { Item } from '../../experimental-tree-control/types'; +import { SelectTreeMenuSlot } from '../select-tree-menu'; const listItems: Item[] = [ { value: '1', label: 'Technology' }, @@ -95,6 +96,72 @@ export const MultipleSelectTree: React.FC = () => { ); }; +export const SingleWithinModalUsingBodyDropdownPlacement: React.FC = () => { + const [ isOpen, setOpen ] = useState( true ); + const [ value, setValue ] = useState( '' ); + const [ selected, setSelected ] = useState< Item[] >( [] ); + + const items = filterItems( listItems, value ); + + return ( + + Selected: { JSON.stringify( selected ) } + + { isOpen && ( + setOpen( false ) } + > + + ! value || + listItems.findIndex( + ( item ) => item.label === typedValue + ) === -1 + } + createValue={ value } + // eslint-disable-next-line no-alert + onCreateNew={ () => alert( 'create new called' ) } + onInputChange={ ( a ) => setValue( a || '' ) } + onSelect={ ( selectedItems ) => { + if ( Array.isArray( selectedItems ) ) { + setSelected( [ + ...selected, + ...selectedItems, + ] ); + } + } } + onRemove={ ( removedItems ) => { + const newValues = Array.isArray( removedItems ) + ? selected.filter( + ( item ) => + ! removedItems.some( + ( { value: removedValue } ) => + item.value === removedValue + ) + ) + : selected.filter( + ( item ) => + item.value !== removedItems.value + ); + setSelected( newValues ); + } } + /> + + ) } + + + ); +}; + export default { title: 'WooCommerce Admin/experimental/SelectTreeControl', component: SelectTree, diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts index 99d39bf3fa9..e971ee0d231 100644 --- a/packages/js/components/src/index.ts +++ b/packages/js/components/src/index.ts @@ -99,7 +99,10 @@ export { TreeControl as __experimentalTreeControl, Item as TreeItemType, } from './experimental-tree-control'; -export { SelectTree as __experimentalSelectTreeControl } from './experimental-select-tree-control'; +export { + SelectTree as __experimentalSelectTreeControl, + SelectTreeMenuSlot as __experimentalSelectTreeMenuSlot, +} from './experimental-select-tree-control'; export { default as TreeSelectControl } from './tree-select-control'; // Exports below can be removed once the @woocommerce/product-editor package is released. diff --git a/packages/js/experimental/changelog/fix-37654-invalid-return-callback-ref b/packages/js/experimental/changelog/fix-37654-invalid-return-callback-ref new file mode 100644 index 00000000000..d14e18f1448 --- /dev/null +++ b/packages/js/experimental/changelog/fix-37654-invalid-return-callback-ref @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix invalid return callback ref warning diff --git a/packages/js/experimental/src/inbox-note/test/inbox-note.tsx b/packages/js/experimental/src/inbox-note/test/inbox-note.tsx index f1078577e04..bbe03171804 100644 --- a/packages/js/experimental/src/inbox-note/test/inbox-note.tsx +++ b/packages/js/experimental/src/inbox-note/test/inbox-note.tsx @@ -23,6 +23,8 @@ jest.mock( 'react-visibility-sensor', () => } ) ); +window.open = jest.fn(); + describe( 'InboxNoteCard', () => { const note = { id: 1, diff --git a/packages/js/experimental/src/inbox-note/test/use-callback-on-link-click.tsx b/packages/js/experimental/src/inbox-note/test/use-callback-on-link-click.tsx index a7c4d334565..2efcb221b98 100644 --- a/packages/js/experimental/src/inbox-note/test/use-callback-on-link-click.tsx +++ b/packages/js/experimental/src/inbox-note/test/use-callback-on-link-click.tsx @@ -42,4 +42,17 @@ describe( 'useCallbackOnLinkClick hook', () => { userEvent.click( getByText( 'Button' ) ); expect( callback ).not.toHaveBeenCalled(); } ); + + it( 'should remove listener on unmount', () => { + const listener = jest.fn(); + const { getByText, unmount } = render( + + ); + const span = getByText( 'Some Text' ); + jest.spyOn( span, 'removeEventListener' ).mockImplementation( + listener + ); + unmount(); + expect( listener ).toHaveBeenCalledTimes( 1 ); + } ); } ); diff --git a/packages/js/experimental/src/inbox-note/use-callback-on-link-click.ts b/packages/js/experimental/src/inbox-note/use-callback-on-link-click.ts index e7243aff958..537c08e0467 100644 --- a/packages/js/experimental/src/inbox-note/use-callback-on-link-click.ts +++ b/packages/js/experimental/src/inbox-note/use-callback-on-link-click.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { useCallback } from '@wordpress/element'; +import { useCallback, useEffect, useRef } from '@wordpress/element'; export function useCallbackOnLinkClick( onClick: ( link: string ) => void ) { const onNodeClick = useCallback( @@ -20,17 +20,21 @@ export function useCallbackOnLinkClick( onClick: ( link: string ) => void ) { [ onClick ] ); - return useCallback( - ( node: HTMLElement ) => { + const nodeRef = useRef< HTMLElement | null >( null ); + + useEffect( () => { + const node = nodeRef.current; + if ( node ) { + node.addEventListener( 'click', onNodeClick ); + } + return () => { if ( node ) { - node.addEventListener( 'click', onNodeClick ); + node.removeEventListener( 'click', onNodeClick ); } - return () => { - if ( node ) { - node.removeEventListener( 'click', onNodeClick ); - } - }; - }, - [ onNodeClick ] - ); + }; + }, [ onNodeClick ] ); + + return useCallback( ( node: HTMLElement ) => { + nodeRef.current = node; + }, [] ); } diff --git a/packages/js/product-editor/changelog/add-inventory-advanced-37401 b/packages/js/product-editor/changelog/add-inventory-advanced-37401 new file mode 100644 index 00000000000..9465dcb1bb7 --- /dev/null +++ b/packages/js/product-editor/changelog/add-inventory-advanced-37401 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adding inventory email, conditional and checkbox blocks. diff --git a/packages/js/product-editor/changelog/add-inventory-sku-block-37399 b/packages/js/product-editor/changelog/add-inventory-sku-block-37399 new file mode 100644 index 00000000000..c6141801e2a --- /dev/null +++ b/packages/js/product-editor/changelog/add-inventory-sku-block-37399 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adding sku block to product editor. diff --git a/packages/js/product-editor/changelog/add-product-link-edit-37610 b/packages/js/product-editor/changelog/add-product-link-edit-37610 new file mode 100644 index 00000000000..ab36038aa75 --- /dev/null +++ b/packages/js/product-editor/changelog/add-product-link-edit-37610 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Refactoring product link modal and adding link to product block editor. diff --git a/packages/js/product-editor/changelog/fix-rest-namespace-blocks-37619 b/packages/js/product-editor/changelog/fix-rest-namespace-blocks-37619 new file mode 100644 index 00000000000..a6315f79155 --- /dev/null +++ b/packages/js/product-editor/changelog/fix-rest-namespace-blocks-37619 @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Adding apifetch middleware to override product api endpoint only for the product editor. diff --git a/packages/js/product-editor/changelog/update-select_tree_dropdown b/packages/js/product-editor/changelog/update-select_tree_dropdown new file mode 100644 index 00000000000..e759cafaa65 --- /dev/null +++ b/packages/js/product-editor/changelog/update-select_tree_dropdown @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix issue with category parent select control clearing search value when typing. diff --git a/packages/js/product-editor/package.json b/packages/js/product-editor/package.json index f508c8d9d77..49b271d990a 100644 --- a/packages/js/product-editor/package.json +++ b/packages/js/product-editor/package.json @@ -41,6 +41,7 @@ "@woocommerce/number": "workspace:*", "@woocommerce/settings": "^1.0.0", "@woocommerce/tracks": "workspace:^1.3.0", + "@wordpress/api-fetch": "wp-6.0", "@wordpress/block-editor": "^9.8.0", "@wordpress/blocks": "^12.3.0", "@wordpress/components": "wp-6.0", diff --git a/packages/js/product-editor/src/blocks/checkbox/block.json b/packages/js/product-editor/src/blocks/checkbox/block.json new file mode 100644 index 00000000000..08b0348a07b --- /dev/null +++ b/packages/js/product-editor/src/blocks/checkbox/block.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "woocommerce/product-checkbox", + "title": "Product checkbox control", + "category": "woocommerce", + "description": "The product checkbox.", + "keywords": [ "products", "checkbox", "input" ], + "textdomain": "default", + "attributes": { + "title": { + "type": "string", + "__experimentalRole": "content" + }, + "label": { + "type": "string" + }, + "property": { + "type": "string" + }, + "tooltip": { + "type": "string" + } + }, + "supports": { + "align": false, + "html": false, + "multiple": true, + "reusable": false, + "inserter": false, + "lock": false, + "__experimentalToolbar": false + }, + "editorStyle": "file:./editor.css" +} diff --git a/packages/js/product-editor/src/blocks/checkbox/edit.tsx b/packages/js/product-editor/src/blocks/checkbox/edit.tsx new file mode 100644 index 00000000000..64655ac5d89 --- /dev/null +++ b/packages/js/product-editor/src/blocks/checkbox/edit.tsx @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import { createElement, createInterpolateElement } from '@wordpress/element'; +import type { BlockAttributes } from '@wordpress/blocks'; +import { CheckboxControl, Tooltip } from '@wordpress/components'; +import { useBlockProps } from '@wordpress/block-editor'; +import { useEntityProp } from '@wordpress/core-data'; +import { Icon, help } from '@wordpress/icons'; + +/** + * Internal dependencies + */ + +export function Edit( { attributes }: { attributes: BlockAttributes } ) { + const blockProps = useBlockProps( { + className: 'woocommerce-product-form__checkbox', + } ); + const { property, title, label, tooltip } = attributes; + const [ value, setValue ] = useEntityProp< boolean >( + 'postType', + 'product', + property + ); + + return ( +
+

{ title }

+ `, { + label: { label }, + tooltip: ( + { tooltip } } + position="top center" + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Incorrect types. + className={ + 'woocommerce-product-form__checkbox-tooltip' + } + delay={ 0 } + > + + + + + ), + } ) + : label + } + checked={ value } + onChange={ ( selected ) => setValue( selected ) } + /> +
+ ); +} diff --git a/packages/js/product-editor/src/blocks/checkbox/editor.scss b/packages/js/product-editor/src/blocks/checkbox/editor.scss new file mode 100644 index 00000000000..472ffb521b5 --- /dev/null +++ b/packages/js/product-editor/src/blocks/checkbox/editor.scss @@ -0,0 +1,24 @@ + +.woocommerce-product-form__checkbox { + + .components-base-control__field { + display: flex; + } + + .components-checkbox-control__label { + display: flex; + } + + &-tooltip-icon { + margin: -2px 0 0 $gap-small; + } +} + +.woocommerce-product-form__checkbox-tooltip { + .components-popover__content { + width: 200px; + min-width: auto; + white-space: normal !important; + } +} + diff --git a/packages/js/product-editor/src/blocks/checkbox/index.ts b/packages/js/product-editor/src/blocks/checkbox/index.ts new file mode 100644 index 00000000000..15144b2b28d --- /dev/null +++ b/packages/js/product-editor/src/blocks/checkbox/index.ts @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import initBlock from '../../utils/init-block'; +import metadata from './block.json'; +import { Edit } from './edit'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/js/product-editor/src/blocks/conditional/block.json b/packages/js/product-editor/src/blocks/conditional/block.json new file mode 100644 index 00000000000..572a2d90c35 --- /dev/null +++ b/packages/js/product-editor/src/blocks/conditional/block.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "woocommerce/conditional", + "title": "Conditional", + "category": "widgets", + "description": "Container to only conditionally render inner blocks.", + "textdomain": "default", + "attributes": { + "mustMatch": { + "__experimentalRole": "content", + "type": "array", + "items": { + "type": "object" + }, + "default": [] + } + }, + "supports": { + "align": false, + "html": false, + "multiple": true, + "reusable": false, + "inserter": false, + "lock": false + } +} diff --git a/packages/js/product-editor/src/blocks/conditional/edit.tsx b/packages/js/product-editor/src/blocks/conditional/edit.tsx new file mode 100644 index 00000000000..61e15444d4d --- /dev/null +++ b/packages/js/product-editor/src/blocks/conditional/edit.tsx @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import type { BlockAttributes } from '@wordpress/blocks'; +import { createElement, useMemo } from '@wordpress/element'; +import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; +import { DisplayState } from '@woocommerce/components'; +import { Product } from '@woocommerce/data'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { useEntityId } from '@wordpress/core-data'; + +export function Edit( { + attributes, +}: { + attributes: BlockAttributes & { + mustMatch: Record< string, Array< string > >; + }; +} ) { + const blockProps = useBlockProps(); + const { mustMatch } = attributes; + + const productId = useEntityId( 'postType', 'product' ); + const product: Product = useSelect( ( select ) => + select( 'core' ).getEditedEntityRecord( + 'postType', + 'product', + productId + ) + ); + + const displayBlocks = useMemo( () => { + for ( const [ prop, values ] of Object.entries( mustMatch ) ) { + if ( ! values.includes( product[ prop ] ) ) { + return false; + } + } + return true; + }, [ mustMatch, product ] ); + + return ( +
+ + + +
+ ); +} diff --git a/packages/js/product-editor/src/blocks/conditional/index.ts b/packages/js/product-editor/src/blocks/conditional/index.ts new file mode 100644 index 00000000000..43b3aabdc22 --- /dev/null +++ b/packages/js/product-editor/src/blocks/conditional/index.ts @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import { initBlock } from '../../utils'; +import metadata from './block.json'; +import { Edit } from './edit'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => + initBlock( { name, metadata: metadata as never, settings } ); diff --git a/packages/js/product-editor/src/blocks/inventory-email/block.json b/packages/js/product-editor/src/blocks/inventory-email/block.json new file mode 100644 index 00000000000..749846acb5a --- /dev/null +++ b/packages/js/product-editor/src/blocks/inventory-email/block.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "woocommerce/product-inventory-email", + "title": "Stock level threshold", + "category": "widgets", + "description": "Stock management minimum quantity.", + "keywords": [ "products", "inventory", "email", "minimum" ], + "textdomain": "default", + "attributes": { + "name": { + "type": "string", + "__experimentalRole": "content" + } + }, + "supports": { + "align": false, + "html": false, + "multiple": false, + "reusable": false, + "inserter": false, + "lock": false + }, + "editorStyle": "file:./editor.css" +} diff --git a/packages/js/product-editor/src/blocks/inventory-email/edit.tsx b/packages/js/product-editor/src/blocks/inventory-email/edit.tsx new file mode 100644 index 00000000000..44af82442d1 --- /dev/null +++ b/packages/js/product-editor/src/blocks/inventory-email/edit.tsx @@ -0,0 +1,85 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Link } from '@woocommerce/components'; + +import { + createElement, + Fragment, + createInterpolateElement, +} from '@wordpress/element'; +import { getSetting } from '@woocommerce/settings'; + +import { useBlockProps } from '@wordpress/block-editor'; + +import { + BaseControl, + // @ts-expect-error `__experimentalInputControl` does exist. + __experimentalInputControl as InputControl, +} from '@wordpress/components'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { useEntityProp } from '@wordpress/core-data'; + +export function Edit() { + const blockProps = useBlockProps( { + className: 'woocommerce-product-form__inventory-email', + } ); + const notifyLowStockAmount = getSetting( 'notifyLowStockAmount', 2 ); + + const [ lowStockAmount, setLowStockAmount ] = useEntityProp( + 'postType', + 'product', + 'low_stock_amount' + ); + + return ( + <> +
+
+
+ store settings.', + 'woocommerce' + ), + { + link: ( + + ), + } + ) } + > + + +
+
+
+
+ + ); +} diff --git a/packages/js/product-editor/src/blocks/inventory-email/index.ts b/packages/js/product-editor/src/blocks/inventory-email/index.ts new file mode 100644 index 00000000000..59d35906a12 --- /dev/null +++ b/packages/js/product-editor/src/blocks/inventory-email/index.ts @@ -0,0 +1,22 @@ +/** + * Internal dependencies + */ +import { initBlock } from '../../utils'; +import metadata from './block.json'; +import { Edit } from './edit'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => + initBlock( { + name, + metadata: metadata as never, + settings, + } ); diff --git a/packages/js/product-editor/src/blocks/inventory-email/style.scss b/packages/js/product-editor/src/blocks/inventory-email/style.scss new file mode 100644 index 00000000000..893e1b68016 --- /dev/null +++ b/packages/js/product-editor/src/blocks/inventory-email/style.scss @@ -0,0 +1,3 @@ +.woocommerce-product-form__inventory-email { + margin-top: $gap-large; +} diff --git a/packages/js/product-editor/src/blocks/inventory-sku/block.json b/packages/js/product-editor/src/blocks/inventory-sku/block.json new file mode 100644 index 00000000000..335c9de9858 --- /dev/null +++ b/packages/js/product-editor/src/blocks/inventory-sku/block.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "woocommerce/product-sku", + "title": "Product text control", + "category": "woocommerce", + "description": "The product sku.", + "keywords": [ "products", "sku" ], + "textdomain": "default", + "attributes": { + "name": { + "type": "string", + "__experimentalRole": "content" + } + }, + "supports": { + "align": false, + "html": false, + "multiple": true, + "reusable": false, + "inserter": false, + "lock": false, + "__experimentalToolbar": false + }, + "editorStyle": "file:./editor.css" +} diff --git a/packages/js/product-editor/src/blocks/inventory-sku/edit.tsx b/packages/js/product-editor/src/blocks/inventory-sku/edit.tsx new file mode 100644 index 00000000000..9c4519a986c --- /dev/null +++ b/packages/js/product-editor/src/blocks/inventory-sku/edit.tsx @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { createElement, createInterpolateElement } from '@wordpress/element'; +import { useBlockProps } from '@wordpress/block-editor'; + +import { + BaseControl, + // @ts-expect-error `__experimentalInputControl` does exist. + __experimentalInputControl as InputControl, +} from '@wordpress/components'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { useEntityProp } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ + +export function Edit() { + const blockProps = useBlockProps(); + + const [ sku, setSku ] = useEntityProp( 'postType', 'product', 'sku' ); + + return ( +
+ ', 'woocommerce' ), + { + description: ( + + { __( '(STOCK KEEPING UNIT)', 'woocommerce' ) } + + ), + } + ) } + > + + +
+ ); +} diff --git a/packages/js/product-editor/src/blocks/inventory-sku/index.ts b/packages/js/product-editor/src/blocks/inventory-sku/index.ts new file mode 100644 index 00000000000..15144b2b28d --- /dev/null +++ b/packages/js/product-editor/src/blocks/inventory-sku/index.ts @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import initBlock from '../../utils/init-block'; +import metadata from './block.json'; +import { Edit } from './edit'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/js/product-editor/src/blocks/inventory-sku/style.scss b/packages/js/product-editor/src/blocks/inventory-sku/style.scss new file mode 100644 index 00000000000..32372ce5b82 --- /dev/null +++ b/packages/js/product-editor/src/blocks/inventory-sku/style.scss @@ -0,0 +1,13 @@ + +.woocommerce-product-form_inventory-sku { + .components-base-control__label { + display: flex; + align-items: center; + } + .woocommerce-product-form__optional-input { + margin-left: $gap-smallest; + } + .woocommerce-tooltip__button { + padding: 0 0 0 $gap-smallest; + } +} diff --git a/packages/js/product-editor/src/components/details-categories-field/create-category-modal.tsx b/packages/js/product-editor/src/components/details-categories-field/create-category-modal.tsx index 0b9d2ae3b31..eefec178245 100644 --- a/packages/js/product-editor/src/components/details-categories-field/create-category-modal.tsx +++ b/packages/js/product-editor/src/components/details-categories-field/create-category-modal.tsx @@ -32,6 +32,15 @@ type CreateCategoryModalProps = { onCreate: ( newCategory: ProductCategory ) => void; }; +function getCategoryItemLabel( item: ProductCategoryNode | null ): string { + return item?.name || ''; +} +function getCategoryItemValue( + item: ProductCategoryNode | null +): string | number { + return item?.id || ''; +} + export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( { initialCategoryName, onCancel, @@ -109,8 +118,8 @@ export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( { onRemove={ () => setCategoryParent( null ) } onInputChange={ debouncedSearch } getFilteredItems={ getFilteredItems } - getItemLabel={ ( item ) => item?.name || '' } - getItemValue={ ( item ) => item?.id || '' } + getItemLabel={ getCategoryItemLabel } + getItemValue={ getCategoryItemValue } > { ( { items, diff --git a/packages/js/product-editor/src/components/details-name-block/edit.tsx b/packages/js/product-editor/src/components/details-name-block/edit.tsx index ea38e9a0703..a06c2de2d9a 100644 --- a/packages/js/product-editor/src/components/details-name-block/edit.tsx +++ b/packages/js/product-editor/src/components/details-name-block/edit.tsx @@ -2,36 +2,179 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { createElement, createInterpolateElement } from '@wordpress/element'; -import { TextControl } from '@woocommerce/components'; +import { + createElement, + Fragment, + createInterpolateElement, + useState, +} from '@wordpress/element'; + import { useBlockProps } from '@wordpress/block-editor'; +import { cleanForSlug } from '@wordpress/url'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { + PRODUCTS_STORE_NAME, + WCDataSelector, + Product, +} from '@woocommerce/data'; +import { + Button, + BaseControl, + // @ts-expect-error `__experimentalInputControl` does exist. + __experimentalInputControl as InputControl, +} from '@wordpress/components'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore No types for this exist yet. // eslint-disable-next-line @woocommerce/dependency-group -import { useEntityProp } from '@wordpress/core-data'; +import { useEntityProp, useEntityId } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { AUTO_DRAFT_NAME } from '../../utils'; +import { EditProductLinkModal } from '../edit-product-link-modal'; + +import { useValidation } from '../../hooks/use-validation'; export function Edit() { const blockProps = useBlockProps(); - const [ name, setName ] = useEntityProp( 'postType', 'product', 'name' ); + + const { editEntityRecord, saveEntityRecord } = useDispatch( 'core' ); + + const [ showProductLinkEditModal, setShowProductLinkEditModal ] = + useState( false ); + + const productId = useEntityId( 'postType', 'product' ); + const product: Product = useSelect( ( select ) => + select( 'core' ).getEditedEntityRecord( + 'postType', + 'product', + productId + ) + ); + + const [ sku, setSku ] = useEntityProp( 'postType', 'product', 'sku' ); + const [ name, setName ] = useEntityProp< string >( + 'postType', + 'product', + 'name' + ); + + const { permalinkPrefix, permalinkSuffix } = useSelect( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ( select: WCDataSelector ) => { + const { getPermalinkParts } = select( PRODUCTS_STORE_NAME ); + if ( productId ) { + const parts = getPermalinkParts( productId ); + return { + permalinkPrefix: parts?.prefix, + permalinkSuffix: parts?.suffix, + }; + } + return {}; + } + ); + + const nameIsValid = useValidation( + 'product/name', + () => Boolean( name ) && name !== AUTO_DRAFT_NAME + ); + + const setSkuIfEmpty = () => { + if ( sku || ! nameIsValid ) { + return; + } + setSku( cleanForSlug( name ) ); + }; return ( -
- ', 'woocommerce' ), - { - required: ( - - { __( '(required)', 'woocommerce' ) } - - ), - } + <> +
+ ', 'woocommerce' ), + { + required: ( + + { __( '*', 'woocommerce' ) } + + ), + } + ) } + > + + + { productId && + nameIsValid && + [ 'publish', 'draft' ].includes( product.status ) && + permalinkPrefix && ( + + { __( 'Product link', 'woocommerce' ) } + :  + + { permalinkPrefix } + { product.slug || cleanForSlug( name ) } + { permalinkSuffix } + + + + ) } + { showProductLinkEditModal && ( + setShowProductLinkEditModal( false ) } + onSaved={ () => setShowProductLinkEditModal( false ) } + saveHandler={ async ( updatedSlug ) => { + const { slug, permalink }: Product = + await saveEntityRecord( 'postType', 'product', { + id: product.id, + slug: updatedSlug, + } ); + + if ( slug && permalink ) { + editEntityRecord( + 'postType', + 'product', + product.id, + { + slug, + permalink, + } + ); + + return { + slug, + permalink, + }; + } + } } + /> ) } - name={ 'woocommerce-product-name' } - placeholder={ __( 'e.g. 12 oz Coffee Mug', 'woocommerce' ) } - onChange={ setName } - value={ name || '' } - /> -
+
+ ); } diff --git a/packages/js/product-editor/src/components/details-name-block/style.scss b/packages/js/product-editor/src/components/details-name-block/style.scss new file mode 100644 index 00000000000..b6e6e66c221 --- /dev/null +++ b/packages/js/product-editor/src/components/details-name-block/style.scss @@ -0,0 +1,25 @@ +.product-details-section { + + &__product-link { + color: #757575; + font-size: 12px; + display: block; + margin-top: $gap-smaller; + + > a { + color: inherit; + text-decoration: none; + font-weight: 600; + } + + .components-button.is-link { + font-size: 12px; + text-decoration: none; + margin-left: $gap-smaller; + } + } +} + +.woocommerce-product-form__required-input { + color: #CC1818; +} diff --git a/packages/js/product-editor/src/components/details-name-field/details-name-field.tsx b/packages/js/product-editor/src/components/details-name-field/details-name-field.tsx index 498c1f016e6..b964a445da3 100644 --- a/packages/js/product-editor/src/components/details-name-field/details-name-field.tsx +++ b/packages/js/product-editor/src/components/details-name-field/details-name-field.tsx @@ -22,11 +22,13 @@ import { */ import { PRODUCT_DETAILS_SLUG } from '../../constants'; import { EditProductLinkModal } from '../edit-product-link-modal'; +import { useProductHelper } from '../../hooks/use-product-helper'; export const DetailsNameField = ( {} ) => { + const { updateProductWithStatus } = useProductHelper(); const [ showProductLinkEditModal, setShowProductLinkEditModal ] = useState( false ); - const { getInputProps, values, touched, errors, setValue } = + const { getInputProps, values, touched, errors, setValue, resetForm } = useFormContext< Product >(); const { permalinkPrefix, permalinkSuffix } = useSelect( @@ -102,6 +104,33 @@ export const DetailsNameField = ( {} ) => { product={ values } onCancel={ () => setShowProductLinkEditModal( false ) } onSaved={ () => setShowProductLinkEditModal( false ) } + saveHandler={ async ( slug ) => { + const updatedProduct = await updateProductWithStatus( + values.id, + { + slug, + }, + values.status, + true + ); + if ( updatedProduct && updatedProduct.id ) { + // only reset the updated slug and permalink fields. + resetForm( + { + ...values, + slug: updatedProduct.slug, + permalink: updatedProduct.permalink, + }, + touched, + errors + ); + + return { + slug: updatedProduct.slug, + permalink: updatedProduct.permalink, + }; + } + } } /> ) } diff --git a/packages/js/product-editor/src/components/edit-product-link-modal/edit-product-link-modal.tsx b/packages/js/product-editor/src/components/edit-product-link-modal/edit-product-link-modal.tsx index fab86bce6f5..9f1170e0937 100644 --- a/packages/js/product-editor/src/components/edit-product-link-modal/edit-product-link-modal.tsx +++ b/packages/js/product-editor/src/components/edit-product-link-modal/edit-product-link-modal.tsx @@ -11,20 +11,17 @@ import { import { useDispatch } from '@wordpress/data'; import { cleanForSlug } from '@wordpress/url'; import { Product } from '@woocommerce/data'; -import { useFormContext } from '@woocommerce/components'; import { recordEvent } from '@woocommerce/tracks'; -/** - * Internal dependencies - */ -import { useProductHelper } from '../../hooks/use-product-helper'; - type EditProductLinkModalProps = { product: Product; permalinkPrefix: string; permalinkSuffix: string; onCancel: () => void; onSaved: () => void; + saveHandler: ( + slug: string + ) => Promise< { slug: string; permalink: string } | undefined >; }; export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( { @@ -33,14 +30,13 @@ export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( { permalinkSuffix, onCancel, onSaved, + saveHandler, } ) => { const { createNotice } = useDispatch( 'core/notices' ); - const { updateProductWithStatus, isUpdatingDraft, isUpdatingPublished } = - useProductHelper(); + const [ isSaving, setIsSaving ] = useState< boolean >( false ); const [ slug, setSlug ] = useState( product.slug || cleanForSlug( product.name ) ); - const { resetForm, touched, errors } = useFormContext< Product >(); const onSave = async () => { recordEvent( 'product_update_slug', { @@ -48,35 +44,19 @@ export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( { product_id: product.id, product_type: product.type, } ); - const updatedProduct = await updateProductWithStatus( - product.id, - { - slug, - }, - product.status, - true - ); - if ( updatedProduct && updatedProduct.id ) { - // only reset the updated slug and permalink fields. - resetForm( - { - ...product, - slug: updatedProduct.slug, - permalink: updatedProduct.permalink, - }, - touched, - errors - ); + + const { slug: updatedSlug, permalink: updatedPermalink } = + ( await saveHandler( slug ) ) ?? {}; + + if ( updatedSlug ) { createNotice( - updatedProduct.slug === cleanForSlug( slug ) - ? 'success' - : 'info', - updatedProduct.slug === cleanForSlug( slug ) + updatedSlug === cleanForSlug( slug ) ? 'success' : 'info', + updatedSlug === cleanForSlug( slug ) ? __( 'Product link successfully updated.', 'woocommerce' ) : __( 'Product link already existed, updated to ', 'woocommerce' - ) + updatedProduct.permalink + ) + updatedPermalink ); } else { createNotice( @@ -122,14 +102,12 @@ export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( {