Commit aa9d5439099 for woocommerce

commit aa9d5439099de069af194a109c66641177455be0
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Fri Oct 30 17:17:56 2026 +0100

    Product Collection block: don't save columns and shrinkColumns attributes when not used (#70289)

    * Product Collection block: don't save columns and shrinkColumns attributes when not used

    * Add changelog

    * Linting

    * Simplify if clause

    * Add guard against missing columns

    * Linting

    * Simplify test assertions

diff --git a/plugins/woocommerce/changelog/fix-product-collection-dont-save-columns-shrink-columns-unnecessarily b/plugins/woocommerce/changelog/fix-product-collection-dont-save-columns-shrink-columns-unnecessarily
new file mode 100644
index 00000000000..9f4518d1330
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-product-collection-dont-save-columns-shrink-columns-unnecessarily
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Product Collection block: don't save columns and shrinkColumns attributes when not used
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/columns-control.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/columns-control.tsx
index bf18863f713..99f57d79c83 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/columns-control.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/columns-control.tsx
@@ -12,8 +12,8 @@ import {
 /**
  * Internal dependencies
  */
-import { DisplayLayoutControlProps } from '../../types';
-import { getDefaultDisplayLayout } from '../../utils';
+import { DisplayLayoutControlProps, LayoutOptions } from '../../types';
+import { getDefaultDisplayLayout, sanitizeDisplayLayout } from '../../utils';

 const columnsLabel = __( 'Columns', 'woocommerce' );
 const toggleLabel = __( 'Responsive', 'woocommerce' );
@@ -30,22 +30,23 @@ interface ColumnsControlProps extends DisplayLayoutControlProps {
 const ColumnsControl = ( props: ColumnsControlProps ) => {
 	const { maxColumns, hideResponsiveToggle } = props;
 	const { type, columns, shrinkColumns } = props.displayLayout;
-	const showColumnsControl = type === 'flex';
+	const showColumnsControl = type === LayoutOptions.GRID;

 	const defaultLayout = getDefaultDisplayLayout();
+	const columnCount = columns ?? defaultLayout.columns;

 	const onShrinkColumnsToggleChange = ( value: boolean ) => {
 		props.setAttributes( {
-			displayLayout: {
+			displayLayout: sanitizeDisplayLayout( {
 				...props.displayLayout,
 				shrinkColumns: value,
-			},
+			} ),
 		} );
 	};

 	const onPanelDeselect = () => {
 		props.setAttributes( {
-			displayLayout: defaultLayout,
+			displayLayout: sanitizeDisplayLayout( defaultLayout ),
 		} );
 	};

@@ -54,10 +55,10 @@ const ColumnsControl = ( props: ColumnsControlProps ) => {
 			return;
 		}
 		props.setAttributes( {
-			displayLayout: {
+			displayLayout: sanitizeDisplayLayout( {
 				...props.displayLayout,
 				columns: value,
-			},
+			} ),
 		} );
 	};

@@ -68,7 +69,7 @@ const ColumnsControl = ( props: ColumnsControlProps ) => {
 		<>
 			<ToolsPanelItem
 				label={ columnsLabel }
-				hasValue={ () => defaultLayout?.columns !== columns }
+				hasValue={ () => defaultLayout.columns !== columnCount }
 				isShownByDefault
 				onDeselect={ onPanelDeselect }
 			>
@@ -77,9 +78,9 @@ const ColumnsControl = ( props: ColumnsControlProps ) => {
 					__nextHasNoMarginBottom
 					label={ columnsLabel }
 					onChange={ onColumnsChange }
-					value={ columns }
+					value={ columnCount }
 					min={ 1 }
-					max={ Math.max( effectiveMaxColumns, columns ) }
+					max={ Math.max( effectiveMaxColumns, columnCount ) }
 				/>
 			</ToolsPanelItem>
 			{ ! hideResponsiveToggle && (
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/layout-options-control.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/layout-options-control.tsx
index 4b63f3f3a8b..5e5805367e7 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/layout-options-control.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/layout-options-control.tsx
@@ -19,6 +19,7 @@ import {
  * Internal dependencies
  */
 import { DisplayLayoutControlProps, LayoutOptions } from '../../types';
+import { getDefaultDisplayLayout, sanitizeDisplayLayout } from '../../utils';

 const getHelpText = ( layoutOptions: LayoutOptions ) => {
 	switch ( layoutOptions ) {
@@ -42,14 +43,16 @@ const getHelpText = ( layoutOptions: LayoutOptions ) => {
 const DEFAULT_VALUE = LayoutOptions.GRID;

 const LayoutOptionsControl = ( props: DisplayLayoutControlProps ) => {
-	const { type, columns, shrinkColumns } = props.displayLayout;
-	const setDisplayLayout = ( displayLayout: LayoutOptions ) => {
+	const { type } = props.displayLayout;
+	const setDisplayLayout = ( nextType: LayoutOptions ) => {
 		props.setAttributes( {
-			displayLayout: {
-				type: displayLayout,
-				columns,
-				shrinkColumns,
-			},
+			displayLayout: sanitizeDisplayLayout( {
+				...( nextType === LayoutOptions.GRID
+					? getDefaultDisplayLayout()
+					: {} ),
+				...props.displayLayout,
+				type: nextType,
+			} ),
 		} );
 	};

diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/use-email-column-adjustments.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/use-email-column-adjustments.ts
index 9f26638178b..7d11a5fa7cd 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/use-email-column-adjustments.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/use-email-column-adjustments.ts
@@ -7,7 +7,8 @@ import { useEffect } from '@wordpress/element';
 /**
  * Internal dependencies
  */
-import { type ProductCollectionAttributes } from '../../types';
+import { type ProductCollectionAttributes, LayoutOptions } from '../../types';
+import { sanitizeDisplayLayout } from '../../utils';

 const MAX_EMAIL_COLUMNS = 2;

@@ -32,16 +33,18 @@ const useEmailColumnAdjustments = (
 			return;
 		}

-		// Only adjust columns if currently more than the max allowed for email
+		// Only adjust columns if currently more than the max allowed for email.
+		// Other layouts do not store a column count.
 		if (
+			displayLayout?.type === LayoutOptions.GRID &&
 			displayLayout.columns &&
 			displayLayout.columns > MAX_EMAIL_COLUMNS
 		) {
 			setAttributes( {
-				displayLayout: {
+				displayLayout: sanitizeDisplayLayout( {
 					...displayLayout,
 					columns: MAX_EMAIL_COLUMNS,
-				},
+				} ),
 			} );
 		}
 	}, [ isEmail, displayLayout, setAttributes ] );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/test/editor-contracts.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/test/editor-contracts.tsx
index 10d7b26d9f1..71b045cf2f9 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/test/editor-contracts.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/test/editor-contracts.tsx
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import { act, render, screen, waitFor } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import {
 	createBlock,
@@ -37,6 +37,7 @@ import {
 import {
 	addProductCollectionToQueryPaginationParentOrAncestor,
 	getDefaultValueOfInherit,
+	sanitizeDisplayLayout,
 } from '../utils';
 import {
 	CoreCollectionNames,
@@ -183,6 +184,52 @@ describe( 'Product Collection editor contracts', () => {
 		}
 	);

+	it.each( [
+		{
+			layout: LayoutOptions.STACK,
+			saved: { type: LayoutOptions.STACK },
+		},
+		{
+			layout: LayoutOptions.CAROUSEL,
+			saved: { type: LayoutOptions.CAROUSEL },
+		},
+	] )(
+		'omits column settings from the saved $layout markup',
+		( { layout, saved } ) => {
+			const block = createBlock( PRODUCT_COLLECTION_BLOCK_NAME, {
+				displayLayout: sanitizeDisplayLayout( {
+					type: layout,
+					columns: 4,
+					shrinkColumns: true,
+				} ),
+			} );
+			const serialized = serialize( block );
+
+			expect( serialized ).toContain(
+				`"displayLayout":${ JSON.stringify( saved ) }`
+			);
+		}
+	);
+
+	it( 'keeps column settings in the saved Grid markup', () => {
+		const block = createBlock( PRODUCT_COLLECTION_BLOCK_NAME, {
+			displayLayout: sanitizeDisplayLayout( {
+				type: LayoutOptions.GRID,
+				columns: 4,
+				shrinkColumns: false,
+			} ),
+		} );
+		const serialized = serialize( block );
+
+		expect( serialized ).toContain(
+			`"displayLayout":${ JSON.stringify( {
+				type: LayoutOptions.GRID,
+				columns: 4,
+				shrinkColumns: false,
+			} ) }`
+		);
+	} );
+
 	it( 'adds Product Collection to the real Pagination metadata filter', () => {
 		addProductCollectionToQueryPaginationParentOrAncestor();

@@ -391,14 +438,12 @@ describe( 'default catalog order control', () => {
 					trackInteraction={ trackInteraction }
 				/>
 			);
-			await act( async () => {
-				await user.selectOptions(
-					screen.getByRole( 'combobox', {
-						name: 'Default sort by',
-					} ),
-					'price-desc'
-				);
-			} );
+			await user.selectOptions(
+				screen.getByRole( 'combobox', {
+					name: 'Default sort by',
+				} ),
+				'price-desc'
+			);

 			expect( editEntityRecord ).toHaveBeenCalledWith(
 				'root',
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
index d1cb54043f8..479fedf5048 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
@@ -56,8 +56,14 @@ export enum WidthOptions {

 export interface ProductCollectionDisplayLayout {
 	type: LayoutOptions;
-	columns: number;
-	shrinkColumns: boolean;
+	/**
+	 * Column count for the Grid layout. Omitted for Stack and Carousel.
+	 */
+	columns?: number;
+	/**
+	 * Whether Grid columns shrink on smaller screens. Omitted for Stack and Carousel.
+	 */
+	shrinkColumns?: boolean;
 }

 export interface ProductCollectionDimensions {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx
index 65727800303..3402ff63c27 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx
@@ -35,6 +35,7 @@ import {
 	SetPreviewState,
 	ProductCollectionUIStatesInEditor,
 	CoreCollectionNames,
+	LayoutOptions,
 } from './types';
 import {
 	coreQueryPaginationBlockName,
@@ -496,8 +497,33 @@ export const getDefaultQueryForSettingsSection = (
 	pages: DEFAULT_QUERY.pages,
 } );

-export const getDefaultDisplayLayout = () =>
-	DEFAULT_ATTRIBUTES.displayLayout as ProductCollectionDisplayLayout;
+export const getDefaultDisplayLayout =
+	(): Required< ProductCollectionDisplayLayout > =>
+		DEFAULT_ATTRIBUTES.displayLayout as Required< ProductCollectionDisplayLayout >;
+
+/**
+ * Drop column settings the current layout does not use.
+ * Stack and Carousel ignore `columns` and `shrinkColumns`, so those keys are
+ * left out of the attributes that get saved in the block comment.
+ */
+export function sanitizeDisplayLayout(
+	displayLayout?: Partial< ProductCollectionDisplayLayout > | null
+): ProductCollectionDisplayLayout {
+	if (
+		displayLayout?.type === LayoutOptions.STACK ||
+		displayLayout?.type === LayoutOptions.CAROUSEL
+	) {
+		return { type: displayLayout.type };
+	}
+
+	const defaults = getDefaultDisplayLayout();
+
+	return {
+		type: displayLayout?.type ?? defaults.type,
+		columns: displayLayout?.columns ?? defaults.columns,
+		shrinkColumns: displayLayout?.shrinkColumns ?? defaults.shrinkColumns,
+	};
+}

 export const getDefaultSettings = (
 	currentAttributes: ProductCollectionAttributes
@@ -585,7 +611,7 @@ export const useGetProduct = ( productId: number | undefined ) => {
 						productId
 					) ) as ProductResponseItem;
 					setProduct( fetchedProduct );
-				} catch ( error ) {
+				} catch {
 					setProduct( null );
 				} finally {
 					setIsLoading( false );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-template/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-template/edit.tsx
index 4b53a495a7e..a0329c03ca8 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-template/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-template/edit.tsx
@@ -222,11 +222,11 @@ const ProductTemplateEdit = (
 			},
 			queryContext = [ { page: 1 } ],
 			templateSlug,
-			displayLayout: { type: layoutType, columns, shrinkColumns } = {
-				type: 'flex',
-				columns: 3,
-				shrinkColumns: false,
-			},
+			displayLayout: {
+				type: layoutType = 'flex',
+				columns = 3,
+				shrinkColumns = false,
+			} = {},
 			queryContextIncludes = [],
 			__privateProductCollectionPreviewState,
 		},
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php
index 5ea89fb8483..57368f2a9f4 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php
@@ -61,13 +61,16 @@ class ProductTemplate extends AbstractBlock {

 		$classnames = '';
 		if ( isset( $block->context['displayLayout'] ) && isset( $block->context['query'] ) ) {
-			$classnames = 'is-product-collection-layout-' . $block->context['displayLayout']['type'] . ' ';
+			$type    = $block->context['displayLayout']['type'] ?? '';
+			$columns = $block->context['displayLayout']['columns'] ?? null;

-			if ( isset( $block->context['displayLayout']['type'] ) && 'flex' === $block->context['displayLayout']['type'] ) {
+			$classnames = 'is-product-collection-layout-' . $type . ' ';
+
+			if ( 'flex' === $type && ! empty( $columns ) ) {
 				if ( isset( $block->context['displayLayout']['shrinkColumns'] ) && $block->context['displayLayout']['shrinkColumns'] ) {
-					$classnames = "wc-block-product-template__responsive columns-{$block->context['displayLayout']['columns']}";
+					$classnames = "wc-block-product-template__responsive columns-{$columns}";
 				} else {
-					$classnames = "is-flex-container columns-{$block->context['displayLayout']['columns']}";
+					$classnames = "is-flex-container columns-{$columns}";
 				}
 			}
 		}