diff --git a/plugins/woocommerce-admin/client/analytics/components/leaderboard/data/top-selling-products-mock-data.js b/plugins/woocommerce-admin/client/analytics/components/leaderboard/data/top-selling-products-mock-data.js
index 584b9903933..8f878b2e09b 100644
--- a/plugins/woocommerce-admin/client/analytics/components/leaderboard/data/top-selling-products-mock-data.js
+++ b/plugins/woocommerce-admin/client/analytics/components/leaderboard/data/top-selling-products-mock-data.js
@@ -23,8 +23,8 @@ And as such will require data layer logic for products to fully build the table
export default [
{
product_id: 20,
- items_sold: 1000,
- net_revenue: 999.99,
+ items_sold: 123456789,
+ net_revenue: 9876543.215,
orders_count: 54,
name: 'awesome shirt',
_links: {
diff --git a/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js b/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js
index 7ee8b89db32..98e9bdbd9b3 100644
--- a/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js
+++ b/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js
@@ -14,6 +14,8 @@ import {
getLeaderboard,
SETTINGS_STORE_NAME,
} from '@woocommerce/data';
+import { CurrencyContext } from '@woocommerce/currency';
+import { formatValue } from '@woocommerce/number';
import { Text } from '@woocommerce/experimental';
/**
@@ -23,7 +25,46 @@ import ReportError from '../report-error';
import sanitizeHTML from '../../../lib/sanitize-html';
import './style.scss';
+const formattable = new Set( [ 'currency', 'number' ] );
+
export class Leaderboard extends Component {
+ getFormattedColumn = ( column ) => {
+ const { format } = column;
+
+ /*
+ * The format property is used for numeric columns and is optional.
+ * The `value` property type is specified as string in the API schema
+ * and it's extensible from other extensions. Therefore, even if the
+ * actual type of numeric columns returned by WooCoomerce's own API is
+ * number, there is no guarantee the value will be a number.
+ */
+ if ( formattable.has( column.format ) && isFinite( column.value ) ) {
+ const value = parseFloat( column.value );
+
+ if ( ! Number.isNaN( value ) ) {
+ const { formatAmount, getCurrencyConfig } = this.context;
+ const display =
+ format === 'currency'
+ ? formatAmount( value )
+ : formatValue( getCurrencyConfig(), format, value );
+
+ return {
+ display,
+ value,
+ };
+ }
+ }
+
+ return {
+ display: (
+
+ ),
+ value: column.value,
+ };
+ };
+
getFormattedHeaders() {
return this.props.headers.map( ( header, i ) => {
return {
@@ -38,18 +79,7 @@ export class Leaderboard extends Component {
getFormattedRows() {
return this.props.rows.map( ( row ) => {
- return row.map( ( column ) => {
- return {
- display: (
-
- ),
- value: column.value,
- };
- } );
+ return row.map( this.getFormattedColumn );
} );
}
@@ -151,6 +181,8 @@ Leaderboard.defaultProps = {
isRequesting: false,
};
+Leaderboard.contextType = CurrencyContext;
+
export default compose(
withSelect( ( select, props ) => {
const { id, query, totalRows, filters } = props;
diff --git a/plugins/woocommerce-admin/client/analytics/components/leaderboard/test/index.js b/plugins/woocommerce-admin/client/analytics/components/leaderboard/test/index.js
index c5c9cacd89d..582da723b6d 100644
--- a/plugins/woocommerce-admin/client/analytics/components/leaderboard/test/index.js
+++ b/plugins/woocommerce-admin/client/analytics/components/leaderboard/test/index.js
@@ -1,18 +1,14 @@
/**
* External dependencies
*/
-import { render } from '@testing-library/react';
-import { numberFormat } from '@woocommerce/number';
-import { CurrencyFactory } from '@woocommerce/currency';
+import { render, screen } from '@testing-library/react';
+import { CurrencyFactory, CurrencyContext } from '@woocommerce/currency';
/**
* Internal dependencies
*/
import { Leaderboard } from '../';
import mockData from '../data/top-selling-products-mock-data';
-import { CURRENCY } from '~/utils/admin-settings';
-
-const { formatAmount, formatDecimal } = CurrencyFactory( CURRENCY );
const headers = [
{
@@ -42,23 +38,26 @@ const rows = mockData.map( ( row ) => {
value: name,
},
{
- display: numberFormat( CURRENCY, itemsSold ),
+ display: itemsSold.toString(),
value: itemsSold,
+ format: 'number',
},
{
- display: numberFormat( CURRENCY, ordersCount ),
- value: ordersCount,
+ display: ordersCount.toString(),
+ value: ordersCount.toString(),
+ format: 'number',
},
{
- display: formatAmount( netRevenue ),
- value: formatDecimal( netRevenue ),
+ display: `${ netRevenue }`,
+ value: netRevenue,
+ format: 'currency',
},
];
} );
describe( 'Leaderboard', () => {
test( 'should render empty message when there are no rows', () => {
- const { queryByText } = render(
+ render(
{
);
expect(
- queryByText( 'No data recorded for the selected time period.' )
+ screen.getByText( 'No data recorded for the selected time period.' )
).toBeInTheDocument();
} );
test( 'should render the headers', () => {
- const { queryByText } = render(
-
- );
-
- expect( queryByText( 'Name' ) ).toBeInTheDocument();
- expect( queryByText( 'Items sold' ) ).toBeInTheDocument();
- expect( queryByText( 'Orders' ) ).toBeInTheDocument();
- expect( queryByText( 'Net sales' ) ).toBeInTheDocument();
- } );
-
- test( 'should render formatted data in the table', () => {
- const { container, queryByText } = render(
+ render(
{
/>
);
- const tableRows = container.querySelectorAll( 'tr' );
+ expect( screen.getByText( 'Name' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Items sold' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Orders' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Net sales' ) ).toBeInTheDocument();
+ } );
- expect( tableRows.length ).toBe( 6 );
- expect( queryByText( 'awesome shirt' ) ).toBeInTheDocument();
- expect( queryByText( '1,000.00' ) ).toBeInTheDocument();
- expect( queryByText( '54.00' ) ).toBeInTheDocument();
- expect( queryByText( '$999.99' ) ).toBeInTheDocument();
+ test( 'should render formatted data in the table', () => {
+ render(
+
+ );
+
+ expect( screen.getAllByRole( 'row' ) ).toHaveLength( 6 );
+ expect( screen.getByText( 'awesome shirt' ) ).toBeInTheDocument();
+ expect( screen.getByText( '123,456,789' ) ).toBeInTheDocument();
+ expect( screen.getByText( '54' ) ).toBeInTheDocument();
+ expect( screen.getByText( '$9,876,543.22' ) ).toBeInTheDocument();
+ } );
+
+ test( 'should format data according to the currency context', () => {
+ const currencySetting = {
+ code: 'PLN',
+ decimalSeparator: ',',
+ precision: 3,
+ priceFormat: '%1$s %2$s',
+ symbol: 'zł',
+ thousandSeparator: '.',
+ };
+
+ render(
+
+
+
+ );
+
+ expect( screen.getByText( 'awesome shirt' ) ).toBeInTheDocument();
+ expect( screen.getByText( '123.456.789' ) ).toBeInTheDocument();
+ expect( screen.getByText( '54' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'zł 9.876.543,215' ) ).toBeInTheDocument();
+ } );
+
+ test( `should not format data that is not specified in a format or doesn't conform to a number value`, () => {
+ const columns = [
+ {
+ display: 'awesome shirt',
+ value: '$123.456', // Not a pure numeric string
+ format: 'currency',
+ },
+ {
+ display: 'awesome pants',
+ value: '123,456', // Not a pure numeric string
+ format: 'number',
+ },
+ {
+ display: 'awesome hat',
+ value: '', // Not a number
+ format: 'number',
+ },
+ {
+ display: 'awesome sticker',
+ value: 123,
+ format: 'product', // Invalid format
+ },
+ {
+ // Not specified format
+ display: 'awesome button',
+ value: 123,
+ },
+ ];
+
+ render(
+ ( {
+ label: i.toString(),
+ } ) ) }
+ rows={ [ columns ] }
+ totalRows={ 5 }
+ />
+ );
+
+ expect( screen.getByText( 'awesome shirt' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'awesome pants' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'awesome hat' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'awesome sticker' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'awesome button' ) ).toBeInTheDocument();
} );
} );
diff --git a/plugins/woocommerce/changelog/fix-36748-analytics-dashboard-currency-formatting b/plugins/woocommerce/changelog/fix-36748-analytics-dashboard-currency-formatting
new file mode 100644
index 00000000000..70dd09fea89
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-36748-analytics-dashboard-currency-formatting
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Make the Leaderboards on the Analytics > Dashboard page use consistent currency and number formatting across the page, and perceive the currency setting comes from the relevant filter.
diff --git a/plugins/woocommerce/src/Admin/API/Leaderboards.php b/plugins/woocommerce/src/Admin/API/Leaderboards.php
index e859b834d69..f40d2470196 100644
--- a/plugins/woocommerce/src/Admin/API/Leaderboards.php
+++ b/plugins/woocommerce/src/Admin/API/Leaderboards.php
@@ -130,10 +130,12 @@ class Leaderboards extends \WC_REST_Data_Controller {
array(
'display' => wc_admin_number_format( $coupon['orders_count'] ),
'value' => $coupon['orders_count'],
+ 'format' => 'number',
),
array(
'display' => wc_price( $coupon['amount'] ),
'value' => $coupon['amount'],
+ 'format' => 'currency',
),
);
}
@@ -199,10 +201,12 @@ class Leaderboards extends \WC_REST_Data_Controller {
array(
'display' => wc_admin_number_format( $category['items_sold'] ),
'value' => $category['items_sold'],
+ 'format' => 'number',
),
array(
'display' => wc_price( $category['net_revenue'] ),
'value' => $category['net_revenue'],
+ 'format' => 'currency',
),
);
}
@@ -266,10 +270,12 @@ class Leaderboards extends \WC_REST_Data_Controller {
array(
'display' => wc_admin_number_format( $customer['orders_count'] ),
'value' => $customer['orders_count'],
+ 'format' => 'number',
),
array(
'display' => wc_price( $customer['total_spend'] ),
'value' => $customer['total_spend'],
+ 'format' => 'currency',
),
);
}
@@ -335,10 +341,12 @@ class Leaderboards extends \WC_REST_Data_Controller {
array(
'display' => wc_admin_number_format( $product['items_sold'] ),
'value' => $product['items_sold'],
+ 'format' => 'number',
),
array(
'display' => wc_price( $product['net_revenue'] ),
'value' => $product['net_revenue'],
+ 'format' => 'currency',
),
);
}
@@ -578,6 +586,14 @@ class Leaderboards extends \WC_REST_Data_Controller {
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
+ 'format' => array(
+ 'description' => __( 'Table cell format.', 'woocommerce' ),
+ 'type' => 'string',
+ 'context' => array( 'view' ),
+ 'enum' => array( 'currency', 'number' ),
+ 'readonly' => true,
+ 'required' => false,
+ ),
),
),
),
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/leaderboards.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/leaderboards.php
index 9018ffb3463..30eada7f88d 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/leaderboards.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/leaderboards.php
@@ -140,9 +140,10 @@ class WC_Admin_Tests_API_Leaderboards extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'label', $header_properties );
$row_properties = $schema['rows']['items']['properties'];
- $this->assertCount( 2, $row_properties );
+ $this->assertCount( 3, $row_properties );
$this->assertArrayHasKey( 'display', $row_properties );
$this->assertArrayHasKey( 'value', $row_properties );
+ $this->assertArrayHasKey( 'format', $row_properties );
}
/**