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 & 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;