Commit 05f9b9785d4 for woocommerce

commit 05f9b9785d4620603e7770b60f5a09c2b4351df2
Author: verofasulo <98944206+verofasulo@users.noreply.github.com>
Date:   Mon May 11 12:36:58 2026 +0200

    Add Tags, Brands, Date, and GTIN columns to the products list (#64713)

    * Add Tags, Brands, Date, and GTIN columns to the products list

    Register four new fields in the experimental products list so they
    appear as toggleable items in the Appearance > Properties picker:

    - Tags: drop enableHiding=false so it surfaces in the picker.
    - Brands: read-only column listing brand names from the brands taxonomy.
    - Date: column rendering the product's date_created via Intl.DateTimeFormat.
    - GTIN, UPC, EAN, ISBN: column rendering the product global_unique_id.

    All four are off by default; users opt in via the Properties menu.

    * fix tags

    ---------

    Co-authored-by: Luigi Teschio <gigitux@gmail.com>

diff --git a/packages/js/experimental-products-app/changelog/add-properties-tags-brands-date-gtin b/packages/js/experimental-products-app/changelog/add-properties-tags-brands-date-gtin
new file mode 100644
index 00000000000..9b70fdcd2cb
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/add-properties-tags-brands-date-gtin
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add Tags, Brands, Date, and GTIN/UPC/EAN/ISBN as toggleable columns in the experimental products list (hidden by default).
diff --git a/packages/js/experimental-products-app/src/fields/brands/field.tsx b/packages/js/experimental-products-app/src/fields/brands/field.tsx
new file mode 100644
index 00000000000..62af1c3efbc
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/brands/field.tsx
@@ -0,0 +1,36 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { decodeEntities } from '@wordpress/html-entities';
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+const fieldDefinition = {
+	type: 'array',
+	label: __( 'Brands', 'woocommerce' ),
+	enableSorting: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	getValue: ( { item } ) => {
+		return ( item.brands ?? [] ).map( ( { id } ) => id.toString() );
+	},
+	render: ( { item } ) => {
+		const names = ( item.brands ?? [] )
+			.map( ( { name } ) => decodeEntities( name ?? '' ) )
+			.filter( Boolean );
+
+		if ( names.length === 0 ) {
+			return <span>{ '—' }</span>;
+		}
+
+		return <span>{ names.join( ', ' ) }</span>;
+	},
+};
diff --git a/packages/js/experimental-products-app/src/fields/date/field.tsx b/packages/js/experimental-products-app/src/fields/date/field.tsx
new file mode 100644
index 00000000000..6028f7750f2
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/date/field.tsx
@@ -0,0 +1,41 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+const fieldDefinition = {
+	type: 'text',
+	label: __( 'Date', 'woocommerce' ),
+	enableSorting: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+const dateFormatter = new Intl.DateTimeFormat( undefined, {
+	dateStyle: 'medium',
+} );
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	getValue: ( { item } ) => item.date_created ?? '',
+	render: ( { item } ) => {
+		const value = item.date_created;
+
+		if ( ! value ) {
+			return <span>{ '—' }</span>;
+		}
+
+		const parsed = new Date( value );
+
+		if ( Number.isNaN( parsed.getTime() ) ) {
+			return <span>{ value }</span>;
+		}
+
+		return <span>{ dateFormatter.format( parsed ) }</span>;
+	},
+};
diff --git a/packages/js/experimental-products-app/src/fields/global_unique_id/field.tsx b/packages/js/experimental-products-app/src/fields/global_unique_id/field.tsx
new file mode 100644
index 00000000000..1aea8192671
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/global_unique_id/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';
+
+const fieldDefinition = {
+	type: 'text',
+	label: __( 'GTIN, UPC, EAN, ISBN', 'woocommerce' ),
+	enableSorting: false,
+	filterBy: false,
+} satisfies Partial< Field< ProductEntityRecord > >;
+
+export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
+	...fieldDefinition,
+	getValue: ( { item } ) => item.global_unique_id ?? '',
+	render: ( { item } ) => {
+		const value = item.global_unique_id;
+
+		if ( ! value ) {
+			return <span>{ '—' }</span>;
+		}
+
+		return <span>{ value }</span>;
+	},
+};
diff --git a/packages/js/experimental-products-app/src/fields/registry.tsx b/packages/js/experimental-products-app/src/fields/registry.tsx
index a63ace4f81a..21134f6d03a 100644
--- a/packages/js/experimental-products-app/src/fields/registry.tsx
+++ b/packages/js/experimental-products-app/src/fields/registry.tsx
@@ -6,13 +6,16 @@ import type { Field } from '@wordpress/dataviews';
 /**
  * Internal dependencies
  */
+import { fieldExtensions as brandsFieldExtensions } from './brands/field';
 import { fieldExtensions as buttonTextFieldExtensions } from './button_text/field';
 import { fieldExtensions as catalogVisibilityFieldExtensions } from './catalog_visibility/field';
 import { fieldExtensions as categoriesFieldExtensions } from './categories/field';
 import { fieldExtensions as crossSellIdsFieldExtensions } from './cross_sell_ids/field';
