Commit 17d9e02e2d8 for woocommerce
commit 17d9e02e2d83b463c7fc32323e9bb0d021417a18
Author: Peter Petrov <peter.petrov89@gmail.com>
Date: Thu Aug 20 14:05:21 2026 +0300
Move role column to end of customers report table for consistent CSV order (#67853)
diff --git a/plugins/woocommerce/changelog/role-column-last-in-customers-table b/plugins/woocommerce/changelog/role-column-last-in-customers-table
new file mode 100644
index 00000000000..f9783f6b0d3
--- /dev/null
+++ b/plugins/woocommerce/changelog/role-column-last-in-customers-table
@@ -0,0 +1,5 @@
+Significance: patch
+Type: tweak
+Comment: Aligns the browser-side Customers CSV with the server export order from #67843; the Role column feature (#67653) is unreleased so no shipped behavior changes.
+
+
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/customers/table.js b/plugins/woocommerce/client/admin/client/analytics/report/customers/table.js
index cfe49039982..565a1074f86 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/customers/table.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/customers/table.js
@@ -35,6 +35,11 @@ function CustomersReportTable( {
};
} );
+ // This order is also the browser-side CSV export order (ReportTable exports
+ // the table as-is when the report fits one page), so it must match
+ // Controller::get_export_columns() in
+ // src/Admin/API/Reports/Customers/Controller.php, which the same Download
+ // button uses for larger reports. New columns go last in both.
const getHeadersContent = () => {
return [
{
@@ -49,10 +54,6 @@ function CustomersReportTable( {
key: 'username',
hiddenByDefault: true,
},
- {
- label: __( 'Role', 'woocommerce' ),
- key: 'role',
- },
{
label: __( 'Last active', 'woocommerce' ),
key: 'date_last_active',
@@ -119,6 +120,10 @@ function CustomersReportTable( {
key: 'shipping_phone',
hiddenByDefault: true,
},
+ {
+ label: __( 'Role', 'woocommerce' ),
+ key: 'role',
+ },
];
};
@@ -206,10 +211,6 @@ function CustomersReportTable( {
display: username,
value: username,
},
- {
- display: role,
- value: role,
- },
{
display: dateLastActiveDisplay,
value: dateLastActive,
@@ -262,6 +263,10 @@ function CustomersReportTable( {
display: shippingPhone,
value: shippingPhone,
},
+ {
+ display: role,
+ value: role,
+ },
];
} );
};
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/customers/test/table.test.js b/plugins/woocommerce/client/admin/client/analytics/report/customers/test/table.test.js
index 1a0a96d6ff2..c1b54ae7ca0 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/customers/test/table.test.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/customers/test/table.test.js
@@ -71,8 +71,30 @@ const baseCustomer = {
country: '',
};
-// Country cell is the 10th column (0-indexed: 9) per getHeadersContent in table.js.
-const COUNTRY_COL = 9;
+// getHeadersContent in table.js, in order. This is also the browser-side CSV
+// export order, so it must match Controller::get_export_columns() in
+// src/Admin/API/Reports/Customers/Controller.php.
+const HEADER_KEYS = [
+ 'name',
+ 'username',
+ 'date_last_active',
+ 'date_registered',
+ 'email',
+ 'orders_count',
+ 'total_spend',
+ 'avg_order_value',
+ 'country',
+ 'city',
+ 'state',
+ 'postcode',
+ 'billing_phone',
+ 'shipping_phone',
+ 'role',
+];
+
+const col = ( key ) => HEADER_KEYS.indexOf( key );
+
+const COUNTRY_COL = col( 'country' );
function getCountryCell( customer ) {
captured.getRowsContent = null;
@@ -160,10 +182,63 @@ describe( 'CustomersReportTable country cell', () => {
} );
} );
+describe( 'CustomersReportTable column order', () => {
+ beforeEach( () => {
+ jest.clearAllMocks();
+ } );
+
+ // The same Download button exports the table client-side for single-page
+ // reports and server-side for larger ones, so drift between the two orders
+ // makes identical reports produce differently ordered CSVs.
+ it( 'keeps the header order in sync with the server-side CSV export', () => {
+ mockCountriesStore( [] );
+ captured.getHeadersContent = null;
+ render( <CustomersReportTable query={ {} } /> );
+
+ expect(
+ captured.getHeadersContent().map( ( header ) => header.key )
+ ).toEqual( HEADER_KEYS );
+ } );
+
+ it( 'emits every cell under its own header', () => {
+ mockCountriesStore( [] );
+ captured.getRowsContent = null;
+ render( <CustomersReportTable query={ {} } /> );
+
+ // One distinct value per field, so a cell landing under the wrong
+ // header surfaces as a mismatch instead of passing by coincidence.
+ const customer = {
+ name: 'Alice',
+ username: 'alice',
+ date_last_active: '2026-08-01T00:00:00',
+ date_registered: '2026-07-01T00:00:00',
+ email: 'alice@example.com',
+ orders_count: 7,
+ total_spend: 123,
+ avg_order_value: 45,
+ country: 'FR',
+ city: 'Paris',
+ state: 'IDF',
+ postcode: '75001',
+ billing_phone: '555-32123',
+ shipping_phone: '555-99887',
+ role: 'Customer',
+ };
+
+ const row = captured.getRowsContent( [ customer ] )[ 0 ];
+
+ expect( row ).toHaveLength( HEADER_KEYS.length );
+ expect(
+ Object.fromEntries(
+ HEADER_KEYS.map( ( key, index ) => [ key, row[ index ].value ] )
+ )
+ ).toEqual( customer );
+ } );
+} );
+
describe( 'CustomersReportTable phone cells', () => {
- // Phone cells are the last two columns per getHeadersContent in table.js.
- const BILLING_PHONE_COL = 13;
- const SHIPPING_PHONE_COL = 14;
+ const BILLING_PHONE_COL = col( 'billing_phone' );
+ const SHIPPING_PHONE_COL = col( 'shipping_phone' );
beforeEach( () => {
jest.clearAllMocks();
@@ -193,6 +268,5 @@ describe( 'CustomersReportTable phone cells', () => {
expect( headers[ BILLING_PHONE_COL ].key ).toBe( 'billing_phone' );
expect( headers[ SHIPPING_PHONE_COL ].key ).toBe( 'shipping_phone' );
- expect( headers ).toHaveLength( SHIPPING_PHONE_COL + 1 );
} );
} );
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Customers/Controller.php b/plugins/woocommerce/src/Admin/API/Reports/Customers/Controller.php
index 1b673ccea04..97e34d35830 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Customers/Controller.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Customers/Controller.php
@@ -751,6 +751,10 @@ class Controller extends GenericController implements ExportableInterface {
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
+ // Appending keeps positional consumers of the released columns working, and
+ // this order must match getHeadersContent() in
+ // client/admin/client/analytics/report/customers/table.js, which produces the
+ // browser-side CSV for single-page reports from the same Download button.
$export_columns = array(
'name' => __( 'Name', 'woocommerce' ),
'username' => __( 'Username', 'woocommerce' ),
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php
index 0e386ea709a..9b6630d94d2 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php
@@ -257,7 +257,6 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
$export_columns = $controller->get_export_columns();
$this->assertArrayHasKey( 'role', $export_columns, 'CSV export should include a role column' );
$this->assertEquals( 'Role', $export_columns['role'] );
- $this->assertSame( 'role', array_key_last( $export_columns ), 'Role must stay the last CSV column so positional consumers of the pre-existing columns are unaffected' );
$export_roles_by_user_id = array();
foreach ( $reports as $report ) {
@@ -270,6 +269,39 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
$this->assertSame( '', $export_roles_by_user_id[0], 'CSV export should leave the role empty for guests' );
}
+ /**
+ * The same Download button exports single-page reports in the browser from the
+ * table column order and larger ones from here, so the two must not drift.
+ *
+ * @testdox Should keep the CSV export column order in sync with the report table.
+ */
+ public function test_export_column_order_matches_report_table() {
+ // Mirrors getHeadersContent() in
+ // client/admin/client/analytics/report/customers/table.js. Keys differ
+ // between the two, the order must not.
+ $expected_order = array(
+ 'name',
+ 'username',
+ 'last_active',
+ 'registered',
+ 'email',
+ 'orders_count',
+ 'total_spend',
+ 'avg_order_value',
+ 'country',
+ 'city',
+ 'region',
+ 'postcode',
+ 'billing_phone',
+ 'shipping_phone',
+ 'role',
+ );
+
+ $controller = new \Automattic\WooCommerce\Admin\API\Reports\Customers\Controller();
+
+ $this->assertSame( $expected_order, array_keys( $controller->get_export_columns() ), 'New CSV columns must be appended, and table.js must be updated to match' );
+ }
+
/**
* Test getting reports.
*