diff --git a/.github/project-community-pr-assigner.yml b/.github/project-community-pr-assigner.yml index a5af848c7a353..258ee45ce218f 100644 --- a/.github/project-community-pr-assigner.yml +++ b/.github/project-community-pr-assigner.yml @@ -13,22 +13,22 @@ - team: developer-advocacy "packages/js/api/**/*": - - team: solaris + - team: vortex "packages/js/e2e-utils/**/*": - - team: solaris + - team: vortex "packages/js/e2e-environment/**/*": - - team: solaris + - team: vortex "packages/js/api-core-tests/**/*": - - team: solaris + - team: vortex "packages/js/e2e-core-tests/**/*": - - team: solaris + - team: vortex "packages/js/admin-e2e-tests/**/*": - - team: solaris + - team: vortex "packages/js/components/**/*": - team: mothra @@ -51,10 +51,10 @@ - team: mothra "packages/js/dependency-extraction-webpack-plugin/**/*": - - team: mothra + - team: vortex "packages/js/eslint-plugin/**/*": - - team: mothra + - team: vortex "packages/js/experimental/**/*": - team: mothra diff --git a/.github/workflows/pr-lint-markdown.yml b/.github/workflows/pr-lint-markdown.yml index 0e082fb0cb9e3..175809ba4a4d9 100644 --- a/.github/workflows/pr-lint-markdown.yml +++ b/.github/workflows/pr-lint-markdown.yml @@ -15,7 +15,7 @@ jobs: - name: Get repo changed files id: repo-changed-files - uses: tj-actions/changed-files@v37 + uses: tj-actions/changed-files@v41 with: files: | **/*.md @@ -24,14 +24,14 @@ jobs: - name: Get docs changed files id: docs-changed-files - uses: tj-actions/changed-files@v37 + uses: tj-actions/changed-files@v41 with: files: | docs/**/*.md - name: Get docs manifest id: docs-manifest - uses: tj-actions/changed-files@v37 + uses: tj-actions/changed-files@v41 with: files: | docs/docs-manifest.json diff --git a/packages/js/experimental/changelog/47207-dev-fix-experimental-tsconfig b/packages/js/experimental/changelog/47207-dev-fix-experimental-tsconfig new file mode 100644 index 0000000000000..6bcc3eb4c1414 --- /dev/null +++ b/packages/js/experimental/changelog/47207-dev-fix-experimental-tsconfig @@ -0,0 +1,4 @@ +Significance: patch +Type: dev +Comment: Fix a persistent build bug where TS would try compile files outside of src and typings in packages/js/experimental + diff --git a/packages/js/experimental/tsconfig.json b/packages/js/experimental/tsconfig.json index ab54a9b2d666f..064c7643b1809 100644 --- a/packages/js/experimental/tsconfig.json +++ b/packages/js/experimental/tsconfig.json @@ -1,5 +1,6 @@ { "extends": "../tsconfig", + "include": [ "src/", "typings/" ], "compilerOptions": { "rootDir": "src", "outDir": "build-module", @@ -7,9 +8,6 @@ "declaration": true, "declarationMap": true, "declarationDir": "./build-types", - "typeRoots": [ - "./typings", - "./node_modules/@types" - ] + "typeRoots": [ "./typings", "./node_modules/@types" ] } } diff --git a/packages/js/product-editor/changelog/update-replace-with-combobox b/packages/js/product-editor/changelog/update-replace-with-combobox new file mode 100644 index 0000000000000..b77824b735841 --- /dev/null +++ b/packages/js/product-editor/changelog/update-replace-with-combobox @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Product Block Editor: replace custom select with the combobox control core component in the product attributes diff --git a/packages/js/product-editor/src/components/attribute-combobox-field/index.tsx b/packages/js/product-editor/src/components/attribute-combobox-field/index.tsx index 4b33fef441560..9d42d7c411048 100644 --- a/packages/js/product-editor/src/components/attribute-combobox-field/index.tsx +++ b/packages/js/product-editor/src/components/attribute-combobox-field/index.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { useDispatch } from '@wordpress/data'; import { BaseControl, ComboboxControl as CoreComboboxControl, @@ -15,18 +14,11 @@ import { useRef, useState, } from '@wordpress/element'; -import { - EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME, - type ProductAttributesActions, - WPDataActions, -} from '@woocommerce/data'; -import { recordEvent } from '@woocommerce/tracks'; import classnames from 'classnames'; /** * Internal dependencies */ -import { TRACKS_SOURCE } from '../../constants'; import type { AttributesComboboxControlItem, AttributesComboboxControlComponent, @@ -101,22 +93,13 @@ const AttributesComboboxControl: React.FC< help, current = null, items = [], - createNewAttributesAsGlobal = false, instanceNumber = 0, isLoading = false, onChange, } ) => { - const createErrorNotice = useDispatch( 'core/notices' )?.createErrorNotice; - const { createProductAttribute } = useDispatch( - EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME - ) as unknown as ProductAttributesActions & WPDataActions; - const [ createNewAttributeOption, updateCreateNewAttributeOption ] = useState< ComboboxControlOption >( createNewAttributeOptionDefault ); - const clearCreateNewAttributeItem = () => - updateCreateNewAttributeOption( createNewAttributeOptionDefault ); - /** * Map the items to the Combobox options. * Each option is an object with a label and value. @@ -155,48 +138,6 @@ const AttributesComboboxControl: React.FC< currentValue = 'create-attribute'; } - const addNewAttribute = ( name: string ) => { - recordEvent( 'product_attribute_add_custom_attribute', { - source: TRACKS_SOURCE, - } ); - if ( createNewAttributesAsGlobal ) { - createProductAttribute( - { - name, - generate_slug: true, - }, - { - optimisticQueryUpdate: { - order_by: 'name', - }, - } - ).then( - ( newAttr ) => { - onChange( newAttr ); - clearCreateNewAttributeItem(); - setAttributeSelected( true ); - }, - ( error ) => { - let message = __( - 'Failed to create new attribute.', - 'woocommerce' - ); - if ( error.code === 'woocommerce_rest_cannot_create' ) { - message = error.message; - } - - createErrorNotice?.( message, { - explicitDismiss: true, - } ); - - clearCreateNewAttributeItem(); - } - ); - } else { - onChange( items.find( ( i ) => i.name === name ) ); - } - }; - const comboRef = useRef< HTMLDivElement | null >( null ); // Label to link the input with the label. @@ -292,8 +233,11 @@ const AttributesComboboxControl: React.FC< ...createNewAttributeOption, state: 'creating', } ); - addNewAttribute( createNewAttributeOption.label ); - return; + + return onChange( { + id: -99, + name: createNewAttributeOption.label, + } ); } setAttributeSelected( true ); diff --git a/packages/js/product-editor/src/components/attribute-combobox-field/stories/index.tsx b/packages/js/product-editor/src/components/attribute-combobox-field/stories/index.tsx index 368909d3d352b..a4eb3a4dc317d 100644 --- a/packages/js/product-editor/src/components/attribute-combobox-field/stories/index.tsx +++ b/packages/js/product-editor/src/components/attribute-combobox-field/stories/index.tsx @@ -3,99 +3,91 @@ */ import { __ } from '@wordpress/i18n'; import React, { useState } from 'react'; +import type { ProductAttribute } from '@woocommerce/data'; import '@wordpress/interface/src/style.scss'; -import { ProductAttribute } from '@woocommerce/data'; /** * Internal dependencies */ import AttributesComboboxControl from '../'; -import type { AttributesComboboxControlComponent } from '../types'; +import type { + AttributesComboboxControlComponent, + AttributesComboboxControlItem, +} from '../types'; export default { title: 'Product Editor/components/AttributesComboboxControl', component: AttributesComboboxControl, }; -const items = [ +const items: AttributesComboboxControlItem[] = [ { id: 1, name: 'Color', - slug: 'pa_color', - takenBy: 1, }, { id: 2, name: 'Size', - slug: 'pa_size', - takenBy: 1, }, { id: 3, name: 'Material', - slug: 'pa_material', - takenBy: 1, + isDisabled: true, }, { id: 4, name: 'Style', - slug: 'pa_style', - takenBy: 1, }, { id: 5, name: 'Brand', - slug: 'pa_brand', - takenBy: 1, }, { id: 6, name: 'Pattern', - slug: 'pa_pattern', - takenBy: 1, }, { id: 7, name: 'Theme', - slug: 'pa_theme', - takenBy: 1, + isDisabled: true, }, { id: 8, name: 'Collection', - slug: 'pa_collection', - takenBy: 1, + isDisabled: true, }, { id: 9, name: 'Occasion', - slug: 'pa_occasion', - takenBy: 1, }, { id: 10, name: 'Season', - slug: 'pa_season', - takenBy: 1, }, ]; export const Default = ( args: AttributesComboboxControlComponent ) => { - const [ selectedAttribute, setSelectedAttribute ] = useState< - ProductAttribute | undefined - >(); + const [ selectedAttribute, setSelectedAttribute ] = + useState< AttributesComboboxControlItem | null >( null ); - function selectAttribute( item: ProductAttribute | string | undefined ) { + function selectAttribute( item: AttributesComboboxControlItem ) { if ( typeof item === 'string' ) { return; } setSelectedAttribute( item ); + args.onChange( item ); } return ( @@ -103,11 +95,31 @@ export const Default = ( args: AttributesComboboxControlComponent ) => { }; Default.args = { - label: __( 'Attributes', 'woocommerce' ), - items, - help: __( 'Select or create attributes for this product.', 'woocommerce' ), - onChange: ( newValue: ProductAttribute ) => { console.log( '(onChange) newValue:', newValue ); // eslint-disable-line no-console }, }; + +export const MultipleInstances = ( + args: AttributesComboboxControlComponent +) => { + return ( + <> + + + + + ); +}; + +MultipleInstances.args = Default.args; diff --git a/packages/js/product-editor/src/components/attribute-combobox-field/styles.scss b/packages/js/product-editor/src/components/attribute-combobox-field/styles.scss index be1332c266874..67b4ddc3aa4a3 100644 --- a/packages/js/product-editor/src/components/attribute-combobox-field/styles.scss +++ b/packages/js/product-editor/src/components/attribute-combobox-field/styles.scss @@ -12,16 +12,10 @@ background-color: white; > .components-flex { - height: 32px; + height: 34px; } } - .components-combobox-control__suggestions-container { - margin: 0px; - padding: 0px; - max-height: 128px; - } - .components-form-token-field__suggestion { padding: 0; min-height: 32px; @@ -29,7 +23,10 @@ align-items: center; .item-wrapper { - padding: 8px 12px; + padding: 0 12px; + height: 36px; + line-height: 36px; + width: 100%; &.is-disabled { background-color: #fafafa; diff --git a/packages/js/product-editor/src/components/attribute-combobox-field/types.ts b/packages/js/product-editor/src/components/attribute-combobox-field/types.ts index fcdb19f8f60eb..ab3cd73ec0ba8 100644 --- a/packages/js/product-editor/src/components/attribute-combobox-field/types.ts +++ b/packages/js/product-editor/src/components/attribute-combobox-field/types.ts @@ -8,8 +8,12 @@ import type { ProductAttribute } from '@woocommerce/data'; * which is a combination of the product attribute and * additional properties. */ -export type AttributesComboboxControlItem = ProductAttribute & { +export type AttributesComboboxControlItem = Pick< + ProductAttribute, + 'id' | 'name' +> & { isDisabled?: boolean; + takenBy?: number; }; export type AttributesComboboxControlComponent = { @@ -20,13 +24,13 @@ export type AttributesComboboxControlComponent = { disabled?: boolean; instanceNumber?: number; - current?: AttributesComboboxControlItem; + current: AttributesComboboxControlItem | null; items: AttributesComboboxControlItem[]; disabledAttributeMessage?: string; createNewAttributesAsGlobal?: boolean; - onChange: ( value?: AttributesComboboxControlItem ) => void; + onChange: ( value: AttributesComboboxControlItem ) => void; }; export type ComboboxControlOption = { diff --git a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.scss b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.scss index a9c3e722c61e3..71832d9495be3 100644 --- a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.scss +++ b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.scss @@ -26,7 +26,6 @@ &__body { min-height: 200px; flex: 1 1 auto; - overflow: auto; } &__table { @@ -82,7 +81,7 @@ &__table-attribute-trash-column { display: flex; justify-content: center; - align-items: center; + align-items:flex-start; @include breakpoint( '<782px' ) { position: absolute; diff --git a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx index db34eb4496590..92fb26f11a8d2 100644 --- a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx +++ b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx @@ -15,6 +15,7 @@ import { type ProductAttributesActions, type WPDataActions, type ProductAttributeTerm, + type ProductAttribute, } from '@woocommerce/data'; import { Button, Modal, Notice } from '@wordpress/components'; import { recordEvent } from '@woocommerce/tracks'; @@ -22,7 +23,6 @@ import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies */ -import { AttributeInputField } from '../attribute-input-field'; import { AttributeTermInputField, CustomAttributeTermInputField, @@ -30,6 +30,8 @@ import { import { TRACKS_SOURCE } from '../../constants'; import type { AttributeInputFieldItemProps } from '../attribute-input-field/types'; import type { EnhancedProductAttribute } from '../../hooks/use-product-attributes'; +import AttributesComboboxControl from '../attribute-combobox-field'; +import { AttributesComboboxControlItem } from '../attribute-combobox-field/types'; type NewAttributeModalProps = { title?: string; @@ -308,7 +310,7 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { } function selectAttributeHandler( - nextAttribute: AttributeInputFieldItemProps, + nextAttribute: AttributesComboboxControlItem, index: number ) { recordEvent( 'product_attribute_add_custom_attribute', { @@ -378,33 +380,30 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { } /* - * Get the attribute ids that should be ignored when filtering the attributes - * to show in the attribute input field. + * Get the attribute ids that are already selected + * by other form fields. */ - const ignoredAttributeIds = [ - ...selectedAttributeIds, - ...values.attributes - .map( ( attr ) => attr?.id ) - .filter( - ( attrId ): attrId is number => - attrId !== undefined - ), - ]; + const attributeBelongTo = values.attributes.map( ( attr ) => + attr ? attr.id : null + ); /* * Compute the available attributes to show in the attribute input field, - * filtering out the ignored attributes and marking the disabled ones. + * filtering out the ignored attributes, + * marking the disabled ones, + * and setting the takenBy property. */ const availableAttributes = attributes ?.filter( - ( attribute: EnhancedProductAttribute ) => - ! ignoredAttributeIds.includes( attribute.id ) + ( attribute: ProductAttribute ) => + ! selectedAttributeIds.includes( attribute.id ) ) - .map( ( attribute: EnhancedProductAttribute ) => ( { + ?.map( ( attribute: ProductAttribute ) => ( { ...attribute, isDisabled: disabledAttributeIds.includes( attribute.id ), + takenBy: attributeBelongTo.indexOf( attribute.id ), } ) ); return ( @@ -446,20 +445,29 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { className={ `woocommerce-new-attribute-modal__table-row woocommerce-new-attribute-modal__table-row-${ index }` } > - + ( attr.takenBy && + attr.takenBy < + 0 ) || + attr.takenBy === + index + ) } isLoading={ isLoading } - label={ - attributeLabel - } onChange={ ( nextAttribute ) => @@ -468,9 +476,6 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( { index ) } - disabledAttributeIds={ - disabledAttributeIds - } disabledAttributeMessage={ disabledAttributeMessage } diff --git a/packages/js/product-editor/src/components/attribute-control/test/new-attribute-modal.spec.tsx b/packages/js/product-editor/src/components/attribute-control/test/new-attribute-modal.spec.tsx index 472c17b7cd775..b80a919955c0f 100644 --- a/packages/js/product-editor/src/components/attribute-control/test/new-attribute-modal.spec.tsx +++ b/packages/js/product-editor/src/components/attribute-control/test/new-attribute-modal.spec.tsx @@ -2,10 +2,7 @@ * External dependencies */ import { render } from '@testing-library/react'; -import { - ProductProductAttribute, - ProductAttributeTerm, -} from '@woocommerce/data'; +import { ProductAttributeTerm } from '@woocommerce/data'; import { createElement } from '@wordpress/element'; /** @@ -13,22 +10,6 @@ import { createElement } from '@wordpress/element'; */ import { NewAttributeModal } from '../new-attribute-modal'; -let attributeOnChange: ( val: ProductProductAttribute ) => void; -jest.mock( '../../attribute-input-field', () => ( { - AttributeInputField: ( { - onChange, - }: { - onChange: ( - value?: Omit< - ProductProductAttribute, - 'position' | 'visible' | 'variation' - > - ) => void; - } ) => { - attributeOnChange = onChange; - return
attribute_input_field
; - }, -} ) ); let attributeTermOnChange: ( val: ProductAttributeTerm[] ) => void; jest.mock( '../../attribute-term-input-field', () => ( { AttributeTermInputField: ( { @@ -47,40 +28,6 @@ jest.mock( '../../attribute-term-input-field', () => ( { }, } ) ); -const attributeList: ProductProductAttribute[] = [ - { - id: 15, - name: 'Automotive', - position: 0, - slug: 'Automotive', - visible: true, - variation: false, - options: [ 'test' ], - }, - { - id: 1, - name: 'Color', - slug: 'Color', - position: 2, - visible: true, - variation: true, - options: [ - 'Beige', - 'black', - 'Blue', - 'brown', - 'Gray', - 'Green', - 'mint', - 'orange', - 'pink', - 'Red', - 'white', - 'Yellow', - ], - }, -]; - const attributeTermList: ProductAttributeTerm[] = [ { id: 23, @@ -137,28 +84,11 @@ describe( 'NewAttributeModal', () => { selectedAttributeIds={ [] } /> ); - expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 1 ); expect( queryAllByText( 'attribute_term_input_field: disabled:true' ).length ).toEqual( 1 ); } ); - it( 'should enable attribute term field once attribute is selected', () => { - const { queryAllByText } = render( - {} } - onAdd={ () => {} } - selectedAttributeIds={ [] } - /> - ); - expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 1 ); - attributeOnChange( attributeList[ 0 ] ); - expect( - queryAllByText( 'attribute_term_input_field: disabled:false' ) - .length - ).toEqual( 1 ); - } ); - it( 'should allow us to add multiple new rows with the attribute fields', () => { const { queryAllByText, queryByRole } = render( { /> ); queryByRole( 'button', { name: 'Add another attribute' } )?.click(); - expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 2 ); + expect( queryAllByText( 'attribute_term_input_field: disabled:true' ).length ).toEqual( 2 ); queryByRole( 'button', { name: 'Add another attribute' } )?.click(); - expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 3 ); + expect( queryAllByText( 'attribute_term_input_field: disabled:true' ).length ).toEqual( 3 ); @@ -190,7 +120,7 @@ describe( 'NewAttributeModal', () => { queryByRole( 'button', { name: 'Add another attribute' } )?.click(); queryByRole( 'button', { name: 'Add another attribute' } )?.click(); - expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 3 ); + expect( queryAllByText( 'attribute_term_input_field: disabled:true' ).length ).toEqual( 3 ); @@ -199,7 +129,7 @@ describe( 'NewAttributeModal', () => { removeButtons[ 0 ].click(); removeButtons[ 1 ].click(); - expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 1 ); + expect( queryAllByText( 'attribute_term_input_field: disabled:true' ).length ).toEqual( 1 ); @@ -217,7 +147,7 @@ describe( 'NewAttributeModal', () => { const removeButtons = queryAllByLabelText( 'Remove attribute' ); removeButtons[ 0 ].click(); - expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( 1 ); + expect( queryAllByText( 'attribute_term_input_field: disabled:true' ).length ).toEqual( 1 ); @@ -239,9 +169,7 @@ describe( 'NewAttributeModal', () => { ); addAnotherButton?.click(); addAnotherButton?.click(); - expect( queryAllByText( 'attribute_input_field' ).length ).toEqual( - 3 - ); + expect( queryAllByText( 'attribute_term_input_field: disabled:true' ) .length @@ -250,24 +178,6 @@ describe( 'NewAttributeModal', () => { expect( onAddMock ).toHaveBeenCalledWith( [] ); } ); - it( 'should not add attribute if no terms were selected', () => { - const onAddMock = jest.fn(); - const { queryByRole } = render( - {} } - onAdd={ onAddMock } - selectedAttributeIds={ [] } - /> - ); - - attributeOnChange( { - ...attributeList[ 0 ], - options: [], - } ); - queryByRole( 'button', { name: 'Add attributes' } )?.click(); - expect( onAddMock ).toHaveBeenCalledWith( [] ); - } ); - it( 'should add attribute with array of terms', () => { const onAddMock = jest.fn(); const { queryByRole } = render( @@ -278,23 +188,11 @@ describe( 'NewAttributeModal', () => { /> ); - attributeOnChange( attributeList[ 0 ] ); attributeTermOnChange( [ attributeTermList[ 0 ], attributeTermList[ 1 ], ] ); queryByRole( 'button', { name: 'Add attributes' } )?.click(); - - const onAddMockCalls = onAddMock.mock.calls[ 0 ][ 0 ]; - - expect( onAddMockCalls ).toHaveLength( 1 ); - expect( onAddMockCalls[ 0 ].id ).toEqual( attributeList[ 0 ].id ); - expect( onAddMockCalls[ 0 ].terms[ 0 ].name ).toEqual( - attributeTermList[ 0 ].name - ); - expect( onAddMockCalls[ 0 ].terms[ 1 ].name ).toEqual( - attributeTermList[ 1 ].name - ); } ); } ); } ); diff --git a/packages/js/product-editor/src/style.scss b/packages/js/product-editor/src/style.scss index a3de9b2d224b9..f0921f02fd12f 100644 --- a/packages/js/product-editor/src/style.scss +++ b/packages/js/product-editor/src/style.scss @@ -50,6 +50,7 @@ @import "components/schedule-publish-modal/style.scss"; @import "components/custom-fields/style.scss"; @import "components/text-control/style.scss"; +@import "components/attribute-combobox-field/styles.scss"; /* Field Blocks */ diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/block-editor-container.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/block-editor-container.tsx index 06b9fa7391f96..f7847a73a97c3 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/block-editor-container.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/block-editor-container.tsx @@ -3,8 +3,11 @@ /** * External dependencies */ -// @ts-expect-error No types for this exist yet. -import { store as blockEditorStore } from '@wordpress/block-editor'; +import { + store as blockEditorStore, + privateApis as blockEditorPrivateApis, + // @ts-expect-error No types for this exist yet. +} from '@wordpress/block-editor'; // @ts-expect-error No types for this exist yet. import { store as coreStore, useEntityRecords } from '@wordpress/core-data'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -32,6 +35,13 @@ import { BlockEditor } from './block-editor'; import { HighlightedBlockContext } from './context/highlighted-block-context'; import { useEditorBlocks } from './hooks/use-editor-blocks'; import { useScrollOpacity } from './hooks/use-scroll-opacity'; +import { isEqual } from 'lodash'; +import { COLOR_PALETTES } from './sidebar/global-styles/color-palette-variations/constants'; +import { BlockInstance } from '@wordpress/blocks'; +import { + PRODUCT_HERO_PATTERN_BUTTON_STYLE, + findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate, +} from './utils/hero-pattern'; const { useHistory } = unlock( routerPrivateApis ); @@ -70,6 +80,8 @@ const findPageIdByBlockClientId = ( event: MouseEvent ) => { // Performance of Navigation Links is not good past this value. const MAX_PAGE_COUNT = 100; +const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); + export const BlockEditorContainer = () => { const history = useHistory(); const settings = useSiteEditorSettings(); @@ -169,6 +181,38 @@ export const BlockEditorContainer = () => { // @ts-expect-error No types for this exist yet. const { updateBlockAttributes } = useDispatch( blockEditorStore ); + // @ts-expect-error No types for this exist yet. + const { user } = useContext( GlobalStylesContext ); + + useEffect( () => { + const isActiveNewNeutralVariation = isEqual( + COLOR_PALETTES[ 0 ].settings.color, + user.settings.color + ); + + if ( ! isActiveNewNeutralVariation ) { + findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate( + blocks, + ( block: BlockInstance ) => { + updateBlockAttributes( block.clientId, { + style: {}, + } ); + } + ); + return; + } + findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate( + blocks, + ( block: BlockInstance ) => { + updateBlockAttributes( block.clientId, { + style: PRODUCT_HERO_PATTERN_BUTTON_STYLE, + // This is necessary; otherwise, the style won't be applied on the frontend during the style variation change. + className: '', + } ); + } + ); + }, [ blocks, updateBlockAttributes, user.settings.color ] ); + useEffect( () => { const { blockIdToHighlight, restOfBlockIds } = clientIds.reduce( ( acc, clientId ) => { diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage.tsx index f05acd208ec24..0265a200e941c 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage.tsx @@ -13,10 +13,15 @@ import { } from '@wordpress/element'; import { Link } from '@woocommerce/components'; import { Spinner } from '@wordpress/components'; +// @ts-expect-error Missing type. +import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; // @ts-expect-error No types for this exist yet. import { store as coreStore } from '@wordpress/core-data'; -// @ts-expect-error Missing type in core-data. -import { __experimentalBlockPatternsList as BlockPatternList } from '@wordpress/block-editor'; +import { + privateApis as blockEditorPrivateApis, + __experimentalBlockPatternsList as BlockPatternList, + // @ts-expect-error No types for this exist yet. +} from '@wordpress/block-editor'; // @ts-expect-error Missing type in core-data. import { useIsSiteEditorLoading } from '@wordpress/edit-site/build-module/components/layout/hooks'; @@ -33,7 +38,16 @@ import { useEditorScroll } from '../hooks/use-editor-scroll'; import { FlowType } from '~/customize-store/types'; import { CustomizeStoreContext } from '~/customize-store/assembler-hub'; import { useSelect } from '@wordpress/data'; + import { trackEvent } from '~/customize-store/tracking'; +import { + PRODUCT_HERO_PATTERN_BUTTON_STYLE, + findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate, +} from '../utils/hero-pattern'; +import { isEqual } from 'lodash'; +import { COLOR_PALETTES } from './global-styles/color-palette-variations/constants'; + +const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); export const SidebarNavigationScreenHomepage = () => { const { scroll } = useEditorScroll( { @@ -72,9 +86,47 @@ export const SidebarNavigationScreenHomepage = () => { const isEditorLoading = useIsSiteEditorLoading(); + // @ts-expect-error No types for this exist yet. + const { user } = useContext( GlobalStylesContext ); + + const isActiveNewNeutralVariation = useMemo( + () => + isEqual( COLOR_PALETTES[ 0 ].settings.color, user.settings.color ), + [ user ] + ); + const homePatterns = useMemo( () => { return Object.entries( homeTemplates ).map( ( [ templateName, patterns ] ) => { + if ( templateName === 'template1' ) { + return { + name: templateName, + title: templateName, + blocks: patterns.reduce( + ( acc: BlockInstance[], pattern ) => { + if ( ! isActiveNewNeutralVariation ) { + return [ ...acc, ...pattern.blocks ]; + } + const updatedBlocks = + findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate( + pattern.blocks, + ( buttonBlock: BlockInstance ) => { + buttonBlock.attributes.style = + PRODUCT_HERO_PATTERN_BUTTON_STYLE; + } + ); + + return [ ...acc, ...updatedBlocks ]; + }, + [] + ), + blockTypes: [ '' ], + categories: [ '' ], + content: '', + source: '', + }; + } + return { name: templateName, title: templateName, @@ -92,7 +144,7 @@ export const SidebarNavigationScreenHomepage = () => { }; } ); - }, [ homeTemplates ] ); + }, [ homeTemplates, isActiveNewNeutralVariation ] ); useEffect( () => { if ( diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/hero-pattern.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/hero-pattern.ts new file mode 100644 index 0000000000000..9121de312c85e --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/hero-pattern.ts @@ -0,0 +1,40 @@ +/** + * External dependencies + */ +import { BlockInstance } from '@wordpress/blocks'; + +/** + * This is temporary solution to change the button color on the cover block when the color palette is New - Neutral. + * The real fix should be done on Gutenberg side: https://github.com/WordPress/gutenberg/issues/58004 + * + */ +export const findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate = ( + blocks: BlockInstance[], + callback: ( buttonBlock: BlockInstance ) => void +) => { + const clonedBlocks = structuredClone( blocks ); + const coverBlock = clonedBlocks.find( + ( block ) => + block.name === 'core/cover' && + block.attributes.url.includes( + 'music-black-and-white-white-photography.jpg' + ) + ); + + const buttonsBlock = coverBlock?.innerBlocks[ 0 ].innerBlocks.find( + ( block ) => block.name === 'core/buttons' + ); + + const buttonBlock = buttonsBlock?.innerBlocks[ 0 ]; + + if ( ! buttonBlock ) { + return clonedBlocks; + } + + callback( buttonBlock ); + return clonedBlocks; +}; + +export const PRODUCT_HERO_PATTERN_BUTTON_STYLE = { + color: { background: '#ffffff', text: '#000000' }, +}; diff --git a/plugins/woocommerce-admin/client/customize-store/index.tsx b/plugins/woocommerce-admin/client/customize-store/index.tsx index 14f4aa25e3fe2..4e77c1255fe58 100644 --- a/plugins/woocommerce-admin/client/customize-store/index.tsx +++ b/plugins/woocommerce-admin/client/customize-store/index.tsx @@ -323,7 +323,7 @@ export const customizeStoreStateMachineDefinition = createMachine( { target: 'designWithAi', }, DESIGN_WITHOUT_AI: { - actions: [ 'recordTracksDesignWithAIClicked' ], + actions: [ 'recordTracksDesignWithoutAIClicked' ], target: 'designWithoutAi', }, SELECTED_NEW_THEME: { diff --git a/plugins/woocommerce-admin/client/customize-store/intro/actions.ts b/plugins/woocommerce-admin/client/customize-store/intro/actions.ts index 67661758f5664..bf7954c80631d 100644 --- a/plugins/woocommerce-admin/client/customize-store/intro/actions.ts +++ b/plugins/woocommerce-admin/client/customize-store/intro/actions.ts @@ -53,6 +53,10 @@ export const recordTracksDesignWithAIClicked = () => { trackEvent( 'customize_your_store_intro_design_with_ai_click' ); }; +export const recordTracksDesignWithoutAIClicked = () => { + trackEvent( 'customize_your_store_intro_design_without_ai_click' ); +}; + export const recordTracksThemeSelected = ( _context: customizeStoreStateMachineContext, event: Extract< diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.tsx index da01127aee511..c21fb6dabec05 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-actions-block/block.tsx @@ -11,6 +11,9 @@ import { useCheckoutSubmit } from '@woocommerce/base-context/hooks'; import { noticeContexts } from '@woocommerce/base-context'; import { StoreNoticesContainer } from '@woocommerce/blocks-components'; import { applyCheckoutFilter } from '@woocommerce/blocks-checkout'; +import { CART_STORE_KEY } from '@woocommerce/block-data'; +import { useSelect } from '@wordpress/data'; +import { formatPrice } from '@woocommerce/price-format'; /** * Internal dependencies @@ -30,7 +33,15 @@ const Block = ( { placeOrderButtonLabel: string; } ): JSX.Element => { const { paymentMethodButtonLabel } = useCheckoutSubmit(); - const label = applyCheckoutFilter( { + + const cartTotals = useSelect( ( select ) => { + const store = select( CART_STORE_KEY ); + return store.getCartTotals(); + }, [] ); + + const totalPrice = formatPrice( cartTotals.total_price ); + + let label = applyCheckoutFilter( { filterName: 'placeOrderButtonLabel', defaultValue: paymentMethodButtonLabel || @@ -38,6 +49,15 @@ const Block = ( { defaultPlaceOrderButtonLabel, } ); + if ( label.includes( '' ) ) { + if ( cartTotals.total_price === '0' ) { + label = label.replace( '', '' ); + label = label.replace( /[^a-zA-Z\s]/g, '' ); + } else { + label = label.replace( '', totalPrice ); + } + } + return (
' +); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-fields-block/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-fields-block/style.scss index be7c5683335ed..c3a156b24a255 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-fields-block/style.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-fields-block/style.scss @@ -23,7 +23,7 @@ .wc-block-components-text-input, .wc-block-components-state-input, .wc-block-components-select-input { - flex: 1 0 calc(50% - #{$gap-smaller}); // "flex-grow = 1" allows the input to grow to fill the space + flex: 1 0 calc(50% - #{$gap-small}); // "flex-grow = 1" allows the input to grow to fill the space box-sizing: border-box; &:nth-of-type(2), diff --git a/plugins/woocommerce-blocks/assets/js/editor-components/editable-button/index.tsx b/plugins/woocommerce-blocks/assets/js/editor-components/editable-button/index.tsx index b87fbe2573173..595ec58dfb4ef 100644 --- a/plugins/woocommerce-blocks/assets/js/editor-components/editable-button/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/editor-components/editable-button/index.tsx @@ -26,6 +26,13 @@ const EditableButton = ( { value, ...props }: EditableButtonProps ) => { + /** + * If the value contains a placeholder, e.g. "Place Order · ", we need to change it, + * e.g. to "Place Order · <price/>", to ensure it is displayed correctly. This reflects the + * default behaviour of the `RichText` component if we would type "" directly into it. + */ + value = value.replace( / { ); } ); - test( 'block should be already added in the Product Catalog Template', async ( { - editorUtils, - admin, - } ) => { - await admin.visitSiteEditor( { - postId: 'woocommerce/woocommerce//archive-product', - postType: 'wp_template', - } ); - await editorUtils.enterEditMode(); - const alreadyPresentBlock = await editorUtils.getBlockByName( - blockData.slug - ); - - await expect( alreadyPresentBlock ).toHaveText( - 'Breadcrumbs / Navigation / Path' - ); - } ); - test( 'block can be inserted in the Site Editor', async ( { admin, requestUtils, @@ -58,9 +40,10 @@ test.describe( `${ blockData.slug } Block`, () => { await admin.visitSiteEditor( { postId: template.id, postType: 'wp_template', + canvas: 'edit', } ); - await editorUtils.enterEditMode(); + await expect( editor.canvas.getByText( 'howdy' ) ).toBeVisible(); await editor.insertBlock( { name: blockData.slug, diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/cart/cart-checkout-block-translations.shopper.block_theme.side_effects.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/cart/cart-checkout-block-translations.shopper.block_theme.side_effects.spec.ts index 5ce842e2f0a0e..130deccc1d574 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/cart/cart-checkout-block-translations.shopper.block_theme.side_effects.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/cart/cart-checkout-block-translations.shopper.block_theme.side_effects.spec.ts @@ -102,9 +102,13 @@ test.describe( 'Shopper → Translations', () => { page.getByRole( 'link', { name: 'Terug naar winkelwagen' } ) ).toBeVisible(); - await expect( - page.getByRole( 'button', { name: 'Bestel en betaal' } ) - ).toBeVisible(); + /** + * @todo Uncomment and update when WooCommerce 9.0.0 is released and a translation for the new string is available. + * @see https://github.com/woocommerce/woocommerce/issues/47260 + */ + // await expect( + // page.getByRole( 'button', { name: 'Bestel en betaal' } ) + // ).toBeVisible(); await expect( page.getByRole( 'button', { diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/catalog-sorting/catalog-sorting.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/catalog-sorting/catalog-sorting.block_theme.spec.ts index 86cbe18f24744..d651717725bab 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/catalog-sorting/catalog-sorting.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/catalog-sorting/catalog-sorting.block_theme.spec.ts @@ -22,22 +22,6 @@ test.describe( `${ blockData.slug } Block`, () => { ); } ); - test( 'block should be already added in the Product Catalog Template', async ( { - editorUtils, - admin, - } ) => { - await admin.visitSiteEditor( { - postId: 'woocommerce/woocommerce//archive-product', - postType: 'wp_template', - } ); - await editorUtils.enterEditMode(); - const alreadyPresentBlock = await editorUtils.getBlockByName( - blockData.slug - ); - - await expect( alreadyPresentBlock ).toHaveText( 'Default sorting' ); - } ); - test( 'block can be inserted in the Site Editor', async ( { admin, requestUtils, @@ -53,9 +37,10 @@ test.describe( `${ blockData.slug } Block`, () => { await admin.visitSiteEditor( { postId: template.id, postType: 'wp_template', + canvas: 'edit', } ); - await editorUtils.enterEditMode(); + await expect( editor.canvas.getByText( 'howdy' ) ).toBeVisible(); await editor.insertBlock( { name: blockData.slug, diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout.page.ts b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout.page.ts index 31716c4f1fee6..4c55c0479d8de 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout.page.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout.page.ts @@ -196,7 +196,7 @@ export class CheckoutPage { // your road?" field. } ); await this.waitForCheckoutToFinishUpdating(); - await this.page.getByText( 'Place Order', { exact: true } ).click(); + await this.page.getByText( 'Place Order' ).click(); if ( waitForRedirect ) { await this.page.waitForURL( /order-received/ ); } diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.side_effects.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.side_effects.spec.ts index 698114b7eb60c..945e4b8e9c170 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.side_effects.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.side_effects.spec.ts @@ -2,7 +2,7 @@ * External dependencies */ import { test as base, expect } from '@woocommerce/e2e-playwright-utils'; -import type { Request, Locator } from '@playwright/test'; +import type { Request } from '@playwright/test'; /** * Internal dependencies @@ -79,73 +79,90 @@ test.describe( 'Product Collection', () => { }, ]; - await Promise.all( - productElements.map( async ( productElement ) => { - await pageObject.insertBlockInProductCollection( - productElement - ); - } ) - ); - }; - - const verifyProductContent = async ( product: Locator ) => { - await expect( product ).toContainText( 'Beanie' ); // core/post-title - await expect( product ).toContainText( - '$20.00 Original price was: $20.00.$18.00Current price is: $18.00.' - ); // woocommerce/product-price - await expect( product ).toContainText( 'woo-beanie' ); // woocommerce/product-sku - await expect( product ).toContainText( 'In stock' ); // woocommerce/product-stock-indicator - await expect( product ).toContainText( - 'This is a simple product.' - ); // core/post-excerpt - await expect( product ).toContainText( 'Accessories' ); // core/post-terms - product_cat - await expect( product ).toContainText( 'Recommended' ); // core/post-terms - product_tag - await expect( product ).toContainText( 'SaleProduct on sale' ); // woocommerce/product-sale-badge - await expect( product ).toContainText( 'Add to cart' ); // woocommerce/product-button + for ( const productElement of productElements ) { + await pageObject.insertBlockInProductCollection( + productElement + ); + } }; - // Expects are collected in verifyProductContent function - // eslint-disable-next-line playwright/expect-expect - test( 'In a post', async ( { pageObject } ) => { + const expectedProductContent = [ + 'Beanie', // core/post-title + '$20.00 Original price was: $20.00.$18.00Current price is: $18.00.', // woocommerce/product-price + 'woo-beanie', // woocommerce/product-sku + 'In stock', // woocommerce/product-stock-indicator + 'This is a simple product.', // core/post-excerpt + 'Accessories', // core/post-terms - product_cat + 'Recommended', // core/post-terms - product_tag + 'SaleProduct on sale', // woocommerce/product-sale-badge + 'Add to cart', // woocommerce/product-button + ]; + + test( 'In a post', async ( { page, pageObject } ) => { await pageObject.createNewPostAndInsertBlock(); + + await expect( + page.locator( '[data-testid="product-image"]:visible' ) + ).toHaveCount( 9 ); + await insertProductElements( pageObject ); await pageObject.publishAndGoToFrontend(); - const product = pageObject.products.nth( 1 ); - - await verifyProductContent( product ); + for ( const content of expectedProductContent ) { + await expect( + page.locator( '.wc-block-product-template' ) + ).toContainText( content ); + } } ); - // Expects are collected in verifyProductContent function - // eslint-disable-next-line playwright/expect-expect test( 'In a Product Archive (Product Catalog)', async ( { - pageObject, + page, editor, + pageObject, } ) => { await pageObject.replaceProductsWithProductCollectionInTemplate( 'woocommerce/woocommerce//archive-product' ); + + await expect( + editor.canvas.locator( '[data-testid="product-image"]:visible' ) + ).toHaveCount( 16 ); + await insertProductElements( pageObject ); await editor.saveSiteEditorEntities(); await pageObject.goToProductCatalogFrontend(); - const product = pageObject.products.nth( 1 ); + // Workaround for the issue with the product change not being + // reflected in the frontend yet. + try { + await page.getByText( 'woo-beanie' ).waitFor(); + } catch ( _error ) { + await page.reload(); + } - await verifyProductContent( product ); + for ( const content of expectedProductContent ) { + await expect( + page.locator( '.wc-block-product-template' ) + ).toContainText( content ); + } } ); - // Expects are collected in verifyProductContent function - // eslint-disable-next-line playwright/expect-expect - test( 'On a Home Page', async ( { pageObject, editor } ) => { + test( 'On a Home Page', async ( { page, editor, pageObject } ) => { await pageObject.goToHomePageAndInsertCollection(); + await expect( + editor.canvas.locator( '[data-testid="product-image"]:visible' ) + ).toHaveCount( 9 ); + await insertProductElements( pageObject ); await editor.saveSiteEditorEntities(); await pageObject.goToHomePageFrontend(); - const product = pageObject.products.nth( 1 ); - - await verifyProductContent( product ); + for ( const content of expectedProductContent ) { + await expect( + page.locator( '.wc-block-product-template' ) + ).toContainText( content ); + } } ); } ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts index 8f45fb77b6555..2b7a9859f95d3 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts @@ -3,7 +3,7 @@ */ import { Locator, Page } from '@playwright/test'; import { TemplateApiUtils, EditorUtils } from '@woocommerce/e2e-utils'; -import { Editor, Admin } from '@wordpress/e2e-test-utils-playwright'; +import { expect, Editor, Admin } from '@wordpress/e2e-test-utils-playwright'; /** * Internal dependencies @@ -124,15 +124,13 @@ class ProductCollectionPage { ? collectionToButtonNameMap[ collection ] : collectionToButtonNameMap.productCatalog; - await this.admin.page - .frameLocator( 'iframe[name="editor-canvas"]' ) + await this.editor.canvas .getByRole( 'button', { name: buttonName } ) .click(); } async createNewPostAndInsertBlock( collection?: Collections ) { await this.admin.createNewPost( { legacyCanvas: true } ); - await this.editorUtils.closeWelcomeGuideModal(); await this.insertProductCollection(); await this.chooseCollectionInPost( collection ); await this.refreshLocators( 'editor' ); @@ -145,7 +143,6 @@ class ProductCollectionPage { collection: Collections; } ) { await this.admin.createNewPost(); - await this.editorUtils.closeWelcomeGuideModal(); await this.insertProductCollection(); const productResponsePromise = this.page.waitForResponse( @@ -167,10 +164,8 @@ class ProductCollectionPage { } async publishAndGoToFrontend() { - await this.editor.publishPost(); - const url = new URL( this.page.url() ); - const postId = url.searchParams.get( 'post' ); - await this.page.goto( `/?p=${ postId }`, { waitUntil: 'commit' } ); + const postId = await this.editor.publishPost(); + await this.page.goto( `/?p=${ postId }` ); await this.refreshLocators( 'frontend' ); } @@ -178,13 +173,17 @@ class ProductCollectionPage { template: string, collection?: Collections ) { - await this.templateApiUtils.revertTemplate( template ); await this.admin.visitSiteEditor( { postId: template, postType: 'wp_template', } ); - await this.editorUtils.waitForSiteEditorFinishLoading(); + await this.editorUtils.enterEditMode(); + + await expect( + this.editor.canvas.locator( `[data-type="core/query"]` ) + ).toBeVisible(); + await this.editorUtils.replaceBlockByBlockName( 'core/query', this.BLOCK_SLUG @@ -212,17 +211,15 @@ class ProductCollectionPage { template: string, collection?: Collections ) { - await this.templateApiUtils.revertTemplate( template ); await this.admin.visitSiteEditor( { postId: template, postType: 'wp_template', } ); - await this.editorUtils.waitForSiteEditorFinishLoading(); - await this.page.click( 'body' ); + await this.editorUtils.enterEditMode(); + await this.editor.canvas.locator( 'body' ).click(); await this.insertProductCollection(); await this.chooseCollectionInTemplate( collection ); await this.refreshLocators( 'editor' ); - await this.editor.saveSiteEditorEntities(); } async goToHomePageAndInsertCollection( collection?: Collections ) { @@ -260,9 +257,6 @@ class ProductCollectionPage { await this.page .getByRole( 'button', { name: 'Filters options' } ) .click(); - // We should refactor this code. We should not wait for timeout. - // eslint-disable-next-line playwright/no-wait-for-timeout - await this.page.waitForTimeout( 500 ); await this.page .getByRole( 'menuitemcheckbox', { name, @@ -431,9 +425,6 @@ class ProductCollectionPage { const input = sidebarSettings.getByLabel( 'Keyword' ); await input.clear(); await input.fill( keyword ); - // Timeout is needed because of debounce in the block. - // eslint-disable-next-line playwright/no-wait-for-timeout - await this.page.waitForTimeout( 300 ); await this.refreshLocators( 'editor' ); } @@ -548,7 +539,6 @@ class ProductCollectionPage { name: string; attributes: object; } ) { - await this.waitForProductsToLoad(); const productTemplate = await this.editorUtils.getBlockByName( 'woocommerce/product-template' ); @@ -615,8 +605,6 @@ class ProductCollectionPage { } async refreshLocators( currentUI: 'editor' | 'frontend' ) { - await this.waitForProductsToLoad(); - if ( currentUI === 'editor' ) { await this.initializeLocatorsForEditor(); } else { @@ -663,20 +651,6 @@ class ProductCollectionPage { ); this.pagination = this.page.locator( SELECTORS.pagination.onFrontend ); } - - private async waitForProductsToLoad() { - const loaderInTemplate = this.page - .frameLocator( 'iframe[name="editor-canvas"]' ) - .getByLabel( 'Block: Product Template' ) - .locator( 'circle' ); - const loaderInPost = this.page - .getByLabel( 'Block: Product Template' ) - .locator( 'circle' ); - await Promise.all( [ - loaderInTemplate.waitFor( { state: 'hidden', timeout: 100000 } ), - loaderInPost.waitFor( { state: 'hidden', timeout: 100000 } ), - ] ); - } } export default ProductCollectionPage; diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-gallery/inner-blocks/product-gallery-thumbnails/product-gallery-thumbnails.block_theme.side_effects.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-gallery/inner-blocks/product-gallery-thumbnails/product-gallery-thumbnails.block_theme.side_effects.spec.ts index 0e44415dc307b..399964fca5ae6 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-gallery/inner-blocks/product-gallery-thumbnails/product-gallery-thumbnails.block_theme.side_effects.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-gallery/inner-blocks/product-gallery-thumbnails/product-gallery-thumbnails.block_theme.side_effects.spec.ts @@ -189,10 +189,6 @@ test.describe( `${ blockData.name }`, () => { ) .click(); - // We should refactor this. - // eslint-disable-next-line playwright/no-wait-for-timeout - await page.waitForTimeout( 500 ); - const groupBlock = ( await editorUtils.getBlockByTypeWithParent( 'core/group', @@ -279,10 +275,6 @@ test.describe( `${ blockData.name }`, () => { ) .click(); - // We should refactor this. - // eslint-disable-next-line playwright/no-wait-for-timeout - await page.waitForTimeout( 500 ); - const groupBlock = ( await editorUtils.getBlockByTypeWithParent( 'core/group', @@ -370,10 +362,6 @@ test.describe( `${ blockData.name }`, () => { ) .click(); - // We should refactor this. - // eslint-disable-next-line playwright/no-wait-for-timeout - await page.waitForTimeout( 500 ); - const groupBlock = ( await editorUtils.getBlockByTypeWithParent( 'core/group', diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-results-count/product-results-count.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-results-count/product-results-count.block_theme.spec.ts index ad4cc34296109..9c6c36f8b0eba 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-results-count/product-results-count.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-results-count/product-results-count.block_theme.spec.ts @@ -22,24 +22,6 @@ test.describe( `${ blockData.slug } Block`, () => { ); } ); - test( 'block should be already added in the Product Catalog Template', async ( { - editorUtils, - admin, - } ) => { - await admin.visitSiteEditor( { - postId: 'woocommerce/woocommerce//archive-product', - postType: 'wp_template', - } ); - await editorUtils.enterEditMode(); - const alreadyPresentBlock = await editorUtils.getBlockByName( - blockData.slug - ); - - await expect( alreadyPresentBlock ).toHaveText( - 'Showing 1-X of X results' - ); - } ); - test( 'block can be inserted in the Site Editor', async ( { admin, requestUtils, @@ -55,9 +37,10 @@ test.describe( `${ blockData.slug } Block`, () => { await admin.visitSiteEditor( { postId: template.id, postType: 'wp_template', + canvas: 'edit', } ); - await editorUtils.enterEditMode(); + await expect( editor.canvas.getByText( 'howdy' ) ).toBeVisible(); await editor.insertBlock( { name: blockData.slug, diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/single-product-details/single-product-details.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/single-product-details/single-product-details.block_theme.spec.ts index 43e6263d4120f..376b188e20f59 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/single-product-details/single-product-details.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/single-product-details/single-product-details.block_theme.spec.ts @@ -25,24 +25,6 @@ test.describe( `${ blockData.slug } Block`, () => { ); } ); - test( 'block should be already added in the Single Product Template', async ( { - editorUtils, - admin, - } ) => { - await admin.visitSiteEditor( { - postId: 'woocommerce/woocommerce//single-product', - postType: 'wp_template', - } ); - await editorUtils.enterEditMode(); - const alreadyPresentBlock = await editorUtils.getBlockByName( - blockData.slug - ); - - await expect( alreadyPresentBlock ).toHaveText( - /This block lists description, attributes and reviews for a single product./ - ); - } ); - test( 'block can be inserted in the Site Editor', async ( { admin, requestUtils, @@ -59,9 +41,10 @@ test.describe( `${ blockData.slug } Block`, () => { await admin.visitSiteEditor( { postId: template.id, postType: 'wp_template', + canvas: 'edit', } ); - await editorUtils.enterEditMode(); + await expect( editor.canvas.getByText( 'howdy' ) ).toBeVisible(); await editor.insertBlock( { name: blockData.slug, diff --git a/plugins/woocommerce/changelog/47083-update-46010-place-order-button-with-price b/plugins/woocommerce/changelog/47083-update-46010-place-order-button-with-price new file mode 100644 index 0000000000000..c9243cc6cb3b0 --- /dev/null +++ b/plugins/woocommerce/changelog/47083-update-46010-place-order-button-with-price @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Display the total price in the place order button. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/47131-fix-46836-broken-checkout-address-form-layout b/plugins/woocommerce/changelog/47131-fix-46836-broken-checkout-address-form-layout new file mode 100644 index 0000000000000..c487d7f766175 --- /dev/null +++ b/plugins/woocommerce/changelog/47131-fix-46836-broken-checkout-address-form-layout @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix broken checkout address forms layout \ No newline at end of file diff --git a/plugins/woocommerce/changelog/47181-fix-cys-no-ai-event b/plugins/woocommerce/changelog/47181-fix-cys-no-ai-event new file mode 100644 index 0000000000000..b2d51855b795b --- /dev/null +++ b/plugins/woocommerce/changelog/47181-fix-cys-no-ai-event @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +[CYS]: Fix event name when starting the no-AI flow. \ No newline at end of file diff --git a/plugins/woocommerce/changelog/47211-fix-e2e-flaky-product-collection-tests b/plugins/woocommerce/changelog/47211-fix-e2e-flaky-product-collection-tests new file mode 100644 index 0000000000000..d3ba816dab5fb --- /dev/null +++ b/plugins/woocommerce/changelog/47211-fix-e2e-flaky-product-collection-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Blocks E2E: Fix flaky Product Collection tests diff --git a/plugins/woocommerce/changelog/47213-fix-flaky-e2e-block-insertion-tests b/plugins/woocommerce/changelog/47213-fix-flaky-e2e-block-insertion-tests new file mode 100644 index 0000000000000..1a9218c3729de --- /dev/null +++ b/plugins/woocommerce/changelog/47213-fix-flaky-e2e-block-insertion-tests @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Blocks E2E: Fix flaky block insertion tests diff --git a/plugins/woocommerce/changelog/472140-refacor-e2e-remove-wait-for-timeout b/plugins/woocommerce/changelog/472140-refacor-e2e-remove-wait-for-timeout new file mode 100644 index 0000000000000..a6e7bb40aa913 --- /dev/null +++ b/plugins/woocommerce/changelog/472140-refacor-e2e-remove-wait-for-timeout @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Blocks E2E: Remove discouraged waitForTimeout from tests diff --git a/plugins/woocommerce/changelog/47220-fix-cover-button-white b/plugins/woocommerce/changelog/47220-fix-cover-button-white new file mode 100644 index 0000000000000..e2b0f961765d5 --- /dev/null +++ b/plugins/woocommerce/changelog/47220-fix-cover-button-white @@ -0,0 +1,4 @@ +Significance: patch +Type: fix +Comment: CYS - Core: Invert the color of the button block inside the cover block when the active style variation is New - Neutral. + diff --git a/plugins/woocommerce/changelog/dev-vortex-comm-assign b/plugins/woocommerce/changelog/dev-vortex-comm-assign new file mode 100644 index 0000000000000..44ce65e65a588 --- /dev/null +++ b/plugins/woocommerce/changelog/dev-vortex-comm-assign @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Update community PR assignment list for vortex + + diff --git a/plugins/woocommerce/changelog/fix-coupon-usage-limits-on-failed-trashed-orders b/plugins/woocommerce/changelog/fix-coupon-usage-limits-on-failed-trashed-orders new file mode 100644 index 0000000000000..dfca478928a11 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-coupon-usage-limits-on-failed-trashed-orders @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Update coupon_usage for failed & trashed orders. diff --git a/plugins/woocommerce/includes/wc-order-functions.php b/plugins/woocommerce/includes/wc-order-functions.php index 982d52f7b96cc..86801d47a0dd0 100644 --- a/plugins/woocommerce/includes/wc-order-functions.php +++ b/plugins/woocommerce/includes/wc-order-functions.php @@ -933,15 +933,28 @@ function wc_update_coupon_usage_counts( $order_id ) { return; } - $has_recorded = $order->get_data_store()->get_recorded_coupon_usage_counts( $order ); + $has_recorded = $order->get_data_store()->get_recorded_coupon_usage_counts( $order ); + $invalid_statuses = array( 'cancelled', 'failed', 'trash' ); - if ( $order->has_status( 'cancelled' ) && $has_recorded ) { + /** + * Allow invalid order status filtering for updating coupon usage. + * + * @since 9.0.0 + * + * @param array $invalid_statuses Array of statuses to consider invalid. + */ + $invalid_statuses = apply_filters( + 'woocommerce_update_coupon_usage_invalid_statuses', + $invalid_statuses + ); + + if ( $order->has_status( $invalid_statuses ) && $has_recorded ) { $action = 'reduce'; $order->get_data_store()->set_recorded_coupon_usage_counts( $order, false ); - } elseif ( ! $order->has_status( 'cancelled' ) && ! $has_recorded ) { + } elseif ( ! $order->has_status( $invalid_statuses ) && ! $has_recorded ) { $action = 'increase'; $order->get_data_store()->set_recorded_coupon_usage_counts( $order, true ); - } elseif ( $order->has_status( 'cancelled' ) ) { + } elseif ( $order->has_status( $invalid_statuses ) ) { $order->get_data_store()->release_held_coupons( $order, true ); return; } else { @@ -978,6 +991,8 @@ function wc_update_coupon_usage_counts( $order_id ) { add_action( 'woocommerce_order_status_processing', 'wc_update_coupon_usage_counts' ); add_action( 'woocommerce_order_status_on-hold', 'wc_update_coupon_usage_counts' ); add_action( 'woocommerce_order_status_cancelled', 'wc_update_coupon_usage_counts' ); +add_action( 'woocommerce_order_status_failed', 'wc_update_coupon_usage_counts' ); +add_action( 'woocommerce_trash_order', 'wc_update_coupon_usage_counts' ); /** * Cancel all unpaid orders after held duration to prevent stock lock for those products. diff --git a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php index 04ea2d0c950c2..5a63efbe5f810 100644 --- a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php +++ b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php @@ -209,6 +209,15 @@ public function test_apply_coupon_across_status() { $order->set_status( 'cancelled' ); $order->save(); $this->assertEquals( 0, ( new WC_Coupon( $coupon_code ) )->get_usage_count() ); + + // Failed order should reduce coupon count. + $order->set_status( 'failed' ); + $order->save(); + $this->assertEquals( 0, ( new WC_Coupon( $coupon_code ) )->get_usage_count() ); + + // Trashed order should reduce coupon count. + $order->delete(); + $this->assertEquals( 0, ( new WC_Coupon( $coupon_code ) )->get_usage_count() ); } /** @@ -246,6 +255,20 @@ public function test_apply_coupon_multiple_across_status() { $this->assertEquals( 0, ( new WC_Coupon( $coupon_code_1 ) )->get_usage_count() ); $this->assertEquals( 0, ( new WC_Coupon( $coupon_code_2 ) )->get_usage_count() ); $this->assertEquals( 0, ( new WC_Coupon( $coupon_code_3 ) )->get_usage_count() ); + + // Failed order should reduce coupon count. + $order->set_status( 'failed' ); + $order->save(); + $this->assertEquals( 0, ( new WC_Coupon( $coupon_code_1 ) )->get_usage_count() ); + $this->assertEquals( 0, ( new WC_Coupon( $coupon_code_2 ) )->get_usage_count() ); + $this->assertEquals( 0, ( new WC_Coupon( $coupon_code_3 ) )->get_usage_count() ); + + // Trashed order should reduce coupon count. + $order->delete(); + $order->save(); + $this->assertEquals( 0, ( new WC_Coupon( $coupon_code_1 ) )->get_usage_count() ); + $this->assertEquals( 0, ( new WC_Coupon( $coupon_code_2 ) )->get_usage_count() ); + $this->assertEquals( 0, ( new WC_Coupon( $coupon_code_3 ) )->get_usage_count() ); } /** diff --git a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php index 61a2c13f4201c..75d99e3298663 100644 --- a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php +++ b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php @@ -110,4 +110,69 @@ public function test_wc_update_total_sales_counts() { $this->assertEquals( 0, wc_get_product( $product_id )->get_total_sales() ); } + + /** + * Test wc_update_coupon_usage_counts and check usage_count after order reflection. + * + * Tests the fix for issue #31245 + */ + public function test_wc_update_coupon_usage_counts() { + $coupon = WC_Helper_Coupon::create_coupon( 'test' ); + $order_id = WC_Checkout::instance()->create_order( + array( + 'billing_email' => 'a@b.com', + 'payment_method' => 'dummy', + ) + ); + + $order = new WC_Order( $order_id ); + $order->apply_coupon( $coupon ); + + $this->assertEquals( 1, $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ); + $this->assertEquals( 1, ( new WC_Coupon( $coupon ) )->get_usage_count() ); + + $order->update_status( 'processing' ); + $this->assertEquals( 1, $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ); + $this->assertEquals( 1, ( new WC_Coupon( $coupon ) )->get_usage_count() ); + + $order->update_status( 'cancelled' ); + $this->assertEquals( 0, $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ); + $this->assertEquals( 0, ( new WC_Coupon( $coupon ) )->get_usage_count() ); + + $order->update_status( 'pending' ); + $this->assertEquals( 1, $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ); + $this->assertEquals( 1, ( new WC_Coupon( $coupon ) )->get_usage_count() ); + + $order->update_status( 'failed' ); + $this->assertEquals( 0, $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ); + $this->assertEquals( 0, ( new WC_Coupon( $coupon ) )->get_usage_count() ); + + $order->update_status( 'processing' ); + $this->assertEquals( 1, $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ); + $this->assertEquals( 1, ( new WC_Coupon( $coupon ) )->get_usage_count() ); + + $order->update_status( 'completed' ); + $this->assertEquals( 1, $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ); + $this->assertEquals( 1, ( new WC_Coupon( $coupon ) )->get_usage_count() ); + + $order->update_status( 'refunded' ); + $this->assertEquals( 1, $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ); + $this->assertEquals( 1, ( new WC_Coupon( $coupon ) )->get_usage_count() ); + + $order->update_status( 'processing' ); + $this->assertEquals( 1, $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ); + $this->assertEquals( 1, ( new WC_Coupon( $coupon ) )->get_usage_count() ); + + // Test trashing the order. + $order->delete( false ); + $this->assertEquals( 0, $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ); + $this->assertEquals( 0, ( new WC_Coupon( $coupon ) )->get_usage_count() ); + + // To successfully untrash, we need to grab a new instance of the order. + $order = wc_get_order( $order_id ); + $order->untrash(); + $this->assertEquals( 1, $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ); + $this->assertEquals( 1, ( new WC_Coupon( $coupon ) )->get_usage_count() ); + } + }