+import { fieldExtensions as dateFieldExtensions } from './date/field';
 import { fieldExtensions as dateOnSaleFromFieldExtensions } from './date_on_sale_from/field';
 import { fieldExtensions as dateOnSaleToFieldExtensions } from './date_on_sale_to/field';
 import { fieldExtensions as descriptionFieldExtensions } from './description/field';
+import { fieldExtensions as globalUniqueIdFieldExtensions } from './global_unique_id/field';
 import { fieldExtensions as downloadableFieldExtensions } from './downloadable/field';
 import { fieldExtensions as downloadableCountFieldExtensions } from './downloadable_count/field';
 import { fieldExtensions as externalUrlFieldExtensions } from './external_url/field';
@@ -73,6 +76,9 @@ export const PRODUCT_FIELD_IDS = [
 	'inventory_summary',
 	'categories',
 	'tags',
+	'brands',
+	'date',
+	'global_unique_id',
 	'organization_summary',
 	'type',
 	'featured',
@@ -121,6 +127,9 @@ const PRODUCT_FIELD_EXTENSIONS: Record<
 	inventory_summary: inventorySummaryFieldExtensions,
 	categories: categoriesFieldExtensions,
 	tags: tagsFieldExtensions,
+	brands: brandsFieldExtensions,
+	date: dateFieldExtensions,
+	global_unique_id: globalUniqueIdFieldExtensions,
 	organization_summary: organizationSummaryFieldExtensions,
 	type: typeFieldExtensions,
 	featured: featuredFieldExtensions,
diff --git a/packages/js/experimental-products-app/src/fields/tags/field.test.tsx b/packages/js/experimental-products-app/src/fields/tags/field.test.tsx
new file mode 100644
index 00000000000..46f49d10b4d
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/tags/field.test.tsx
@@ -0,0 +1,37 @@
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord } from '../types';
+
+import { fieldExtensions } from './field';
+
+const renderTags = ( item: Partial< ProductEntityRecord > ) => {
+	if ( ! fieldExtensions.render ) {
+		throw new Error( 'tags render not implemented' );
+	}
+
+	const render = fieldExtensions.render as ( props: {
+		item: ProductEntityRecord;
+	} ) => unknown;
+
+	return render( {
+		item: item as ProductEntityRecord,
+	} );
+};
+
+describe( 'tags field', () => {
+	it( 'renders tag names instead of tag IDs', () => {
+		expect(
+			renderTags( {
+				tags: [
+					{ id: 12, name: 'Summer' },
+					{ id: 34, name: 'Sale &amp; clearance' },
+				],
+			} )
+		).toBe( 'Summer, Sale & clearance' );
+	} );
+
+	it( 'renders nothing when there are no tags', () => {
+		expect( renderTags( { tags: [] } ) ).toBe( '' );
+	} );
+} );
diff --git a/packages/js/experimental-products-app/src/fields/tags/field.tsx b/packages/js/experimental-products-app/src/fields/tags/field.tsx
index 2d4f9988c5c..56661830725 100644
--- a/packages/js/experimental-products-app/src/fields/tags/field.tsx
+++ b/packages/js/experimental-products-app/src/fields/tags/field.tsx
@@ -2,6 +2,7 @@
  * External dependencies
  */
 import { __ } from '@wordpress/i18n';
+import { decodeEntities } from '@wordpress/html-entities';

 import type { DataFormControlProps, Field } from '@wordpress/dataviews';

@@ -20,7 +21,6 @@ const fieldDefinition = {
 		'woocommerce'
 	),
 	enableSorting: false,
-	enableHiding: false,
 	filterBy: false,
 } satisfies Partial< Field< ProductEntityRecord > >;

@@ -36,6 +36,11 @@ export const fieldExtensions: Partial< Field< ProductEntityRecord > > = {
 			} ) ),
 		};
 	},
+	render: ( { item } ) => {
+		return ( item.tags ?? [] )
+			.map( ( { name } ) => decodeEntities( name ?? '' ) )
+			.join( ', ' );
+	},
 	Edit: ( props: DataFormControlProps< ProductEntityRecord > ) => (
 		<TaxonomyEdit
 			{ ...props }
diff --git a/packages/js/experimental-products-app/src/fields/types.ts b/packages/js/experimental-products-app/src/fields/types.ts
index 7544d4dcc08..7dbdad1ad5b 100644
--- a/packages/js/experimental-products-app/src/fields/types.ts
+++ b/packages/js/experimental-products-app/src/fields/types.ts
@@ -19,6 +19,12 @@ export type ProductEntityRecord = Omit< Product, 'categories' | 'tags' > & {
 		id: number;
 		name?: string;
 	} >;
+	brands?: Array< {
+		id: number;
+		name?: string;
+		slug?: string;
+	} >;
+	global_unique_id?: string;
 	cross_sell_ids?: number[];
 	upsell_ids?: number[];
 	date_on_sale_from?: string | null;