Commit 71091cbd80 for woocommerce
commit 71091cbd80ed19853c3286f7756142abeee4ba38
Author: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
Date: Tue Dec 9 17:04:14 2025 +0900
Analytics: Implement add date status and manual update UI (#62276)
* Fix Undefined array key "option_key"
* Add Import Status Bar Component for Analytics
This commit introduces the Import Status Bar component, which displays the current analytics import status, including the last processed date, next scheduled import time, and a manual "Update now" button. The component is styled with SCSS and integrates with the existing analytics framework. Additionally, a custom hook for managing import status is added, along with corresponding types and unit tests to ensure functionality.
- Create ImportStatusBar component and styles
- Implement useImportStatus hook for fetching and managing import status
- Add types for import status and hook return values
- Write unit tests for ImportStatusBar and useImportStatus
This feature enhances the user experience by providing real-time feedback on the analytics import process.
* Add changelog
* Update changelog
* Fix tests
* Fix tests
* Wrap in feature flag
* Fix lint
* Fix lint
* Fix lint
* Improve translators comment
* Add loading spinner to Import Status Bar component
This update enhances the Import Status Bar component by introducing a loading spinner that indicates when data is being processed. The spinner is displayed during loading states for both the last processed date and the next scheduled import time. Additionally, the component now checks the WooCommerce Admin settings to determine if immediate import is enabled, preventing rendering in such cases.
- Add Spinner component from WordPress
- Update conditional rendering logic based on WooCommerce settings
- Style adjustments for spinner in SCSS
* Fix tests
* Add doc comment
* Address pr feedback
* Refactor Import Status Bar component
- Remove unused SCSS class for updating status.
- Update conditional check for immediate import setting to use optional chaining.
- Improve error handling in the import trigger function to provide more specific error messages.
These changes streamline the component's styling and enhance its robustness in handling settings and errors.
* Fix tests
diff --git a/plugins/woocommerce/changelog/wooa7s-794-analytics-overview-add-date-status-and-manual-update-ui b/plugins/woocommerce/changelog/wooa7s-794-analytics-overview-add-date-status-and-manual-update-ui
new file mode 100644
index 0000000000..b25ce172f4
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooa7s-794-analytics-overview-add-date-status-and-manual-update-ui
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add date status and manual update UI to Analytics pages
diff --git a/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/import-status-bar.scss b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/import-status-bar.scss
new file mode 100644
index 0000000000..bf13a265fd
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/import-status-bar.scss
@@ -0,0 +1,65 @@
+/**
+ * Analytics Import Status Bar Styles
+ *
+ */
+
+.woocommerce-analytics-import-status-bar-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+
+ &__label {
+ font-size: 13px;
+ line-height: 20px;
+ color: $gray-900;
+ font-weight: 400;
+ }
+}
+
+.woocommerce-analytics-import-status-bar {
+ display: flex;
+ align-items: center;
+ border: 1px solid $gray-400;
+ border-radius: 4px;
+ padding: 16px 12px;
+ font-size: 11px;
+ line-height: 20px;
+ font-weight: 400;
+ height: 50px;
+
+ &__content {
+ display: flex;
+ align-items: center;
+ gap: 32px;
+
+
+ @media screen and ( max-width: $break-wide ) {
+ gap: 24px;
+ }
+ }
+
+ &__item {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__label {
+ color: $gray-700;
+ white-space: nowrap;
+ }
+
+ &__value {
+ color: $gray-900;
+ }
+
+ &__trigger {
+ font-size: 11px;
+ font-weight: 500;
+ line-height: 20px;
+ }
+
+ .components-spinner {
+ height: 8px;
+ width: 8px;
+ }
+}
diff --git a/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/import-status-bar.tsx b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/import-status-bar.tsx
new file mode 100644
index 0000000000..38e708a3b6
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/import-status-bar.tsx
@@ -0,0 +1,174 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { dateI18n } from '@wordpress/date';
+import { Button, Spinner } from '@wordpress/components';
+import { useDispatch } from '@wordpress/data';
+import { useSettings } from '@woocommerce/data';
+
+/**
+ * Internal dependencies
+ */
+import { useImportStatus } from './use-import-status';
+import './import-status-bar.scss';
+
+/**
+ * Analytics Import Status Bar Component
+ *
+ * Displays the current analytics import status including:
+ * - Last processed date
+ * - Next scheduled import time
+ * - Manual "Update now" button
+ *
+ * Only displays in scheduled mode. Hidden in immediate mode.
+ *
+ * @return {JSX.Element|null} The status bar component or null if hidden
+ */
+export function ImportStatusBar(): JSX.Element | null {
+ const { status, isLoading, triggerImport, isTriggeringImport } =
+ useImportStatus();
+ const { createNotice } = useDispatch( 'core/notices' );
+ const { wcAdminSettings } = useSettings( 'wc_admin', [
+ 'wcAdminSettings',
+ ] ) as unknown as {
+ wcAdminSettings: {
+ woocommerce_analytics_immediate_import: 'yes' | 'no';
+ };
+ };
+
+ // Don't render if immediate import is enabled
+ // Use the value from the settings hook rather than the status object; accessing settings is faster because they are preloaded.
+ if (
+ ! wcAdminSettings?.woocommerce_analytics_immediate_import ||
+ wcAdminSettings.woocommerce_analytics_immediate_import === 'yes'
+ ) {
+ return null;
+ }
+
+ /**
+ * Format a date string for display
+ *
+ * @param {string|null} date - Date string in 'Y-m-d H:i:s' format (site timezone)
+ * @return {string} Formatted date string or "Never"
+ */
+ const formatLastProcessedDate = ( date: string | null ): string => {
+ if ( ! date ) {
+ return __( 'Never', 'woocommerce' );
+ }
+ return dateI18n( 'M j H:i', date, undefined );
+ };
+
+ const formatNextScheduledDate = ( date: string | null ): string => {
+ if ( ! date ) {
+ return __( 'Never', 'woocommerce' );
+ }
+
+ return dateI18n(
+ /**
+ * translators: %s: formatted date and time in site timezone.
+ * Used to display the next scheduled time for the Analytics import, e.g. "Nov 21 at 12:00".
+ * "M j" shows the month and day, "at" as literal, "H:i" shows time (24-hour format).
+ */
+ __( 'M j \\a\\t H:i', 'woocommerce' ),
+ date,
+ undefined
+ );
+ };
+
+ /**
+ * Handle manual import trigger
+ */
+ const handleTriggerImport = async (): Promise< void > => {
+ try {
+ await triggerImport();
+ createNotice(
+ 'success',
+ __(
+ 'Analytics import has started. Your store data will be updated soon.',
+ 'woocommerce'
+ ),
+ {
+ type: 'snackbar',
+ isDismissible: true,
+ }
+ );
+ } catch ( err ) {
+ createNotice(
+ 'error',
+ err instanceof Error
+ ? err.message
+ : __(
+ 'Failed to trigger analytics update.',
+ 'woocommerce'
+ ),
+ {
+ isDismissible: true,
+ }
+ );
+ }
+ };
+
+ // Disable button when an import is already scheduled/running or currently triggering
+ const isButtonDisabled =
+ status?.import_in_progress_or_due || isTriggeringImport;
+
+ return (
+ <div className="woocommerce-analytics-import-status-bar-wrapper">
+ <div className="woocommerce-analytics-import-status-bar-wrapper__label">
+ { __( 'Data status:', 'woocommerce' ) }
+ </div>
+ <div
+ className="woocommerce-analytics-import-status-bar"
+ role="status"
+ aria-live="polite"
+ aria-atomic="true"
+ aria-busy={ isLoading || isTriggeringImport }
+ >
+ <div className="woocommerce-analytics-import-status-bar__content">
+ <span className="woocommerce-analytics-import-status-bar__item">
+ <span className="woocommerce-analytics-import-status-bar__label">
+ { __( 'Last updated', 'woocommerce' ) }
+ </span>
+ <span className="woocommerce-analytics-import-status-bar__value">
+ { isLoading ? (
+ <Spinner />
+ ) : (
+ formatLastProcessedDate(
+ status?.last_processed_date || null
+ )
+ ) }
+ </span>
+ </span>
+ <span className="woocommerce-analytics-import-status-bar__item">
+ <span className="woocommerce-analytics-import-status-bar__label">
+ { __( 'Next update', 'woocommerce' ) }
+ </span>
+ <span className="woocommerce-analytics-import-status-bar__value">
+ { isLoading ? (
+ <Spinner />
+ ) : (
+ formatNextScheduledDate(
+ status?.next_scheduled || null
+ )
+ ) }
+ </span>
+ </span>
+ <Button
+ variant="tertiary"
+ onClick={ handleTriggerImport }
+ disabled={ isButtonDisabled || isLoading }
+ isBusy={ isTriggeringImport }
+ className="woocommerce-analytics-import-status-bar__trigger"
+ aria-label={ __(
+ 'Manually trigger analytics data import',
+ 'woocommerce'
+ ) }
+ >
+ { __( 'Update now', 'woocommerce' ) }
+ </Button>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/index.ts b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/index.ts
new file mode 100644
index 0000000000..945fbfb330
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/index.ts
@@ -0,0 +1,7 @@
+/**
+ * Export Analytics Import Status Bar component and related utilities
+ */
+
+export { ImportStatusBar } from './import-status-bar';
+export { useImportStatus } from './use-import-status';
+export type { ImportStatus, UseImportStatusReturn } from './types';
diff --git a/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/test/import-status-bar.test.tsx b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/test/import-status-bar.test.tsx
new file mode 100644
index 0000000000..793d652b65
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/test/import-status-bar.test.tsx
@@ -0,0 +1,332 @@
+/**
+ * External dependencies
+ */
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { useDispatch } from '@wordpress/data';
+import { useSettings } from '@woocommerce/data';
+
+/**
+ * Internal dependencies
+ */
+import { ImportStatusBar } from '../import-status-bar';
+import { useImportStatus } from '../use-import-status';
+import type { UseImportStatusReturn } from '../types';
+
+jest.mock( '../use-import-status' );
+
+jest.mock( '@wordpress/data', () => ( {
+ ...jest.requireActual( '@wordpress/data' ),
+ useDispatch: jest.fn().mockImplementation( () => ( {
+ createNotice: jest.fn(),
+ } ) ),
+} ) );
+jest.mock( '@wordpress/date', () => ( {
+ dateI18n: jest.fn( ( format, date ) => {
+ // Simple mock that returns a date-like string
+ if ( ! date ) return 'Never';
+ return 'Nov 21 00:00';
+ } ),
+} ) );
+jest.mock( '@woocommerce/data', () => ( {
+ ...jest.requireActual( '@woocommerce/data' ),
+ useSettings: jest.fn().mockImplementation( () => ( {
+ wcAdminSettings: {
+ woocommerce_analytics_immediate_import: 'no',
+ },
+ } ) ),
+} ) );
+
+const mockUseImportStatus = useImportStatus as jest.MockedFunction<
+ typeof useImportStatus
+>;
+const mockUseDispatch = useDispatch as jest.MockedFunction<
+ typeof useDispatch
+>;
+
+const mockUseSettings = useSettings as jest.MockedFunction<
+ typeof useSettings
+>;
+
+describe( 'ImportStatusBar', () => {
+ const mockCreateNotice = jest.fn();
+ const mockTriggerImport = jest.fn();
+
+ beforeEach( () => {
+ jest.clearAllMocks();
+ mockUseDispatch.mockReturnValue( {
+ createNotice: mockCreateNotice,
+ } );
+ mockUseSettings.mockReturnValue( {
+ wcAdminSettings: {
+ woocommerce_analytics_immediate_import: 'no',
+ },
+ } as unknown as ReturnType< typeof useSettings > );
+ } );
+
+ const createMockReturn = (
+ overrides: Partial< UseImportStatusReturn > = {}
+ ): UseImportStatusReturn => ( {
+ status: {
+ mode: 'scheduled',
+ last_processed_date: '2024-11-21 00:00:00',
+ next_scheduled: '2024-11-21 12:00:00',
+ import_in_progress_or_due: false,
+ },
+ isLoading: false,
+ error: null,
+ triggerImport: mockTriggerImport,
+ isTriggeringImport: false,
+ ...overrides,
+ } );
+
+ it( 'should not render when mode is immediate', () => {
+ mockUseSettings.mockReturnValue( {
+ wcAdminSettings: {
+ woocommerce_analytics_immediate_import: 'yes',
+ },
+ } as unknown as ReturnType< typeof useSettings > );
+ // Mock useImportStatus to avoid destructuring error even though component returns early
+ mockUseImportStatus.mockReturnValue( createMockReturn() );
+
+ const { container } = render( <ImportStatusBar /> );
+ expect( container ).toBeEmptyDOMElement();
+ } );
+
+ it( 'should show spinners when loading', () => {
+ mockUseImportStatus.mockReturnValue(
+ createMockReturn( {
+ status: null,
+ isLoading: true,
+ } )
+ );
+
+ render( <ImportStatusBar /> );
+
+ // Component should render and show spinners when loading
+ expect( screen.getByText( /Last updated$/i ) ).toBeInTheDocument();
+ expect( screen.getByText( /Next update$/i ) ).toBeInTheDocument();
+ // Check for spinner elements (they have role="presentation" and are SVGs)
+ const spinners = screen.getAllByRole( 'presentation' );
+ expect( spinners.length ).toBeGreaterThan( 0 );
+ } );
+
+ it( 'should render status when mode is scheduled', () => {
+ mockUseImportStatus.mockReturnValue( createMockReturn() );
+
+ render( <ImportStatusBar /> );
+
+ expect( screen.getByText( /Last updated$/i ) ).toBeInTheDocument();
+ expect( screen.getByText( /Next update$/i ) ).toBeInTheDocument();
+ expect(
+ screen.getByRole( 'button', {
+ name: /Manually trigger analytics data import/i,
+ } )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'should disable button when import_in_progress_or_due is true', () => {
+ mockUseImportStatus.mockReturnValue(
+ createMockReturn( {
+ status: {
+ mode: 'scheduled',
+ last_processed_date: '2024-11-21 00:00:00',
+ next_scheduled: '2024-11-21 12:00:00',
+ import_in_progress_or_due: true,
+ },
+ } )
+ );
+
+ render( <ImportStatusBar /> );
+
+ const button = screen.getByRole( 'button', {
+ name: /Manually trigger analytics data import/i,
+ } );
+ expect( button ).toBeDisabled();
+ } );
+
+ it( 'should disable button when isTriggeringImport is true', () => {
+ mockUseImportStatus.mockReturnValue(
+ createMockReturn( {
+ isTriggeringImport: true,
+ } )
+ );
+
+ render( <ImportStatusBar /> );
+
+ const button = screen.getByRole( 'button', {
+ name: /Manually trigger analytics data import/i,
+ } );
+ expect( button ).toBeDisabled();
+ } );
+
+ it( 'should trigger import on button click', async () => {
+ mockTriggerImport.mockResolvedValue( undefined );
+ mockUseImportStatus.mockReturnValue( createMockReturn() );
+
+ render( <ImportStatusBar /> );
+
+ const button = screen.getByRole( 'button', {
+ name: /Manually trigger analytics data import/i,
+ } );
+ fireEvent.click( button );
+
+ await waitFor( () => {
+ expect( mockTriggerImport ).toHaveBeenCalled();
+ } );
+
+ expect( mockCreateNotice ).toHaveBeenCalledWith(
+ 'success',
+ expect.stringContaining( 'Analytics import has started' ),
+ expect.objectContaining( {
+ type: 'snackbar',
+ isDismissible: true,
+ } )
+ );
+ } );
+
+ it( 'should show error notice when import fails', async () => {
+ const errorMessage = 'API Error';
+ mockTriggerImport.mockRejectedValue( new Error( errorMessage ) );
+ mockUseImportStatus.mockReturnValue(
+ createMockReturn( {
+ error: errorMessage,
+ } )
+ );
+
+ render( <ImportStatusBar /> );
+
+ const button = screen.getByRole( 'button', {
+ name: /Manually trigger analytics data import/i,
+ } );
+ fireEvent.click( button );
+
+ await waitFor( () => {
+ expect( mockTriggerImport ).toHaveBeenCalled();
+ } );
+
+ expect( mockCreateNotice ).toHaveBeenCalledWith(
+ 'error',
+ expect.stringContaining( errorMessage ),
+ expect.objectContaining( {
+ isDismissible: true,
+ } )
+ );
+ } );
+
+ it( 'should format dates correctly', () => {
+ mockUseImportStatus.mockReturnValue( createMockReturn() );
+
+ render( <ImportStatusBar /> );
+
+ // dateI18n is mocked to return "Nov 21 00:00"
+ const dateTexts = screen.getAllByText( /Nov 21 00:00/i );
+ expect( dateTexts ).toHaveLength( 2 ); // Last updated and Next update
+ } );
+
+ it( 'should show "Never" when dates are null', () => {
+ mockUseImportStatus.mockReturnValue(
+ createMockReturn( {
+ status: {
+ mode: 'scheduled',
+ last_processed_date: null,
+ next_scheduled: null,
+ import_in_progress_or_due: false,
+ },
+ } )
+ );
+
+ render( <ImportStatusBar /> );
+
+ const neverTexts = screen.getAllByText( /Never/i );
+ expect( neverTexts ).toHaveLength( 2 ); // Last updated: Never, Next update: Never
+ } );
+
+ it( 'should have accessible ARIA attributes', () => {
+ mockUseImportStatus.mockReturnValue( createMockReturn() );
+
+ render( <ImportStatusBar /> );
+
+ const container = screen.getByRole( 'status' );
+ expect( container ).toHaveAttribute( 'aria-live', 'polite' );
+ expect( container ).toHaveAttribute( 'aria-atomic', 'true' );
+
+ const button = screen.getByRole( 'button', {
+ name: /Manually trigger analytics data import/i,
+ } );
+ expect( button ).toHaveAttribute(
+ 'aria-label',
+ 'Manually trigger analytics data import'
+ );
+ } );
+
+ it( 'should be keyboard accessible', () => {
+ mockTriggerImport.mockResolvedValue( undefined );
+ mockUseImportStatus.mockReturnValue( createMockReturn() );
+
+ render( <ImportStatusBar /> );
+
+ const button = screen.getByRole( 'button', {
+ name: /Manually trigger analytics data import/i,
+ } );
+
+ // Button should be focusable
+ button.focus();
+ expect( button ).toHaveFocus();
+
+ // Enter key should trigger
+ fireEvent.keyPress( button, {
+ key: 'Enter',
+ code: 'Enter',
+ charCode: 13,
+ } );
+
+ // Note: In real browser, Button component handles Enter/Space
+ // In tests, we verify the button exists and is not disabled
+ expect( button ).not.toBeDisabled();
+ } );
+
+ it( 'should show fallback error message when error is null', async () => {
+ mockTriggerImport.mockRejectedValue( new Error( 'API Error' ) );
+ mockUseImportStatus.mockReturnValue(
+ createMockReturn( {
+ error: null, // No error in state
+ } )
+ );
+
+ render( <ImportStatusBar /> );
+
+ const button = screen.getByRole( 'button', {
+ name: /Manually trigger analytics data import/i,
+ } );
+ fireEvent.click( button );
+
+ await waitFor( () => {
+ expect( mockTriggerImport ).toHaveBeenCalled();
+ } );
+
+ expect( mockCreateNotice ).toHaveBeenCalledWith(
+ 'error',
+ expect.stringContaining( 'API Error' ),
+ expect.objectContaining( {
+ isDismissible: true,
+ } )
+ );
+ } );
+
+ it( 'should show "Never" when status is null', () => {
+ mockUseImportStatus.mockReturnValue(
+ createMockReturn( {
+ status: null,
+ isLoading: false,
+ } )
+ );
+
+ render( <ImportStatusBar /> );
+
+ // Component should render and show "Never" for dates when status is null
+ expect( screen.getByText( /Last updated$/i ) ).toBeInTheDocument();
+ expect( screen.getByText( /Next update$/i ) ).toBeInTheDocument();
+ const neverTexts = screen.getAllByText( /Never/i );
+ expect( neverTexts ).toHaveLength( 2 ); // Last updated: Never, Next update: Never
+ } );
+} );
diff --git a/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/test/use-import-status.test.ts b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/test/use-import-status.test.ts
new file mode 100644
index 0000000000..c9353b43d5
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/test/use-import-status.test.ts
@@ -0,0 +1,295 @@
+/**
+ * External dependencies
+ */
+import { renderHook, act } from '@testing-library/react-hooks';
+import apiFetch from '@wordpress/api-fetch';
+
+/**
+ * Internal dependencies
+ */
+import { useImportStatus } from '../use-import-status';
+import type { ImportStatus } from '../types';
+
+jest.mock( '@wordpress/api-fetch' );
+
+const mockApiFetch = apiFetch as jest.MockedFunction< typeof apiFetch >;
+
+describe( 'useImportStatus', () => {
+ beforeEach( () => {
+ jest.clearAllMocks();
+ jest.useFakeTimers();
+ } );
+
+ afterEach( () => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ } );
+
+ const createMockStatus = (
+ overrides: Partial< ImportStatus > = {}
+ ): ImportStatus => ( {
+ mode: 'scheduled',
+ last_processed_date: '2024-11-21 00:00:00',
+ next_scheduled: '2024-11-21 12:00:00',
+ import_in_progress_or_due: false,
+ ...overrides,
+ } );
+
+ it( 'should fetch initial status on mount', async () => {
+ const mockStatus = createMockStatus();
+ mockApiFetch.mockResolvedValue( mockStatus );
+
+ const { result, waitForNextUpdate } = renderHook( () =>
+ useImportStatus()
+ );
+
+ expect( result.current.isLoading ).toBe( true );
+ expect( result.current.status ).toBeNull();
+
+ await waitForNextUpdate();
+
+ expect( result.current.isLoading ).toBe( false );
+ expect( result.current.status ).toEqual( mockStatus );
+ expect( result.current.error ).toBeNull();
+ expect( mockApiFetch ).toHaveBeenCalledWith( {
+ path: '/wc-analytics/imports/status',
+ method: 'GET',
+ } );
+ } );
+
+ it( 'should start polling when import_in_progress_or_due is true', async () => {
+ const mockStatus = createMockStatus( {
+ import_in_progress_or_due: true,
+ } );
+ mockApiFetch.mockResolvedValue( mockStatus );
+
+ const { waitForNextUpdate } = renderHook( () => useImportStatus() );
+ await waitForNextUpdate();
+
+ // Initial fetch
+ expect( mockApiFetch ).toHaveBeenCalledTimes( 1 );
+
+ // Advance timer by 5 seconds
+ act( () => {
+ jest.advanceTimersByTime( 5000 );
+ } );
+
+ // Wait for the async fetch to complete
+ await act( async () => {
+ await Promise.resolve();
+ } );
+
+ expect( mockApiFetch ).toHaveBeenCalledTimes( 2 );
+
+ // Advance again
+ act( () => {
+ jest.advanceTimersByTime( 5000 );
+ } );
+
+ await act( async () => {
+ await Promise.resolve();
+ } );
+
+ expect( mockApiFetch ).toHaveBeenCalledTimes( 3 );
+ } );
+
+ it( 'should stop polling when import_in_progress_or_due becomes false', async () => {
+ const initialStatus = createMockStatus( {
+ import_in_progress_or_due: true,
+ } );
+ const updatedStatus = createMockStatus( {
+ import_in_progress_or_due: false,
+ } );
+
+ mockApiFetch
+ .mockResolvedValueOnce( initialStatus )
+ .mockResolvedValueOnce( updatedStatus );
+
+ const { waitForNextUpdate } = renderHook( () => useImportStatus() );
+ await waitForNextUpdate();
+
+ // Polling starts
+ act( () => {
+ jest.advanceTimersByTime( 5000 );
+ } );
+ await waitForNextUpdate();
+
+ // Now polling should have stopped
+ const callCountAfterStop = mockApiFetch.mock.calls.length;
+
+ act( () => {
+ jest.advanceTimersByTime( 10000 );
+ } );
+
+ // No additional calls should be made
+ expect( mockApiFetch ).toHaveBeenCalledTimes( callCountAfterStop );
+ } );
+
+ it( 'should trigger import and refetch status', async () => {
+ const mockStatus = createMockStatus();
+ mockApiFetch.mockResolvedValue( mockStatus );
+
+ const { result, waitForNextUpdate } = renderHook( () =>
+ useImportStatus()
+ );
+ await waitForNextUpdate();
+
+ mockApiFetch.mockClear();
+
+ const updatedStatus = createMockStatus( {
+ import_in_progress_or_due: true,
+ } );
+ mockApiFetch
+ .mockResolvedValueOnce( {} ) // POST response
+ .mockResolvedValueOnce( updatedStatus ); // GET refetch
+
+ await act( async () => {
+ await result.current.triggerImport();
+ } );
+
+ expect( mockApiFetch ).toHaveBeenCalledWith( {
+ path: '/wc-analytics/imports/trigger',
+ method: 'POST',
+ } );
+ expect( mockApiFetch ).toHaveBeenCalledWith( {
+ path: '/wc-analytics/imports/status',
+ method: 'GET',
+ } );
+ expect( result.current.status ).toEqual( updatedStatus );
+ } );
+
+ it( 'should handle fetch errors', async () => {
+ const errorMessage = 'Network error';
+ mockApiFetch.mockRejectedValue( new Error( errorMessage ) );
+
+ const { result, waitForNextUpdate } = renderHook( () =>
+ useImportStatus()
+ );
+ await waitForNextUpdate();
+
+ expect( result.current.error ).toBe( errorMessage );
+ expect( result.current.status ).toBeNull();
+ expect( result.current.isLoading ).toBe( false );
+ } );
+
+ it( 'should handle trigger import errors', async () => {
+ const mockStatus = createMockStatus();
+ mockApiFetch.mockResolvedValue( mockStatus );
+
+ const { result, waitForNextUpdate } = renderHook( () =>
+ useImportStatus()
+ );
+ await waitForNextUpdate();
+
+ const errorMessage = 'API Error';
+ mockApiFetch.mockRejectedValue( new Error( errorMessage ) );
+
+ await expect(
+ act( async () => {
+ await result.current.triggerImport();
+ } )
+ ).rejects.toThrow( errorMessage );
+
+ expect( result.current.error ).toBe( errorMessage );
+ } );
+
+ it( 'should set isTriggeringImport during trigger', async () => {
+ const mockStatus = createMockStatus();
+ mockApiFetch.mockResolvedValue( mockStatus );
+
+ const { result, waitForNextUpdate } = renderHook( () =>
+ useImportStatus()
+ );
+ await waitForNextUpdate();
+
+ expect( result.current.isTriggeringImport ).toBe( false );
+
+ let triggerPromise: Promise< void >;
+ act( () => {
+ triggerPromise = result.current.triggerImport();
+ } );
+
+ expect( result.current.isTriggeringImport ).toBe( true );
+
+ await act( async () => {
+ await triggerPromise;
+ } );
+
+ expect( result.current.isTriggeringImport ).toBe( false );
+ } );
+
+ it( 'should cleanup interval on unmount', async () => {
+ const mockStatus = createMockStatus( {
+ import_in_progress_or_due: true,
+ } );
+ mockApiFetch.mockResolvedValue( mockStatus );
+
+ const { unmount, waitForNextUpdate } = renderHook( () =>
+ useImportStatus()
+ );
+ await waitForNextUpdate();
+
+ const callCountBeforeUnmount = mockApiFetch.mock.calls.length;
+ unmount();
+
+ // Advance timers to verify no more calls
+ act( () => {
+ jest.advanceTimersByTime( 10000 );
+ } );
+
+ // No additional calls after unmount
+ expect( mockApiFetch ).toHaveBeenCalledTimes( callCountBeforeUnmount );
+ } );
+
+ it( 'should not start polling when import_in_progress_or_due is false', async () => {
+ const mockStatus = createMockStatus( {
+ import_in_progress_or_due: false,
+ } );
+ mockApiFetch.mockResolvedValue( mockStatus );
+
+ const { waitForNextUpdate } = renderHook( () => useImportStatus() );
+ await waitForNextUpdate();
+
+ // Only initial fetch
+ expect( mockApiFetch ).toHaveBeenCalledTimes( 1 );
+
+ // Advance timers
+ act( () => {
+ jest.advanceTimersByTime( 15000 );
+ } );
+
+ // No additional calls
+ expect( mockApiFetch ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ it( 'should handle non-Error fetch failures gracefully', async () => {
+ mockApiFetch.mockRejectedValue( 'String error' );
+
+ const { result, waitForNextUpdate } = renderHook( () =>
+ useImportStatus()
+ );
+ await waitForNextUpdate();
+
+ expect( result.current.error ).toBe( 'Failed to fetch status' );
+ } );
+
+ it( 'should handle non-Error trigger failures gracefully', async () => {
+ const mockStatus = createMockStatus();
+ mockApiFetch.mockResolvedValue( mockStatus );
+
+ const { result, waitForNextUpdate } = renderHook( () =>
+ useImportStatus()
+ );
+ await waitForNextUpdate();
+
+ mockApiFetch.mockRejectedValue( 'String error' );
+
+ await expect(
+ act( async () => {
+ await result.current.triggerImport();
+ } )
+ ).rejects.toBe( 'String error' );
+
+ expect( result.current.error ).toBe( 'Failed to trigger import' );
+ } );
+} );
diff --git a/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/types.ts b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/types.ts
new file mode 100644
index 0000000000..3ff5c77003
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/types.ts
@@ -0,0 +1,60 @@
+/**
+ * Import status response from the API
+ */
+export interface ImportStatus {
+ /**
+ * Import mode: 'scheduled' or 'immediate'
+ */
+ mode: 'scheduled' | 'immediate';
+
+ /**
+ * Last processed order date (site timezone, 'Y-m-d H:i:s' format)
+ * null if never processed or in immediate mode
+ */
+ last_processed_date: string | null;
+
+ /**
+ * Next scheduled import time (site timezone, 'Y-m-d H:i:s' format)
+ * null in immediate mode or if not scheduled
+ */
+ next_scheduled: string | null;
+
+ /**
+ * Whether a manual import has been in progress or is due to run soon
+ * null in immediate mode
+ */
+ import_in_progress_or_due: boolean | null;
+}
+
+/**
+ * Return value from useImportStatus hook
+ */
+export interface UseImportStatusReturn {
+ /**
+ * Current import status
+ * null while loading or if fetch failed
+ */
+ status: ImportStatus | null;
+
+ /**
+ * Whether the initial status fetch is in progress
+ */
+ isLoading: boolean;
+
+ /**
+ * Error message if status fetch failed
+ * null if no error
+ */
+ error: string | null;
+
+ /**
+ * Function to trigger a manual import
+ * Calls POST /wc-analytics/imports/trigger
+ */
+ triggerImport: () => Promise< void >;
+
+ /**
+ * Whether a manual import trigger is in progress
+ */
+ isTriggeringImport: boolean;
+}
diff --git a/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/use-import-status.ts b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/use-import-status.ts
new file mode 100644
index 0000000000..3cc131ee3b
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/analytics/components/import-status-bar/use-import-status.ts
@@ -0,0 +1,123 @@
+/**
+ * External dependencies
+ */
+import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
+import apiFetch from '@wordpress/api-fetch';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import type { ImportStatus, UseImportStatusReturn } from './types';
+
+/**
+ * Polling interval in milliseconds (5 seconds)
+ */
+const POLL_INTERVAL = 5000;
+
+/**
+ * Custom hook to fetch and manage analytics import status
+ *
+ * Features:
+ * - Fetches import status from /wc-analytics/imports/status
+ * - Auto-polls every 5s when import_in_progress_or_due is true
+ * - Stops polling when flag becomes false
+ * - Provides triggerImport function to manually trigger imports
+ * - Clean cleanup on unmount
+ *
+ * @return {UseImportStatusReturn} Import status state and actions
+ */
+export function useImportStatus(): UseImportStatusReturn {
+ const [ status, setStatus ] = useState< ImportStatus | null >( null );
+ const [ isLoading, setIsLoading ] = useState( true );
+ const [ error, setError ] = useState< string | null >( null );
+ const [ isTriggeringImport, setIsTriggeringImport ] = useState( false );
+ const intervalRef = useRef< number | null >( null );
+
+ /**
+ * Fetch import status from API
+ */
+ const fetchStatus = useCallback( async () => {
+ try {
+ const data = await apiFetch< ImportStatus >( {
+ path: '/wc-analytics/imports/status',
+ method: 'GET',
+ } );
+ setStatus( data );
+ setError( null );
+ } catch ( err ) {
+ setError(
+ err instanceof Error
+ ? err.message
+ : __( 'Failed to fetch status', 'woocommerce' )
+ );
+ } finally {
+ setIsLoading( false );
+ }
+ }, [] );
+
+ /**
+ * Trigger a manual import
+ */
+ const triggerImport = useCallback( async () => {
+ setIsTriggeringImport( true );
+ try {
+ await apiFetch( {
+ path: '/wc-analytics/imports/trigger',
+ method: 'POST',
+ } );
+ // Immediately refetch to get updated status
+ await fetchStatus();
+ } catch ( err ) {
+ setError(
+ err instanceof Error
+ ? err.message
+ : __( 'Failed to trigger import', 'woocommerce' )
+ );
+ throw err; // Re-throw so component can handle it
+ } finally {
+ setIsTriggeringImport( false );
+ }
+ }, [ fetchStatus ] );
+
+ /**
+ * Initial fetch on mount
+ */
+ useEffect( () => {
+ fetchStatus();
+ }, [ fetchStatus ] );
+
+ /**
+ * Polling lifecycle management
+ * Start polling when import_in_progress_or_due is true
+ * Stop polling when it becomes false
+ */
+ useEffect( () => {
+ if ( status?.import_in_progress_or_due ) {
+ // Start polling
+ intervalRef.current = window.setInterval(
+ fetchStatus,
+ POLL_INTERVAL
+ );
+ } else if ( intervalRef.current ) {
+ clearInterval( intervalRef.current );
+ intervalRef.current = null;
+ }
+
+ // Cleanup on unmount or when dependencies change
+ return () => {
+ if ( intervalRef.current ) {
+ clearInterval( intervalRef.current );
+ intervalRef.current = null;
+ }
+ };
+ }, [ status?.import_in_progress_or_due, fetchStatus ] );
+
+ return {
+ status,
+ isLoading,
+ error,
+ triggerImport,
+ isTriggeringImport,
+ };
+}
diff --git a/plugins/woocommerce/client/admin/client/analytics/components/report-header/index.ts b/plugins/woocommerce/client/admin/client/analytics/components/report-header/index.ts
new file mode 100644
index 0000000000..89a18ed7e7
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/analytics/components/report-header/index.ts
@@ -0,0 +1 @@
+export { default as ReportHeader } from './report-header';
diff --git a/plugins/woocommerce/client/admin/client/analytics/components/report-header/report-header.scss b/plugins/woocommerce/client/admin/client/analytics/components/report-header/report-header.scss
new file mode 100644
index 0000000000..612b247a34
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/analytics/components/report-header/report-header.scss
@@ -0,0 +1,42 @@
+/**
+ * Analytics Report Header Styles
+ *
+ * Container for ReportFilters and ImportStatusBar
+ */
+
+.woocommerce-analytics-report-header {
+ position: relative;
+ display: flex;
+ align-items: flex-start;
+
+ .woocommerce-filters {
+ flex: 1; // Allow the filters to grow and take up remaining space
+ width: 100%;
+ }
+
+ .woocommerce-analytics-import-status-bar-wrapper {
+ // Absolute positioning ensures the filters’ width is independent from the status bar, which can change its size dynamically as needed
+ position: absolute;
+ right: 0;
+ }
+
+ @media screen and ( max-width: $break-large ) {
+ flex-direction: column;
+
+ .woocommerce-analytics-import-status-bar-wrapper {
+ position: relative;
+ right: auto;
+ }
+ }
+
+ @media screen and ( max-width: $break-medium ) {
+ .woocommerce-analytics-import-status-bar-wrapper,
+ .woocommerce-analytics-import-status-bar__content {
+ width: 100%;
+ }
+
+ .woocommerce-analytics-import-status-bar__content {
+ justify-content: space-between;
+ }
+ }
+}
diff --git a/plugins/woocommerce/client/admin/client/analytics/components/report-header/report-header.tsx b/plugins/woocommerce/client/admin/client/analytics/components/report-header/report-header.tsx
new file mode 100644
index 0000000000..1067dee06a
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/analytics/components/report-header/report-header.tsx
@@ -0,0 +1,46 @@
+/**
+ * Internal dependencies
+ */
+import { ImportStatusBar } from '../import-status-bar';
+import ReportFilters from '../report-filters';
+
+import './report-header.scss';
+
+interface ReportHeaderProps {
+ /**
+ * Config option passed through to `AdvancedFilters`
+ */
+ advancedFilters?: object;
+ /**
+ * Config option passed through to `FilterPicker`
+ */
+ filters?: Array< unknown >;
+ /**
+ * The `path` parameter supplied by React-Router
+ */
+ path: string;
+ /**
+ * The query string represented in object form
+ */
+ query: object;
+ /**
+ * Whether the date picker must be shown
+ */
+ showDatePicker?: boolean;
+ /**
+ * The report where filters are placed
+ */
+ report: string;
+}
+
+export default function ReportHeader( props: ReportHeaderProps ): JSX.Element {
+ return (
+ <div className="woocommerce-analytics-report-header">
+ { /* @ts-expect-error - ReportFilters is a valid component but not typed */ }
+ <ReportFilters { ...props } />
+ { !! window.wcAdminFeatures?.[ 'analytics-scheduled-import' ] && (
+ <ImportStatusBar />
+ ) }
+ </div>
+ );
+}
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/categories/index.js b/plugins/woocommerce/client/admin/client/analytics/report/categories/index.js
index 02110c765d..66fdfef2aa 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/categories/index.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/categories/index.js
@@ -14,7 +14,7 @@ import getSelectedChart from '../../../lib/get-selected-chart';
import ReportChart from '../../components/report-chart';
import ReportSummary from '../../components/report-summary';
import ProductsReportTable from '../products/table';
-import ReportFilters from '../../components/report-filters';
+import { ReportHeader } from '../../components/report-header';
class CategoriesReport extends Component {
getChartMeta() {
@@ -59,7 +59,7 @@ class CategoriesReport extends Component {
return (
<Fragment>
- <ReportFilters
+ <ReportHeader
query={ query }
path={ path }
filters={ filters }
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/coupons/index.js b/plugins/woocommerce/client/admin/client/analytics/report/coupons/index.js
index e2bf92ec3d..cd5f036593 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/coupons/index.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/coupons/index.js
@@ -13,7 +13,7 @@ import CouponsReportTable from './table';
import getSelectedChart from '../../../lib/get-selected-chart';
import ReportChart from '../../components/report-chart';
import ReportSummary from '../../components/report-summary';
-import ReportFilters from '../../components/report-filters';
+import { ReportHeader } from '../../components/report-header';
class CouponsReport extends Component {
getChartMeta() {
@@ -47,7 +47,7 @@ class CouponsReport extends Component {
return (
<Fragment>
- <ReportFilters
+ <ReportHeader
query={ query }
path={ path }
filters={ filters }
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/customers/index.js b/plugins/woocommerce/client/admin/client/analytics/report/customers/index.js
index 15312213b8..cb6425776b 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/customers/index.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/customers/index.js
@@ -9,7 +9,7 @@ import PropTypes from 'prop-types';
*/
import { filters, advancedFilters } from './config';
import CustomersReportTable from './table';
-import ReportFilters from '../../components/report-filters';
+import { ReportHeader } from '../../components/report-header';
export default class CustomersReport extends Component {
render() {
@@ -22,7 +22,7 @@ export default class CustomersReport extends Component {
return (
<Fragment>
- <ReportFilters
+ <ReportHeader
query={ query }
path={ path }
filters={ filters }
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/downloads/index.js b/plugins/woocommerce/client/admin/client/analytics/report/downloads/index.js
index e13be72f42..20f412ceb1 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/downloads/index.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/downloads/index.js
@@ -12,7 +12,7 @@ import DownloadsReportTable from './table';
import getSelectedChart from '../../../lib/get-selected-chart';
import ReportChart from '../../components/report-chart';
import ReportSummary from '../../components/report-summary';
-import ReportFilters from '../../components/report-filters';
+import { ReportHeader } from '../../components/report-header';
export default class DownloadsReport extends Component {
render() {
@@ -20,7 +20,7 @@ export default class DownloadsReport extends Component {
return (
<Fragment>
- <ReportFilters
+ <ReportHeader
query={ query }
path={ path }
filters={ filters }
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/orders/index.js b/plugins/woocommerce/client/admin/client/analytics/report/orders/index.js
index d447aa0e4e..0bbae7f20d 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/orders/index.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/orders/index.js
@@ -13,7 +13,7 @@ import getSelectedChart from '../../../lib/get-selected-chart';
import OrdersReportTable from './table';
import ReportChart from '../../components/report-chart';
import ReportSummary from '../../components/report-summary';
-import ReportFilters from '../../components/report-filters';
+import { ReportHeader } from '../../components/report-header';
import { ReportDateTour } from '~/guided-tours/report-date-tour';
export default class OrdersReport extends Component {
@@ -22,7 +22,7 @@ export default class OrdersReport extends Component {
return (
<Fragment>
- <ReportFilters
+ <ReportHeader
query={ query }
path={ path }
filters={ filters }
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/products/index.js b/plugins/woocommerce/client/admin/client/analytics/report/products/index.js
index db3922f38c..8f63dc056d 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/products/index.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/products/index.js
@@ -18,7 +18,7 @@ import ProductsReportTable from './table';
import ReportChart from '../../components/report-chart';
import ReportSummary from '../../components/report-summary';
import VariationsReportTable from '../variations/table';
-import ReportFilters from '../../components/report-filters';
+import { ReportHeader } from '../../components/report-header';
class ProductsReport extends Component {
getChartMeta() {
@@ -71,7 +71,7 @@ class ProductsReport extends Component {
return (
<Fragment>
- <ReportFilters
+ <ReportHeader
query={ query }
path={ path }
filters={ filters }
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/revenue/index.js b/plugins/woocommerce/client/admin/client/analytics/report/revenue/index.js
index 18f604222a..c8638f9680 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/revenue/index.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/revenue/index.js
@@ -13,7 +13,7 @@ import getSelectedChart from '../../../lib/get-selected-chart';
import ReportChart from '../../components/report-chart';
import ReportSummary from '../../components/report-summary';
import RevenueReportTable from './table';
-import ReportFilters from '../../components/report-filters';
+import { ReportHeader } from '../../components/report-header';
import { ReportDateTour } from '~/guided-tours/report-date-tour';
export default class RevenueReport extends Component {
@@ -22,7 +22,7 @@ export default class RevenueReport extends Component {
return (
<Fragment>
- <ReportFilters
+ <ReportHeader
query={ query }
path={ path }
report="revenue"
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/stock/index.js b/plugins/woocommerce/client/admin/client/analytics/report/stock/index.js
index 37d2d1958d..82e609e179 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/stock/index.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/stock/index.js
@@ -9,14 +9,14 @@ import PropTypes from 'prop-types';
*/
import { advancedFilters, showDatePicker, filters } from './config';
import StockReportTable from './table';
-import ReportFilters from '../../components/report-filters';
+import { ReportHeader } from '../../components/report-header';
export default class StockReport extends Component {
render() {
const { query, path } = this.props;
return (
<Fragment>
- <ReportFilters
+ <ReportHeader
query={ query }
path={ path }
showDatePicker={ showDatePicker }
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/taxes/index.js b/plugins/woocommerce/client/admin/client/analytics/report/taxes/index.js
index 493ca5e30b..cd2b7ac073 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/taxes/index.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/taxes/index.js
@@ -13,7 +13,7 @@ import getSelectedChart from '../../../lib/get-selected-chart';
import ReportChart from '../../components/report-chart';
import ReportSummary from '../../components/report-summary';
import TaxesReportTable from './table';
-import ReportFilters from '../../components/report-filters';
+import { ReportHeader } from '../../components/report-header';
class TaxesReport extends Component {
getChartMeta() {
@@ -42,7 +42,7 @@ class TaxesReport extends Component {
}
return (
<Fragment>
- <ReportFilters
+ <ReportHeader
query={ query }
path={ path }
filters={ filters }
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/variations/index.js b/plugins/woocommerce/client/admin/client/analytics/report/variations/index.js
index 245094a973..585b2b185a 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/variations/index.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/variations/index.js
@@ -14,7 +14,7 @@ import getSelectedChart from '../../../lib/get-selected-chart';
import ReportChart from '../../components/report-chart';
import ReportSummary from '../../components/report-summary';
import VariationsReportTable from './table';
-import ReportFilters from '../../components/report-filters';
+import { ReportHeader } from '../../components/report-header';
const getChartMeta = ( { query } ) => {
const isCompareView =
@@ -48,7 +48,7 @@ const VariationsReport = ( props ) => {
return (
<Fragment>
- <ReportFilters
+ <ReportHeader
query={ query }
path={ path }
filters={ filters }
diff --git a/plugins/woocommerce/client/admin/client/dashboard/customizable.js b/plugins/woocommerce/client/admin/client/dashboard/customizable.js
index 8c28d7c978..25e64ef743 100644
--- a/plugins/woocommerce/client/admin/client/dashboard/customizable.js
+++ b/plugins/woocommerce/client/admin/client/dashboard/customizable.js
@@ -12,11 +12,6 @@ import { withSelect } from '@wordpress/data';
import { H } from '@woocommerce/components';
import { settingsStore, useUserPreferences } from '@woocommerce/data';
import { getQuery } from '@woocommerce/navigation';
-import {
- getCurrentDates,
- getDateParamsFromQuery,
- isoDateFormat,
-} from '@woocommerce/date';
import { recordEvent } from '@woocommerce/tracks';
import {
CurrencyContext,
@@ -29,7 +24,7 @@ import {
import './style.scss';
import defaultSections, { DEFAULT_SECTIONS_FILTER } from './default-sections';
import Section from './section';
-import ReportFilters from '../analytics/components/report-filters';
+import { ReportHeader } from '../analytics/components/report-header';
const DASHBOARD_FILTERS_FILTER = 'woocommerce_admin_dashboard_filters';
@@ -247,32 +242,16 @@ const CustomizableDashboard = ( { defaultDateRange, path, query } ) => {
};
const renderDashboardReports = () => {
- const { period, compare, before, after } = getDateParamsFromQuery(
- query,
- defaultDateRange
- );
- const { primary: primaryDate, secondary: secondaryDate } =
- getCurrentDates( query, defaultDateRange );
- const dateQuery = {
- period,
- compare,
- before,
- after,
- primaryDate,
- secondaryDate,
- };
const visibleSectionKeys = sections
.filter( ( section ) => section.isVisible )
.map( ( section ) => section.key );
return (
<>
- <ReportFilters
+ <ReportHeader
report="dashboard"
query={ query }
path={ path }
- dateQuery={ dateQuery }
- isoDateFormat={ isoDateFormat }
filters={ filters }
/>
{ sections.map( ( section, index ) => {
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings.php b/plugins/woocommerce/src/Internal/Admin/Settings.php
index ab9333910b..1e80057deb 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings.php
@@ -369,9 +369,10 @@ class Settings {
// Format the import interval to a human-readable string.
$import_interval_string = human_time_diff( 0, $import_interval );
$settings[] = array(
- 'id' => 'woocommerce_analytics_import_interval',
- 'type' => 'hidden',
- 'default' => $import_interval_string,
+ 'id' => 'woocommerce_analytics_import_interval',
+ 'option_key' => 'woocommerce_analytics_import_interval',
+ 'type' => 'hidden',
+ 'default' => $import_interval_string,
);
}