Commit 446c9cde354 for woocommerce

commit 446c9cde35456d8ac393c6fcbcf4fe761e822d8c
Author: Luigi Teschio <gigitux@gmail.com>
Date:   Mon Apr 27 16:34:26 2026 +0200

    [DataViews - All Products] Refactor Product Actions (#64419)

    * Refactor product actions and improve layout handling in the experimental products app

    * lint code

    * Update packages/js/experimental-products-app/typings/index.d.ts

    Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

    * fix hook

    * lint code

    ---------

    Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

diff --git a/packages/js/experimental-products-app/src/dataviews-actions/index.tsx b/packages/js/experimental-products-app/src/dataviews-actions/index.tsx
index 5dd17ccc0e2..0eb95004f04 100644
--- a/packages/js/experimental-products-app/src/dataviews-actions/index.tsx
+++ b/packages/js/experimental-products-app/src/dataviews-actions/index.tsx
@@ -1,45 +1,64 @@
 /**
  * External dependencies
  */
+import { edit, external } from '@wordpress/icons';
+import { __, _x } from '@wordpress/i18n';
+import { addQueryArgs } from '@wordpress/url';
+import { getAdminLink } from '@woocommerce/settings';
+import type { Action } from '@wordpress/dataviews';
 import { useMemo } from '@wordpress/element';
-import { edit } from '@wordpress/icons';
-import { privateApis as routerPrivateApis } from '@wordpress/router';
-import { __ } from '@wordpress/i18n';
-import { Product } from '@woocommerce/data';

 /**
  * Internal dependencies
  */
-import { unlock } from '../lock-unlock';
-
-const { useHistory, useLocation } = unlock( routerPrivateApis );
-
-export const useEditProductAction = ( { postType }: { postType: string } ) => {
-	const history = useHistory();
-	const location = useLocation();
-	return useMemo(
-		() => ( {
-			id: 'edit-product',
-			label: __( 'Edit', 'woocommerce' ),
-			isPrimary: true,
-			icon: edit,
-			supportsBulk: true,
-			isEligible( product: Product ) {
-				if ( product.status === 'trash' ) {
-					return false;
-				}
-				return true;
-			},
-			callback( items: Product[] ) {
-				const product = items[ 0 ];
-				history.push( {
-					...location.params,
-					postId: product.id,
-					postType,
-					quickEdit: true,
-				} );
-			},
-		} ),
-		[ history, location.params ]
-	);
-};
+import type { ProductEntityRecord } from '../fields/types';
+
+export const editAction = (): Action< ProductEntityRecord > => ( {
+	id: 'edit-product',
+	label: __( 'Edit', 'woocommerce' ),
+	isPrimary: true,
+	icon: edit,
+	isEligible( product ) {
+		return product.status !== 'trash';
+	},
+	callback( items, { onActionPerformed } ) {
+		const product = items[ 0 ];
+
+		if ( product ) {
+			window.location.href = getAdminLink(
+				addQueryArgs( 'post.php', {
+					post: product.id,
+					action: 'edit',
+				} )
+			);
+		}
+
+		if ( onActionPerformed ) {
+			onActionPerformed( items );
+		}
+	},
+} );
+
+export const viewAction = (): Action< ProductEntityRecord > => ( {
+	id: 'view-product',
+	label: _x( 'View', 'verb', 'woocommerce' ),
+	isPrimary: true,
+	icon: external,
+	isEligible( product ) {
+		return product.status !== 'trash' && !! product.permalink;
+	},
+	callback( items, { onActionPerformed } ) {
+		const product = items[ 0 ];
+
+		if ( product?.permalink ) {
+			window.open( product.permalink, '_blank' );
+		}
+
+		if ( onActionPerformed ) {
+			onActionPerformed( items );
+		}
+	},
+} );
+
+export const useProductActions = () =>
+	useMemo( () => [ editAction(), viewAction() ], [] );
diff --git a/packages/js/experimental-products-app/src/fields/utils/currency.ts b/packages/js/experimental-products-app/src/fields/utils/currency.ts
index 612ad8991b8..97438f35890 100644
--- a/packages/js/experimental-products-app/src/fields/utils/currency.ts
+++ b/packages/js/experimental-products-app/src/fields/utils/currency.ts
@@ -1,10 +1,7 @@
 /**
  * External dependencies
  */
-import {
-	CURRENCY,
-	// @ts-expect-error - The CURRENCY object doesn't have types yet.
-} from '@woocommerce/settings';
+import { CURRENCY } from '@woocommerce/settings';

 type CurrencyObject = {
 	code: string;
diff --git a/packages/js/experimental-products-app/src/product-list/index.tsx b/packages/js/experimental-products-app/src/product-list/index.tsx
index 35bcc12e916..ee6e2690944 100644
--- a/packages/js/experimental-products-app/src/product-list/index.tsx
+++ b/packages/js/experimental-products-app/src/product-list/index.tsx
@@ -2,22 +2,18 @@
  * External dependencies
  */
 import { DataViews, View } from '@wordpress/dataviews';
-import {
-	useState,
-	useMemo,
-	useCallback,
-	useEffect,
-	Fragment,
-} from '@wordpress/element';
-import { ProductQuery, productsStore } from '@woocommerce/data';
+import { useState, useMemo, useCallback, useEffect } from '@wordpress/element';
+import { ProductQuery } from '@woocommerce/data';
 import { privateApis as routerPrivateApis } from '@wordpress/router';
-import { store as coreStore } from '@wordpress/core-data';
+import { store as coreStore, useEntityRecords } from '@wordpress/core-data';
 import { __ } from '@wordpress/i18n';
 import { useSelect } from '@wordpress/data';
 import clsx from 'clsx';
-import { Button } from '@wordpress/components';
+import { Button, Stack } from '@wordpress/ui';
 import { privateApis as editorPrivateApis } from '@wordpress/editor';
 import { Page } from '@wordpress/admin-ui';
+import { addQueryArgs } from '@wordpress/url';
+import { getAdminLink } from '@woocommerce/settings';

 /**
  * Internal dependencies
@@ -29,7 +25,7 @@ import {
 	DEFAULT_PRODUCT_TABLE_LAYOUT,
 	DEFAULT_PRODUCT_TABLE_VIEW,
 } from './layouts';
-import { useEditProductAction } from '../dataviews-actions';
+import { useProductActions } from '../dataviews-actions';

 const { usePostActions } = unlock( editorPrivateApis );
 const { useHistory, useLocation } = unlock( routerPrivateApis );
@@ -79,11 +75,7 @@ function getItemId( item: ProductEntityRecord ) {
 	return item.id.toString();
 }

-export default function ProductList( {
-	subTitle,
-	className,
-	hideTitleFromUI = false,
-}: ProductListProps ) {
+export default function ProductList( { className }: ProductListProps ) {
 	const history = useHistory();
 	const location = useLocation();
 	const {
@@ -133,19 +125,14 @@ export default function ProductList( {
 		[ history, location.params ]
 	);

-	// TODO: Use the Woo data store to get all the products, as this doesn't contain all the product data.
-	const { records, totalCount, isLoading } = useSelect(
-		( select ) => {
-			// @ts-expect-error - The productsStore doesn't have types yet.
-			const { getProducts, getProductsTotalCount, isResolving } =
-				select( productsStore );
-			return {
-				records: getProducts( queryParams ) as ProductEntityRecord[],
-				totalCount: getProductsTotalCount( queryParams ),
-				isLoading: isResolving( 'getProducts', [ queryParams ] ),
-			};
-		},
-		[ queryParams ]
+	const {
+		records,
+		totalItems: totalCount,
+		isResolving: isLoading,
+	} = useEntityRecords< ProductEntityRecord >(
+		'root',
+		'product',
+		queryParams
 	);

 	const paginationInfo = useMemo(
@@ -158,14 +145,10 @@ export default function ProductList( {
 		[ totalCount, view.perPage ]
 	);

-	const { labels, canCreateRecord } = useSelect(
+	const { canCreateRecord } = useSelect(
 		( select ) => {
-			const { getPostType, canUser } = select( coreStore );
-			const postTypeData:
-				| { labels: Record< string, string > }
-				| undefined = getPostType( postType );
+			const { canUser } = select( coreStore );
 			return {
-				labels: postTypeData?.labels,
 				canCreateRecord: canUser( 'create', {
 					kind: 'postType',
 					name: postType,
@@ -179,36 +162,74 @@ export default function ProductList( {
 		postType,
 		context: 'list',
 	} );
-	const editAction = useEditProductAction( { postType } );
+	const productActions = useProductActions();
 	const actions = useMemo(
-		() => [ editAction, ...postTypeActions ],
-		[ postTypeActions, editAction ]
+		() => [
+			...productActions,
+			...postTypeActions.filter(
+				( { id }: { id: string } ) => id !== 'view-post'
+			),
+		],
+		[ postTypeActions, productActions ]
 	);

 	const classes = clsx( 'edit-site-page', className );

-	const pageActions = ! hideTitleFromUI && (
-		<Fragment>
-			{ labels?.add_new_item && canCreateRecord && (
-				<Button
-					variant="primary"
-					disabled={ true }
-					__next40pxDefaultSize
-				>
-					{ labels.add_new_item }
-				</Button>
-			) }
-		</Fragment>
+	const pageActions = (
+		<Stack gap="lg">
+			<Button
+				size="compact"
+				variant="outline"
+				onClick={ () =>
+					( window.location.href = getAdminLink(
+						addQueryArgs( 'edit.php', {
+							post_type: 'product',
+							page: 'product_exporter',
+						} )
+					) )
+				}
+			>
+				{ __( 'Export', 'woocommerce' ) }
+			</Button>
+			<Button
+				size="compact"
+				onClick={ () =>
+					( window.location.href = getAdminLink(
+						addQueryArgs( 'edit.php', {
+							post_type: 'product',
+							page: 'product_importer',
+						} )
+					) )
+				}
+				variant="outline"
+			>
+				{ __( 'Import', 'woocommerce' ) }
+			</Button>
+			<Button
+				size="compact"
+				disabled={ canCreateRecord === false }
+				onClick={ () =>
+					( window.location.href = getAdminLink(
+						addQueryArgs( 'post-new.php', {
+							post_type: 'product',
+						} )
+					) )
+				}
+			>
+				{ __( 'Add new product', 'woocommerce' ) }
+			</Button>
+		</Stack>
 	);

 	return (
 		<Page
 			className={ classes }
 			ariaLabel={ __( 'Products', 'woocommerce' ) }
-			title={
-				hideTitleFromUI ? undefined : __( 'Products', 'woocommerce' )
-			}
-			subTitle={ hideTitleFromUI ? undefined : subTitle }
+			subTitle={ __(
+				'Add, edit, and manage the products you sell in your store',
+				'woocommerce'
+			) }
+			title={ __( 'Products', 'woocommerce' ) }
 			actions={ pageActions }
 		>
 			<DataViews
diff --git a/packages/js/experimental-products-app/src/style.scss b/packages/js/experimental-products-app/src/style.scss
index bafa651beb2..7b59e23fc22 100644
--- a/packages/js/experimental-products-app/src/style.scss
+++ b/packages/js/experimental-products-app/src/style.scss
@@ -1,5 +1,7 @@
+@import "../node_modules/@wordpress/theme/src/prebuilt/css/design-tokens.css";
 @import "@wordpress/components/build-style/style.css";
 @import "@wordpress/dataviews/build-style/style.css";
+@import "@wordpress/admin-ui/build-style/style.css";

 .product_page_woocommerce-products-dashboard {
 	background-color: unset;
diff --git a/packages/js/experimental-products-app/tsconfig.json b/packages/js/experimental-products-app/tsconfig.json
index 376e1607afd..7611087fbe7 100644
--- a/packages/js/experimental-products-app/tsconfig.json
+++ b/packages/js/experimental-products-app/tsconfig.json
@@ -10,9 +10,16 @@
 		"resolveJsonModule": true,
 		"jsx": "react-jsx",
 		"jsxFactory": null,
-		"jsxFragmentFactory": null
+		"jsxFragmentFactory": null,
+		"typeRoots": [
+			"./typings",
+			"./node_modules/@types"
+		]
 	},
-	"include": [ "src/**/*" ],
+	"include": [
+		"typings/**/*",
+		"src/**/*"
+	],
 	"exclude": [
 		"**/test/**",
 		"**/stories/**",
diff --git a/packages/js/experimental-products-app/typings/index.d.ts b/packages/js/experimental-products-app/typings/index.d.ts
new file mode 100644
index 00000000000..6c9a0270236
--- /dev/null
+++ b/packages/js/experimental-products-app/typings/index.d.ts
@@ -0,0 +1,16 @@
+declare module '@woocommerce/settings' {
+	export declare const CURRENCY: {
+		code: string;
+		precision: number;
+		symbol: string;
+		symbolPosition: string;
+		decimalSeparator?: string;
+		thousandSeparator?: string;
+	};
+	export declare function getAdminLink( path: string ): string;
+	export function getSetting< T >(
+		name: string,
+		fallback?: unknown,
+		filter?: ( val: unknown, fb: unknown ) => unknown
+	): T;
+}