Commit c7d5c87a182 for woocommerce

commit c7d5c87a182e521916efcf8631b580cbc32d38fa
Author: Luigi Teschio <gigitux@gmail.com>
Date:   Tue Apr 21 17:05:07 2026 +0200

    [DataViews - All Products] Add inventory field modules (#64270)

    * Add shared controls for products app fields

    * Add pricing fields to products app

    * Add inventory fields to products app

    * remove not necessary code

    * clean up code

    * Update packages/js/experimental-products-app/tsconfig.json

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

    * Update packages/js/experimental-products-app/src/fields/components/date-picker.tsx

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

    * Fix list-item: import style.scss and default alt to empty string

    Agent-Logs-Url: https://github.com/woocommerce/woocommerce/sessions/f3cf7f22-fc05-4c03-a256-0b68bdb81cd5

    Co-authored-by: gigitux <4463174+gigitux@users.noreply.github.com>

    * fix build

    * fix build

    * lint code

    * fix tsconfig

    * lint code

    * Apply suggestion from @Copilot

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

    * lint code

    * fix unit test

    ---------

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
    Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
    Co-authored-by: gigitux <4463174+gigitux@users.noreply.github.com>

diff --git a/packages/js/experimental-products-app/changelog/add-products-fields-inventory-shipping b/packages/js/experimental-products-app/changelog/add-products-fields-inventory-shipping
new file mode 100644
index 00000000000..7bc4c3fdfe2
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/add-products-fields-inventory-shipping
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add inventory and shipping field modules for the experimental products app
diff --git a/packages/js/experimental-products-app/src/fields/date_on_sale_from/field.test.tsx b/packages/js/experimental-products-app/src/fields/date_on_sale_from/field.test.tsx
deleted file mode 100644
index 3cad58c0fd6..00000000000
--- a/packages/js/experimental-products-app/src/fields/date_on_sale_from/field.test.tsx
+++ /dev/null
@@ -1,252 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-/**
- * External dependencies
- */
-import { act, render } from '@testing-library/react';
-
-/**
- * Internal dependencies
- */
-import { fieldExtensions } from './field';
-
-let lastDatePickerProps: any;
-
-jest.mock( '../components/date-picker', () => ( {
-	DatePicker: ( props: any ) => {
-		lastDatePickerProps = props;
-		return <div data-testid="date-picker" />;
-	},
-	parseDateTimeLocal: jest.requireActual( '../components/date-picker' )
-		.parseDateTimeLocal,
-	formatDateTimeLocal: jest.requireActual( '../components/date-picker' )
-		.formatDateTimeLocal,
-} ) );
-
-const Edit = fieldExtensions.Edit!;
-const baseField = { id: 'date_on_sale_from', label: 'Sale start' } as any;
-
-const makeProduct = ( overrides: Record< string, any > = {} ) => ( {
-	date_on_sale_from: '',
-	date_on_sale_to: '',
-	...overrides,
-} );
-
-describe( 'date_on_sale_from field', () => {
-	beforeEach( () => {
-		lastDatePickerProps = undefined;
-		jest.clearAllMocks();
-	} );
-
-	describe( 'isVisible', () => {
-		it( 'returns true when date_on_sale_from is set', () => {
-			expect(
-				fieldExtensions.isVisible!(
-					makeProduct( {
-						date_on_sale_from: '2025-03-01T00:00',
-					} ) as any,
-					{} as any
-				)
-			).toBe( true );
-		} );
-
-		it( 'returns true when date_on_sale_to is set', () => {
-			expect(
-				fieldExtensions.isVisible!(
-					makeProduct( {
-						date_on_sale_to: '2025-03-01T00:00',
-					} ) as any,
-					{} as any
-				)
-			).toBe( true );
-		} );
-
-		it( 'returns false when neither date is set', () => {
-			expect(
-				fieldExtensions.isVisible!( makeProduct() as any, {} as any )
-			).toBe( false );
-		} );
-	} );
-
-	describe( 'Edit', () => {
-		it( 'passes isDateDisabled that rejects past dates', () => {
-			render(
-				<Edit
-					data={ makeProduct() as any }
-					onChange={ jest.fn() }
-					field={ baseField }
-				/>
-			);
-
-			const yesterday = new Date();
-			yesterday.setDate( yesterday.getDate() - 1 );
-			expect( lastDatePickerProps.isDateDisabled( yesterday ) ).toBe(
-				true
-			);
-
-			const tomorrow = new Date();
-			tomorrow.setDate( tomorrow.getDate() + 1 );
-			expect( lastDatePickerProps.isDateDisabled( tomorrow ) ).toBe(
-				false
-			);
-		} );
-
-		it( 'passes min as today', () => {
-			render(
-				<Edit
-					data={ makeProduct() as any }
-					onChange={ jest.fn() }
-					field={ baseField }
-				/>
-			);
-
-			const min = lastDatePickerProps.min as Date;
-			const today = new Date();
-			today.setHours( 0, 0, 0, 0 );
-
-			expect( min.getTime() ).toBe( today.getTime() );
-		} );
-
-		it( 'calls onChange with value when no end date conflict', () => {
-			const onChange = jest.fn();
-
-			render(
-				<Edit
-					data={
-						makeProduct( {
-							date_on_sale_from: '2025-03-01T00:00',
-							date_on_sale_to: '2025-03-20T00:00',
-						} ) as any
-					}
-					onChange={ onChange }
-					field={ baseField }
-				/>
-			);
-
-			act( () => {
-				lastDatePickerProps.onChange( {
-					date_on_sale_from: '2025-03-05T10:00',
-				} );
-			} );
-
-			expect( onChange ).toHaveBeenCalledTimes( 1 );
-			expect( onChange ).toHaveBeenCalledWith( {
-				date_on_sale_from: '2025-03-05T10:00',
-			} );
-		} );
-
-		it( 'auto-adjusts end date when start >= end in a single onChange call', () => {
-			const onChange = jest.fn();
-
-			render(
-				<Edit
-					data={
-						makeProduct( {
-							date_on_sale_from: '2025-03-01T00:00',
-							date_on_sale_to: '2025-03-10T00:00',
-						} ) as any
-					}
-					onChange={ onChange }
-					field={ baseField }
-				/>
-			);
-
-			act( () => {
-				lastDatePickerProps.onChange( {
-					date_on_sale_from: '2025-03-15T10:00',
-				} );
-			} );
-
-			expect( onChange ).toHaveBeenCalledTimes( 1 );
-			expect( onChange ).toHaveBeenCalledWith( {
-				date_on_sale_from: '2025-03-15T10:00',
-				date_on_sale_to: '2025-03-16T10:00',
-			} );
-		} );
-
-		it( 'auto-adjusts end date when start equals end exactly', () => {
-			const onChange = jest.fn();
-
-			render(
-				<Edit
-					data={
-						makeProduct( {
-							date_on_sale_from: '2025-03-01T00:00',
-							date_on_sale_to: '2025-03-10T12:00',
-						} ) as any
-					}
-					onChange={ onChange }
-					field={ baseField }
-				/>
-			);
-
-			act( () => {
-				lastDatePickerProps.onChange( {
-					date_on_sale_from: '2025-03-10T12:00',
-				} );
-			} );
-
-			expect( onChange ).toHaveBeenCalledTimes( 1 );
-			expect( onChange ).toHaveBeenCalledWith( {
-				date_on_sale_from: '2025-03-10T12:00',
-				date_on_sale_to: '2025-03-11T12:00',
-			} );
-		} );
-
-		it( 'does not adjust end date when end is not set', () => {
-			const onChange = jest.fn();
-
-			render(
-				<Edit
-					data={
-						makeProduct( {
-							date_on_sale_from: '',
-							date_on_sale_to: '',
-						} ) as any
-					}
-					onChange={ onChange }
-					field={ baseField }
-				/>
-			);
-
-			act( () => {
-				lastDatePickerProps.onChange( {
-					date_on_sale_from: '2025-03-15T10:00',
-				} );
-			} );
-
-			expect( onChange ).toHaveBeenCalledTimes( 1 );
-			expect( onChange ).toHaveBeenCalledWith( {
-				date_on_sale_from: '2025-03-15T10:00',
-			} );
-		} );
-
-		it( 'does not adjust end date when start is cleared', () => {
-			const onChange = jest.fn();
-
-			render(
-				<Edit
-					data={
-						makeProduct( {
-							date_on_sale_from: '2025-03-01T00:00',
-							date_on_sale_to: '2025-03-10T00:00',
-						} ) as any
-					}
-					onChange={ onChange }
-					field={ baseField }
-				/>
-			);
-
-			act( () => {
-				lastDatePickerProps.onChange( {
-					date_on_sale_from: null,
-				} );
-			} );
-
-			expect( onChange ).toHaveBeenCalledTimes( 1 );
-			expect( onChange ).toHaveBeenCalledWith( {
-				date_on_sale_from: null,
-			} );
-		} );
-	} );
-} );
-/* eslint-enable @typescript-eslint/no-explicit-any */
diff --git a/packages/js/experimental-products-app/src/fields/date_on_sale_to/field.test.tsx b/packages/js/experimental-products-app/src/fields/date_on_sale_to/field.test.tsx
deleted file mode 100644
index 65a6db92a78..00000000000
--- a/packages/js/experimental-products-app/src/fields/date_on_sale_to/field.test.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-/**
- * External dependencies
- */
-import { render } from '@testing-library/react';
-
-/**
- * Internal dependencies
- */
-import { fieldExtensions } from './field';
-
-let lastDatePickerProps: any;
-
-jest.mock( '../components/date-picker', () => ( {
-	DatePicker: ( props: any ) => {
-		lastDatePickerProps = props;
-		return <div data-testid="date-picker" />;
-	},
-	parseDateTimeLocal: jest.requireActual( '../components/date-picker' )
-		.parseDateTimeLocal,
-} ) );
-
-const Edit = fieldExtensions.Edit!;
-const baseField = { id: 'date_on_sale_to', label: 'Sale end' } as any;
-
-const makeProduct = ( overrides: Record< string, any > = {} ) => ( {
-	date_on_sale_from: '',
-	date_on_sale_to: '',
-	...overrides,
-} );
-
-describe( 'date_on_sale_to field', () => {
-	beforeEach( () => {
-		lastDatePickerProps = undefined;
-		jest.clearAllMocks();
-	} );
-
-	describe( 'Edit', () => {
-		it( 'sets min to start date + 1 minute when start date exists', () => {
-			render(
-				<Edit
-					data={
-						makeProduct( {
-							date_on_sale_from: '2025-03-15T10:00',
-							date_on_sale_to: '2025-03-20T12:00',
-						} ) as any
-					}
-					onChange={ jest.fn() }
-					field={ baseField }
-				/>
-			);
-
-			const min = lastDatePickerProps.min as Date;
-			expect( min.getFullYear() ).toBe( 2025 );
-			expect( min.getMonth() ).toBe( 2 ); // March
-			expect( min.getDate() ).toBe( 15 );
-			expect( min.getHours() ).toBe( 10 );
-			expect( min.getMinutes() ).toBe( 1 );
-		} );
-
-		it( 'prevents end date from equalling start date', () => {
-			render(
-				<Edit
-					data={
-						makeProduct( {
-							date_on_sale_from: '2025-03-15T10:00',
-							date_on_sale_to: '2025-03-20T12:00',
-						} ) as any
-					}
-					onChange={ jest.fn() }
-					field={ baseField }
-				/>
-			);
-
-			const min = lastDatePickerProps.min as Date;
-			const start = new Date( 2025, 2, 15, 10, 0 );
-
-			// min must be strictly after start
-			expect( min.getTime() ).toBeGreaterThan( start.getTime() );
-		} );
-
-		it( 'sets min to today when no start date', () => {
-			render(
-				<Edit
-					data={
-						makeProduct( {
-							date_on_sale_to: '2025-03-20T12:00',
-						} ) as any
-					}
-					onChange={ jest.fn() }
-					field={ baseField }
-				/>
-			);
-
-			const min = lastDatePickerProps.min as Date;
-			const today = new Date();
-			today.setHours( 0, 0, 0, 0 );
-
-			expect( min.getTime() ).toBe( today.getTime() );
-		} );
-
-		it( 'disables calendar dates before start date', () => {
-			render(
-				<Edit
-					data={
-						makeProduct( {
-							date_on_sale_from: '2025-03-15T10:00',
-							date_on_sale_to: '2025-03-20T12:00',
-						} ) as any
-					}
-					onChange={ jest.fn() }
-					field={ baseField }
-				/>
-			);
-
-			const march14 = new Date( 2025, 2, 14 );
-			const march15 = new Date( 2025, 2, 15 );
-			const march16 = new Date( 2025, 2, 16 );
-
-			expect( lastDatePickerProps.isDateDisabled( march14 ) ).toBe(
-				true
-			);
-			expect( lastDatePickerProps.isDateDisabled( march15 ) ).toBe(
-				false
-			);
-			expect( lastDatePickerProps.isDateDisabled( march16 ) ).toBe(
-				false
-			);
-		} );
-
-		it( 'disables calendar dates before today when no start date', () => {
-			render(
-				<Edit
-					data={
-						makeProduct( {
-							date_on_sale_to: '2025-03-20T12:00',
-						} ) as any
-					}
-					onChange={ jest.fn() }
-					field={ baseField }
-				/>
-			);
-
-			const yesterday = new Date();
-			yesterday.setDate( yesterday.getDate() - 1 );
-
-			const tomorrow = new Date();
-			tomorrow.setDate( tomorrow.getDate() + 1 );
-
-			expect( lastDatePickerProps.isDateDisabled( yesterday ) ).toBe(
-				true
-			);
-			expect( lastDatePickerProps.isDateDisabled( tomorrow ) ).toBe(
-				false
-			);
-		} );
-
-		it( 'passes onChange directly without wrapper', () => {
-			const onChange = jest.fn();
-
-			render(
-				<Edit
-					data={
-						makeProduct( {
-							date_on_sale_to: '2025-03-20T12:00',
-						} ) as any
-					}
-					onChange={ onChange }
-					field={ baseField }
-				/>
-			);
-
-			expect( lastDatePickerProps.onChange ).toBe( onChange );
-		} );
-	} );
-} );
-/* eslint-enable @typescript-eslint/no-explicit-any */
diff --git a/packages/js/experimental-products-app/src/fields/height/field.tsx b/packages/js/experimental-products-app/src/fields/height/field.tsx
new file mode 100644
index 00000000000..dca06c38211
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/height/field.tsx
@@ -0,0 +1,26 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+import { createDimensionField } from '../components/dimension';
+
+const fieldDefinition = {
+	type: 'text',
+	label: __( 'Height', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	...createDimensionField( 'height' ),
+};
diff --git a/packages/js/experimental-products-app/src/fields/inventory_summary/field.tsx b/packages/js/experimental-products-app/src/fields/inventory_summary/field.tsx
new file mode 100644
index 00000000000..c15ad34dc42
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/inventory_summary/field.tsx
@@ -0,0 +1,57 @@
+/**
+ * External dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+const fieldDefinition = {
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+	elements: [
+		{ label: __( 'In stock', 'woocommerce' ), value: 'instock' },
+		{ label: __( 'Out of stock', 'woocommerce' ), value: 'outofstock' },
+		{ label: __( 'On backorder', 'woocommerce' ), value: 'onbackorder' },
+	],
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	render: ( { item, field } ) => {
+		const status = item.stock_status?.toLowerCase?.() ?? 'instock';
+		const match = field.elements?.find(
+			( element: { value: string } ) => element.value === status
+		);
+
+		if ( item.manage_stock ) {
+			if ( ! Number.isFinite( item.stock_quantity ) ) {
+				return (
+					<div className="woocommerce-fields-field__inventory-summary">
+						{ __( 'No stock quantity set', 'woocommerce' ) }
+					</div>
+				);
+			}
+
+			return (
+				<div className="woocommerce-fields-field__inventory-summary">
+					{ sprintf(
+						/* translators: %d: stock quantity */
+						__( '%d available in stock', 'woocommerce' ),
+						item.stock_quantity
+					) }
+				</div>
+			);
+		}
+		return (
+			<div className="woocommerce-fields-field__inventory-summary">
+				{ match?.label ?? item.stock_status }
+			</div>
+		);
+	},
+};
diff --git a/packages/js/experimental-products-app/src/fields/length/field.tsx b/packages/js/experimental-products-app/src/fields/length/field.tsx
new file mode 100644
index 00000000000..7cba4ce2c45
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/length/field.tsx
@@ -0,0 +1,26 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+import { createDimensionField } from '../components/dimension';
+
+const fieldDefinition = {
+	type: 'text',
+	label: __( 'Length', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	...createDimensionField( 'length' ),
+};
diff --git a/packages/js/experimental-products-app/src/fields/manage_stock/field.tsx b/packages/js/experimental-products-app/src/fields/manage_stock/field.tsx
new file mode 100644
index 00000000000..a4c82e4b217
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/manage_stock/field.tsx
@@ -0,0 +1,25 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+const fieldDefinition = {
+	type: 'boolean',
+	label: __( 'Track inventory', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	type: 'boolean',
+	Edit: 'toggle',
+};
diff --git a/packages/js/experimental-products-app/src/fields/on_sale/test/field.test.tsx b/packages/js/experimental-products-app/src/fields/on_sale/test/field.test.tsx
deleted file mode 100644
index 747393eecec..00000000000
--- a/packages/js/experimental-products-app/src/fields/on_sale/test/field.test.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * Internal dependencies
- */
-import type { ProductEntityRecord } from '../../types';
-
-import { fieldExtensions } from '../field';
-
-const makeProduct = (
-	overrides: Partial< ProductEntityRecord > = {}
-): ProductEntityRecord =>
-	( {
-		id: 1,
-		type: 'simple',
-		on_sale: false,
-		sale_price: '',
-		...overrides,
-	} as unknown as ProductEntityRecord );
-
-describe( 'on_sale field', () => {
-	it( 'has type boolean', () => {
-		expect( fieldExtensions.type ).toBe( 'boolean' );
-	} );
-
-	describe( 'getValue', () => {
-		const { getValue } = fieldExtensions;
-
-		it( 'returns false when not on sale and no sale price', () => {
-			expect(
-				getValue!( {
-					item: makeProduct( { on_sale: false, sale_price: '' } ),
-				} )
-			).toBe( false );
-		} );
-
-		it( 'returns true when on_sale is true', () => {
-			expect(
-				getValue!( { item: makeProduct( { on_sale: true } ) } )
-			).toBe( true );
-		} );
-
-		it( 'returns true when sale_price is set', () => {
-			expect(
-				getValue!( {
-					item: makeProduct( {
-						on_sale: false,
-						sale_price: '9.99',
-					} ),
-				} )
-			).toBe( true );
-		} );
-	} );
-
-	describe( 'setValue', () => {
-		const { setValue } = fieldExtensions;
-
-		it( 'sets on_sale true when enabling', () => {
-			expect( setValue!( { value: true } ) ).toEqual( {
-				on_sale: true,
-			} );
-		} );
-
-		it( 'clears sale fields when disabling', () => {
-			expect( setValue!( { value: false } ) ).toEqual( {
-				on_sale: false,
-				sale_price: '',
-				date_on_sale_from: null,
-				date_on_sale_to: null,
-			} );
-		} );
-	} );
-
-	it( 'uses the built-in toggle Edit component', () => {
-		expect( fieldExtensions.Edit ).toBe( 'toggle' );
-	} );
-} );
diff --git a/packages/js/experimental-products-app/src/fields/price_summary/field.test.tsx b/packages/js/experimental-products-app/src/fields/price_summary/field.test.tsx
deleted file mode 100644
index c788b177949..00000000000
--- a/packages/js/experimental-products-app/src/fields/price_summary/field.test.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * External dependencies
- */
-import { render, screen } from '@testing-library/react';
-
-import React from 'react';
-
-/**
- * Internal dependencies
- */
-import type { ProductEntityRecord } from '../types';
-
-import { fieldExtensions } from './field';
-
-const renderSummary = ( item: Partial< ProductEntityRecord > ) => {
-	if ( ! fieldExtensions.render ) {
-		throw new Error( 'price_summary render not implemented' );
-	}
-
-	const Render = fieldExtensions.render as React.ComponentType< {
-		item: Partial< ProductEntityRecord >;
-	} >;
-
-	return render( React.createElement( Render, { item } ) );
-};
-
-describe( 'price_summary field', () => {
-	beforeEach( () => {
-		document.documentElement.lang = 'en-US';
-	} );
-
-	it( 'shows the sale price and label when the product is on sale', () => {
-		renderSummary( {
-			on_sale: true,
-			regular_price: '150',
-			sale_price: '100',
-		} );
-
-		expect( screen.getByText( '$100.00 · On sale' ) ).toBeInTheDocument();
-	} );
-
-	it( 'falls back to the regular price when the product is not on sale', () => {
-		renderSummary( {
-			on_sale: false,
-			regular_price: '99',
-		} );
-
-		expect( screen.getByText( '$99.00' ) ).toBeInTheDocument();
-		expect( screen.queryByText( /On sale/i ) ).not.toBeInTheDocument();
-	} );
-
-	it( 'renders nothing when no price data is available', () => {
-		const { container } = renderSummary( {} );
-
-		expect( container ).toBeEmptyDOMElement();
-	} );
-} );
diff --git a/packages/js/experimental-products-app/src/fields/shipping_class/field.tsx b/packages/js/experimental-products-app/src/fields/shipping_class/field.tsx
new file mode 100644
index 00000000000..4e174986834
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/shipping_class/field.tsx
@@ -0,0 +1,78 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useSelect } from '@wordpress/data';
+import { SelectControl } from '@wordpress/components';
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+
+import type { ProductEntityRecord } from '../types';
+
+interface ProductShippingClass {
+	id: number;
+	slug: string;
+	name: string;
+	description: string;
+	count: number;
+}
+
+const fieldDefinition = {
+	type: 'text',
+	label: __( 'Shipping Class', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	id: 'shipping_class',
+	label: __( 'Shipping Class', 'woocommerce' ),
+	enableSorting: false,
+	type: 'text',
+	getValue: ( { item } ) => item.shipping_class,
+	Edit: ( { data, onChange, field } ) => {
+		const { shippingClasses } = useSelect( ( select ) => {
+			// TODO: Register shipping class entity and use it instead.
+			// eslint-disable-next-line @wordpress/data-no-store-string-literals
+			const { getProductShippingClasses } = select(
+				'experimental/wc/admin/products/shipping-classes'
+			);
+			return {
+				shippingClasses:
+					// @ts-expect-error - The store return type lives in Woo core.
+					getProductShippingClasses() as ProductShippingClass[],
+			};
+		}, [] );
+
+		const options = [
+			{
+				label: __( 'No shipping class', 'woocommerce' ),
+				value: '',
+			},
+			...( shippingClasses?.length
+				? shippingClasses.map( ( shippingClass ) => ( {
+						label: shippingClass.name,
+						value: shippingClass.slug,
+				  } ) )
+				: [] ),
+		];
+
+		return (
+			<SelectControl
+				label={ field.label }
+				value={ data.shipping_class }
+				options={ options }
+				onChange={ ( value ) =>
+					onChange( {
+						shipping_class: value,
+					} )
+				}
+			/>
+		);
+	},
+};
diff --git a/packages/js/experimental-products-app/src/fields/shipping_summary/field.tsx b/packages/js/experimental-products-app/src/fields/shipping_summary/field.tsx
new file mode 100644
index 00000000000..bc26afa8f7c
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/shipping_summary/field.tsx
@@ -0,0 +1,64 @@
+/**
+ * External dependencies
+ */
+import { useEntityRecord } from '@wordpress/core-data';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord, SettingsEntityRecord } from '../types';
+
+const ShippingSummaryRender = ( { item }: { item: ProductEntityRecord } ) => {
+	const {
+		record: storeProductsSettings,
+		isResolving: storeProductsSettingsResolving,
+	} = useEntityRecord< SettingsEntityRecord >(
+		'root',
+		'settings',
+		'products'
+	);
+
+	if ( storeProductsSettingsResolving ) {
+		return null;
+	}
+
+	const length = item.dimensions?.length;
+	const width = item.dimensions?.width;
+	const height = item.dimensions?.height;
+	const weight = item.weight;
+	const weightUnit = storeProductsSettings?.values?.woocommerce_weight_unit;
+	const dimensionUnit =
+		storeProductsSettings?.values?.woocommerce_dimension_unit;
+
+	// Return null if no shipping info available
+	if ( ! length && ! width && ! height && ! weight ) {
+		return null;
+	}
+
+	const parts: string[] = [];
+
+	if ( length && width && height ) {
+		parts.push(
+			`${ length } ${ dimensionUnit } x ${ width } ${ dimensionUnit } x ${ height } ${ dimensionUnit }`
+		);
+	}
+
+	if ( weight ) {
+		parts.push( `${ weight } ${ weightUnit }` );
+	}
+
+	return <span>{ parts.join( ' · ' ) }</span>;
+};
+
+const fieldDefinition = {
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	render: ( { item } ) => <ShippingSummaryRender item={ item } />,
+};
diff --git a/packages/js/experimental-products-app/src/fields/stock/field.tsx b/packages/js/experimental-products-app/src/fields/stock/field.tsx
new file mode 100644
index 00000000000..ed33eeb476a
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/stock/field.tsx
@@ -0,0 +1,74 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { SelectControl } from '@wordpress/components';
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+function isValidStockStatus( value: string ) {
+	return (
+		value === 'instock' || value === 'outofstock' || value === 'onbackorder'
+	);
+}
+
+const fieldDefinition = {
+	label: __( 'Stock', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: {
+		operators: [ 'is' ],
+	},
+	elements: [
+		{ label: __( 'In stock', 'woocommerce' ), value: 'instock' },
+		{ label: __( 'Out of stock', 'woocommerce' ), value: 'outofstock' },
+		{ label: __( 'On backorder', 'woocommerce' ), value: 'onbackorder' },
+	],
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	isVisible: ( item ) => {
+		return ! item.manage_stock;
+	},
+	getValue: ( { item } ) => item.stock_status,
+	render: ( { item, field } ) => {
+		const match = field?.elements?.find(
+			( status ) => status.value === item.stock_status
+		);
+		return match ? (
+			<div className="woocommerce-fields-field__stock">
+				<span
+					className={ `woocommerce-fields-field__stock-label woocommerce-fields-field__stock-label--${ match.value }` }
+				>
+					{ match.label }
+				</span>
+				{ item.stock_quantity && item.stock_quantity > 0 && (
+					<span className="woocommerce-fields-field__stock-quantity">
+						({ item.stock_quantity })
+					</span>
+				) }
+			</div>
+		) : (
+			item.stock_status
+		);
+	},
+	Edit: ( { data, onChange, field } ) => (
+		<SelectControl
+			label={ __( 'Status', 'woocommerce' ) }
+			value={ data.stock_status }
+			options={ field?.elements || [] }
+			onChange={ ( value ) => {
+				if ( value && isValidStockStatus( value ) ) {
+					onChange( {
+						stock_status: value,
+					} );
+				}
+			} }
+		/>
+	),
+};
diff --git a/packages/js/experimental-products-app/src/fields/stock/style.scss b/packages/js/experimental-products-app/src/fields/stock/style.scss
new file mode 100644
index 00000000000..9d1d8a3a1f2
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/stock/style.scss
@@ -0,0 +1,14 @@
+.woocommerce-fields-field__stock {
+	display: flex;
+	gap: 5px;
+
+	.woocommerce-fields-field__stock-label {
+		&--outofstock {
+			color: #cc1818;
+		}
+
+		&--onbackorder {
+			color: #ffa500;
+		}
+	}
+}
diff --git a/packages/js/experimental-products-app/src/fields/stock_quantity/field.tsx b/packages/js/experimental-products-app/src/fields/stock_quantity/field.tsx
new file mode 100644
index 00000000000..a5bd56b7bc1
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/stock_quantity/field.tsx
@@ -0,0 +1,26 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+const fieldDefinition = {
+	type: 'integer',
+	label: __( 'Available stock', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	isVisible: ( item ) => {
+		return !! item.manage_stock;
+	},
+};
diff --git a/packages/js/experimental-products-app/src/fields/tax_status/field.tsx b/packages/js/experimental-products-app/src/fields/tax_status/field.tsx
new file mode 100644
index 00000000000..a21630fa28d
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/tax_status/field.tsx
@@ -0,0 +1,38 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { CheckboxControl } from '@wordpress/components';
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+
+import type { ProductEntityRecord } from '../types';
+
+const fieldDefinition = {
+	label: __( 'Tax Status', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	enableSorting: false,
+	Edit: ( { data, onChange, field } ) => {
+		return (
+			<CheckboxControl
+				label={ field.label }
+				checked={ data.tax_status === 'taxable' }
+				onChange={ () =>
+					onChange( {
+						tax_status:
+							data.tax_status === 'taxable' ? 'none' : 'taxable',
+					} )
+				}
+			/>
+		);
+	},
+};
diff --git a/packages/js/experimental-products-app/src/fields/weight/field.tsx b/packages/js/experimental-products-app/src/fields/weight/field.tsx
new file mode 100644
index 00000000000..ec53e70ff5c
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/weight/field.tsx
@@ -0,0 +1,61 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useEntityRecord } from '@wordpress/core-data';
+import { InputControl, InputLayout } from '@wordpress/ui';
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+
+import type { ProductEntityRecord, SettingsEntityRecord } from '../types';
+
+const fieldDefinition = {
+	type: 'text',
+	label: __( 'Weight', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	label: __( 'Weight', 'woocommerce' ),
+	Edit: ( { data, onChange, field } ) => {
+		const {
+			record: storeProductsSettings,
+			isResolving: storeProductsSettingsResolving,
+		} = useEntityRecord< SettingsEntityRecord >(
+			'root',
+			'settings',
+			'products'
+		);
+
+		if ( storeProductsSettingsResolving ) {
+			return null;
+		}
+
+		const weightUnit =
+			storeProductsSettings?.values?.woocommerce_weight_unit;
+
+		return (
+			<InputControl
+				label={ field.label }
+				value={ data.weight }
+				onChange={ ( event ) =>
+					onChange( { weight: event.target.value } )
+				}
+				type="number"
+				min={ 0 }
+				step="any"
+				suffix={
+					<InputLayout.Slot padding="minimal">
+						{ weightUnit }
+					</InputLayout.Slot>
+				}
+			/>
+		);
+	},
+};
diff --git a/packages/js/experimental-products-app/src/fields/width/field.tsx b/packages/js/experimental-products-app/src/fields/width/field.tsx
new file mode 100644
index 00000000000..1b72dc4f440
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/width/field.tsx
@@ -0,0 +1,26 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+import { createDimensionField } from '../components/dimension';
+
+const fieldDefinition = {
+	type: 'text',
+	label: __( 'Width', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	...createDimensionField( 'width' ),
+};