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