Commit 8edc64c123f for woocommerce

commit 8edc64c123fd53c263bca8c7349ab8c1c532e6b0
Author: Luigi Teschio <gigitux@gmail.com>
Date:   Tue Apr 21 16:17:50 2026 +0200

    [DataViews - All Products] Add pricing field modules (#64269)

    * Add shared controls for products app fields

    * Add pricing 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

    ---------

    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-pricing b/packages/js/experimental-products-app/changelog/add-products-fields-pricing
new file mode 100644
index 00000000000..c09210746f7
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/add-products-fields-pricing
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add pricing field modules for the experimental products app
diff --git a/packages/js/experimental-products-app/src/fields/components/date-picker.tsx b/packages/js/experimental-products-app/src/fields/components/date-picker.tsx
index 34bbfe5eea7..05a20ae2d26 100644
--- a/packages/js/experimental-products-app/src/fields/components/date-picker.tsx
+++ b/packages/js/experimental-products-app/src/fields/components/date-picker.tsx
@@ -5,7 +5,6 @@ import { InputControl } from '@wordpress/ui';
 /**
  * Internal dependencies
  */
-
 import type { ProductEntityRecord } from '../types';

 type DateFieldKey = 'date_on_sale_from' | 'date_on_sale_to';
diff --git a/packages/js/experimental-products-app/src/fields/components/list-item/index.tsx b/packages/js/experimental-products-app/src/fields/components/list-item/index.tsx
index 4df12e55d94..ff68535b7ba 100644
--- a/packages/js/experimental-products-app/src/fields/components/list-item/index.tsx
+++ b/packages/js/experimental-products-app/src/fields/components/list-item/index.tsx
@@ -7,11 +7,6 @@ import { __ } from '@wordpress/i18n';
 import { isValidElement } from '@wordpress/element';
 import { Stack } from '@wordpress/ui';

-/**
- * Internal dependencies
- */
-import './style.scss';
-
 type Item = {
 	id: number | string;
 	title: string;
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
new file mode 100644
index 00000000000..3cad58c0fd6
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/date_on_sale_from/field.test.tsx
@@ -0,0 +1,252 @@
+/* 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_from/field.tsx b/packages/js/experimental-products-app/src/fields/date_on_sale_from/field.tsx
new file mode 100644
index 00000000000..9319d5526ba
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/date_on_sale_from/field.tsx
@@ -0,0 +1,89 @@
+/**
+ * External dependencies
+ */
+import { useCallback, useMemo } from '@wordpress/element';
+
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+import {
+	DatePicker,
+	parseDateTimeLocal,
+	formatDateTimeLocal,
+} from '../components/date-picker';
+
+const fieldDefinition = {
+	type: 'datetime',
+	label: __( 'Start sale on', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	isVisible: ( product ) => {
+		return !! product.date_on_sale_to || !! product.date_on_sale_from;
+	},
+	Edit: ( { data, onChange, field } ) => {
+		const today = useMemo( () => {
+			const d = new Date();
+			d.setHours( 0, 0, 0, 0 );
+			return d;
+		}, [] );
+
+		const handleChange = useCallback(
+			( value: { date_on_sale_from?: string | null } ) => {
+				const newStart = value.date_on_sale_from;
+				const currentEnd = data.date_on_sale_to;
+
+				if (
+					typeof newStart !== 'string' ||
+					! newStart ||
+					typeof currentEnd !== 'string' ||
+					! currentEnd
+				) {
+					onChange( value );
+					return;
+				}
+
+				const startDate = parseDateTimeLocal( newStart );
+				const endDate = parseDateTimeLocal( currentEnd );
+
+				if (
+					startDate &&
+					endDate &&
+					startDate.getTime() >= endDate.getTime()
+				) {
+					const newEndDate = new Date( startDate );
+					newEndDate.setDate( newEndDate.getDate() + 1 );
+
+					onChange( {
+						...value,
+						date_on_sale_to: formatDateTimeLocal( newEndDate ),
+					} );
+					return;
+				}
+
+				onChange( value );
+			},
+			[ data.date_on_sale_to, onChange ]
+		);
+
+		return (
+			<DatePicker
+				data={ data }
+				onChange={ handleChange }
+				field={ field }
+				fieldKey="date_on_sale_from"
+				min={ today }
+			/>
+		);
+	},
+};
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
new file mode 100644
index 00000000000..65a6db92a78
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/date_on_sale_to/field.test.tsx
@@ -0,0 +1,177 @@
+/* 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/date_on_sale_to/field.tsx b/packages/js/experimental-products-app/src/fields/date_on_sale_to/field.tsx
new file mode 100644
index 00000000000..9048db2083f
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/date_on_sale_to/field.tsx
@@ -0,0 +1,61 @@
+/**
+ * External dependencies
+ */
+import { useMemo } from '@wordpress/element';
+
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+import { DatePicker, parseDateTimeLocal } from '../components/date-picker';
+
+const fieldDefinition = {
+	type: 'datetime',
+	label: __( 'End sale on', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	isVisible: ( product ) => {
+		return !! product.date_on_sale_to || !! product.date_on_sale_from;
+	},
+	Edit: ( { data, onChange, field } ) => {
+		const dateOnSaleFrom = useMemo(
+			() =>
+				typeof data?.date_on_sale_from === 'string' &&
+				data.date_on_sale_from
+					? parseDateTimeLocal( data.date_on_sale_from )
+					: null,
+			[ data?.date_on_sale_from ]
+		);
+
+		const minDate = useMemo( () => {
+			if ( dateOnSaleFrom ) {
+				const min = new Date( dateOnSaleFrom );
+				min.setMinutes( min.getMinutes() + 1 );
+				return min;
+			}
+			const today = new Date();
+			today.setHours( 0, 0, 0, 0 );
+			return today;
+		}, [ dateOnSaleFrom ] );
+
+		return (
+			<DatePicker
+				data={ data }
+				onChange={ onChange }
+				field={ field }
+				fieldKey="date_on_sale_to"
+				min={ minDate }
+			/>
+		);
+	},
+};
diff --git a/packages/js/experimental-products-app/src/fields/name/field.tsx b/packages/js/experimental-products-app/src/fields/name/field.tsx
new file mode 100644
index 00000000000..5e7644a0f7d
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/name/field.tsx
@@ -0,0 +1,27 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import { type ProductEntityRecord } from '../types';
+
+const fieldDefinition = {
+	type: 'text',
+	label: __( 'Name', 'woocommerce' ),
+	enableSorting: true,
+	filterBy: false,
+	enableHiding: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	header: <span>{ __( 'Product', 'woocommerce' ) }</span>,
+	isValid: {
+		required: true,
+	},
+};
diff --git a/packages/js/experimental-products-app/src/fields/on_sale/field.tsx b/packages/js/experimental-products-app/src/fields/on_sale/field.tsx
new file mode 100644
index 00000000000..ec4b96b8590
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/on_sale/field.tsx
@@ -0,0 +1,35 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+const fieldDefinition = {
+	type: 'boolean',
+	label: __( 'On sale', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	type: 'boolean',
+	Edit: 'toggle',
+	getValue: ( { item } ) => !! item.on_sale || !! item.sale_price,
+	setValue: ( { value } ) =>
+		value
+			? { on_sale: true }
+			: {
+					on_sale: false,
+					sale_price: '',
+					date_on_sale_from: null,
+					date_on_sale_to: null,
+			  },
+};
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
new file mode 100644
index 00000000000..747393eecec
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/on_sale/test/field.test.tsx
@@ -0,0 +1,75 @@
+/**
+ * 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/field.tsx b/packages/js/experimental-products-app/src/fields/price/field.tsx
new file mode 100644
index 00000000000..4798541ed85
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/price/field.tsx
@@ -0,0 +1,190 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useCallback } from '@wordpress/element';
+import type { Field } from '@wordpress/dataviews';
+import { InputControl, Stack, InputLayout } from '@wordpress/ui';
+
+/**
+ * Internal dependencies
+ */
+
+import { formatCurrency, getCurrencyObject } from '../utils/currency';
+
+import type { ProductEntityRecord } from '../types';
+
+import { toNumberOrNaN } from './utils';
+
+type PriceRange = [ number | string, number | string ];
+type PriceFilterData = Omit< ProductEntityRecord, 'price' > & {
+	price: string | PriceRange;
+};
+
+const isLeftPositioned = ( position: string ) =>
+	position === 'left' || position === 'left_space';
+
+const isRightPositioned = ( position: string ) =>
+	position === 'right' || position === 'right_space';
+
+const CurrencyPrefixSlot = () => {
+	const currency = getCurrencyObject();
+
+	if ( ! isLeftPositioned( currency.symbolPosition ) ) {
+		return null;
+	}
+
+	return (
+		<InputLayout.Slot padding="minimal">
+			{ currency.symbol }
+		</InputLayout.Slot>
+	);
+};
+
+const CurrencySuffixSlot = () => {
+	const currency = getCurrencyObject();
+
+	if ( ! isRightPositioned( currency.symbolPosition ) ) {
+		return null;
+	}
+
+	return (
+		<InputLayout.Slot padding="minimal">
+			{ currency.symbol }
+		</InputLayout.Slot>
+	);
+};
+
+const fieldDefinition = {
+	label: __( 'Price', 'woocommerce' ),
+	enableSorting: true,
+	filterBy: {
+		operators: [ 'is', 'between', 'greaterThanOrEqual', 'lessThanOrEqual' ],
+	},
+} satisfies Partial< Field< PriceFilterData > >;
+
+export const fieldExtensions: Partial< Field< PriceFilterData > > = {
+	...fieldDefinition,
+	getValueFormatted: ( { item, field } ) => {
+		const value: unknown = field.getValue( { item } );
+		if ( value === null || value === undefined || value === '' ) {
+			// Return em-dash for empty values to prevent DataViews fallback to "undefined"
+			return '\u2014';
+		}
+		const num = toNumberOrNaN( value );
+		if ( Number.isNaN( num ) ) {
+			// Return em-dash for NaN values to prevent DataViews fallback to "NaN"
+			return '\u2014';
+		}
+		const currency = getCurrencyObject();
+		return formatCurrency( num, currency.code );
+	},
+	render: ( { item } ) => {
+		const rawPrice = item.price;
+		const price = toNumberOrNaN( rawPrice );
+
+		// Show an em dash when no sale price is provided instead of leaving the cell blank.
+		if (
+			rawPrice === undefined ||
+			rawPrice === null ||
+			rawPrice === '' ||
+			Number.isNaN( price )
+		) {
+			return <span>{ '\u2014' }</span>;
+		}
+
+		const currency = getCurrencyObject();
+		const regularPrice = toNumberOrNaN( item.regular_price );
+
+		// Only render the strikethrough when the regular price is a valid number,
+		// since partially saved products may send empty or NaN-like values here.
+		if ( item.on_sale && Number.isFinite( regularPrice ) ) {
+			return (
+				<Stack direction="row">
+					<s>{ formatCurrency( regularPrice, currency.code ) }</s>
+					<span>{ formatCurrency( price, currency.code ) }</span>
+				</Stack>
+			);
+		}
+
+		return <span>{ formatCurrency( price, currency.code ) }</span>;
+	},
+	Edit: ( { data, onChange, hideLabelFromVision, operator, field } ) => {
+		const currency = getCurrencyObject();
+		const step = Math.pow( 10, -currency.precision );
+		const [ minValue = '', maxValue = '' ] = Array.isArray( data.price )
+			? data.price
+			: [];
+		const min = String( minValue );
+		const max = String( maxValue );
+
+		const onChangeMin = useCallback(
+			( newValue: string | undefined ) => {
+				onChange( {
+					price: [ parseFloat( newValue || '' ), max ] as [
+						number | string,
+						number | string
+					],
+				} );
+			},
+			[ onChange, max ]
+		);
+
+		const onChangeMax = useCallback(
+			( newValue: string | undefined ) => {
+				onChange( {
+					price: [ min, parseFloat( newValue || '' ) ] as [
+						number | string,
+						number | string
+					],
+				} );
+			},
+			[ onChange, min ]
+		);
+
+		if ( operator === 'between' ) {
+			return (
+				<Stack direction="row">
+					<InputControl
+						label={ __( 'From', 'woocommerce' ) }
+						type="number"
+						step={ step }
+						value={ min }
+						prefix={ <CurrencyPrefixSlot /> }
+						suffix={ <CurrencySuffixSlot /> }
+						onChange={ ( event ) =>
+							onChangeMin( event.target.value )
+						}
+					/>
+					<InputControl
+						label={ __( 'To', 'woocommerce' ) }
+						type="number"
+						step={ step }
+						value={ max }
+						prefix={ <CurrencyPrefixSlot /> }
+						suffix={ <CurrencySuffixSlot /> }
+						onChange={ ( event ) =>
+							onChangeMax( event.target.value )
+						}
+					/>
+				</Stack>
+			);
+		}
+
+		const singleValue = typeof data.price === 'string' ? data.price : '';
+
+		return (
+			<InputControl
+				label={ hideLabelFromVision ? '' : field.label }
+				type="number"
+				step={ step }
+				value={ singleValue }
+				prefix={ <CurrencyPrefixSlot /> }
+				suffix={ <CurrencySuffixSlot /> }
+				onChange={ ( event ) =>
+					onChange( { price: event.target.value } )
+				}
+			/>
+		);
+	},
+};
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
new file mode 100644
index 00000000000..c788b177949
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/price_summary/field.test.tsx
@@ -0,0 +1,57 @@
+/**
+ * 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/price_summary/field.tsx b/packages/js/experimental-products-app/src/fields/price_summary/field.tsx
new file mode 100644
index 00000000000..5872950d30d
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/price_summary/field.tsx
@@ -0,0 +1,60 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import { getCurrencyObject } from '../utils/currency';
+
+import type { ProductEntityRecord } from '../types';
+
+import { toNumberOrNaN } from '../price/utils';
+
+const formatPrice = ( amount: number, currencyCode: string ) => {
+	const locale = document.documentElement.lang || 'en-US';
+	return new Intl.NumberFormat( locale, {
+		style: 'currency',
+		currency: currencyCode,
+	} ).format( amount );
+};
+
+const fieldDefinition = {
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	render: ( { item } ) => {
+		const salePrice = toNumberOrNaN( item.sale_price );
+		const regularPrice = toNumberOrNaN( item.regular_price );
+		const hasSalePrice =
+			Boolean( item.on_sale ) && Number.isFinite( salePrice );
+
+		const priceToDisplay = hasSalePrice ? salePrice : regularPrice;
+
+		if ( ! Number.isFinite( priceToDisplay ) ) {
+			return null;
+		}
+
+		const currency = getCurrencyObject();
+		const priceText = formatPrice( priceToDisplay, currency.code );
+
+		if ( hasSalePrice ) {
+			return (
+				<span>
+					{ priceText }
+					{ ' · ' }
+					{ __( 'On sale', 'woocommerce' ) }
+				</span>
+			);
+		}
+
+		return <span>{ priceText }</span>;
+	},
+};
diff --git a/packages/js/experimental-products-app/src/fields/product_status/field.tsx b/packages/js/experimental-products-app/src/fields/product_status/field.tsx
new file mode 100644
index 00000000000..19a2afbf09a
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/product_status/field.tsx
@@ -0,0 +1,68 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+import { SelectControl } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+import { ProductStatusBadge } from '../components/product-status-badge';
+
+function isValidStatus( value: string ) {
+	return value === 'draft' || value === 'publish' || value === 'trash';
+}
+
+const fieldDefinition = {
+	type: 'text',
+	label: __( 'Status', 'woocommerce' ),
+	enableSorting: false,
+	filterBy: false,
+	elements: [
+		{ value: 'draft', label: __( 'Draft', 'woocommerce' ) },
+		{ value: 'publish', label: __( 'Active', 'woocommerce' ) },
+		{ value: 'trash', label: __( 'Trash', 'woocommerce' ) },
+	],
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	getValue: ( { item } ) => item.status,
+	render: ( { item }: { item: ProductEntityRecord } ) => (
+		<ProductStatusBadge status={ item.status } />
+	),
+	Edit: ( { data, onChange, field } ) => {
+		const description =
+			data.status === 'publish'
+				? __(
+						'This product is live and visible on your store.',
+						'woocommerce'
+				  )
+				: __(
+						'This product is not visible to customers.',
+						'woocommerce'
+				  );
+
+		return (
+			<SelectControl
+				label={ field.label }
+				help={ description }
+				value={ data.status }
+				options={ field.elements?.filter(
+					( element: { label: string; value: string } ) =>
+						element.value !== 'trash'
+				) }
+				onChange={ ( value ) => {
+					if ( value && isValidStatus( value ) ) {
+						onChange( {
+							status: value,
+						} );
+					}
+				} }
+			/>
+		);
+	},
+};
diff --git a/packages/js/experimental-products-app/src/fields/regular_price/field.tsx b/packages/js/experimental-products-app/src/fields/regular_price/field.tsx
new file mode 100644
index 00000000000..2561525b1b1
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/regular_price/field.tsx
@@ -0,0 +1,31 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import { type ProductEntityRecord } from '../types';
+
+import { CurrencyInput } from '../components/currency-input';
+
+import { validatePrice } from '../price/utils';
+
+const fieldDefinition = {
+	type: 'text',
+	label: __( 'Regular Price', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	isValid: {
+		custom: ( item ) => validatePrice( item.regular_price ),
+	},
+	Edit: CurrencyInput,
+};
diff --git a/packages/js/experimental-products-app/src/fields/sale_price/field.tsx b/packages/js/experimental-products-app/src/fields/sale_price/field.tsx
new file mode 100644
index 00000000000..c13def90fa7
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/sale_price/field.tsx
@@ -0,0 +1,34 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import { type ProductEntityRecord } from '../types';
+
+import { CurrencyInput } from '../components/currency-input';
+
+import { validateSalePrice } from './validation';
+
+const fieldDefinition = {
+	type: 'text',
+	label: __( 'Sale Price', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	isVisible: ( item ) => {
+		return !! item.on_sale || !! item.sale_price;
+	},
+	isValid: {
+		custom: ( item ) => validateSalePrice( item ),
+	},
+	Edit: CurrencyInput,
+};
diff --git a/packages/js/experimental-products-app/src/fields/sale_price/test/field.test.ts b/packages/js/experimental-products-app/src/fields/sale_price/test/field.test.ts
new file mode 100644
index 00000000000..1d14fe35f60
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/sale_price/test/field.test.ts
@@ -0,0 +1,110 @@
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../../types';
+
+import { validatePrice } from '../../price/utils';
+
+import { validateSalePrice } from '../validation';
+
+function makeItem(
+	overrides: Partial< ProductEntityRecord > = {}
+): ProductEntityRecord {
+	return {
+		regular_price: '',
+		sale_price: '',
+		on_sale: false,
+		...overrides,
+	} as ProductEntityRecord;
+}
+
+describe( 'validatePrice', () => {
+	it( 'returns null for empty string', () => {
+		expect( validatePrice( '' ) ).toBeNull();
+	} );
+
+	it( 'returns null for whitespace-only string', () => {
+		expect( validatePrice( '  ' ) ).toBeNull();
+	} );
+
+	it( 'returns null for undefined', () => {
+		expect( validatePrice( undefined ) ).toBeNull();
+	} );
+
+	it( 'returns null for null', () => {
+		expect( validatePrice( null ) ).toBeNull();
+	} );
+
+	it( 'returns null for valid integer', () => {
+		expect( validatePrice( '10' ) ).toBeNull();
+	} );
+
+	it( 'returns null for valid decimal', () => {
+		expect( validatePrice( '9.99' ) ).toBeNull();
+	} );
+
+	it( 'returns null for zero', () => {
+		expect( validatePrice( '0' ) ).toBeNull();
+	} );
+
+	it( 'returns error for non-numeric string', () => {
+		expect( validatePrice( 'abc' ) ).toBe( 'Please enter a valid price.' );
+	} );
+
+	it( 'returns error for negative number', () => {
+		expect( validatePrice( '-5' ) ).toBe( 'Price must not be negative.' );
+	} );
+} );
+
+describe( 'validateSalePrice', () => {
+	it( 'returns null when both prices are empty', () => {
+		expect( validateSalePrice( makeItem() ) ).toBeNull();
+	} );
+
+	it( 'returns null when sale price is empty', () => {
+		const item = makeItem( { regular_price: '10' } );
+		expect( validateSalePrice( item ) ).toBeNull();
+	} );
+
+	it( 'returns null when regular price is empty', () => {
+		const item = makeItem( { sale_price: '5' } );
+		expect( validateSalePrice( item ) ).toBeNull();
+	} );
+
+	it( 'returns null when sale price is lower than regular price', () => {
+		const item = makeItem( { regular_price: '20', sale_price: '15' } );
+		expect( validateSalePrice( item ) ).toBeNull();
+	} );
+
+	it( 'returns error when sale price equals regular price', () => {
+		const item = makeItem( { regular_price: '20', sale_price: '20' } );
+		expect( validateSalePrice( item ) ).toBe(
+			'Sale price must be lower than the regular price.'
+		);
+	} );
+
+	it( 'returns error when sale price exceeds regular price', () => {
+		const item = makeItem( { regular_price: '10', sale_price: '15' } );
+		expect( validateSalePrice( item ) ).toBe(
+			'Sale price must be lower than the regular price.'
+		);
+	} );
+
+	it( 'handles decimal prices correctly', () => {
+		const item = makeItem( {
+			regular_price: '9.99',
+			sale_price: '9.98',
+		} );
+		expect( validateSalePrice( item ) ).toBeNull();
+	} );
+
+	it( 'returns error for equal decimal prices', () => {
+		const item = makeItem( {
+			regular_price: '9.99',
+			sale_price: '9.99',
+		} );
+		expect( validateSalePrice( item ) ).toBe(
+			'Sale price must be lower than the regular price.'
+		);
+	} );
+} );
diff --git a/packages/js/experimental-products-app/src/fields/sale_price/validation.ts b/packages/js/experimental-products-app/src/fields/sale_price/validation.ts
new file mode 100644
index 00000000000..95ec9d659f4
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/sale_price/validation.ts
@@ -0,0 +1,36 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+import { toNumberOrNaN, validatePrice } from '../price/utils';
+
+export function validateSalePrice( item: ProductEntityRecord ): string | null {
+	// First check if sale price is a valid number.
+	const numericError = validatePrice( item.sale_price );
+	if ( numericError ) {
+		return numericError;
+	}
+
+	const regularPrice = toNumberOrNaN( item.regular_price );
+
+	// Only compare when regular price is also numeric.
+	if ( Number.isNaN( regularPrice ) ) {
+		return null;
+	}
+
+	const salePrice = toNumberOrNaN( item.sale_price );
+	if ( salePrice >= regularPrice ) {
+		return __(
+			'Sale price must be lower than the regular price.',
+			'woocommerce'
+		);
+	}
+
+	return null;
+}
diff --git a/packages/js/experimental-products-app/src/fields/schedule_sale/field.tsx b/packages/js/experimental-products-app/src/fields/schedule_sale/field.tsx
new file mode 100644
index 00000000000..b657caad17e
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/schedule_sale/field.tsx
@@ -0,0 +1,95 @@
+/**
+ * External dependencies
+ */
+import {
+	BaseControl,
+	FlexBlock,
+	FormToggle,
+	__experimentalHStack as HStack,
+} from '@wordpress/components';
+
+import { useInstanceId } from '@wordpress/compose';
+
+import { useState } from '@wordpress/element';
+
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+import { getLocalDefaultSaleStart } from '../price/utils';
+
+const fieldDefinition = {
+	type: 'boolean',
+	label: __( 'Schedule sale', 'woocommerce' ),
+	enableSorting: false,
+	enableHiding: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	isVisible: ( item ) => {
+		return !! item.on_sale || !! item.sale_price;
+	},
+	Edit: ( { data, onChange, field } ) => {
+		const toggleId = useInstanceId( FormToggle, 'schedule-sale-toggle' );
+		const [ tempDateOnSaleFrom, setTempDateOnSaleFrom ] = useState(
+			data.date_on_sale_from || ''
+		);
+		const [ tempDateOnSaleTo, setTempDateOnSaleTo ] = useState(
+			data.date_on_sale_to || ''
+		);
+		const checked = !! data.date_on_sale_to || !! data.date_on_sale_from;
+		return (
+			<BaseControl className="components-toggle-control">
+				<HStack justify="flex-start" spacing={ 2 }>
+					<FormToggle
+						id={ toggleId }
+						checked={ checked }
+						onChange={ () => {
+							if ( checked ) {
+								setTempDateOnSaleFrom(
+									data.date_on_sale_from || ''
+								);
+								setTempDateOnSaleTo(
+									data.date_on_sale_to || ''
+								);
+								onChange( {
+									date_on_sale_from: '',
+									date_on_sale_to: '',
+								} );
+							} else {
+								let dateOnSaleFrom =
+									data.date_on_sale_from ||
+									tempDateOnSaleFrom;
+								const dateOnSaleTo =
+									data.date_on_sale_to || tempDateOnSaleTo;
+
+								if ( ! dateOnSaleFrom && ! dateOnSaleTo ) {
+									dateOnSaleFrom = getLocalDefaultSaleStart();
+								}
+
+								onChange( {
+									date_on_sale_from: dateOnSaleFrom,
+									date_on_sale_to: dateOnSaleTo,
+								} );
+							}
+						} }
+					/>
+					<FlexBlock
+						as="label"
+						htmlFor={ toggleId }
+						className="components-toggle-control__label"
+					>
+						{ field.label }
+					</FlexBlock>
+				</HStack>
+			</BaseControl>
+		);
+	},
+};
diff --git a/packages/js/experimental-products-app/src/fields/sku/field.tsx b/packages/js/experimental-products-app/src/fields/sku/field.tsx
new file mode 100644
index 00000000000..beb588d0f0a
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/sku/field.tsx
@@ -0,0 +1,35 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+const fieldDefinition = {
+	type: 'text',
+	label: __( 'SKU', 'woocommerce' ),
+	description: __(
+		'Give your product a unique code to help identify it in orders and fulfilment.',
+		'woocommerce'
+	),
+	enableSorting: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	render: ( { item } ) => {
+		const sku = item.sku;
+
+		if ( sku === undefined || sku === null || sku === '' ) {
+			return <span>{ '\u2014' }</span>;
+		}
+
+		return <span>{ sku }</span>;
+	},
+};
diff --git a/packages/js/experimental-products-app/tsconfig.json b/packages/js/experimental-products-app/tsconfig.json
index 205e7551412..376e1607afd 100644
--- a/packages/js/experimental-products-app/tsconfig.json
+++ b/packages/js/experimental-products-app/tsconfig.json
@@ -8,11 +8,15 @@
 		"declarationMap": true,
 		"declarationDir": "./build-types",
 		"resolveJsonModule": true,
-		"typeRoots": [ "./typings", "./node_modules/@types" ],
 		"jsx": "react-jsx",
 		"jsxFactory": null,
 		"jsxFragmentFactory": null
 	},
 	"include": [ "src/**/*" ],
-	"exclude": [ "**/test/**", "**/stories/**" ]
+	"exclude": [
+		"**/test/**",
+		"**/stories/**",
+		"**/*.test.ts",
+		"**/*.test.tsx"
+	]
 }