Commit 5d7f4cddd59 for woocommerce
commit 5d7f4cddd5958a71bc0620e9acc9b71f3b5c2c88
Author: Luigi Teschio <gigitux@gmail.com>
Date: Tue Apr 21 15:28:40 2026 +0200
[DataViews - All Products] Add shared field controls (#64268)
* Add shared controls for products app fields
* 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 lint
---------
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-shared-foundation b/packages/js/experimental-products-app/changelog/add-products-fields-shared-foundation
new file mode 100644
index 00000000000..0ebf062f5b0
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/add-products-fields-shared-foundation
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add shared products app field controls and utilities
diff --git a/packages/js/experimental-products-app/src/app.tsx b/packages/js/experimental-products-app/src/app.tsx
index 5e989ce2de8..32f9b669e20 100644
--- a/packages/js/experimental-products-app/src/app.tsx
+++ b/packages/js/experimental-products-app/src/app.tsx
@@ -1,7 +1,6 @@
/**
* External dependencies
*/
-import { createElement } from '@wordpress/element';
import { privateApis as routerPrivateApis } from '@wordpress/router';
import { privateApis as themeProviderPrivateApis } from '@wordpress/theme';
import { UnsavedChangesWarning } from '@wordpress/editor';
diff --git a/packages/js/experimental-products-app/src/fields/components/currency-input.tsx b/packages/js/experimental-products-app/src/fields/components/currency-input.tsx
new file mode 100644
index 00000000000..dbf2abfb7c0
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/currency-input.tsx
@@ -0,0 +1,79 @@
+/**
+ * External dependencies
+ */
+import {
+ privateApis,
+ __experimentalInputControlPrefixWrapper as InputControlPrefixWrapper,
+ __experimentalInputControlSuffixWrapper as InputControlSuffixWrapper,
+} from '@wordpress/components';
+
+import type { DataFormControlProps } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../../lock-unlock';
+
+import type { ProductEntityRecord } from '../types';
+
+import { getCurrencyObject } from '../utils/currency';
+
+const { ValidatedInputControl } = unlock( privateApis );
+
+const { symbol, symbolPosition, precision } = getCurrencyObject();
+const isCurrencyLeft =
+ symbolPosition === 'left' || symbolPosition === 'left_space';
+// Step matches store decimal precision (e.g. precision=2 → step=0.01).
+const step = Math.pow( 10, -precision );
+
+type PriceField = 'regular_price' | 'sale_price';
+
+/**
+ * Shared Edit component for currency price fields (regular_price, sale_price).
+ * Renders a number input with min=0 and currency prefix/suffix.
+ *
+ * @param root0 Props from DataForm.
+ * @param root0.data Current product entity record.
+ * @param root0.field Normalized field definition.
+ * @param root0.onChange Callback to update entity values.
+ * @param root0.validity Per-rule validation state from useFormValidity.
+ */
+export function CurrencyInput( {
+ data,
+ field,
+ onChange,
+ validity,
+}: DataFormControlProps< ProductEntityRecord > ) {
+ const fieldId = field.id as PriceField;
+ const value = data[ fieldId ] ?? '';
+
+ return (
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- ValidatedInputControl is a private API
+ <ValidatedInputControl
+ id={ `currency-input-${ fieldId }` }
+ label={ field.label }
+ value={ value }
+ onChange={ ( newValue: string ) => {
+ onChange( { [ fieldId ]: newValue } );
+ } }
+ type="number"
+ min={ 0 }
+ step={ step }
+ customValidity={ validity?.custom }
+ prefix={
+ isCurrencyLeft ? (
+ <InputControlPrefixWrapper>
+ { symbol }
+ </InputControlPrefixWrapper>
+ ) : undefined
+ }
+ suffix={
+ ! isCurrencyLeft ? (
+ <InputControlSuffixWrapper>
+ { symbol }
+ </InputControlSuffixWrapper>
+ ) : undefined
+ }
+ />
+ );
+}
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
new file mode 100644
index 00000000000..34bbfe5eea7
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/date-picker.tsx
@@ -0,0 +1,68 @@
+/**
+ * External dependencies
+ */
+import { InputControl } from '@wordpress/ui';
+/**
+ * Internal dependencies
+ */
+
+import type { ProductEntityRecord } from '../types';
+
+type DateFieldKey = 'date_on_sale_from' | 'date_on_sale_to';
+
+type DatePickerProps = {
+ data: ProductEntityRecord;
+ onChange: ( value: Partial< ProductEntityRecord > ) => void;
+ field: {
+ id?: string;
+ label?: string;
+ };
+ fieldKey: DateFieldKey;
+ min?: Date;
+};
+
+export function parseDateTimeLocal( value: string ) {
+ if ( ! value ) {
+ return null;
+ }
+
+ const parsedDate = new Date( value );
+
+ if ( Number.isNaN( parsedDate.getTime() ) ) {
+ return null;
+ }
+
+ return parsedDate;
+}
+
+export function formatDateTimeLocal( date: Date ) {
+ const year = date.getFullYear();
+ const month = String( date.getMonth() + 1 ).padStart( 2, '0' );
+ const day = String( date.getDate() ).padStart( 2, '0' );
+ const hours = String( date.getHours() ).padStart( 2, '0' );
+ const minutes = String( date.getMinutes() ).padStart( 2, '0' );
+
+ return `${ year }-${ month }-${ day }T${ hours }:${ minutes }`;
+}
+
+export function DatePicker( {
+ data,
+ onChange,
+ field,
+ fieldKey,
+ min,
+}: DatePickerProps ) {
+ return (
+ <InputControl
+ label={ field.label }
+ type="datetime-local"
+ value={ data[ fieldKey ] || '' }
+ min={ min ? formatDateTimeLocal( min ) : undefined }
+ onChange={ ( event ) =>
+ onChange( {
+ [ fieldKey ]: event.target.value || null,
+ } as Partial< ProductEntityRecord > )
+ }
+ />
+ );
+}
diff --git a/packages/js/experimental-products-app/src/fields/components/dimension.tsx b/packages/js/experimental-products-app/src/fields/components/dimension.tsx
new file mode 100644
index 00000000000..4abcb6249db
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/dimension.tsx
@@ -0,0 +1,61 @@
+/**
+ * External dependencies
+ */
+import { useEntityRecord } from '@wordpress/core-data';
+
+import type { Field } from '@wordpress/dataviews';
+import { InputControl, InputLayout } from '@wordpress/ui';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductEntityRecord, SettingsEntityRecord } from '../types';
+
+export type DimensionKey = 'height' | 'width' | 'length';
+
+export const createDimensionField = (
+ key: DimensionKey
+): Partial< Field< ProductEntityRecord > > => {
+ return {
+ Edit: ( { data, onChange, field } ) => {
+ const {
+ record: storeProductsSettings,
+ isResolving: storeProductsSettingsResolving,
+ } = useEntityRecord< SettingsEntityRecord >(
+ 'root',
+ 'settings',
+ 'products'
+ );
+
+ if ( storeProductsSettingsResolving ) {
+ return null;
+ }
+
+ const dimensionUnit =
+ storeProductsSettings?.values?.woocommerce_dimension_unit;
+
+ return (
+ <InputControl
+ label={ field.label }
+ value={ data.dimensions[ key ] }
+ onChange={ ( event ) => {
+ onChange( {
+ dimensions: {
+ ...data.dimensions,
+ [ key ]: event.target.value,
+ },
+ } );
+ } }
+ type="number"
+ min={ 0 }
+ step="any"
+ suffix={
+ <InputLayout.Slot padding="minimal">
+ { dimensionUnit }
+ </InputLayout.Slot>
+ }
+ />
+ );
+ },
+ };
+};
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
new file mode 100644
index 00000000000..4df12e55d94
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/list-item/index.tsx
@@ -0,0 +1,84 @@
+/**
+ * External dependencies
+ */
+import { Button } from '@wordpress/components';
+import { close } from '@wordpress/icons';
+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;
+ thumbnail?: string | JSX.Element;
+ meta?: string;
+ altText?: string;
+};
+
+interface ListItemProps {
+ items: Item[];
+ onRemove?: ( item: Item ) => void;
+ showRemoveButton?: boolean;
+}
+
+export const ListItem = ( {
+ items,
+ onRemove,
+ showRemoveButton = true,
+}: ListItemProps ) => {
+ return (
+ <Stack direction="column">
+ { items.map( ( item ) => (
+ <Stack
+ key={ item.id }
+ direction="row"
+ align="center"
+ justify="space-between"
+ className="woocommerce-list-item"
+ >
+ { item.thumbnail && (
+ <div className="woocommerce-list-item__thumbnail">
+ { typeof item.thumbnail === 'string' && (
+ <img
+ src={ item.thumbnail }
+ alt={ item.altText ?? '' }
+ className="woocommerce-list-item__thumbnail-image"
+ />
+ ) }
+ { isValidElement( item.thumbnail ) &&
+ item.thumbnail }
+ </div>
+ ) }
+ <Stack
+ className="woocommerce-list-item__info"
+ direction="column"
+ >
+ <div className="woocommerce-list-item__title">
+ { item.title }
+ </div>
+ { item.meta && (
+ <div className="woocommerce-list-item__meta">
+ { item.meta }
+ </div>
+ ) }
+ </Stack>
+ { showRemoveButton && onRemove && (
+ <Button
+ icon={ close }
+ variant="tertiary"
+ iconSize={ 16 }
+ onClick={ () => onRemove( item ) }
+ aria-label={ __( 'Remove item', 'woocommerce' ) }
+ className="woocommerce-list-item__remove-button"
+ />
+ ) }
+ </Stack>
+ ) ) }
+ </Stack>
+ );
+};
diff --git a/packages/js/experimental-products-app/src/fields/components/list-item/style.scss b/packages/js/experimental-products-app/src/fields/components/list-item/style.scss
new file mode 100644
index 00000000000..888194ea7fc
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/list-item/style.scss
@@ -0,0 +1,46 @@
+.woocommerce-list-item {
+ padding: 12px 0;
+ border-bottom: 1px solid #e0e0e0;
+ width: 100%;
+
+ &:last-of-type {
+ border-bottom: none;
+ }
+
+ &__thumbnail {
+ width: 48px;
+ height: 48px;
+ border-radius: 4px;
+ flex-shrink: 0;
+
+ &-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ }
+
+ &__info {
+ flex: 1;
+ min-width: 0;
+ }
+
+ &__title {
+ font-size: 14px;
+ font-weight: 500;
+ color: #1e1e1e;
+ }
+
+ &__meta {
+ font-size: 12px;
+ color: #757575;
+ font-weight: 500;
+ }
+
+ &__remove-button {
+ color: #757575;
+ padding: 8px;
+ min-width: auto;
+ }
+}
diff --git a/packages/js/experimental-products-app/src/fields/components/product-selector.tsx b/packages/js/experimental-products-app/src/fields/components/product-selector.tsx
new file mode 100644
index 00000000000..64de2dfd6cd
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/product-selector.tsx
@@ -0,0 +1,195 @@
+/**
+ * External dependencies
+ */
+import { BaseControl, FormTokenField, Spinner } from '@wordpress/components';
+import { store as coreStore } from '@wordpress/core-data';
+import { resolveSelect } from '@wordpress/data';
+import { useEffect, useMemo, useState } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import type { Product } from '@woocommerce/data';
+
+/**
+ * Internal dependencies
+ */
+import type { ProductStatus } from '../types';
+
+type ProductSelectorProps = {
+ label?: string;
+ description?: string;
+ selectedProductIds: number[];
+ onSelectedProductIdsChange: ( ids: number[] ) => void;
+ excludeProductIds?: number[];
+ includeProductStatuses?: ProductStatus[];
+};
+
+const SEARCH_DELAY_MS = 250;
+
+function getProductToken( product: Pick< Product, 'id' | 'name' > ) {
+ return `${ product.name || __( '(Untitled product)', 'woocommerce' ) } (#${
+ product.id
+ })`;
+}
+
+export function ProductSelector( {
+ label,
+ description,
+ selectedProductIds,
+ onSelectedProductIdsChange,
+ excludeProductIds = [],
+ includeProductStatuses,
+}: ProductSelectorProps ) {
+ const [ inputValue, setInputValue ] = useState( '' );
+ const [ suggestions, setSuggestions ] = useState< Product[] >( [] );
+ const [ selectedProducts, setSelectedProducts ] = useState< Product[] >(
+ []
+ );
+ const [ isLoading, setIsLoading ] = useState( false );
+
+ useEffect( () => {
+ let cancelled = false;
+
+ if ( selectedProductIds.length === 0 ) {
+ setSelectedProducts( [] );
+ return;
+ }
+
+ setIsLoading( true );
+
+ void resolveSelect( coreStore )
+ .getEntityRecords( 'postType', 'product', {
+ include: selectedProductIds,
+ per_page: selectedProductIds.length,
+ status: includeProductStatuses,
+ } )
+ .then( ( records: unknown ) => {
+ if ( cancelled || ! Array.isArray( records ) ) {
+ return;
+ }
+
+ const productsById = new Map(
+ records.map( ( product ) => [
+ product.id,
+ product as Product,
+ ] )
+ );
+ setSelectedProducts(
+ selectedProductIds
+ .map( ( id ) => productsById.get( id ) )
+ .filter(
+ ( product ): product is Product =>
+ product !== undefined
+ )
+ );
+ } )
+ .finally( () => {
+ if ( ! cancelled ) {
+ setIsLoading( false );
+ }
+ } );
+
+ return () => {
+ cancelled = true;
+ };
+ }, [ includeProductStatuses, selectedProductIds ] );
+
+ useEffect( () => {
+ const query = inputValue.trim();
+
+ if ( ! query ) {
+ setSuggestions( [] );
+ return;
+ }
+
+ let cancelled = false;
+ setIsLoading( true );
+
+ const timer = window.setTimeout( () => {
+ void resolveSelect( coreStore )
+ .getEntityRecords( 'postType', 'product', {
+ search: query,
+ per_page: 20,
+ exclude: [ ...excludeProductIds, ...selectedProductIds ],
+ status: includeProductStatuses,
+ } )
+ .then( ( records: unknown ) => {
+ if ( ! cancelled ) {
+ setSuggestions(
+ Array.isArray( records )
+ ? ( records as Product[] )
+ : []
+ );
+ }
+ } )
+ .finally( () => {
+ if ( ! cancelled ) {
+ setIsLoading( false );
+ }
+ } );
+ }, SEARCH_DELAY_MS );
+
+ return () => {
+ cancelled = true;
+ window.clearTimeout( timer );
+ };
+ }, [
+ excludeProductIds,
+ includeProductStatuses,
+ inputValue,
+ selectedProductIds,
+ ] );
+
+ const productTokenMap = useMemo( () => {
+ const allProducts = [ ...selectedProducts, ...suggestions ];
+ return new Map(
+ allProducts.map( ( product ) => [
+ getProductToken( product ),
+ product.id,
+ ] )
+ );
+ }, [ selectedProducts, suggestions ] );
+
+ return (
+ // eslint-disable-next-line @wordpress/no-base-control-with-label-without-id
+ <BaseControl
+ label={ label }
+ help={
+ isLoading ? (
+ <span
+ style={ {
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: '8px',
+ } }
+ >
+ <Spinner />
+ { description ||
+ __( 'Loading products…', 'woocommerce' ) }
+ </span>
+ ) : (
+ description
+ )
+ }
+ >
+ <FormTokenField
+ value={ selectedProducts.map( getProductToken ) }
+ suggestions={ suggestions.map( getProductToken ) }
+ onInputChange={ setInputValue }
+ onChange={ ( tokens ) => {
+ onSelectedProductIdsChange(
+ tokens
+ .map( ( token ) =>
+ productTokenMap.get(
+ typeof token === 'string'
+ ? token
+ : token.value
+ )
+ )
+ .filter(
+ ( id ): id is number => typeof id === 'number'
+ )
+ );
+ } }
+ />
+ </BaseControl>
+ );
+}
diff --git a/packages/js/experimental-products-app/src/fields/components/product-status-badge.tsx b/packages/js/experimental-products-app/src/fields/components/product-status-badge.tsx
new file mode 100644
index 00000000000..6ed56b9077b
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/components/product-status-badge.tsx
@@ -0,0 +1,60 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Badge } from '@wordpress/ui';
+import { ProductStatus } from '@woocommerce/data';
+
+type BadgeStatusConfig = {
+ label: string;
+ intent?: React.ComponentProps< typeof Badge >[ 'intent' ];
+};
+
+const statuses = {
+ draft: {
+ label: __( 'Draft', 'woocommerce' ),
+ intent: 'draft',
+ },
+ publish: {
+ label: __( 'Active', 'woocommerce' ),
+ intent: 'stable',
+ },
+ trash: {
+ label: __( 'Trash', 'woocommerce' ),
+ intent: 'none',
+ },
+ 'auto-draft': {
+ label: __( 'Draft', 'woocommerce' ),
+ intent: 'draft',
+ },
+ deleted: {
+ label: __( 'Deleted', 'woocommerce' ),
+ intent: 'none',
+ },
+ pending: {
+ label: __( 'Pending', 'woocommerce' ),
+ intent: 'none',
+ },
+ private: {
+ label: __( 'Private', 'woocommerce' ),
+ intent: 'none',
+ },
+ future: {
+ label: __( 'Scheduled', 'woocommerce' ),
+ intent: 'none',
+ },
+ any: {
+ label: __( 'Any', 'woocommerce' ),
+ intent: 'none',
+ },
+} satisfies Record< ProductStatus, BadgeStatusConfig >;
+
+export const ProductStatusBadge = ( { status }: { status: ProductStatus } ) => {
+ const statusData = statuses[ status ];
+
+ if ( ! statusData ) {
+ return <Badge intent="none">{ __( 'Unknown', 'woocommerce' ) }</Badge>;
+ }
+
+ return <Badge intent={ statusData.intent }>{ statusData.label }</Badge>;
+};
diff --git a/packages/js/experimental-products-app/src/fields/price/utils.ts b/packages/js/experimental-products-app/src/fields/price/utils.ts
new file mode 100644
index 00000000000..e7b27b729a6
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/price/utils.ts
@@ -0,0 +1,54 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+export const getLocalDefaultSaleStart = () => {
+ const tomorrow = new Date();
+ tomorrow.setDate( tomorrow.getDate() + 1 );
+ tomorrow.setHours( 7, 0, 0, 0 );
+ tomorrow.setMinutes( tomorrow.getMinutes() - tomorrow.getTimezoneOffset() );
+ return tomorrow.toISOString().slice( 0, 16 );
+};
+
+export const toNumberOrNaN = ( value: unknown ) => {
+ if ( typeof value === 'number' ) {
+ return value;
+ }
+
+ if ( typeof value === 'string' && value.trim() !== '' ) {
+ return Number.parseFloat( value );
+ }
+
+ return Number.NaN;
+};
+
+/**
+ * Validates that a price value is either empty or a valid non-negative number.
+ * Temporary: will be obsolete when price fields switch to the number field type.
+ *
+ * @param value The price value to validate.
+ * @return Null if valid, or an error message string if invalid.
+ */
+export function validatePrice( value: unknown ): string | null {
+ // Empty values are allowed (not required).
+ if (
+ value === undefined ||
+ value === null ||
+ ( typeof value === 'string' && value.trim() === '' )
+ ) {
+ return null;
+ }
+
+ const parsed = toNumberOrNaN( value );
+
+ if ( Number.isNaN( parsed ) ) {
+ return __( 'Please enter a valid price.', 'woocommerce' );
+ }
+
+ if ( parsed < 0 ) {
+ return __( 'Price must not be negative.', 'woocommerce' );
+ }
+
+ return null;
+}
diff --git a/packages/js/experimental-products-app/src/fields/types.ts b/packages/js/experimental-products-app/src/fields/types.ts
new file mode 100644
index 00000000000..7c2b61c1c55
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/types.ts
@@ -0,0 +1,38 @@
+/**
+ * External dependencies
+ */
+import type {
+ Product,
+ ProductStatus as WooProductStatus,
+} from '@woocommerce/data';
+
+export type ProductEntityRecord = Omit< Product, 'categories' | 'tags' > & {
+ categories: Array< {
+ id: number;
+ name?: string;
+ image?: {
+ src?: string;
+ alt?: string;
+ };
+ } >;
+ tags: Array< {
+ id: number;
+ name?: string;
+ } >;
+ cross_sell_ids?: number[];
+ upsell_ids?: number[];
+ date_on_sale_from?: string | null;
+ date_on_sale_to?: string | null;
+ seo_title?: string;
+ seo_description?: string;
+ visible_in_pos?: boolean;
+};
+
+export type ProductStatus = WooProductStatus;
+
+export type SettingsEntityRecord = {
+ values?: {
+ woocommerce_dimension_unit?: string;
+ woocommerce_weight_unit?: string;
+ };
+};
diff --git a/packages/js/experimental-products-app/src/fields/utils/currency.ts b/packages/js/experimental-products-app/src/fields/utils/currency.ts
new file mode 100644
index 00000000000..612ad8991b8
--- /dev/null
+++ b/packages/js/experimental-products-app/src/fields/utils/currency.ts
@@ -0,0 +1,46 @@
+/**
+ * External dependencies
+ */
+import {
+ CURRENCY,
+ // @ts-expect-error - The CURRENCY object doesn't have types yet.
+} from '@woocommerce/settings';
+
+type CurrencyObject = {
+ code: string;
+ symbol: string;
+ symbolPosition: string;
+ precision: number;
+ decimalSeparator?: string;
+ thousandSeparator?: string;
+};
+
+export function getCurrencyObject(): CurrencyObject {
+ return {
+ code: CURRENCY.code || 'USD',
+ symbol: CURRENCY.symbol || '$',
+ symbolPosition: CURRENCY.symbolPosition || 'left',
+ precision: Number( CURRENCY.precision ?? 2 ),
+ decimalSeparator: CURRENCY.decimalSeparator,
+ thousandSeparator: CURRENCY.thousandSeparator,
+ };
+}
+
+export function formatCurrency(
+ value: number | string,
+ currencyCode = getCurrencyObject().code
+) {
+ const amount = typeof value === 'number' ? value : Number( value );
+ const locale =
+ typeof document !== 'undefined'
+ ? document.documentElement.lang || 'en-US'
+ : 'en-US';
+ const { precision } = getCurrencyObject();
+
+ return new Intl.NumberFormat( locale, {
+ style: 'currency',
+ currency: currencyCode,
+ minimumFractionDigits: precision,
+ maximumFractionDigits: precision,
+ } ).format( amount );
+}
diff --git a/packages/js/experimental-products-app/src/layout.tsx b/packages/js/experimental-products-app/src/layout.tsx
index e76e1c884a6..71af0112f64 100644
--- a/packages/js/experimental-products-app/src/layout.tsx
+++ b/packages/js/experimental-products-app/src/layout.tsx
@@ -1,7 +1,6 @@
/**
* External dependencies
*/
-import { createElement, Fragment } from '@wordpress/element';
import {
useViewportMatch,
useResizeObserver,
diff --git a/packages/js/experimental-products-app/src/products.tsx b/packages/js/experimental-products-app/src/products.tsx
index 83d07ddc6e9..74a38086004 100644
--- a/packages/js/experimental-products-app/src/products.tsx
+++ b/packages/js/experimental-products-app/src/products.tsx
@@ -1,13 +1,7 @@
/**
* External dependencies
*/
-import {
- StrictMode,
- Suspense,
- createElement,
- createRoot,
- lazy,
-} from '@wordpress/element';
+import { StrictMode, Suspense, createRoot, lazy } from '@wordpress/element';
import {
Root,
// @ts-expect-error missing types.
diff --git a/packages/js/experimental-products-app/src/router.tsx b/packages/js/experimental-products-app/src/router.tsx
index 68155949723..fe995bb0f2e 100644
--- a/packages/js/experimental-products-app/src/router.tsx
+++ b/packages/js/experimental-products-app/src/router.tsx
@@ -1,7 +1,6 @@
/**
* External dependencies
*/
-import { createElement } from '@wordpress/element';
import { privateApis as routerPrivateApis } from '@wordpress/router';
/**
diff --git a/packages/js/experimental-products-app/src/sidebar/index.tsx b/packages/js/experimental-products-app/src/sidebar/index.tsx
index a0a85f291c5..d1b2b751c72 100644
--- a/packages/js/experimental-products-app/src/sidebar/index.tsx
+++ b/packages/js/experimental-products-app/src/sidebar/index.tsx
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { createElement, useRef } from '@wordpress/element';
+import { useRef } from '@wordpress/element';
function SidebarContentWrapper( { children }: { children: React.ReactNode } ) {
const wrapperRef = useRef< HTMLDivElement | null >( null );
diff --git a/packages/js/experimental-products-app/src/utilites/new-navigation.tsx b/packages/js/experimental-products-app/src/utilites/new-navigation.tsx
index 29b582a587a..2e527015673 100644
--- a/packages/js/experimental-products-app/src/utilites/new-navigation.tsx
+++ b/packages/js/experimental-products-app/src/utilites/new-navigation.tsx
@@ -1,12 +1,7 @@
/**
* External dependencies
*/
-import {
- createElement,
- createContext,
- useState,
- useContext,
-} from '@wordpress/element';
+import { createContext, useState, useContext } from '@wordpress/element';
type NewNavigationContextType = {
showNewNavigation: boolean;
diff --git a/packages/js/experimental-products-app/tsconfig.json b/packages/js/experimental-products-app/tsconfig.json
index 6739b347343..205e7551412 100644
--- a/packages/js/experimental-products-app/tsconfig.json
+++ b/packages/js/experimental-products-app/tsconfig.json
@@ -8,17 +8,11 @@
"declarationMap": true,
"declarationDir": "./build-types",
"resolveJsonModule": true,
- "typeRoots": [
- "./typings",
- "./node_modules/@types"
- ]
+ "typeRoots": [ "./typings", "./node_modules/@types" ],
+ "jsx": "react-jsx",
+ "jsxFactory": null,
+ "jsxFragmentFactory": null
},
- "include": [
- "typings/**/*",
- "src/**/*"
- ],
- "exclude": [
- "**/test/**",
- "**/stories/**"
- ]
+ "include": [ "src/**/*" ],
+ "exclude": [ "**/test/**", "**/stories/**" ]
}