-

+

From 3dd6a4037bb076d60156ee1ccdc97c9a768f64e8 Mon Sep 17 00:00:00 2001 From: Niels Lange Date: Wed, 31 Jul 2024 15:17:21 +0200 Subject: [PATCH 008/587] Display address card for virtual products if shopper's address is known (#50127) * Display address card for virtual products * Remove obsolete dependency * Optimise dependencies * Add changefile(s) from automation for the following project(s): woocommerce-blocks * Adjust core e2e tests --------- Co-authored-by: github-actions --- .../js/blocks/checkout/address-card/index.tsx | 6 +++- .../checkout-billing-address-block/block.tsx | 4 ++- .../assets/js/blocks/checkout/test/block.js | 34 +++++++++++++++++-- ...-display-address-card-for-virtual-products | 4 +++ .../tests/shopper/checkout-block.spec.js | 6 ++-- 5 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 plugins/woocommerce/changelog/50127-fix-47557-display-address-card-for-virtual-products diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/index.tsx index e88ab5efe6a..c6b97fe1e51 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/index.tsx @@ -52,6 +52,10 @@ const AddressCard = ( { address, formatToUse ); + const label = + target === 'shipping' + ? __( 'Edit shipping address', 'woocommerce' ) + : __( 'Edit billing address', 'woocommerce' ); return (
@@ -82,7 +86,7 @@ const AddressCard = ( { className="wc-block-components-address-card__edit" aria-controls={ target } aria-expanded={ isExpanded } - aria-label={ __( 'Edit address', 'woocommerce' ) } + aria-label={ label } onClick={ ( e ) => { e.preventDefault(); onEdit(); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx index c59bc46022c..bcdcbac3e69 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx @@ -7,6 +7,7 @@ import { useCheckoutAddress, useEditorContext, noticeContexts, + useShippingData, } from '@woocommerce/base-context'; import Noninteractive from '@woocommerce/base-components/noninteractive'; import type { ShippingAddress, FormFieldsConfig } from '@woocommerce/settings'; @@ -42,6 +43,7 @@ const Block = ( { useBillingAsShipping, } = useCheckoutAddress(); const { isEditor } = useEditorContext(); + const { needsShipping } = useShippingData(); // Syncs shipping address with billing address if "Force shipping to the customer billing address" is enabled. useEffectOnce( () => { @@ -110,7 +112,7 @@ const Block = ( { shippingAddress ); const defaultEditingAddress = - isEditor || ! hasAddress || billingMatchesShipping; + isEditor || ! hasAddress || ( needsShipping && billingMatchesShipping ); return ( <> diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/test/block.js b/plugins/woocommerce-blocks/assets/js/blocks/checkout/test/block.js index 88a366d2294..bb4e72e4564 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/test/block.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/test/block.js @@ -98,7 +98,11 @@ const CheckoutBlock = () => { - + @@ -148,7 +152,7 @@ describe( 'Testing Checkout', () => { expect( fetchMock ).toHaveBeenCalledTimes( 1 ); } ); - it( 'Renders the address card if the address is filled', async () => { + it( 'Renders the shipping address card if the address is filled and the cart contains a shippable product', async () => { act( () => { const cartWithAddress = { ...previewCart, @@ -190,7 +194,7 @@ describe( 'Testing Checkout', () => { await waitFor( () => expect( fetchMock ).toHaveBeenCalled() ); expect( - screen.getByRole( 'button', { name: 'Edit address' } ) + screen.getByRole( 'button', { name: 'Edit shipping address' } ) ).toBeInTheDocument(); expect( @@ -258,6 +262,30 @@ describe( 'Testing Checkout', () => { expect( fetchMock ).toHaveBeenCalledTimes( 1 ); } ); + it( 'Renders the billing address card if the address is filled and the cart contains a virtual product', async () => { + act( () => { + const cartWithVirtualProduct = { + ...previewCart, + needs_shipping: false, + }; + fetchMock.mockResponse( ( req ) => { + if ( req.url.match( /wc\/store\/v1\/cart/ ) ) { + return Promise.resolve( + JSON.stringify( cartWithVirtualProduct ) + ); + } + return Promise.resolve( '' ); + } ); + } ); + render( ); + + await waitFor( () => expect( fetchMock ).toHaveBeenCalled() ); + + expect( + screen.getByRole( 'button', { name: 'Edit billing address' } ) + ).toBeInTheDocument(); + } ); + it( 'Ensures checkbox labels have unique IDs', async () => { await act( async () => { // Set required settings diff --git a/plugins/woocommerce/changelog/50127-fix-47557-display-address-card-for-virtual-products b/plugins/woocommerce/changelog/50127-fix-47557-display-address-card-for-virtual-products new file mode 100644 index 00000000000..eabec756bb8 --- /dev/null +++ b/plugins/woocommerce/changelog/50127-fix-47557-display-address-card-for-virtual-products @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Display address card for virtual products if shopper's address is known \ No newline at end of file diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block.spec.js index 26b7e7f4b22..24ce0cb50f6 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block.spec.js @@ -440,7 +440,7 @@ test.describe( // verify shipping details await page - .getByLabel( 'Edit address', { exact: true } ) + .getByLabel( 'Edit shipping address', { exact: true } ) .first() .click(); await expect( @@ -471,7 +471,7 @@ test.describe( // verify billing details await page - .getByLabel( 'Edit address', { exact: true } ) + .getByLabel( 'Edit billing address', { exact: true } ) .last() .click(); await expect( @@ -782,7 +782,7 @@ test.describe( ).toBeVisible(); await page - .getByLabel( 'Edit address', { exact: true } ) + .getByLabel( 'Edit shipping address', { exact: true } ) .first() .click(); From d3bd80fc61554169e3811aa5e4658f817b22c622 Mon Sep 17 00:00:00 2001 From: Nathan Silveira Date: Wed, 31 Jul 2024 11:30:32 -0300 Subject: [PATCH 009/587] SelectTree: keep the focus on the input while navigating between menu items (#49989) * Commit at a functional state * Change role to 'listbox' * Add --highlighted class rules * Fix overflow in create category modal * Add countNumberOfItems Fix multiple bugs Refactor Rename and move use-linked-tree file to linked-tree-utils * Add comments * Escape regExp * Allow to select/remove with the enter key * Add changelogs * Fix unit tests * Fix bug on css selector, since role was changed * Fix bug in index calculation and handle focus on checkboxes and expander button correctly * Only add activedescendant when something is highlighted preventDefault when pressing arrowUp * Fix bug: items array was being used instead of using linked tree * Call onSelect when pressing enter * Add guards to prevent tests breaking * Add additional tests for SelectTree * Add comments and rename some functions in linked-tree-utils --- .../update-navigation-while-on-input | 4 + .../selected-items.tsx | 43 ++-- .../select-tree-menu.tsx | 13 +- .../select-tree.tsx | 198 +++++++++++++--- .../test/select-tree.test.tsx | 85 ++++++- .../hooks/use-linked-tree.ts | 50 ----- .../hooks/use-tree-item.ts | 12 +- .../hooks/use-tree.ts | 4 +- .../linked-tree-utils.ts | 211 ++++++++++++++++++ .../tree-control.tsx | 4 +- .../experimental-tree-control/tree-item.scss | 2 + .../experimental-tree-control/tree-item.tsx | 45 ++-- .../src/experimental-tree-control/tree.scss | 5 +- .../src/experimental-tree-control/tree.tsx | 24 +- .../src/experimental-tree-control/types.ts | 13 +- .../update-navigation-while-on-input | 4 + .../src/blocks/generic/taxonomy/editor.scss | 5 +- 17 files changed, 585 insertions(+), 137 deletions(-) create mode 100644 packages/js/components/changelog/update-navigation-while-on-input delete mode 100644 packages/js/components/src/experimental-tree-control/hooks/use-linked-tree.ts create mode 100644 packages/js/components/src/experimental-tree-control/linked-tree-utils.ts create mode 100644 packages/js/product-editor/changelog/update-navigation-while-on-input diff --git a/packages/js/components/changelog/update-navigation-while-on-input b/packages/js/components/changelog/update-navigation-while-on-input new file mode 100644 index 00000000000..169cc6b8f5e --- /dev/null +++ b/packages/js/components/changelog/update-navigation-while-on-input @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update SelectTree and Tree controls to allow highlighting items without focus diff --git a/packages/js/components/src/experimental-select-control/selected-items.tsx b/packages/js/components/src/experimental-select-control/selected-items.tsx index 99056a6d2ca..acdff11f0be 100644 --- a/packages/js/components/src/experimental-select-control/selected-items.tsx +++ b/packages/js/components/src/experimental-select-control/selected-items.tsx @@ -77,6 +77,25 @@ const PrivateSelectedItems = < ItemType, >( ); } + const focusSibling = ( event: React.KeyboardEvent< HTMLDivElement > ) => { + const selectedItem = ( event.target as HTMLElement ).closest( + '.woocommerce-experimental-select-control__selected-item' + ); + const sibling = + event.key === 'ArrowLeft' || event.key === 'Backspace' + ? selectedItem?.previousSibling + : selectedItem?.nextSibling; + if ( sibling ) { + ( + ( sibling as HTMLElement ).querySelector( + '.woocommerce-tag__remove' + ) as HTMLElement + )?.focus(); + return true; + } + return false; + }; + return (
{ items.map( ( item, index ) => { @@ -102,24 +121,9 @@ const PrivateSelectedItems = < ItemType, >( event.key === 'ArrowLeft' || event.key === 'ArrowRight' ) { - const selectedItem = ( - event.target as HTMLElement - ).closest( - '.woocommerce-experimental-select-control__selected-item' - ); - const sibling = - event.key === 'ArrowLeft' - ? selectedItem?.previousSibling - : selectedItem?.nextSibling; - if ( sibling ) { - ( - ( - sibling as HTMLElement - ).querySelector( - '.woocommerce-tag__remove' - ) as HTMLElement - )?.focus(); - } else if ( + const focused = focusSibling( event ); + if ( + ! focused && event.key === 'ArrowRight' && onSelectedItemsEnd ) { @@ -130,6 +134,9 @@ const PrivateSelectedItems = < ItemType, >( event.key === 'ArrowDown' ) { event.preventDefault(); // prevent unwanted scroll + } else if ( event.key === 'Backspace' ) { + onRemove( item ); + focusSibling( event ); } } } onBlur={ onBlur } 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 index 1b7267389b4..b1c9dd4d1ba 100644 --- 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 @@ -10,6 +10,7 @@ import { useLayoutEffect, useState, } from '@wordpress/element'; +import { escapeRegExp } from 'lodash'; /** * Internal dependencies @@ -26,6 +27,7 @@ type MenuProps = { isLoading?: boolean; position?: Popover.Position; scrollIntoViewOnOpen?: boolean; + highlightedIndex?: number; items: LinkedTree[]; treeRef?: React.ForwardedRef< HTMLOListElement >; onClose?: () => void; @@ -44,6 +46,7 @@ export const SelectTreeMenu = ( { onEscape, shouldShowCreateButton, onFirstItemLoop, + onExpand, ...props }: MenuProps ) => { const [ boundingRect, setBoundingRect ] = useState< DOMRect >(); @@ -66,7 +69,7 @@ export const SelectTreeMenu = ( { // Scroll the selected item into view when the menu opens. useEffect( () => { if ( isOpen && scrollIntoViewOnOpen ) { - selectControlMenuRef.current?.scrollIntoView(); + selectControlMenuRef.current?.scrollIntoView?.(); } }, [ isOpen, scrollIntoViewOnOpen ] ); @@ -74,9 +77,10 @@ export const SelectTreeMenu = ( { if ( ! props.createValue || ! item.children?.length ) return false; return item.children.some( ( child ) => { if ( - new RegExp( props.createValue || '', 'ig' ).test( - child.data.label - ) + new RegExp( + escapeRegExp( props.createValue || '' ), + 'ig' + ).test( child.data.label ) ) { return true; } @@ -130,6 +134,7 @@ export const SelectTreeMenu = ( { ref={ ref } items={ items } onTreeBlur={ onClose } + onExpand={ onExpand } shouldItemBeExpanded={ shouldItemBeExpanded } 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 bb08478484a..ed5d7b09b06 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 @@ -19,8 +19,17 @@ import { speak } from '@wordpress/a11y'; /** * Internal dependencies */ -import { useLinkedTree } from '../experimental-tree-control/hooks/use-linked-tree'; -import { Item, TreeControlProps } from '../experimental-tree-control/types'; +import { + toggleNode, + createLinkedTree, + getVisibleNodeIndex as getVisibleNodeIndex, + getNodeDataByIndex, +} from '../experimental-tree-control/linked-tree-utils'; +import { + Item, + LinkedTree, + 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'; @@ -55,7 +64,17 @@ export const SelectTree = function SelectTree( { onClear = () => {}, ...props }: SelectTreeProps ) { - const linkedTree = useLinkedTree( items ); + const [ linkedTree, setLinkedTree ] = useState< LinkedTree[] >( [] ); + const [ highlightedIndex, setHighlightedIndex ] = useState( -1 ); + + // whenever the items change, the linked tree needs to be recalculated + useEffect( () => { + setLinkedTree( createLinkedTree( items, props.createValue ) ); + }, [ items.length ] ); + + // reset highlighted index when the input value changes + useEffect( () => setHighlightedIndex( -1 ), [ props.createValue ] ); + const selectTreeInstanceId = useInstanceId( SelectTree, 'woocommerce-experimental-select-tree-control__dropdown' @@ -111,6 +130,19 @@ export const SelectTree = function SelectTree( { } }, [ isFocused ] ); + // Scroll the newly highlighted item into view + useEffect( + () => + document + .querySelector( + '.experimental-woocommerce-tree-item--highlighted' + ) + ?.scrollIntoView?.( { + block: 'nearest', + } ), + [ highlightedIndex ] + ); + let placeholder: string | undefined = ''; if ( Array.isArray( props.selected ) ) { placeholder = props.selected.length === 0 ? props.placeholder : ''; @@ -118,12 +150,30 @@ export const SelectTree = function SelectTree( { placeholder = props.placeholder; } + // reset highlighted index when the input value changes + useEffect( () => { + if ( + highlightedIndex === items.length && + ! shouldShowCreateButton?.( props.createValue ) + ) { + setHighlightedIndex( items.length - 1 ); + } + }, [ props.createValue ] ); + const inputProps: React.InputHTMLAttributes< HTMLInputElement > = { className: 'woocommerce-experimental-select-control__input', id: `${ props.id }-input`, 'aria-autocomplete': 'list', - 'aria-controls': `${ props.id }-menu`, + 'aria-activedescendant': + highlightedIndex >= 0 + ? `woocommerce-experimental-tree-control__menu-item-${ highlightedIndex }` + : undefined, + 'aria-controls': menuInstanceId, + 'aria-owns': menuInstanceId, + role: 'combobox', autoComplete: 'off', + 'aria-expanded': isOpen, + 'aria-haspopup': 'tree', disabled, onFocus: ( event ) => { if ( props.multiple ) { @@ -159,40 +209,121 @@ export const SelectTree = function SelectTree( { 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 ( + // is advancing from the last menu item to the create button + highlightedIndex === items.length - 1 && + shouldShowCreateButton?.( props.createValue ) + ) { + setHighlightedIndex( items.length ); + } else { + const visibleNodeIndex = getVisibleNodeIndex( + linkedTree, + Math.min( highlightedIndex + 1, items.length ), + 'down' + ); + if ( visibleNodeIndex !== undefined ) { + setHighlightedIndex( visibleNodeIndex ); + } + } + } else if ( event.key === 'ArrowUp' ) { + event.preventDefault(); + if ( highlightedIndex > 0 ) { + const visibleNodeIndex = getVisibleNodeIndex( + linkedTree, + Math.max( highlightedIndex - 1, -1 ), + 'up' + ); + if ( visibleNodeIndex !== undefined ) { + setHighlightedIndex( visibleNodeIndex ); + } + } else { + setHighlightedIndex( -1 ); + } } else if ( event.key === 'Tab' || event.key === 'Escape' ) { setIsOpen( false ); recalculateInputValue(); - } else if ( event.key === ',' || event.key === 'Enter' ) { + } else if ( event.key === 'Enter' || event.key === ',' ) { event.preventDefault(); - const item = items.find( - ( i ) => i.label === escapeHTML( inputValue ) - ); - const isAlreadySelected = - Array.isArray( props.selected ) && - Boolean( - props.selected.find( - ( i ) => i.label === escapeHTML( inputValue ) - ) + if ( + highlightedIndex === items.length && + shouldShowCreateButton + ) { + props.onCreateNew?.(); + } else if ( + // is selecting an item + highlightedIndex !== -1 + ) { + const nodeData = getNodeDataByIndex( + linkedTree, + highlightedIndex ); - if ( props.onSelect && item && ! isAlreadySelected ) { - props.onSelect( item ); - setInputValue( '' ); - recalculateInputValue(); + if ( ! nodeData ) { + return; + } + if ( props.multiple && Array.isArray( props.selected ) ) { + if ( + ! Boolean( + props.selected.find( + ( i ) => i.label === nodeData.label + ) + ) + ) { + if ( props.onSelect ) { + props.onSelect( nodeData ); + } + } else if ( props.onRemove ) { + props.onRemove( nodeData ); + } + setInputValue( '' ); + } else { + onInputChange?.( nodeData.label ); + props.onSelect?.( nodeData ); + setIsOpen( false ); + setIsFocused( false ); + focusOnInput(); + } + } else if ( inputValue ) { + // no highlighted item, but there is an input value, check if it matches any item + + const item = items.find( + ( i ) => i.label === escapeHTML( inputValue ) + ); + const isAlreadySelected = Array.isArray( props.selected ) + ? Boolean( + props.selected.find( + ( i ) => + i.label === escapeHTML( inputValue ) + ) + ) + : props.selected?.label === escapeHTML( inputValue ); + if ( item && ! isAlreadySelected ) { + props.onSelect?.( item ); + setInputValue( '' ); + recalculateInputValue(); + } } } else if ( - ( event.key === 'ArrowLeft' || event.key === 'Backspace' ) && + event.key === 'Backspace' && // test if the cursor is at the beginning of the input with nothing selected ( event.target as HTMLInputElement ).selectionStart === 0 && ( event.target as HTMLInputElement ).selectionEnd === 0 && selectedItemsFocusHandle.current ) { selectedItemsFocusHandle.current(); + } else if ( event.key === 'ArrowRight' ) { + setLinkedTree( + toggleNode( linkedTree, highlightedIndex, true ) + ); + } else if ( event.key === 'ArrowLeft' ) { + setLinkedTree( + toggleNode( linkedTree, highlightedIndex, false ) + ); + } else if ( event.key === 'Home' ) { + event.preventDefault(); + setHighlightedIndex( 0 ); + } else if ( event.key === 'End' ) { + event.preventDefault(); + setHighlightedIndex( items.length - 1 ); } }, onChange: ( event ) => { @@ -248,10 +379,6 @@ export const SelectTree = function SelectTree( { comboBoxProps={ { className: 'woocommerce-experimental-select-control__combo-box-wrapper', - role: 'combobox', - 'aria-expanded': isOpen, - 'aria-haspopup': 'tree', - 'aria-owns': `${ props.id }-menu`, } } inputProps={ inputProps } suffix={ @@ -281,7 +408,11 @@ export const SelectTree = function SelectTree( { item?.label || '' } @@ -290,6 +421,7 @@ export const SelectTree = function SelectTree( { } onRemove={ ( item ) => { if ( + item && ! Array.isArray( item ) && props.onRemove ) { @@ -346,6 +478,12 @@ export const SelectTree = function SelectTree( { isEventOutside={ isEventOutside } isLoading={ isLoading } isOpen={ isOpen } + highlightedIndex={ highlightedIndex } + onExpand={ ( index, value ) => { + setLinkedTree( + toggleNode( linkedTree, index, value ) + ); + } } items={ linkedTree } shouldShowCreateButton={ shouldShowCreateButton } onEscape={ () => { diff --git a/packages/js/components/src/experimental-select-tree-control/test/select-tree.test.tsx b/packages/js/components/src/experimental-select-tree-control/test/select-tree.test.tsx index b6f9dd266fb..07a8694897e 100644 --- a/packages/js/components/src/experimental-select-tree-control/test/select-tree.test.tsx +++ b/packages/js/components/src/experimental-select-tree-control/test/select-tree.test.tsx @@ -1,4 +1,6 @@ import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { useState } from 'react'; import React, { createElement } from '@wordpress/element'; import { SelectTree } from '../select-tree'; import { Item } from '../../experimental-tree-control'; @@ -26,6 +28,44 @@ const DEFAULT_PROPS = { placeholder: 'Type here', }; +const TestComponent = ( { multiple }: { multiple?: boolean } ) => { + const [ typedValue, setTypedValue ] = useState( '' ); + const [ selected, setSelected ] = useState< any >( [] ); + + return createElement( SelectTree, { + ...DEFAULT_PROPS, + multiple, + shouldShowCreateButton: () => true, + onInputChange: ( value ) => { + setTypedValue( value || '' ); + }, + createValue: typedValue, + selected: Array.isArray( selected ) + ? selected.map( ( i ) => ( { + value: String( i.id ), + label: i.name, + } ) ) + : { + value: String( selected.id ), + label: selected.name, + }, + onSelect: ( item: Item | Item[] ) => + item && Array.isArray( item ) + ? setSelected( + item.map( ( i ) => ( { + id: +i.value, + name: i.label, + parent: i.parent ? +i.parent : 0, + } ) ) + ) + : setSelected( { + id: +item.value, + name: item.label, + parent: item.parent ? +item.parent : 0, + } ), + } ); +}; + describe( 'SelectTree', () => { beforeEach( () => { jest.clearAllMocks(); @@ -36,7 +76,7 @@ describe( 'SelectTree', () => { ); expect( queryByText( 'Item 1' ) ).not.toBeInTheDocument(); - queryByRole( 'textbox' )?.focus(); + queryByRole( 'combobox' )?.focus(); expect( queryByText( 'Item 1' ) ).toBeInTheDocument(); } ); @@ -47,20 +87,21 @@ describe( 'SelectTree', () => { shouldShowCreateButton={ () => true } /> ); - queryByRole( 'textbox' )?.focus(); + queryByRole( 'combobox' )?.focus(); expect( queryByText( 'Create new' ) ).toBeInTheDocument(); } ); it( 'should not show create button when callback is false or no callback', () => { const { queryByText, queryByRole } = render( ); - queryByRole( 'textbox' )?.focus(); + queryByRole( 'combobox' )?.focus(); expect( queryByText( 'Create new' ) ).not.toBeInTheDocument(); } ); it( 'should show a root item when focused and child when expand button is clicked', () => { - const { queryByText, queryByLabelText, queryByRole } = - render( ); - queryByRole( 'textbox' )?.focus(); + const { queryByText, queryByLabelText, queryByRole } = render( + + ); + queryByRole( 'combobox' )?.focus(); expect( queryByText( 'Item 1' ) ).toBeInTheDocument(); expect( queryByText( 'Item 2' ) ).not.toBeInTheDocument(); @@ -72,7 +113,7 @@ describe( 'SelectTree', () => { const { queryAllByRole, queryByRole } = render( ); - queryByRole( 'textbox' )?.focus(); + queryByRole( 'combobox' )?.focus(); expect( queryAllByRole( 'treeitem' )[ 0 ] ).toHaveAttribute( 'aria-selected', 'true' @@ -87,7 +128,7 @@ describe( 'SelectTree', () => { shouldShowCreateButton={ () => true } /> ); - queryByRole( 'textbox' )?.focus(); + queryByRole( 'combobox' )?.focus(); expect( queryByText( 'Create "new item"' ) ).toBeInTheDocument(); } ); it( 'should call onCreateNew when Create "" button is clicked', () => { @@ -100,8 +141,34 @@ describe( 'SelectTree', () => { onCreateNew={ mockFn } /> ); - queryByRole( 'textbox' )?.focus(); + queryByRole( 'combobox' )?.focus(); queryByText( 'Create "new item"' )?.click(); expect( mockFn ).toBeCalledTimes( 1 ); } ); + it( 'correctly selects existing item in single mode with arrow keys', async () => { + const { findByRole } = render( ); + const combobox = ( await findByRole( 'combobox' ) ) as HTMLInputElement; + combobox.focus(); + userEvent.keyboard( '{arrowdown}{enter}' ); + expect( combobox.value ).toBe( 'Item 1' ); + } ); + it( 'correctly selects existing item in single mode by typing and pressing Enter', async () => { + const { findByRole } = render( ); + const combobox = ( await findByRole( 'combobox' ) ) as HTMLInputElement; + combobox.focus(); + userEvent.keyboard( 'Item 1{enter}' ); + userEvent.tab(); + expect( combobox.value ).toBe( 'Item 1' ); + } ); + it( 'correctly selects existing item in multiple mode by typing and pressing Enter', async () => { + const { findByRole, getAllByText } = render( + + ); + const combobox = ( await findByRole( 'combobox' ) ) as HTMLInputElement; + combobox.focus(); + userEvent.keyboard( 'Item 1' ); + userEvent.keyboard( '{enter}' ); + expect( combobox.value ).toBe( '' ); // input is cleared + expect( getAllByText( 'Item 1' )[ 0 ] ).toBeInTheDocument(); // item is selected (turns into a token) + } ); } ); diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-linked-tree.ts b/packages/js/components/src/experimental-tree-control/hooks/use-linked-tree.ts deleted file mode 100644 index 94ff95706b8..00000000000 --- a/packages/js/components/src/experimental-tree-control/hooks/use-linked-tree.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * External dependencies - */ -import { useMemo } from 'react'; - -/** - * Internal dependencies - */ -import { Item, LinkedTree } from '../types'; - -type MemoItems = { - [ value: Item[ 'value' ] ]: LinkedTree; -}; - -function findChildren( - items: Item[], - parent?: Item[ 'parent' ], - memo: MemoItems = {} -): LinkedTree[] { - const children: Item[] = []; - const others: Item[] = []; - - items.forEach( ( item ) => { - if ( item.parent === parent ) { - children.push( item ); - } else { - others.push( item ); - } - memo[ item.value ] = { - parent: undefined, - data: item, - children: [], - }; - } ); - - return children.map( ( child ) => { - const linkedTree = memo[ child.value ]; - linkedTree.parent = child.parent ? memo[ child.parent ] : undefined; - linkedTree.children = findChildren( others, child.value, memo ); - return linkedTree; - } ); -} - -export function useLinkedTree( items: Item[] ): LinkedTree[] { - const linkedTree = useMemo( () => { - return findChildren( items, undefined, {} ); - }, [ items ] ); - - return linkedTree; -} diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts b/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts index 2a4e7dca5ee..3c1dba15f04 100644 --- a/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts +++ b/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts @@ -32,6 +32,9 @@ export function useTreeItem( { onFirstItemLoop, onTreeBlur, onEscape, + highlightedIndex, + isHighlighted, + onExpand, ...props }: TreeItemProps ) { const nextLevel = level + 1; @@ -79,16 +82,19 @@ export function useTreeItem( { getLabel, treeItemProps: { ...props, - role: 'none', + id: + 'woocommerce-experimental-tree-control__menu-item-' + + item.index, + role: 'option', }, headingProps: { role: 'treeitem', 'aria-selected': selection.checkedStatus !== 'unchecked', 'aria-expanded': item.children.length - ? expander.isExpanded + ? item.data.isExpanded : undefined, 'aria-owns': - item.children.length && expander.isExpanded + item.children.length && item.data.isExpanded ? subTreeId : undefined, style: { diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts b/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts index 6f6c0d2c878..fc7c697ade5 100644 --- a/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts +++ b/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts @@ -10,7 +10,7 @@ import { TreeProps } from '../types'; export function useTree( { items, level = 1, - role = 'tree', + role = 'listbox', multiple, selected, getItemLabel, @@ -25,6 +25,8 @@ export function useTree( { shouldShowCreateButton, onFirstItemLoop, onEscape, + highlightedIndex, + onExpand, ...props }: TreeProps ) { return { diff --git a/packages/js/components/src/experimental-tree-control/linked-tree-utils.ts b/packages/js/components/src/experimental-tree-control/linked-tree-utils.ts new file mode 100644 index 00000000000..38f33003411 --- /dev/null +++ b/packages/js/components/src/experimental-tree-control/linked-tree-utils.ts @@ -0,0 +1,211 @@ +/** + * Internal dependencies + */ +import { AugmentedItem, Item, LinkedTree } from './types'; + +type MemoItems = { + [ value: AugmentedItem[ 'value' ] ]: LinkedTree; +}; + +const shouldItemBeExpanded = ( + item: LinkedTree, + createValue: string | undefined +): boolean => { + if ( ! createValue || ! item.children?.length ) return false; + return item.children.some( ( child ) => { + if ( new RegExp( createValue || '', 'ig' ).test( child.data.label ) ) { + return true; + } + return shouldItemBeExpanded( child, createValue ); + } ); +}; + +function findChildren( + items: AugmentedItem[], + memo: MemoItems = {}, + parent?: AugmentedItem[ 'parent' ], + createValue?: string | undefined +): LinkedTree[] { + const children: AugmentedItem[] = []; + const others: AugmentedItem[] = []; + + items.forEach( ( item ) => { + if ( item.parent === parent ) { + children.push( item ); + } else { + others.push( item ); + } + memo[ item.value ] = { + parent: undefined, + data: item, + children: [], + }; + } ); + + return children.map( ( child ) => { + const linkedTree = memo[ child.value ]; + linkedTree.parent = child.parent ? memo[ child.parent ] : undefined; + linkedTree.children = findChildren( + others, + memo, + child.value, + createValue + ); + linkedTree.data.isExpanded = + linkedTree.children.length === 0 + ? true + : shouldItemBeExpanded( linkedTree, createValue ); + return linkedTree; + } ); +} + +function populateIndexes( + linkedTree: LinkedTree[], + startCount = 0 +): LinkedTree[] { + let count = startCount; + + function populate( tree: LinkedTree[] ): number { + for ( const node of tree ) { + node.index = count; + count++; + if ( node.children ) { + count = populate( node.children ); + } + } + return count; + } + + populate( linkedTree ); + return linkedTree; +} + +// creates a linked tree from an array of Items +export function createLinkedTree( + items: Item[], + value: string | undefined +): LinkedTree[] { + const augmentedItems = items.map( ( i ) => ( { + ...i, + isExpanded: false, + } ) ); + return populateIndexes( + findChildren( augmentedItems, {}, undefined, value ) + ); +} + +// Toggles the expanded state of a node in a linked tree +export function toggleNode( + tree: LinkedTree[], + number: number, + value: boolean +): LinkedTree[] { + return tree.map( ( node ) => { + return { + ...node, + children: node.children + ? toggleNode( node.children, number, value ) + : node.children, + data: { + ...node.data, + isExpanded: + node.index === number ? value : node.data.isExpanded, + }, + ...( node.parent + ? { + parent: { + ...node.parent, + data: { + ...node.parent.data, + isExpanded: + node.parent.index === number + ? value + : node.parent.data.isExpanded, + }, + }, + } + : {} ), + }; + } ); +} + +// Gets the index of the next/previous visible node in the linked tree +export function getVisibleNodeIndex( + tree: LinkedTree[], + highlightedIndex: number, + direction: 'up' | 'down' +): number | undefined { + if ( direction === 'down' ) { + for ( const node of tree ) { + if ( ! node.parent || node.parent.data.isExpanded ) { + if ( + node.index !== undefined && + node.index >= highlightedIndex + ) { + return node.index; + } + const visibleNodeIndex = getVisibleNodeIndex( + node.children, + highlightedIndex, + direction + ); + if ( visibleNodeIndex !== undefined ) { + return visibleNodeIndex; + } + } + } + } else { + for ( let i = tree.length - 1; i >= 0; i-- ) { + const node = tree[ i ]; + if ( ! node.parent || node.parent.data.isExpanded ) { + const visibleNodeIndex = getVisibleNodeIndex( + node.children, + highlightedIndex, + direction + ); + if ( visibleNodeIndex !== undefined ) { + return visibleNodeIndex; + } + if ( + node.index !== undefined && + node.index <= highlightedIndex + ) { + return node.index; + } + } + } + } + + return undefined; +} + +// Counts the number of nodes in a LinkedTree +export function countNumberOfNodes( linkedTree: LinkedTree[] ) { + let count = 0; + for ( const node of linkedTree ) { + count++; + if ( node.children ) { + count += countNumberOfNodes( node.children ); + } + } + return count; +} + +// Gets the data of a node by its index +export function getNodeDataByIndex( + linkedTree: LinkedTree[], + index: number +): Item | undefined { + for ( const node of linkedTree ) { + if ( node.index === index ) { + return node.data; + } + if ( node.children ) { + const child = getNodeDataByIndex( node.children, index ); + if ( child ) { + return child; + } + } + } + return undefined; +} diff --git a/packages/js/components/src/experimental-tree-control/tree-control.tsx b/packages/js/components/src/experimental-tree-control/tree-control.tsx index 24a484a2995..d2e3db9db8e 100644 --- a/packages/js/components/src/experimental-tree-control/tree-control.tsx +++ b/packages/js/components/src/experimental-tree-control/tree-control.tsx @@ -6,7 +6,7 @@ import { createElement, forwardRef } from 'react'; /** * Internal dependencies */ -import { useLinkedTree } from './hooks/use-linked-tree'; +import { createLinkedTree } from './linked-tree-utils'; import { Tree } from './tree'; import { TreeControlProps } from './types'; @@ -14,7 +14,7 @@ export const TreeControl = forwardRef( function ForwardedTree( { items, ...props }: TreeControlProps, ref: React.ForwardedRef< HTMLOListElement > ) { - const linkedTree = useLinkedTree( items ); + const linkedTree = createLinkedTree( items, props.createValue ); return ; } ); diff --git a/packages/js/components/src/experimental-tree-control/tree-item.scss b/packages/js/components/src/experimental-tree-control/tree-item.scss index e0bd703c354..8e5f7a95e83 100644 --- a/packages/js/components/src/experimental-tree-control/tree-item.scss +++ b/packages/js/components/src/experimental-tree-control/tree-item.scss @@ -6,6 +6,8 @@ $control-size: $gap-large; &--highlighted { > .experimental-woocommerce-tree-item__heading { background-color: $gray-100; + outline: 1.5px solid var( --wp-admin-theme-color ); + outline-offset: -1.5px; } } diff --git a/packages/js/components/src/experimental-tree-control/tree-item.tsx b/packages/js/components/src/experimental-tree-control/tree-item.tsx index 6ca9aa14577..afbfe870895 100644 --- a/packages/js/components/src/experimental-tree-control/tree-item.tsx +++ b/packages/js/components/src/experimental-tree-control/tree-item.tsx @@ -24,21 +24,25 @@ export const TreeItem = forwardRef( function ForwardedTreeItem( treeItemProps, headingProps, treeProps, - expander: { isExpanded, onToggleExpand }, selection, - highlighter: { isHighlighted }, getLabel, } = useTreeItem( { ...props, ref, } ); - function handleEscapePress( - event: React.KeyboardEvent< HTMLInputElement > - ) { + function handleKeyDown( event: React.KeyboardEvent< HTMLElement > ) { if ( event.key === 'Escape' && props.onEscape ) { event.preventDefault(); props.onEscape(); + } else if ( event.key === 'ArrowLeft' ) { + if ( item.index !== undefined ) { + props.onExpand?.( item.index, false ); + } + } else if ( event.key === 'ArrowRight' ) { + if ( item.index !== undefined ) { + props.onExpand?.( item.index, true ); + } } } @@ -50,7 +54,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem( 'experimental-woocommerce-tree-item', { 'experimental-woocommerce-tree-item--highlighted': - isHighlighted, + props.isHighlighted, } ) } > @@ -67,7 +71,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem( } checked={ selection.checkedStatus === 'checked' } onChange={ selection.onSelectChild } - onKeyDown={ handleEscapePress } + onKeyDown={ handleKeyDown } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore __nextHasNoMarginBottom is a valid prop __nextHasNoMarginBottom={ true } @@ -80,7 +84,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem( onChange={ ( event ) => selection.onSelectChild( event.target.checked ) } - onKeyDown={ handleEscapePress } + onKeyDown={ handleKeyDown } /> ) } @@ -94,11 +98,21 @@ export const TreeItem = forwardRef( function ForwardedTreeItem( { Boolean( item.children?.length ) && (
- { Boolean( item.children.length ) && isExpanded && ( - + { Boolean( item.children.length ) && item.data.isExpanded && ( + ) } ); diff --git a/packages/js/components/src/experimental-tree-control/tree.scss b/packages/js/components/src/experimental-tree-control/tree.scss index 7225539369d..341d3a225ab 100644 --- a/packages/js/components/src/experimental-tree-control/tree.scss +++ b/packages/js/components/src/experimental-tree-control/tree.scss @@ -16,8 +16,9 @@ width: 100%; cursor: default; &:hover, - &:focus-within { - outline: 1.5px solid var( --wp-admin-theme-color ); + &:focus-within, + &--highlighted { + outline: 1.5px solid var(--wp-admin-theme-color); outline-offset: -1.5px; background-color: $gray-100; } diff --git a/packages/js/components/src/experimental-tree-control/tree.tsx b/packages/js/components/src/experimental-tree-control/tree.tsx index 08957af0904..ccbe500829a 100644 --- a/packages/js/components/src/experimental-tree-control/tree.tsx +++ b/packages/js/components/src/experimental-tree-control/tree.tsx @@ -14,6 +14,7 @@ import { useMergeRefs } from '@wordpress/compose'; import { useTree } from './hooks/use-tree'; import { TreeItem } from './tree-item'; import { TreeProps } from './types'; +import { countNumberOfNodes } from './linked-tree-utils'; export const Tree = forwardRef( function ForwardedTree( props: TreeProps, @@ -27,6 +28,8 @@ export const Tree = forwardRef( function ForwardedTree( ref, } ); + const numberOfItems = countNumberOfNodes( items ); + const isCreateButtonVisible = props.shouldShowCreateButton && props.shouldShowCreateButton( props.createValue ); @@ -45,7 +48,12 @@ export const Tree = forwardRef( function ForwardedTree( { items.map( ( child, index ) => ( { ( rootListRef.current - ?.closest( 'ol[role="tree"]' ) + ?.closest( 'ol[role="listbox"]' ) ?.parentElement?.querySelector( '.experimental-woocommerce-tree__button' ) as HTMLButtonElement @@ -67,7 +75,17 @@ export const Tree = forwardRef( function ForwardedTree( ) : null } { isCreateButtonVisible && ( + ); +} diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/functions.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/functions.tsx index 964b2a31b36..7f45fa91eee 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/functions.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/functions.tsx @@ -14,6 +14,7 @@ import { StatusLevel, Subscription } from '../../types'; import ConnectButton from '../actions/connect-button'; import Install from '../actions/install'; import RenewButton from '../actions/renew-button'; +import AutoRenewButton from '../actions/auto-renew-button'; import SubscribeButton from '../actions/subscribe-button'; import Update from '../actions/update'; import StatusPopover from './status-popover'; @@ -23,6 +24,7 @@ import { appendURLParams, renewUrl, subscribeUrl, + enableAutorenewalUrl, } from '../../../../utils/functions'; import { MARKETPLACE_COLLABORATION_PATH, @@ -60,7 +62,7 @@ function getStatusBadge( subscription: Subscription ): StatusBadge | false { ), sharing: ( sharing @@ -78,16 +80,7 @@ function getStatusBadge( subscription: Subscription ): StatusBadge | false { ), }; } - if ( subscription.local.installed && ! subscription.active ) { - return { - text: __( 'Not connected', 'woocommerce' ), - level: StatusLevel.Warning, - explanation: __( - 'To receive updates and support, please connect your subscription to this store.', - 'woocommerce' - ), - }; - } + if ( subscription.expired ) { return { text: __( 'Expired', 'woocommerce' ), @@ -107,6 +100,14 @@ function getStatusBadge( subscription: Subscription ): StatusBadge | false { ), sharing: ( + + sharing + + ), + transferring: ( ), + } + ), + }; + } + + if ( subscription.local.installed && ! subscription.active ) { + return { + text: __( 'Not connected', 'woocommerce' ), + level: StatusLevel.Warning, + explanation: __( + 'To receive updates and support, please connect your subscription to this store.', + 'woocommerce' + ), + }; + } + + if ( subscription.expiring && ! subscription.autorenew ) { + return { + text: __( 'Expires soon', 'woocommerce' ), + level: StatusLevel.Error, + explanation: createInterpolateElement( + __( + 'To receive updates and support, please renew this subscription before it expires or use a subscription from another account by sharing or transferring.', + 'woocommerce' + ), + { + renew: ( + + renew + + ), + sharing: ( + + sharing + + ), transferring: ( - { statusBadge && ( - - ) } { subscription.is_shared && ( + ); + } + + return subscription.autorenew + ? __( 'Active', 'woocommerce' ) + : __( 'Cancelled', 'woocommerce' ); + } return { - display: subscription.autorenew - ? __( 'On', 'woocommerce' ) - : __( 'Off', 'woocommerce' ), - value: subscription.autorenew, + display: getStatus(), }; } @@ -322,7 +370,10 @@ export function actions( subscription: Subscription ): TableRow { actionButton = ( ); + } else if ( ! subscription.autorenew ) { + actionButton = ; } + return { display: (
diff --git a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/status-popover.tsx b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/status-popover.tsx index c3dfd4aed8b..27096256c07 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/status-popover.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/my-subscriptions/table/rows/status-popover.tsx @@ -2,8 +2,8 @@ * External dependencies */ import { Popover } from '@wordpress/components'; -import { Icon, info } from '@wordpress/icons'; -import { useState } from '@wordpress/element'; +import { useState, useRef, useEffect } from '@wordpress/element'; +import clsx from 'clsx'; /** * Internal dependencies @@ -14,35 +14,78 @@ export default function StatusPopover( props: { text: string; level: StatusLevel; explanation: string | JSX.Element; + explanationOnHover?: boolean; } ) { - const [ isVisible, setIsVisible ] = useState( false ); + const [ isHovered, setIsHovered ] = useState( false ); + const [ isClicked, setIsClicked ] = useState( false ); + const hoverTimeoutId = useRef< null | NodeJS.Timeout >( null ); + + useEffect( () => { + return () => { + if ( hoverTimeoutId.current ) { + clearTimeout( hoverTimeoutId.current ); + } + }; + }, [] ); + + const startHover = () => { + if ( ! props.explanationOnHover ) { + return; + } + + if ( hoverTimeoutId.current ) { + clearTimeout( hoverTimeoutId.current ); + } + + setIsHovered( true ); + }; + + const endHover = () => { + if ( ! props.explanationOnHover ) { + return; + } + + if ( hoverTimeoutId.current ) { + clearTimeout( hoverTimeoutId.current ); + } + + // Add a small delay in case user hovers from the button to the popover. + // In such a case we don't want to hide the popover. + hoverTimeoutId.current = setTimeout( () => { + setIsHovered( false ); + }, 350 ); + }; function shouldShowExplanation() { if ( props.explanation === '' ) { return false; } - return isVisible; + return isClicked || ( props.explanationOnHover && isHovered ); } return ( ); diff --git a/plugins/woocommerce/changelog/50278-fix-improve_label b/plugins/woocommerce/changelog/50278-fix-improve_label new file mode 100644 index 00000000000..0bc697665eb --- /dev/null +++ b/plugins/woocommerce/changelog/50278-fix-improve_label @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +CYS: improve CTA \ No newline at end of file diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/assembler.page.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/assembler.page.js index 64ee1de8f0d..1432f70568a 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/assembler.page.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/assembler.page.js @@ -17,7 +17,7 @@ export class AssemblerPage { ); await frame - .getByRole( 'button', { name: 'Save' } ) + .getByRole( 'button', { name: 'Finish customizing' } ) .waitFor( { timeout: 25000 } ); } diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/color-picker.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/color-picker.spec.js index 6a5a91e646b..d90a0f457ec 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/color-picker.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/color-picker.spec.js @@ -302,7 +302,7 @@ test.describe( 'Assembler -> Color Pickers', { tag: '@gutenberg' }, () => { await assembler.locator( '[aria-label="Back"]' ).click(); - const saveButton = assembler.getByText( 'Save' ); + const saveButton = assembler.getByText( 'Finish customizing' ); const waitResponse = page.waitForResponse( ( response ) => @@ -427,7 +427,7 @@ test.describe( 'Assembler -> Color Pickers', { tag: '@gutenberg' }, () => { await assembler.locator( '[aria-label="Back"]' ).click(); - const saveButton = assembler.getByText( 'Save' ); + const saveButton = assembler.getByText( 'Finish customizing' ); const waitResponseGlobalStyles = page.waitForResponse( ( response ) => diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/font-picker.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/font-picker.spec.js index 57c25e6dca1..bea205eaac0 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/font-picker.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/font-picker.spec.js @@ -202,7 +202,7 @@ test.describe( 'Assembler -> Font Picker', { tag: '@gutenberg' }, () => { await assembler.locator( '[aria-label="Back"]' ).click(); - const saveButton = assembler.getByText( 'Save' ); + const saveButton = assembler.getByText( 'Finish customizing' ); const waitResponse = page.waitForResponse( ( response ) => diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/footer.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/footer.spec.js index 926e13277c2..5a4f0fab315 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/footer.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/footer.spec.js @@ -108,7 +108,7 @@ test.describe( 'Assembler -> Footers', { tag: '@gutenberg' }, () => { await assembler.locator( '[aria-label="Back"]' ).click(); - const saveButton = assembler.getByText( 'Save' ); + const saveButton = assembler.getByText( 'Finish customizing' ); const waitResponse = page.waitForResponse( ( response ) => diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/header.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/header.spec.js index e70140f6536..507724e0de1 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/header.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/header.spec.js @@ -108,7 +108,7 @@ test.describe( 'Assembler -> headers', { tag: '@gutenberg' }, () => { await assembler.locator( '[aria-label="Back"]' ).click(); - const saveButton = assembler.getByText( 'Save' ); + const saveButton = assembler.getByText( 'Finish customizing' ); const waitResponse = page.waitForResponse( ( response ) => diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js index 66239125734..e6f9d9e2141 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js @@ -157,7 +157,7 @@ test.describe( 'Assembler -> Homepage', { tag: '@gutenberg' }, () => { await assembler.locator( '[aria-label="Back"]' ).click(); - const saveButton = assembler.getByText( 'Save' ); + const saveButton = assembler.getByText( 'Finish customizing' ); const waitResponse = page.waitForResponse( ( response ) => diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/logo-picker/logo-picker.page.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/logo-picker/logo-picker.page.js index 0fb8a38542f..c9423ff9659 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/logo-picker/logo-picker.page.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/logo-picker/logo-picker.page.js @@ -80,11 +80,11 @@ export class LogoPickerPage { ); await assemblerLocator.locator( '[aria-label="Back"]' ).click(); await assemblerLocator - .getByRole( 'button', { name: 'Save', exact: true } ) + .getByRole( 'button', { name: 'Finish customizing', exact: true } ) .waitFor(); await Promise.all( [ waitForLogoResponse, - assemblerLocator.getByText( 'Save' ).click(), + assemblerLocator.getByText( 'Finish customizing' ).click(), ] ); await assemblerLocator.getByText( 'Your store looks great!' ).waitFor(); } diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/loading-screen/loading-screen.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/loading-screen/loading-screen.spec.js index 52ce5b1b669..62290c1f80e 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/loading-screen/loading-screen.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/loading-screen/loading-screen.spec.js @@ -123,7 +123,9 @@ test.describe( 'Assembler - Loading Page', { tag: '@gutenberg' }, () => { await pageObject.waitForLoadingScreenFinish(); const assembler = await pageObject.getAssembler(); - await assembler.getByRole( 'button', { name: 'Save' } ).click(); + await assembler + .getByRole( 'button', { name: 'Finish customizing' } ) + .click(); await assembler.getByText( 'Your store looks great!' ).waitFor(); // Abort any additional unnecessary requests await page.evaluate( () => window.stop() ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/transitional.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/transitional.spec.js index fd11179c79b..b156bec00aa 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/transitional.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/transitional.spec.js @@ -72,7 +72,7 @@ test.describe( await expect( page.url() ).toBe( `${ baseURL }${ INTRO_URL }` ); } ); - test( 'Clicking on "Save" in the assembler should go to the transitional page', async ( { + test( 'Clicking on "Finish customizing" in the assembler should go to the transitional page', async ( { pageObject, baseURL, } ) => { @@ -80,7 +80,9 @@ test.describe( await pageObject.waitForLoadingScreenFinish(); const assembler = await pageObject.getAssembler(); - await assembler.getByRole( 'button', { name: 'Save' } ).click(); + await assembler + .getByRole( 'button', { name: 'Finish customizing' } ) + .click(); await expect( assembler.locator( 'text=Your store looks great!' ) From f7f280fd1bcf85dca31c70c5c98a0c5975b81b78 Mon Sep 17 00:00:00 2001 From: piinthecloud Date: Mon, 5 Aug 2024 17:51:32 +0200 Subject: [PATCH 069/587] create product collection category and move to main docs folder (#50368) * create product collection category and move to main docs folder * add readme * update title * update title * add changelog and remove emojis * Update docs/product-collection-block/register-product-collection.md Co-authored-by: Karol Manijak <20098064+kmanijak@users.noreply.github.com> --------- Co-authored-by: Shani Co-authored-by: Karol Manijak <20098064+kmanijak@users.noreply.github.com> --- docs/docs-manifest.json | 19 ++++++++++++++++++- docs/product-collection-block/README.md | 5 +++++ .../register-product-collection.md | 14 ++++++++++---- .../changelog/move-product-collection | 4 ++++ 4 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 docs/product-collection-block/README.md rename {plugins/woocommerce-blocks/docs/third-party-developers/extensibility => docs/product-collection-block}/register-product-collection.md (94%) create mode 100644 plugins/woocommerce/changelog/move-product-collection diff --git a/docs/docs-manifest.json b/docs/docs-manifest.json index cf69b715e95..a994f533f1d 100644 --- a/docs/docs-manifest.json +++ b/docs/docs-manifest.json @@ -997,6 +997,23 @@ ], "categories": [] }, + { + "content": "", + "category_slug": "product-collection", + "category_title": "Product Collection Block", + "posts": [ + { + "post_title": "Registering custom collections in product collection block", + "menu_title": "Registering custom collections", + "tags": "how-to", + "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-collection-block/register-product-collection.md", + "hash": "88445929a9f76512e1e8ff60be7beff7e912f31fbad552abf18862ed85f00585", + "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-collection-block/register-product-collection.md", + "id": "3bf26fc7c56ae6e6a56e1171f750f5204fcfcece" + } + ], + "categories": [] + }, { "content": "\nDiscover how to customize the WooCommerce product editor, from extending product data to adding unique functionalities.\n\nThis handbook is a guide for extension developers looking to add support for the new product editor in their extensions. The product editor uses [Gutenberg's Block Editor](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-editor), which is going to help WooCommerce evolve alongside the WordPress ecosystem.", "category_slug": "product-editor", @@ -1680,5 +1697,5 @@ "categories": [] } ], - "hash": "aba0ae4b152f1007f64966aa21601ba73786ce4b1be4219f18ecbf295ee10221" + "hash": "c24136612fe16fc7dee435a2ad2198ce242ab63dbba483fd0a18efd88dc3a289" } \ No newline at end of file diff --git a/docs/product-collection-block/README.md b/docs/product-collection-block/README.md new file mode 100644 index 00000000000..d49163534c8 --- /dev/null +++ b/docs/product-collection-block/README.md @@ -0,0 +1,5 @@ +--- +category_title: Product Collection Block +category_slug: product-collection +post_title: Product collection block +--- diff --git a/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/register-product-collection.md b/docs/product-collection-block/register-product-collection.md similarity index 94% rename from plugins/woocommerce-blocks/docs/third-party-developers/extensibility/register-product-collection.md rename to docs/product-collection-block/register-product-collection.md index 56b346b4c71..9f96f791262 100644 --- a/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/register-product-collection.md +++ b/docs/product-collection-block/register-product-collection.md @@ -1,6 +1,12 @@ +--- +post_title: Registering custom collections in product collection block +menu_title: Registering custom collections +tags: how-to +--- + # Register Product Collection -The `__experimentalRegisterProductCollection` function is part of the `@woocommerce/blocks-registry` package. This function allows 3PDs to register a new collection. This function accepts most of the arguments that are accepted by [Block Variation](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/#defining-a-block-variation). +The `__experimentalRegisterProductCollection` function is part of the `@woocommerce/blocks-registry` package. This function allows third party developers to register a new collection. This function accepts most of the arguments that are accepted by [Block Variation](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/#defining-a-block-variation). > [!WARNING] > It's experimental and may change in the future. Please use it with caution. @@ -34,8 +40,8 @@ A Collection is defined by an object that can contain the following fields: - `title` (type `string`): The title of the collection, which will be displayed in various places including the block inserter and collection chooser. - `description` (optional, type `string`): A human-readable description of the collection. - `innerBlocks` (optional, type `Array[]`): An array of inner blocks that will be added to the collection. If not provided, the default inner blocks will be used. -- `isDefault`: ⚠️ It's set to `false` for all collections. 3PDs doesn't need to pass this argument. -- `isActive`: ⚠️ It will be managed by us. 3PDs doesn't need to pass this argument. +- `isDefault`: It's set to `false` for all collections. Third party developers don't need to pass this argument. +- `isActive`: It will be managed by us. Third party developers don't need to pass this argument. - `usesReference` (optional, type `Array[]`): An array of strings specifying the required reference for the collection. Acceptable values are `product`, `archive`, `cart`, and `order`. When the required reference isn't available on Editor side but will be available in Frontend, we will show a preview label. ### Attributes @@ -91,7 +97,7 @@ The `preview` attribute is optional, and it is used to set the preview state of - `attributes` (type `object`): The current attributes of the collection. - `location` (type `object`): The location of the collection. Accepted values are `product`, `archive`, `cart`, `order`, `site`. -For more info, you may check PR #46369, in which the Preview feature was added +For more info, you may check [PR #46369](https://github.com/woocommerce/woocommerce/pull/46369), in which the Preview feature was added ## Examples diff --git a/plugins/woocommerce/changelog/move-product-collection b/plugins/woocommerce/changelog/move-product-collection new file mode 100644 index 00000000000..c7dcdc00e9c --- /dev/null +++ b/plugins/woocommerce/changelog/move-product-collection @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +moving product collection docs to main docs folder From ec6711346bd7856796441c109c7bbe014f90f26c Mon Sep 17 00:00:00 2001 From: DAnn2012 Date: Tue, 6 Aug 2024 02:36:17 +0200 Subject: [PATCH 070/587] Fix typo (sidebar-navigation-screen-typography.tsx) (#50332) * Update sidebar-navigation-screen-typography.tsx * Add changefile(s) from automation for the following project(s): woocommerce --------- Co-authored-by: github-actions --- .../sidebar-navigation-screen-typography.tsx | 2 +- plugins/woocommerce/changelog/50332-patch-19 | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/50332-patch-19 diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography/sidebar-navigation-screen-typography.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography/sidebar-navigation-screen-typography.tsx index 88f66bc0caa..a9202a4da37 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography/sidebar-navigation-screen-typography.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography/sidebar-navigation-screen-typography.tsx @@ -183,7 +183,7 @@ export const SidebarNavigationScreenTypography = ( { className="core-profiler__checkbox" label={ interpolateComponents( { mixedString: __( - 'More fonts are available! Opt in to connect your store and access the full font library, plus get more relevant content and a tailored store setup experience. Opting in will enable {{link}}usage tracking{{/link}}, which you can opt out of at any time via WooCommerece settings.', + 'More fonts are available! Opt in to connect your store and access the full font library, plus get more relevant content and a tailored store setup experience. Opting in will enable {{link}}usage tracking{{/link}}, which you can opt out of at any time via WooCommerce settings.', 'woocommerce' ), components: { diff --git a/plugins/woocommerce/changelog/50332-patch-19 b/plugins/woocommerce/changelog/50332-patch-19 new file mode 100644 index 00000000000..043fa2f15ca --- /dev/null +++ b/plugins/woocommerce/changelog/50332-patch-19 @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak +Comment: Fix typo (sidebar-navigation-screen-typography.tsx) + From 3d16f16ae199e6ec858d6756fab0751b2ce1de9b Mon Sep 17 00:00:00 2001 From: Adrian Duffell <9312929+adrianduffell@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:29:01 +0800 Subject: [PATCH 071/587] Use admin password reset link on admin login screen (#50200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use admin password reset link on admin login screen * Add changelog * Take a different approach that’s compatible with JN * Update code comment * use existing variable * Update tests * Lintfix * Change to alternative aproach for detecting admin login form * Whitespace * Update test * Lint fix --- .../changelog/update-admin-reset-password | 4 +++ .../includes/wc-account-functions.php | 5 +++ .../legacy/unit-tests/account/functions.php | 31 +++++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-admin-reset-password diff --git a/plugins/woocommerce/changelog/update-admin-reset-password b/plugins/woocommerce/changelog/update-admin-reset-password new file mode 100644 index 00000000000..a0d9de4f7e4 --- /dev/null +++ b/plugins/woocommerce/changelog/update-admin-reset-password @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Use admin password reset on admin login screen diff --git a/plugins/woocommerce/includes/wc-account-functions.php b/plugins/woocommerce/includes/wc-account-functions.php index 3f34a3a5801..0abfaba1b4d 100644 --- a/plugins/woocommerce/includes/wc-account-functions.php +++ b/plugins/woocommerce/includes/wc-account-functions.php @@ -22,6 +22,11 @@ function wc_lostpassword_url( $default_url = '' ) { return $default_url; } + // Don't change the admin form. + if ( did_action( 'login_form_login' ) ) { + return $default_url; + } + // Don't redirect to the woocommerce endpoint on global network admin lost passwords. if ( is_multisite() && isset( $_GET['redirect_to'] ) && false !== strpos( wp_unslash( $_GET['redirect_to'] ), network_admin_url() ) ) { // WPCS: input var ok, sanitization ok, CSRF ok. return $default_url; diff --git a/plugins/woocommerce/tests/legacy/unit-tests/account/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/account/functions.php index 1cb8dae1a22..db1a701439f 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/account/functions.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/account/functions.php @@ -11,12 +11,39 @@ class WC_Tests_Account_Functions extends WC_Unit_Test_Case { /** - * Test wc_lostpassword_url(). + * Test wc_lostpassword_url() from admin screen. * * @since 3.3.0 */ public function test_wc_lostpassword_url() { - $this->assertEquals( 'http://' . WP_TESTS_DOMAIN . '?lost-password', wc_lostpassword_url() ); + // phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment + do_action( 'login_form_login' ); // Simulate admin login screen. + + // Admin URL is expected. + $expected_url = admin_url( '/wp-login.php?action=lostpassword' ); + + $this->assertEquals( $expected_url, wc_lostpassword_url( $expected_url ) ); + } + + /** + * Test wc_lostpassword_url() from my account page. + */ + public function test_wc_lostpassword_url_from_account_page() { + // Create the account page, since other tests may delete it. + $page = wc_create_page( + 'myaccount', + 'woocommerce_myaccount_page_id', + 'My Account', + '', + '', + 'publish' + ); + $this->go_to( wc_get_page_permalink( 'myaccount' ) ); + + // Front-end URL is expected. + $expected_url = wc_get_endpoint_url( 'lost-password', '', get_the_permalink( 'myaccount' ) ); + + $this->assertEquals( $expected_url, wc_lostpassword_url() ); } /** From 328d9442882b6952c1c41671b066d9d27667cc9d Mon Sep 17 00:00:00 2001 From: Vladimir Reznichenko Date: Tue, 6 Aug 2024 09:30:10 +0200 Subject: [PATCH 072/587] Monorepo: update packageManager version in package.json file. (#50384) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bb3c76374b5..2920ad9d71d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "title": "WooCommerce Monorepo", "description": "Monorepo for the WooCommerce ecosystem", "homepage": "https://woocommerce.com/", - "packageManager": "pnpm@^9.1.0", + "packageManager": "pnpm@9.6.0", "engines": { "node": "^20.11.1", "pnpm": "^9.1.0" From 52119dfcc9cdd98ec21fd6189b2d51f4d4957f62 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Tue, 6 Aug 2024 11:23:04 +0200 Subject: [PATCH 073/587] Fix CI Metrics job (#50214) --- .github/workflows/ci.yml | 9 ----- .github/workflows/scripts/run-metrics.sh | 21 ++++++++---- .../woocommerce/changelog/fix-ci-metrics-job | 4 +++ plugins/woocommerce/package.json | 8 +++-- .../tests/e2e-pw/utils/simple-products.js | 34 ++++++++++++------- .../tests/metrics/playwright.config.js | 9 ++--- .../tests/metrics/specs/editor.spec.js | 1 - .../metrics/specs/product-editor.spec.js | 8 ++--- tools/compare-perf/config.js | 18 +++++----- 9 files changed, 65 insertions(+), 47 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-ci-metrics-job diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9d43842306..c4ca04202cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -194,15 +194,6 @@ jobs: name: flaky-tests-${{ strategy.job-index }} path: ${{ env.ARTIFACTS_PATH }}/flaky-tests if-no-files-found: ignore - - - name: 'Archive metrics results' - if: ${{ success() && startsWith(matrix.name, 'Metrics') }} # this seems too fragile, we should update the reporting path and use the generic upload step above - uses: actions/upload-artifact@v4 - env: - WP_ARTIFACTS_PATH: ${{ github.workspace }}/artifacts - with: - name: metrics-results - path: ${{ env.WP_ARTIFACTS_PATH }}/*.performance-results*.json evaluate-project-jobs: # In order to add a required status check we need a consistent job that we can grab onto. diff --git a/.github/workflows/scripts/run-metrics.sh b/.github/workflows/scripts/run-metrics.sh index 4bbe85ee470..7086646a4ae 100755 --- a/.github/workflows/scripts/run-metrics.sh +++ b/.github/workflows/scripts/run-metrics.sh @@ -7,25 +7,34 @@ if [[ -z "$GITHUB_EVENT_NAME" ]]; then exit 1 fi +echo "Installing dependencies" +pnpm install --filter="compare-perf" + if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then echo "Comparing performance with trunk" pnpm --filter="compare-perf" run compare perf $GITHUB_SHA trunk --tests-branch $GITHUB_SHA elif [[ "$GITHUB_EVENT_NAME" == "push" ]]; then echo "Comparing performance with base branch" - # The base hash used here need to be a commit that is compatible with the current WP version - # The current one is 19f3d0884617d7ecdcf37664f648a51e2987cada - # it needs to be updated every time it becomes unsupported by the current wp-env (WP version). - # It is used as a base comparison point to avoid fluctuation in the performance metrics. WP_VERSION=$(awk -F ': ' '/^Tested up to/{print $2}' readme.txt) + # Updating the WP version used for performance jobs means there’s a high + # chance that the reference commit used for performance test stability + # becomes incompatible with the WP version. So, every time the "Tested up + # to" flag is updated in the readme.txt, we also have to update the + # reference commit below (BASE_SHA). The new reference needs to meet the + # following requirements: + # - Be compatible with the new WP version used in the “Tested up to” flag. + # - Be tracked on https://www.codevitals.run/project/woo for all existing + # metrics. + BASE_SHA=3d7d7f02017383937f1a4158d433d0e5d44b3dc9 echo "WP_VERSION: $WP_VERSION" IFS=. read -ra WP_VERSION_ARRAY <<< "$WP_VERSION" WP_MAJOR="${WP_VERSION_ARRAY[0]}.${WP_VERSION_ARRAY[1]}" - pnpm --filter="compare-perf" run compare perf $GITHUB_SHA 19f3d0884617d7ecdcf37664f648a51e2987cada --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" + pnpm --filter="compare-perf" run compare perf $GITHUB_SHA $BASE_SHA --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" echo "Publish results to CodeVitals" COMMITTED_AT=$(git show -s $GITHUB_SHA --format="%cI") - pnpm --filter="compare-perf" run log $CODEVITALS_PROJECT_TOKEN trunk $GITHUB_SHA 19f3d0884617d7ecdcf37664f648a51e2987cada $COMMITTED_AT + pnpm --filter="compare-perf" run log $CODEVITALS_PROJECT_TOKEN trunk $GITHUB_SHA $BASE_SHA $COMMITTED_AT else echo "Unsupported event: $GITHUB_EVENT_NAME" fi diff --git a/plugins/woocommerce/changelog/fix-ci-metrics-job b/plugins/woocommerce/changelog/fix-ci-metrics-job new file mode 100644 index 00000000000..d743f02743a --- /dev/null +++ b/plugins/woocommerce/changelog/fix-ci-metrics-job @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Fix Metrics CI job diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index 0506a8a3dbe..6c41b1cead1 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -529,8 +529,12 @@ ".wp-env.json" ], "events": [ - "disabled" - ] + "push" + ], + "report": { + "resultsBlobName": "core-metrics-report", + "resultsPath": "../../tools/compare-perf/artifacts/" + } }, { "name": "Blocks e2e tests", diff --git a/plugins/woocommerce/tests/e2e-pw/utils/simple-products.js b/plugins/woocommerce/tests/e2e-pw/utils/simple-products.js index 1e9af15844b..a37c31ce486 100644 --- a/plugins/woocommerce/tests/e2e-pw/utils/simple-products.js +++ b/plugins/woocommerce/tests/e2e-pw/utils/simple-products.js @@ -22,22 +22,32 @@ async function isBlockProductEditorEnabled( page ) { /** * This function is typically used for enabling/disabling the block product editor in settings page. * - * @param {string} action The action that will be performed. - * @param {import('@playwright/test').Page} page + * @param {string} action The action that will be performed. + * @param {import('@playwright/test').Page} page */ async function toggleBlockProductEditor( action = 'enable', page ) { await page.goto( SETTINGS_URL ); - if ( action === 'disable' ) { - await page - .locator( '#woocommerce_feature_product_block_editor_enabled' ) - .uncheck(); - } else { - await page - .locator( '#woocommerce_feature_product_block_editor_enabled' ) - .check(); + + const enableProductEditor = page.locator( + '#woocommerce_feature_product_block_editor_enabled' + ); + const isEnabled = await enableProductEditor.isChecked(); + + if ( + ( action === 'enable' && isEnabled ) || + ( action === 'disable' && ! isEnabled ) + ) { + // No need to toggle the setting. + return; } + + if ( action === 'enable' ) { + await enableProductEditor.check(); + } else if ( action === 'disable' ) { + await enableProductEditor.uncheck(); + } + await page - .locator( '.submit' ) .getByRole( 'button', { name: 'Save changes', } ) @@ -81,7 +91,7 @@ async function expectBlockProductEditor( page ) { /** * Click on a block editor tab. * - * @param {string} tabName + * @param {string} tabName * @param {import('@playwright/test').Page} page */ async function clickOnTab( tabName, page ) { diff --git a/plugins/woocommerce/tests/metrics/playwright.config.js b/plugins/woocommerce/tests/metrics/playwright.config.js index 20043d2088c..053ea9231d2 100644 --- a/plugins/woocommerce/tests/metrics/playwright.config.js +++ b/plugins/woocommerce/tests/metrics/playwright.config.js @@ -1,3 +1,6 @@ +/** + * External dependencies + */ import path from 'path'; import { fileURLToPath } from 'url'; import { defineConfig, devices } from '@playwright/test'; @@ -11,9 +14,7 @@ process.env.STORAGE_STATE_PATH ??= path.join( process.env.WP_BASE_URL ??= 'http://localhost:8086'; const config = defineConfig( { - reporter: process.env.CI - ? './config/performance-reporter.ts' - : [ [ 'list' ], [ './config/performance-reporter.ts' ] ], + reporter: [ [ 'list' ], [ './config/performance-reporter.ts' ] ], forbidOnly: !! process.env.CI, fullyParallel: false, workers: 1, @@ -24,7 +25,7 @@ const config = defineConfig( { testDir: './specs', outputDir: path.join( process.env.WP_ARTIFACTS_PATH, 'test-results' ), snapshotPathTemplate: - '{testDir}/{testFileDir}/__snapshots__/{arg}-{projectName}{ext}', + '{testDir}/{testFileDir}/__snapshots__/{arg}-{projectName}{ext}', globalSetup: fileURLToPath( new URL( './config/global-setup.ts', 'file:' + __filename ).href ), diff --git a/plugins/woocommerce/tests/metrics/specs/editor.spec.js b/plugins/woocommerce/tests/metrics/specs/editor.spec.js index 62d0432e78a..30307a4d022 100644 --- a/plugins/woocommerce/tests/metrics/specs/editor.spec.js +++ b/plugins/woocommerce/tests/metrics/specs/editor.spec.js @@ -146,7 +146,6 @@ test.describe( 'Editor Performance', () => { await perfUtils.loadBlocksForLargePost(); await editor.insertBlock( { name: 'core/paragraph' } ); draftId = await perfUtils.saveDraft(); - console.log( draftId ); } ); test( 'Run the test', async ( { admin, page, perfUtils, metrics } ) => { diff --git a/plugins/woocommerce/tests/metrics/specs/product-editor.spec.js b/plugins/woocommerce/tests/metrics/specs/product-editor.spec.js index ce5eebb090f..b23eab5dda9 100644 --- a/plugins/woocommerce/tests/metrics/specs/product-editor.spec.js +++ b/plugins/woocommerce/tests/metrics/specs/product-editor.spec.js @@ -30,10 +30,6 @@ test.describe( 'Product editor performance', () => { }, } ); - test.beforeEach( async ( { page } ) => { - await toggleBlockProductEditor( 'enable', page ); - } ); - test.afterAll( async ( {}, testInfo ) => { const medians = {}; Object.keys( results ).forEach( ( metric ) => { @@ -45,6 +41,10 @@ test.describe( 'Product editor performance', () => { } ); } ); + test( 'Enable Product Editor', async ( { page } ) => { + await toggleBlockProductEditor( 'enable', page ); + } ); + test.describe( 'Loading', () => { const samples = 2; const throwaway = 1; diff --git a/tools/compare-perf/config.js b/tools/compare-perf/config.js index e8f1e5b2e0d..a84d788b392 100644 --- a/tools/compare-perf/config.js +++ b/tools/compare-perf/config.js @@ -2,13 +2,13 @@ const path = require( 'path' ); const getPnpmPackage = ( sourceDir ) => { const packageJson = require( path.join( sourceDir, 'package.json' ) ); - let pnpm_package = 'pnpm'; + let pnpmPackage = 'pnpm'; if ( packageJson.engines.pnpm ) { - pnpm_package = `pnpm@${ packageJson.engines.pnpm }`; + pnpmPackage = `pnpm@${ packageJson.engines.pnpm }`; } - return pnpm_package; + return pnpmPackage; }; const config = { @@ -16,18 +16,18 @@ const config = { pluginPath: '/plugins/woocommerce', testsPath: '/plugins/woocommerce/tests/metrics/specs', getSetupTestRunner: ( sourceDir ) => { - const pnpm_package = getPnpmPackage( sourceDir ); + const pnpmPackage = getPnpmPackage( sourceDir ); - return `npm install -g ${ pnpm_package } && pnpm install --filter="@woocommerce/plugin-woocommerce" &> /dev/null && cd plugins/woocommerce && pnpm exec playwright install chromium`; + return `npm install -g ${ pnpmPackage } && pnpm install --frozen-lockfile --filter="@woocommerce/plugin-woocommerce" &> /dev/null && cd plugins/woocommerce && pnpm exec playwright install chromium`; }, getSetupCommand: ( sourceDir ) => { - const pnpm_package = getPnpmPackage( sourceDir ); + const pnpmPackage = getPnpmPackage( sourceDir ); - return `npm install -g ${ pnpm_package } && pnpm install &> /dev/null && pnpm build &> /dev/null`; + return `npm install -g ${ pnpmPackage } && pnpm install --frozen-lockfile &> /dev/null && pnpm build &> /dev/null`; }, getTestCommand: ( sourceDir ) => { - const pnpm_package = getPnpmPackage( sourceDir ); - return `npm install -g ${ pnpm_package } && cd plugins/woocommerce && pnpm test:metrics`; + const pnpmPackage = getPnpmPackage( sourceDir ); + return `npm install -g ${ pnpmPackage } && cd plugins/woocommerce && pnpm test:metrics`; }, }; From c410610c6212acb89911f630cfb2d88b93eb1c5d Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Tue, 6 Aug 2024 06:39:35 -0300 Subject: [PATCH 074/587] Rename and move errorHandler method (#50277) * Rename and move errorHandler method * Add changelog --- .../dev-50276_rename_and_move_error_handler | 4 ++ .../edit.tsx | 9 ++-- .../header/hooks/use-preview/use-preview.tsx | 7 ++- .../hooks/use-save-draft/use-save-draft.tsx | 7 ++- .../use-product-manager.ts | 38 ++-------------- .../src/utils/format-product-error.ts | 43 +++++++++++++++++++ 6 files changed, 67 insertions(+), 41 deletions(-) create mode 100644 packages/js/product-editor/changelog/dev-50276_rename_and_move_error_handler create mode 100644 packages/js/product-editor/src/utils/format-product-error.ts diff --git a/packages/js/product-editor/changelog/dev-50276_rename_and_move_error_handler b/packages/js/product-editor/changelog/dev-50276_rename_and_move_error_handler new file mode 100644 index 00000000000..0951cfcbedc --- /dev/null +++ b/packages/js/product-editor/changelog/dev-50276_rename_and_move_error_handler @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Rename and move errorHandler method #50277 diff --git a/packages/js/product-editor/src/blocks/product-fields/product-details-section-description/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/product-details-section-description/edit.tsx index a11376f0840..de6995726a4 100644 --- a/packages/js/product-editor/src/blocks/product-fields/product-details-section-description/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/product-details-section-description/edit.tsx @@ -42,7 +42,7 @@ import type { import { ProductDetailsSectionDescriptionBlockAttributes } from './types'; import * as wooIcons from '../../../icons'; import isProductFormTemplateSystemEnabled from '../../../utils/is-product-form-template-system-enabled'; -import { errorHandler } from '../../../hooks/use-product-manager'; +import { formatProductError } from '../../../utils/format-product-error'; export function ProductDetailsSectionDescriptionBlockEdit( { attributes, @@ -189,7 +189,7 @@ export function ProductDetailsSectionDescriptionBlockEdit( { } catch ( error ) { const { message, errorProps } = await getProductErrorMessageAndProps( - errorHandler( + formatProductError( error as WPError, productStatus ) as WPError, @@ -311,7 +311,10 @@ export function ProductDetailsSectionDescriptionBlockEdit( { } catch ( error ) { const { message, errorProps } = await getProductErrorMessageAndProps( - errorHandler( error as WPError, productStatus ) as WPError, + formatProductError( + error as WPError, + productStatus + ) as WPError, selectedTab ); createErrorNotice( message, errorProps ); diff --git a/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx b/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx index e3302b6be98..33af470f52f 100644 --- a/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx +++ b/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx @@ -16,7 +16,7 @@ import { useValidations } from '../../../../contexts/validation-context'; import { WPError } from '../../../../hooks/use-error-handler'; import { useProductURL } from '../../../../hooks/use-product-url'; import { PreviewButtonProps } from '../../preview-button'; -import { errorHandler } from '../../../../hooks/use-product-manager'; +import { formatProductError } from '../../../../utils/format-product-error'; export function usePreview( { productStatus, @@ -129,7 +129,10 @@ export function usePreview( { } catch ( error ) { if ( onSaveError ) { onSaveError( - errorHandler( error as WPError, productStatus ) as WPError + formatProductError( + error as WPError, + productStatus + ) as WPError ); } } diff --git a/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx b/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx index d1e2a64bc0b..7379ced3471 100644 --- a/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx +++ b/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx @@ -18,7 +18,7 @@ import { useValidations } from '../../../../contexts/validation-context'; import { WPError } from '../../../../hooks/use-error-handler'; import { SaveDraftButtonProps } from '../../save-draft-button'; import { recordProductEvent } from '../../../../utils/record-product-event'; -import { errorHandler } from '../../../../hooks/use-product-manager'; +import { formatProductError } from '../../../../utils/format-product-error'; export function useSaveDraft( { productStatus, @@ -108,7 +108,10 @@ export function useSaveDraft( { } catch ( error ) { if ( onSaveError ) { onSaveError( - errorHandler( error as WPError, productStatus ) as WPError + formatProductError( + error as WPError, + productStatus + ) as WPError ); } } diff --git a/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts b/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts index 584bdc3fae7..cb9e6afd57e 100644 --- a/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts +++ b/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts @@ -12,37 +12,7 @@ import { Product, ProductStatus, PRODUCTS_STORE_NAME } from '@woocommerce/data'; import { useValidations } from '../../contexts/validation-context'; import type { WPError } from '../../hooks/use-error-handler'; import { AUTO_DRAFT_NAME } from '../../utils/constants'; - -export function errorHandler( error: WPError, productStatus: ProductStatus ) { - if ( error.code ) { - return error; - } - - const errorObj = Object.values( error ).find( - ( value ) => value !== undefined - ) as WPError | undefined; - - if ( 'variations' in error && error.variations ) { - return { - ...errorObj, - code: 'variable_product_no_variation_prices', - }; - } - - if ( errorObj !== undefined ) { - return { - ...errorObj, - code: 'product_form_field_error', - }; - } - - return { - code: - productStatus === 'publish' || productStatus === 'future' - ? 'product_publish_error' - : 'product_create_error', - }; -} +import { formatProductError } from '../../utils/format-product-error'; export function useProductManager< T = Product >( postType: string ) { const [ id ] = useEntityProp< number >( 'postType', postType, 'id' ); @@ -107,7 +77,7 @@ export function useProductManager< T = Product >( postType: string ) { return savedProduct as T; } catch ( error ) { - throw errorHandler( error as WPError, status ); + throw formatProductError( error as WPError, status ); } finally { setIsSaving( false ); } @@ -128,7 +98,7 @@ export function useProductManager< T = Product >( postType: string ) { return duplicatedProduct as T; } catch ( error ) { - throw errorHandler( error as WPError, status ); + throw formatProductError( error as WPError, status ); } finally { setIsSaving( false ); } @@ -174,7 +144,7 @@ export function useProductManager< T = Product >( postType: string ) { return deletedProduct as T; } catch ( error ) { - throw errorHandler( error as WPError, status ); + throw formatProductError( error as WPError, status ); } finally { setTrashing( false ); } diff --git a/packages/js/product-editor/src/utils/format-product-error.ts b/packages/js/product-editor/src/utils/format-product-error.ts new file mode 100644 index 00000000000..6ac039c838b --- /dev/null +++ b/packages/js/product-editor/src/utils/format-product-error.ts @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import { ProductStatus } from '@woocommerce/data'; + +/** + * Internal dependencies + */ +import type { WPError } from '../hooks/use-error-handler'; + +export function formatProductError( + error: WPError, + productStatus: ProductStatus +) { + if ( error.code ) { + return error; + } + + const errorObj = Object.values( error ).find( + ( value ) => value !== undefined + ) as WPError | undefined; + + if ( 'variations' in error && error.variations ) { + return { + ...errorObj, + code: 'variable_product_no_variation_prices', + }; + } + + if ( errorObj !== undefined ) { + return { + ...errorObj, + code: 'product_form_field_error', + }; + } + + return { + code: + productStatus === 'publish' || productStatus === 'future' + ? 'product_publish_error' + : 'product_create_error', + }; +} From 5e5378e444527df7dc5bf794899aa9399c7c38ab Mon Sep 17 00:00:00 2001 From: Tarun Vijwani <11503784+tarunvijwani@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:41:26 +0400 Subject: [PATCH 075/587] Remove Active Shipping Zones check for displaying shipping calculator on Cart Page (#49214) * Remove active shipping zones check for displaying shipping calculator - Remove active shipping zones check for displaying shipping calculator on Cart Page * Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce --------- Co-authored-by: Tarun Vijwani Co-authored-by: github-actions Co-authored-by: Seghir Nadir --- .../totals/shipping/shipping-address.tsx | 29 +------------------ ...emove-active-shipping-zone-check-cart-page | 4 +++ .../src/Blocks/BlockTypes/Cart.php | 1 - 3 files changed, 5 insertions(+), 29 deletions(-) create mode 100644 plugins/woocommerce/changelog/49214-fix-remove-active-shipping-zone-check-cart-page diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx index dd9eb92adf7..2c3e645eabb 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/totals/shipping/shipping-address.tsx @@ -3,11 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { formatShippingAddress } from '@woocommerce/base-utils'; -import { useEditorContext } from '@woocommerce/base-context'; -import { - ShippingAddress as ShippingAddressType, - getSetting, -} from '@woocommerce/settings'; +import { ShippingAddress as ShippingAddressType } from '@woocommerce/settings'; import PickupLocation from '@woocommerce/base-components/cart-checkout/pickup-location'; import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; import { useSelect } from '@wordpress/data'; @@ -25,41 +21,18 @@ export interface ShippingAddressProps { shippingAddress: ShippingAddressType; } -export type ActiveShippingZones = { - description: string; -}[]; - export const ShippingAddress = ( { showCalculator, isShippingCalculatorOpen, setIsShippingCalculatorOpen, shippingAddress, }: ShippingAddressProps ): JSX.Element | null => { - const { isEditor } = useEditorContext(); const prefersCollection = useSelect( ( select ) => select( CHECKOUT_STORE_KEY ).prefersCollection() ); - const activeShippingZones: ActiveShippingZones = getSetting( - 'activeShippingZones' - ); - - const hasMultipleAndDefaultZone = - activeShippingZones.length > 1 && - activeShippingZones.some( - ( zone: { description: string } ) => - zone.description === 'Everywhere' || - zone.description === 'Locations outside all other zones' - ); const hasFormattedAddress = !! formatShippingAddress( shippingAddress ); - // If there is no default customer location set in the store, - // and the customer hasn't provided their address, - // and only one default shipping method is available for all locations, - // then the shipping calculator will be hidden to avoid confusion. - if ( ! hasFormattedAddress && ! isEditor && ! hasMultipleAndDefaultZone ) { - return null; - } const label = hasFormattedAddress ? __( 'Change address', 'woocommerce' ) : __( 'Calculate shipping for your location', 'woocommerce' ); diff --git a/plugins/woocommerce/changelog/49214-fix-remove-active-shipping-zone-check-cart-page b/plugins/woocommerce/changelog/49214-fix-remove-active-shipping-zone-check-cart-page new file mode 100644 index 00000000000..985ae712a88 --- /dev/null +++ b/plugins/woocommerce/changelog/49214-fix-remove-active-shipping-zone-check-cart-page @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Remove Active Shipping Zones check for displaying shipping calculator on the Cart Page. \ No newline at end of file diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php b/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php index b3cabefb20c..11f8bdb2d26 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php @@ -245,7 +245,6 @@ class Cart extends AbstractBlock { $this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ) ); $this->asset_data_registry->register_page_id( isset( $attributes['checkoutPageId'] ) ? $attributes['checkoutPageId'] : 0 ); $this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() ); - $this->asset_data_registry->add( 'activeShippingZones', CartCheckoutUtils::get_shipping_zones() ); $pickup_location_settings = LocalPickupUtils::get_local_pickup_settings(); $this->asset_data_registry->add( 'localPickupEnabled', $pickup_location_settings['enabled'] ); From 657a3fb99acff25805f51c484e372105de11000e Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:39:57 +0100 Subject: [PATCH 076/587] [testing workflows] Update concurrency to not cancel the workflow when running on trunk (#50397) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4ca04202cc..e9eecfb8193 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ on: concurrency: group: '${{ github.workflow }}-${{ github.ref }}-${{ inputs.trigger }}' - cancel-in-progress: true + cancel-in-progress: ${{ github.ref != 'trunk' }} env: FORCE_COLOR: 1 From 1488a028171e86ca3ea114d97e11342583b5e215 Mon Sep 17 00:00:00 2001 From: Sam Seay Date: Tue, 6 Aug 2024 19:20:31 +0800 Subject: [PATCH 077/587] Remove unused php imports from BlockTemplatesRegistry that were accidentally left in a previous commit (#49757) * Remove unused php imports that were accidentally left in ddabdb1bc2b0c29f53008ce7418f6a36bbcf5252 --- plugins/woocommerce/changelog/49757-dev-remove-unused-imports | 4 ++++ plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/49757-dev-remove-unused-imports diff --git a/plugins/woocommerce/changelog/49757-dev-remove-unused-imports b/plugins/woocommerce/changelog/49757-dev-remove-unused-imports new file mode 100644 index 00000000000..983bf338098 --- /dev/null +++ b/plugins/woocommerce/changelog/49757-dev-remove-unused-imports @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: Removal of an unused import is a non-functional change. + diff --git a/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php b/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php index 966af6dfd05..76cec00b179 100644 --- a/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php +++ b/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php @@ -10,8 +10,6 @@ use Automattic\WooCommerce\Blocks\Templates\CartTemplate; use Automattic\WooCommerce\Blocks\Templates\CheckoutTemplate; use Automattic\WooCommerce\Blocks\Templates\CheckoutHeaderTemplate; use Automattic\WooCommerce\Blocks\Templates\ComingSoonTemplate; -use Automattic\WooCommerce\Blocks\Templates\ComingSoonEntireSiteTemplate; -use Automattic\WooCommerce\Blocks\Templates\ComingSoonStoreOnlyTemplate; use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate; use Automattic\WooCommerce\Blocks\Templates\ProductAttributeTemplate; use Automattic\WooCommerce\Blocks\Templates\ProductCatalogTemplate; From 783f6dd3c2955008803f53c9e62ffa6fe956fbaf Mon Sep 17 00:00:00 2001 From: Vladimir Reznichenko Date: Tue, 6 Aug 2024 14:17:24 +0200 Subject: [PATCH 078/587] Monorepo: update pinned pnpm version to 9.1.3 in packageManager property. (#50402) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2920ad9d71d..ed0db1d6f99 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "title": "WooCommerce Monorepo", "description": "Monorepo for the WooCommerce ecosystem", "homepage": "https://woocommerce.com/", - "packageManager": "pnpm@9.6.0", + "packageManager": "pnpm@9.1.3", "engines": { "node": "^20.11.1", "pnpm": "^9.1.0" From b74bd01ca1d300a1dd5ad06f236e5ff0e0bd7578 Mon Sep 17 00:00:00 2001 From: RJ <27843274+rjchow@users.noreply.github.com> Date: Tue, 6 Aug 2024 20:20:45 +0800 Subject: [PATCH 079/587] fix: remove defaultProps in prep for react 19 (#50266) --- ...-remove-functional-component-default-props | 4 +++ packages/js/components/src/calendar/input.js | 18 +++------- packages/js/components/src/date/index.js | 13 ++++--- packages/js/components/src/summary/index.js | 10 +++--- packages/js/components/src/summary/number.js | 24 ++++--------- packages/js/components/src/timeline/index.js | 24 ++++++------- .../components/src/timeline/timeline-group.js | 17 ++++----- .../components/src/timeline/timeline-item.js | 9 +---- .../js/components/src/view-more-list/index.js | 6 +--- .../client/activity-panel/activity-panel.js | 6 +--- .../client/activity-panel/panels/help.js | 19 +++++----- .../components/report-table/index.js | 36 +++++++------------ .../homescreen/activity-panel/orders/index.js | 9 ----- .../marketing/coupons/knowledge-base/index.js | 15 +++----- .../coupons/recommended-extensions/index.js | 15 +++----- .../products-control/index.js | 15 +++----- ...-remove-functional-component-default-props | 4 +++ 17 files changed, 86 insertions(+), 158 deletions(-) create mode 100644 packages/js/components/changelog/50266-fix-remove-functional-component-default-props create mode 100644 plugins/woocommerce/changelog/50266-fix-remove-functional-component-default-props diff --git a/packages/js/components/changelog/50266-fix-remove-functional-component-default-props b/packages/js/components/changelog/50266-fix-remove-functional-component-default-props new file mode 100644 index 00000000000..6e876bf13d2 --- /dev/null +++ b/packages/js/components/changelog/50266-fix-remove-functional-component-default-props @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Removed defaultProps from React functional components since they will be deprecated for React 19 \ No newline at end of file diff --git a/packages/js/components/src/calendar/input.js b/packages/js/components/src/calendar/input.js index 1b3c433dd18..049b529954b 100644 --- a/packages/js/components/src/calendar/input.js +++ b/packages/js/components/src/calendar/input.js @@ -9,17 +9,17 @@ import { uniqueId, noop } from 'lodash'; import PropTypes from 'prop-types'; const DateInput = ( { - disabled, + disabled = false, value, onChange, dateFormat, label, describedBy, error, - onFocus, - onBlur, - onKeyDown, - errorPosition, + onFocus = () => {}, + onBlur = () => {}, + onKeyDown = noop, + errorPosition = 'bottom center', } ) => { const classes = classnames( 'woocommerce-calendar__input', { 'is-empty': value.length === 0, @@ -73,12 +73,4 @@ DateInput.propTypes = { onKeyDown: PropTypes.func, }; -DateInput.defaultProps = { - disabled: false, - onFocus: () => {}, - onBlur: () => {}, - errorPosition: 'bottom center', - onKeyDown: noop, -}; - export default DateInput; diff --git a/packages/js/components/src/date/index.js b/packages/js/components/src/date/index.js index e67a986039f..281d54ed538 100644 --- a/packages/js/components/src/date/index.js +++ b/packages/js/components/src/date/index.js @@ -15,7 +15,12 @@ import { createElement } from '@wordpress/element'; * @param {string} props.visibleFormat * @return {Object} - */ -const Date = ( { date, machineFormat, screenReaderFormat, visibleFormat } ) => { +const Date = ( { + date, + machineFormat = 'Y-m-d H:i:s', + screenReaderFormat = 'F j, Y', + visibleFormat = 'Y-m-d', +} ) => { return (

231D(`M*c^x4g(Y>y7GS^y`(zFx!=<{(m_*kHpSd52Tjl+_c%+7 z0PwqF;oHPCQ-=J06ci{cjeR=;H_(Oui)Z7RT!c{1UZi9g9EfBNd7n$+!S*5{Vd-AZ zMdI4R97zlLcu|YBS7m0gzNdt^ZW5Z64Op(hQtOv~Tp}mDg5o+XwwTZ7-Duis2p92? z&{#3~HY+plij@t%-db32EZp`%_orh5#ojuwB42I*2DrA zTLnl}l~-08s-%;F`mM&w$bd!w|A-d6sF(=X*j}J)w{TmHz|cExN_MVPTDtKaSeX3$ z&SJTKa#jpp4)T^&%$8%ox(rsi7XXEif!rV_pwgm&uxog{PB-?}p@GZNs1l9mH7IT%n8AlC-dy`3BZKcn3$2x$LVaqj_wPTI;>7%V_|wo5)i7mf%0h?d z+~gr`yL`;!yn21Wz2x)nhC(YiKRDSXk|q$;$>CMHcAdk;@R&K zYGo#sqI~0vUzd4|E4iycCB4i|Q#}b9IWEDge;Bgez5>|Tx=P66rm}6B34DgutV$?8 z4WYtS8p!p{|1o>t*NLb5TODiVoo!7{G)?Y@pY-=V}_-Dyu*e6P(8qqIJ+FwmPK5Vmh-#5sA!+^-}fH$k`bG7t&vqFD|PfT2b^R zV`|{b!G%juM5zzTt+Auh{4)G5`LO7qLgeiCcX*T__a2UXf{)$2gI8mr{s3G_`{loX8*YXj`D zN}j5je$vMmL_b-%w)yM>L*!S3-3RU~eU5gy6cJQDp*dw(la%b(N{!b!4b-EEf^egl zlMlL#aE=$53sX}XT7T~W5S)Cjh5F+~ZV`PSwr5S62r`_3GCKwSlvqwqfr$LSUIh3J zWy4-5_k5hDv$$n_BDF)c2H5x>@^DjEO*s43H`*bH3hk_d{)X~ zTHsZ`@g3b%=peF@4Rp*4=km_E^!l?{7vyr|ewUz)f01IauLSd~XMEwijs-E*acR{_ zm-g!?`%a_p>UMQX_6k&t22bVQ=U!L2^)Do&p?=igYDtbakyF|z|B#b+Am*1Us%#Mt zDgXzK;ScLuiB{Z8sTZ-@@~Nz%9xT)~`}%BXzTDMSJyx#)<4XK1!P99$391&$)=rXK zDw|9UVV#GLxnkrJpg8*q+O${EcPZ6H&64H2A~1s^)3d9qzZD#E$9i&i;}l(p8Mlig zr?UL*vc49i>8DcO$BKlS%#7URj4Yiiq4gwm-+$5*%6x%wNV#Ib; z3*w9ZpLdj_wF!~uKOgJW%JeYlMa@loKaQkYZTz$k$2!bynQ#`dQVMm z8w0omKd=2m(VH4Vx%%IKUojYey0nPOCUc^2yU}4k{831J)82k7QQ`)KjquUmJ{KM* zoZ69=G2tH*H88kGro9Q7Bu#nNJ@k?3`ILNyCw)Tg?L)hDzVS#6&fe+OL*C8oTPz|1 zKKX0~Rs}En$Ftox(&pFy7!=Qlw`H&Dr;CcQ-bd#Q980BjP4OAUj0x=o9{grePPlpQ zu4$uz`MUl~L2d8g=Wj92D5jnbhIPJuvv*>u5shg~>n8GaU(R1=jU!GHlN&n{9G<2U z5#eHfgID&y(N|StP#-v+HyuvVagm@Qh!B-ryR|khK_KT+n_p<~Xj4P<9^j3xY*Uat z`CAxOG=cNOt_P^;nB+6kdJ8@Xoq4grA6Hwmt7B}3ozZm*GwARV(fT3O>UHk^X_g|z zqdArKEHI!~{?2~Y&R5I~P!%N<(^iFh2*rC`NH9Zca~KP>1=Q7n!-sag%KwY){svu72_ zl6Fb<{fz<~zpMcR7-fUqWWJmaUzLly4-N%Yoc3=~bP1gdd5&IpYe$3T3q`{_w!RO) z-VkN{|H=VHcu_o!hM|$w# zAHVx_$u+JTp*F^fPRV$b3buSo##E;|HHZD!Vi#-`H<;ZInnKPgYHjaEnn#eO z#8FVMhAFTrD8j31%mAFsp13~r`#`Kq9UxE*HrthZq0B}k3W#Yx&67|jwN0A4KZl=K`v?fIfnvtoXg|PN{uu*Lk#eXGCoS!(-m;cbeleUvc4>iNBcP7X>AdTA3w(D+~p2Wt{N&bba-g zlw8gu)#v}kQZ*#cNf<(H`|PqF9OXWEJ9=x$zmg0JgbKj#FjHPfx7`hc3owF$JLT#i zHG2VV;Ix~o%R5|ihKec|6OG!o3rixmUx4j2bGgC&+~M3yC0l5+_C;~riFV?dQfmOq zm$WED{Rr}_vI4;&7_QBr0RZ3mhvX{B>j|<@AfWNupgAMDGW3)tangx47A$gtj{us;6sCz07;pmybaC(i|SmO;?VbcJpvxuo;_L((BZr$qW!*!4V1;E41o1L06g}z|H&oiM4}YXZ9b1 zN4Qn}F9P3kOXj}rm`D+Er*Jb65|S2^yU3zrFQ(-2~5mi&0Y zCs(Rpf6~UIV8}#ZY)}DnrTBHK9g04sP(^>)c<8I6v0X)$tT{ZqqxLJT5aO70d+ByE;;Kgk$Wzl^T!2DD_eoMy`0 z+m+m9M?+Q}B9X6!HNN`)-LH$;ArRTrnV5TwH_|CrwH%>YS}wKCD=Svc8Qjo#oO6n( zOg9Svi>g4CiZ6(cSfSkI#q1MSX-&EtF16uAL_~6=&_#Na6Y0Jg{)wISmF;Dk zZVuqn<9rJS1{SwqEAapBK!=u%nFZI}nAT4t;5j_J4Xz#Uw4)Xa^(eopp`p*YXf49U zoZN8Md6Q9d!I~*mLiL_JRj|!<(PL=*O^I$GB<5wR*5o-3i&u|H^U$hyc{!5cW9@8F zw`$L=R(fCNw}G3X5j)I<{0j4)?QXJLz-+W^=Cv`mnPv-*0nY&2s~?Z4Yj+aB#7B#` z7#W3s`hAO+!u2N=X)lnEmH zMp8u!9u7>rG@TJQ2Qv8USZ)s{0WsdC7ZB3ik|@l1<7vlpKk&MTKc$A|i;bbTz{AGa zm(Gv+|9|=Ut#V4LPcgs!y%$z4gLGAtHDg7DTz$4_^iRnU3n}!=^744w6z~t{7h-7> z%>z_z71`_Ap+11vvf>yO5vZk?=y;P!<9>QVF;ZFFCK74vk*3YFatBZ3QCoGE`+<8( zElwoMWb&Gb6IyCgs^@0|1|FH^t` z2#u8pa<5a$_mf7auqsq6Kd(=|A(Mc#G{K7U7`Aq;}pP z;iI({5!Z5n0rRZTD7lg($J=;v02=Hc;grNQGdZd0&5Kh6Wu-_DScH$?YZt>o!i)k$ z1(EMHi6e-#G#Cui!q_JdJWD($J{;6|ELAr6nClcs*Meht8tP% ziYa@ZiTV=ZYbt40SaP|C*~N4;c_oLRk~l5lEmkbUIjlOw7ycV7UqJ~}Y~+!v`#%3L zU<~gwpbYLu4i^j7opMYl6Vu_fLW(R^ithhgQSn}n08aB!>iDxTC7K*N=sZlyxhu zAz(1AgCd-P6MS+y^(M*gom(kd>~D7FTp2Vh{$K%7Fk?JrD5*3& zkri;22JGLi6a`&MCTcK~%H1jpR|%3e?lY^%56WG}kqw@F2H>kn2F%4? z0@1xxB`TkZT8$tifF@2m#fl1mB^|%n0=VgzlGtpvkB@Ophwe@l&&u8*@3X*+>poUp zSBm^|@RvPe_?0zX7U+;Ndeafa(q{8&=;B89yN?jEu1X`Tm$n6)utES{1w7lDD9jor zP7ApD0O5yOXMdy^wX}Y#P!Vm?J?8vApNC(Io{mj_B-mq zSkeZmWbY@;tTxs?jp`W#Au;cTR4awueV+Y5SI@|p%@z@+BPeq>$}D0#Jm>F-}q#7%Uo@ z9;Ivl?;_Wa3DdO;QVKy?ZAOvsFQg6drX&%N5+2Vh0Pe!1kn@?SOSPnDTQKjxI~#$+ z5~s~(NA7-r!m3-kpKb&}Z#yP8R>>~|G=^A%Ymz%bhTwly)-Ko$0hZBpl?d35UI#2m zDnc26K2bF6V%pe)lL|iyDOS*XJ{9_ULFf!7T2+k+L*?HG*ol9DRf08UZ{+3GDhv#? z3^0|ZH5NvHs$MJbuy;ZoX=)PQQLUS`czS z&`Usvi)>(T)d=sy=#-w7WFVIO62O&zdHrBqzG{dCB_J#I8I5Bqo_yNr6q81fCBxlT zlyzBtDeqKr@>8K5`!tivN^q@zuMLlI0T%Xi)7qHJ_1eu_ zrFyVf%yT0h6P}ExWk*VRUsxc2s0K|y&4ohnWotZH*;JUxt@-&2EK!p(qzH`t@Kf%9 zHve@F!v6F$%Io3y6F0s-t!T=0jD>Q)5Zf4%PeIT-^IrWTdR?7}trVrb;b!1o31B#D zzXo-uw1#y0L+=A#fO;wf`S8{TIGev(-;49#D}5X(6DsUz%MjYC;!A`*?6y6h-ugp^!UD zC1NeY2o>|1g@bqWG?v0mdO3SbZ}o?zkAbMHFVJB7H}|uWBrMsx3W8b-WQp~f50AJC z7aCpeCa>f`oeKoK`*LQJ|Dpc4S>$|&hqe={XE?yFv@y=#X1Ixg05-z?m< z>d2|Tux{?1jD^uFz%X8Pek??cS>|iHCD@>(q`=EU`W>L1L-w3NYOc>Q)H#7ZP*Uh| zLd6yQ@D2aWJN(Ab$hh*2Y~y*Qm}!kUHtP2yT~79@Yl4C`bW=0Zw_mqxR|$v@#zoE? zfrWyn2w2w_KhtdO-UQvhyy?3s4AG?;rtDj8N;I+l8()gZ3=-dj*uML#Zv-3F;jbU}Db*|jKUo^I%Cjn`9<+i$H)e2JnkE^0Q0Qy4(>%-qAn{>HW zt1_IpBeJ)~Pdg{y+*r#AcNL=DUf+7lV=$37QPMo-E|pw}CcPs`s?j7k8e}<~DlEU2 zEr{Fu7lPWz=kdv(`03KvnL)M5|63t=f^Wb?gmaLPYnP5?S+P>Gp^?~T?`SL8TGaH< zLrk%&Sz6}r3?rY${Q~?Qnex;r)7@3p9`R3iQW|k}(^pEG;MCz7MqC&sI!#v9hS_wArqn3JdElUmC8*Jk+?A8VXr4ky?O9%hCDW?V8H5Lt(BC+!Bl!*lY$!T*eq zjWw*uHCtmfP;gM6{v~>PA}3!dNBe|AWT5OLzaA!;L>aUu=?QeSNTDZc^E{_6g(I;s zdn!!YoB1ryo>+6P-G4*JnH>IEzGz0pOi_|MFOzC>>y5h$d!mb^l^xW@wMmNmm6D+( z?+6R6(lr+|ijo7RwnV$n=Qg6*@+?`P>uF@lTPZSgJ0s2|k9STrGTq%Vg5TWa4O=6;0g!f8a>=>$b`vp(i~NZR%*LpMWT!{Bte znkRV5mCy$T&@_Gl+$K}P3f-8lt!zI}73Y$3XRi?m$84vOGpreZ&#pAmqHI-Ct|#FD z=>^>b`=SAYX|hQaRyzb&HEf5U$`#Vx2kpQOo*^a7DVNBynCPC~AQ_YLcofe`24cVf zfG++;BE_N3w^MmJ?pcdtfT*=gTO-TAs33sKWQba9#&#=j%XOQ9ew{A8GCQGeDeg2` za}Cj@8|oI?b4qjmEYe05!Bv>~E^%}15Pdpyz7pxBD$P|uJyLkee3&>vEfGD6A-}JZd*GFbK&~&L*T`-Dg%dngoXygUZ|X%F(XvuuhKGM#S>qgbk$YF4>{7 zXBUK$y}i9X^n{|X-CSL}I~?(-5x|Ymnz{d>57NhSBi=nTWQ_1)e#wLCTym2Cdn&mr zzdGxv2_Ul&Z#K3N#>X3wPgE#vi2<7C8@+Dp$vsHI7=#(_1wT{g&unY{51c}2>_0f}R&7HJ^sq~s`S)0w8JP1a{f(wJBo6ZsblB6M1 zX$61@^S?b2bZ~A00Mi7`j#l8N_k-fwY3a2q18Ury^0NQv6)Drx%%r9+{chX%^6viq zkidKK<(MWjWIGV;-h|vQa23iy8&{i$0qA7Wtc3T|W}2>Fg@@3prhaM~5eKR*+qQl@ zuoj)^$y|#=u^E~Dqx&yp+d<6DEX#k~b1cXb=+dN(EnhSfDige4e`uJ2(NWVQu`3Ot zfj=evjnYxyz91`ACeR`#ITqLj#!|V4lGf{qMO;~#V8WegoC3v)rYav-F7>F+rs_or zkbr`5^S_A}ciMJ72UK)i!({7`;s-x3+lMdBf67Eme2?<{gV6g__Wax_)K zonp{sU6=i8Y912Nyz;9pt3Q6%C*k&PL?(J7AT>2v>zav9wFNlh;>OSk2ns(*U~Ta# z5i-!k*xM0y&nHjPU#Qx%f74QTOJTw4W|s-r*2nL}7tcuX_eTmnBHmCKF>4zakM1s4 zmv4H-o%0Wpn+Mv+ogy(#?WX3>uf}a$$?4BhD0ZTUrm9>*0{pyszyH~zdnFtr?SWUQ{X`$D7P2m3})UH$WTV}@1?V7}LB}Zk;__#fWF)Y*a}X9iJ-qW|0diPyVTndqhv}()?B$ z$T>#LJA`ssO` zYM6I?i*#>H_qJVALIiv^gN?*A+YE=Z-Sim^{42`kz20(croG}$ol zGx=ac^c6bCKIc8Tnz#LP(?!qmu5e z9fq<=&eu&ZiElg__{KR~x*IBtI6!4UM)ZhNO$npTr&cH85qYp_wH8k%doETZx91z8|+ zLHLgyR&D1v2vcf#^xkSpQMp#c;z?lGoxFqgg(=JSGmn%R&N- z`cy;z`I$&p>2j>@g9Cntxv=(52OGkysqap9%gdjJCx3=>=)JAj&t2KUe<&-qv58tO zG_VV2w;)rmMKrnXh5l%m=$LtvoDGV#(1Q}TIam}UZCiY%zUlo$pyRKmJ3P^N9~7k`YsflA)Fxg{gg`$y4{p8s41xU;Fd0 zc>0HJ9AJC`pUWRX>|7K1JTYoTAK;a27C~3$d@uWEMcOS`38TCX-YeS|1m6kP2X@UO z%?!)O{&Cxi113GovE&M5)qrd5V6|(x=8;Gyqg|+f$~%4R zb=mV_@@rUo!%a5g?f5XVYcZ1TLYKEb*`D@omTYpYQ{Boz3s3RmurLn^O`g$&>df*v z;L`9}q|0(-S57e>2JBMX!in*==s4ge1g7XI88CR?2p;*VMNGBy5<;V41dLQ=@;(ix z`OfjYu=MI}-v~0i|0H$xjr6j zZ%d^SjlkAGeZVANFe~bDR9^J29ID6vQx*k^-Wss%6#8Hgg}dbDBf)3M>%QOQiOY*D`ogWcfbr*$Jti zT9>-~NKn*w{)6!YcezbW1tN;y<+GUjysmJ;XMGqAv@?Q;*uD!m2y28qW&{{iI;}0V z9z44b!07rzC&#B+1-xiQ z<0C)5gFCMK1Ex1SiXBB^l7fho?e}HwVJ0FeKn+R_NcCrOo?A$?C8v8UW|mSZDMt`T zz-8)7UVqwsy~PE9ae<#a5iz5In~qgWbGsh7T5j$bzD!0WbJa=eBk|&YraOI$f^5{#l-82Iv;V#ts%(8hgAXCZQyF%{`go_@$HqDXx4>Fpq8d&mFD;7>&FD zX&D+J^Vrt+tXK$%oj3AB*VN=iH{`|h4?-z51UA?+Y&CiP7c3Ybz}6OYfV^I0vVOnw zathiE>)C-YMF7>eBRe($P#U??(XYdyz zv2zwX)#Tt8b%=W@ybq9e9^`e$NX-YO(uyBAzMbtvPcC~A^N2WRG2X7CHMnKwU9?19P}G@SKF+(d*;n%%fY=QVb}2Y ziOr&CAkZy^@6l;~~1q25**$)3=p|a(7pO;tT&F{BjZ2D~pTCkK+?j_?aZ zPu~w3wN+qLVXM=TUe%8@k)-q0vWX1lY6LCjBoHE?cg>^9ttP;#`H)ZD*0JoC630nf z8xo|=%#>_B<9PA2Zrn4u5duiZZ{~cl(S*$=XXJI!%ST`^NFxj zv3ztW$ZQ?>FxpqJr>)lkgGt*qqTm-T!H&xx+4|Cv7CUc{!1g4z#tvJTcT|K#dV$>| zx>|9%F3+xeIk2w78>LQ*T-wQ0gDh$w?Y?&ZQ_e>8++*j>G(_yi7c$^YeGE(zqun`y z^(FII1W`z|&1QDLk@^UjtJpO)h!)ZmA_FFaxfTKpDgt>VK1g&Q5M8BZUYJYoZC^M6 zhfujGCEQ!St;n&{xGlTWt8e*Kv$=+iN&2a6!7ql{7pZU{7fVnBTJBga!I5ji2|elUiej zw&a+{<#BTESHDCUCGV5+Sv3xv6mWYNn14@yi>@N)O*f&(v9hw^nww=aY2<+YTspy< zGw@QK9qtdEYKU*5+)!hDzmkbNa2&-tUs_KMGTkA7Jj| zS_4+AgD00InfqfZnVFz3qpXst7O#Of1QswK|y;KxWJ zxndWnnW(8ip4boY1r^ml(2hG1Y^ogYtGnmAk)RttQqn)T#=GBhvbRUn{i;trfrpkk z%{MjL9q+kmO#}4IcC={?&52}90x{Vz7nI8bFk9m^Lf-3tv<6J#BEZ(*UDF~O({q#O z65L>a^*DgaZ$f@cl}`AICT0nMB~NbIciv0V90Gp9EO>WTiU?)iFvGo^UFNo1}Mx!5wwy<{XUHvK)__(CuBq`=Uj1nT}-$W)YUKNMydM=vV#)N z8bpva968qfg&{EqhZVf$v7Ra}P)0_Z8)HF`FlM|o=wAB1w&;EB5$rQ99vO8_ z8WSzKhIsRD{9LQ5x@UwOWTu$`4Z?YW@Ul0hglo2dXA~BHlbgD%b?;;$iq(CUcc174 zQg;L|a;%zgS^c1O#=JnZa1!?_ zATw0{lXM-Hg5~ZNz%NI6b>1IV);G3~k{*IEh1%wL{o=27c))HVI7>zMR0<$xE-%%Lk5{Y(>SF`J=$01EMxXa)7qxJzhFvQZus83KMFOaMqd5@{y8KLO|IwZZ6nG~?VDIUxUx zzQ@V`yiSvL5E#*8*OItrq>ZUX@2%^pshdZ`p#22u9UiZfJxDsdS|8&egs~7vHHefX zpCphih5EJ^H>(qK_)*SQT&Mj#$JqkbSCBZ=-NT%w$$fOjk~G0~hF*PZc{VX0`TDqT z@SFh5J!mg;6pb^5Q4)d_*3VqQwx)2!IZweM+}-6}uC8mu_|Pe3vAng2>zexRlx}k;G9HesbgcWkZ zB9aftl(r&f3M32pYbEEZ=h^F&B~Oe^%5 z2Rlg&e?*`gP5z9;m^{h(N-2aDLKqE!2?MW6^|G>~;(nU-2O3&>P3W_4&n;;o{mdG~ zj;{s>S9KCqI4&i=P^c#@$vD0qOe2cDKOj3uqmh~(M|D+#il~8#35R!@FTe+yfaOog zt2G)zBJaYUedr^4JtQ3SyEPv%7834q9`dSr#C-j`Q}yptrbiKm4CbeNe*EVkm;g&j zn8(|&u+qEFDTPBMCs+HF|99tjk|8|~G<&LQ%#(M_`6d5o#+ryX-%giSpBk$&)Vdbg zEtHl)a1FTA9|yO0ZM9r5x#WV%vaZ0HK=;Y#62qwl9piE#f^yp1ny?x zR^V8P1h$m;%6>4jR%-`uSe%o|r^)$B^qJ1GUTFe_jAI%iehj|%Gzk$YT9CbZ?gR)b zcmW7*)34SfWubuC2Ja_pqD5n(vkgplgEq8(F(T5u)rgg!cm0E7=Jk%^H2GDiS6o!9 z#rSO`MD$+>#b)cU2xeV<*~drZRcm6#UaJbZC~O%4SRZC@(q*;if7F5YuRvK1-E7T{ zmhj({3Cpv$^O3bDeXFb82?UurnK^VlQ#w1P1q+U}^19yYltdAE;M5J`9YBCUL>rKW zp#O%Dy|e$)jq_u1|E*Uz=Rf12Wc}9+VFm6pZz`@ zHAMtsKZ@8fjbJS?l~W?5XN?TN!JI;$5TB$^env|?jhE17!3DAOW)eHK(+N`ZNR_o& zSOxDp%Cbs~E7|a*L}`mM6EncHkWzW;>?q3MCQ&BKeFwn4la{RVs}dGd^y5Lgo9 zh2omPa+r57mTo>k1Y_7puk&m#^xl=cR5soG3Ce2%N4QzQDtMDl z336xvPev%Y+DjA2dBXA{KrIjns-w4bs}bsHyrXV+Ts`m7n0!N7#6&ydB$ngOe!d5D z^Xl{S@!pXW^ETThX}jWy`=o;X%x~FN&()LnwPgpHFXdb7 zIoSQVcrmxEwpcE%9Auvgt;1XM%mWz^5LOU5AT2&9mG)?y$?C**E^A$9>L(#y^8tx=z@mf@mJw<4wosy8=XkP#t3CgXv|Jl9$ zf4{_6fA|U&=-AJV68BZO`c9&#znrTk?tvPN+2QKSxj1;7&|dtLZKsNcLNF=y3eOgy zf{-#26Lmn0zipmSlfJ)D!Hh~G=Aor=y!U%y%9?B_r)PYd|JPx#_b85pm>`0I%d z>NQYp0%r4%|-*{ub zx7jMkIA3F_Elw*&G`K&%1GdKh*GDC_Nu?mH1bgWDN5M-WS+PvrROl%yIY}zM4P*mG zVTa!E)mL1q8a0eIIon(ogafb-r2KR!4=3+}UqcF-h>D%S|Giq1KUM_=kTqLf7-U{r z7PztC0Nbh4-6ichPevQd!(K~8i7~^BIJHs+X*1~{;AHKdqTQh+*P#c(KgR~l@TpMv z1ehuD*9#6H8>ypnLd>PM0Dy6Rc|XO)LeK20A~kbZ%zmwZvGsaGNs{)H;#bsMp)x`O zR#e_G6>L&o4m_(ViGxV@EmY45Q@U(N@#BOlDX*Y%$2?W^*70=N%oP+HaHTZYlOXVp zPcaLE%8k%2{f{UFuUHDak|K@!^oV};uX;K{*OmDDCrjJK!y=;bYtgm1OYzz0P627xAz6w=E|P_ILitba$VILjWF)Lxr`X3; zpWUCAWOI(WS)tCPJT3GOja7=+;9u8hz&z37T+_H~AjGx8$iq>J;WZ||`+#`qf*0ss=BB z+n)Z+)J)9nnHY0yGrdN4Wz<}eGU85pDJG&Oti7TaPFpJ z%&jKho^_B}-BoCr1!TiB=qb(ETRP8$$r)D6RCm_PfiV$WuwJ-imba3Lqg@SRh785A(HxJ+2hdfnAHaOY zb1jvr?VtzP_&V#B1E6-avX@Tj570`3_Z@ngTUF_?@O(P`Sh z+-s!6iThozt%HOe8bgaxD;Mrbuj*JRP<_ITAtF@me*Segx(9n}dQ(!K7j;;gED;JI3ls)3J5(l#tM{d)k{?mA8y3=F`12lDA{d_rB>)G!zv ztUzC`XdodF7@FG##yMoD5E9)d9eFF;WBb^-lVi@~2SHLnG$nA7aN*&ls}Mz;UD!7= zRcixP#i(%78t!!}w~s2v$$+3HYQ<*rLd;&hjbOWqw@Pr^b1)0;At25=!QS&P#PYh_ z_HLCp4+iKr3TKaCJ|5y!Z0Ej#*|z|%s%I>m&n4!|`RM4Ut(x3q98E!)+_va*S@+r& zNmZ>t4g{kx8#zBMFbykyrUO#ucy!mdSTbGza>78aXal&H$tnSOu7@Wmr0Lk`?o6#( zw9=cQO(fxk%)n(!T!en5c&$vMamp%|gw>If!p8@saUc|A2Lb7Kh4&cm2{*8PGUs>w zK8l47MXSK#9KhZ?&8bp5@2(>Z|3177Eo+Fr+S`}(`K&EzyQ0eT+mK)&D;z$l=CNIx z#P%{oQ#A?iU%Yf_`{bAI;vETm?4E*7myTYs)suoUtD*mHMxm}{!MBl7nDq*lpEX@d zOZgVCa}buCx#r9YrolFt1>_)@mv|NmTh3kYXH6zH%dIZu_GVR}6|slAoi; zdl>L)7&Sh~`&!Va91Bi#C^}b52R!Ty5x@Ls|P*hel#0rh@_5ZyU{06LQ zwV+N64Tk-2#WhOc5-`uqiG_-u;ne}^s04y?k)c;Zxc#FK3QaHVib9SF9syS=Z{{g; z4cya)SVEbE$88zj9|zMJTb&YGUQ%8k@?I3NW#>B^8jDl7!fK+K9p<3+M9Eh*BPQ;$ z&PxHloqJfEsTN4D8`vC9eBl|7~#W!QpW4XpY5tp*)!K+a>}&V4DJLR`~7(Q7{P40~`KWgyADf2A